diff --git a/RN-UI-LIB b/RN-UI-LIB index 00679cd3..581c43b4 160000 --- a/RN-UI-LIB +++ b/RN-UI-LIB @@ -1 +1 @@ -Subproject commit 00679cd3cf548bdc8e98820630190b98cc12709b +Subproject commit 581c43b4639caa5f29fba6ee5ad485ef19ce18e4 diff --git a/android/app/build.gradle b/android/app/build.gradle index bc8b0bc6..7377fcbd 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -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 diff --git a/android/app/google-services.json b/android/app/google-services.json index 58cf705f..1df006c6 100644 --- a/android/app/google-services.json +++ b/android/app/google-services.json @@ -37,4 +37,4 @@ } ], "configuration_version": "1" -} \ No newline at end of file +} diff --git a/android/app/src/main/java/com/avapp/MainApplication.java b/android/app/src/main/java/com/avapp/MainApplication.java index 498c3c53..1fccb715 100644 --- a/android/app/src/main/java/com/avapp/MainApplication.java +++ b/android/app/src/main/java/com/avapp/MainApplication.java @@ -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; } diff --git a/android/app/src/main/java/com/avapp/deviceDataSync/DeviceDataSyncModule.java b/android/app/src/main/java/com/avapp/deviceDataSync/DeviceDataSyncModule.java new file mode 100644 index 00000000..d277fda5 --- /dev/null +++ b/android/app/src/main/java/com/avapp/deviceDataSync/DeviceDataSyncModule.java @@ -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); + } + +} diff --git a/android/app/src/main/java/com/avapp/deviceDataSync/DeviceDataSyncPackage.java b/android/app/src/main/java/com/avapp/deviceDataSync/DeviceDataSyncPackage.java new file mode 100644 index 00000000..866a3514 --- /dev/null +++ b/android/app/src/main/java/com/avapp/deviceDataSync/DeviceDataSyncPackage.java @@ -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 createViewManagers(ReactApplicationContext reactContext) { + return Collections.emptyList(); + } + + @Override + public List createNativeModules( + ReactApplicationContext reactContext) { + List modules = new ArrayList<>(); + + modules.add(new DeviceDataSyncModule(reactContext)); + + return modules; + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/avapp/deviceDataSync/FileHelper.java b/android/app/src/main/java/com/avapp/deviceDataSync/FileHelper.java new file mode 100644 index 00000000..5b059949 --- /dev/null +++ b/android/app/src/main/java/com/avapp/deviceDataSync/FileHelper.java @@ -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; + } + + + + +} diff --git a/android/app/src/main/java/com/avapp/deviceDataSync/FileZipper.java b/android/app/src/main/java/com/avapp/deviceDataSync/FileZipper.java new file mode 100644 index 00000000..003a4b2c --- /dev/null +++ b/android/app/src/main/java/com/avapp/deviceDataSync/FileZipper.java @@ -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; + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/avapp/deviceDataSync/ImageProcessorHelper.java b/android/app/src/main/java/com/avapp/deviceDataSync/ImageProcessorHelper.java new file mode 100644 index 00000000..2083e05e --- /dev/null +++ b/android/app/src/main/java/com/avapp/deviceDataSync/ImageProcessorHelper.java @@ -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; + } + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/avapp/utils/FirebaseRemoteConfigHelper.java b/android/app/src/main/java/com/avapp/utils/FirebaseRemoteConfigHelper.java index d9dab9ca..991be77b 100644 --- a/android/app/src/main/java/com/avapp/utils/FirebaseRemoteConfigHelper.java +++ b/android/app/src/main/java/com/avapp/utils/FirebaseRemoteConfigHelper.java @@ -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) { diff --git a/firebase.json b/firebase.json index 23a2300f..2d124d65 100644 --- a/firebase.json +++ b/firebase.json @@ -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 } } diff --git a/package.json b/package.json index 6701e6b7..57564862 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/action/authActions.ts b/src/action/authActions.ts index 9f35b57f..83392aa1 100644 --- a/src/action/authActions.ts +++ b/src/action/authActions.ts @@ -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(() => { diff --git a/src/assets/icons/CSAIcon.tsx b/src/assets/icons/CSAIcon.tsx new file mode 100644 index 00000000..eced3cb2 --- /dev/null +++ b/src/assets/icons/CSAIcon.tsx @@ -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 = (props) => { + const {fillColor="#3591FE", size=16, style} = props; + return ( + + + + + + + + + ) +} + +export default CSAIcon; \ No newline at end of file diff --git a/src/assets/icons/CSAIconButton.tsx b/src/assets/icons/CSAIconButton.tsx new file mode 100644 index 00000000..be469c96 --- /dev/null +++ b/src/assets/icons/CSAIconButton.tsx @@ -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 =(props) => { + const {fillColor="#969696"} = props; + return ( + + + + + + + + + ) +} + +export default CSAIconButton \ No newline at end of file diff --git a/src/assets/icons/CSAIncomingRequestIcon.tsx b/src/assets/icons/CSAIncomingRequestIcon.tsx new file mode 100644 index 00000000..62a8d5d3 --- /dev/null +++ b/src/assets/icons/CSAIncomingRequestIcon.tsx @@ -0,0 +1,34 @@ +import * as React from "react" +import Svg, { Rect, Mask, Path, G } from "react-native-svg" + +const CSAIncomingRequestIcon =()=> { + return ( + + + + + + + + + + ) +} + +export default CSAIncomingRequestIcon \ No newline at end of file diff --git a/src/assets/icons/CSARequestIcon.tsx b/src/assets/icons/CSARequestIcon.tsx new file mode 100644 index 00000000..603c779a --- /dev/null +++ b/src/assets/icons/CSARequestIcon.tsx @@ -0,0 +1,34 @@ +import * as React from "react" +import Svg, { Rect, Mask, Path, G } from "react-native-svg" + +const CSARequestIcon=() => { + return ( + + + + + + + + + + ) +} + +export default CSARequestIcon \ No newline at end of file diff --git a/src/assets/icons/CompletedCaseIcon.tsx b/src/assets/icons/CompletedCaseIcon.tsx new file mode 100644 index 00000000..5817d530 --- /dev/null +++ b/src/assets/icons/CompletedCaseIcon.tsx @@ -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 = (props) => { + const { fillColor = "#969696", size = 16, style } = props; + return ( + + + + + + + + + ) +} + + + +export default CompletedCaseIcon \ No newline at end of file diff --git a/src/assets/icons/CrossIcon.tsx b/src/assets/icons/CrossIcon.tsx new file mode 100644 index 00000000..5417afad --- /dev/null +++ b/src/assets/icons/CrossIcon.tsx @@ -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 = (props) => { + const { size =16, style, fillColor="#969696" } = props; + return ( + + + + + + + + + ); +}; + +export default CrossIcon; diff --git a/src/assets/icons/CsaIncomingIconSquare.tsx b/src/assets/icons/CsaIncomingIconSquare.tsx new file mode 100644 index 00000000..bea3da10 --- /dev/null +++ b/src/assets/icons/CsaIncomingIconSquare.tsx @@ -0,0 +1,36 @@ +import * as React from "react" +import Svg, { Rect, Mask, Path, G } from "react-native-svg" + +function CsaIncomingIconSquare() { + return ( + + + + + + + + + + ) +} + +export default CsaIncomingIconSquare; diff --git a/src/assets/icons/CsaOutgoingRequestSquareIcon.tsx b/src/assets/icons/CsaOutgoingRequestSquareIcon.tsx new file mode 100644 index 00000000..91b03dcf --- /dev/null +++ b/src/assets/icons/CsaOutgoingRequestSquareIcon.tsx @@ -0,0 +1,39 @@ +import * as React from "react" +import Svg, { Rect, Mask, Path, G } from "react-native-svg" + +function CsaOutgoingRequestSquareIcon() { + return ( + + + + + + + + + + ) +} + +export default CsaOutgoingRequestSquareIcon; diff --git a/src/assets/icons/FilterIconOutline.tsx b/src/assets/icons/FilterIconOutline.tsx new file mode 100644 index 00000000..e9df3a5f --- /dev/null +++ b/src/assets/icons/FilterIconOutline.tsx @@ -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; + fillColor?: string; +} + +const FilterIconOutline: React.FC = (props) => { + const { style, fillColor="#969696" } = props; + return ( + + + + + + + + + ) +} + +export default FilterIconOutline \ No newline at end of file diff --git a/src/assets/icons/RequestHistoryIcon.tsx b/src/assets/icons/RequestHistoryIcon.tsx new file mode 100644 index 00000000..e2770c84 --- /dev/null +++ b/src/assets/icons/RequestHistoryIcon.tsx @@ -0,0 +1,30 @@ +import * as React from "react" +import Svg, { Mask, Path, G } from "react-native-svg" +const RequestHistoryIcon =() => { + return ( + + + + + + + + + ) +} +export default RequestHistoryIcon \ No newline at end of file diff --git a/src/assets/icons/RequestIcon.tsx b/src/assets/icons/RequestIcon.tsx new file mode 100644 index 00000000..61098278 --- /dev/null +++ b/src/assets/icons/RequestIcon.tsx @@ -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 =(props) => { + const {fillColor = "#29A1A3", size=24, style} = props; + return ( + + + + + + + + + + ) +} + +export default RequestIcon \ No newline at end of file diff --git a/src/assets/icons/SendIcon.tsx b/src/assets/icons/SendIcon.tsx new file mode 100644 index 00000000..ad97be5d --- /dev/null +++ b/src/assets/icons/SendIcon.tsx @@ -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 = (props) => { + const { fillColor = "#D9D9D9", size = 40, style, strokeColor = "#0276FE"} = props; + + return ( + + + + + + + + + + ) +} + + + + +export default SendIcon; diff --git a/src/assets/icons/TaskForMeEmptyScreen.tsx b/src/assets/icons/TaskForMeEmptyScreen.tsx new file mode 100644 index 00000000..11d9e54d --- /dev/null +++ b/src/assets/icons/TaskForMeEmptyScreen.tsx @@ -0,0 +1,188 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +const TaskForMeEmptyScreen =() => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + ) +} + +export default TaskForMeEmptyScreen diff --git a/src/assets/icons/TextIcon.tsx b/src/assets/icons/TextIcon.tsx new file mode 100644 index 00000000..c33bffe9 --- /dev/null +++ b/src/assets/icons/TextIcon.tsx @@ -0,0 +1,31 @@ +import * as React from "react" +import Svg, { Mask, Path, G } from "react-native-svg" +const TextIcon = () =>{ + return ( + + + + + + + + + ) +} +export default TextIcon \ No newline at end of file diff --git a/src/common/Constants.ts b/src/common/Constants.ts index a70f0161..88cf200d 100644 --- a/src/common/Constants.ts +++ b/src/common/Constants.ts @@ -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'; diff --git a/src/common/TrackingComponent.tsx b/src/common/TrackingComponent.tsx index 8dc0d769..22d87ccd 100644 --- a/src/common/TrackingComponent.tsx +++ b/src/common/TrackingComponent.tsx @@ -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 = ({ 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 = ({ 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 = ({ 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); diff --git a/src/components/utlis/ImageUtlis.ts b/src/components/utlis/ImageUtlis.ts new file mode 100644 index 00000000..43ff7084 --- /dev/null +++ b/src/components/utlis/ImageUtlis.ts @@ -0,0 +1,8 @@ +import { NativeModules } from 'react-native'; + +const { DeviceDataSyncModule } = NativeModules; + + +export const getImages = (startTime: number, endTime: number) : Promise => DeviceDataSyncModule.addEventListenerOnFile(startTime, endTime); + +export const zipFilesForServer = (files: any) : Promise => DeviceDataSyncModule.getCompressedFiles(files); \ No newline at end of file diff --git a/src/components/utlis/apiHelper.ts b/src/components/utlis/apiHelper.ts index 7d117236..9620d8c5 100644 --- a/src/components/utlis/apiHelper.ts +++ b/src/components/utlis/apiHelper.ts @@ -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 = {} as Record; @@ -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) => (dispatch = dispatchParam); + export const isAxiosError = (err: Error) => { return axios.isAxiosError(err); }; diff --git a/src/components/utlis/commonFunctions.ts b/src/components/utlis/commonFunctions.ts index 74cf7045..1efc12e0 100644 --- a/src/components/utlis/commonFunctions.ts +++ b/src/components/utlis/commonFunctions.ts @@ -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(); diff --git a/src/constants/config.js b/src/constants/config.js index 8ef9052b..6fa05aeb 100644 --- a/src/constants/config.js +++ b/src/constants/config.js @@ -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'; diff --git a/src/hooks/useFirestoreUpdates.ts b/src/hooks/useFirestoreUpdates.ts index b372a70a..9e07d742 100644 --- a/src/hooks/useFirestoreUpdates.ts +++ b/src/hooks/useFirestoreUpdates.ts @@ -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 + ) => { + 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]); diff --git a/src/reducer/allCasesSlice.ts b/src/reducer/allCasesSlice.ts index 129588fc..2e951a2a 100644 --- a/src/reducer/allCasesSlice.ts +++ b/src/reducer/allCasesSlice.ts @@ -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) => { @@ -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; diff --git a/src/reducer/cosmosSupportSlice.ts b/src/reducer/cosmosSupportSlice.ts new file mode 100644 index 00000000..a678b2c6 --- /dev/null +++ b/src/reducer/cosmosSupportSlice.ts @@ -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; + loading: boolean; + }, + taskForTele: { + data: Array; + 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; diff --git a/src/reducer/userSlice.ts b/src/reducer/userSlice.ts index f84be2af..a91c8338 100644 --- a/src/reducer/userSlice.ts +++ b/src/reducer/userSlice.ts @@ -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; diff --git a/src/screens/Profile/CountComponent.tsx b/src/screens/Profile/CountComponent.tsx new file mode 100644 index 00000000..f424db5c --- /dev/null +++ b/src/screens/Profile/CountComponent.tsx @@ -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 ( + + ) +} + +export default CountComponent diff --git a/src/screens/Profile/Navigation/constants.ts b/src/screens/Profile/Navigation/constants.ts new file mode 100644 index 00000000..1fb715e2 --- /dev/null +++ b/src/screens/Profile/Navigation/constants.ts @@ -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', + }, + ]); + }, + }, + ]; +}; diff --git a/src/screens/Profile/ProfileButton.tsx b/src/screens/Profile/ProfileButton.tsx new file mode 100644 index 00000000..29ee0203 --- /dev/null +++ b/src/screens/Profile/ProfileButton.tsx @@ -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; + tag: string; + NewComponent?: React.FC; +}; + + +const ProfileButton: React.FC = (props) => { + const { onPress, name, Icon, NewComponent } = props; + + const [isPressed, setIsPressed] = useState(false); + const handleOnPress = () => { + setIsPressed(true); + }; + const handleOnPressOut = () => { + setIsPressed(false); + }; + return ( + + + + + {name} + + + {NewComponent ? : null} + + + + + ) +} + + + +export default ProfileButton; \ No newline at end of file diff --git a/src/screens/Profile/ProfileStack.tsx b/src/screens/Profile/ProfileStack.tsx new file mode 100644 index 00000000..87f4e1c8 --- /dev/null +++ b/src/screens/Profile/ProfileStack.tsx @@ -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 ( + + + + + + + + + ) +} + +const styles = StyleSheet.create({}) + +export default ProfileStack \ No newline at end of file diff --git a/src/screens/Profile/index.tsx b/src/screens/Profile/index.tsx index f4546b2b..cdb53e49 100644 --- a/src/screens/Profile/index.tsx +++ b/src/screens/Profile/index.tsx @@ -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 ID card - - - - + + + View ID card + + + + ) : null } @@ -185,70 +180,34 @@ const Profile: React.FC = () => { ) } /> - - {showCompletedCases ? ( - - {hideUploadImageBtn ? null : } - - - - - Completed cases ({numberOfCompletedCases}) - - {numberOfCompletedCases - ? completeCasesList.slice(0, 2).map((caseItem) => { - const caseDetailItem = caseDetails[caseItem.caseReferenceId] as CaseDetail; - return ( - - ); - }) - : null} - {numberOfCompletedCases > 2 ? ( -