Offline Reports
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 and then we group multiple cards 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 and query for the card. The provided query runs whenever the user opens the dashboard containing this card. 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. 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;
};
Using the Dashboard in the Field App
After saving the dashboard sync the field app, and from the bottom more tab click on the dashboard option. It will take you to the dashboard screen and will show all the cards that are added to the dashboard.

Report cards only passing List of subjects.

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 usingenrolment.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
'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
'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.
- To find observation of a concept avoid using the function
-
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
-
-
-
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
-
Updated 3 months ago