Change1:
...
New Backend API to fetch address hierarchy for a selected node :
Expand |
---|
title | api/CustomerPortal/fetchGeneralAddressCode |
---|
|
Code Block |
---|
const fetchGeneralAddressCode = async (organisationId, extendedModels, params, options = {}) => {
const { searchType, queryString } = params;
** searchType is hardcoded at frontend and sent in the body**
if (!searchType) {
throw helper.wrongInputError('Location Key should be present');
}
const metaData = await addressHierarchy.getAddressMetadata(extendedModels, organisationId);
if (!metaData) {
throw helper.wrongInputError('Metadata not found');
}
const hierarchy = metaData.hierarchy || [];
const nodeHierarchyMetaData = hierarchy.find(obj => helper.sanitizeStringCode(obj.location_key) === helper.sanitizeStringCode(searchType));
if (!nodeHierarchyMetaData) {
throw helper.wrongInputError('Invalid Location Key');
}
const parentMetaDataArr = [];
let searchTypeNodeFound = false;
if (Array.isArray(hierarchy) && hierarchy.length > 0) {
for (let i = 0; i < (hierarchy.length - 1); i++) {
if (searchTypeNodeFound || helper.sanitizeStringCode(hierarchy[i].location_key) === helper.sanitizeStringCode(searchType)) {
searchTypeNodeFound = true;
parentMetaDataArr.push(hierarchy[i + 1]);
}
}
}
const queryParameters = [];
let heirarchyId = nodeHierarchyMetaData.id;
let nodeCode = helper.sanitizeStringCode(queryString);
const toRet = {};
toRet.data = {};
queryParameters.push(organisationId);
queryParameters.push(heirarchyId);
queryParameters.push(nodeCode);
const selectPart = 'SELECT addressNode.id as node_id,addressNode.code as node_code,addressNode.name as node_name,addressNode.hierarchy_id as hierarchy_id';
const fromPart = ' FROM addressNode';
let extendedSelectPart = '';
let joinPart = '';
const wherePart = ' WHERE addressNode.organisation_id = $1 and addressNode.hierarchy_id = $2 and addressNode.code = $3';
if (parentMetaDataArr.length) {
extendedSelectPart = ',addressNode.parent_node_id as parent_node_id,parentAddressNode.code as parent_code,parentAddressNode.name as parent_name';
joinPart = ' JOIN addressNode AS parentAddressNode on addressNode.parent_node_id = parentAddressNode.id';
}
const queryToExecute = selectPart + extendedSelectPart + fromPart + joinPart + wherePart;
const result = await helper.executeQueryAsync(extendedModels.AddressNode, queryToExecute, queryParameters, options);
if (result[0] && result[0].node_name) {
toRet.data[searchType] = result[0].node_name;
} else {
throw helper.wrongInputError('Data not found for this location key');
}
let nextNodesResult = result;
for (let i = 0; i < parentMetaDataArr.length; i++) {
try {
heirarchyId = parentMetaDataArr[i].id;
nodeCode = nextNodesResult[0].parent_code;
const locationKey = parentMetaDataArr[i].location_key;
queryParameters[1] = heirarchyId;
queryParameters[2] = nodeCode;
if (i === (parentMetaDataArr.length - 1)) {
const lastNodeQueryResult = selectPart + fromPart + wherePart;
nextNodesResult = await helper.executeQueryAsync(extendedModels.AddressNode, lastNodeQueryResult, queryParameters, options);
} else {
nextNodesResult = await helper.executeQueryAsync(extendedModels.AddressNode, queryToExecute, queryParameters, options);
}
if (nextNodesResult[0] && nextNodesResult[0].node_name) {
toRet.data[locationKey] = nextNodesResult[0].node_name;
}
} catch (err) {
//do nothing
}
}
return toRet;
}; |
|
Code Block |
---|
//success response |
...
{
"data": {
"pincode": "DAM",
"city": "Z1",
"state": "DAMMAM"
}
} |
...
Change2:
common/models/customer-portal-parts/address-node
Expand |
---|
|
Code Block |
---|
{
"searchType": "pincode",
"queryString": "DAM"
} |
|
Changes in filter for address node search:
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.js
→ getAddressNodeData
...
Current filter
...
is applicable only on code or name
...
Current filter logic:
...
, changes to add both code and name
search
common/models/customer-portal-parts/address-node-api-helper.js
→ getAddressNodeData
Current filter logic:
Code Block |
---|
const conditions.code = {ilike: queryString};
if (shouldSearchByName) {
conditions.name = {ilike: queryString};
delete conditions.code;
} |
Updated filter logic:
Code Block |
---|
if hierarchy_id: hierarchyId,
organisation_id: organisationId
};
if (isDataSearchApplied) {
searchQuery = helper.sanitizeStringCode(searchQuery);
let queryString = `${searchQuery}%`;
if (sortResult) {
queryString = `%${queryString}`;
}
conditions.code = {ilike: queryString};
if (shouldSearchByName) {
conditions.name = {ilike: queryString};
delete conditions.code;
}
}
const heirarchyData = await extendedModels.AddressNode.find({
where: conditions,
fields: { code: true, name: true, id: true },
order: sortResult && 'name ASC',
});
|
Updated filter logic:
Expand |
---|
title | Updated filter logic: |
---|
|
Code Block |
---|
const conditions = {
and: [
{hierarchy_id: hierarchyId},
{organisation_id: organisationId},
],
};
if (isDataSearchApplied) {
searchQuery = helper.sanitizeStringCode(searchQuery);
let queryString = `${searchQuery}%`;
if (sortResult) {
queryString = `%${queryString}`;
}
const orConditions = shouldIncludeNameInSearchFilter
? [
{ name: { ilike: queryString } },
{ code: { ilike: queryString } }
]
:
[{ code: { ilike: queryString } }];
conditions.and.push({ or: orConditions });
}
const heirarchyData = await extendedModels.AddressNode.find({
where: conditions,
fields: { code: true, name: true, id: true }
});
|
|
Frontend changes:
At frontend side changes are to be done at 3 places
Add Consignment modal (src/components/pages/details/AddDetails.tsx
)
Counter Booking → Add Address ( src/components/pages/OpsDashboard/Manage/counterBooking/AddAddress2.tsx
)
Customer-Portal → src/components/address/create-address.tsx
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).
Debouncing will be implemented at all places whereever we are fetching list to be populated in the dropdown.
In case allowOverrideHierarchy
is false , we will let user only enter values from dropdown only.
In case allowOverrideHierarchy
is true: user can either select from dropdown or enter other values too.
Only those fields will be disabled for which data is fetched, only when allow override hierarchy is false
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.js
→ getAddressNodeData
(mentioned above in backend changes).
Customer Portal changes
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
Code Block |
---|
// this logic will be used to get whether isAddressHierarchyPresent is true or false
if (Array.isArray(response.data.hierarchy) && response.data.hierarchy.length) {
setIsAddressHierarchyPresent(true);
} |
In case isAddressHierarchyPresent
is false then we will render only Simple Input text field.
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
Expand |
---|
title | saveAddressUsingAddressHierarchy code snippet called on onChange |
---|
|
Code Block | const saveAddressUsingAddressHierarchy = async (nodeValue: string, nodeType: string) => {
if (!isKeyOfNewAddress(nodeType) || !nodeValue) return;
setLoading(true);
const fetchLocality = await fetchAddressUsingAddressHierarchy({ queryString: nodeValue, searchType: nodeType });
if (!fetchLocality.isSuccess) {
setNewAddress({
...newAddress,
city: '',
state: '',
country: '',
});
setDisableLocality(false);
} else {
let nodeFound = false;
const parentLevelNodes = [];
for (let i = 0; i < hierarchyData?.length; i += 1) {
const curNode = hierarchyData[i].location_key.toLowerCase();
if ((nodeFound || curNode === nodeType.toLocaleLowerCase()) && curNode in fetchLocality?.data(shouldSearchByBothNameAndCode) {
condition.and.push({
or: [
{ name: { ilike: queryString } },
{ code: { ilike: queryString } }
]
});
} else {
conditions.and.push({ code: { ilike: queryString } });
} |
Frontend changes:
Changes are to be done at 3 places
Add Consignment modal (src/components/pages/details/AddDetails.tsx
)
Counter Booking → Add Address ( src/components/pages/OpsDashboard/Manage/counterBooking/AddAddress2.tsx
)
Customer-Portal → src/components/address/create-address.tsx
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).
Debouncing will be implemented at all places wherever we are fetching list to be populated in the dropdown. (500ms)
In case allowOverrideHierarchy
is false , we will let user only enter values from dropdown only.
In case allowOverrideHierarchy
is true: user can either select from dropdown or enter other values too.
Only those fields will be disabled for which data is fetched, only when allow override hierarchy is false
Customer Portal changes
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
Code Block |
---|
// this logic will be used to get whether isAddressHierarchyPresent is true or false
if (Array.isArray(response.data.hierarchy) && response.data.hierarchy.length) {
nodeFound = truesetIsAddressHierarchyPresent(true);
parentLevelNodes.push({ key: hierarchyData[i].location_key, value: fetchLocality?.data[curNode] });
}
}
const fetchedAddressFieldsObj = parentLevelNodes?.reduce((acc, curr) => {
} |
In case isAddressHierarchyPresent
is false then we will render only Simple Input text field.
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
Expand |
---|
title | saveAddressUsingAddressHierarchy code snippet called on onChange |
---|
|
Code Block |
---|
const saveAddressUsingAddressHierarchy = async (nodeValue: string, nodeType: string) => {
if (!isKeyOfNewAddress(curr.keynodeType)) {|| !nodeValue) return;
setLoading(true);
acc[curr.key] = curr.value || null; const fetchLocality = await fetchAddressUsingAddressHierarchy({ queryString: nodeValue, searchType: nodeType });
if (curr!fetchLocality.key !== nodeType && (!isAddressMappingAllowed && curr.value)) {isSuccess) {
setNewAddress({
setDisableLocality(true, curr.key);...newAddress,
city: '',
} else { state: '',
setDisableLocality(false, curr.key); country: '',
});
} setDisableLocality(false);
} else {
let nodeFound return= accfalse;
},const {} as {parentLevelNodes = [key: string]:;
string | null }); for (let i = setNewAddress({0; i < hierarchyData?.length; i += 1) {
...newAddress, const curNode = hierarchyData[i].location_key.toLowerCase();
...fetchedAddressFieldsObj, if ((nodeFound || });
}curNode === nodeType.toLocaleLowerCase()) && curNode in fetchLocality?.data) {
setLoading(false); }; |
|
Function to be called on typing text to be searched in address fields( pincode, city, state country)
Expand |
---|
title | Searching function logic called on onSearch |
---|
|
Code Block |
---|
const loadPincodes = async (value: any) nodeFound => {
true;
if (value?.length < 3) return; if (!hierarchyData?.some((node: any) => nodeparentLevelNodes.push({ key: hierarchyData[i].location_key === 'pincode')) return, value: fetchLocality?.data[curNode] });
setLoading(true); }
const response = await fetchPincodeList(value); }
if (response && response.isSuccess && Array.isArray(response?.data)) { const fetchedAddressFieldsObj = parentLevelNodes?.reduce((acc, curr) setLocalityPincodesList(response.data || []);=> {
} if setLoading(false);
(isKeyOfNewAddress(curr.key)) {
}; const loadCitiesList = async (value: any) => { acc[curr.key] if (value?.length < 3) return;= curr.value || null;
if (!hierarchyData?.some((node: any) => node.location_key === 'city')) return; if (curr.key !== nodeType && setLoading(true);
!isAddressMappingAllowed && curr.value)) {
const response = await fetchCitiesList(value); if (response && response.isSuccess && Array.isArray(response?.data)) {setDisableLocality(true, curr.key);
setLocalityCitiesList(response.data || []); } else {
} setLoading(false); }; const loadStatesList = async (value: any) => {setDisableLocality(false, curr.key);
if (value?.length < 3) return; }
if (!hierarchyData?.some((node: any) => node.location_key === 'state')) return; }
setLoading(true); const response = await fetchStatesList(value)return acc;
if (response && response.isSuccess && Array.isArray(response?.data)) {
}, {} as { [key: string]: string | null });
setLocalityStatesList(response.data || []); setNewAddress({
} setLoading(false);...newAddress,
}; const loadCountriesList = async (value: any) => { ...fetchedAddressFieldsObj,
if (value?.length < 3}) return;
if (!hierarchyData?.some((node: any) => node.location_key === 'country')) return;
}
setLoading(false);
setLoading(true);
const response = await fetchCountriesList(value);
}; |
|
Function to be called on typing text to be searched in address fields( pincode, city, state country)
Expand |
---|
title | Searching function logic called on onSearch |
---|
|
Code Block |
---|
const loadPincodes = async (value: any) => {
if (response && response.isSuccess && Array.isArray(response?.data)) {value?.length < 3) return;
if setCountriesList(response.data || []);
}!hierarchyData?.some((node: any) => node.location_key === 'pincode')) return;
setLoading(falsetrue);
}; |
|
Debouncing logic is also applied :
Expand |
---|
|
Code Block |
---|
useEffect(() => { const response = await loadPincodes(newAddress.pincodefetchPincodeList(value);
}, [debouncePincode]); if (response && response.isSuccess && useEffect(() =>Array.isArray(response?.data)) {
loadCitiesList(newAddress.city); },setLocalityPincodesList(response.data || [debounceCity]);
useEffect(() => { }
loadStatesListsetLoading(newAddress.statefalse);
}, [debounceState]);
; |
|
Debouncing logic is also applied :
Expand |
---|
|
Code Block |
---|
useEffect(() => {
loadCountriesListloadPincodes(newAddress.countrypincode);
}, [debounceCountrydebouncePincode]);
//import { useDebounce } from 'hooks/use-debounce'; using already imported (was being used for w3wCode) useDebounce HoC
//SEARCH_TIMEOUT = 500 -> debounce time is used by default
const debouncePincode = useDebounce(newAddress.pincode);
const debounceCity = useDebounce(newAddress.city);
const debounceState = useDebounce(newAddress.state);
const debounceCountry = 500 -> debounce time is used by default
const debouncePincode = useDebounce(newAddress.countrypincode);
|
|
Following rendering logic is used for all nodes (pincode, state, city, country)
...
Expand |
---|
title | Rendering snippet for address fields |
---|
|
Code Block |
---|
const renderPincodeBox = () => {
if (isAddressHierarchyPresent && isAddressMappingAllowed && hierarchyFieldsArr.include('pincode')) {
return (
<AutoComplete
value={newAddress.pincode}
className={classes.pincodeInput}
dropdownClassName={classes.pincodeInputDropdown}
options={
localityPincodesList.map((pincode: any) => {
return {
label: `${pincode.name} (${pincode.code})`,
value: pincode.code,
};
})
}
onSearch={loadPincodes}
onSelect={(e) => saveAddressUsingAddressHierarchy(e, 'pincode')}
onChange={(value) => updateAddressFieldWithValidations('pincode', value)}
style={{ textAlign: 'left' }}
allowClear
>
<Input
className={classes.autoCompleteInputBox}
allowClear
placeholder={t('address_pincode')}
/>
</AutoComplete>
);
}
else if (isAddressHierarchyPresent && !isAddressMappingAllowed && hierarchyFieldsArr.include('pincode')) {
return (
<Form.Item
className={classes.pincodeWithExtra}
name="pincode"
extra={samplePincode ? `Ex. ${samplePincode}` : undefined}
>
<Select
value={newAddress.pincode}
dropdownClassName={classes.pincodeInputDropdown}
placeholder={t('address_pincode')}
onChange={(e) => {
updateAddressFieldWithValidations('pincode', e);
const isValid = isValidPincode(e);
if (isValid !== validPincode) {
setValidPincode(isValid);
}
}}
options={
localityPincodesList.map((pincode: any) => {
return {
label: `${pincode.name} (${pincode.code})`,
value: pincode.code,
};
})
}
onSelect={(e) => saveAddressUsingAddressHierarchy(e, 'pincode')}
defaultActiveFirstOption={false}
filterOption={false}
onSearch={loadPincodes}
showSearch
allowClear
style={validPincode ? {} : { borderColor: 'red' }}
/>
</Form.Item>
);
}Item>
else { );
}
return ( return (
<Input <Input
value={newAddress.pincode}
className={classes.pincodeInput}
placeholder={t('address_citypincode')}
onChange={(e) => updateAddressFieldWithValidations('pincodeInputpincode', e.target.value)}
/>
);
}
};
|
|
Additional Details:
Changes in src/network/pickup.api.ts
...
Expand |
---|
title | changes in src/network/api.constants.ts |
---|
|
Code Block |
---|
export const FETCH_STATES_LIST = '/api/CustomerPortal/addressManagement/getAddressNodeData?type=State';
export const FETCH_COUNTRIES_NODE_DATA = '/api/CustomerPortal/addressManagement/getAddressNodeData?type=Country';
export const FETCH_ADDRESS_FROM_ADDRESS_HIERARCHY = '/api/CustomerPortal/fetchGeneralAddressCode'; |
|
CRMDashboard changes:
General Function to be called whenever any option from rendered dropdown is selected, it make call to api api/CustomerPortal/fetchGeneralAddressCode
...
In case isAddressHierarchyPresent
is false then we will render only Simple Input text field.
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.
...
'/api/CustomerPortal/fetchGeneralAddressCode'; |
|
CRMDashboard changes:
Similar logic as of Customer-Portal is used for rendering like on basis of isAddressHierarchyPresent and allowOverrideHierarchy.
Expand |
---|
title | saveAddressUsingAddressHierarchy code snippet called on onChange |
---|
|
Code Block |
---|
const saveAddressUsingAddressHierarchy = async (nodeValue, nodeType) => {
if (!nodeType) return;
if (!nodeValue) {
handleAddressFields(nodeType, null, false);
return;
}
const fetchLocality = await fetchAddressUsingAddressHierarchy({ queryString: nodeValue, searchType: nodeType });
if (!fetchLocality.isSuccess) {
form.setFieldsValue({
cityName: '',
stateName: '',
countryName: '',
});
setDisableLocality(false, null);
} else {
handleAddressFields(nodeType, fetchLocality, false);
}
}; |
|
...