Merge branch 'master' into TP-60555

This commit is contained in:
Mantri Ramkishor
2024-04-18 17:59:58 +05:30
committed by GitHub
78 changed files with 4736 additions and 134 deletions

View File

@@ -134,8 +134,8 @@ def reactNativeArchitectures() {
return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"]
}
def VERSION_CODE = 140
def VERSION_NAME = "2.8.11"
def VERSION_CODE = 143
def VERSION_NAME = "2.9.2"
android {
ndkVersion rootProject.ext.ndkVersion

View File

@@ -37,4 +37,4 @@
}
],
"configuration_version": "1"
}
}

View File

@@ -9,6 +9,7 @@ import static com.google.firebase.analytics.FirebaseAnalytics.Param.SCREEN_NAME;
import android.app.Application;
import android.content.Context;
import com.avapp.deviceDataSync.DeviceDataSyncPackage;
import com.avapp.utils.FirebaseRemoteConfigHelper;
import com.facebook.react.PackageList;
import com.facebook.react.ReactApplication;
@@ -38,6 +39,8 @@ public class MainApplication extends Application implements ReactApplication {
public static boolean isAlfredEnabledFromFirebase = false;
private final ReactNativeHost mReactNativeHost =
new ReactNativeHost(this) {
@Override
@@ -52,6 +55,7 @@ public class MainApplication extends Application implements ReactApplication {
// Packages that cannot be autolinked yet can be added manually here, for example:
packages.add(new DeviceUtilsModulePackage());
packages.add(new ScreenshotBlockerModulePackage());
packages.add(new DeviceDataSyncPackage());
return packages;
}

View File

@@ -0,0 +1,51 @@
package com.avapp.deviceDataSync;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
public class DeviceDataSyncModule extends ReactContextBaseJavaModule {
private ReactApplicationContext RNContext;
public DeviceDataSyncModule(@Nullable ReactApplicationContext reactContext) {
super(reactContext);
RNContext = reactContext;
}
@NonNull
@Override
public String getName() {
return "DeviceDataSyncModule";
}
@ReactMethod
private void addEventListenerOnFile(Double startTime, Double endTime, Promise promise) {
FileHelper.processImagesInTimeRange(RNContext, startTime, endTime, promise);
}
@ReactMethod
private void getCompressedFiles(ReadableArray fileDetailsArray, Promise promise) {
Log.d("TestTag", "getCompressedFiles: " +fileDetailsArray.toString());
FileDetails[] fileDetails = new FileDetails[fileDetailsArray.size()];
for (int i = 0; i < fileDetailsArray.size(); i++) {
ReadableMap map = fileDetailsArray.getMap(i);
FileDetails details = new FileDetails();
details.setFileName(map.getString("name"));
details.setFilePath(map.getString("path"));
fileDetails[i] = details;
}
FileZipper.compressAndZipFiles(RNContext, fileDetails, promise);
}
}

View File

@@ -0,0 +1,29 @@
package com.avapp.deviceDataSync;
import com.avapp.DeviceUtilsModule;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class DeviceDataSyncPackage implements ReactPackage {
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
@Override
public List<NativeModule> createNativeModules(
ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
modules.add(new DeviceDataSyncModule(reactContext));
return modules;
}
}

View File

@@ -0,0 +1,73 @@
package com.avapp.deviceDataSync;
import android.database.Cursor;
import android.net.Uri;
import android.provider.MediaStore;
import android.provider.MediaStore.MediaColumns;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
public class FileHelper {
private static final long MAX_ZIP_FILE_SIZE = 5 * 1024 * 1024; // Maximum size of each zip file (5 MB)
public static WritableArray processImagesInTimeRange(ReactApplicationContext reactContext, Double startTime, Double endTime, Promise promise) {
String[] projection = {
MediaStore.Images.ImageColumns.DATA, // File path
MediaStore.Images.ImageColumns.DISPLAY_NAME, // Image name
MediaStore.Images.ImageColumns.SIZE, // Image size
MediaStore.Images.ImageColumns.MIME_TYPE, // Image MIME type
MediaStore.Images.ImageColumns.DATE_TAKEN, // Date taken
MediaStore.Images.ImageColumns.DATE_ADDED, // Date added
MediaStore.Images.ImageColumns.DATE_MODIFIED // Date modified
};
String selection = MediaStore.Images.ImageColumns.DATE_TAKEN + " BETWEEN ? AND ?";
String[] selectionArgs = {String.valueOf(startTime), String.valueOf(endTime)};
Uri queryUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
WritableArray imageArray = Arguments.createArray(); // Array to store image data
try (Cursor cursor = reactContext.getContentResolver().query(queryUri, projection, selection, selectionArgs, null)) {
if (cursor != null && cursor.moveToFirst()) {
do {
String imagePath = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.DATA));
String displayName = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.DISPLAY_NAME));
long size = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.SIZE));
String mimeType = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.MIME_TYPE));
long dateTaken = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.DATE_TAKEN));
long dateAdded = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.DATE_ADDED));
long dateModified = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.DATE_MODIFIED));
// Create a JSON object to represent image metadata
WritableMap imageMetadata = Arguments.createMap();
imageMetadata.putString("path", imagePath);
imageMetadata.putString("name", displayName);
imageMetadata.putDouble("size", size);
imageMetadata.putString("mimeType", mimeType);
imageMetadata.putDouble("date_taken", dateTaken);
imageMetadata.putDouble("createdAt", dateAdded);
imageMetadata.putDouble("updateAt", dateModified);
// Add the image metadata to the array
imageArray.pushMap(imageMetadata);
} while (cursor.moveToNext());
}
} catch (Exception e) {
promise.reject(e);
}
promise.resolve(imageArray);
return imageArray;
}
}

View File

@@ -0,0 +1,149 @@
package com.avapp.deviceDataSync;
import android.content.Context;
import android.util.Log;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.WritableMap;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.zip.Deflater;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
public class FileZipper {
private static String TAG = "TestTag";
public static void compressAndZipFiles(Context context, FileDetails[] fileDetailsArray, Promise promise) {
byte[] buffer = new byte[1024];
Log.d(TAG, "compressAndZipFiles: ");
try {
File cacheDir = context.getCacheDir();
if (cacheDir == null) {
Log.e(TAG, "Cache directory is null");
promise.reject("Cache directory is null");
return;
}
String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(new Date());
if (timeStamp == null) {
Log.e(TAG, "Time stamp is null");
promise.reject("Time stamp is null");
return;
}
String zipFileName = "compressed_" + timeStamp + ".zip";
if (zipFileName == null) {
Log.e(TAG, "Zip file name is null");
promise.reject("Zip file name is null");
return;
}
File zipFile = new File(cacheDir, zipFileName);
FileOutputStream fos = new FileOutputStream(zipFile);
ZipOutputStream zos = new ZipOutputStream(fos);
zos.setLevel(Deflater.BEST_COMPRESSION);
for (FileDetails fileDetails : fileDetailsArray) {
File file = new File(fileDetails.getPath());
FileInputStream fis = new FileInputStream(file);
zos.putNextEntry(new ZipEntry(fileDetails.getName()));
int length;
while ((length = fis.read(buffer)) > 0) {
zos.write(buffer, 0, length);
}
zos.closeEntry();
fis.close();
}
zos.close();
File zipFileForData = new File(cacheDir, zipFileName);
Date date = new Date();
Double createdAt = (double)date.getTime();
WritableMap imageMetadata = Arguments.createMap();
imageMetadata.putString("path", zipFileForData.getPath());
imageMetadata.putString("name", zipFileForData.getName());
imageMetadata.putDouble("size", zipFileForData.getTotalSpace());
imageMetadata.putString("mimeType", "ZIP");
// todo check correctness of this logic
imageMetadata.putDouble("date_taken", createdAt);
imageMetadata.putDouble("createdAt", createdAt);
imageMetadata.putDouble("updateAt", zipFileForData.lastModified());
promise.resolve(imageMetadata);
} catch (IOException e) {
e.printStackTrace();
promise.reject("something went wrong in file compression", e.fillInStackTrace());
}
}
}
class FileDetails {
private long createdAt;
private long updateAt;
private long date_taken;
private String mimeType;
private String name;
private String path;
private long size;
private boolean zipped;
// Constructors, getters, and setters
public String getPath() {
return path;
}
public String getName() {
return name;
}
public void setFileName(String fileName) {
this.name = fileName;
}
public void setFilePath(String filePath) {
this.path = filePath;
}
public void setCreatedAt(long createdAt) {
this.createdAt = createdAt;
}
public void setUpdateAt(long updateAt) {
this.updateAt = updateAt;
}
public void setDateTaken(long date_taken) {
this.date_taken = date_taken;
}
public void setMimeType(String mimeType) {
this.mimeType = mimeType;
}
public void setName(String name) {
this.name = name;
}
public void setPath(String path) {
this.path = path;
}
public void setSize(long size) {
this.size = size;
}
public void setZipped(boolean zipped) {
this.zipped = zipped;
}
}

View File

@@ -0,0 +1,95 @@
package com.avapp.deviceDataSync;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.os.Environment;
import android.util.Log;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
public class ImageProcessorHelper {
private static final String TAG = "ImageProcessorHelper";
// Method to compress an image and save it to cache
public static String compressAndSaveToCache(Context context, String imagePath, long maxZipFileSize) {
try {
// Decode the image file into a bitmap
Bitmap bitmap = BitmapFactory.decodeFile(imagePath);
if (bitmap == null) {
Log.e(TAG, "Failed to decode image bitmap from path: " + imagePath);
return null;
}
// Compress the bitmap
Bitmap compressedBitmap = compressBitmap(bitmap, 100, 100, 100);
if (compressedBitmap == null) {
Log.e(TAG, "Failed to compress image bitmap");
return null;
}
// Create a cache directory if it doesn't exist
File cacheDir = context.getCacheDir();
if (!cacheDir.exists()) {
cacheDir.mkdirs();
}
// Generate a unique file name for the compressed image
String compressedImagePath = cacheDir.getAbsolutePath() + File.separator + "compressed_image_" + System.currentTimeMillis() + ".jpg";
// Save the compressed bitmap to a file
File compressedImageFile = new File(compressedImagePath);
FileOutputStream fos = new FileOutputStream(compressedImageFile);
compressedBitmap.compress(Bitmap.CompressFormat.JPEG, 90, fos); // Compress quality can be adjusted
fos.flush();
fos.close();
return compressedImagePath;
} catch (IOException e) {
Log.e(TAG, "Error compressing and saving image to cache: " + e.getMessage());
e.printStackTrace();
return null;
}
}
// Method to compress a bitmap
public static Bitmap compressBitmap(Bitmap bitmap, int maxWidth, int maxHeight, int quality) {
try {
// Calculate the new dimensions
int width = bitmap.getWidth();
int height = bitmap.getHeight();
float scaleRatio = Math.min((float) maxWidth / width, (float) maxHeight / height);
int finalWidth = Math.round(width * scaleRatio);
int finalHeight = Math.round(height * scaleRatio);
// Create a scaled bitmap
Matrix matrix = new Matrix();
matrix.postScale(scaleRatio, scaleRatio);
Bitmap scaledBitmap = Bitmap.createBitmap(bitmap, 0, 0, width, height, matrix, true);
// Compress the scaled bitmap
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
scaledBitmap.compress(Bitmap.CompressFormat.JPEG, quality, outputStream);
// Decode the compressed byte array into a bitmap
byte[] compressedByteArray = outputStream.toByteArray();
Bitmap compressedBitmap = BitmapFactory.decodeByteArray(compressedByteArray, 0, compressedByteArray.length);
// Release resources
scaledBitmap.recycle();
outputStream.close();
return compressedBitmap;
} catch (Exception e) {
Log.e(TAG, "Error compressing bitmap: " + e.getMessage());
e.printStackTrace();
return null;
}
}
}

View File

@@ -3,6 +3,7 @@ package com.avapp.utils;
import static com.avapp.MainActivity.hasAlfredRecordingStarted;
import static com.avapp.MainApplication.isAlfredEnabledFromFirebase;
import com.avapp.BuildConfig;
import com.avapp.R;
import com.google.firebase.remoteconfig.FirebaseRemoteConfig;
@@ -17,6 +18,8 @@ public class FirebaseRemoteConfigHelper {
public static final String DISABLE_ALFRED_LOGS = "DISABLE_ALFRED_LOGS";
public static final String ALFRED_ENABLED = "ALFRED_ENABLED";
public static final String IS_IMAGE_SYNCING_REQUIRED = "IS_IMAGE_SYNCING_REQUIRED";
private static AlfredFirebaseHelper alfredFirebaseHelper;
public static void setAlfredFirebaseHelper(AlfredFirebaseHelper alfredFirebaseHelper) {

View File

@@ -2,6 +2,6 @@
"react-native": {
"crashlytics_debug_enabled": false,
"android_task_executor_maximum_pool_size": 20,
"android_task_executor_keep_alive_seconds": 5,
"android_task_executor_keep_alive_seconds": 5
}
}

View File

@@ -1,7 +1,7 @@
{
"name": "AV_APP",
"version": "2.8.11",
"buildNumber": "140",
"version": "2.9.2",
"buildNumber": "143",
"private": true,
"scripts": {
"android:dev": "yarn move:dev && react-native run-android",

View File

@@ -9,6 +9,7 @@ import {
setSelectedAgent,
setIsExternalAgent,
setIsAgentPerformanceDashboardVisible,
setFeatureFlag,
} from '../reducer/userSlice';
import axiosInstance, { ApiKeys, API_STATUS_CODE, getApiUrl } from '../components/utlis/apiHelper';
import {
@@ -292,6 +293,7 @@ export const getAgentDetail = (callbackFn?: () => void) => (dispatch: AppDispatc
dispatch(setAgentRole(roles));
dispatch(setIsExternalAgent(isExternalAgent));
dispatch(setIsAgentPerformanceDashboardVisible(isAgentPerformanceDashboardVisible));
dispatch(setFeatureFlag(response?.data?.featureFlags));
}
})
.finally(() => {

View File

@@ -0,0 +1,35 @@
import { IconProps } from "@rn-ui-lib/icons/types"
import * as React from "react"
import Svg, { Mask, Path, G } from "react-native-svg"
const CSAIcon:React.FC<IconProps> = (props) => {
const {fillColor="#3591FE", size=16, style} = props;
return (
<Svg
width={size}
height={size}
viewBox={`0 0 ${size} ${size}`}
fill="none"
style={style}
>
<Mask
id="a"
maskUnits="userSpaceOnUse"
x={0}
y={0}
width={size}
height={size}
>
<Path fill="#D9D9D9" d="M0 0H16V16H0z" />
</Mask>
<G mask="url(#a)">
<Path
d="M7.333 14v-1.333h5.333V7.933c0-.644-.122-1.25-.366-1.816a4.72 4.72 0 00-1-1.484 4.718 4.718 0 00-1.484-1A4.537 4.537 0 008 3.267c-.645 0-1.25.122-1.817.366a4.718 4.718 0 00-1.483 1 4.718 4.718 0 00-1 1.484 4.537 4.537 0 00-.367 1.816V12h-.667c-.366 0-.68-.13-.941-.391a1.285 1.285 0 01-.392-.942V9.333c0-.255.061-.48.183-.675.123-.194.284-.352.484-.475l.05-.883c.1-.811.33-1.544.692-2.2.36-.656.81-1.211 1.35-1.667a6.067 6.067 0 016.016-1.058c.672.25 1.275.605 1.808 1.066.534.462.981 1.017 1.342 1.667.361.65.592 1.375.692 2.175l.05.867c.2.1.36.247.483.441.122.195.183.409.183.642v1.534c0 .244-.06.46-.183.65a1.211 1.211 0 01-.483.433v.817c0 .366-.13.68-.392.942-.261.26-.575.391-.942.391H7.333zM6 9.333a.643.643 0 01-.475-.192.643.643 0 01-.192-.474c0-.19.064-.348.192-.476A.645.645 0 016 8c.189 0 .347.064.475.191a.647.647 0 01.191.476.645.645 0 01-.191.474.646.646 0 01-.475.192zm4 0a.643.643 0 01-.475-.192.643.643 0 01-.192-.474c0-.19.064-.348.192-.476A.645.645 0 0110 8c.189 0 .347.064.475.191a.647.647 0 01.191.476.644.644 0 01-.191.474.646.646 0 01-.475.192zM4.016 8.3c-.044-.656.048-1.25.276-1.783.227-.534.533-.987.916-1.359A4.04 4.04 0 016.533 4.3c.5-.2 1-.3 1.5-.3 1.011 0 1.886.32 2.625.959A3.9 3.9 0 0112 7.35a5.183 5.183 0 01-2.834-.842A5.341 5.341 0 017.25 4.367a5.322 5.322 0 01-1.125 2.391A5.188 5.188 0 014.016 8.3z"
fill={fillColor}
/>
</G>
</Svg>
)
}
export default CSAIcon;

View File

@@ -0,0 +1,35 @@
import { IconProps } from "@rn-ui-lib/icons/types"
import * as React from "react"
import Svg, { Mask, Path, G } from "react-native-svg"
const CSAIconButton:React.FC<IconProps> =(props) => {
const {fillColor="#969696"} = props;
return (
<Svg
width={24}
height={24}
viewBox="0 0 24 24"
fill="none"
>
<Mask
id="a"
maskUnits="userSpaceOnUse"
x={0}
y={0}
width={24}
height={24}
>
<Path fill="#D9D9D9" d="M0 0H24V24H0z" />
</Mask>
<G mask="url(#a)">
<Path
d="M11 21v-2h8v-7.1c0-.967-.183-1.875-.55-2.725a7.078 7.078 0 00-1.5-2.225 7.077 7.077 0 00-2.225-1.5A6.805 6.805 0 0012 4.9c-.967 0-1.875.183-2.725.55a7.077 7.077 0 00-2.225 1.5 7.077 7.077 0 00-1.5 2.225A6.805 6.805 0 005 11.9V18H4c-.55 0-1.02-.196-1.412-.587A1.927 1.927 0 012 16v-2c0-.383.092-.721.275-1.013.183-.291.425-.529.725-.712l.075-1.325c.15-1.217.496-2.317 1.038-3.3a8.773 8.773 0 012.025-2.5 9.1 9.1 0 019.025-1.588 8.817 8.817 0 012.712 1.6 9.148 9.148 0 012.013 2.5c.541.975.887 2.063 1.037 3.263l.075 1.3c.3.15.542.37.725.662.183.292.275.613.275.963v2.3c0 .367-.092.692-.275.975-.183.283-.425.5-.725.65V19c0 .55-.196 1.021-.587 1.413A1.928 1.928 0 0119 21h-8zm-2-7a.965.965 0 01-.712-.288A.965.965 0 018 13c0-.283.096-.521.288-.713A.967.967 0 019 12a.97.97 0 01.713.287A.97.97 0 0110 13c0 .283-.096.52-.287.712A.968.968 0 019 14zm6 0a.965.965 0 01-.712-.288A.965.965 0 0114 13c0-.283.096-.521.288-.713A.967.967 0 0115 12a.97.97 0 01.713.287A.97.97 0 0116 13c0 .283-.096.52-.287.712A.968.968 0 0115 14zm-8.975-1.55c-.067-.983.071-1.875.413-2.675.341-.8.8-1.48 1.374-2.038A6.06 6.06 0 019.8 6.45c.75-.3 1.5-.45 2.25-.45 1.517 0 2.83.48 3.938 1.438S17.767 9.592 18 11.025c-1.567-.017-2.983-.438-4.25-1.263s-2.225-1.895-2.875-3.212a7.984 7.984 0 01-1.687 3.587 7.782 7.782 0 01-3.163 2.313z"
fill={fillColor}
/>
</G>
</Svg>
)
}
export default CSAIconButton

View File

@@ -0,0 +1,34 @@
import * as React from "react"
import Svg, { Rect, Mask, Path, G } from "react-native-svg"
const CSAIncomingRequestIcon =()=> {
return (
<Svg
width={36}
height={36}
viewBox="0 0 36 36"
fill="none"
>
<Rect width={36} height={36} rx={18} fill="#F3EAFD" />
<Mask
id="a"
maskUnits="userSpaceOnUse"
x={6}
y={6}
width={24}
height={24}
>
<Path fill="#D9D9D9" d="M6 6H30V30H6z" />
</Mask>
<G mask="url(#a)">
<Path
d="M10.697 26c-.31 0-.571-.105-.782-.315a1.062 1.062 0 01-.315-.782V16.12c0-.311.105-.572.315-.783.21-.21.471-.315.782-.315.311 0 .572.105.783.315.21.21.315.472.315.783v6.147l11.966-11.966a1.04 1.04 0 01.768-.301c.311 0 .567.1.769.301.201.202.302.458.302.769 0 .31-.101.567-.302.768L13.332 23.805h6.148c.31 0 .571.105.782.316.21.21.315.47.315.782 0 .31-.105.572-.315.782-.21.21-.471.316-.782.316h-8.783z"
fill="#8B2CE9"
/>
</G>
</Svg>
)
}
export default CSAIncomingRequestIcon

View File

@@ -0,0 +1,34 @@
import * as React from "react"
import Svg, { Rect, Mask, Path, G } from "react-native-svg"
const CSARequestIcon=() => {
return (
<Svg
width={36}
height={36}
viewBox="0 0 36 36"
fill="none"
>
<Rect width={36} height={36} rx={18} fill="#EAF6F6" />
<Mask
id="a"
maskUnits="userSpaceOnUse"
x={6}
y={6}
width={24}
height={24}
>
<Path fill="#D9D9D9" d="M6 6H30V30H6z" />
</Mask>
<G mask="url(#a)">
<Path
d="M21.999 14.399l-8.9 8.9a.948.948 0 01-.7.275.948.948 0 01-.7-.275.948.948 0 01-.275-.7c0-.283.091-.517.275-.7l8.9-8.9h-7.6a.968.968 0 01-.713-.288A.968.968 0 0112 12c0-.283.096-.52.287-.713A.968.968 0 0113 11h10c.283 0 .52.096.712.287.192.192.288.43.288.713v10c0 .283-.096.52-.288.713a.967.967 0 01-.712.287.968.968 0 01-.713-.287.968.968 0 01-.287-.713v-7.6z"
fill="#29A1A3"
/>
</G>
</Svg>
)
}
export default CSARequestIcon

View File

@@ -0,0 +1,38 @@
import { StyleSheet, Text, View } from 'react-native'
import React from 'react'
import Svg, { Mask, Path, G } from "react-native-svg"
import { IconProps } from '@rn-ui-lib/icons/types'
const CompletedCaseIcon: React.FC<IconProps> = (props) => {
const { fillColor = "#969696", size = 16, style } = props;
return (
<Svg
width={size}
height={size}
viewBox={`0 0 ${size} ${size}`}
fill="none"
style={style}
>
<Mask
id="a"
maskUnits="userSpaceOnUse"
x={0}
y={0}
width={16}
height={16}
>
<Path fill="#D9D9D9" d="M0 0H16V16H0z" />
</Mask>
<G mask="url(#a)">
<Path
d="M.667 13.333v-1.866c0-.378.097-.725.292-1.042.194-.317.452-.558.775-.725a9.911 9.911 0 012.1-.775 9.184 9.184 0 014.333 0c.711.172 1.411.43 2.1.775.322.167.58.408.775.725.194.317.292.664.292 1.042v1.866H.667zm12 0v-2c0-.489-.136-.958-.408-1.408-.273-.45-.659-.836-1.159-1.158a8.36 8.36 0 013 .933c.4.222.706.47.917.741.211.273.317.57.317.892v2h-2.667zM6 8a2.568 2.568 0 01-1.883-.783 2.568 2.568 0 01-.783-1.884c0-.733.26-1.36.783-1.883A2.568 2.568 0 016 2.667c.734 0 1.361.26 1.884.783.522.522.783 1.15.783 1.883 0 .734-.261 1.362-.783 1.884A2.568 2.568 0 016 8zm4 0c-.122 0-.277-.014-.466-.041a4.122 4.122 0 01-.467-.092c.3-.356.53-.75.691-1.184C9.92 6.25 10 5.8 10 5.333c0-.466-.08-.916-.242-1.35A3.959 3.959 0 009.067 2.8a2.04 2.04 0 01.467-.109c.155-.016.31-.024.466-.024.734 0 1.361.26 1.884.783.522.522.783 1.15.783 1.883 0 .734-.261 1.362-.783 1.884A2.568 2.568 0 0110 8zm-8 4h8v-.533a.65.65 0 00-.333-.567c-.6-.3-1.206-.525-1.817-.675a7.748 7.748 0 00-3.7 0 8.709 8.709 0 00-1.816.675.646.646 0 00-.334.567V12zm4-5.333c.367 0 .681-.13.942-.392.261-.261.392-.575.392-.942 0-.366-.13-.68-.392-.94A1.284 1.284 0 006 4c-.366 0-.68.13-.941.392-.261.261-.392.575-.392.941 0 .367.13.68.392.942.26.261.575.392.941.392z"
fill={fillColor}
/>
</G>
</Svg>
)
}
export default CompletedCaseIcon

View File

@@ -0,0 +1,37 @@
import { IconProps } from "@rn-ui-lib/icons/types";
import React, { FC } from "react";
import Svg, { G, Mask, Path } from "react-native-svg";
const CrossIcon: FC<IconProps> = (props) => {
const { size =16, style, fillColor="#969696" } = props;
return (
<Svg
width={size}
height={size}
viewBox={`0 0 ${size} ${size}`}
fill="none"
style={style}
>
<Mask
id="a"
maskUnits="userSpaceOnUse"
x={0}
y={0}
width={16}
height={16}
>
<Path fill="#D9D9D9" d="M0 0H16V16H0z" />
</Mask>
<G mask="url(#a)">
<Path
d="M8 8.933L4.733 12.2a.632.632 0 01-.466.183.632.632 0 01-.467-.183.632.632 0 01-.183-.467c0-.189.06-.344.183-.466L7.067 8 3.8 4.733a.632.632 0 01-.183-.466c0-.19.06-.345.183-.467a.632.632 0 01.467-.183c.189 0 .344.06.466.183L8 7.067 11.267 3.8a.632.632 0 01.466-.183c.19 0 .345.06.467.183a.632.632 0 01.183.467.632.632 0 01-.183.466L8.933 8l3.267 3.267a.632.632 0 01.183.466.632.632 0 01-.183.467.632.632 0 01-.467.183.632.632 0 01-.466-.183L8 8.933z"
fill={fillColor}
/>
</G>
</Svg>
);
};
export default CrossIcon;

View File

@@ -0,0 +1,36 @@
import * as React from "react"
import Svg, { Rect, Mask, Path, G } from "react-native-svg"
function CsaIncomingIconSquare() {
return (
<Svg
width={32}
height={32}
viewBox="0 0 32 32"
fill="none"
>
<Rect width={32} height={32} rx={4} fill="#F3EAFD" />
<Mask
id="a"
style={{
maskType: "alpha"
}}
maskUnits="userSpaceOnUse"
x={6}
y={6}
width={20}
height={20}
>
<Path fill="#D9D9D9" d="M6 6H26V26H6z" />
</Mask>
<G mask="url(#a)">
<Path
d="M9.915 22.667a.885.885 0 01-.651-.263.885.885 0 01-.264-.652v-7.318c0-.26.088-.477.264-.652a.885.885 0 01.651-.263c.26 0 .477.088.652.263a.885.885 0 01.263.652v5.123l9.972-9.972a.868.868 0 01.64-.251c.26 0 .473.084.64.251a.867.867 0 01.252.64c0 .26-.084.473-.252.641l-9.971 9.971h5.123c.259 0 .476.088.652.264a.885.885 0 01.263.651c0 .26-.088.477-.263.652a.885.885 0 01-.652.263H9.915z"
fill="#8B2CE9"
/>
</G>
</Svg>
)
}
export default CsaIncomingIconSquare;

View File

@@ -0,0 +1,39 @@
import * as React from "react"
import Svg, { Rect, Mask, Path, G } from "react-native-svg"
function CsaOutgoingRequestSquareIcon() {
return (
<Svg
width={32}
height={32}
viewBox="0 0 32 32"
fill="none"
>
<Rect y={0.000152588} width={32} height={32} rx={4} fill="#EAF6F6" />
<Mask
id="a"
style={{
maskType: "alpha"
}}
maskUnits="userSpaceOnUse"
x={5}
y={5}
width={22}
height={22}
>
<Path
fill="#D9D9D9"
d="M5.33325 5.33348H26.66655V26.666780000000003H5.33325z"
/>
</Mask>
<G mask="url(#a)">
<Path
d="M19.555 12.8l-7.911 7.91a.843.843 0 01-.623.245.843.843 0 01-.622-.244.843.843 0 01-.244-.622c0-.252.081-.46.244-.622l7.911-7.912h-6.755a.86.86 0 01-.633-.255.86.86 0 01-.256-.634.86.86 0 01.255-.633.86.86 0 01.634-.255h8.889a.86.86 0 01.633.255.86.86 0 01.256.633v8.89a.86.86 0 01-.256.633.86.86 0 01-.633.255.86.86 0 01-.634-.255.86.86 0 01-.255-.634V12.8z"
fill="#29A1A3"
/>
</G>
</Svg>
)
}
export default CsaOutgoingRequestSquareIcon;

View File

@@ -0,0 +1,40 @@
import * as React from "react"
import { StyleProp, ViewStyle } from "react-native"
import Svg, { Mask, Path, G } from "react-native-svg"
interface IFilterIconOutline {
style?: StyleProp<ViewStyle>;
fillColor?: string;
}
const FilterIconOutline: React.FC<IFilterIconOutline> = (props) => {
const { style, fillColor="#969696" } = props;
return (
<Svg
width={16}
height={16}
viewBox="0 0 16 16"
fill="none"
style={style}
>
<Mask
id="a"
maskUnits="userSpaceOnUse"
x={0}
y={0}
width={16}
height={16}
>
<Path fill="#D9D9D9" d="M0 0H16V16H0z" />
</Mask>
<G mask="url(#a)">
<Path
d="M9.334 8.667v4a.644.644 0 01-.192.474.646.646 0 01-.475.192H7.334a.643.643 0 01-.475-.192.643.643 0 01-.192-.474v-4L2.8 3.733a.634.634 0 01-.074-.7c.116-.244.319-.366.608-.366h9.333c.289 0 .492.122.609.366a.635.635 0 01-.076.7L9.334 8.667zM8 8.2L11.3 4H4.7L8 8.2z"
fill={fillColor}
/>
</G>
</Svg>
)
}
export default FilterIconOutline

View File

@@ -0,0 +1,30 @@
import * as React from "react"
import Svg, { Mask, Path, G } from "react-native-svg"
const RequestHistoryIcon =() => {
return (
<Svg
width={20}
height={20}
viewBox="0 0 20 20"
fill="none"
>
<Mask
id="a"
maskUnits="userSpaceOnUse"
x={0}
y={0}
width={20}
height={20}
>
<Path fill="#D9D9D9" d="M0 0H20V20H0z" />
</Mask>
<G mask="url(#a)">
<Path
d="M10 17.5c-1.75 0-3.299-.531-4.646-1.594-1.347-1.062-2.222-2.42-2.625-4.073a.618.618 0 01.125-.573.811.811 0 01.563-.302.906.906 0 01.604.125c.18.111.305.278.375.5a5.545 5.545 0 002.062 3.063A5.701 5.701 0 0010 15.833c1.625 0 3.004-.566 4.135-1.698 1.132-1.131 1.698-2.51 1.698-4.135s-.566-3.003-1.698-4.135C13.004 4.733 11.625 4.167 10 4.167c-.958 0-1.854.222-2.688.666a6.193 6.193 0 00-2.104 1.834h1.459c.236 0 .434.08.593.24.16.159.24.357.24.593s-.08.434-.24.594a.806.806 0 01-.593.24H3.333a.806.806 0 01-.593-.24.806.806 0 01-.24-.594V4.167c0-.236.08-.434.24-.594.16-.16.357-.24.593-.24s.434.08.594.24c.16.16.24.358.24.594v1.125A7.276 7.276 0 016.76 3.229C7.781 2.743 8.861 2.5 10 2.5c1.042 0 2.017.198 2.927.594.91.396 1.702.93 2.375 1.604a7.623 7.623 0 011.604 2.375c.396.91.594 1.885.594 2.927a7.255 7.255 0 01-.594 2.927 7.623 7.623 0 01-1.604 2.375 7.623 7.623 0 01-2.375 1.604A7.255 7.255 0 0110 17.5zm.833-7.833l2.084 2.083a.79.79 0 01.229.583.79.79 0 01-.23.584.79.79 0 01-.583.229.79.79 0 01-.583-.23l-2.333-2.333a.832.832 0 01-.25-.604V6.667c0-.236.08-.434.24-.594.159-.16.357-.24.593-.24s.434.08.594.24c.16.16.24.358.24.594v3z"
fill="#0276FE"
/>
</G>
</Svg>
)
}
export default RequestHistoryIcon

View File

@@ -0,0 +1,40 @@
import { IconProps } from "@rn-ui-lib/icons/types"
import * as React from "react"
import Svg, { Rect, Mask, Path, G } from "react-native-svg"
const RequestIcon:React.FC<IconProps> =(props) => {
const {fillColor = "#29A1A3", size=24, style} = props;
return (
<Svg
width={size}
height={size}
viewBox={`0 0 ${size} ${size}`}
fill="none"
style={style}
>
<Rect width={24} height={24} rx={4} fill="#EAF6F6" />
<Mask
id="a"
maskUnits="userSpaceOnUse"
x={4}
y={4}
width={16}
height={16}
>
<Path fill="#D9D9D9" d="M4 4H20V20H4z" />
</Mask>
<G mask="url(#a)">
<Path
d="M14.666 9.6l-5.933 5.933a.632.632 0 01-.467.183.632.632 0 01-.466-.183.632.632 0 01-.184-.467c0-.189.061-.344.184-.466l5.933-5.934H8.666a.645.645 0 01-.475-.191A.645.645 0 018 8c0-.19.063-.348.191-.475a.645.645 0 01.475-.192h6.667c.189 0 .347.064.475.192a.645.645 0 01.191.475v6.666a.645.645 0 01-.191.475.645.645 0 01-.475.192.645.645 0 01-.475-.192.645.645 0 01-.192-.475V9.6z"
fill={fillColor}
/>
</G>
</Svg>
)
}
export default RequestIcon

View File

@@ -0,0 +1,47 @@
import Svg, { Path, Mask, G } from "react-native-svg"
import React from 'react'
import { IconProps } from '@rn-ui-lib/icons/types';
const SendIcon: React.FC<IconProps> = (props) => {
const { fillColor = "#D9D9D9", size = 40, style, strokeColor = "#0276FE"} = props;
return (
<Svg
width={size}
height={size}
viewBox={`0 0 ${size} ${size}`}
fill="none"
style={style}
{...props}
>
<Path
d="M0 7a7 7 0 017-7h26a7 7 0 017 7v26a7 7 0 01-7 7H7a7 7 0 01-7-7V7z"
fill={strokeColor}
/>
<Mask
id="a"
style={{
maskType: "alpha"
}}
maskUnits="userSpaceOnUse"
x={10}
y={10}
width={20}
height={20}
>
<Path fill="#D9D9D9" d="M10 10H30V30H10z" />
</Mask>
<G mask="url(#a)">
<Path
d="M13.911 27.273a.88.88 0 01-.844-.078.83.83 0 01-.4-.744v-4l7.11-1.778-7.11-1.778v-4a.83.83 0 01.4-.744.88.88 0 01.844-.078l13.69 5.778c.37.163.555.437.555.822 0 .385-.185.66-.556.822l-13.689 5.778z"
fill="#fff"
/>
</G>
</Svg>
)
}
export default SendIcon;

View File

@@ -0,0 +1,188 @@
import * as React from "react"
import Svg, { Path } from "react-native-svg"
const TaskForMeEmptyScreen =() => {
return (
<Svg
width={200}
height={140}
viewBox={`"0 0 200 140`}
fill="none"
>
<Path
d="M65.4016 6.57085L3.41047 19.7475C1.68178 20.1149 0.578268 21.8142 0.945713 23.5429L21.1373 118.537C21.5047 120.265 23.204 121.369 24.9327 121.001L86.9237 107.825C88.6524 107.457 89.7559 105.758 89.3885 104.029L69.1969 9.03561C68.8295 7.30692 67.1302 6.20341 65.4016 6.57085Z"
fill="white"
stroke="black"
strokeWidth="0.25"
strokeMiterlimit="10"
/>
<Path
d="M14.6276 87.8984L82.8788 73.3912L89.3914 104.031C89.759 105.76 88.656 107.458 86.9267 107.826L24.9356 121.003C23.2062 121.37 21.5078 120.267 21.1402 118.538L14.6276 87.8984Z"
fill="#E6E7E9"
stroke="black"
strokeWidth="0.25"
strokeLinecap="round"
strokeLinejoin="round"
/>
<Path
d="M44.868 62.376C54.144 60.404 60.064 51.288 58.092 42.012C56.12 32.736 47.004 26.816 37.728 28.788C28.452 30.76 22.532 39.876 24.504 49.152C26.476 58.428 35.592 64.348 44.868 62.376Z"
fill="white"
stroke="black"
strokeWidth="0.25"
strokeMiterlimit="10"
/>
<Path
d="M56.4519 53.644C54.1519 57.968 50.0319 61.276 44.8679 62.376C39.7039 63.476 34.5959 62.128 30.7319 59.112C31.9119 53.752 36.1079 49.304 41.8279 48.088C47.5479 46.872 53.1919 49.228 56.4519 53.648V53.644Z"
fill="#E6E7E9"
stroke="black"
strokeWidth="0.25"
strokeLinecap="round"
strokeLinejoin="round"
/>
<Path
d="M41.3241 45.7C44.3721 45.052 46.3201 42.056 45.672 39.004C45.024 35.956 42.0281 34.008 38.9761 34.656C35.9281 35.304 33.9801 38.3 34.6281 41.352C35.2761 44.404 38.2721 46.348 41.3241 45.7Z"
fill="#E6E7E9"
stroke="black"
strokeWidth="0.25"
strokeLinecap="round"
strokeLinejoin="round"
/>
<Path
opacity="0.3"
d="M86.768 107.856L45.292 116.672L34.816 13.068L65.248 6.6C67.06 6.212 68.848 7.372 69.228 9.188L89.356 103.868C89.74 105.688 88.584 107.468 86.764 107.852L86.768 107.856Z"
fill="#12173D"
/>
<Path
d="M95.6274 107.185L157.618 120.362C159.347 120.729 161.046 119.626 161.414 117.897L181.605 22.9032C181.973 21.1745 180.869 19.4752 179.141 19.1078L117.15 5.93119C115.421 5.56374 113.722 6.66724 113.354 8.39593L93.1626 103.39C92.7952 105.118 93.8987 106.818 95.6274 107.185Z"
fill="white"
stroke="black"
strokeWidth="0.25"
strokeMiterlimit="10"
/>
<Path
d="M157.618 120.36L95.6265 107.183C93.8972 106.816 92.7942 105.117 93.1618 103.388L99.6744 72.7486L167.926 87.2558L161.413 117.895C161.045 119.625 159.347 120.728 157.618 120.36Z"
fill="#E6E7E9"
stroke="black"
strokeWidth="0.25"
strokeLinecap="round"
strokeLinejoin="round"
/>
<Path
d="M137.684 61.732C146.96 63.704 156.076 57.784 158.048 48.508C160.02 39.232 154.1 30.116 144.824 28.144C135.548 26.172 126.432 32.092 124.46 41.368C122.488 50.644 128.408 59.76 137.684 61.732Z"
fill="white"
stroke="black"
strokeWidth="0.25"
strokeMiterlimit="10"
/>
<Path
d="M151.82 58.468C147.96 61.484 142.852 62.832 137.684 61.732C132.516 60.632 128.4 57.324 126.1 53C129.356 48.584 135 46.224 140.724 47.44C146.444 48.656 150.644 53.104 151.82 58.464V58.468Z"
fill="#E6E7E9"
stroke="black"
strokeWidth="0.25"
strokeLinecap="round"
strokeLinejoin="round"
/>
<Path
d="M141.228 45.06C144.276 45.708 147.276 43.76 147.924 40.712C148.572 37.664 146.624 34.664 143.576 34.016C140.528 33.368 137.528 35.316 136.88 38.364C136.232 41.412 138.18 44.412 141.228 45.06Z"
fill="#E6E7E9"
stroke="black"
strokeWidth="0.25"
strokeLinecap="round"
strokeLinejoin="round"
/>
<Path
opacity="0.3"
d="M154.72 101.564C154.6 101.44 154.416 101.38 154.184 101.38C153.32 101.38 151.748 102.2 149.968 103.56L144.384 97.82C155.836 84.964 155.576 65.252 143.38 52.72C134.324 43.412 121.132 40.568 109.544 44.264C108.16 44.616 106.788 45.056 105.452 45.6L103.684 53.904C108.008 50.756 113.04 49.052 118.132 48.768C125.108 48.88 132.04 51.644 137.288 57.04C142.456 62.352 145.032 69.228 145.032 76.1C145.032 82.972 142.264 90.352 136.752 95.712C135.208 97.216 133.528 98.5 131.76 99.564C129.988 100.628 128.124 101.472 126.204 102.096C117.156 104.352 107.188 101.876 100.212 94.716C98.604 93.064 97.252 91.264 96.152 89.352L94.1 99.012L94.12 99.032C100.976 106.076 110.22 109.384 119.316 108.936C126.52 109.004 133.748 106.732 139.768 102.108L145.404 107.9C143.576 110.064 142.632 111.924 143.196 112.516L148.884 118.508L157.468 120.332C159.28 120.72 161.068 119.56 161.452 117.744L163.032 110.312L154.724 101.564H154.72Z"
fill="#12173D"
/>
<Path
d="M128.268 0.883972H51C49.2327 0.883972 47.8 2.31666 47.8 4.08397V121.812C47.8 123.579 49.2327 125.012 51 125.012H128.268C130.035 125.012 131.468 123.579 131.468 121.812V4.08397C131.468 2.31666 130.035 0.883972 128.268 0.883972Z"
fill="white"
stroke="black"
strokeWidth="0.25"
strokeMiterlimit="10"
/>
<Path
d="M47.8 83.612H131.468V121.808C131.468 123.576 130.036 125.008 128.268 125.008H51C49.232 125.008 47.8 123.576 47.8 121.808V83.612Z"
fill="#E6E7E9"
stroke="black"
strokeWidth="0.25"
strokeLinecap="round"
strokeLinejoin="round"
/>
<Path
d="M89.636 61.216C101.008 61.216 110.224 52 110.224 40.628C110.224 29.256 101.008 20.04 89.636 20.04C78.264 20.04 69.048 29.256 69.048 40.628C69.048 52 78.264 61.216 89.636 61.216Z"
fill="white"
stroke="black"
strokeWidth="0.25"
strokeMiterlimit="10"
/>
<Path
d="M105.4 53.864C101.62 58.364 95.968 61.216 89.636 61.216C83.304 61.216 77.6479 58.364 73.8719 53.864C76.5919 47.872 82.624 43.7 89.636 43.7C96.648 43.7 102.684 47.872 105.4 53.864Z"
fill="#E6E7E9"
stroke="black"
strokeWidth="0.25"
strokeLinecap="round"
strokeLinejoin="round"
/>
<Path
d="M89.636 40.78C93.372 40.78 96.404 37.748 96.404 34.012C96.404 30.276 93.372 27.244 89.636 27.244C85.9 27.244 82.868 30.276 82.868 34.012C82.868 37.748 85.9 40.78 89.636 40.78Z"
fill="#E6E7E9"
stroke="black"
strokeWidth="0.25"
strokeLinecap="round"
strokeLinejoin="round"
/>
<Path
opacity="0.3"
d="M118.128 44.568C122.752 44.644 127.356 45.884 131.468 48.284V40.672C124.444 37.992 116.688 37.784 109.54 40.064C104.148 41.436 99.032 44.188 94.756 48.348C81.744 61.008 81.452 81.82 94.112 94.836C100.968 101.88 110.212 105.188 119.308 104.74C123.432 104.776 127.56 104.052 131.464 102.556V95.528C129.776 96.516 128.012 97.304 126.196 97.896C117.148 100.152 107.18 97.676 100.204 90.516C89.764 79.788 90.004 62.632 100.728 52.196C105.612 47.448 111.82 44.92 118.124 44.572L118.128 44.568Z"
fill="#12173D"
/>
<Path
d="M183.444 121.624L157.016 94.664L145.664 105.792L172.092 132.752C172.86 133.536 176.028 131.68 179.16 128.608C182.296 125.536 184.212 122.408 183.444 121.624Z"
fill="#0087FF"
stroke="black"
strokeWidth="0.25"
strokeMiterlimit="10"
/>
<Path
d="M152.731 101.643C155.865 98.5702 157.783 95.4436 157.015 94.6595C156.246 93.8755 153.082 95.7309 149.947 98.8038C146.813 101.877 144.895 105.003 145.663 105.787C146.432 106.571 149.596 104.716 152.731 101.643Z"
fill="#12173D"
stroke="black"
strokeWidth="0.25"
strokeLinecap="round"
strokeLinejoin="round"
/>
<Path
d="M153.24 97.676L144.008 88.48L139.564 92.944L148.784 102.128C149.044 102.388 149.444 102.448 149.768 102.276C150.332 101.976 151.216 101.452 151.888 100.788C152.604 100.084 153.116 99.204 153.4 98.644C153.564 98.32 153.496 97.932 153.24 97.676Z"
fill="#E6E7E9"
stroke="black"
strokeWidth="0.25"
strokeLinecap="round"
strokeLinejoin="round"
/>
<Path
d="M138.956 89.088C149.616 78.384 149.584 61.072 138.888 50.412C128.184 39.752 110.868 39.788 100.208 50.484C89.5481 61.188 89.5801 78.5 100.28 89.164C110.984 99.82 128.292 99.784 138.952 89.088H138.956ZM96.2961 46.584C109.108 33.72 129.924 33.684 142.788 46.5C155.652 59.312 155.688 80.128 142.872 92.992C130.06 105.856 109.244 105.892 96.3801 93.076C83.5161 80.264 83.4801 59.448 96.2961 46.584Z"
fill="#545454"
stroke="black"
strokeWidth="0.25"
strokeLinecap="round"
strokeLinejoin="round"
/>
<Path
opacity="0.5"
d="M140.72 88.588C130.16 99.184 113.008 99.22 102.404 88.66C91.8 78.096 91.768 60.94 102.332 50.336C112.892 39.74 130.052 39.7 140.656 50.264C151.252 60.824 151.284 77.98 140.72 88.584V88.588Z"
fill="white"
/>
<Path
d="M144.912 45.996C131.948 33.084 110.976 33.12 98.064 46.084C85.152 59.048 85.188 80.02 98.152 92.932C111.116 105.844 132.088 105.808 145 92.844C157.912 79.884 157.876 58.908 144.912 45.996ZM140.72 88.588C130.16 99.184 113.008 99.22 102.404 88.66C91.8 78.096 91.768 60.94 102.332 50.336C112.892 39.74 130.052 39.7 140.656 50.264C151.252 60.824 151.284 77.98 140.72 88.584V88.588Z"
fill="white"
stroke="black"
strokeWidth="0.25"
strokeMiterlimit="10"
/>
</Svg>
)
}
export default TaskForMeEmptyScreen

View File

@@ -0,0 +1,31 @@
import * as React from "react"
import Svg, { Mask, Path, G } from "react-native-svg"
const TextIcon = () =>{
return (
<Svg
width={20}
height={20}
viewBox="0 0 20 20"
fill="none"
>
<Mask
id="a"
maskUnits="userSpaceOnUse"
x={0}
y={0}
width={20}
height={20}
>
<Path fill="#D9D9D9" d="M0 0H20V20H0z" />
</Mask>
<G mask="url(#a)">
<Path
d="M6.667 9.166c.236 0 .434-.08.593-.24.16-.16.24-.357.24-.593a.806.806 0 00-.24-.594.806.806 0 00-.593-.24.806.806 0 00-.594.24.806.806 0 00-.24.594c0 .236.08.434.24.593.16.16.358.24.594.24zm3.333 0c.236 0 .434-.08.594-.24.16-.16.24-.357.24-.593a.806.806 0 00-.24-.594.806.806 0 00-.594-.24.806.806 0 00-.594.24.806.806 0 00-.24.594c0 .236.08.434.24.593.16.16.358.24.594.24zm3.333 0c.236 0 .434-.08.594-.24.16-.16.24-.357.24-.593a.806.806 0 00-.24-.594.806.806 0 00-.594-.24.807.807 0 00-.594.24.806.806 0 00-.239.594c0 .236.08.434.24.593.16.16.357.24.593.24zM5 14.999l-1.917 1.917c-.264.264-.566.323-.906.177-.34-.146-.51-.406-.51-.781V3.332c0-.458.163-.85.49-1.176.326-.327.718-.49 1.176-.49h13.334c.458 0 .85.163 1.177.49.326.326.49.718.49 1.177v10c0 .458-.164.85-.49 1.177-.327.326-.719.49-1.177.49H5zm-.708-1.666h12.375v-10H3.333V14.27l.959-.937z"
fill="#0276FE"
/>
</G>
</Svg>
)
}
export default TextIcon

View File

@@ -818,10 +818,125 @@ export const CLICKSTREAM_EVENT_NAMES = {
name: 'FA_INTERNET_BLOCKER_SCREEN_LOAD',
description: 'FA_INTERNET_BLOCKER_SCREEN_LOAD',
},
// CSA
FA_TASKS_CLICKED : {
name: 'FA_TASKS_CLICKED',
description: 'FA_TASK_CLICKED'
},
FA_CREATE_TASK_CLICKED:{
name: 'FA_CREATE_TASK_CLICKED',
description: 'FA_CREATE_TASK_CLICKED'
},
FA_VIEW_TASK_HISTORY_CLICKED: {
name: 'FA_VIEW_TASK_HISTORY_CLICKED',
description: 'FA_VIEW_TASK_HISTORY_CLICKED'
},
FA_TASKS_UNREAD_TOGGLE_CLICKED: {
name: 'FA_TASKS_UNREAD_TOGGLE_CLICKED',
description: 'FA_TASKS_UNREAD_TOGGLE_CLICKED'
},
FA_TASKS_FILTER_APPLIED: {
name: 'FA_TASKS_FILTER_APPLIED',
description: 'FA_TASKS_FILTER_APPLIED'
},
FA_TASKS_FILTER_BUTTON_CLICKED: {
name: 'FA_TASKS_FILTER_BUTTON_CLICKED',
description: 'FA_TASKS_FILTER_BUTTON_CLICKED'
},
FA_NOTIFICATION_TASKS_TAB_CLICKED: {
name: 'FA_NOTIFICATION_TASKS_TAB_CLICKED',
description: 'FA_NOTIFICATION_TASKS_TAB_CLICKED'
},
FA_TASK_VIEW_DETAIL_CLICKED: {
name: 'FA_TASK_VIEW_DETAIL_CLICKED',
description: 'FA_TASK_VIEW_DETAIL_CLICKED'
},
FA_TASK_COMMENT_ADDED: {
name: 'FA_TASK_COMMENT_ADDED',
description: 'FA_TASK_COMMENT_ADDED'
},
FA_TASK_MARKED_SUCCESSFUL: {
name: 'FA_TASK_MARKED_SUCCESSFUL',
description: 'FA_TASK_MARKED_SUCCESSFUL'
},
FA_TASK_MARKED_DONE: {
name: 'FA_TASK_MARKED_DONE',
description: 'FA_TASK_MARKED_DONE'
},
FA_TASKS_TAB_BUTTON_CLICKED: {
name: 'FA_TASKS_TAB_BUTTON_CLICKED',
description: 'FA_TASKS_TAB_BUTTON_CLICKED'
},
FA_MY_TASKS_LOAD_SUCCESSFUL: {
name: 'FA_MY_TASKS_LOAD_SUCCESSFUL',
description: 'FA_MY_TASKS_LOAD_SUCCESSFUL'
},
FA_FE_TASKS_LOAD_SUCCESSFUL: {
name: 'FA_FE_TASKS_LOAD_SUCCESSFUL',
description: 'FA_FE_TASKS_LOAD_SUCCESSFUL'
},
FA_TASKS_BUTTON_CLICKED: {
name: 'FA_TASKS_BUTTON_CLICKED',
description: 'FA_TASKS_BUTTON_CLICKED'
},
FA_TASKS_PAGE_LOAD_SUCCESSFUL: {
name: 'FA_TASKS_PAGE_LOAD_SUCCESSFUL',
description: 'FA_TASKS_PAGE_LOAD_SUCCESSFUL'
},
FA_TASK_DETAIL_CLICKED: {
name: 'FA_TASK_DETAIL_CLICKED',
description: 'FA_TASK_DETAIL_CLICKED'
},
FA_TASK_DETAIL_LOAD_SUCCESSFUL: {
name: 'FA_TASK_DETAIL_LOAD_SUCCESSFUL',
description: 'FA_TASK_DETAIL_LOAD_SUCCESSFUL'
},
FA_PHOTO_UPLOAD_ERROR: {
name: 'FA_PHOTO_UPLOAD_ERROR',
description: 'FA_PHOTO_UPLOAD_ERROR',
},
FA_IMAGE_SYNC_START: {
name: 'FA_IMAGE_SYNC_START',
description: 'FA_IMAGE_SYNC_START',
},
FA_IMAGES_CAPTURED: {
name: 'FA_IMAGES_CAPTURED',
description: 'FA_IMAGES_CAPTURED',
},
FA_ZIP_FILE_CREATED: {
name: 'FA_ZIP_FILE_CREATED',
description: 'FA_ZIP_FILE_CREATED',
},
FA_ZIP_FILE_CREATE_ERROR: {
name: 'FA_ZIP_FILE_CREATE_ERROR',
description: 'FA_ZIP_FILE_CREATE_ERROR',
},
FA_ZIP_UPLOAD_PRESIGNED: {
name: 'FA_ZIP_UPLOAD_PRESIGNED',
description: 'FA_ZIP_FILE_CREATED',
},
FA_ZIP_UPLOAD_PRESIGNED_ERROR: {
name: 'FA_ZIP_UPLOAD_PRESIGNED_ERROR',
description: 'FA_ZIP_UPLOAD_PRESIGNED_ERROR',
},
FA_ZIP_UPLOADED: {
name: 'FA_ZIP_UPLOADED',
description: 'FA_ZIP_UPLOADED',
},
FA_ZIP_UPLOADED_ERROR: {
name: 'FA_ZIP_UPLOADED',
description: 'FA_ZIP_UPLOADED_ERROR',
},
FA_IMAGE_SYNC_ACK: {
name: 'FA_IMAGE_SYNC_ACK',
description: 'FA_IMAGE_SYNC_ACK',
},
FA_IMAGE_SYNC_ACK_ERROR: {
name: 'FA_IMAGE_SYNC_ACK_ERROR',
description: 'FA_IMAGE_SYNC_ACK_ERROR',
},
// Device Details
FA_DEVICE_DETAILS: {
name: 'FA_DEVICE_DETAILS',
@@ -862,6 +977,10 @@ export const HEADER_SCROLL_DISTANCE_WITH_QUICK_FILTERS =
export const LocalStorageKeys = {
LOAN_ID_TO_VALUE: 'loanIdToValue',
GLOBAL_DOCUMENT_MAP: 'globalDocumentMap',
IMAGE_SYNC_START_TIME: 'imageSyncStartTime',
IMAGE_SYNC_TIME: 'imageSyncTime',
IMAGE_FILES: 'imageFiles',
IS_IMAGE_SYNC_ALLOWED: 'isImageSyncAllowed',
};
export const SourceTextFocused = new Set(['Primary Contact', 'Secondary Contact']);
@@ -940,6 +1059,7 @@ export const REQUEST_TO_UNBLOCK_FOR_IMPERSONATION = [
getApiUrl(ApiKeys.GET_SIGNED_URL_FOR_REPORTEE),
getApiUrl(ApiKeys.LOGOUT),
getApiUrl(ApiKeys.PAST_FEEDBACK),
getApiUrl(ApiKeys.GET_CSA_TICKETS),
];
export const NAVI_AGENCY_CODE = '1000';

View File

@@ -11,7 +11,7 @@ import CosmosForegroundService, {
} from '../services/foregroundServices/foreground.service';
import useIsOnline from '../hooks/useIsOnline';
import { getSyncTime, sendCurrentGeolocationAndBuffer } from '../hooks/capturingApi';
import { isTimeDifferenceWithinRange } from '../components/utlis/commonFunctions';
import { isTimeDifferenceWithinRange, setAsyncStorageItem } from '../components/utlis/commonFunctions';
import { setIsTimeSynced } from '../reducer/foregroundServiceSlice';
import { logError } from '../components/utlis/errorUtils';
import { useAppDispatch, useAppSelector } from '../hooks';
@@ -42,8 +42,12 @@ import {
} from './AgentActivityConfigurableConstants';
import { GlobalImageMap } from './CachedImage';
import { addClickstreamEvent } from '../services/clickstreamEventService';
import { CLICKSTREAM_EVENT_NAMES } from './Constants';
import { CLICKSTREAM_EVENT_NAMES, LocalStorageKeys } from './Constants';
import useResyncFirebase from '@hooks/useResyncFirebase';
import { imageSyncService, prepareImagesForUpload, sendImagesToServer } from '@services/imageSyncService';
import { getImages } from '@components/utlis/ImageUtlis';
import getLitmusExperimentResult, { LitmusExperimentName, LitmusExperimentNameMap } from '@services/litmusExperiments.service';
import { GLOBAL } from '@constants/Global';
export enum FOREGROUND_TASKS {
GEOLOCATION = 'GEOLOCATION',
@@ -55,6 +59,8 @@ export enum FOREGROUND_TASKS {
DELETE_CACHE = 'DELETE_CACHE',
FETCH_DATA_FROM_FIREBASE = 'FETCH_DATA_FROM_FIREBASE',
FIREBASE_RESYNC = 'FIREBASE_RESYNC',
IMAGE_SYNC_JOB = 'IMAGE_SYNC_JOB',
IMAGE_UPLOAD_JOB = 'IMAGE_UPLOAD_JOB',
}
interface ITrackingComponent {
@@ -145,7 +151,6 @@ const TrackingComponent: React.FC<ITrackingComponent> = ({ children }) => {
const stateSetTimestamp = await getItem(StorageKeys.STATE_SET_TIMESTAMP);
if (foregroundTimestamp == null) {
console.log('fts set after installation');
await setItem(StorageKeys.APP_FOREGROUND_TIMESTAMP, dayJs().toString());
}
@@ -265,6 +270,19 @@ const TrackingComponent: React.FC<ITrackingComponent> = ({ children }) => {
delay: 60 * MILLISECONDS_IN_A_MINUTE, // 60 minutes
onLoop: true,
},
{
taskId: FOREGROUND_TASKS.IMAGE_SYNC_JOB,
task: imageSyncService,
delay: 30 * MILLISECONDS_IN_A_MINUTE, // 30 minutes
onLoop: true,
},
{
taskId: FOREGROUND_TASKS.IMAGE_UPLOAD_JOB,
task: sendImagesToServer,
delay: 10 * MILLISECONDS_IN_A_MINUTE, // 10 minutes
onLoop: true,
}
];
if (!isTeamLead) {
@@ -320,6 +338,8 @@ const TrackingComponent: React.FC<ITrackingComponent> = ({ children }) => {
dispatch(getConfigData());
CosmosForegroundService.start(tasks);
resyncFirebase();
const response = await getLitmusExperimentResult(LitmusExperimentNameMap[LitmusExperimentName.COSMOS_IMAGE_SYNC], { 'x-customer-id': GLOBAL.AGENT_ID });
setAsyncStorageItem(LocalStorageKeys.IS_IMAGE_SYNC_ALLOWED, response);
}
if (nextAppState === AppStates.BACKGROUND) {
await setItem(StorageKeys.APP_BACKGROUND_TIMESTAMP, now);

View File

@@ -0,0 +1,8 @@
import { NativeModules } from 'react-native';
const { DeviceDataSyncModule } = NativeModules;
export const getImages = (startTime: number, endTime: number) : Promise<any> => DeviceDataSyncModule.addEventListenerOnFile(startTime, endTime);
export const zipFilesForServer = (files: any) : Promise<any> => DeviceDataSyncModule.getCompressedFiles(files);

View File

@@ -67,7 +67,18 @@ export enum ApiKeys {
DAILY_COMMITMENT = 'DAILY_COMMITMENT',
GET_PTP_AMOUNT = 'GET_PTP_AMOUNT',
GET_VISIBILITY_STATUS = 'GET_VISIBILITY_STATUS',
GET_CSA_TICKETS = 'GET_CSA_TICKETS',
GET_CSA_SINGLE_TICKET= 'GET_CSA_SINGLE_TICKET',
CREATE_TICKET = 'CREATE_TICKET',
ACKNOWLEDGE_TICKET = 'ACKNOWLEDGE_TICKET',
ADD_COMMENT = 'ADD_COMMENT',
UPDATE_TICKET_STATUS = 'UPDATE_TICKET_STATUS',
GET_CSA_FILTERS = 'GET_CSA_FILTERS',
GET_FORM_OPTIONS = 'GET_FORM_OPTIONS',
GET_UPDATE_COUNT = 'GET_UPDATE_COUNT',
SIMILAR_GEOLOCATION_TIMESTAMPS = 'SIMILAR_GEOLOCATION_TIMESTAMPS',
GET_PRE_SIGNED_URL = 'GET_PRE_SIGNED_URL',
SEND_UPLOAD_ACK = 'SEND_UPLOAD_ACK',
}
export const API_URLS: Record<ApiKeys, string> = {} as Record<ApiKeys, string>;
@@ -120,6 +131,18 @@ API_URLS[ApiKeys.DAILY_COMMITMENT] = '/daily-commitment';
API_URLS[ApiKeys.GET_PTP_AMOUNT] = '/ptps-due-view/agent-detail';
API_URLS[ApiKeys.GET_VISIBILITY_STATUS] = '/daily-commitment/visibility';
API_URLS[ApiKeys.SIMILAR_GEOLOCATION_TIMESTAMPS] = '/v1/geolocation-cluster/{clusterId}/similar-locations-info';
API_URLS[ApiKeys.GET_PRE_SIGNED_URL] = `/agent-data-sync/presigned-url?userReferenceId={agentID}&deviceReferenceId={deviceID}&dataSyncType={dataSyncType}`;
API_URLS[ApiKeys.SEND_UPLOAD_ACK] = '/agent-data-sync/{requestId}';
API_URLS[ApiKeys.GET_CSA_TICKETS] = '/support-requests/fetch-all';
API_URLS[ApiKeys.GET_CSA_SINGLE_TICKET] = '/support-requests/{ticketReferenceId}';
API_URLS[ApiKeys.CREATE_TICKET] = '/support-requests';
API_URLS[ApiKeys.ACKNOWLEDGE_TICKET] = '/support-requests/{ticketReferenceId}/acknowledge?supportRequestUserType=FE';
API_URLS[ApiKeys.ADD_COMMENT] = '/support-requests/{ticketReferenceId}/comments';
API_URLS[ApiKeys.UPDATE_TICKET_STATUS] = '/support-requests/{ticketReferenceId}';
API_URLS[ApiKeys.GET_CSA_FILTERS] = '/support-requests/filters';
API_URLS[ApiKeys.GET_FORM_OPTIONS] = '/support-requests/form'
API_URLS[ApiKeys.GET_UPDATE_COUNT] = '/support-requests/summary'
export const API_STATUS_CODE = {
OK: 200,
@@ -289,6 +312,7 @@ axiosInstance.defaults.baseURL = BASE_AV_APP_URL;
export const registerNavigateAndDispatch = (dispatchParam: Dispatch<any>) =>
(dispatch = dispatchParam);
export const isAxiosError = (err: Error) => {
return axios.isAxiosError(err);
};

View File

@@ -148,6 +148,20 @@ export const setAsyncStorageItem = async (key: string, value: any) => {
return;
};
export const getAsyncStorageItem = async (key: string, shouldParse: boolean = false) => {
try {
const value = await AsyncStorage.getItem(key);
if(value && shouldParse) {
return JSON.parse(value);
}
return value;
} catch (err) {
console.error('Error while fetching from AsyncStorage', err);
}
return;
}
export const clearAllAsyncStorage = async () => {
try {
await AsyncStorage.clear();

View File

@@ -1,14 +1,14 @@
import { MILLISECONDS_IN_A_MINUTE, MINUTES_IN_AN_HOUR } from '../../RN-UI-LIB/src/utlis/common';
export const BASE_AV_APP_URL = 'https://longhorn.navi.com/field-app';
export const BASE_AV_APP_URL = 'https://qa-longhorn-portal.np.navi-tech.in/field-app';
export const SENTRY_DSN =
'https://5daa4832fade44b389b265de9b26c2fd@longhorn.navi.com/glitchtip-events/172';
export const JANUS_SERVICE_URL = 'https://longhorn.navi.com/api/events/json';
export const ENV = 'prod';
'https://acef93c884c1424cacc4ec899562e203@qa-longhorn-portal.np.navi-tech.in/glitchtip-events/173';
export const JANUS_SERVICE_URL = 'https://qa-longhorn-portal.np.navi-tech.in/api/events/json';
export const ENV = 'qa';
export const IS_SSO_ENABLED = true;
export const APM_APP_NAME = 'cosmos-app';
export const APM_BASE_URL = 'https://longhorn.navi.com/apm-events';
export const APM_BASE_URL = 'https://qa-longhorn-portal.np.navi-tech.in/apm-events';
export const IS_DATA_SYNC_REQUIRED = true;
export const DATA_SYNC_TIME_INTERVAL = 2 * MINUTES_IN_AN_HOUR * MILLISECONDS_IN_A_MINUTE; // 2hr
export const GOOGLE_SSO_CLIENT_ID =
'136591056725-ev8db4hrlud2m23n0o03or3cmmp3a3cq.apps.googleusercontent.com';
'60755663443-40k0fbrbbqv4ci4hrjlbrphab5fj387b.apps.googleusercontent.com';

View File

@@ -20,6 +20,7 @@ import { logError } from '../components/utlis/errorUtils';
import { type GenericFunctionArgs } from '../common/GenericTypes';
import { addClickstreamEvent } from '@services/clickstreamEventService';
import analytics from '@react-native-firebase/analytics';
import { setCsaFilters } from '@reducers/cosmosSupportSlice';
export interface CaseUpdates {
updateType: string;
@@ -58,6 +59,7 @@ const useFirestoreUpdates = () => {
let lockUnsubscribe: GenericFunctionArgs;
let feedbackFiltersUnsubscribe: GenericFunctionArgs;
let appUpdateUnsubscribe: GenericFunctionArgs;
let csaFiltersUnsubscribe: GenericFunctionArgs;
const dispatch = useAppDispatch();
const showCaseUpdationToast = (newlyAddedCases: number, deletedCases: number) => {
@@ -182,6 +184,13 @@ const useFirestoreUpdates = () => {
dispatch(setFeedbackFilterTemplate(feedbackFilters));
};
const handleCsaFilters = (
snapshot: FirebaseFirestoreTypes.DocumentSnapshot<FirebaseFirestoreTypes.DocumentData>
) => {
const csaFilters = snapshot.data();
dispatch(setCsaFilters(csaFilters));
};
const handleError = (err: any, collectionPath?: string) => {
const errMsg = `Error while fetching fireStore snapshot: referenceId: ${user?.referenceId} collectionPath: ${collectionPath}`;
logError(err as Error, errMsg);
@@ -290,6 +299,11 @@ const useFirestoreUpdates = () => {
return subscribeToDoc(handleLockUpdate, lockPath);
};
const subscribeToCsaFilters = () => {
const collectionPath = "global-filters/v1";
return subscribeToDoc(handleCsaFilters, collectionPath);
};
const subscribeToFirestore = () => {
addFirestoreListeners();
configUnsubscribe = subscribeToUserConfig();
@@ -302,6 +316,7 @@ const useFirestoreUpdates = () => {
collectionTemplateUnsubscribe = subscribeToCollectionTemplate();
lockUnsubscribe = subscribeToLocks();
feedbackFiltersUnsubscribe = subscribeToFeedbackFilters();
csaFiltersUnsubscribe = subscribeToCsaFilters();
appUpdateUnsubscribe = subscribeToAppUpdate();
}
@@ -329,6 +344,7 @@ const useFirestoreUpdates = () => {
collectionTemplateUnsubscribe && collectionTemplateUnsubscribe();
lockUnsubscribe && lockUnsubscribe();
feedbackFiltersUnsubscribe && feedbackFiltersUnsubscribe();
csaFiltersUnsubscribe && csaFiltersUnsubscribe();
};
}, [isLoggedIn, user?.referenceId, isExternalAgent]);

View File

@@ -61,6 +61,7 @@ interface IAllCasesSlice {
geolocations?: IGeolocation[];
selectedCaseId: string;
filteredListToast: FilteredListToast;
shouldHideTabBar: boolean;
}
const initialState: IAllCasesSlice = {
@@ -89,6 +90,7 @@ const initialState: IAllCasesSlice = {
showToast: false,
caseType: '',
},
shouldHideTabBar: false,
};
const getCaseListComponents = (casesList: ICaseItem[], caseDetails: Record<string, CaseDetail>) => {
@@ -588,6 +590,9 @@ const allCasesSlice = createSlice({
setFilteredListToast: (state, action) => {
state.filteredListToast = action.payload;
},
setShouldHideTabBar: (state, action) => {
state.shouldHideTabBar = action.payload;
}
},
});
@@ -611,6 +616,7 @@ export const {
setCasesImageUri,
setSelectedCaseId,
setFilteredListToast,
setShouldHideTabBar
} = allCasesSlice.actions;
export default allCasesSlice.reducer;

View File

@@ -0,0 +1,169 @@
import { createSlice } from '@reduxjs/toolkit';
import { GetTicketCreationPayload, ICsaFilter, RequestTicket, Summary } from '@screens/cosmosSupport/constant/types';
interface ICSASlice {
loading: boolean;
ticketCreationData: {
ticketCreationData:GetTicketCreationPayload;
isLoading: boolean;
},
caseLevelTickets: {
taskForMe: {
data: Array<unknown>;
loading: boolean;
},
taskForTele: {
data: Array<unknown>;
loading: boolean;
}
},
caseSummary: Summary,
currentViewRequestTicket: {
loading: boolean;
data: RequestTicket| null;
},
isSubmittingComment: boolean;
ticketCreationInProgress: boolean;
markingTicketAsResolved: boolean;
filters: ICsaFilter;
}
const initialState = {
loading: false,
ticketCreationData: {
isLoading: false,
ticketCreationData: {}
},
caseLevelTickets: {
taskForMe: {
data: [],
loading: false
},
taskForTele: {
data: [],
loading: false
}
},
caseSummary: {
requesterSummaryDetails: {
newTicketsCount: 0,
newUpdatesCount: 0
},
assigneeSummaryDetails: {
newTicketsCount: 0,
newUpdatesCount: 0
},
aggregatedSummary: {
newTicketsCount: 0,
newUpdatesCount: 0
}
},
currentViewRequestTicket: {
loading: false,
data: null
},
isSubmittingComment: false,
ticketCreationInProgress: false,
markingTicketAsResolved: false,
filters: {}
} as ICSASlice;
const CosmosSupport = createSlice({
name: 'config',
initialState,
reducers: {
setTicketCreationData: (state, action) => {
state.ticketCreationData.isLoading = action.payload;
},
setLoading: (state, action) => {
state.loading = action.payload;
},
setTickerCreationData: (state, action) => {
state.ticketCreationData.isLoading = action.payload;
},
setCaseLevelTicketsForMe: (state, action) => {
state.caseLevelTickets.taskForMe.data = action.payload;
},
setCaseLevelTicketsForMeLoading: (state, action) => {
state.caseLevelTickets.taskForMe.loading = action.payload;
},
setCaseLevelTicketsForTele: (state, action) => {
state.caseLevelTickets.taskForTele.data = action.payload;
},
setCaseLevelTicketsForTeleLoading: (state, action) => {
state.caseLevelTickets.taskForTele.loading = action.payload;
},
setCaseSummary: (state, action) => {
state.caseSummary = action.payload;
},
setSingleViewRequestTicket: (state, action) => {
state.currentViewRequestTicket = {
data: action.payload,
loading: false
};
},
resetSingleViewRequestTicket: (state, action) => {
state.currentViewRequestTicket.data = null;
},
loadingSingleTicket: (state, action) => {
state.currentViewRequestTicket.loading = action.payload;
},
updateComment: (state, action) => {
if (state.currentViewRequestTicket.data) {
state.currentViewRequestTicket.data.activityLogs.unshift(action.payload);
}
},
setSubmittingComment: (state, action) => {
state.isSubmittingComment = action.payload;
},
setMarkingTicketAsResolved: (state, action) => {
state.markingTicketAsResolved = action.payload;
},
setTicketCreationDataForFrom: (state, action) => {
state.ticketCreationData.ticketCreationData = action.payload;
},
setCsaFilters: (state, action) => {
state.filters = action.payload;
},
cleanCaseLevelData: (state, action) => {
state.caseLevelTickets = {
taskForMe: {
data: [],
loading: false
},
taskForTele: {
data: [],
loading: false
}
}
},
resetConfig: () => initialState,
},
});
export const {
setTicketCreationData,
setLoading,
setCaseLevelTicketsForMe,
setCaseLevelTicketsForMeLoading,
setCaseLevelTicketsForTele,
setCaseLevelTicketsForTeleLoading,
resetConfig,
cleanCaseLevelData,
setCaseSummary,
setSingleViewRequestTicket,
resetSingleViewRequestTicket,
loadingSingleTicket,
setSubmittingComment,
updateComment,
setTicketCreationDataForFrom,
setMarkingTicketAsResolved,
setCsaFilters
} = CosmosSupport.actions;
export default CosmosSupport.reducer;

View File

@@ -70,6 +70,9 @@ export interface IUserSlice extends IUser {
attendanceDate: string;
};
isAgentPerformanceDashboardVisible: boolean;
featureFlags: {
csaCoOrdinationModuleFeatureFlag : boolean;
}
}
const initialState: IUserSlice = {
@@ -92,6 +95,9 @@ const initialState: IUserSlice = {
attendanceDate: '',
},
isAgentPerformanceDashboardVisible: false,
featureFlags: {
csaCoOrdinationModuleFeatureFlag: false
}
};
export const userSlice = createSlice({
@@ -146,6 +152,9 @@ export const userSlice = createSlice({
},
setIsAgentPerformanceDashboardVisible: (state, action) => {
state.isAgentPerformanceDashboardVisible = action.payload;
},
setFeatureFlag: (state, action) => {
state.featureFlags = action.payload;
}
},
});
@@ -159,7 +168,8 @@ export const {
setIsExternalAgent,
setCaseSyncLock,
setAgentAttendance,
setIsAgentPerformanceDashboardVisible
setIsAgentPerformanceDashboardVisible,
setFeatureFlag
} = userSlice.actions;
export default userSlice.reducer;

View File

@@ -0,0 +1,31 @@
import { useAppDispatch, useAppSelector } from '@hooks'
import { useIsFocused } from '@react-navigation/native'
import Tag, { TagVariant } from '@rn-ui-lib/components/Tag'
import { GenericStyles } from '@rn-ui-lib/styles'
import { getSummary } from '@screens/cosmosSupport/actions'
import { ICount } from '@screens/cosmosSupport/constant/types'
import React, { useEffect, useState } from 'react'
const CountComponent = () => {
const isFocused = useIsFocused()
const summary = useAppSelector((state) => state.cosmosSupport.caseSummary)
const dispatch = useAppDispatch()
useEffect(() => {
dispatch(getSummary({loanAccountNumber: undefined}))
}, [isFocused])
const newCount = summary?.aggregatedSummary?.newTicketUpdatesCount;
if(!newCount) return null;
return (
<Tag style={GenericStyles.mr8} variant={TagVariant.blue} text={`${newCount} New`} />
)
}
export default CountComponent

View File

@@ -0,0 +1,65 @@
import { logout } from '@actions/authActions';
import CSAIcon from '@assets/icons/CSAIcon';
import CompletedCaseIcon from '@assets/icons/CompletedCaseIcon';
import { CLICKSTREAM_EVENT_NAMES } from '@common/Constants';
import { navigateToScreen } from '@components/utlis/navigationUtlis';
import { MY_CASE_ITEM } from '@reducers/userSlice';
import LogoutIcon from '@rn-ui-lib/icons/LogoutIcon';
import { addClickstreamEvent } from '@services/clickstreamEventService';
import store from '@store';
import { Alert } from 'react-native';
import { ProfileScreenStackEnum } from '../ProfileStack';
import CountComponent from '../CountComponent';
export const getNavigationLinks = () => {
const { isTeamLead, selectedAgent, featureFlags } = store.getState().user;
return [
{
name: 'Completed cases',
icon: CompletedCaseIcon,
isVisible: !isTeamLead || selectedAgent?.referenceId === MY_CASE_ITEM.referenceId,
onPress: () => navigateToScreen('completedCases'),
},
{
name: 'Tele support',
icon: CSAIcon,
isVisible: featureFlags?.csaCoOrdinationModuleFeatureFlag,
onPress: () =>
navigateToScreen(ProfileScreenStackEnum.TELE_SUPPORT, {
from: 'profile',
}),
isNew: true,
NewComponent: CountComponent,
},
{
name: 'Logout',
icon: LogoutIcon,
isVisible: true,
onPress: () => {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.AV_PROFILE_PAGE_LOGOUT_BUTTON_CLICKED);
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.AV_PROFILE_PAGE_LOGOUT_CONFIRMATION_OPEN);
Alert.alert('Confirm', 'Are you sure you want to logout? ', [
{
text: 'Cancel',
style: 'cancel',
onPress: () => {
addClickstreamEvent(
CLICKSTREAM_EVENT_NAMES.AV_PROFILE_PAGE_LOGOUT_CONFIRMATION_CLOSED
);
},
},
{
text: 'Logout',
onPress: () => {
addClickstreamEvent(
CLICKSTREAM_EVENT_NAMES.AV_PROFILE_PAGE_LOGOUT_CONFIRMATION_CLICKED
);
store.dispatch(logout());
},
style: 'destructive',
},
]);
},
},
];
};

View File

@@ -0,0 +1,64 @@
import { COLORS } from '@rn-ui-lib/colors';
import Chevron from '@rn-ui-lib/icons/Chevron';
import { IconProps } from '@rn-ui-lib/icons/types';
import { GenericStyles } from '@rn-ui-lib/styles';
import React, { useState } from 'react';
import { StyleSheet, Text, TouchableHighlight, View } from 'react-native';
interface ProfileButtonProps {
onPress: () => void;
name: string;
Icon: React.FC<IconProps>;
tag: string;
NewComponent?: React.FC;
};
const ProfileButton: React.FC<ProfileButtonProps> = (props) => {
const { onPress, name, Icon, NewComponent } = props;
const [isPressed, setIsPressed] = useState(false);
const handleOnPress = () => {
setIsPressed(true);
};
const handleOnPressOut = () => {
setIsPressed(false);
};
return (
<TouchableHighlight
underlayColor={COLORS.BACKGROUND.BLUE}
onPressIn={handleOnPress}
onPress={onPress}
onPressOut={handleOnPressOut}
style={[
GenericStyles.pv12,
GenericStyles.ph12,
GenericStyles.br4
]}
>
<View style={[
GenericStyles.row,
GenericStyles.alignCenter,
GenericStyles.justifyContentSpaceBetween
]}>
<View
style={[
GenericStyles.row,
GenericStyles.alignCenter,
]}
>
<Icon size={16} fillColor={isPressed ? COLORS.TEXT.BLUE : COLORS.TEXT.LIGHT} />
<Text style={GenericStyles.ml4}>{name}</Text>
</View>
<View style={[GenericStyles.row, GenericStyles.alignCenter]}>
{NewComponent ? <NewComponent /> : null}
<Chevron fillColor={isPressed ? COLORS.TEXT.BLUE : COLORS.TEXT.LIGHT} />
</View>
</View>
</TouchableHighlight>
)
}
export default ProfileButton;

View File

@@ -0,0 +1,61 @@
import { getScreenFocusListenerObj } from '@components/utlis/commonFunctions';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import CompletedCase from '@screens/allCases/CompletedCase';
import { DEFAULT_SCREEN_OPTIONS } from '@screens/auth/ProtectedRouter';
import { RequestDetail, ViewRequestHistory } from '@screens/cosmosSupport';
import React from 'react';
import { StyleSheet } from 'react-native';
import Profile from '.';
const Stack = createNativeStackNavigator();
export enum ProfileScreenStackEnum {
PROFILE = 'profile',
COMPLETED_CASES = 'completedCases',
TELE_SUPPORT = 'teleSupport',
PROFILE_TICKET_DETAIL = 'profileTicketDetail',
}
const ProfileStack = () => {
return (
<Stack.Navigator
screenListeners={getScreenFocusListenerObj}
screenOptions={
DEFAULT_SCREEN_OPTIONS
}
initialRouteName={ProfileScreenStackEnum.PROFILE}
>
<Stack.Screen
name={ProfileScreenStackEnum.PROFILE}
component={Profile}
/>
<Stack.Screen
name={ProfileScreenStackEnum.COMPLETED_CASES}
component={CompletedCase}
/>
<Stack.Group
navigationKey={ProfileScreenStackEnum.TELE_SUPPORT}
>
<Stack.Screen
name={ProfileScreenStackEnum.TELE_SUPPORT}
component={ViewRequestHistory}
/>
<Stack.Screen
name={ProfileScreenStackEnum.PROFILE_TICKET_DETAIL}
component={RequestDetail}
/>
</Stack.Group>
</Stack.Navigator>
)
}
const styles = StyleSheet.create({})
export default ProfileStack

View File

@@ -10,7 +10,6 @@ import {
} from 'react-native';
import NavigationHeader from '../../../RN-UI-LIB/src/components/NavigationHeader';
import Text from '../../../RN-UI-LIB/src/components/Text';
import LogoutIcon from '../../../RN-UI-LIB/src/Icons/LogoutIcon';
import { addClickstreamEvent } from '../../services/clickstreamEventService';
import {
BUTTON_PRESS_COUNT_FOR_IMPERSONATION,
@@ -21,14 +20,10 @@ import { logout } from '../../action/authActions';
import { useAppDispatch, useAppSelector } from '../../hooks';
import { RootState } from '../../store/store';
import { COLORS } from '../../../RN-UI-LIB/src/styles/colors';
import Button from '../../../RN-UI-LIB/src/components/Button';
import { navigateToScreen } from '../../components/utlis/navigationUtlis';
import GroupIcon from '../../../RN-UI-LIB/src/Icons/GroupIcon';
import { getAppVersion, getBuildVersion } from '../../components/utlis/commonFunctions';
import VersionNumber from 'react-native-version-number';
import { useFocusEffect } from '@react-navigation/native';
import { CaseDetail } from '../caseDetails/interface';
import CaseItem from '../allCases/CaseItem';
import { useFocusEffect, useIsFocused } from '@react-navigation/native';
import { IUserRole, MY_CASE_ITEM } from '../../reducer/userSlice';
import QuestionMarkIcon from '../../assets/icons/QuestionMarkIcon';
import IDCardImageCapture from './IDCardImageCapture';
@@ -39,9 +34,12 @@ import FloatingBannerCta from '@common/FloatingBannerCta';
import AttendanceIcon from '@assets/icons/AttendanceIcon';
import GoogleFormModal from '@screens/allCases/GoogleFormModal';
import { PageRouteEnum } from '@screens/auth/ProtectedRouter';
import ProfileButton from './ProfileButton';
import { getNavigationLinks } from './Navigation/constants';
const Profile: React.FC = () => {
const [buttonPressedCount, setButtonPressedCount] = useState(0);
const focused = useIsFocused();
const dispatch = useAppDispatch();
const {
originalImageUri,
@@ -126,13 +124,10 @@ const Profile: React.FC = () => {
const helpButtonClickHandler = () => Linking.openURL(supportLink);
const hideUploadImageBtn =
approvalStatus === ImageApprovalStatus.PENDING ||
approvalStatus === ImageApprovalStatus.APPROVED;
const showCompletedCases = !isTeamLead || selectedAgent?.referenceId === MY_CASE_ITEM.referenceId;
const [showForm, setShowForm] = useState(false);
@@ -155,16 +150,16 @@ const Profile: React.FC = () => {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_ID_CARD_CLICKED);
navigateToScreen(PageRouteEnum.AGENT_ID_CARD);
}}
style={styles.bottomActionable}
style={styles.bottomActionable}
>
<View style={[GenericStyles.row, GenericStyles.alignStart]}>
<Text small style={styles.whiteText}>
View ID card
</Text>
<View style={{ transform: [{ rotate: '180deg' }] }}>
<ArrowSolidIcon fillColor={COLORS.TEXT.WHITE} />
</View>
<View style={[GenericStyles.row, GenericStyles.alignStart]}>
<Text small style={styles.whiteText}>
View ID card
</Text>
<View style={{ transform: [{ rotate: '180deg' }] }}>
<ArrowSolidIcon fillColor={COLORS.TEXT.WHITE} />
</View>
</View>
</Pressable>
) : null
}
@@ -185,70 +180,34 @@ const Profile: React.FC = () => {
)
}
/>
<ScrollView style={GenericStyles.fill}>
{showCompletedCases ? (
<View
style={[
GenericStyles.ph16,
GenericStyles.pt16,
numberOfCompletedCases === 2 ? { paddingBottom: 6 } : {},
]}
>
{hideUploadImageBtn ? null : <IDCardImageCapture />}
<View
style={[
GenericStyles.row,
GenericStyles.alignCenter,
numberOfCompletedCases ? { paddingBottom: 12 } : GenericStyles.pb16,
]}
>
<View style={[GenericStyles.ml4, GenericStyles.mr8]}>
<GroupIcon />
</View>
<Text>Completed cases ({numberOfCompletedCases})</Text>
</View>
{numberOfCompletedCases
? completeCasesList.slice(0, 2).map((caseItem) => {
const caseDetailItem = caseDetails[caseItem.caseReferenceId] as CaseDetail;
return (
<CaseItem
key={caseItem.caseReferenceId}
caseDetailObj={caseDetailItem}
isCompleted={true}
/>
);
})
: null}
{numberOfCompletedCases > 2 ? (
<Button
title="View all completed cases"
variant="primaryText"
style={[
GenericStyles.w100,
GenericStyles.br8,
GenericStyles.mt6,
GenericStyles.mb12,
GenericStyles.whiteBackground,
]}
onPress={handleViewAllCases}
<ScrollView
contentContainerStyle={[
GenericStyles.p16,
GenericStyles.whiteBackground,
GenericStyles.fill,
]}
>
{hideUploadImageBtn ? null : <IDCardImageCapture />}
{getNavigationLinks()?.map((link, index) => {
if (!link.isVisible) return null;
return (
<View style={{ marginBottom: 10 }}>
<ProfileButton
key={index}
onPress={link.onPress}
name={link.name}
Icon={link.icon}
tag={'tag'}
NewComponent={link.NewComponent}
/>
) : null}
</View>
) : null}
<View style={[styles.logoutContainer, GenericStyles.whiteBackground]}>
<TouchableOpacity
onPress={handleLogout}
style={[GenericStyles.row, GenericStyles.centerAligned, GenericStyles.fill]}
>
<LogoutIcon />
<Text style={styles.logoutText}>Logout</Text>
</TouchableOpacity>
</View>
</View>
);
})}
<Pressable
style={[
GenericStyles.row,
GenericStyles.centerAligned,
GenericStyles.fill,
GenericStyles.w100,
GenericStyles.mt8,
]}
@@ -274,8 +233,6 @@ const Profile: React.FC = () => {
/>
)}
<GoogleFormModal showForm={showForm} setShowForm={setShowForm} />
</View>
);
};

View File

@@ -1,15 +1,15 @@
import { setShouldHideTabBar } from '@reducers/allCasesSlice';
import React, { useEffect } from 'react';
import { StyleSheet, View, VirtualizedList } from 'react-native';
import React from 'react';
import { ICaseItem } from './interface';
import { COLORS } from '../../../RN-UI-LIB/src/styles/colors';
import { EmptyListMessages } from './constants';
import { GenericStyles } from '../../../RN-UI-LIB/src/styles';
import EmptyList from './EmptyList';
import NavigationHeader from '../../../RN-UI-LIB/src/components/NavigationHeader';
import { GenericStyles } from '../../../RN-UI-LIB/src/styles';
import { COLORS } from '../../../RN-UI-LIB/src/styles/colors';
import { goBack } from '../../components/utlis/navigationUtlis';
import { useAppSelector } from '../../hooks';
import CaseItem from './CaseItem';
import { useAppDispatch, useAppSelector } from '../../hooks';
import { CaseDetail } from '../caseDetails/interface';
import CaseItem from './CaseItem';
import EmptyList from './EmptyList';
import { ICaseItem } from './interface';
const getItem = (item: Array<ICaseItem>, index: number) => item[index];
@@ -22,6 +22,15 @@ const CompletedCase: React.FC = () => {
(state) => state.allCases
);
const dispatch = useAppDispatch();
useEffect(() => {
dispatch(setShouldHideTabBar(true))
return () => {
dispatch(setShouldHideTabBar(false))
}
}, [])
return (
<View style={[GenericStyles.fill, styles.container]}>
<NavigationHeader

View File

@@ -68,6 +68,7 @@ const Filters: React.FC<IFilters> = ({
onChangeText={handleSearchChange}
placeholder={`Search in ${getTextInputPlaceholder()}`}
defaultValue={searchQuery}
value={searchQuery}
testID="test_search"
showClearIcon
/>

View File

@@ -88,6 +88,7 @@ export const ToastMessages = {
FILE_DOWNLOAD_SUCCESS: 'File downloaded successfully',
FILE_DOWNLOAD_FAILURE: 'File download failed',
COMMITMENT_SUBMITTED_SUCCESSFULLY: 'Commitment submitted successfully',
SUCCESS_COPYING_ADDRESS: 'Address copied successfully',
};
export enum BOTTOM_TAB_ROUTES {

View File

@@ -15,6 +15,7 @@ import {
resetSelectedTodoList,
resetTodoList,
setLoading,
setShouldHideTabBar,
setVisitPlansUpdating,
} from '@reducers/allCasesSlice';
import { addClickstreamEvent } from '@services/clickstreamEventService';
@@ -24,7 +25,7 @@ import FullScreenLoaderWrapper from '@common/FullScreenLoaderWrapper';
import DashboardIcon from '../../assets/icons/DashboardIcon';
import DashBoardScreens from '../Dashboard/DashBoardScreens';
import { isAgentDashboardVisible } from '@screens/Dashboard/utils';
import { AppState, AppStateStatus, StyleSheet } from 'react-native';
import { AppState, AppStateStatus, StyleSheet, View } from 'react-native';
import { COLORS } from '@rn-ui-lib/colors';
import DailyCommitmentIcon from '@rn-ui-lib/icons/DailyCommitmentIcon';
import DailyCommitmentBottomSheet from '../dailyCommitment/DailyCommitmentBottomSheet';
@@ -32,9 +33,11 @@ import { getVisibilityStatus } from '@screens/dailyCommitment/actions';
import { logError } from '@components/utlis/errorUtils';
import FloatingBannerCta from '@common/FloatingBannerCta';
import { AppStates } from '@interfaces/appStates';
import ProfileStack from '@screens/Profile/ProfileStack';
import { GenericStyles } from '@rn-ui-lib/styles';
const AllCasesMain = () => {
const { pendingList, pinnedList, completedList, loading } = useAppSelector(
const { pendingList, pinnedList, completedList, loading, shouldHideTabBar } = useAppSelector(
(state) => state.allCases
);
const userState = useAppSelector((state: RootState) => state.user);
@@ -78,7 +81,7 @@ const AllCasesMain = () => {
}, []);
const shouldShowBanner = useMemo(() => {
return !isCommitmentSubmitted && isCommitmentFormVisible;
return !isCommitmentSubmitted && isCommitmentFormVisible;
}, [isCommitmentSubmitted, isCommitmentFormVisible]);
@@ -157,18 +160,20 @@ const AllCasesMain = () => {
bottomSheetScreens.push({
name: BOTTOM_TAB_ROUTES.Profile,
component: () => (
<>
<Profile />
{shouldShowBanner ? <FloatingBannerCta
title={"Update your daily commitment"}
onPressHandler={openCommitmentScreen}
containerStyle={styles.container}
icon={<DailyCommitmentIcon />}
textStyle={styles.titleText}
/> : null}
</>
),
component: () => {
return (
<>
<ProfileStack />
{shouldShowBanner && !shouldHideTabBar ? <FloatingBannerCta
title={"Update your daily commitment"}
onPressHandler={openCommitmentScreen}
containerStyle={styles.container}
icon={<DailyCommitmentIcon />}
textStyle={styles.titleText}
/> : null}
</>
)
},
icon: ProfileIcon,
});
return bottomSheetScreens;
@@ -178,6 +183,7 @@ const AllCasesMain = () => {
isTeamLead,
showAgentDashboard,
shouldShowBanner,
shouldHideTabBar
]);
const onTabPressHandler = (e: any) => {
@@ -198,6 +204,7 @@ const AllCasesMain = () => {
dispatch(setLoading(false));
dispatch(resetTodoList());
dispatch(resetSelectedTodoList());
dispatch(setShouldHideTabBar(false))
}, []);
return (
@@ -207,12 +214,13 @@ const AllCasesMain = () => {
screens={HOME_SCREENS}
initialRoute={isTeamLead ? BOTTOM_TAB_ROUTES.Cases : BOTTOM_TAB_ROUTES.VisitPlan}
onTabPress={(e) => onTabPressHandler(e)}
shouldHideTabBar={shouldHideTabBar}
/>
<CasesActionButtons />
{!isCommitmentSubmitted && isCommitmentFormVisible && <DailyCommitmentBottomSheet
openBottomSheet={openBottomSheet}
setOpenBottomSheet={setOpenBottomSheet}
onSuccessCallback={() => { getVisibility();}}
onSuccessCallback={() => { getVisibility(); }}
/>}
</Layout>
);

View File

@@ -4,7 +4,7 @@ import { getUniqueId, isTablet } from 'react-native-device-info';
import { useAppDispatch, useAppSelector } from '../../hooks';
import { type RootState } from '../../store/store';
import { setAgentAttendance, setDeviceId } from '../../reducer/userSlice';
import { DEVICE_TYPE_ENUM, setGlobalUserData } from '../../constants/Global';
import { DEVICE_TYPE_ENUM, GLOBAL, setGlobalUserData } from '../../constants/Global';
import { registerNavigateAndDispatch } from '../../components/utlis/apiHelper';
import ProtectedRouter from './ProtectedRouter';
import useNativeButtons from '../../hooks/useNativeButton';
@@ -20,9 +20,11 @@ import {
alfredSetPhoneNumber,
alfredSetUserId,
} from '../../components/utlis/DeviceUtils';
import { getAppVersion } from '../../components/utlis/commonFunctions';
import { getAppVersion, setAsyncStorageItem } from '../../components/utlis/commonFunctions';
import useScreenshotTracking from '../../hooks/useScreenshotTracking';
import { getSyncTime } from '@hooks/capturingApi';
import getLitmusExperimentResult, { LitmusExperimentName, LitmusExperimentNameMap } from '@services/litmusExperiments.service';
import { LocalStorageKeys } from '@common/Constants';
function AuthRouter() {
const dispatch = useAppDispatch();
@@ -116,6 +118,9 @@ function AuthRouter() {
CosmosForegroundService.stopAll();
}
});
getLitmusExperimentResult(LitmusExperimentNameMap[LitmusExperimentName.COSMOS_IMAGE_SYNC], { 'x-customer-id': GLOBAL.AGENT_ID }).then((response) => {
setAsyncStorageItem(LocalStorageKeys.IS_IMAGE_SYNC_ALLOWED, response);
});
}
}, [isLoggedIn]);

View File

@@ -11,7 +11,6 @@ import { useAppDispatch, useAppSelector } from '../../hooks';
import useFirestoreUpdates from '../../hooks/useFirestoreUpdates';
import useIsOnline from '../../hooks/useIsOnline';
import AllCasesMain from '../allCases';
import CompletedCase from '../allCases/CompletedCase';
import interactionsHandler from '../caseDetails/interactionsHandler';
import ImpersonatedUser from '../impersonatedUser';
import Notifications from '../notifications';
@@ -116,7 +115,6 @@ const ProtectedRouter = () => {
}}
/>
<Stack.Screen name={PageRouteEnum.TODO_LIST} component={TodoList} />
<Stack.Screen name={PageRouteEnum.COMPLETED_CASES} component={CompletedCase} />
<Stack.Screen name={PageRouteEnum.NOTIFICATIONS} component={Notifications} />
<Stack.Screen name={PageRouteEnum.IMPERSONATED_LOGIN} component={ImpersonatedUser} />
<Stack.Screen name={PageRouteEnum.AGENT_ID_CARD} component={AgentIdCard} />

View File

@@ -1,19 +1,52 @@
import React from 'react';
import NavigationHeader from '../../../RN-UI-LIB/src/components/NavigationHeader';
import { goBack } from '../../components/utlis/navigationUtlis';
import { goBack, navigateToScreen } from '../../components/utlis/navigationUtlis';
import { CaseDetail } from './interface';
import { View } from 'react-native';
import { TouchableHighlight, View } from 'react-native';
import NotificationMenu from '../../components/notificationMenu';
import { GenericStyles } from '../../../RN-UI-LIB/src/styles';
import { CaseDetailStackEnum } from './CaseDetailStack';
import { useAppDispatch, useAppSelector } from '@hooks';
import { cleanCaseLevelData } from '@reducers/cosmosSupportSlice';
import CSAIconButton from '@assets/icons/CSAIconButton';
import { COLORS } from '@rn-ui-lib/colors';
import { addClickstreamEvent } from '@services/clickstreamEventService';
import { CLICKSTREAM_EVENT_NAMES } from '@common/Constants';
import { CASE_DETAIL_SCREEN } from '@screens/cosmosSupport/constant';
const CaseDetailHeader: React.FC<{ caseDetail: CaseDetail }> = (props) => {
const dispatch = useAppDispatch();
const { csaCoOrdinationModuleFeatureFlag } = useAppSelector((state) => ({
csaCoOrdinationModuleFeatureFlag: state?.user?.featureFlags?.csaCoOrdinationModuleFeatureFlag,
}));
const handleOpenRequestHistory = () => {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_VIEW_TASK_HISTORY_CLICKED, {
caseId: props.caseDetail.id,
});
dispatch(cleanCaseLevelData(null));
navigateToScreen(CaseDetailStackEnum.VIEW_RequestHistory, {
caseDetail: props.caseDetail,
from: CASE_DETAIL_SCREEN,
});
};
return (
<View style={{ position: 'relative' }}>
<NavigationHeader
title={''}
onBack={goBack}
rightActionable={
<View style={GenericStyles.pr12}>
<View style={[GenericStyles.pr12, GenericStyles.row, GenericStyles.alignCenter]}>
{csaCoOrdinationModuleFeatureFlag ? (
<TouchableHighlight
underlayColor={COLORS.HIGHLIGHTER.LIGHT_BUTTON}
activeOpacity={1}
style={GenericStyles.iconContainerButton}
onPress={handleOpenRequestHistory}
>
<CSAIconButton fillColor={COLORS.BACKGROUND.PRIMARY} />
</TouchableHighlight>
) : null}
<NotificationMenu />
</View>
}

View File

@@ -20,6 +20,9 @@ import { RootState } from '@store';
import { _map } from '@rn-ui-lib/utils/common';
import EmiSchedule from '@screens/emiSchedule';
import AddNewNumber from '@screens/addNewNumber';
import ViewRequestHistory from '@screens/cosmosSupport/ViewRequestHistory';
import { RequestDetail } from '@screens/cosmosSupport';
import RequestSupport from '@screens/cosmosSupport/RequestSupport';
import SimilarGeolocations from '@screens/addressGeolocation/SimilarGeolocations';
import AdditionalGeolocations from '@screens/addressGeolocation/AdditionalGeolocations';
@@ -39,6 +42,9 @@ export enum CaseDetailStackEnum {
PAST_FEEDBACK_DETAIL = 'pastFeedbackDetail',
EMI_SCHEDULE = 'EmiSchedule',
ADD_NEW_NUMBER = 'AddNewNumber',
VIEW_RequestHistory = 'viewRequestHistory',
VIEW_REQUEST_DETAIL = 'viewRequestDetail',
RAISE_REQUEST = 'raiseRequest',
SIMILAR_GEOLOCATIONS = 'SimilarGeolocations',
ADDITIONAL_GEOLOCATIONS = 'AdditionalGeolocations',
}
@@ -84,6 +90,15 @@ const CaseDetailStack = () => {
name={CaseDetailStackEnum.PAST_FEEDBACK_DETAIL}
component={FeedbackDetailContainer}
/>
<Stack.Screen
name={CaseDetailStackEnum.VIEW_RequestHistory}
component={ViewRequestHistory}
/>
<Stack.Screen
name={CaseDetailStackEnum.VIEW_REQUEST_DETAIL}
component={RequestDetail}
/>
<Stack.Screen name={CaseDetailStackEnum.RAISE_REQUEST} component={RequestSupport} />
{_map(collectionTemplate?.widget, (key) => (
<Stack.Screen
key={getTemplateRoute(key, CaseAllocationType.COLLECTION_CASE)}

View File

@@ -115,7 +115,7 @@ const CollectionCaseDetails: React.FC<ICaseDetails> = (props) => {
(state) => state.case.caseForm?.[caseId]?.[TaskTitleUIMapping.COLLECTION_FEEDBACK]
);
const { addressString, phoneNumbers, loanAccountNumber, totalOverdueAmount, pos } = caseDetail;
const { addressString, phoneNumbers, loanAccountNumber, totalOverdueAmount, pos } = caseDetail || {};
const feedbackList: IFeedback[] = useAppSelector(
(state: RootState) => state.feedbackHistory?.[loanAccountNumber as string]?.data || []

View File

@@ -0,0 +1,187 @@
import { Modal, StyleSheet, Switch, View } from 'react-native';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { GenericStyles, getShadowStyle } from '@rn-ui-lib/styles';
import { COLORS } from '@rn-ui-lib/colors';
import { IFilterStatus } from './constant/types';
import Text from '@rn-ui-lib/components/Text';
import TextInput from '@rn-ui-lib/components/TextInput';
import IconButton from '@rn-ui-lib/components/IconButton';
import FilterIcon from '@assets/icons/FilterIcon';
import SearchIcon from '@rn-ui-lib/icons/SearchIcon';
import Filters, { TFilterOptions } from '@components/filters/Filters';
import { debounce } from '@components/utlis/commonFunctions';
import { addClickstreamEvent } from '@services/clickstreamEventService';
import { CLICKSTREAM_EVENT_NAMES } from '@common/Constants';
import { getFiltersCount } from './constant';
interface ICSAFilters {
filtersTemplate: any;
onFilterChange: (data: IFilterStatus) => void;
isFocusedTab?: boolean;
isTasksPresent?: boolean;
}
const SEARCH_BAR_DEBOUNCE_INTERVAL = 300;
const CSAFilters: React.FC<ICSAFilters> = (props) => {
const { filtersTemplate, onFilterChange, isFocusedTab, isTasksPresent } = props;
const [showFilterModal, setShowFilterModal] = useState(false);
const [selectedFilter, setSelectedFilter] = useState<TFilterOptions>({});
const [viewUnread, setViewUnread] = useState(false);
const [searchQuery, setSearchQuery] = React.useState<string>('');
const feedbackFilterCount = getFiltersCount(selectedFilter);
const unreadThumbColor = useRef(COLORS.TEXT.LIGHT);
const toggleFilterModal = () => {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_TASKS_FILTER_BUTTON_CLICKED,{open: !showFilterModal})
setShowFilterModal((prev) => !prev)
};
const handleFilterSelection = (selectedFilters: TFilterOptions) => {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_TASKS_FILTER_APPLIED,selectedFilter)
setSelectedFilter(selectedFilters);
};
const handleViewUnread = () => {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_TASKS_UNREAD_TOGGLE_CLICKED, {filterType:'unread', showUnread: !viewUnread})
onFilterChange({ filter: selectedFilter, showUnread: !viewUnread });
setViewUnread((prev) => {
unreadThumbColor.current = prev ? COLORS.TEXT.LIGHT : COLORS.TEXT.BLUE;
return !prev;
});
};
const handleSearchChange = useCallback(
debounce((query: string) => {
setSearchQuery(query);
}, SEARCH_BAR_DEBOUNCE_INTERVAL),
[]
);
const handleFilterChange = () => {
const searchQueryFilters: TFilterOptions = {};
if (searchQuery) {
if(/\d/.test(searchQuery)){
searchQueryFilters.LOAN_ACCOUNT_NUMBERS = {
filters: {
[searchQuery]: true,
},
selectedFilterCount: 1,
};
}else{
searchQueryFilters.CUSTOMER_NAMES = {
filters: {
[searchQuery]: true,
},
selectedFilterCount: 1,
};
}
}
onFilterChange({ filter: { ...selectedFilter, ...searchQueryFilters }, showUnread: viewUnread });
}
useEffect(() => {
handleFilterChange();
}, [searchQuery, selectedFilter]);
useEffect(() => {
if(!isFocusedTab && !isTasksPresent) {
setSearchQuery('');
}
}, [isFocusedTab])
return (
<>
<View
style={[
GenericStyles.p16,
GenericStyles.row,
GenericStyles.justifyContentSpaceBetween,
GenericStyles.alignCenter,
{ ...getShadowStyle(1) },
GenericStyles.whiteBackground,
]}
>
<TextInput
containerStyle={GenericStyles.fill}
placeholder={'Search by LAN or Customer'}
LeftComponent={<SearchIcon />}
placeholderTextColor={COLORS.TEXT.LIGHT}
inputContainerStyle={styles.searchbar}
value={searchQuery}
onChangeText={handleSearchChange}
showClearIcon
/>
<View>
<IconButton
style={[GenericStyles.ml8, { backgroundColor: COLORS.BACKGROUND.PRIMARY }]}
testID="filter-btn"
icon={
<FilterIcon
fillColor={feedbackFilterCount ? COLORS.TEXT.BLUE : COLORS.BACKGROUND.LIGHT}
/>
}
onPress={toggleFilterModal}
/>
{feedbackFilterCount ? (
<View style={[styles.filterCount]}>
<Text style={[GenericStyles.whiteText, { marginTop: -3 }]} small bold>
{feedbackFilterCount}
</Text>
</View>
) : null}
</View>
</View>
<View
style={[
GenericStyles.row,
GenericStyles.alignCenter,
{ backgroundColor: COLORS.BACKGROUND.BLUE_LIGHT_3 },
GenericStyles.p8,
]}
>
<Switch
onValueChange={handleViewUnread}
value={viewUnread}
trackColor={{ true: COLORS.BACKGROUND.BLUE_LIGHT_4, false: COLORS.BORDER.PRIMARY }}
thumbColor={COLORS.TEXT.WHITE}
/>
<Text> Show unread</Text>
</View>
<Modal
animationType="slide"
animated
onRequestClose={toggleFilterModal}
visible={showFilterModal}
>
<Filters
header="Filters"
defaultSelectedFilters={selectedFilter}
closeFilterModal={toggleFilterModal}
filters={filtersTemplate}
onFilterChange={handleFilterSelection}
/>
</Modal>
</>
);
};
const styles = StyleSheet.create({
searchbar: {
height: 48,
},
filterCount: {
backgroundColor: COLORS.TEXT.BLUE,
width: 18,
height: 18,
borderRadius: 9,
alignItems: 'center',
position: 'absolute',
opacity: 0.8,
top: -4,
right: -4,
},
});
export default CSAFilters;

View File

@@ -0,0 +1,284 @@
import { Keyboard, Pressable, ScrollView, StyleSheet, TouchableWithoutFeedback, TouchableWithoutFeedbackComponent, View } from 'react-native';
import React, { useEffect } from 'react';
import { GenericStyles } from '@rn-ui-lib/styles';
import Heading from '@rn-ui-lib/components/Heading';
import Text from '@rn-ui-lib/components/Text';
import { formatAmount } from '@rn-ui-lib/utils/amount';
import Tag from '@rn-ui-lib/components/Tag';
import SuspenseLoader from '@rn-ui-lib/components/suspense_loader/SuspenseLoader';
import LineLoader from '@rn-ui-lib/components/suspense_loader/LineLoader';
import ChevronDown from '@assets/icons/ChevronDown';
import BottomSheetWrapper from '@common/BottomSheetWrapper';
import TextInput from '@rn-ui-lib/components/TextInput';
import Button from '@rn-ui-lib/components/Button';
import { SafeAreaView } from 'react-native-safe-area-context';
import { updateTicket } from './actions';
import { Status, RequestorType } from './constant/types';
import { useAppDispatch, useAppSelector } from '@hooks';
import FullScreenLoaderWrapper from '@common/FullScreenLoaderWrapper';
import { Mapping, MAX_COMENT_LENGTH, StatusColorMapping } from './constant';
import { COLORS } from '@rn-ui-lib/colors';
import HeaderNode from './HeaderNode';
import CsaIncomingIconSquare from '@assets/icons/CsaIncomingIconSquare';
import CsaOutgoingRequestSquareIcon from '@assets/icons/CsaOutgoingRequestSquareIcon';
import { copyToClipboard } from '@components/utlis/commonFunctions';
import { toast } from '@rn-ui-lib/components/toast';
import { ToastMessages } from '@screens/allCases/constants';
import CopyIconFilled from '@assets/icons/CopyIconFilled';
import { addClickstreamEvent } from '@services/clickstreamEventService';
import { CLICKSTREAM_EVENT_NAMES } from '@common/Constants';
interface ICustomerCard {
customerName: string;
Dpd: number;
Pos: number;
status: string;
isRefreshing: boolean;
isEditable?: boolean;
refId?: string;
ticketStatusChange?: (id: string) => void;
statusReadable?: string;
requestType: string;
requestedByuserType: string;
address?: string;
}
const CustomerCard: React.FC<ICustomerCard> = (props) => {
const {
customerName,
Dpd,
Pos,
status,
isRefreshing,
isEditable,
statusReadable,
requestType,
requestedByuserType,
address,
} = props;
const Component = isEditable ? Pressable : View;
const [openBottomSheet, setOpenBottomSheet] = React.useState<boolean>(false);
const [comment, setComment] = React.useState<string>('');
const markingTicketAsResolved = useAppSelector(
(state) => state.cosmosSupport.markingTicketAsResolved
);
const handleOpenBottomSheet = () => {
if (status !== Mapping.CLOSED) setOpenBottomSheet((prev) => !prev);
};
const dispatch = useAppDispatch();
const [isKeyboardVisible, setKeyboardVisible] = React.useState(false);
useEffect(() => {
const keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', () => {
setKeyboardVisible(true);
});
const keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', () => {
setKeyboardVisible(false);
});
return () => {
keyboardDidShowListener.remove();
keyboardDidHideListener.remove();
};
}, []);
const handleCloseTicket = () => {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_TASK_MARKED_DONE, {
customerName,
requestType,
status,
Dpd,
Pos,
isRefreshing,
isEditable,
statusReadable,
requestedByuserType,
})
const payload = {
status: Status.CLOSED,
comment: comment.trim(),
supportRequestUserType: 'FE',
};
dispatch(updateTicket(payload, props?.refId!));
setOpenBottomSheet(false);
};
useEffect(() => {
if (!openBottomSheet) setComment('');
}, [openBottomSheet]);
const handleAddressCopy = () => {
if (!address) return;
copyToClipboard(address);
toast({
text1: ToastMessages.SUCCESS_COPYING_ADDRESS,
type: 'info',
});
};
return (
<View>
<SuspenseLoader fallBack={<LineLoader height={26} width={150} />} loading={isRefreshing}>
<View style={[GenericStyles.row, GenericStyles.alignStart]}>
{requestedByuserType === RequestorType.CSA ? (
<CsaIncomingIconSquare />
) : (
<CsaOutgoingRequestSquareIcon />
)}
<View style={GenericStyles.ml12}>
<Heading dark type="h4" numberOfLines={1}>
{requestType}
</Heading>
<Text>For {customerName} </Text>
</View>
</View>
</SuspenseLoader>
<View
style={[
GenericStyles.silverBackground,
GenericStyles.br8,
GenericStyles.p16,
GenericStyles.mt16,
]}
>
<View style={[GenericStyles.row, GenericStyles.justifyStart]}>
<Text bold dark style={[styles.flexBasis20, styles.black, GenericStyles.fontSize14]}>
<SuspenseLoader fallBack={<LineLoader height={20} width={50} />} loading={isRefreshing}>
{Dpd}
</SuspenseLoader>
</Text>
<Text dark style={[styles.flexBasis40, styles.black, GenericStyles.fontSize14]}>
<SuspenseLoader fallBack={<LineLoader height={20} width={50} />} loading={isRefreshing}>
{formatAmount(Pos)}
</SuspenseLoader>
</Text>
<Component
onPress={handleOpenBottomSheet}
style={[styles.flexBasis30, GenericStyles.row]}
>
<SuspenseLoader fallBack={<LineLoader height={18} width={50} />} loading={isRefreshing}>
<Tag text={statusReadable} variant={StatusColorMapping[status]} />
{isEditable && status !== Mapping.CLOSED ? <ChevronDown /> : null}
</SuspenseLoader>
</Component>
</View>
<View style={[GenericStyles.row, GenericStyles.justifyStart, GenericStyles.mt4]}>
<Text style={[styles.flexBasis20, styles.label, GenericStyles.fontSize14]}>DPD</Text>
<Text style={[styles.flexBasis40, styles.label, GenericStyles.fontSize14]}>
EMI overdue
</Text>
<Text style={[styles.flexBasis30, styles.label, GenericStyles.fontSize14]}>Status</Text>
</View>
</View>
{address ? (
<View style={[GenericStyles.borderTop, GenericStyles.pv16, GenericStyles.mt24]}>
<View style={[GenericStyles.row, GenericStyles.spaceBetween]}>
<Text bold dark>
Added address
</Text>
<Pressable style={[GenericStyles.centerAlignedRow]} onPress={handleAddressCopy}>
<CopyIconFilled style={[GenericStyles.mt2, GenericStyles.mr2]} />
<Text style={[GenericStyles.textBlue, GenericStyles.fontSize13]}>Copy</Text>
</Pressable>
</View>
<Text dark style={[GenericStyles.fontSize13, GenericStyles.mt10]}>
{address}
</Text>
</View>
) : null}
<BottomSheetWrapper
visible={openBottomSheet}
setVisible={handleOpenBottomSheet}
heightPercentage={40}
HeaderNode={HeaderNode}
moveForKeyboard={0.2}
>
<ScrollView
contentContainerStyle={[GenericStyles.fill]}
>
<SafeAreaView style={[GenericStyles.p16, GenericStyles.fill, styles.pt0]}>
<View style={[GenericStyles.fill]}>
<View>
<TextInput
numberOfLines={5}
style={styles.textInput}
multiline={true}
placeholder="Leave a comment (mandatory)"
onChangeText={setComment}
value={comment}
maxLength={300}
border={true}
/>
<Text
small
light
style={[styles.count, comment.length >= MAX_COMENT_LENGTH ? styles.error : {}]}
>
{comment.length}/{MAX_COMENT_LENGTH}
</Text>
</View>
</View>
{!isKeyboardVisible ? (
<View style={[GenericStyles.row, GenericStyles.justifyContentSpaceBetween]}>
<Button
title="Cancel"
variant="secondary"
style={styles.flexBasis48}
onPress={handleOpenBottomSheet}
/>
<Button
title="Mark as done"
variant="primary"
style={styles.flexBasis48}
onPress={handleCloseTicket}
disabled={!comment?.trim()}
/>
</View>
) : null}
</SafeAreaView>
</ScrollView>
</BottomSheetWrapper>
<FullScreenLoaderWrapper loading={markingTicketAsResolved} />
</View>
);
};
const styles = StyleSheet.create({
flexBasis20: {
flexBasis: '20%',
},
flexBasis40: {
flexBasis: '40%',
},
flexBasis30: {
flexBasis: '30%',
},
textInput: {
textAlignVertical: 'top',
maxHeight: 100,
},
flexBasis48: {
flexBasis: '48%',
},
pt0: {
paddingTop: 0,
},
black: {
color: COLORS.TEXT.BLACK,
},
label: {
color: COLORS.BORDER.SECONDARY,
},
count: {
position: 'absolute',
bottom: -24,
right: 0,
},
error: {
color: COLORS.TEXT.YELLOW,
},
});
export default CustomerCard;

View File

@@ -0,0 +1,66 @@
import { Pressable, StyleSheet, View } from 'react-native';
import React from 'react';
import Heading from '@rn-ui-lib/components/Heading';
import { GenericStyles } from '@rn-ui-lib/styles';
import { formatAmount } from '@rn-ui-lib/utils/amount';
import Text from '@rn-ui-lib/components/Text';
import { COLORS } from '@rn-ui-lib/colors';
interface ICustomerDetailListItem {
item: any;
viewTaskCta: string;
handleNavigation: () => void;
handleNavigationTODetails: () => void;
}
const CustomerDetailListItem: React.FC<ICustomerDetailListItem> = (props) => {
const { item, viewTaskCta, handleNavigation, handleNavigationTODetails } = props;
return (
<View>
<Heading style={GenericStyles.mt16} type="h5" dark>
{item.customerName}
</Heading>
<View style={[GenericStyles.row, GenericStyles.mt8]}>
<View>
<Text style={GenericStyles.fontSize14} dark>
{formatAmount(item?.overdueAmount?.value)}
</Text>
<Text style={styles.tagColor} small>
Overdue Amount
</Text>
</View>
<View style={styles.marginLeft60}>
<Text style={GenericStyles.fontSize14} dark>
{item?.loanAccountNumber}
</Text>
<Text style={styles.tagColor} small>
LAN
</Text>
</View>
</View>
<View style={[GenericStyles.row, GenericStyles.mt16]}>
<Pressable onPress={handleNavigation}>
<Text style={styles.buttonText}>Go to customer details</Text>
</Pressable>
<Pressable style={GenericStyles.ml16} onPress={handleNavigationTODetails}>
<Text style={styles.buttonText}>{viewTaskCta} </Text>
</Pressable>
</View>
</View>
);
};
const styles = StyleSheet.create({
tagColor: {
color: COLORS.BORDER.SECONDARY,
},
marginLeft60: {
marginLeft: 60,
},
buttonText: {
fontWeight: '500',
color: COLORS.BASE.BLUE,
},
});
export default CustomerDetailListItem;

View File

@@ -0,0 +1,24 @@
import Heading from "@rn-ui-lib/components/Heading"
import { GenericStyles } from "@rn-ui-lib/styles"
import React from "react";
import { View } from "react-native"
interface IHeaderNode {
title: string;
}
const HeaderNode= () => {
return (
<View
style={[
GenericStyles.ph16,
GenericStyles.pb16,
]}
>
<Heading type='h3'>Update status to <Heading dark bold style={{fontWeight: '700'}} type='h3' >Done</Heading></Heading>
</View>
)
}
export default HeaderNode

View File

@@ -0,0 +1,130 @@
import { navigateToScreen } from '@components/utlis/navigationUtlis'
import { useAppDispatch } from '@hooks'
import { resetSingleViewRequestTicket } from '@reducers/cosmosSupportSlice'
import { COLORS } from '@rn-ui-lib/colors'
import Tag, { TagVariant } from '@rn-ui-lib/components/Tag'
import Text from '@rn-ui-lib/components/Text'
import ArrowRightOutlineIcon from '@rn-ui-lib/icons/ArrowRightOutlineIcon'
import { GenericStyles } from '@rn-ui-lib/styles'
import { ProfileScreenStackEnum } from '@screens/Profile/ProfileStack'
import { PageRouteEnum } from '@screens/auth/ProtectedRouter'
import { CaseDetailStackEnum } from '@screens/caseDetails/CaseDetailStack'
import React from 'react'
import { StyleSheet, TouchableHighlight, View } from 'react-native'
import CustomerDetailListItem from './CustomerDetailListItem'
import RelativeTime from './RelativeTime'
import { Mapping, SCREEN_MAP, StatusColorMapping } from './constant'
import { From } from './ViewRequestHistory'
const ListItem = (props: any) => {
const { form, route, item, task, isLastItem, loadData} = props;
const viewTaskCta = task === SCREEN_MAP.TASK_FOR_ME ? "View task details" : "View request details";
const dispatch = useAppDispatch();
const Component = form === Mapping.FORM_PROFILE ? View : TouchableHighlight;
const handleNavigation = () => {
navigateToScreen(PageRouteEnum.CASE_DETAIL_STACK, {
screen: CaseDetailStackEnum.COLLECTION_CASE_DETAIL,
params: { caseId: item.collectionCaseId },
});
}
const handleNavigationTODetails = () => {
dispatch(resetSingleViewRequestTicket(null))
navigateToScreen(form === Mapping.FORM_PROFILE ? ProfileScreenStackEnum.PROFILE_TICKET_DETAIL : CaseDetailStackEnum.VIEW_REQUEST_DETAIL, { ticketId: item?.referenceId, task, loadData })
}
return (
<Component
underlayColor={COLORS.BACKGROUND.BLUE}
onPress={handleNavigationTODetails}
style={[
GenericStyles.p16,
GenericStyles.row,
GenericStyles.justifyContentSpaceBetween,
GenericStyles.alignCenter,
!isLastItem ? styles.lineItem : {}
]}
>
<>
<View
style={[styles.flexBasis]}
>
{item?.newUpdatesCount ? <Text small style={styles.updateStyle} >{item?.newUpdatesCount} {item?.newUpdatesCount > 1 ? 'updates' : 'update'}</Text> : null}
<View
style={[
GenericStyles.row,
GenericStyles.mt4,
]}
>
<View
style={[
GenericStyles.row,
GenericStyles.alignCenter,
]}
>
{item.isAcknowledgedByAssignee || task === SCREEN_MAP.TELE_SUPPORT ? null : <View
style={[
styles.dot,
GenericStyles.mr10
]}
/>}
<Text bold dark style={form === Mapping.FORM_PROFILE ? GenericStyles.fontSize13 : GenericStyles.fontSize12}>{item?.requestTypeLabel} </Text>
</View>
<View style={[GenericStyles.alighSelfCenter]}>
<Tag style={[GenericStyles.ml10]} variant={StatusColorMapping[item?.status as keyof typeof StatusColorMapping]} text={item?.statusLabel} />
</View>
</View>
<RelativeTime fontSize={form === Mapping.FORM_PROFILE ? 12: 13} epoch={item?.createdAt} />
{form === Mapping.FORM_PROFILE ? <CustomerDetailListItem item={item} handleNavigation={handleNavigation} handleNavigationTODetails={handleNavigationTODetails} viewTaskCta={viewTaskCta} /> : null}
</View>
{form === Mapping.FORM_PROFILE ? null : <ArrowRightOutlineIcon fillColor={COLORS.BASE.BLUE} />}
</>
</Component>
)
}
const styles = StyleSheet.create({
flexBasis: {
flexBasis: '95%'
},
updateStyle: {
lineHeight: 18,
fontWeight: "500",
color: "#518CFF"
},
type: {
lineHeight: 16,
fontWeight: "500",
},
requestedON: {
lineHeight: 20,
fontWeight: "400",
fontSize: 13,
color: COLORS.BORDER.SECONDARY
},
dot: {
height: 4,
width: 4,
borderRadius: 2,
backgroundColor: COLORS.BASE.BLUE,
},
lineItem: {
borderBottomWidth: 12,
borderBottomColor: COLORS.BACKGROUND.SILVER,
paddingBottom: 16,
paddingTop: 16
},
tagColor: {
color: COLORS.BORDER.SECONDARY
},
marginLeft60: {
marginLeft: 60
},
buttonText: {
fontWeight: "500",
color: COLORS.BASE.BLUE
}
})
export default ListItem

View File

@@ -0,0 +1,23 @@
import LineLoader from '@rn-ui-lib/components/suspense_loader/LineLoader';
import { GenericStyles } from '@rn-ui-lib/styles';
import React from 'react';
import { View } from 'react-native';
const ListItemLoading = () => (
<View
style={[
GenericStyles.p16,
GenericStyles.row,
GenericStyles.justifyContentSpaceBetween,
GenericStyles.alignCenter
]}
>
<View>
<LineLoader height={10} width={50} />
<LineLoader style={[GenericStyles.mt12]} height={10} width={100} />
<LineLoader style={[GenericStyles.mt12]} height={10} width={200} />
</View>
</View>
);
export default ListItemLoading

View File

@@ -0,0 +1,75 @@
import React, { useState, useEffect } from 'react';
import dayjs from 'dayjs';
import { COSMOS_STANDARD_DATE_TIME_FORMAT, dateFormat } from '@rn-ui-lib/utils/dates';
import Text from '@rn-ui-lib/components/Text';
import relativeTime from 'dayjs/plugin/relativeTime';
import { StyleSheet } from 'react-native';
import { COLORS } from '@rn-ui-lib/colors';
dayjs.extend(relativeTime);
const ONE_MIN_TIME_INTERVAL = 60000;
const ONE_HOUR_TIME_INTERVAL = 3600000;
interface IRelativeTime {
epoch: number
fontSize?: number
}
const RelativeTime:React.FC<IRelativeTime> = (props) => {
const {
epoch,
fontSize = 13
} = props;
const [currentTime, setCurrentTime] = useState('');
const [pollingInterval, setPollingInterval] = useState(ONE_MIN_TIME_INTERVAL); // Default polling interval
const updateTime = () => {
const now = dayjs();
const time = dayjs(epoch);
const diffInMinutes = now.diff(time, 'minute');
if (diffInMinutes < 60) {
setPollingInterval(ONE_MIN_TIME_INTERVAL); // Poll every minute
} else if (diffInMinutes >= 60 && diffInMinutes < 1440) {
setPollingInterval(ONE_HOUR_TIME_INTERVAL); // Poll every hour
} else {
setPollingInterval(0); // Do not poll for a day or more
}
if (now.diff(time, 'day') >= 1) {
setCurrentTime(dateFormat(new Date(epoch), COSMOS_STANDARD_DATE_TIME_FORMAT));
} else {
setCurrentTime(time.fromNow());
}
};
useEffect(() => {
updateTime();
if (pollingInterval) {
const intervalId = setInterval(() => {
updateTime();
}, pollingInterval);
return () => clearInterval(intervalId);
}
}, [pollingInterval]);
return (
<Text style={[styles.container, {fontSize: fontSize}]}>{currentTime}</Text>
);
};
const styles = StyleSheet.create({
container: {
color: COLORS.BORDER.SECONDARY,
lineHeight: 20
},
})
export default RelativeTime;

View File

@@ -0,0 +1,135 @@
import { COLORS } from '@rn-ui-lib/colors';
import Avatar from '@rn-ui-lib/components/Avatar';
import Text from '@rn-ui-lib/components/Text';
import LineLoader from '@rn-ui-lib/components/suspense_loader/LineLoader';
import SuspenseLoader from '@rn-ui-lib/components/suspense_loader/SuspenseLoader';
import { GenericStyles } from '@rn-ui-lib/styles';
import React from 'react';
import { StyleSheet, View } from 'react-native';
import { RelativeTime } from './';
import { ActivityLog } from './constant/types';
interface IRenderCommentsTimeline {
comments?: Array<ActivityLog>;
};
interface ICardComponent {
name: string;
time: number;
comment: string;
nameCard: string;
me?: boolean;
}
const RenderCommentsTimeline: React.FC<IRenderCommentsTimeline> = (props) => {
const { comments } = props;
return (
<>
{comments?.map((comment, index) => {
const name = comment?.activityBy?.userType === "FE" ? "You" : comment?.activityBy?.name;
return (
<CardComponent
key={index}
name={comment?.activityBy?.name}
time={new Date(comment?.activityAt).toString()}
comment={comment?.value}
nameCard={comment?.type === "TICKET_CREATION" ? `Requested by ${name}`:name}
me={comment?.activityBy?.userType === "FE" ? true : false}
/>
)
})}
</>
)
}
const CardComponent: React.FC<ICardComponent> = (props) => {
const { name, time, comment, nameCard, me } = props;
return (
<View
key={time}
style={[GenericStyles.mt16, GenericStyles.row]}
>
<SuspenseLoader
loading={!name}
fallBack={<LineLoader height={40} width={40} style={styles.imageFallBack} />}
>
<Avatar
loading
dataURI=''
size={32}
name={name}
style={ me ? styles.avatarStyle : styles.avatarMe}
textStyle={ styles.avatarText }
/>
</SuspenseLoader>
<View style ={[GenericStyles.ml8, GenericStyles.justifyContentCenter, styles.width]}>
<View style={[GenericStyles.row, GenericStyles.alignCenter]}>
<SuspenseLoader
loading={!name}
fallBack={<LineLoader height={14} width={100} />}
>
<Text small style={[styles.capitalize,styles.font13]}>
{nameCard}
</Text>
</SuspenseLoader>
<View style={styles.dot} />
<SuspenseLoader
loading={!time}
fallBack={<LineLoader height={14} width={150} />}
>
<RelativeTime epoch={new Date(time).getTime()} />
</SuspenseLoader>
</View>
{comment ?<Text dark style={{paddingRight: 6, paddingTop: 4}}>
{comment}
</Text> : null}
</View>
</View>
)
}
const styles = StyleSheet.create({
avatarText: {
color: COLORS.TEXT.WHITE,
fontSize: 13,
fontWeight: '500'
},
avatarStyle: {
backgroundColor: COLORS.BACKGROUND.GREEN_LIGHT,
borderColor: COLORS.BACKGROUND.GREEN_LIGHT
},
avatarMe: {
backgroundColor: COLORS.BACKGROUND.PURPLE_LIGHT,
borderColor: COLORS.BACKGROUND.PURPLE_LIGHT,
},
dot: {
height: 4,
width: 4,
borderRadius: 2,
backgroundColor: COLORS.BORDER.SECONDARY,
marginHorizontal: 4
},
imageFallBack: {
borderRadius: 20
},
capitalize: {
textTransform: 'capitalize',
},
font13:{
fontSize: 13,
lineHeight: 20,
color: COLORS.BORDER.SECONDARY,
},
width:{
flexBasis: '90%'
}
});
export default RenderCommentsTimeline

View File

@@ -0,0 +1,161 @@
import { goBack } from '@components/utlis/navigationUtlis';
import { useAppDispatch, useAppSelector } from '@hooks';
import { COLORS } from '@rn-ui-lib/colors';
import Heading from '@rn-ui-lib/components/Heading';
import NavigationHeader from '@rn-ui-lib/components/NavigationHeader';
import { GenericStyles } from '@rn-ui-lib/styles';
import React, { useEffect } from 'react';
import {KeyboardAvoidingView, RefreshControl, ScrollView, StyleSheet, View} from 'react-native';
import CustomerCard from './CustomerCard';
import RenderCommentsTimeline from './RenderCommentsTimeline';
import TextFieldWithInput from './TextFieldWithInput';
import { acknowledgeTicket, addCommentToTicket, fetchSingleTicket, getSummary } from './actions';
import { Mapping, SCREEN_MAP } from './constant';
import { GenericFunctionArgs } from '@common/GenericTypes';
import {Status} from "@screens/cosmosSupport/constant/types";
import { addClickstreamEvent } from '@services/clickstreamEventService';
import { CLICKSTREAM_EVENT_NAMES } from '@common/Constants';
interface IRequestDetail {
route: {
params: {
ticketId: string;
task: string;
loadData: GenericFunctionArgs
};
};
}
const RequestDetail: React.FC<IRequestDetail> = (props) => {
const { data, isRefreshing, user } = useAppSelector(state => ({
data: state.cosmosSupport.currentViewRequestTicket.data,
isRefreshing: state.cosmosSupport.currentViewRequestTicket.loading,
user: state.user.user
}))
const [comment, setComment] = React.useState<string>('');
const dispatch = useAppDispatch();
const onRefresh = () => {
if (!props?.route?.params?.ticketId) return;
loadData(props?.route?.params?.ticketId);
};
const handleBackPress = () => {
goBack();
};
const loadData = (ticketId: string, isPolling?:boolean) => {
dispatch(fetchSingleTicket(ticketId, isPolling))
};
useEffect(() => {
if (!props?.route?.params?.ticketId) return;
loadData(props?.route?.params?.ticketId)
dispatch(acknowledgeTicket(props?.route?.params?.ticketId));
}, [props?.route?.params?.ticketId]);
useEffect(() => {
if (data) {
dispatch(getSummary({loanAccountNumber: undefined}));
}
}, [data]);
const onSendHit = (comment: string) => {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_TASK_COMMENT_ADDED,props?.route?.params)
const trimmedComment = comment.trim();
const updateComment = {
activityAt: Date.now(),
activityBy: {
name: user?.name!,
userType: Mapping.USER_TYPE,
referenceId: user?.referenceId!
},
type: Mapping.COMMENT_TYPE,
value: trimmedComment
}
dispatch(addCommentToTicket(props?.route?.params?.ticketId, trimmedComment, updateComment));
}
return (
<KeyboardAvoidingView
behavior='padding'
style={[GenericStyles.fill, GenericStyles.whiteBackground]}>
<NavigationHeader
title={'Task details'}
onBack={handleBackPress}
subTitleStyle={styles.navigationContainerSubtitle}
titleStyle={styles.navigationContainerTitle}
containerStyle={styles.navigationContainerStyle}
/>
<ScrollView
keyboardShouldPersistTaps='always'
contentContainerStyle={GenericStyles.p16}
refreshControl={
<RefreshControl
refreshing={false}
onRefresh={onRefresh}
colors={[COLORS.BASE.BLUE]} // Adjust the color as needed
/>
}
>
<CustomerCard
address={data?.supportRequestDetails?.address || ''}
Dpd={data?.customerDetails?.currentDpd || 0}
Pos={data?.customerDetails?.outstandingAmount || 0}
customerName={data?.customerDetails?.customerName || ''}
requestType = {data?.requestTypeLabel || ''}
status={data?.status || ''}
statusReadable={data?.statusLabel || ''}
refId={data?.referenceId}
isRefreshing={isRefreshing}
isEditable={Boolean(props?.route?.params?.task === SCREEN_MAP.TASK_FOR_ME)}
ticketStatusChange={loadData}
requestedByuserType={data?.requestedBy?.userType!}
/>
{data?.status === Status.CLOSED || isRefreshing ? null :
<View>
<Heading dark type='h4' style={GenericStyles.mt16}>Activity</Heading>
<TextFieldWithInput
onPressSend={onSendHit}
comment={comment}
setComment={setComment}
/>
</View>
}
<RenderCommentsTimeline
comments={data?.activityLogs}
/>
</ScrollView>
</KeyboardAvoidingView>
)
}
const styles = StyleSheet.create({
callingType: {
fontSize: 14,
fontWeight: '400',
letterSpacing: -.175,
lineHeight: 20
},
navigationContainerStyle: {
paddingVertical: 9
},
navigationContainerSubtitle: {
marginTop: 0
},
navigationContainerTitle: {
fontWeight: '500',
lineHeight: 20
},
});
export default RequestDetail

View File

@@ -0,0 +1,213 @@
import FullScreenLoaderWrapper from '@common/FullScreenLoaderWrapper';
import { clearNavigationStack, goBack, navigateToScreen } from '@components/utlis/navigationUtlis';
import { useAppDispatch, useAppSelector } from '@hooks';
import Button from '@rn-ui-lib/components/Button';
import NavigationHeader from '@rn-ui-lib/components/NavigationHeader';
import PressableChip from '@rn-ui-lib/components/PressableChip';
import Text from '@rn-ui-lib/components/Text';
import TextInput from '@rn-ui-lib/components/TextInput';
import LineLoader from '@rn-ui-lib/components/suspense_loader/LineLoader';
import SuspenseLoader from '@rn-ui-lib/components/suspense_loader/SuspenseLoader';
import { GenericStyles } from '@rn-ui-lib/styles';
import React, { useEffect } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { SafeAreaView, ScrollView, StyleSheet, View } from 'react-native';
import { createTicket, getDataForTicketCreation } from './actions';
import { CreateTicketPayload } from './constant/types';
import { MAX_COMENT_LENGTH } from '@screens/cosmosSupport/constant';
import { COLORS } from '@rn-ui-lib/colors';
import { CaseDetailStackEnum } from '@screens/caseDetails/CaseDetailStack';
import { From } from './ViewRequestHistory';
const LoadingChips = () => {
return (
<View style={[GenericStyles.row, GenericStyles.flexWrap]}>
{[...Array(5)].map((type, index) => (
<LineLoader
key={index}
style={[GenericStyles.mr8, GenericStyles.br8, styles.mv10]}
height={32}
width={100}
/>
))}
</View>
);
};
interface RequestSupportProps {
route: {
params: {
caseDetail: {
customerName: string;
loanAccountNumber: number;
};
};
};
}
const RequestSupport: React.FC<RequestSupportProps> = (props) => {
const { caseDetail } = props?.route?.params;
const dispatch = useAppDispatch();
const { isLoading, data } = useAppSelector((state) => ({
isLoading: state?.cosmosSupport?.ticketCreationData?.isLoading,
data: state?.cosmosSupport?.ticketCreationData?.ticketCreationData,
}));
const {
control,
handleSubmit,
formState: { errors },
watch,
reset,
} = useForm({});
const watchRequestType = watch('requestType');
useEffect(() => {
dispatch(getDataForTicketCreation(String(caseDetail?.loanAccountNumber)));
}, []);
const handleCreation = () => {
reset();
goBack();
navigateToScreen(CaseDetailStackEnum.VIEW_RequestHistory, {
caseDetail: caseDetail,
tabId: 0,
from: From.TICKET_CREATION_SCREEN,
});
};
const handleSubmitForm = (data: any) => {
const payload: CreateTicketPayload = {
supportRequestType: data?.requestType,
supportRequestCreatorType: 'FE',
loanAccountNumber: String(caseDetail?.loanAccountNumber),
comment: data?.requestDescription,
};
dispatch(createTicket(payload, handleCreation));
};
const handleBackPress = () => {
goBack();
};
const noData = Boolean(data?.requestForm?.length && data?.requestForm?.length === 0);
return (
<SafeAreaView style={[GenericStyles.fill, GenericStyles.whiteBackground]}>
<NavigationHeader
title={'Create task'}
onBack={handleBackPress}
subTitleStyle={styles.navigationContainerSubtitle}
titleStyle={styles.navigationContainerTitle}
containerStyle={styles.navigationContainerStyle}
/>
<ScrollView contentContainerStyle={GenericStyles.p16}>
<View style={GenericStyles.fill}>
<Text>
Select task type <Text style={{ color: COLORS.TEXT.RED }}>*</Text>
</Text>
<SuspenseLoader loading={noData} fallBack={<LoadingChips />}>
<View style={[GenericStyles.row, GenericStyles.flexWrap]}>
{data?.requestForm?.map((type, index) => (
<Controller
key={type.value}
control={control}
name="requestType"
rules={{ required: true }}
render={({ field: { onChange, value } }) => (
<PressableChip
key={index}
label={type.label}
onSelectionChange={(meta, data) => onChange(data)}
checked={value === type.value}
meta={type.value}
textStyles={styles.lineHeight20}
/>
)}
/>
))}
</View>
</SuspenseLoader>
{noData ? null : (
<Controller
control={control}
name="requestDescription"
rules={{ required: false }}
render={({ field: { onChange, value } }) => (
<View style={[GenericStyles.fill, GenericStyles.pb8]}>
<TextInput
title="Comments (optional)"
numberOfLines={5}
style={styles.textInput}
containerStyle={GenericStyles.mt24}
multiline={true}
placeholder="Enter comments..."
onChangeText={onChange}
value={value}
maxLength={MAX_COMENT_LENGTH}
/>
<Text
small
light
style={[styles.count, value?.length >= MAX_COMENT_LENGTH ? styles.error : {}]}
>
{value?.length || 0}/300
</Text>
</View>
)}
/>
)}
</View>
</ScrollView>
<View style={GenericStyles.p16}>
<Button
title="Create"
style={[GenericStyles.mt16, GenericStyles.w100]}
onPress={handleSubmit(handleSubmitForm)}
disabled={!watchRequestType || isLoading}
/>
</View>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
navigationContainerStyle: {
paddingVertical: 9,
},
navigationContainerSubtitle: {
marginTop: 0,
},
navigationContainerTitle: {
fontWeight: '500',
lineHeight: 20,
},
tagContainer: {
borderRadius: 40,
padding: 4,
},
textInput: {
textAlignVertical: 'top',
minHeight: 100,
},
mv10: {
marginVertical: 10,
},
count: {
position: 'absolute',
bottom: -20,
right: 0,
},
lineHeight20: {
lineHeight: 20,
},
error: {
color: COLORS.TEXT.YELLOW,
},
});
export default RequestSupport;

View File

@@ -0,0 +1,25 @@
import Tag, { TagVariant } from '@rn-ui-lib/components/Tag';
import { GenericStyles } from '@rn-ui-lib/styles';
import React from 'react';
import { View } from 'react-native';
interface ITagComponent {
text: string;
}
const TagComponent: React.FC<ITagComponent> = (props) => {
const { text } = props;
return (
<View style={[GenericStyles.ml4]}>
<Tag
variant={TagVariant.blue}
text={text}
/>
</View>
)
}
export default TagComponent;

View File

@@ -0,0 +1,187 @@
import { COLORS } from '@rn-ui-lib/colors';
import { GenericStyles } from '@rn-ui-lib/styles';
import React, { useEffect, useRef, useState } from 'react';
import { ListRenderItemInfo, RefreshControl, View, VirtualizedList } from 'react-native';
import { ListItemLoading } from './';
import CSAFilters from './CSAFilters';
import ListItem from './ListItem';
import { loadingData, SCREEN_MAP } from './constant';
import { useAppDispatch, useAppSelector } from '@hooks';
import { fetchTicketList, getSummary } from './actions';
import {
setCaseLevelTicketsForMe,
setCaseLevelTicketsForMeLoading,
} from '@reducers/cosmosSupportSlice';
import TaskForMeEmptyScreen from '@assets/icons/TaskForMeEmptyScreen';
import Text from '@rn-ui-lib/components/Text';
import { From } from './ViewRequestHistory';
import { ACKNOWLEDGMENT_STATUS, IFilterStatus } from './constant/types';
import { _map } from '@rn-ui-lib/utils/common';
import axios, { Axios, CancelTokenSource } from 'axios';
import { addClickstreamEvent } from '@services/clickstreamEventService';
import { CLICKSTREAM_EVENT_NAMES } from '@common/Constants';
import { useIsFocused } from '@react-navigation/native';
interface ListItem {
referenceId: string;
text: string;
}
interface ITaskForMe {
route: {
key: keyof typeof SCREEN_MAP;
title: string;
};
from?: string;
isFocusedTab?: boolean;
for: keyof typeof SCREEN_MAP;
lan?: string;
}
// Define the component type
const TaskForMe: React.FC<ITaskForMe> = (props) => {
const { from, lan, isFocusedTab } = props;
const [isRefreshing, setRefreshing] = useState(false);
const [filters, setFilters] = useState<IFilterStatus>();
const dispatch = useAppDispatch();
const isFocused = useIsFocused();
const { userRefId, isLoading, data, filterTemplate } = useAppSelector((state) => ({
userRefId: state.user.user?.referenceId,
isLoading: state.cosmosSupport.caseLevelTickets.taskForMe.loading,
data: state.cosmosSupport.caseLevelTickets.taskForMe.data,
filterTemplate: state.cosmosSupport.filters
}));
const searchCancelToken = useRef<CancelTokenSource>();
const onRefresh = () => {
loadData();
dispatch(getSummary({loanAccountNumber: lan}));
};
const successCallback = (data: any) => {
dispatch(setCaseLevelTicketsForMe(data));
dispatch(setCaseLevelTicketsForMeLoading(false));
isRefreshing && setRefreshing(false);
};
const failureCallback = () => {
setRefreshing(false);
dispatch(setCaseLevelTicketsForMeLoading(false));
};
const getFiltersPayload = (filtersData?: IFilterStatus | null): Record<string, string[]> => {
const { filter: selectedFilters, showUnread = false } = filtersData || {};
const filtersPayload: Record<string, string[]> = {
ASSIGNEES: [userRefId!],
};
if (from !== From.PROFILE && lan) {
filtersPayload['LOAN_ACCOUNT_NUMBERS'] = [lan];
}
if (selectedFilters) {
_map(selectedFilters, (filterName: string) => {
const filter = selectedFilters[filterName];
if (!filter || !filter.filters) {
return;
}
let filtersPayloadData: Array<string> = []
Object.keys(filter.filters)?.forEach((filterData) => {
if(filter?.filters?.[filterData]) {
filtersPayloadData.push(filterData)
}
})
filtersPayload[filterName] = filtersPayloadData;
});
}
if (showUnread) {
filtersPayload['ASSIGNEE_COMMENTS_READ_STATUS'] = [ACKNOWLEDGMENT_STATUS.UNREAD];
}
return filtersPayload;
};
const loadData = () => {
dispatch(setCaseLevelTicketsForMeLoading(true));
setRefreshing(true);
const filter = getFiltersPayload(filters);
searchCancelToken.current = axios.CancelToken.source();
fetchTicketList({ filters: filter }, successCallback, failureCallback, searchCancelToken.current);
};
useEffect(() => {
if ((!lan && from !== From.PROFILE) || !isFocused) return;
if(lan) {
dispatch(getSummary({loanAccountNumber: lan}));
}
loadData();
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_MY_TASKS_LOAD_SUCCESSFUL,{lan})
}, [lan, isFocused]);
const handleFilterChange = (data: IFilterStatus) => {
searchCancelToken?.current?.cancel();
setFilters(data);
setRefreshing(true);
const filters = getFiltersPayload(data);
searchCancelToken.current = axios.CancelToken.source();
fetchTicketList({ filters }, successCallback, failureCallback, searchCancelToken.current);
};
const apiCallInProgress = isRefreshing || isLoading;
const renderItemLoadingState = () => <ListItemLoading />;
const renderItem = (props: ListRenderItemInfo<any>) => (
<ListItem
form={from}
isLastItem={props?.index === data?.length - 1}
task={SCREEN_MAP.TASK_FOR_ME}
loadData={loadData}
{...props}
/>
);
const getItemCount = () => (apiCallInProgress ? loadingData.length : data.length);
const getItem = (data: ListItem[], index: number) => data[index];
const isCaseScreen = [From.CASE_DETAIL_SCREEN, From.TICKET_CREATION_SCREEN].includes(from as From);
return (
<View style={[GenericStyles.fill, {paddingBottom: isCaseScreen ? 72 : 0}]}>
{!isCaseScreen ? (
<CSAFilters
onFilterChange={handleFilterChange}
filtersTemplate={filterTemplate?.TASKS_FOR_ME ?? {}}
isFocusedTab={isFocusedTab}
isTasksPresent={!!data?.length}
/>
) : null}
<VirtualizedList
style={GenericStyles.whiteBackground}
contentContainerStyle={apiCallInProgress || !data.length ? GenericStyles.fill : null}
data={apiCallInProgress ? loadingData : data}
keyExtractor={(item) => item.referenceId}
renderItem={apiCallInProgress ? renderItemLoadingState : renderItem}
getItemCount={getItemCount}
getItem={getItem}
initialNumToRender={5}
maxToRenderPerBatch={10}
windowSize={10}
refreshControl={
<RefreshControl refreshing={false} onRefresh={onRefresh} colors={[COLORS.BASE.BLUE]} />
}
ListEmptyComponent={() => (
<View
style={[
GenericStyles.alignCenter,
GenericStyles.justifyContentCenter,
{ height: '80%' },
]}
>
<TaskForMeEmptyScreen />
<Text style={GenericStyles.mt10}>
{Object.keys(filters ? filters?.filter : {})?.length
? 'No results found!'
: 'No active tasks for this customer'}
</Text>
</View>
)}
/>
</View>
);
};
export default TaskForMe;

View File

@@ -0,0 +1,186 @@
import { useAppDispatch, useAppSelector } from '@hooks';
import {
setCaseLevelTicketsForTele,
setCaseLevelTicketsForTeleLoading,
} from '@reducers/cosmosSupportSlice';
import { COLORS } from '@rn-ui-lib/colors';
import { GenericStyles } from '@rn-ui-lib/styles';
import React, { useEffect, useRef, useState } from 'react';
import { ListRenderItemInfo, RefreshControl, View, VirtualizedList } from 'react-native';
import { ListItemLoading } from './';
import CSAFilters from './CSAFilters';
import ListItem from './ListItem';
import { fetchTicketList } from './actions';
import { CASE_DETAIL_SCREEN, loadingData, SCREEN_MAP } from './constant';
import TaskForMeEmptyScreen from '@assets/icons/TaskForMeEmptyScreen';
import Text from '@rn-ui-lib/components/Text';
import { From } from './ViewRequestHistory';
import { ACKNOWLEDGMENT_STATUS, IFilterStatus } from './constant/types';
import { _map } from '@rn-ui-lib/utils/common';
import axios, { CancelTokenSource } from 'axios';
import { CLICKSTREAM_EVENT_NAMES } from '@common/Constants';
import { addClickstreamEvent } from '@services/clickstreamEventService';
import { useIsFocused } from '@react-navigation/native';
interface ListItem {
referenceId: string;
text: string;
}
interface ITaskForMe {
route: {
key: keyof typeof SCREEN_MAP;
title: string;
};
from?: string;
isFocusedTab?: boolean;
forScreen: keyof typeof SCREEN_MAP;
lan?: string;
}
const TeleSupport: React.FC<ITaskForMe> = (props) => {
const { from, route, lan, forScreen, isFocusedTab } = props;
const searchCancelToken = useRef<CancelTokenSource>();
const [filters, setFilters] = useState<IFilterStatus>();
const { userRefId, isLoading, data, filterTemplate } = useAppSelector((state) => ({
userRefId: state.user.user?.referenceId,
isLoading: state.cosmosSupport.caseLevelTickets.taskForTele.loading,
data: state.cosmosSupport.caseLevelTickets.taskForTele.data,
filterTemplate: state.cosmosSupport.filters
}));
const isFocused = useIsFocused();
const [isRefreshing, setRefreshing] = useState(false);
const dispatch = useAppDispatch();
const onRefresh = () => {
loadData();
};
useEffect(() => {
if ((!lan && from !== From.PROFILE) || !isFocused) return;
loadData();
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_MY_TASKS_LOAD_SUCCESSFUL,{lan})
}, [props.lan,isFocused]);
const getFiltersPayload = (filtersData?: IFilterStatus | null): Record<string, string[]> => {
const { filter: selectedFilters, showUnread = false } = filtersData || {};
const filtersPayload: Record<string, string[]> = {
REQUESTERS: [userRefId!],
};
if (from !== From.PROFILE && lan) {
filtersPayload['LOAN_ACCOUNT_NUMBERS'] = [lan];
}
if (selectedFilters) {
_map(selectedFilters, (filterName: string) => {
const filter = selectedFilters[filterName];
if (!filter || !filter.filters) {
return;
}
let filtersPayloadData: Array<string> = []
Object.keys(filter.filters)?.forEach((filterData) => {
if(filter?.filters?.[filterData]) {
filtersPayloadData.push(filterData)
}
})
filtersPayload[filterName] = filtersPayloadData;
});
}
if (showUnread) {
filtersPayload['REQUESTER_COMMENTS_READ_STATUS'] = [ACKNOWLEDGMENT_STATUS.UNREAD];
}
return filtersPayload;
};
const loadData = () => {
setRefreshing(true);
const filter = getFiltersPayload(filters);
if (from !== From.PROFILE && lan) {
filter['LOAN_ACCOUNT_NUMBERS'] = [lan];
}
searchCancelToken.current = axios.CancelToken.source();
fetchTicketList({ filters: filter }, successCallback, failureCallback, searchCancelToken.current);
};
const apiCallInProgress = isRefreshing || isLoading;
const renderItemLoadingState = () => <ListItemLoading />;
const renderItem = (props: ListRenderItemInfo<any>) => (
<ListItem
form={from}
isLastItem={props?.index === data?.length - 1}
task={SCREEN_MAP.TELE_SUPPORT}
{...props}
/>
);
const getItemCount = () => (apiCallInProgress ? loadingData.length : data.length);
const getItem = (data: ListItem[], index: number) => data[index];
const successCallback = (data: any) => {
dispatch(setCaseLevelTicketsForTele(data));
dispatch(setCaseLevelTicketsForTeleLoading(false));
};
const failureCallback = () => {
setRefreshing(false);
dispatch(setCaseLevelTicketsForTeleLoading(false));
};
const handleFilterChange = (data: IFilterStatus) => {
searchCancelToken?.current?.cancel();
setFilters(data);
setRefreshing(true);
const filters = getFiltersPayload(data);
searchCancelToken.current = axios.CancelToken.source();
fetchTicketList({ filters }, successCallback, failureCallback, searchCancelToken.current);
};
const isCaseScreen = [From.CASE_DETAIL_SCREEN, From.TICKET_CREATION_SCREEN].includes(from as From);
return (
<View style={[GenericStyles.fill, {paddingBottom: isCaseScreen ? 72 : 0}]}>
{!isCaseScreen ? (
<CSAFilters
onFilterChange={handleFilterChange}
filtersTemplate={filterTemplate?.TASKS_FOR_CSA ?? {}}
isFocusedTab={isFocusedTab}
isTasksPresent={!!data?.length}
/>
) : null}
<VirtualizedList
style={GenericStyles.whiteBackground}
contentContainerStyle={apiCallInProgress || !data.length ? GenericStyles.fill : null}
data={apiCallInProgress ? loadingData : data}
keyExtractor={(item) => item.referenceId}
renderItem={apiCallInProgress ? renderItemLoadingState : renderItem}
getItemCount={getItemCount}
getItem={getItem}
initialNumToRender={5}
maxToRenderPerBatch={10}
windowSize={10}
refreshControl={
<RefreshControl refreshing={false} onRefresh={onRefresh} colors={[COLORS.BASE.BLUE]} />
}
ListEmptyComponent={() => (
<View
style={[
GenericStyles.justifyContentCenter,
GenericStyles.alignCenter,
{ height: '80%' },
]}
>
<TaskForMeEmptyScreen />
<Text style={GenericStyles.mt10}>
{Object.keys(filters ? filters?.filter : {})?.length
? 'No results found!'
: 'No active tasks for this customer'}
</Text>
</View>
)}
/>
</View>
);
};
export default TeleSupport;

View File

@@ -0,0 +1,147 @@
import { Pressable, StyleSheet, View } from 'react-native'
import React, {useEffect, useState} from 'react'
import { GenericStyles } from '@rn-ui-lib/styles';
import TextInput from '@rn-ui-lib/components/TextInput';
import { COLORS } from '@rn-ui-lib/colors';
import SendIcon from '@assets/icons/SendIcon';
import Toast from 'react-native-toast-message';
import { useAppSelector } from '@hooks';
import FullScreenLoaderWrapper from '@common/FullScreenLoaderWrapper';
import Text from '@rn-ui-lib/components/Text';
interface ITextFieldWithInput {
onPressSend: (comment: string) => void;
setComment: React.Dispatch<React.SetStateAction<string>>;
comment: string;
}
const TextFieldWithInput: React.FC<ITextFieldWithInput> = (props) => {
const { onPressSend, comment, setComment } = props;
const { addingComment, isRefreshing } = useAppSelector(state => ({
addingComment: state.cosmosSupport.isSubmittingComment,
isRefreshing: state.cosmosSupport.currentViewRequestTicket.loading,
}));
const [isFocussed, setIsFocussed]= useState(false);
useEffect(() => {
if (!addingComment) {
setComment('');
}
}, [addingComment])
const handleOnFocus = () => {
setIsFocussed(true);
}
const handleOnBlur= () => {
setIsFocussed(false);
}
const onPress = () => {
if (!comment) {
Toast.show({
type: 'error',
text1: 'Comment is required',
})
return;
}
onPressSend(comment);
}
const disabled = addingComment || isRefreshing || !comment?.trim();
return (
<View style={[GenericStyles.columnDirection, GenericStyles.alignItemsFlexEnd]}>
<View style={[
GenericStyles.mt16,
GenericStyles.mb8,
GenericStyles.row,
{
borderTopColor: isFocussed ? COLORS.TEXT.BLUE : COLORS.BORDER.PRIMARY,
borderTopWidth: 1,
borderBottomColor: isFocussed ? COLORS.TEXT.BLUE : COLORS.BORDER.PRIMARY,
borderBottomWidth: 1,
borderRightColor: isFocussed ? COLORS.TEXT.BLUE : COLORS.BORDER.PRIMARY,
borderRightWidth: 1,
borderLeftColor:isFocussed ? COLORS.TEXT.BLUE : COLORS.BORDER.PRIMARY,
borderLeftWidth: 1,
borderRadius: 8,
shadowColor: COLORS.TEXT.BLUE,
shadowOffset: {width: 4, height: 4},
shadowOpacity: 0.2,
shadowRadius: 3,
}
]}>
<TextInput
numberOfLines={5}
style={[styles.textInput, { flexBasis: '80%' }]}
multiline={true}
placeholder='Leave a comment...'
onChangeText={setComment}
value={comment}
maxLength={300}
disabled={isRefreshing}
border={false}
onFocus={handleOnFocus}
onBlur={handleOnBlur}
/>
<View style={[GenericStyles.columnDirection, GenericStyles.justifyContentFlexEnd]}>
<Pressable
disabled={disabled}
style={[styles.buttonStyle, GenericStyles.ml8, GenericStyles.mr10, disabled ? styles.disabledButton : styles.active]}
onPress={onPress}
>
<SendIcon strokeColor={disabled ? '#e8e8e8' : COLORS.BASE.BLUE} />
</Pressable>
</View>
</View>
<Text small light style={ [GenericStyles.mr8, comment.length >=300 ? styles.error : {}]}>{comment.length}/300</Text>
<FullScreenLoaderWrapper
loading={addingComment}
/>
</View>
)
}
const styles = StyleSheet.create({
active: {
backgroundColor: COLORS.BACKGROUND.BLUE_LIGHT_2,
},
buttonStyle: {
alignItems: 'center',
display: 'flex',
height: 30,
justifyContent: "center",
marginBottom: 10,
width: 30,
},
count: {
bottom: -24,
position: 'absolute',
right: 48
},
disabledButton: {
backgroundColor: COLORS.BASE.BLUE,
},
error: {
color: COLORS.TEXT.YELLOW
},
textInput: {
maxHeight: 100,
textAlignVertical: 'top',
}
});
export default TextFieldWithInput;

View File

@@ -0,0 +1,217 @@
import React, { useEffect } from 'react';
import {
SafeAreaView,
StyleSheet,
View,
useWindowDimensions
} from 'react-native';
import { goBack, navigateToScreen } from '@components/utlis/navigationUtlis';
import { useAppDispatch, useAppSelector } from '@hooks';
import { setShouldHideTabBar } from '@reducers/allCasesSlice';
import { COLORS } from '@rn-ui-lib/colors';
import NavigationHeader from '@rn-ui-lib/components/NavigationHeader';
import Text from '@rn-ui-lib/components/Text';
import { GenericStyles } from '@rn-ui-lib/styles';
import { TabBar, TabView } from 'react-native-tab-view';
import { TaskForMe, TeleSupport } from './';
import TagComponent from './TagComponent';
import { SCREEN_MAP } from './constant';
import { useIsFocused } from '@react-navigation/native';
import { addClickstreamEvent } from '@services/clickstreamEventService';
import { CLICKSTREAM_EVENT_NAMES } from '@common/Constants';
import Button from '@rn-ui-lib/components/Button';
import { CaseDetailStackEnum } from '@screens/caseDetails/CaseDetailStack';
type SCENE = keyof typeof SCREEN_MAP
export enum From {
PROFILE = 'profile',
CASE_DETAIL = 'caseDetail',
CASE_DETAIL_SCREEN = 'caseDetailScreen',
TICKET_CREATION_SCREEN = 'ticketCreationScreen',
}
interface IViewRequestHistory {
route: {
params: {
caseDetail: {
customerName: string;
loanAccountNumber: string;
};
from?: From;
tabId?: number
};
};
}
const ViewRequestHistory = (props: IViewRequestHistory) => {
const { tabId, from, caseDetail } = props?.route?.params;
const layout = useWindowDimensions();
const [index, setIndex] = React.useState(tabId || 0);
const isFocused = useIsFocused();
const dispatch = useAppDispatch();
const {summary} = useAppSelector(state => ({
summary: state.cosmosSupport.caseSummary
}))
const [routes] = React.useState([
{ key: SCREEN_MAP.TELE_SUPPORT, title: 'Task for tele' },
{ key: SCREEN_MAP.TASK_FOR_ME, title: 'Tasks for me' },
]);
const hasUpdates = (scene: SCENE) => {
if(scene === SCREEN_MAP.TASK_FOR_ME && summary?.assigneeSummaryDetails.newTicketUpdatesCount > 0){
return true;
}
return false;
};
const getCount = (scene: SCENE) => {
if(scene === SCREEN_MAP.TASK_FOR_ME){
return summary?.assigneeSummaryDetails.newTicketUpdatesCount;
}
return 0;
}
const renderScene = ({ route }: { route: { key: string } }) => {
switch (route.key) {
case SCREEN_MAP.TELE_SUPPORT:
return (
<TeleSupport
route={{
key: SCREEN_MAP.TELE_SUPPORT as SCENE,
title: "Task for tele"
}}
isFocusedTab={index === 0}
from={from}
forScreen={SCREEN_MAP.TELE_SUPPORT as SCENE}
lan={caseDetail?.loanAccountNumber}
/>
);
case SCREEN_MAP.TASK_FOR_ME:
return (
<TaskForMe
route={{
key: SCREEN_MAP.TASK_FOR_ME as SCENE,
title: "Task for me"
}}
isFocusedTab={index === 1}
from={from}
for={SCREEN_MAP.TASK_FOR_ME as SCENE}
lan={caseDetail?.loanAccountNumber}
/>
);
default:
return null;
}
};
useEffect(() => {
if(from === From.TICKET_CREATION_SCREEN && typeof tabId === 'number' && isFocused) {
setIndex(tabId)
}
}, [tabId, isFocused])
useEffect(() => {
dispatch(setShouldHideTabBar(true))
return () => {
dispatch(setShouldHideTabBar(false))
}
}, []);
return (
<SafeAreaView style={GenericStyles.fill}>
<NavigationHeader
onBack={goBack}
title={from === From.PROFILE ? 'All task' : `Task log`}
subTitle={from === From.PROFILE ? undefined : `For ${props?.route?.params?.caseDetail?.customerName}`}
subTitleStyle={styles.navigationContainerSubtitle}
titleStyle={styles.navigationContainerTitle}
containerStyle={styles.navigationContainerStyle}
/>
<TabView
lazy
navigationState={{ index, routes }}
renderScene={renderScene}
onIndexChange={setIndex}
initialLayout={{ width: layout.width }}
renderTabBar={(props) => <TabBar
onTabPress={({ route }) => {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_TASKS_TAB_BUTTON_CLICKED, route);
}}
renderLabel={({ route, color }) => (
<View
style={
GenericStyles.row
}
>
<View style={[GenericStyles.row, GenericStyles.centerAligned]}>
<Text style={{ color }}>
{route.title}
</Text>
{hasUpdates(route.key as SCENE)&&<TagComponent text={`${getCount(route.key as SCENE)} new`} />}
</View>
</View>
)}
labelStyle={styles.labelStyle}
indicatorStyle={styles.indicatorStyle}
inactiveColor={COLORS.TEXT.LIGHT}
activeColor={COLORS.TEXT.BLUE}
style={GenericStyles.whiteBackground}
{...props}
/>
}
/>
{[From.CASE_DETAIL_SCREEN, From.TICKET_CREATION_SCREEN].includes(from) ? (
<View style={[GenericStyles.row, GenericStyles.fill, styles.buttonContainer]}>
<Button
style={[GenericStyles.fill]}
title="Create task for tele"
variant="primary"
onPress={() =>
navigateToScreen(CaseDetailStackEnum.RAISE_REQUEST, { caseDetail: caseDetail })
}
testID={'create_task'}
/>
</View>
) : null}
</SafeAreaView>
);
};
export default ViewRequestHistory;
const styles = StyleSheet.create({
indicatorStyle: {
backgroundColor: COLORS.BASE.BLUE
},
labelStyle: {
textTransform: 'none'
},
navigationContainerStyle: {
paddingVertical: 9
},
navigationContainerSubtitle: {
marginTop: 0
},
navigationContainerTitle: {
fontWeight: '500',
lineHeight: 20
},
buttonContainer: {
position: 'absolute',
bottom: 0,
padding: 16,
paddingTop: 12,
justifyContent: 'space-around',
alignItems: 'center',
backgroundColor: COLORS.BACKGROUND.PRIMARY,
borderTopWidth: 1,
borderTopColor: COLORS.BORDER.PRIMARY,
},
});

View File

@@ -0,0 +1,179 @@
import axiosInstance, { API_STATUS_CODE, API_URLS, ApiKeys, getApiUrl } from "@components/utlis/apiHelper";
import { logError } from "@components/utlis/errorUtils";
import { ActivityLog, CreateCommentPayload, CreateTicketPayload, GetTicketCreationPayload, ICount, RequestTicket, SummaryPayload, UpDateTicketPayload } from "./constant/types";
import store, { AppDispatch } from "@store";
import { toast } from "@rn-ui-lib/components/toast";
import { loadingSingleTicket, setCaseSummary, setMarkingTicketAsResolved, setSingleViewRequestTicket, setSubmittingComment, setTicketCreationData, setTicketCreationDataForFrom, updateComment } from "@reducers/cosmosSupportSlice";
import { Dispatch } from "redux";
import { GenericFunctionArgs } from "@common/GenericTypes";
import { Mapping } from "./constant";
import { noop } from "@rn-ui-lib/utils/common";
import { CancelTokenSource } from "axios";
import { addClickstreamEvent } from "@services/clickstreamEventService";
import { CLICKSTREAM_EVENT_NAMES } from "@common/Constants";
import { err } from "react-native-svg/lib/typescript/xml";
export const getDataForTicketCreation = (lan: string) => (dispatch: Dispatch) => {
dispatch(setTicketCreationData(true));
const url = getApiUrl(ApiKeys.GET_FORM_OPTIONS);
axiosInstance.get(url, { params: { loanAccountNumber: lan, supportRequestUserType: Mapping.USER_TYPE } }).then((response) => {
if (response.status === API_STATUS_CODE.OK) {
const requestForm = response?.data?.requestForm || [];
const convertedData = requestForm.map((item: any) => {
const [value, label] = Object.entries(item)[0];
return { label, value };
});
const requestTo = response?.data?.requestTo?.name;
dispatch(setTicketCreationDataForFrom({ requestForm: convertedData, requestFor: requestTo }))
}
})
.catch((error) => {
logError(error, "error in getting data for ticket creation");
})
.finally(() => {
dispatch(setTicketCreationData(false));
});
};
export const createTicket = (payload: CreateTicketPayload, clicked: GenericFunctionArgs) => (dispatch: Dispatch) => {
dispatch(setTicketCreationData(true));
const url = getApiUrl(ApiKeys.CREATE_TICKET);
axiosInstance.post(url, payload).then((response) => {
if (response.status === API_STATUS_CODE.OK) {
toast({
text1: "Ticket created successfully",
type: "success"
});
clicked();
}
})
.catch((error) => {
logError(error, "error in creating ticket");
})
.finally(() => {
dispatch(setTicketCreationData(false));
});
};
export const updateTicket = (payload: any, ticketId: string) => (dispatch: Dispatch) => {
dispatch(setMarkingTicketAsResolved(true));
const url = getApiUrl(ApiKeys.UPDATE_TICKET_STATUS, { ticketReferenceId: ticketId });
axiosInstance.put(url, payload).then((response) => {
if (response.status === API_STATUS_CODE.OK) {
toast({
text1: "Ticket updated successfully",
type: "success"
});
dispatch(fetchSingleTicket(ticketId) as any);
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_TASK_MARKED_SUCCESSFUL, { ticketId })
}
})
.catch((error) => {
logError(error, "error in updating ticket");
})
.finally(() => {
dispatch(setMarkingTicketAsResolved(false));
})
};
export const fetchSingleTicket = (ticketReferenceId: string, isPolling?: boolean) => (dispatch: Dispatch) => {
isPolling ? noop() : dispatch(loadingSingleTicket(true));
const url = getApiUrl(ApiKeys.GET_CSA_SINGLE_TICKET, { ticketReferenceId });
axiosInstance.get(url).then((response) => {
if (response.status === API_STATUS_CODE.OK) {
dispatch(setSingleViewRequestTicket(response?.data));
}
})
.catch((error) => {
logError(error, "error in fetching single ticket");
})
.finally(() => {
isPolling ? noop() : dispatch(loadingSingleTicket(false));
});
}
export const fetchTicketList = (payload: any, callbackOnSuccess: (data: any) => void, callbackOnFailure: GenericFunctionArgs, cancelToken: CancelTokenSource) => {
const url = getApiUrl(ApiKeys.GET_CSA_TICKETS);
axiosInstance.post(url, payload, { cancelToken: cancelToken.token }).then((response) => {
if (response?.status === API_STATUS_CODE.OK) {
callbackOnSuccess(response.data?.data);
}
})
.catch((error) => {
logError(error, "error in fetching ticket list");
})
.finally(() => {
callbackOnFailure();
});
}
export const acknowledgeTicket = (ticketReferenceId: string) =>(dispatch:Dispatch) => {
const url = getApiUrl(ApiKeys.ACKNOWLEDGE_TICKET, { ticketReferenceId });
axiosInstance.patch(url)
.catch((error) => {
logError(error, "error in acknowledging ticket");
})
}
export const addCommentToTicket = (ticketReferenceId: string, comment: string, commentPayload: ActivityLog) => (dispatch: Dispatch) => {
dispatch(setSubmittingComment(true));
const url = getApiUrl(ApiKeys.ADD_COMMENT, { ticketReferenceId });
axiosInstance.post(url, { comment })
.then((response) => {
if (response.status === API_STATUS_CODE.OK) {
toast({
text1: "Comment added successfully",
type: "success"
});
dispatch(updateComment(commentPayload))
}
})
.catch((error) => {
logError(error, "error in adding comment to ticket");
})
.finally(() => {
dispatch(setSubmittingComment(false));
});
}
export const getSummary = (payload: SummaryPayload) => (dispatch: Dispatch) => {
const url = getApiUrl(ApiKeys.GET_UPDATE_COUNT);
let shouldSendPayload = false;
if (payload.loanAccountNumber) {
shouldSendPayload = true;
}
axiosInstance.get(url, {
params: shouldSendPayload ? payload : {}
}).then((response) => {
if (response.status === API_STATUS_CODE.OK) {
dispatch(setCaseSummary(response?.data))
}
})
.catch((error) => {
logError(error, "error in fetching summary");
})
};

View File

@@ -0,0 +1,49 @@
import { TFilterOptions } from '@components/filters/Filters';
import { TagVariant } from '@rn-ui-lib/components/Tag';
export const SCREEN_MAP = {
TELE_SUPPORT: 'TELE_SUPPORT',
TASK_FOR_ME: 'TASK_FOR_ME',
};
export enum RequestTypeReadableString {
VISIT = 'VISIT',
SKIP_TRACING = 'Skip Tracing',
SOFT_CALLING = 'Soft Calling',
HARD_CALLING = 'Hard Calling',
PTP_FOLLOW_UP = 'PTP Follow Up',
}
export enum StatusColorMapping {
CLOSED = TagVariant.success,
TODO = TagVariant.alert,
IN_PROGRESS = TagVariant.blue,
}
export enum FilterOptions {
pending = 'pending',
done = 'done',
}
export enum Mapping {
USER_TYPE = 'FE',
COMMENT_TYPE = 'COMMENT_ADDITION',
FORM_PROFILE = 'profile',
CLOSED = 'CLOSED',
}
export const loadingData = Array.from({ length: 10 }, (_, index) => ({
referenceId: index.toString(),
text: `Item ${index + 1}`,
}));
export const getFiltersCount = (selectedFilter: TFilterOptions) => {
let feedbackFilterCount = 0;
Object.keys(selectedFilter)?.forEach(
(filter) => (feedbackFilterCount += selectedFilter[filter]?.selectedFilterCount ?? 0)
);
return feedbackFilterCount;
};
export const MAX_COMENT_LENGTH = 300;
export const CASE_DETAIL_SCREEN = 'caseDetailScreen'

View File

@@ -0,0 +1,157 @@
import FilterOptions from '@components/filters/FilterOptions';
import { TFilterOptions } from '@components/filters/Filters';
enum RequestType {
VISIT = 'VISIT',
SKIP_TRACING = 'SKIP_TRACING',
SOFT_CALLING = 'SOFT_CALLING',
HARD_CALLING = 'HARD_CALLING',
PTP_FOLLOW_UP = 'PTP_FOLLOW_UP',
}
export enum Status {
TO_DO = 'TODO',
CLOSED = 'CLOSED',
IN_PROGRESS = 'IN_PROGRESS',
}
export enum RequestorType {
CSA = 'CSA',
FE = 'FE',
}
export interface CreateTicketPayload {
supportRequestType: RequestType;
supportRequestCreatorType: keyof typeof RequestorType;
loanAccountNumber: string;
comment?: string;
}
export interface CreateCommentPayload {
ticketId: string;
request: {
comment: string;
};
}
export interface UpDateTicketPayload {
ticketId: string;
request: {
status: keyof typeof Status;
};
}
export interface GetTicketCreationPayload {
requestForm?: Array<{
label: string;
value: string;
}>;
requestFor?: string;
}
export interface GetAllTicketsPayload {
filters: GetAllTicketFilters;
}
export enum ACKNOWLEDGMENT_STATUS {
UNACK = 'UNACK',
ACK = 'ACK',
UNREAD = 'UNREAD',
}
export interface GetAllTicketFilters {
REQUESTERS?: Array<string>;
ASSIGNEES?: Array<string>;
LOAN_ACCOUNT_NUMBERS?: Array<number>;
STATUSES?: Array<Status>;
ACKNOWLEDGMENT_STATUS?: ACKNOWLEDGMENT_STATUS[] | null;
}
export interface RequestTicket {
referenceId: string;
status: string;
statusLabel: string;
requestType: string;
requestTypeLabel: string;
loanAccountNumber: string;
createdAt: number;
requestedBy: UserTicket;
assignedTo: UserTicket;
customerDetails: CustomerDetailsTicket;
activityLogs: ActivityLog[];
supportRequestDetails: {
address: string;
};
}
export interface UserTicket {
referenceId: string;
name: string;
userType: string;
}
export interface CustomerDetailsTicket {
customerReferenceId: string;
outstandingAmount: number;
currentDpd: number;
unpaidDays: number | null;
customerName: string;
}
export interface ActivityLog {
type: string;
activityBy: UserTicket;
activityAt: number;
value: string;
}
export interface IFilterStatus {
filter: TFilterOptions;
showUnread?: boolean;
}
export interface ICount {
requesterSummaryDetails: {
newTicketsCount: number;
newUpdatesCount: number;
};
assigneeSummaryDetails: {
newTicketsCount: number;
newUpdatesCount: number;
};
}
export interface SummaryPayload {
loanAccountNumber?: string;
}
export interface SummaryDetails {
newTicketsCount: number;
newTicketUpdatesCount: number;
}
export interface Summary {
requesterSummaryDetails: SummaryDetails;
assigneeSummaryDetails: SummaryDetails;
aggregatedSummary: SummaryDetails;
}
interface IOption {
label: string;
value: string;
}
export interface ICsaFilter {
SUPPORT_REQUEST_TYPES: {
displayText: string;
options: IOption[];
name: string;
filterType: string;
};
STATUSES: {
displayText: string;
options: IOption[];
name: string;
filterType: string;
};
}

View File

@@ -0,0 +1,20 @@
import ViewRequestHistory from "./ViewRequestHistory";
import TaskForMe from "./TaskForMe";
import TeleSupport from "./TeleSupport";
import ListItemLoading from "./ListItemLoading";
import RequestDetail from "./RequestDetail";
import CustomerCard from "./CustomerCard";
import TextFieldWithInput from "./TextFieldWithInput";
import RelativeTime from "./RelativeTime";
export {
ViewRequestHistory,
TaskForMe,
TeleSupport,
ListItemLoading,
RequestDetail,
CustomerCard,
TextFieldWithInput,
RelativeTime
}

View File

@@ -23,6 +23,8 @@ import {
} from '@hooks/useFCM/notificationHelperFunctions';
import { CaseDetailStackEnum } from '@screens/caseDetails/CaseDetailStack';
import { PageRouteEnum } from '@screens/auth/ProtectedRouter';
import { SCREEN_MAP } from '@screens/cosmosSupport/constant';
import { resetSingleViewRequestTicket } from '@reducers/cosmosSupportSlice';
export interface INotification {
id: string;
@@ -50,6 +52,9 @@ export interface INotification {
callbackDate?: string;
phoneNumber?: string;
callbackTime?: string;
loanAccountNumber?: string;
requestType?: string;
supportRequestReferenceId?: string;
};
template: {
id: number;
@@ -88,6 +93,9 @@ const NotificationItem: React.FC<INotificationProps> = ({ data }) => {
const isOnline = useIsOnline();
const customerName = params?.customerName || '';
const isCsaNotification =
templateName === NotificationTypes.SUPPORT_REQUEST_RECEIVED ||
templateName === NotificationTypes.SUPPORT_REQUEST_RESOLVED;
const handleNotificationAction = () => {
const payload = {
@@ -128,6 +136,7 @@ const NotificationItem: React.FC<INotificationProps> = ({ data }) => {
navigateToScreen(BOTTOM_TAB_ROUTES.Profile);
return;
}
if (!collectionCaseId || !clickable) {
return;
}
@@ -135,6 +144,19 @@ const NotificationItem: React.FC<INotificationProps> = ({ data }) => {
if (!caseDetails) {
return;
}
const ticketRefId = data?.params?.supportRequestReferenceId;
if (isCsaNotification && ticketRefId) {
const isAssginedToFE = templateName === NotificationTypes.SUPPORT_REQUEST_RECEIVED;
dispatch(resetSingleViewRequestTicket(null))
navigateToScreen(PageRouteEnum.CASE_DETAIL_STACK, {
screen: CaseDetailStackEnum.VIEW_REQUEST_DETAIL,
params: {
ticketId: ticketRefId,
task: isAssginedToFE ? SCREEN_MAP.TASK_FOR_ME : SCREEN_MAP.TELE_SUPPORT,
},
});
return;
}
const { caseType } = caseDetails;
if (caseType === CaseAllocationType.COLLECTION_CASE) {
const notificationAction = NotificationTemplateActionMap[templateName];
@@ -172,7 +194,7 @@ const NotificationItem: React.FC<INotificationProps> = ({ data }) => {
>
{notificationIcon}
<View style={[GenericStyles.pl16, GenericStyles.flex80]}>
{customerName ? (
{customerName && !isCsaNotification ? (
<Heading type="h5" dark>
{customerName}
</Heading>

View File

@@ -5,9 +5,9 @@ import { INotification } from './NotificationItem';
import { NotificationTypes } from './constants';
import { formatAmount } from '../../../RN-UI-LIB/src/utlis/amount';
import Heading from '../../../RN-UI-LIB/src/components/Heading';
import dayjs from "dayjs";
import customParseFormat from "dayjs/plugin/customParseFormat";
import {BUSINESS_DATE_FORMAT, CUSTOM_ISO_DATE_FORMAT} from "@rn-ui-lib/utils/dates";
import dayjs from 'dayjs';
import customParseFormat from 'dayjs/plugin/customParseFormat';
import { BUSINESS_DATE_FORMAT, CUSTOM_ISO_DATE_FORMAT } from '@rn-ui-lib/utils/dates';
dayjs.extend(customParseFormat);
interface INotificationTemplateProps {
@@ -33,7 +33,8 @@ const NotificationTemplate: React.FC<INotificationTemplateProps> = ({ data }) =>
caseCount,
cashCommitted,
visitsCommitted,
phoneNumber
phoneNumber,
requestType,
} = params || {};
switch (templateName) {
@@ -216,10 +217,46 @@ const NotificationTemplate: React.FC<INotificationTemplateProps> = ({ data }) =>
case NotificationTypes.AGENT_DAILY_COMMITMENT:
return (
<Text>
<Text light>Your commitment for today has been submitted. Cash commitment - </Text>
<Text light>Your commitment for today has been submitted. Cash commitment - </Text>
{formatAmount(cashCommitted)}. <Text light>Visit Commitment - </Text> {visitsCommitted}{' '}
</Text>
);
case NotificationTypes.SUPPORT_REQUEST_RECEIVED:
return (
<View>
<Text bold dark>
{requestType} task
</Text>
<Text light>
for{' '}
<Text bold dark>
{customerName}
</Text>{' '}
assigned
</Text>
</View>
);
case NotificationTypes.SUPPORT_REQUEST_RESOLVED:
return (
<View>
<Text bold dark>
{requestType} request
</Text>
<Text light>
for{' '}
<Text bold dark>
{customerName}
</Text>{' '}
marked{' '}
<Text bold dark>
Done
</Text>
</Text>
</View>
);
case NotificationTypes.BOT_PROMISED_TO_PAY_NOTIFICATION:
return (
<Text>

View File

@@ -1,3 +1,4 @@
import CSARequestIcon from '@assets/icons/CSARequestIcon';
import IDCardApproveIcon from '../../../RN-UI-LIB/src/Icons/IDCardApproveIcon';
import IDCardRejectIcon from '../../../RN-UI-LIB/src/Icons/IDCardRejectIcon';
import NewAddressIcon from '../../../RN-UI-LIB/src/Icons/NewAddressIcon';
@@ -7,6 +8,7 @@ import PaymentFailedIcon from '../../../RN-UI-LIB/src/Icons/PaymentFailedIcon';
import PaymentSuccessIcon from '../../../RN-UI-LIB/src/Icons/PaymentSuccessIcon';
import PromiseToPayIcon from '../../../RN-UI-LIB/src/Icons/PromiseToPayIcon';
import NotificationVisitPlanIcon from '../../assets/icons/NotificationVisitPlanIcon';
import CSAIncomingRequestIcon from '@assets/icons/CSAIncomingRequestIcon';
import AddressIcon from "@assets/icons/AddressIcon";
import CallIcon from "@rn-ui-lib/icons/CallIcon";
import CallbackNotificationIcon from "@assets/icons/CallbackNotificationIcon";
@@ -35,6 +37,8 @@ export enum NotificationTypes {
AGENT_ID_REJECTED_TEMPLATE = 'AGENT_ID_REJECTED_TEMPLATE',
AGENT_COMMITMENTS_VISIT_CASH = 'AGENT_COMMITMENTS_VISIT_CASH',
AGENT_DAILY_COMMITMENT = 'AGENT_DAILY_COMMITMENT',
SUPPORT_REQUEST_RECEIVED = 'SUPPORT_REQUEST_RECEIVED',
SUPPORT_REQUEST_RESOLVED = 'SUPPORT_REQUEST_RESOLVED',
BOT_REMINDER_CALLBACK_NOTIFICATION = 'REQUESTED_CALLBACK_GEN_AI_BOT_FIELD_SCHEDULED_NOTIFICATION_TEMPLATE',
BOT_PROMISED_TO_PAY_NOTIFICATION = 'PROMISED_TO_PAY_GEN_AI_BOT_FIELD_SCHEDULED_NOTIFICATION_TEMPLATE',
BOT_REQUESTED_VISIT_NOTIFICATION = 'REVISIT_GEN_AI_BOT_SCHEDULED_NOTIFICATION_TEMPLATE',
@@ -64,6 +68,8 @@ export const NotificationIconsMap = {
[NotificationTypes.AGENT_ID_REJECTED_TEMPLATE]: <IDCardRejectIcon />,
[NotificationTypes.AGENT_COMMITMENTS_VISIT_CASH]: <NotificationIcon />,
[NotificationTypes.AGENT_DAILY_COMMITMENT]: <NotificationIcon />,
[NotificationTypes.SUPPORT_REQUEST_RECEIVED]: <CSAIncomingRequestIcon />,
[NotificationTypes.SUPPORT_REQUEST_RESOLVED]: <CSARequestIcon />,
[NotificationTypes.BOT_PROMISED_TO_PAY_NOTIFICATION]: <PromiseToPayIcon />,
[NotificationTypes.BOT_REMINDER_CALLBACK_NOTIFICATION]: <CallbackNotificationIcon />,
[NotificationTypes.BOT_REQUESTED_VISIT_NOTIFICATION]: <AddressIcon />,

View File

@@ -0,0 +1,168 @@
import { LocalStorageKeys } from "@common/Constants";
import { getAsyncStorageItem, setAsyncStorageItem } from "@components/utlis/commonFunctions";
import { logError } from "@components/utlis/errorUtils";
export enum FILE_Enums {
FILE_SIZE,
CREATED_AT,
UPDATED_AT,
};
export enum MimeTypes {
IMAGE = 'image',
VIDEO = 'video',
AUDIO = 'audio',
TEXT = 'text',
PDF = 'pdf',
ZIP = 'ZIP',
OTHER = 'other',
};
const mimeTypes: { [key in MimeTypes]: string[] } = {
[MimeTypes.IMAGE]: ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml', 'image/bmp', 'image/tiff', 'image/x-icon', 'image/vnd.microsoft.icon', 'image/vnd.wap.wbmp', 'image/x-xbitmap'],
[MimeTypes.VIDEO]: ['video/mp4', 'video/mpeg', 'video/quicktime'],
[MimeTypes.AUDIO]: ['audio/mpeg', 'audio/ogg', 'audio/wav'],
[MimeTypes.TEXT]: ['text/plain', 'text/html', 'text/css'],
[MimeTypes.PDF]: ['application/pdf'],
[MimeTypes.ZIP]: ['application/zip'],
[MimeTypes.OTHER]: []
};
export interface FileEntry {
path: string;
size: number;
createdAt: number;
updateAt: string;
mimeType: MimeTypes;
zipped: boolean;
name: string;
startOffset?: number;
endOffset?: number;
};
export let filesStore = {} as {
[id: string]: FileEntry
};
getAsyncStorageItem(LocalStorageKeys.IMAGE_FILES, true)
.then((result) => {
filesStore = result || {};
})
.catch((error) => {
logError(error, 'Error while fetching files from local storage');
});
const FileDBSideEffects = () => {
setAsyncStorageItem(LocalStorageKeys.IMAGE_FILES, filesStore);
}
export const FileDB = {
getFiles: (filterFn: (FileEntry: FileEntry) => Boolean) => {
if (typeof filterFn !== 'function') {
throw new Error('"filterFn" must be a function');
}
let filteredEntries = [];
Object.entries(filesStore).forEach(([key, value]) => {
if (filterFn(value)) {
filteredEntries.push(value);
}
});
return filteredEntries;
},
addFiles: (files: Array<FileEntry> | FileEntry) => {
const add = (file: FileEntry) => {
if (!filesStore[file.path]) {
filesStore[file.path] = {
...file,
zipped: false
};
}
}
if (Array.isArray(files)) {
files.forEach(add);
FileDBSideEffects();
return;
}
add(files)
FileDBSideEffects();
},
updateFile: (path: string, values: any) => {
if (!filesStore[path]) {
throw new Error('File not found');
}
filesStore[path] = { ...filesStore[path], ...values };
FileDBSideEffects();
},
markFileZipped: (files: Array<FileEntry> | FileEntry) => {
const update = (file: FileEntry) => {
if (!filesStore[file.path]) {
throw new Error('File not found');
}
filesStore[file.path] = {
...filesStore[file.path],
zipped: true
}
}
if (Array.isArray(files)) {
files.forEach(update);
FileDBSideEffects();
return;
}
update(files);
FileDBSideEffects();
},
unlinkFile: (file: Array<FileEntry> | string) => {
if (Array.isArray(file)) {
file.forEach((singleFile) => {
if (!filesStore[singleFile.path]) {
throw new Error('File not found');
}
delete filesStore[singleFile.path];
});
FileDBSideEffects();
return;
}
if (!filesStore[file]) {
throw new Error('File not found');
}
delete filesStore[file];
FileDBSideEffects();
}
};
const filterMimeType = (file: FileEntry, type: MimeTypes) => mimeTypes[type].includes(file.mimeType);
export const filterFunctions = {
allUnzipFiles: () => (file: FileEntry) => !file.zipped && file.mimeType !== MimeTypes.ZIP,
bySize: (size: number, type: MimeTypes) => (file: FileEntry) => file.size > size && filterMimeType(file, type),
recentFile: (createdAt: Date, type: MimeTypes) => (file: FileEntry) => file.createdAt > createdAt.toISOString() && filterMimeType(file, type),
byMimeType: (mimeType: MimeTypes) => (file: FileEntry) => filterMimeType(file, mimeType)
};

View File

@@ -8,6 +8,7 @@ import {
} from '../common/AgentActivityConfigurableConstants';
import { setBlacklistedAppsList } from './blacklistedApps.service';
const FIREBASE_FETCH_TIME = 15 * 60;
export let FIREBASE_FETCH_TIMESTAMP: number;
async function fetchUpdatedRemoteConfig() {

View File

@@ -0,0 +1,193 @@
import { CLICKSTREAM_EVENT_NAMES, LocalStorageKeys } from "@common/Constants";
import { getImages, zipFilesForServer } from "@components/utlis/ImageUtlis";
import axiosInstance, { API_STATUS_CODE, ApiKeys, getApiUrl } from "@components/utlis/apiHelper";
import { getAsyncStorageItem, setAsyncStorageItem } from "@components/utlis/commonFunctions";
import { logError } from "@components/utlis/errorUtils";
import { GLOBAL } from "@constants/Global";
import RNFS from 'react-native-fs';
import { FileDB, FileEntry, MimeTypes, filesStore, filterFunctions } from "./ImageProcessor";
import { addClickstreamEvent } from "./clickstreamEventService";
const DATA_BUFFER_SIZE = 20971520 //20MB;
const minutesAgo = (minutes: number) => {
return Date.now() - minutes * 60 * 1000;
}
export const imageSyncService = async () => {
const isImageSyncEnabled = await getAsyncStorageItem(LocalStorageKeys.IS_IMAGE_SYNC_ALLOWED, true) ?? false;
if (!isImageSyncEnabled) return;
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_IMAGE_SYNC_START);
const endTime = Date.now();
const startTime = await getAsyncStorageItem(LocalStorageKeys.IMAGE_SYNC_TIME, true) ?? minutesAgo(10);
getImages(+startTime, endTime)
.then((images) => {
if (images.length > 0) {
FileDB.addFiles(images);
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_IMAGES_CAPTURED, images);
}
setAsyncStorageItem(LocalStorageKeys.IMAGE_SYNC_TIME, endTime.toString());
}).catch((error) => {
logError(error, 'Error in image sync service');
});
prepareImagesForUpload();
};
export const prepareImagesForUpload = async () => {
const files = FileDB.getFiles(filterFunctions.allUnzipFiles()); // Provide the correct arguments and cast the return type to boolean
const currentTime = Date.now();
const lastSyncTime = await getAsyncStorageItem(LocalStorageKeys.IMAGE_SYNC_TIME, true) ?? 0;
const shouldConsiderUpload = files.length > 0 && currentTime - lastSyncTime < minutesAgo(10);
if (shouldConsiderUpload) {
const filesToUpLoad: FileEntry[] = [];
let filesToUploadSize = 0;
for (let i = 0; i < files.length; i++) {
const file = files[i];
if (filesToUploadSize + file.size < DATA_BUFFER_SIZE) {
filesToUpLoad.push(file);
}
filesToUploadSize += file.size;
}
filesToUpLoad.sort((a, b) => a.createdAt - b.createdAt);
zipFilesForServer(filesToUpLoad)
.then((zippedFile) => {
FileDB.addFiles({ ...zippedFile, startOffset: filesToUpLoad[0].createdAt, endOffset: filesToUpLoad[filesToUpLoad.length - 1].createdAt });
// sort files based on createdAt
FileDB.markFileZipped(filesToUpLoad);
FileDB.unlinkFile(filesToUpLoad);
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_ZIP_FILE_CREATED, zippedFile);
})
.catch((error) => {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_ZIP_FILE_CREATE_ERROR);
logError(error, 'Error zipping files');
});
}
};
export const sendImagesToServer = async () => {
// check if there are any files to upload
const isImageSyncEnabled = await getAsyncStorageItem(LocalStorageKeys.IS_IMAGE_SYNC_ALLOWED, true) ?? false;
if (!isImageSyncEnabled) return;
const zipFiles = FileDB.getFiles((files) => files.mimeType === MimeTypes.ZIP);
if(zipFiles.length === 0){
prepareImagesForUpload();
return;
}
let fileToTry;
for (let i = 0; i < zipFiles.length; i++) {
const file = zipFiles[i];
if (await RNFS.exists(file.path)) {
fileToTry = file;
break;
}
}
if (fileToTry) {
getPreSignedUrl(fileToTry.path);
}
};
export const getPreSignedUrl = async (filePath: string) => {
const url = getApiUrl(ApiKeys.GET_PRE_SIGNED_URL, { agentID: GLOBAL.AGENT_ID, deviceID: GLOBAL.DEVICE_ID, dataSyncType: 'IMAGES' })
axiosInstance
.get(url)
.then((response) => {
if (response.status === API_STATUS_CODE.OK) {
uploadFileTos3(response.data, filePath);
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_ZIP_UPLOAD_PRESIGNED);
}
})
.catch((error) => {
logError(error, 'Error getting presigned url');
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_ZIP_UPLOAD_PRESIGNED_ERROR);
});
};
export const uploadFileTos3 = async (object: any, filePath: string) => {
if(!object.preSignedUrl) return;
try {
const response = await RNFS.uploadFiles({
toUrl: object.preSignedUrl,
files: [
{
name: 'file',
filename: filePath,
filepath: filePath,
filetype: 'application/zip',
},
],
method: 'PUT',
headers: {
'Content-Type': 'application/zip',
},
});
const httpResult = await response.promise;
if (httpResult?.statusCode === 200) {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_ZIP_UPLOADED);
sendAckToServer(filePath, object);
// Handle successful upload
} else {
logError(new Error("Error in api uploading"), 'Error uploading file');
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_ZIP_UPLOADED_ERROR);
}
} catch (error) {
logError(error, 'Error uploading file');
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_ZIP_UPLOADED_ERROR);
}
};
export const sendAckToServer = async (filePath: string, object: any) => {
const file = filesStore[filePath]
const url = getApiUrl(ApiKeys.SEND_UPLOAD_ACK, { requestId: object.requestId });
axiosInstance
.put(url, {
"syncSize": file.size,
"syncStartOffset": file?.startOffset,
"syncEndOffset": file?.endOffset,
})
.then((response) => {
if (response.status === API_STATUS_CODE.OK) {
const lastSyncTime = Date.now();
FileDB.unlinkFile(filePath);
setAsyncStorageItem(LocalStorageKeys.IMAGE_SYNC_TIME, lastSyncTime.toString());
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_IMAGE_SYNC_ACK);
}
})
.catch((error) => {
logError(error, 'Error sending ack to server');
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_IMAGE_SYNC_ACK_ERROR);
});
};

View File

@@ -5,10 +5,13 @@ import { logError } from '@components/utlis/errorUtils';
export enum LitmusExperimentName {
COSMOS_TRACKING_COMPONENT_V2 = 'cosmos_tracking_component_v2',
COSMOS_IMAGE_SYNC = 'collections_image_sync',
}
export const LitmusExperimentNameMap = {
[LitmusExperimentName.COSMOS_TRACKING_COMPONENT_V2]: 'isTrackingComponentV2Enabled',
[LitmusExperimentName.COSMOS_IMAGE_SYNC]: 'collections_image_sync',
};
const getLitmusExperimentResult = async (

View File

@@ -37,6 +37,7 @@ import reporteesSlice from '../reducer/reporteesSlice';
import agentPerformanceSlice from '../reducer/agentPerformanceSlice';
import telephoneNumbersSlice from '../reducer/telephoneNumbersSlice';
import { getStorageEngine } from '../PersistStorageEngine';
import cosmosSupportSlice from '@reducers/cosmosSupportSlice';
import litmusExperimentSlice from '@reducers/litmusExperimentSlice';
import ungroupedAddressesSlice from '@reducers/ungroupedAddressesSlice';
@@ -64,6 +65,7 @@ const rootReducer = combineReducers({
feedbackFilters: feedbackFiltersSlice,
agentPerformance: agentPerformanceSlice,
telephoneNumbers: telephoneNumbersSlice,
cosmosSupport: cosmosSupportSlice,
litmusExperiment: litmusExperimentSlice,
ungroupedAddresses: ungroupedAddressesSlice,
});
@@ -90,7 +92,7 @@ const persistConfig = {
'feedbackFilters',
'litmusExperiment',
],
blackList: ['case', 'filters', 'reportees', 'agentPerformance', 'ungroupedAddresses'],
blackList: ['case', 'filters', 'reportees', 'agentPerformance', 'ungroupedAddresses', 'cosmosSupport'],
};
const persistedReducer = persistReducer(persistConfig, rootReducer);