Offline Report Cards and Custom Dashboards

Avni allows you to create different indicator reports that are available offline to the field users. These reports help field users to derive more insights on the captured data.

Creating an offline report is a two-step process. First, we need to create a report card that holds the actual query function. Second, we group multiple cards into to a dashboard.

Creating a Report Card

Creating a new report card is no different than creating any other Avni entity. Open app designer and go to the report card tab. Click on the new report card and provide the details like name description, etc.

Report cards can be of 2 types:

  1. Standard Report Cards, the different types of which are as follows (Entity specified in brackets indicates the type of entity listed on clicking on the card):

    • Pending approval (Entity Approval Statuses)

    • Approved (Entity Approval Statuses)

    • Rejected (Entity Approval Statuses)

    • Scheduled visits (Individuals)

    • Overdue visits (Individuals)

    • Last 24 hours registrations (Individuals)

    • Last 24 hours enrolments (Individuals)

    • Last 24 hours visits (Individuals)

    • Total (Individuals)

    • Comments (Individuals)

    • Call tasks (Tasks)

    • Open subject tasks (Tasks)

    • Due checklist (Individuals)

  2. Custom Report cards: Report card with configurable Query, which returns a list of Individuals as the response. Length of the list is shown on the card and on clicking the card, the list of Individuals returned is shown. Please note that the query function can return a list of Individuals or an object with these properties, { primaryValue: '20', secondaryValue: '(5%)', lineListFunction }, here lineListFunction should always return the list of subjects.

Creating a Dashboard

After all the cards are done it's time to group them together using the dashboard. Offline Dashboards have the following sub-components:

  • Sections : Visual Partitions used to club together cards of specific grouping type
  • Offline (Custom) Report Cards : Usually Clickableblocks with count information about grouping of Individuals or EntityApprovals of specific type
  • Filters : Configurable filters that get applied to all "Report Cards" count and listing

Users with access to the "App Designer" can Create, Modifiy or Delete Custom Dashboards as seen below.

Steps to configure a Custom Dashboard

  • Click on the dashboard tab on the app designer and click on the new dashboard.
  • This will take you to the new dashboard screen. Provide the name and description of the dashboard.
  • You can create sections on this screen and
  • Select all the cards you need to add to the section in the dashboard.
  • After adding all the cards, you can re-arrange the cards in the order you want them to see in the field app.

Dashboard Filters

You can also create filters for a dashboard on the same screen by clicking on "Add Filter". This shows a popup as in the below screenshot where you can configure your filter and set the filter name, type, widget type and other values based on your filter type.

Once all the changes are done. Save the dashboard.

For the filters to be applied to the cards in the dashboard, the code for the cards will need to handle the filters.

Sample Code for handling filters in report card:

'use strict';
({params, imports}) => {
//console.log('------>',params.ruleInput.filter( type => type.Name === "Gender" ));
//console.log("params:::", JSON.stringify(params.ruleInput));
  let individualList = params.db.objects('Individual').filtered("voided = false and subjectType.name = 'Individual'" )
     .filter( (individual) => individual.getAgeInYears() >= 18 && individual.getAgeInYears() <= 49  &&  individual.getObservationReadableValue('Is sterilisation done') === 'No');
  
  if (params.ruleInput) {

       let genderFilter = params.ruleInput.filter(rule => rule.type === "Gender");
       let genderValue = genderFilter[0].filterValue[0].name;
      
        console.log('genderFilter---------',genderFilter);
        console.log('genderValue---------',genderValue);        
        
      return individualList
     .filter( (individual) => {
     console.log("individual.gender:::", JSON.stringify(individual.gender.name));
     return individual.gender.name === genderValue;
     });
     }
     else return individualList;
};

Assigning custom Dashboards to User Groups

Custom Dashboards created need to be assigned specifically to a User Group, in-order for Users to see it on the Avni-client mobile app. You may do this, by navigating to the "Admin" app -> "User Groups" -> (User_GROUP) -> "Dashboards" tab, and assigning one or more Custom Dashboards to a User-Group.

In addition, You can also mark one of these Custom Dashboards as the Primary (Is Primary: True) dashboard from the "Admin" app -> "User Groups" -> (User_GROUP) -> "Dashboards".

Using the Dashboard in the Field App

After saving the dashboard sync the field app, and from the bottom "More" tab click on the "Dashboards" option. It will take you to the dashboard screen and will show all the cards that are added to the dashboard.

578

Report cards only passing List of subjects.

566

Report cards returning primaryValue and secondaryValue object

Clicking any card will take the user to the subject listing page, which will display all the subject names returned by the card query.

Users can click on any subject and navigate to their dashboard.

Report card query example

As mentioned earlier query can return a list of Individuals or an object with properties, { primaryValue: '20', secondaryValue: '(5%)', lineListFunction }. DB instance is passed using the params and useful libraries like lodash and moment are available in the imports parameter of the function. Below are some examples of writing the lineListFunction.

The below function returns a list of pregnant women having any high-risk conditions.

'use strict';
({params, imports}) => {
    const isHighRiskWomen = (enrolment) => {
        const weight = enrolment.findLatestObservationInEntireEnrolment('Weight');
        const hb = enrolment.findLatestObservationInEntireEnrolment('Hb');
        const numberOfLiveChildren = enrolment.findLatestObservationInEntireEnrolment('Number of live children');
        return (weight && weight.getReadableValue() < 40) || (hb && hb.getReadableValue() < 8) ||
            (numberOfLiveChildren && numberOfLiveChildren.getReadableValue() > 3)
    };
    return {
      lineListFunction: () => params.db.objects('Individual')
        .filtered(`SUBQUERY(enrolments, $enrolment, SUBQUERY($enrolment.encounters, $encounter, $encounter.encounterType.name = 'Monthly monitoring of pregnant woman' and $encounter.voided = false).@count > 0 and $enrolment.voided = false and voided = false).@count > 0`)
        .filter((individual) => individual.voided === false && _.some(individual.enrolments, (enrolment) => enrolment.program.name === 'Pregnant Woman' && isHighRiskWomen(enrolment)))
    }
};

It is important to write optimized query and load very less data in memory. There will be the cases where query can't be written in realm and we need to load the data in memory, but remember more data we load into the memory slower will be the reports. As an example consider below two cases, in the first case we directly query realm to fetch all the individuals enrolled in Child program, but in the second case we first load all individuals into memory and then filter those cases.

'use strict';
({params, imports}) => ({
    lineListFunction: () => params.db.objects('Individual')
        .filtered(`SUBQUERY(enrolments, $enrolment, $enrolment.program.name = 'Child' and voided = false).@count > 0`)
});
'use strict';
({params, imports}) => {
    return params.db.objects('Individual')
        .filter((individual) => individual.voided === false && _.some(individual.enrolments, (enrolment) => enrolment.program.name === 'Child'))
};

For using the filters in the rules also check section on Dashboard Card Rule here - Writing rules

Tips for writing optimized queries:

Avoid using generic functions:

  • To find observation of a concept avoid using the function findLatestObservationInEntireEnrolment unless absolutely necessary since it searches for the observation in all encounters and enrolment observations. Use specific functions.
  • Eg: To find observation in enrolment can use the function enrolment.findObservation or to find observations in specific encounter type can get the encounters using enrolment.lastFulfilledEncounter(...encounterTypeNames) and then find observation. Refer code examples for the below 3 usecases.
  • Find children with birth weight less than 2. Birth weight is captured in enrolment
    
    'use strict';
    ({params, imports}) => {
        const isLowBirthWeight = (enrolment) => {
            const obs = enrolment.findObservation('Birth Weight');
            return obs ? obs.getReadableValue() <= 2 : false;
        };
        return params.db.objects('Individual')
            .filtered(`voided = false and SUBQUERY(enrolments, $enrolment, $enrolment.program.name = 'Child' and $enrolment.programExitDateTime = null and $enrolment.voided = false).@count > 0`)
            .filter((individual) => _.some(individual.enrolments, enrolment => enrolment.program.name === 'Child' && _.isNil(enrolment.programExitDateTime) && !enrolment.voided && isLowBirthWeight(enrolment)))
    };
    
    'use strict';
    ({params, imports}) => {
        const isLowBirthWeight = (enrolment) => {
            const obs = enrolment.findLatestObservationInEntireEnrolment('Birth Weight');
            return obs ? obs.getReadableValue() <= 2 : false;
        };
        return params.db.objects('Individual')
            .filtered(`SUBQUERY(enrolments, $enrolment, $enrolment.program.name = 'Child' and $enrolment.programExitDateTime = null and $enrolment.voided = false and SUBQUERY($enrolment.observations, $observation, $observation.concept.uuid = 'c82cd1c8-d0a9-4237-b791-8d64e52b6c4a').@count > 0 and voided = false).@count > 0`)
            .filter((individual) => individual.voided === false && _.some(individual.enrolments, enrolment => enrolment.program.name === 'Child' && isLowBirthWeight(enrolment)))
    };
    
    do voided check first in realm instead of JS - helps in filtering ahead
    Check for concept where it is used - no need to check in all encounters and enrolment
    
    Find MAM status from value of Nutritional status concept captured in Child Followup encounter
    
    // While this example is illustrating the right JS function to use, but it may be better to filter(ed)
    // encounter schema than to start with Individual
    // i.e. someting like db.objects("ProgramEncounter").filtered("programEnrolment.individual.voided = false AND programEnrolment.voided = false AND ...")
    // then return Individuals using .map(enc => enc.programEnrolment.individual) after filtering all program encounters
    'use strict';
    ({params, imports}) => {
        const isUndernourished = (enrolment) => {
            const encounter = enrolment.lastFulfilledEncounter('Child Followup'); 
            if(_.isNil(encounter)) return false; 
           
           const obs = encounter.findObservation("Nutritional status of child");
           return (!_.isNil(obs) && _.isEqual(obs.getValueWrapper().getValue(), "MAM"));
        };
        
        return params.db.objects('Individual')
            .filtered(`voided = false and SUBQUERY(enrolments, $enrolment,$enrolment.program.name = 'Child' and $enrolment.programExitDateTime = null and $enrolment.voided = false and SUBQUERY($enrolment.encounters, $encounter, $encounter.encounterType.name = 'Child Followup' and $encounter.voided = false).@count > 0).@count > 0`)
            .filter((individual) => individual.getAgeInMonths() > 6 && _.some(individual.enrolments, enrolment => enrolment.program.name === 'Child' && _.isNil(enrolment.programExitDateTime) && !enrolment.voided && isUndernourished(enrolment)))
    };
    
    'use strict';
    ({params, imports}) => {
        const isUndernourished = (enrolment) => {
            const obs = enrolment.findLatestObservationInEntireEnrolment('Nutritional status of child');
            return obs ? _.includes(['MAM'], obs.getReadableValue()) : false;
        };
        return params.db.objects('Individual')
            .filtered(`SUBQUERY(enrolments, $enrolment, $enrolment.program.name = 'Child' and $enrolment.programExitDateTime = null and $enrolment.voided = false and SUBQUERY($enrolment.encounters, $encounter, $encounter.encounterType.name = 'Child Followup' and $encounter.voided = false and SUBQUERY($encounter.observations, $observation, $observation.concept.uuid = '3fb85722-fd53-43db-9e8b-d34767af9f7e').@count > 0).@count > 0 and voided = false).@count > 0`)
            .filter((individual) => individual.voided === false && individual.getAgeInMonths() > 6 && _.some(individual.enrolments, enrolment => enrolment.program.name === 'Child' && isUndernourished(enrolment)))
    };
    
    Check only in specific encounter type
    
    Find sick children using the presence of value for concept 'Refer to the hospital for' which is not a mandatory concept
    
    // also see comments in Recommended way for use case 2
    'use strict';
    ({params, imports}) => {
        const isChildSick = (enrolment) => {
          const encounter = enrolment.lastFulfilledEncounter('Child Followup', 'Child PNC'); 
          if(_.isNil(encounter)) return false; 
           
          const obs = encounter.findObservation('Refer to the hospital for');
          return !_.isNil(obs);
        };
        
        return params.db.objects('Individual')
            .filtered(`voided = false and SUBQUERY(enrolments, $enrolment, $enrolment.program.name = 'Child' and $enrolment.programExitDateTime = null and $enrolment.voided = false).@count > 0`)
            .filter(individual => _.some(individual.enrolments, enrolment => enrolment.program.name === 'Child' && _.isNil(enrolment.programExitDateTime) && !enrolment.voided && isChildSick(enrolment)))
    };
    
    'use strict';
    ({params, imports}) => {
        const isChildSick = (enrolment) => {
             const obs = enrolment.findLatestObservationFromEncounters('Refer to the hospital for');    
             return obs ? obs.getReadableValue() != undefined : false;
        };
        
        return params.db.objects('Individual')
            .filtered(`SUBQUERY(enrolments, $enrolment, $enrolment.program.name = 'Child' and $enrolment.programExitDateTime = null and $enrolment.voided = false).@count > 0`)
            .filter((individual) => individual.voided === false && (_.some(individual.enrolments, enrolment => enrolment.program.name === 'Child' && isChildSick(enrolment))) )
    };
    
    Check only in last encounter, not all encounters since the concept is not a mandatory concept. 
    Using findLatestObservationFromEncounters will check in all encounters and mark child has sick even if the concept value had represented sick in any of the previous encounters, resulting in bug, since the concept is not a mandatory concept.
    

Based on the usecase decide whether to write the logic using realm query or JS.

  • Not always achieving the purpose using realm queries might be efficient/possible.

    • Eg: consider a usecase where a mandatory concept is used in a program encounter. Now to check the latest value of the concept, its sufficient to check the last encounter and need not iterate all encounters. Since realm subquery doesn't support searching only in the last encounter, for such usecases, using realm queries not only becomes slow and also sometimes inappropriate depending on the usecase. So in such cases, using JS code for the logic, is more efficient. (refer the below code example)

      • Find dead children using concept value captured in encounter cancel or program exit form.
        
        'use strict';
        ({params, imports}) => { 
           const moment = imports.moment;
        
           const isChildDead = (enrolment) => {
              const exitObservation = enrolment.findExitObservation('29238876-fbd8-4f39-b749-edb66024e25e');
              if(!_.isNil(exitObservation) && _.isEqual(exitObservation.getValueWrapper().getValue(), "cbb0969c-c7fe-4ce4-b8a2-670c4e3c5039"))
                return true;
              
              const encounters = enrolment.getEncounters(false);
              const sortedEncounters = _.sortBy(encounters, (encounter) => {
              return _.isNil(encounter.cancelDateTime)? moment().diff(encounter.encounterDateTime) : 
              moment().diff(encounter.cancelDateTime)}); 
              const latestEncounter = _.head(sortedEncounters);
              if(_.isNil(latestEncounter)) return false; 
               
              const cancelObservation = latestEncounter.findCancelEncounterObservation('0066a0f7-c087-40f4-ae44-a3e931967767');
              if(_.isNil(cancelObservation)) return false;
              return _.isEqual(cancelObservation.getValueWrapper().getValue(), "cbb0969c-c7fe-4ce4-b8a2-670c4e3c5039")
            };
        
        return params.db.objects('Individual')
                .filtered(`voided = false`)
                .filter(individual => _.some(individual.enrolments, enrolment => enrolment.program.name === 'Child' && isChildDead(enrolment)));
        }
        
        'use strict';
        ({params, imports}) => {
        
        return params.db.objects('Individual')
                .filtered(`subquery(enrolments, $enrolment, $enrolment.program.name == "Child" and subquery(programExitObservations, $exitObservation, $exitObservation.concept.uuid ==  "29238876-fbd8-4f39-b749-edb66024e25e" and ( $exitObservation.valueJSON ==  '{"answer":"cbb0969c-c7fe-4ce4-b8a2-670c4e3c5039"}')  ).@count > 0 ).@count > 0 OR subquery(enrolments.encounters, $encounter, $encounter.voided == false and subquery(cancelObservations, $cancelObservation, $cancelObservation.concept.uuid ==  "0066a0f7-c087-40f4-ae44-a3e931967767" and ( $cancelObservation.valueJSON ==  '{"answer":"cbb0969c-c7fe-4ce4-b8a2-670c4e3c5039"}')  ).@count > 0 ).@count > 0`)
                .filter(ind => ind.voided == false)
        };
        
        Moving to JS since realm query iterates through all encounters which can be avoided in JS
        In this cases since the intention is to find if child is dead, hence it can be assumed to be captured in the last encounter or in program exit form based on the domain knowledge
        
        
    • Consider another usecase, where observations of numeric concepts need to be compared. This is not possible to achieve via realm query since the solution would involve the need for JSON parsing of the stored observation. Hence JS logic is appropriate here. (refer below code example)

      • Find children with birth weight less than 2. Birth weight is captured in enrolment
        
        'use strict';
        ({params, imports}) => {
            const isLowBirthWeight = (enrolment) => {
                const obs = enrolment.findObservation('Birth Weight');
                return obs ? obs.getReadableValue() <= 2 : false;
            };
            return params.db.objects('Individual')
                .filtered(`voided = false and SUBQUERY(enrolments, $enrolment, $enrolment.program.name = 'Child' and $enrolment.programExitDateTime = null and $enrolment.voided = false).@count > 0`)
                .filter((individual) => _.some(individual.enrolments, enrolment => enrolment.program.name === 'Child' && _.isNil(enrolment.programExitDateTime) && !enrolment.voided && isLowBirthWeight(enrolment)))
        };
        
        'use strict';
        ({params, imports}) => {
            const isLowBirthWeight = (enrolment) => {
                const obs = enrolment.findLatestObservationInEntireEnrolment('Birth Weight');
                return obs ? obs.getReadableValue() <= 2 : false;
            };
            return params.db.objects('Individual')
                .filtered(`SUBQUERY(enrolments, $enrolment, $enrolment.program.name = 'Child' and $enrolment.programExitDateTime = null and $enrolment.voided = false and SUBQUERY($enrolment.observations, $observation, $observation.concept.uuid = 'c82cd1c8-d0a9-4237-b791-8d64e52b6c4a').@count > 0 and voided = false).@count > 0`)
                .filter((individual) => individual.voided === false && _.some(individual.enrolments, enrolment => enrolment.program.name === 'Child' && isLowBirthWeight(enrolment)))
        };
        
        Moving to realm query for checking birth weight was not possible. If it were a equals comparison it can be achieved using 'CONTAINS' in realm
        
  • But in cases where time complexity is the same for both cases, writing realm queries would be efficient to achieve the purpose. (refer below code example)

    • Find 13 months children who are completely immunised
      
      'use strict';
      ({params, imports}) => {        
         return params.db.objects('Individual')
              .filtered(`voided = false and SUBQUERY(enrolments, $enrolment, $enrolment.program.name = 'Child' and $enrolment.programExitDateTime = null and $enrolment.voided = false and SUBQUERY(checklists, $checklist, SUBQUERY(items, $item, ($item.detail.concept.name = 'BCG' OR $item.detail.concept.name = 'Polio 0' OR $item.detail.concept.name = 'Polio 1' OR $item.detail.concept.name = 'Polio 2' OR $item.detail.concept.name = 'Polio 3' OR $item.detail.concept.name = 'Pentavalent 1' OR $item.detail.concept.name = 'Pentavalent 2' OR $item.detail.concept.name = 'Pentavalent 3' OR $item.detail.concept.name = 'Measles 1' OR $item.detail.concept.name = 'Measles 2' OR $item.detail.concept.name = 'FIPV 1' OR $item.detail.concept.name = 'FIPV 2' OR $item.detail.concept.name = 'Rota 1' OR $item.detail.concept.name = 'Rota 2') and $item.completionDate <> nil).@count = 14).@count > 0).@count > 0`)
              .filter(individual => individual.getAgeInMonths() >= 13)     
      };
      
      'use strict';
      ({params, imports}) => {
          const isChildGettingImmunised = (enrolment) => {
              if (enrolment.hasChecklist) {
                  const vaccineToCheck = ['BCG', 'Polio 0', 'Polio 1', 'Polio 2', 'Polio 3', 'Pentavalent 1', 'Pentavalent 2', 'Pentavalent 3', 'Measles 1', 'Measles 2', 'FIPV 1', 'FIPV 2', 'Rota 1', 'Rota 2'];
                  const checklist = _.head(enrolment.getChecklists());
                  return _.chain(checklist.items)
                      .filter(({detail}) => _.includes(vaccineToCheck, detail.concept.name))
                      .every(({completionDate}) => !_.isNil(completionDate))
                      .value();
              }
              return false;
          };
      
          return params.db.objects('Individual')
              .filtered(`SUBQUERY(enrolments, $enrolment, $enrolment.program.name = 'Child' and $enrolment.programExitDateTime = null and $enrolment.voided = false).@count > 0`)
              .filter((individual) => individual.voided === false && individual.getAgeInMonths() >= 13 && _.some(individual.enrolments, enrolment => enrolment.program.name === 'Child' && isChildGettingImmunised(enrolment)))
      };
      
      Moving to realm query since no of children with age < 13 months were less
      
  • In most cases, filtering as much as possible using realm queries (for cases like voided checks) and then doing JS filtering on top of it if needed, would be appropriate. (refer the below code example)

    • Find dead children using concept value captured in encounter cancel or program exit form.
      
      'use strict';
      ({params, imports}) => { 
         const moment = imports.moment;
      
         const isChildDead = (enrolment) => {
            const exitObservation = enrolment.findExitObservation('29238876-fbd8-4f39-b749-edb66024e25e');
            if(!_.isNil(exitObservation) && _.isEqual(exitObservation.getValueWrapper().getValue(), "cbb0969c-c7fe-4ce4-b8a2-670c4e3c5039"))
              return true;
            
            const encounters = enrolment.getEncounters(false);
            const sortedEncounters = _.sortBy(encounters, (encounter) => {
            return _.isNil(encounter.cancelDateTime)? moment().diff(encounter.encounterDateTime) : 
            moment().diff(encounter.cancelDateTime)}); 
            const latestEncounter = _.head(sortedEncounters);
            if(_.isNil(latestEncounter)) return false; 
             
            const cancelObservation = latestEncounter.findCancelEncounterObservation('0066a0f7-c087-40f4-ae44-a3e931967767');
            if(_.isNil(cancelObservation)) return false;
            return _.isEqual(cancelObservation.getValueWrapper().getValue(), "cbb0969c-c7fe-4ce4-b8a2-670c4e3c5039")
          };
      
      return params.db.objects('Individual')
              .filtered(`voided = false`)
              .filter(individual => _.some(individual.enrolments, enrolment => enrolment.program.name === 'Child' && isChildDead(enrolment)));
      }
      
      'use strict';
      ({params, imports}) => {
      
      return params.db.objects('Individual')
              .filtered(`subquery(enrolments, $enrolment, $enrolment.program.name == "Child" and subquery(programExitObservations, $exitObservation, $exitObservation.concept.uuid ==  "29238876-fbd8-4f39-b749-edb66024e25e" and ( $exitObservation.valueJSON ==  '{"answer":"cbb0969c-c7fe-4ce4-b8a2-670c4e3c5039"}')  ).@count > 0 ).@count > 0 OR subquery(enrolments.encounters, $encounter, $encounter.voided == false and subquery(cancelObservations, $cancelObservation, $cancelObservation.concept.uuid ==  "0066a0f7-c087-40f4-ae44-a3e931967767" and ( $cancelObservation.valueJSON ==  '{"answer":"cbb0969c-c7fe-4ce4-b8a2-670c4e3c5039"}')  ).@count > 0 ).@count > 0`)
              .filter(ind => ind.voided == false);
      };
      
      Moving to JS since realm query iterates through all encounters which can be avoided in JS
      In this cases since the intention is to find if child is dead it can be assumed to be captured in the last encounter or in program exit form based on the domain knowledge
      
      

Also check - https://avni.readme.io/docs/writing-rules#using-paramsdb-object-when-writing-rules

Use Concept UUIDs instead of their names for comparison

Though not much performance improvement, using concept uuids(for comparing with concept answers), instead of getting its readable values did provide minor improvement(in seconds) when need to iterate through thousands of rows. (refer below code example)

  • Find children with congential abnormality based on values of certain concepts
    
    'use strict';
    ({params, imports}) => {
        const isChildCongenitalAnamoly = (enrolment) => {
           const _ = imports.lodash;
        
           const encounter = enrolment.lastFulfilledEncounter('Child PNC'); 
           if(_.isNil(encounter)) return false; 
           
           const obs1 = encounter.findObservation("Is the infant's mouth cleft pallet seen?");
           const condition2 = obs1 ? obs1.getValueWrapper().getValue() === '3a9fe9a1-a866-47ed-b75c-c0071ea22d97' : false;
             
           const obs2 = encounter.findObservation('Is there visible tumor on back or on head of infant?');
           const condition3 = obs2 ? obs2.getValueWrapper().getValue() === '3a9fe9a1-a866-47ed-b75c-c0071ea22d97' : false;
             
           const obs3 = encounter.findObservation("Is foam coming from infant's mouth continuously?");
           const condition4 = obs3 ? obs3.getValueWrapper().getValue() === '3a9fe9a1-a866-47ed-b75c-c0071ea22d97' : false;
                      
             return condition2 || condition3 || condition4;
        };
        
        const isChildCongenitalAnamolyReg = (individual) => {
             const obs = individual.findObservation('Has any congenital abnormality?');
             return obs ? obs.getValueWrapper().getValue() === '3a9fe9a1-a866-47ed-b75c-c0071ea22d97' : false;
        };
        
        return params.db.objects('Individual')
            .filtered(`voided = false and SUBQUERY(enrolments, $enrolment, $enrolment.program.name = 'Child' and $enrolment.programExitDateTime = null and $enrolment.voided = false).@count > 0`)
            .filter((individual) => (isChildCongenitalAnamolyReg(individual) || 
                _.some(individual.enrolments, enrolment => enrolment.program.name === 'Child' && _.isNil(enrolment.programExitDateTime) && !enrolment.voided && isChildCongenitalAnamoly(enrolment) )) )
    };
    
    'use strict';
    ({params, imports}) => {
        const isChildCongenitalAnamoly = (enrolment) => {
             
             const obs1 = enrolment.findLatestObservationInEntireEnrolment("Is the infant's mouth cleft pallet seen?");
             const condition2 = obs1 ? obs1.getReadableValue() === 'Yes' : false;
             
         const obs2 = enrolment.findLatestObservationInEntireEnrolment('Is there visible tumor on back or on head of infant?');
             const condition3 = obs2 ? obs2.getReadableValue() === 'Yes' : false;
             
             const obs3 = enrolment.findLatestObservationInEntireEnrolment("Is foam coming from infant's mouth continuously?");
             const condition4 = obs3 ? obs3.getReadableValue() === 'Yes' : false;
                      
             return condition2 || condition3 || condition4;
        };
        
        const isChildCongenitalAnamolyReg = (individual) => {
             const obs = individual.getObservationReadableValue('Has any congenital abnormality?');
             return obs ? obs === 'Yes' : false;
        };
        
        return params.db.objects('Individual')
            .filtered(`SUBQUERY(enrolments, $enrolment, $enrolment.program.name = 'Child' and $enrolment.programExitDateTime = null and $enrolment.voided = false).@count > 0`)
            .filter((individual) => individual.voided === false && (isChildCongenitalAnamolyReg(individual) || 
                _.some(individual.enrolments, enrolment => enrolment.program.name === 'Child' && isChildCongenitalAnamoly(enrolment) )) )
    };
    
    Use concept uuid instead of readableValue to compare and check for value only in specific encounter type where the concept was used
    

Nested Report Cards

Frequently there are cases where across report cards very similar logic is used and only a value used for comparison, changes. Eg: in one of our partner organisations, we load 'Total SAM children' and 'Total MAM children'. For rendering each takes around 20-30s. And hence the dashboard nos doesn't load until both the report card results are calculated and it makes the user to wait for a minute. If the logic is combined, we can render the results in 30s since it would need only retrieval from db and iterating once.
The above kind of scenarios also lead to code duplication across report cards and when some requirement changes, then the change needs to be done in both.

In-order to handle such scenarios, we recommend using the Nested Report Card. This is a non-standard report card, which has the ability to show upto a maximum of 9 report cards, based on a single Query's response.

The query can return an object with "reportCards" property, which holds within it an array of objets with properties, { cardName: 'nested-i', cardColor: '#123456', textColor: '#654321', primaryValue: '20', secondaryValue: '(5%)', lineListFunction: () => {/*Do something*/} }. DB instance is passed using the params and useful libraries like lodash and moment are available in the imports parameter of the function.

'use strict';
({params, imports}) => {
    /*
    Business logic
    */
    return {reportCards: \[
        {
            cardName: 'nested-i',
            cardColor: '#123456',
            textColor: '#654321',
            primaryValue: '20',
            secondaryValue: '(5%)',
            lineListFunction: () => {
                /*Do something*/
            }
        },
        {
            cardName: 'nested-i+1',
            cardColor: '#123456',
            textColor: '#654321',
            primaryValue: '20',
            secondaryValue: '(5%)',
            lineListFunction: () => {
                /*Do something*/
            }
        }
    ]
	}
};
- primaryValue
- secondaryValue
- lineListFunction
- cardName
- cardColor
- textColor
// Documentation - https://docs.mongodb.com/realm-legacy/docs/javascript/latest/index.html#queries

'use strict';
({params, imports}) => {
const _ = imports.lodash;
const moment = imports.moment;

const substanceUseDue = (enrolment) => {
    const substanceUseEnc = enrolment.scheduledEncountersOfType('Record Substance use details');
    
    const substanceUse = substanceUseEnc
    .filter((e) => moment().isSameOrAfter(moment(e.earliestVisitDateTime)) && e.cancelDateTime === null && e.encounterDateTime === null );
    
    return substanceUse.length > 0 ? true : false;
    
    };
const indList = params.db.objects('Individual')
        .filtered(`SUBQUERY(enrolments, $enrolment, $enrolment.program.name = 'Substance use' and $enrolment.programExitDateTime = null and $enrolment.voided = false and SUBQUERY($enrolment.encounters, $encounter, $encounter.encounterType.name = 'Record Substance use details' and $encounter.voided = false ).@count > 0 and voided = false).@count > 0`)
        .filter((individual) => _.some(individual.enrolments, enrolment => substanceUseDue(enrolment)
        )); 
        
const includingVoidedLength = indList.length;
const excludingVoidedLength = 6;  
const llf1 = () => { return params.db.objects('Individual')
        .filtered(`SUBQUERY(enrolments, $enrolment, $enrolment.program.name = 'Substance use' and $enrolment.programExitDateTime = null and $enrolment.voided = false and SUBQUERY($enrolment.encounters, $encounter, $encounter.encounterType.name = 'Record Substance use details' and $encounter.voided = false ).@count > 0 and voided = false).@count > 0`)
        .filter((individual) => _.some(individual.enrolments, enrolment => substanceUseDue(enrolment)
        ));    
        };
           

return {reportCards: [{
      cardName: 'nested 1',
      textColor: '#bb34ff',
      primaryValue: includingVoidedLength,   
      secondaryValue: null,
      lineListFunction: llf1
  },
  {
      cardName: 'nested 2',
      cardColor: '#ff34ff',
      primaryValue: excludingVoidedLength,   
      secondaryValue: null,
      lineListFunction: () => {return params.db.objects('Individual')
        .filtered(`SUBQUERY(enrolments, $enrolment, $enrolment.program.name = 'Substance use' and $enrolment.programExitDateTime = null and $enrolment.voided = false and SUBQUERY($enrolment.encounters, $encounter, $encounter.encounterType.name = 'Record Substance use details' and $encounter.voided = false ).@count > 0 and voided = false).@count > 0`)
        .filter((individual) => individual.voided === false  && _.some(individual.enrolments, enrolment => substanceUseDue(enrolment)
        ));}
  }]}
};

Screenshot of Nested Custom Dashboard Report Card Edit screen on Avni Webapp

Screenshot of Nested Report Cards in Custom Dashboard in Avni Client

Note: If there is a mismatch between the count of nested report cards configured and the length of reportCards property returned by the query response, then we show an appropriate error message on all Nested Report Cards corresponding to the Custom Report Card.

Reference screen-shots of Avni-Client Custom Dashboard with Approvals ReportCards and Location filter

Default state of Approvals Report Cards without any filter applied

Default state of Approvals Report Cards without any filter applied


Custom Dashboards filter page

Custom Dashboards filter page


State of Approvals Report Cards after the Location filter was applied

State of Approvals Report Cards after the Location filter was applied