diff --git a/RN-UI-LIB b/RN-UI-LIB index a6ebb528..e5ab5d07 160000 --- a/RN-UI-LIB +++ b/RN-UI-LIB @@ -1 +1 @@ -Subproject commit a6ebb5280b0348a7b819f5f361f021e50615c23b +Subproject commit e5ab5d07c94ee83ffb30913328bd0734c19fc32f diff --git a/src/assets/icons/FilterIcon.tsx b/src/assets/icons/FilterIcon.tsx new file mode 100644 index 00000000..e157206c --- /dev/null +++ b/src/assets/icons/FilterIcon.tsx @@ -0,0 +1,16 @@ +import * as React from "react"; +import Svg, { Path } from "react-native-svg"; +const FilterIcon = () => ( + + + +); +export default FilterIcon; diff --git a/src/common/Constants.ts b/src/common/Constants.ts new file mode 100644 index 00000000..b5cc32b0 --- /dev/null +++ b/src/common/Constants.ts @@ -0,0 +1,20 @@ +export enum CONDITIONAL_OPERATORS { + EQUALS = 'EQUALS', + LESS_THAN_EQUAL_TO = 'LESS_THAN_EQUAL_TO', +} + +export enum FILTER_TYPES { + DATE = 'DATE', + STRING = 'STRING', + SEARCH = 'SEARCH', +} + +export enum SELECTION_TYPES { + SINGLE = 'SINGLE', + MULTIPLE = 'MULTIPLE', +} + +export interface Option { + label: string; + value: any; +} diff --git a/src/components/screens/allCases/allCasesFilters/FiltersContainer.tsx b/src/components/screens/allCases/allCasesFilters/FiltersContainer.tsx new file mode 100644 index 00000000..6de5c317 --- /dev/null +++ b/src/components/screens/allCases/allCasesFilters/FiltersContainer.tsx @@ -0,0 +1,441 @@ +import React, {useEffect, useMemo} from 'react'; +import {PixelRatio, StyleSheet, TouchableOpacity, View} from 'react-native'; +import {GenericStyles} from '../../../../../RN-UI-LIB/src/styles'; +import Heading from '../../../../../RN-UI-LIB/src/components/Heading'; +import Text from '../../../../../RN-UI-LIB/src/components/Text'; +import {COLORS} from '../../../../../RN-UI-LIB/src/styles/colors'; +import {useSelector} from 'react-redux'; +import {RootState} from '../../../../store/store'; +import TextInput from '../../../../../RN-UI-LIB/src/components/TextInput'; +import CheckboxGroup from '../../../../../RN-UI-LIB/src/components/chechbox/CheckboxGroup'; +import {useAppDispatch} from '../../../../hooks'; +import { + CONDITIONAL_OPERATORS, + FILTER_TYPES, + SELECTION_TYPES, +} from '../../../../common/Constants'; +import {getObjectValueFromKeys} from '../../../utlis/parsers'; +import {IFilter} from '../../../../screens/allCases/interface'; +import { + clearSelectedFilters, + setSelectedFilters, +} from '../../../../reducer/allCasesSlice'; +import RadioGroup from '../../../../../RN-UI-LIB/src/components/radio_button/RadioGroup'; +import RadioButton from '../../../../../RN-UI-LIB/src/components/radio_button/RadioButton'; +import {Search} from '../../../../../RN-UI-LIB/src/utlis/search'; + +interface FilterContainerProps { + closeFilterModal: () => void; +} + +class Data {} + +export const evaluateFilterForCases = ( + caseRecord: Data, + filters: Record, + selectedFilters: Record, +) => { + let evaluatedResult = true; + Object.keys(selectedFilters).forEach(key => { + switch (filters[key].filterType) { + case FILTER_TYPES.DATE: + switch (filters[key].operator) { + case CONDITIONAL_OPERATORS.EQUALS: + if (selectedFilters[key]) { + evaluatedResult = + evaluatedResult && + selectedFilters[key][ + (getObjectValueFromKeys( + caseRecord, + filters[key].fieldToCompare.split('.'), + ) - + new Date().getTime()) / + 1000 + ]; + } + break; + case CONDITIONAL_OPERATORS.LESS_THAN_EQUAL_TO: + if (selectedFilters[key]) { + evaluatedResult = + evaluatedResult && + getObjectValueFromKeys( + caseRecord, + filters[key].fieldToCompare.split('.'), + ) <= + new Date().getTime() - + selectedFilters[key] * 1000; + } + break; + default: + break; + } + case FILTER_TYPES.STRING: + switch (filters[key].operator) { + case CONDITIONAL_OPERATORS.EQUALS: + if (selectedFilters[key]) { + evaluatedResult = + evaluatedResult && + selectedFilters[key][ + getObjectValueFromKeys( + caseRecord, + filters[key].fieldToCompare.split('.'), + ) + ]; + // selectedFilters[key].some( + // filter => { + // console.log('filter.value .....' , filter.value); + // console.log('field value' , getObjectValueFromKeys( + // caseRecord, + // filters[key].fieldToCompare.split( + // '.', + // ))); + // return filter.value === + // getObjectValueFromKeys( + // caseRecord, + // filters[key].fieldToCompare.split( + // '.', + // ), + // ) + // }, + // ); + } + break; + case CONDITIONAL_OPERATORS.LESS_THAN_EQUAL_TO: + if (selectedFilters[key]) { + evaluatedResult = + evaluatedResult && + selectedFilters[key] <= + getObjectValueFromKeys( + caseRecord, + filters[key].fieldToCompare.split('.'), + ); + // selectedFilters[key].some( + // filter => + // filter.value <= + // getObjectValueFromKeys( + // caseRecord, + // filters[key].fieldToCompare.split('.'), + // ), + // ); + } + break; + default: + break; + } + break; + default: + break; + } + }); + return evaluatedResult; +}; + +const FiltersContainer: React.FC = props => { + const {closeFilterModal} = props; + const {filters, selectedFilters, casesList, filterList} = useSelector( + (state: RootState) => ({ + filters: state.allCases.filters, + selectedFilters: state.allCases.selectedFilters, + casesList: state.allCases.casesList, + filterList: state.allCases.filterList, + }), + ); + const [selectedFiltersMap, setSelectedFiltersMap] = React.useState< + Record + >(selectedFilters); + const filterKeys = Object.keys(filters); + const [selectedFilterKey, setSelectedFilterKey] = React.useState(filterKeys[0] || ''); + const [filterSearchString, setFilterSearchString] = + React.useState(''); + const dispatch = useAppDispatch(); + const handleFilterSelection = (filterValues: any) => { + console.log('handleFilterSelection', filterValues); + console.log(filterValues); + selectedFilterKey && + setSelectedFiltersMap({[selectedFilterKey]: filterValues}); + }; + const disableApplyButton = useMemo( + () => Object.keys(selectedFiltersMap).length === 0, + [selectedFiltersMap], + ); + + const applyFilter = () => { + // dispatch( + // setFilterList( + // casesList.filter(record => + // evaluateFilterForCases( + // record, + // Object.keys(selectedFilters), + // filters, + // selectedFilters, + // ), + // ), + // ), + // ); + dispatch(setSelectedFilters(selectedFiltersMap)); + closeFilterModal(); + }; + + const onClearAll = () => { + setSelectedFiltersMap({}); + Object.keys(selectedFilters).length > 0 && dispatch(clearSelectedFilters()); + }; + + const Options = React.useMemo(() => { + if (selectedFilterKey) { + const options = + filterSearchString.length > 0 + ? Search( + filterSearchString, + filters[selectedFilterKey].options || [], + {keys: ['label']}, + ).map(option => option.obj) + : filters[selectedFilterKey].options; + console.log('options..........', options); + switch (filters[selectedFilterKey].selectionType) { + case SELECTION_TYPES.MULTIPLE: + // const options = filters[selectedFilterKey].options?.filter( + // option => + // option.label + // .toLowerCase() + // .indexOf(filterSearchString) > -1, + // ); + // const selectedFilterValues = selectedFilters[ + // selectedFilterKey + // ]?.map((x: any) => x.value); + // console.log("selectedFilters", selectedFilterValues) + return ( + + + + ); + case SELECTION_TYPES.SINGLE: + return ( + + + {options?.map(option => ( + + ))} + + + ); + } + } + return <>; + }, [selectedFilterKey, selectedFiltersMap, filterSearchString]); + + return ( + + + + Filters + + + + Close + + + + + + {filterKeys.map( + key => + ( + { + setSelectedFilterKey(key); + setFilterSearchString(''); + }}> + + {filters[key].displayText} + + {selectedFiltersMap[key] && ( + + + {typeof selectedFiltersMap[key] === + 'object' + ? Object.keys( + selectedFiltersMap[key], + ).length + : 1} + + + )} + + ), + )} + + + {selectedFilterKey && ( + <> + + + setFilterSearchString(value) + } + /> + + {Options} + + )} + + + + + + Clear all + + + + + Apply + + + + + ); +}; + +const styles = StyleSheet.create({ + blueText: { + color: COLORS.TEXT.BLUE, + }, + borderBottom: { + borderColor: COLORS.BORDER.PRIMARY, + borderWidth: 1, + }, + mv8: { + marginVertical: 4, + }, + p8: { + padding: 8, + }, + filterColumn: { + flex: 1, + alignItems: 'stretch', + }, + filterSelector: { + paddingHorizontal: 12, + paddingVertical: 8, + }, + selectedFilterKey: { + backgroundColor: COLORS.BACKGROUND.BLUE, + borderRadius: 6, + }, + borderRight1: { + borderRightWidth: 1, + borderRightColor: COLORS.BORDER.PRIMARY, + }, + bottomCTA: { + paddingVertical: 12, + }, + applyButton: { + backgroundColor: COLORS.TEXT.BLUE, + }, + clearAllButton: { + borderTopColor: COLORS.BORDER.PRIMARY, + borderRightColor: COLORS.BORDER.PRIMARY, + borderRightWidth: 1, + borderTopWidth: 1, + }, + ph7: { + paddingHorizontal: 7, + }, + filterCount: { + backgroundColor: COLORS.BACKGROUND.SILVER, + borderColor: COLORS.BORDER.PRIMARY, + borderWidth: 1, + height: PixelRatio.roundToNearestPixel(25), + width: PixelRatio.roundToNearestPixel(25), + borderRadius: 20, + alignItems: 'center', + }, + filterCountSelected: { + backgroundColor: COLORS.TEXT.BLUE, + height: PixelRatio.roundToNearestPixel(25), + width: PixelRatio.roundToNearestPixel(25), + borderRadius: 20, + alignItems: 'center', + }, +}); + +export default FiltersContainer; diff --git a/src/components/utlis/parsers.ts b/src/components/utlis/parsers.ts new file mode 100644 index 00000000..22ece6dd --- /dev/null +++ b/src/components/utlis/parsers.ts @@ -0,0 +1,11 @@ +export const getObjectValueFromKeys: (obj: Record , keysArray: Array) => any = (obj , keysArray) => { + + if (keysArray.length && obj[keysArray[0]]){ + return getObjectValueFromKeys(obj[keysArray[0]], keysArray.slice(1)); + }else if (obj){ + console.log(obj); + return obj + } + + return null; +} diff --git a/src/constants/config.js b/src/constants/config.js index 2846c4ad..3cb54d22 100644 --- a/src/constants/config.js +++ b/src/constants/config.js @@ -1 +1 @@ -export const BASE_AV_APP_URL = 'https://dev-longhorn-server.np.navi-tech.in/av'; \ No newline at end of file +export const BASE_AV_APP_URL = 'https://dev-longhorn-server.np.navi-tech.in/av/'; diff --git a/src/reducer/allCasesSlice.ts b/src/reducer/allCasesSlice.ts index ec388c98..fe31bafe 100644 --- a/src/reducer/allCasesSlice.ts +++ b/src/reducer/allCasesSlice.ts @@ -1,8 +1,10 @@ -import {_map} from '../../RN-UI-LIB/src/utlis/common'; import {Search} from '../../RN-UI-LIB/src/utlis/search'; import {createSlice} from '@reduxjs/toolkit'; import {navigateToScreen} from '../components/utlis/navigationUtlis'; -import {ICaseItem} from '../screens/allCases/interface'; +import { + ICaseItem, IFilter, +} from '../screens/allCases/interface'; +import {CONDITIONAL_OPERATORS, FILTER_TYPES, SELECTION_TYPES} from "../common/Constants"; export type ICasesMap = {[key: string]: ICaseItem}; @@ -21,6 +23,9 @@ interface IAllCasesSlice { newlyPinnedCases: number; completedCases: number; caseDetails: any; + filters: Record; + searchQuery: string; + selectedFilters: Record } const initialState: IAllCasesSlice = { @@ -38,6 +43,89 @@ const initialState: IAllCasesSlice = { newlyPinnedCases: 0, completedCases: 0, caseDetails: {}, + filters: { + pincodes: { + filterType: FILTER_TYPES.STRING, + selectionType: SELECTION_TYPES.MULTIPLE, + displayText: 'Pincode', + fieldToCompare: 'currentTask.metadata.address.pinCode', + operator: CONDITIONAL_OPERATORS.EQUALS, + options: [ + { + label: "500000", + value: "500000" + }, + { + label: "560036", + value: "560036" + }, + { + label: "560037", + value: "560037" + } + ] + }, + caseStatus: { + filterType: FILTER_TYPES.STRING, + selectionType: SELECTION_TYPES.MULTIPLE, + displayText: 'Feedback', + fieldToCompare: 'caseStatus', + operator: CONDITIONAL_OPERATORS.EQUALS, + options: [ + { + label: "New", + value: "NEW" + }, + { + label: "Unassigned", + value: "UNASSIGNED" + }, + { + label: "Assigned", + value: "ASSIGNED" + }, + { + label: "In Progress", + value: "IN_PROGRESS" + }, + { + label: "Closed", + value: "CLOSED" + }, + { + label: "Force closed", + value: "FORCE_CLOSED" + }, + { + label: "Expired", + value: "EXPIRED" + } + ] + }, + allocatedAt: { + filterType: FILTER_TYPES.DATE, + selectionType: SELECTION_TYPES.SINGLE, + displayText: 'Allocation Date', + fieldToCompare: 'allocatedAt', + operator: CONDITIONAL_OPERATORS.LESS_THAN_EQUAL_TO, + options: [ + { + label: "Last 2 days", + value: 172800 + }, + { + label: "Last 5 days", + value: 432000 + }, + { + label: "Last 10 days", + value: 864000 + } + ], + }, + }, + searchQuery: '', + selectedFilters: {} }; const getPinnedListDetails = (casesList: ICaseItem[]) => { @@ -71,7 +159,7 @@ const allCasesSlice = createSlice({ prev[item.id] = { ...item, isSynced: true, - + }; return prev; }, initialValue); @@ -214,7 +302,22 @@ const allCasesSlice = createSlice({ console.log("is side data notfound") state.caseDetails[id].isSynced = false; } - + }, + setFilters: (state , action) => { + state.filters = {...state.filters , ...action.payload} + }, + setSelectedFilters: (state , action) => { + state.selectedFilters = {...state.selectedFilters , ...action.payload} + }, + clearSelectedFilters: (state) => { + state.selectedFilters = {}; + }, + setFilterList: (state, action) => { + console.log('filterList.....', action.payload); + state.filterList = action.payload; + }, + setSearchQuery: (state, action) => { + state.searchQuery = action.payload; } }, }); @@ -228,10 +331,15 @@ export const { filterData, proceedToTodoList, deleteIntermediateTodoListItem, + setSelectedFilters, + setFilterList, + clearSelectedFilters, + setFilters, setSelectedTodoListMap, resetSelectedTodoList, updateCaseDetail, - updateSingleCase + updateSingleCase, + setSearchQuery, } = allCasesSlice.actions; export default allCasesSlice.reducer; diff --git a/src/screens/allCases/AllCases.tsx b/src/screens/allCases/AllCases.tsx index 7ff726c5..26510a4f 100644 --- a/src/screens/allCases/AllCases.tsx +++ b/src/screens/allCases/AllCases.tsx @@ -1,6 +1,12 @@ import React, {useEffect, useMemo, useState} from 'react'; -import {StyleSheet, View, VirtualizedList} from 'react-native'; -import {useSelector} from 'react-redux'; +import { + Modal, + StyleSheet, + TouchableOpacity, + View, + VirtualizedList, +} from 'react-native'; +import {useDispatch, useSelector} from 'react-redux'; import Text from '../../../RN-UI-LIB/src/components/Text'; import {GenericStyles} from '../../../RN-UI-LIB/src/styles'; import {COLORS} from '../../../RN-UI-LIB/src/styles/colors'; @@ -8,49 +14,61 @@ import {RootState} from '../../store/store'; import {CaseStatuses, ICaseItem, CaseTypes} from './interface'; import {COMPLETED_STATUSES, ListHeaderItems} from './contants'; import CaseItem from './CaseItem'; +import {Search} from '../../../RN-UI-LIB/src/utlis/search'; import CasesActionButtons from './CasesActionButtons'; import FloatingInfoText from '../../components/floatingInfoText'; +import {setSearchQuery} from '../../reducer/allCasesSlice'; +import FiltersContainer, { + evaluateFilterForCases, +} from '../../components/screens/allCases/allCasesFilters/FiltersContainer'; +import TextInput from '../../../RN-UI-LIB/src/components/TextInput'; +import SearchIcon from '../../../RN-UI-LIB/src/Icons/SearchIcon'; +import FilterIcon from '../../assets/icons/FilterIcon'; export const Separator = () => ; export const getItem = (item: Array, index: number) => item[index]; const AllCases = () => { - const {casesList, newlyPinnedCases, selectedTodoListCount} = useSelector( - (state: RootState) => state.allCases, - ); + const { + casesList, + newlyPinnedCases, + selectedTodoListCount, + selectedFilters, + filters, + searchQuery, + } = useSelector((state: RootState) => state.allCases); + const dispatch = useDispatch(); + const [pendingCases , setPendingCases] = useState([]); - const pendingCases = useMemo( - () => - casesList.filter(caseItem => { - if (!COMPLETED_STATUSES.includes(caseItem.caseStatus)) { - return caseItem; - } - }), - [casesList], - ); - - const [filteredList, setFilteredList] = - useState>(pendingCases); - - useEffect(() => { - setFilteredList(pendingCases); - }, [pendingCases]); + useEffect(()=>{ + const filteredList = casesList.filter(caseItem => { + return !COMPLETED_STATUSES.includes(caseItem.caseStatus) && + evaluateFilterForCases( + caseItem, + filters, + selectedFilters, + ); + }); + console.log('inside useEffect'); + // @ts-ignore + setPendingCases(searchQuery.length > 0 ? Search(searchQuery , filteredList || [] , { keys: ['customerInfo.name']}).map(filteredListItem => filteredListItem.obj) : filteredList); + },[casesList , filters , selectedFilters , searchQuery]) const {compiledList, completedPinnedCasesCount, pendingPinnedCasesCount} = useMemo(() => { const pinnedList: Array = []; const otherList: Array = []; const compiledList: Array = []; - if (!filteredList.length) { + if (!pendingCases.length) { return {}; } // Filter input - compiledList.push(ListHeaderItems.FILTER as ICaseItem); + // compiledList.push(ListHeaderItems.FILTER as ICaseItem); //extracting data for todo list and other cases - filteredList.forEach(caseItem => { + pendingCases.forEach(caseItem => { if ( caseItem.pinRank === null || caseItem.pinRank === undefined @@ -88,34 +106,75 @@ const AllCases = () => { completedPinnedCasesCount, pendingPinnedCasesCount, }; - }, [filteredList]); + }, [pendingCases]); const handleSearchChange = (query: string) => { - const filterList = pendingCases.filter(caseItem => caseItem); + // const filterList = query.length > 0 ? Search(query , filteredList || [] , { keys: ['customerInfo.name']}).map(filteredListItem => filteredListItem.obj) : filteredList; + // setFilteredList(filterList); + dispatch(setSearchQuery(query)); }; + const [showFilterModal, setShowFilterModal] = + React.useState(false); return ( + <> + + } + onChangeText={handleSearchChange} + placeholder="Search by name, address" + /> + setShowFilterModal(prev => !prev)}> + + + + setShowFilterModal(prev => !prev)} + visible={showFilterModal}> + + setShowFilterModal(prev => !prev) + } + /> + + {compiledList ? ( - ( - - )} - keyExtractor={item => item.caseReferenceId} - getItemCount={item => item.length} - getItem={getItem} - ItemSeparatorComponent={} - /> + <> + ( + + )} + keyExtractor={item => item.caseReferenceId} + getItemCount={item => item.length} + getItem={getItem} + ItemSeparatorComponent={} + /> + ) : ( Nothing to show )} @@ -141,6 +200,19 @@ const styles = StyleSheet.create({ backgroundColor: COLORS.BACKGROUND.PRIMARY, borderTopWidth: 1, }, + pv12: { + paddingVertical: 12, + }, + filterButton: { + flexBasis: '20%', + alignItems: 'center', + borderWidth: 1, + borderColor: COLORS.BORDER.PRIMARY, + borderRadius: 10, + height: 52, + justifyContent: 'center', + marginLeft: 4, + }, }); export default AllCases; diff --git a/src/screens/allCases/CaseItem.tsx b/src/screens/allCases/CaseItem.tsx index 99c0dd1a..36900b77 100644 --- a/src/screens/allCases/CaseItem.tsx +++ b/src/screens/allCases/CaseItem.tsx @@ -1,5 +1,11 @@ import React from 'react'; -import {ListRenderItemInfo, StyleSheet, View} from 'react-native'; +import { + ListRenderItemInfo, + Modal, + StyleSheet, + TouchableOpacity, + View, +} from 'react-native'; import SearchIcon from '../../../RN-UI-LIB/src/Icons/SearchIcon'; import {GenericStyles} from '../../../RN-UI-LIB/src/styles'; import TextInput from '../../../RN-UI-LIB/src/components/TextInput'; @@ -8,6 +14,9 @@ import Heading from '../../../RN-UI-LIB/src/components/Heading'; import Text from '../../../RN-UI-LIB/src/components/Text'; import ListItem from './ListItem'; import {COLORS} from '../../../RN-UI-LIB/src/styles/colors'; +import Button from '../../../RN-UI-LIB/src/components/Button'; +import FilterIcon from '../../assets/icons/FilterIcon'; +import FiltersContainer from '../../components/screens/allCases/allCasesFilters/FiltersContainer'; interface ICaseItemProps { data: ListRenderItemInfo; @@ -23,23 +32,46 @@ const CaseItem: React.FC = ({ handleSearchChange, }) => { const {FILTER, CASES_HEADER, TODO_HEADER} = CaseTypes; + const [showFilterModal, setShowFilterModal] = + React.useState(false); switch (caseData.type) { - case FILTER: - return ( - - } - onChangeText={handleSearchChange} - placeholder="Search by name, address" - /> - - ); + // case FILTER: + // return ( + // <> + // + // } + // onChangeText={handleSearchChange} + // placeholder="Search by name, address" + // /> + // setShowFilterModal(prev => !prev)}> + // + // + // + // setShowFilterModal(prev => !prev)} + // visible={showFilterModal}> + // + // setShowFilterModal(prev => !prev) + // } + // /> + // + // + // ); case CASES_HEADER: return ( -