diff --git a/packages/openchs-android/src/action/common/AddressLevelsState.js b/packages/openchs-android/src/action/common/AddressLevelsState.js index 00f74a63e..b6f8d4f6e 100644 --- a/packages/openchs-android/src/action/common/AddressLevelsState.js +++ b/packages/openchs-android/src/action/common/AddressLevelsState.js @@ -4,7 +4,10 @@ class AddressLevelsState { constructor(levels = []) { const unsortedLevels = Object.entries(_.uniqBy(levels, l => l.uuid) .reduce((acc, {locationMappings, uuid, name, level, type, parentUuid, typeUuid, isSelected = false}) => { - acc[level] = _.defaultTo(acc[level], []).concat([{ + const accumulatorKey = level + "->" + type; + // accumulating just by type affects our ability to sort the levels. accumulating just by level affects our ability to group levels of the same type + // hence using a composite key of level + type with a separator + acc[accumulatorKey] = _.defaultTo(acc[accumulatorKey], []).concat([{ uuid, name, level, @@ -16,8 +19,8 @@ class AddressLevelsState { }]); return acc; }, {})); - const levelTypeOrderedUnsortedLevels = _.orderBy(unsortedLevels, ([level, value]) => level, ['desc']); - this.levels = levelTypeOrderedUnsortedLevels.map(([levelNum, levels]) => { + const sortedLevels = _.orderBy(unsortedLevels, ([levelKey, value]) => levelKey, ['desc']); + this.levels = sortedLevels.map(([levelKey, levels]) => { const levelType = levels[0].type; const other = _.find(levels, (level) => _.startsWith(level.name, "Other")); if(!_.isNil(other)) { diff --git a/packages/openchs-android/src/action/mydashboard/FiltersActions.js b/packages/openchs-android/src/action/mydashboard/FiltersActions.js index f0b4e7c5e..83097174d 100644 --- a/packages/openchs-android/src/action/mydashboard/FiltersActions.js +++ b/packages/openchs-android/src/action/mydashboard/FiltersActions.js @@ -4,6 +4,7 @@ import _ from "lodash"; import FormMappingService from "../../service/FormMappingService"; import {ArrayUtil} from "openchs-models"; import AddressLevelState from '../common/AddressLevelsState'; +import General from "../../utility/General"; class FiltersActions { @@ -64,10 +65,8 @@ class FiltersActions { addressLevelState: action.addressLevelState }; const addressLevelService = beans.get(AddressLevelService); - const lowestSelectedAddressLevels = action.addressLevelState.lowestSelectedAddresses; - const lowestAddressLevels = lowestSelectedAddressLevels - .reduce((acc, parent) => acc.concat(addressLevelService.getDescendantsOfNode(parent)), []); - newState.locationSearchCriteria.toggleLowestAddresses(lowestAddressLevels); + const toMatchAddresses = [...action.addressLevelState.selectedAddresses].concat(addressLevelService.getAllDescendants(action.addressLevelState.selectedAddresses)); + newState.locationSearchCriteria.toggleLowestAddresses(toMatchAddresses); return newState; } diff --git a/packages/openchs-android/src/action/mydashboard/MyDashboardActions.js b/packages/openchs-android/src/action/mydashboard/MyDashboardActions.js index a1940ab98..66b7ff826 100644 --- a/packages/openchs-android/src/action/mydashboard/MyDashboardActions.js +++ b/packages/openchs-android/src/action/mydashboard/MyDashboardActions.js @@ -10,7 +10,6 @@ import {firebaseEvents, logEvent} from "../../utility/Analytics"; import RealmQueryService from "../../service/query/RealmQueryService"; import SubjectTypeService from "../../service/SubjectTypeService"; import {DashboardCacheFilter} from "openchs-models"; -import General from "../../utility/General"; function getApplicableEncounterTypes(holder) { return _.isEmpty(holder.selectedGeneralEncounterTypes) ? holder.selectedEncounterTypes : holder.selectedGeneralEncounterTypes; diff --git a/packages/openchs-android/src/service/AddressLevelService.js b/packages/openchs-android/src/service/AddressLevelService.js index a285769b7..d28955109 100644 --- a/packages/openchs-android/src/service/AddressLevelService.js +++ b/packages/openchs-android/src/service/AddressLevelService.js @@ -1,6 +1,7 @@ import Service from '../framework/bean/Service'; -import {AddressLevel} from 'avni-models'; +import {AddressLevel} from 'openchs-models'; import BaseAddressLevelService from "./BaseAddressLevelService"; +import _ from "lodash"; @Service("addressLevelService") class AddressLevelService extends BaseAddressLevelService { @@ -11,6 +12,13 @@ class AddressLevelService extends BaseAddressLevelService { getSchema() { return AddressLevel.schema.name; } + + getAllDescendants(addresses) { + const addressLevelService = this; + return addresses + .filter(location => location.level === _.get(_.minBy(addresses, 'level'), 'level')) + .reduce((acc, parent) => acc.concat(addressLevelService.getDescendantsOfNode(parent)), []); + } } export default AddressLevelService; diff --git a/packages/openchs-android/src/service/RuleEvaluationService.js b/packages/openchs-android/src/service/RuleEvaluationService.js index aa520d997..d4ead055f 100644 --- a/packages/openchs-android/src/service/RuleEvaluationService.js +++ b/packages/openchs-android/src/service/RuleEvaluationService.js @@ -464,21 +464,34 @@ class RuleEvaluationService extends BaseService { params: _.merge({visitSchedule: scheduledVisits, entity, entityContext, services: this.services}, this.getCommonParams()), imports: getImports() }); + this.checkIfScheduledVisitsAreValid(nextVisits); return nextVisits; } catch (e) { - General.logDebug("Rule-Failure", `New enrolment decision failed for form: ${form.uuid}`); + General.logDebug("Rule-Failure", `Visit Schedule failed for form: ${form.uuid}`); this.saveFailedRules(e, form.uuid, this.getIndividualUUID(entity, entityName)); } - } else { + } else if (!_.isEmpty(rulesFromTheBundle)) { const nextVisits = rulesFromTheBundle .reduce((schedule, rule) => { General.logDebug(`RuleEvaluationService`, `Executing Rule: ${rule.name} Class: ${rule.fnName}`); return this.runRuleAndSaveFailure(rule, entityName, entity, schedule, visitScheduleConfig, null, entityContext); }, scheduledVisits); General.logDebug("RuleEvaluationService - Next Visits", nextVisits); - return nextVisits; + try { + this.checkIfScheduledVisitsAreValid(nextVisits); + return nextVisits; + } catch(e) { + General.logDebug("Rule-Failure", `Visit Schedule (old) failed for form: ${form.uuid}`); + this.saveFailedRules(e, form.uuid, this.getIndividualUUID(entity, entityName)); + } + } + return defaultVisitSchedule; + } + + checkIfScheduledVisitsAreValid(nextVisits) { + if (_.some(nextVisits, visit => _.isNil(visit.earliestDate))) { + throw new Error("Visit(s) scheduled without earliestDate"); } - return scheduledVisits; } getChecklists(entity, entityName, defaultChecklists = []) { diff --git a/packages/openchs-android/src/service/query/IndividualSearchCriteria.js b/packages/openchs-android/src/service/query/IndividualSearchCriteria.js index 1139a1693..0c32c5891 100644 --- a/packages/openchs-android/src/service/query/IndividualSearchCriteria.js +++ b/packages/openchs-android/src/service/query/IndividualSearchCriteria.js @@ -98,13 +98,6 @@ class IndividualSearchCriteria { this.allowedSubjectUUIDs = subjectUUIDs; } - toggleLowestAddress(lowestAddress) { - if (BaseEntity.collectionHasEntity(this.lowestAddressLevels, lowestAddress)) - BaseEntity.removeFromCollection(this.lowestAddressLevels, lowestAddress); - else - this.lowestAddressLevels.push(lowestAddress); - } - toggleLowestAddresses(lowestAddresses) { this.lowestAddressLevels = lowestAddresses; } diff --git a/packages/openchs-android/src/service/reports/DashboardFilterService.js b/packages/openchs-android/src/service/reports/DashboardFilterService.js index cf79dc8d5..e6b268bc1 100644 --- a/packages/openchs-android/src/service/reports/DashboardFilterService.js +++ b/packages/openchs-android/src/service/reports/DashboardFilterService.js @@ -80,9 +80,8 @@ class DashboardFilterService extends BaseService { } else { const addressLevelService = this.getService(AddressLevelService); const addressFilterValues = [...filterValue.selectedAddresses]; - const descendants = filterValue.selectedAddresses - .filter(location => location.level === _.get(_.minBy(filterValue.selectedAddresses, 'level'), 'level')) - .reduce((acc, parent) => acc.concat(addressLevelService.getDescendantsOfNode(parent)), []); + + const descendants = addressLevelService.getAllDescendants(filterValue.selectedAddresses); ruleInput.filterValue = addressFilterValues.concat(descendants .map(addressLevel => _.pick(addressLevel, ['uuid', 'name', 'level', 'type', 'parentUuid']))); General.logDebug('DashboardFilterService', `Effective address filters: ${JSON.stringify(_.countBy(ruleInput.filterValue, "type"))}`); diff --git a/packages/openchs-android/src/utility/General.js b/packages/openchs-android/src/utility/General.js index b15dd54f4..3291cd4bf 100644 --- a/packages/openchs-android/src/utility/General.js +++ b/packages/openchs-android/src/utility/General.js @@ -202,7 +202,7 @@ class General { } static toISTDate(x) { - if (x.toString().includes("18:30:00")) + if (x && x.toString().includes("18:30:00")) return moment(x).add(330, "m").toDate(); return x; } diff --git a/packages/openchs-android/src/views/customDashboard/CardListView.js b/packages/openchs-android/src/views/customDashboard/CardListView.js index 544d80ba3..662989e33 100644 --- a/packages/openchs-android/src/views/customDashboard/CardListView.js +++ b/packages/openchs-android/src/views/customDashboard/CardListView.js @@ -3,7 +3,7 @@ import React from 'react'; import Styles from '../primitives/Styles'; import {CountResult} from './CountResult'; import _, {get} from 'lodash'; -import MCIcon from 'react-native-vector-icons/MaterialCommunityIcons'; +import Colors from '../primitives/Colors'; export const CardListView = ({reportCard, I18n, onCardPress, countResult, index, isLastCard}) => { const {name, colour, itemKey} = reportCard; @@ -11,6 +11,7 @@ export const CardListView = ({reportCard, I18n, onCardPress, countResult, index, const textColor = (countResult && countResult.textColor) || Styles.blackColor; const descriptionColor = (countResult && countResult.textColor) || Styles.blackColor; const cardColor = (countResult && countResult.cardColor) || colour || '#999999'; + const chevronColor = Colors.darker(0.1, cardColor); const clickable = get(countResult, 'clickable'); const renderNumber = () => { @@ -22,6 +23,9 @@ export const CardListView = ({reportCard, I18n, onCardPress, countResult, index, secondary={countResult.secondaryValue} primaryStyle={[styles.primaryTextStyle, {color: textColor}, countResult.hasErrorMsg && styles.cardPrimaryTextErrorStyle]} secondaryStyle={[styles.secondaryTextStyle, {color: textColor}, countResult.hasErrorMsg && styles.cardSecondaryTextErrorStyle]} + clickable={clickable} + chevronColor={chevronColor} + colour={textColor} /> ); }; @@ -34,10 +38,6 @@ export const CardListView = ({reportCard, I18n, onCardPress, countResult, index, ]}> {I18n.t(cardName)} - - {clickable && - } - {renderNumber()} @@ -51,7 +51,7 @@ const styles = StyleSheet.create({ rowContainer: { flexDirection: 'row', flexWrap: 'nowrap', - height: 100, + minHeight: 100, borderWidth: StyleSheet.hairlineWidth, borderColor: '#DCDCDC' }, @@ -70,17 +70,16 @@ const styles = StyleSheet.create({ }, nameContainer: { marginLeft: 5, - paddingHorizontal: 3, + paddingHorizontal: 16, flex: 0.7, flexDirection: 'row', - paddingLeft: 16, + alignItems: 'center', borderRightWidth: StyleSheet.hairlineWidth, borderColor: '#DCDCDC' }, nameTextStyle: { - paddingTop: 15, - fontSize: Styles.titleSize, - width: '90%' + paddingVertical: 15, + fontSize: Styles.normalTextSize }, numberContainer: { flex: 0.3, @@ -92,7 +91,8 @@ const styles = StyleSheet.create({ fontStyle: 'normal', }, secondaryTextStyle: { - fontSize: 16, + fontSize: 14, + fontWeight: '300', fontStyle: 'normal', }, cardPrimaryTextErrorStyle: { diff --git a/packages/openchs-android/src/views/customDashboard/CardTileView.js b/packages/openchs-android/src/views/customDashboard/CardTileView.js index b8a32da06..5bc67ef34 100644 --- a/packages/openchs-android/src/views/customDashboard/CardTileView.js +++ b/packages/openchs-android/src/views/customDashboard/CardTileView.js @@ -36,7 +36,7 @@ const renderNumber = function (countResult = {}, textColor) { ); }; -const cardGap = 16; +const cardGap = 14; export const CardTileView = ({index, reportCard, I18n, onCardPress, countResult}) => { const {name, colour, itemKey, iconName} = reportCard; @@ -56,8 +56,10 @@ export const CardTileView = ({index, reportCard, I18n, onCardPress, countResult} marginTop: cardGap, marginLeft: index % 2 !== 0 ? cardGap : 0, width: cardWidth, + minHeight: 100, backgroundColor: cardColor, borderColor: cardBorderColor, + borderWidth: 1, paddingLeft: 16, }]}> @@ -69,14 +71,14 @@ export const CardTileView = ({index, reportCard, I18n, onCardPress, countResult} {iconName && renderIcon(iconName, textColor)} - + {clickable && - } + } @@ -90,11 +92,11 @@ const styles = StyleSheet.create({ borderWidth: StyleSheet.hairlineWidth, }, cardNameTextStyle: { - fontSize: 18, + fontSize: Styles.normalTextSize, fontStyle: 'normal' }, cardNameContainerStyle: { - paddingBottom: 40, + paddingBottom: 20, marginRight: 12 }, cardPrimaryTextStyle: { @@ -103,7 +105,7 @@ const styles = StyleSheet.create({ fontStyle: 'normal', }, cardSecondaryTextStyle: { - fontSize: 16, + fontSize: 14, fontStyle: 'normal', }, iconContainer: { diff --git a/packages/openchs-android/src/views/customDashboard/CountResult.js b/packages/openchs-android/src/views/customDashboard/CountResult.js index 58cf8c3ae..9e6843b2d 100644 --- a/packages/openchs-android/src/views/customDashboard/CountResult.js +++ b/packages/openchs-android/src/views/customDashboard/CountResult.js @@ -1,15 +1,22 @@ import React from 'react'; import {Text, View} from "react-native"; +import MCIcon from 'react-native-vector-icons/MaterialCommunityIcons'; -export const CountResult = ({primary, secondary, primaryStyle, secondaryStyle, direction}) => { +export const CountResult = ({primary, secondary, primaryStyle, secondaryStyle, direction, clickable, chevronColor, colour}) => { return ( {primary} {secondary ? - {secondary} + ({secondary}) : null} + + + {clickable && + } + ) }; diff --git a/packages/openchs-android/test/model/TestAddressLevelFactory.js b/packages/openchs-android/test/model/TestAddressLevelFactory.js index bdd7957f8..2b2228312 100644 --- a/packages/openchs-android/test/model/TestAddressLevelFactory.js +++ b/packages/openchs-android/test/model/TestAddressLevelFactory.js @@ -3,11 +3,11 @@ import General from "../../src/utility/General"; import _ from 'lodash'; class TestAddressLevelFactory { - static createWithDefaults({parent, level, name}) { + static createWithDefaults({parent, level, name, type}) { const addressLevel = new AddressLevel(); addressLevel.uuid = General.randomUUID(); addressLevel.name = _.defaultTo(name, addressLevel.uuid); - addressLevel.type = level+'_level'; + addressLevel.type = type || level+'_level'; addressLevel.level = level; if (!_.isNil(parent)) { const locationMapping = new LocationMapping(); diff --git a/packages/openchs-android/test/state/AddressLevelStateTest.js b/packages/openchs-android/test/state/AddressLevelStateTest.js index 8e769b06b..23b743809 100644 --- a/packages/openchs-android/test/state/AddressLevelStateTest.js +++ b/packages/openchs-android/test/state/AddressLevelStateTest.js @@ -35,4 +35,22 @@ it('should sort addressLevels by levelType and within each levelType by name', f assert.equal(levels[0].locationMappings[0].parent.uuid, parentLevelElement[0].uuid); } }); +}); + +it ('should treat multiple address level types at the same level as separate', function() { + const addrLevel1 = TestAddressLevelFactory.createWithDefaults({level: 1, type: 'a', name: 'location1'}); + const addrLevel2 = TestAddressLevelFactory.createWithDefaults({level: 1, type: 'b', name: 'location2'}); + const addrLevel3 = TestAddressLevelFactory.createWithDefaults({level: 1, type: 'c', name: 'location3'}); + + const allLevels = [ + addrLevel1, addrLevel2, addrLevel3, + ] + // Shuffle the addressLevels + let addressLevelsState = new AddressLevelsState(_.shuffle(allLevels)); + assert.equal(addressLevelsState.levels.length, 3); + addressLevelsState.levels.map(([levelType, levels]) => { + assert.equal(levels.length, 1); // 1 group for each levelType + assert.equal(levels[0].type, levelType); // group consists of same type of addressLevels + }); + }); \ No newline at end of file