/
Autofill address fields based on address hierarchy

New Backend API to fetch address hierarchy for a selected node :


1const fetchGeneralAddressCode = async (organisationId, extendedModels, params, options = {}) => { 2 3 const { searchType, queryString } = params; 4 ** searchType is hardcoded at frontend and sent in the body** 5 if (!searchType) { 6 throw helper.wrongInputError('Location Key should be present'); 7 } 8 9 const metaData = await addressHierarchy.getAddressMetadata(extendedModels, organisationId); 10 if (!metaData) { 11 throw helper.wrongInputError('Metadata not found'); 12 } 13 const hierarchy = metaData.hierarchy || []; 14 const nodeHierarchyMetaData = hierarchy.find(obj => helper.sanitizeStringCode(obj.location_key) === helper.sanitizeStringCode(searchType)); 15 if (!nodeHierarchyMetaData) { 16 throw helper.wrongInputError('Invalid Location Key'); 17 } 18 const parentMetaDataArr = []; 19 let searchTypeNodeFound = false; 20 if (Array.isArray(hierarchy) && hierarchy.length > 0) { 21 for (let i = 0; i < (hierarchy.length - 1); i++) { 22 if (searchTypeNodeFound || helper.sanitizeStringCode(hierarchy[i].location_key) === helper.sanitizeStringCode(searchType)) { 23 searchTypeNodeFound = true; 24 parentMetaDataArr.push(hierarchy[i + 1]); 25 } 26 } 27 } 28 29 const queryParameters = []; 30 let heirarchyId = nodeHierarchyMetaData.id; 31 let nodeCode = helper.sanitizeStringCode(queryString); 32 33 const toRet = {}; 34 toRet.data = {}; 35 queryParameters.push(organisationId); 36 queryParameters.push(heirarchyId); 37 queryParameters.push(nodeCode); 38 39 const selectPart = 'SELECT addressNode.id as node_id,addressNode.code as node_code,addressNode.name as node_name,addressNode.hierarchy_id as hierarchy_id'; 40 const fromPart = ' FROM addressNode'; 41 let extendedSelectPart = ''; 42 let joinPart = ''; 43 const wherePart = ' WHERE addressNode.organisation_id = $1 and addressNode.hierarchy_id = $2 and addressNode.code = $3'; 44 45 if (parentMetaDataArr.length) { 46 extendedSelectPart = ',addressNode.parent_node_id as parent_node_id,parentAddressNode.code as parent_code,parentAddressNode.name as parent_name'; 47 joinPart = ' JOIN addressNode AS parentAddressNode on addressNode.parent_node_id = parentAddressNode.id'; 48 } 49 const queryToExecute = selectPart + extendedSelectPart + fromPart + joinPart + wherePart; 50 const result = await helper.executeQueryAsync(extendedModels.AddressNode, queryToExecute, queryParameters, options); 51 52 if (result[0] && result[0].node_name) { 53 toRet.data[searchType] = result[0].node_name; 54 } else { 55 throw helper.wrongInputError('Data not found for this location key'); 56 } 57 58 let nextNodesResult = result; 59 for (let i = 0; i < parentMetaDataArr.length; i++) { 60 try { 61 heirarchyId = parentMetaDataArr[i].id; 62 nodeCode = nextNodesResult[0].parent_code; 63 const locationKey = parentMetaDataArr[i].location_key; 64 65 queryParameters[1] = heirarchyId; 66 queryParameters[2] = nodeCode; 67 if (i === (parentMetaDataArr.length - 1)) { 68 const lastNodeQueryResult = selectPart + fromPart + wherePart; 69 nextNodesResult = await helper.executeQueryAsync(extendedModels.AddressNode, lastNodeQueryResult, queryParameters, options); 70 } else { 71 nextNodesResult = await helper.executeQueryAsync(extendedModels.AddressNode, queryToExecute, queryParameters, options); 72 } 73 if (nextNodesResult[0] && nextNodesResult[0].node_name) { 74 toRet.data[locationKey] = nextNodesResult[0].node_name; 75 } 76 } catch (err) { 77 //do nothing 78 } 79 } 80 return toRet; 81};
1//success response 2{ 3 "data": { 4 "pincode": "DAM", 5 "city": "Z1", 6 "state": "DAMMAM" 7 } 8}
1{ 2 "searchType": "pincode", 3 "queryString": "DAM" 4}

Changes in filter for address node search:

  1. Instead of showing only Name of options in dropdown it will now be shown as Name (Code).
    This will require another update in find query in common/models/customer-portal-parts/address-node-api-helper.jsgetAddressNodeData

  2. Current filter is applicable only on code or name, changes to add both code and name search
    common/models/customer-portal-parts/address-node-api-helper.jsgetAddressNodeData

Current filter logic:

1conditions.code = {ilike: queryString}; 2 if (shouldSearchByName) { 3 conditions.name = {ilike: queryString}; 4 delete conditions.code; 5 }

Updated filter logic:

1if (shouldSearchByBothNameAndCode) { 2 condition.and.push({ 3 or: [ 4 { name: { ilike: queryString } }, 5 { code: { ilike: queryString } } 6 ] 7 }); 8 } else { 9 conditions.and.push({ code: { ilike: queryString } }); 10 }

Frontend changes:

  1. Changes are to be done at 3 places

    1. Add Consignment modal (src/components/pages/details/AddDetails.tsx )

    2. Counter Booking → Add Address ( src/components/pages/OpsDashboard/Manage/counterBooking/AddAddress2.tsx )

    3. Customer-Portalsrc/components/address/create-address.tsx

  2. List to be populated in dropdown will be fetched after entering atleast 3 characters in the input field for corresponding node( pincode, city, state, country).

  3. Debouncing will be implemented at all places wherever we are fetching list to be populated in the dropdown. (500ms)

  4. In case allowOverrideHierarchy is false , we will let user only enter values from dropdown only.

  5. In case allowOverrideHierarchy is true: user can either select from dropdown or enter other values too.

  6. Only those fields will be disabled for which data is fetched, only when allow override hierarchy is false

Customer Portal changes

  1. Changes in src/components/address/create-address.tsx

General Function to be called whenever any option from rendered dropdown is selected, it make call to api api/CustomerPortal/fetchGeneralAddressCode

1// this logic will be used to get whether isAddressHierarchyPresent is true or false 2 if (Array.isArray(response.data.hierarchy) && response.data.hierarchy.length) { 3 setIsAddressHierarchyPresent(true); 4 }
  1. In case isAddressHierarchyPresent is false then we will render only Simple Input text field.

  2. If isAddressHierarchyPresent is true:

  • It fetches data from AddressNode table on basis of address hierarchy setup, and returns data for current and higher nodes to populate.

  • It also handle the logic to either disbale the higher level nodes based on whether their value is fetched and allow_override_hierarchy is false.

  • It also update (auto populate) the value of higher nodes.

  • Drop down only on those fields which are in hierarchy

1const saveAddressUsingAddressHierarchy = async (nodeValue: string, nodeType: string) => { 2 if (!isKeyOfNewAddress(nodeType) || !nodeValue) return; 3 setLoading(true); 4 5 const fetchLocality = await fetchAddressUsingAddressHierarchy({ queryString: nodeValue, searchType: nodeType }); 6 7 if (!fetchLocality.isSuccess) { 8 setNewAddress({ 9 ...newAddress, 10 city: '', 11 state: '', 12 country: '', 13 }); 14 setDisableLocality(false); 15 } else { 16 let nodeFound = false; 17 const parentLevelNodes = []; 18 for (let i = 0; i < hierarchyData?.length; i += 1) { 19 const curNode = hierarchyData[i].location_key.toLowerCase(); 20 if ((nodeFound || curNode === nodeType.toLocaleLowerCase()) && curNode in fetchLocality?.data) { 21 nodeFound = true; 22 parentLevelNodes.push({ key: hierarchyData[i].location_key, value: fetchLocality?.data[curNode] }); 23 } 24 } 25 const fetchedAddressFieldsObj = parentLevelNodes?.reduce((acc, curr) => { 26 if (isKeyOfNewAddress(curr.key)) { 27 acc[curr.key] = curr.value || null; 28 if (curr.key !== nodeType && (!isAddressMappingAllowed && curr.value)) { 29 setDisableLocality(true, curr.key); 30 } else { 31 setDisableLocality(false, curr.key); 32 } 33 } 34 return acc; 35 }, {} as { [key: string]: string | null }); 36 setNewAddress({ 37 ...newAddress, 38 ...fetchedAddressFieldsObj, 39 }); 40 } 41 42 setLoading(false); 43 };

Function to be called on typing text to be searched in address fields( pincode, city, state country)

1const loadPincodes = async (value: any) => { 2 if (value?.length < 3) return; 3 if (!hierarchyData?.some((node: any) => node.location_key === 'pincode')) return; 4 setLoading(true); 5 6 const response = await fetchPincodeList(value); 7 if (response && response.isSuccess && Array.isArray(response?.data)) { 8 setLocalityPincodesList(response.data || []); 9 } 10 setLoading(false); 11 };

Debouncing logic is also applied :

1 useEffect(() => { 2 loadPincodes(newAddress.pincode); 3 }, [debouncePincode]); 4 5//import { useDebounce } from 'hooks/use-debounce'; using already imported (was being used for w3wCode) useDebounce HoC 6//SEARCH_TIMEOUT = 500 -> debounce time is used by default 7const debouncePincode = useDebounce(newAddress.pincode);

Following rendering logic is used for all nodes (pincode, state, city, country)

  • It renders only dropdown for case when allow_override_hierarchy is false (user can only select options from dropdown only).

  • When allow_override_hierarchy is true: user can type and search will be done in db, if data found user can select any option from dropdown and on basis of it higher level nodes is auto-populated, and can also enter anything out of option in dropdown for current node and higher nodes too.

  • On clearing a node’s value, all higher node's value will also be cleared.

1 const renderPincodeBox = () => { 2 if (isAddressHierarchyPresent && isAddressMappingAllowed && hierarchyFieldsArr.include('pincode')) { 3 return ( 4 <AutoComplete 5 value={newAddress.pincode} 6 className={classes.pincodeInput} 7 dropdownClassName={classes.pincodeInputDropdown} 8 options={ 9 localityPincodesList.map((pincode: any) => { 10 return { 11 label: `${pincode.name} (${pincode.code})`, 12 value: pincode.code, 13 }; 14 }) 15 } 16 onSearch={loadPincodes} 17 onSelect={(e) => saveAddressUsingAddressHierarchy(e, 'pincode')} 18 onChange={(value) => updateAddressFieldWithValidations('pincode', value)} 19 style={{ textAlign: 'left' }} 20 allowClear 21 > 22 <Input 23 className={classes.autoCompleteInputBox} 24 allowClear 25 placeholder={t('address_pincode')} 26 /> 27 </AutoComplete> 28 ); 29 } 30 if (isAddressHierarchyPresent && !isAddressMappingAllowed && hierarchyFieldsArr.include('pincode')) { 31 return ( 32 <Form.Item 33 className={classes.pincodeWithExtra} 34 name="pincode" 35 extra={samplePincode ? `Ex. ${samplePincode}` : undefined} 36 > 37 <Select 38 value={newAddress.pincode} 39 dropdownClassName={classes.pincodeInputDropdown} 40 placeholder={t('address_pincode')} 41 onChange={(e) => { 42 updateAddressFieldWithValidations('pincode', e); 43 const isValid = isValidPincode(e); 44 if (isValid !== validPincode) { 45 setValidPincode(isValid); 46 } 47 }} 48 options={ 49 localityPincodesList.map((pincode: any) => { 50 return { 51 label: `${pincode.name} (${pincode.code})`, 52 value: pincode.code, 53 }; 54 }) 55 } 56 onSelect={(e) => saveAddressUsingAddressHierarchy(e, 'pincode')} 57 defaultActiveFirstOption={false} 58 filterOption={false} 59 onSearch={loadPincodes} 60 showSearch 61 allowClear 62 style={validPincode ? {} : { borderColor: 'red' }} 63 /> 64 </Form.Item> 65 ); 66 } 67 return ( 68 <Input 69 value={newAddress.pincode} 70 className={classes.pincodeInput} 71 placeholder={t('address_pincode')} 72 onChange={(e) => updateAddressFieldWithValidations('pincode', e.target.value)} 73 /> 74 ); 75 };


Additional Details:

Changes in src/network/pickup.api.ts

1//api to fetch states list 2export const FETCH_STATES_LIST = '/api/CustomerPortal/addressManagement/getAddressNodeData?type=State'; 3//api to fetch nodes value based on address hierarchy (new dev api) 4export const FETCH_ADDRESS_FROM_ADDRESS_HIERARCHY = '/api/CustomerPortal/fetchGeneralAddressCode'; 5 6export const fetchAddressUsingAddressHierarchy = (params: any) => { 7 return POST(`${API_BASE_URL}${FETCH_ADDRESS_FROM_ADDRESS_HIERARCHY}`, params); 8}; 9 10//exporting functions to fetch nodes' list to be called from create-address.tsx file on entering sompething in inpit fileds (> 3chars) 11export const fetchCountriesList = (params: any) => { 12 return GET(`${API_BASE_URL}${FETCH_COUNTRIES_NODE_DATA}`, 13 { isDataSearchApplied: true, searchQuery: params, sortResult: true }); 14}; 15 16export const fetchCitiesList = (params: any) => { 17 return GET(`${API_BASE_URL}${FETCH_CITIES_LIST}`, 18 { isDataSearchApplied: true, searchQuery: params, sortResult: true }); 19}; 20 21 22export const fetchStatesList = (params: any) => { 23 return GET(`${API_BASE_URL}${FETCH_STATES_LIST}`, 24 { isDataSearchApplied: true, searchQuery: params, sortResult: true }); 25};

Changes in src/network/api.constants.ts

1export const FETCH_STATES_LIST = '/api/CustomerPortal/addressManagement/getAddressNodeData?type=State'; 2export const FETCH_COUNTRIES_NODE_DATA = '/api/CustomerPortal/addressManagement/getAddressNodeData?type=Country'; 3export const FETCH_ADDRESS_FROM_ADDRESS_HIERARCHY = '/api/CustomerPortal/fetchGeneralAddressCode';


CRMDashboard changes:

Similar logic as of Customer-Portal is used for rendering like on basis of isAddressHierarchyPresent and allowOverrideHierarchy.

1 const saveAddressUsingAddressHierarchy = async (nodeValue, nodeType) => { 2 if (!nodeType) return; 3 4 if (!nodeValue) { 5 handleAddressFields(nodeType, null, false); 6 return; 7 } 8 9 const fetchLocality = await fetchAddressUsingAddressHierarchy({ queryString: nodeValue, searchType: nodeType }); 10 11 if (!fetchLocality.isSuccess) { 12 form.setFieldsValue({ 13 cityName: '', 14 stateName: '', 15 countryName: '', 16 }); 17 setDisableLocality(false, null); 18 } else { 19 handleAddressFields(nodeType, fetchLocality, false); 20 } 21 };
1 2const debouncePincode = lodash.debounce(async (value) => { 3 const pincodesListResp = await searchAddressNodeData({ type: 'pincode', isDataSearchApplied: true, searchQuery: value, sortResult: true }); 4 if (pincodesListResp?.isSuccess && Array.isArray(pincodesListResp?.data)) { 5 setPincodesList(pincodesListResp.data || []); 6 } 7 }, 500); 8 9 const loadPincodes = async (value) => { 10 if (value?.length < 3) return; 11 if (!hierarchyData?.some((node: any) => node.location_key === 'pincode')) return; 12 debouncePincode(value); 13 }; 14
1<FormItem 2 {...formItemLayoutNew} 3 className={classes.formCounterBooking} 4 label={<div>{t("pincode")}</div>} 5 > 6 {!allowOverrideHierarchy && isAddressHierarchyPresent && getFieldDecorator('pincode', { 7 initialValue: address['pincode'], 8 rules: [ 9 { required: false, message: 'Cannot be empty!' }, 10 ], 11 })( 12 <Select 13 style={{ textAlign: 'left' }} 14 showSearch 15 optionFilterProp="children" 16 onChange={(value) => saveAddressUsingAddressHierarchy(value, 'pincode')} 17 placeholder={t("pincode_placeholder")} 18 onSearch={loadPincodes} 19 allowClear 20 > 21 {pincodesList?.map(opt => <Option style={{ textAlign: 'left' }} key={opt.id} value={opt.code}>{opt.name} ({opt.code})</Option>)} 22 </Select>, 23 )} 24 {allowOverrideHierarchy && isAddressHierarchyPresent && getFieldDecorator('pincode', { 25 initialValue: address['pincode'], 26 rules: [ 27 { required: false, message: 'Cannot be empty!' }, 28 ], 29 })( 30 <AutoComplete 31 options={pincodesList?.map(item => ({ label: `${item.name} (${item.code})`, value: item.code }))} 32 onSearch={loadPincodes} 33 onSelect={(value) => saveAddressUsingAddressHierarchy(value, 'pincode')} 34 onChange={(value) => handleAddressFields('pincode', value, !allowOverrideHierarchy)} 35 style={{ textAlign: 'left' }} 36 allowClear 37 > 38 <Input 39 allowClear 40 placeholder={t("pincode_placeholder")} 41 /> 42 </AutoComplete>, 43 )} 44 {!isAddressHierarchyPresent && getFieldDecorator('pincode', { 45 initialValue: address['pincode'], 46 rules: [ 47 { required: false, message: 'Cannot be empty!' }, 48 ], 49 })( 50 <Input placeholder={t("pincode_placeholder")} />, 51 )} 52 </FormItem>

Above logic is updated in

  1. src/components/pages/OpsDashboard/Manage/counterBooking/AddAddress2.tsx

  2. src/components/pages/details/AddDetails.tsx