Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
35e80e2
fix(#10240): fix count target layout overlap with goal label
cliftonmcintosh Mar 27, 2026
da8c6a3
feat(#10240): hide count number when goal is exceeded with permission
cliftonmcintosh Mar 27, 2026
13d2919
feat(#10240): test count number visibility for different permission a…
cliftonmcintosh Mar 27, 2026
5b305d3
feat(#10240): assert permission string when hiding count past goal
cliftonmcintosh Mar 27, 2026
95e1d0f
feat(#10240): add can_hide_target_count_past_goal permission to defau…
cliftonmcintosh Mar 27, 2026
e2ef816
feat(#10240): test that count is shown when target has no goal
cliftonmcintosh Mar 28, 2026
8247ce0
feat(#10240): use async/await for permission check in ngOnInit
cliftonmcintosh Mar 30, 2026
123bb09
Revert feature flag additions
cliftonmcintosh Mar 31, 2026
9c6aa21
feat(#10240): cap displayed count at goal when limit_count_to_goal is…
cliftonmcintosh Mar 31, 2026
b0347ca
fix(#10240): align count numbers across targets with and without goals
cliftonmcintosh Mar 31, 2026
fc76773
feat(#10240): pass limit_count_to_goal through rules engine target ag…
cliftonmcintosh Mar 31, 2026
588de79
fix(#10240): fix line length linting error in target-state.js
cliftonmcintosh Apr 1, 2026
a0c6c04
chore(#10240): install cht-conf branch with limit_count_to_goal support
cliftonmcintosh Apr 3, 2026
470c115
feat(#10240): add e2e test for limit_count_to_goal target display
cliftonmcintosh Apr 3, 2026
e7a8831
fix(#10240): move goal text below count number to align display acros…
cliftonmcintosh Apr 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
427 changes: 424 additions & 3 deletions package-lock.json
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changes in this file are because of the temporary change to the cht-conf dependency. They should be reverted when we update that dependency again.

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@
"chai-shallow-deep-equal": "^1.4.6",
"chai-things": "^0.2.0",
"chokidar": "^4.0.3",
"cht-conf": "^6.0.2",
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reminder that this needs to be changed before merging.

"cht-conf": "github:cliftonmcintosh/cht-conf#feat/10240-hide-target-counts-past-goal",
"cht-conf-test-harness": "^5.0.3",
"clean-css-cli": "^5.6.2",
"couchdb-compile": "^1.11.2",
Expand Down
3 changes: 2 additions & 1 deletion shared-libs/rules-engine/src/target-state.js
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,8 @@ module.exports = {
const aggregateTarget = target => {
const aggregated = pick(
target,
['id', 'type', 'goal', 'translation_key', 'name', 'icon', 'subtitle_translation_key', 'visible']
['id', 'type', 'goal', 'translation_key', 'name', 'icon', 'subtitle_translation_key', 'visible',
'limit_count_to_goal']
);
aggregated.value = scoreTarget(target);

Expand Down
14 changes: 14 additions & 0 deletions tests/e2e/default/targets/analytics.wdio-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,20 @@ describe('Targets', () => {
await browser.waitUntil(async () => (await analyticsPage.noAdminTargets().isDisplayed()) === true);
});

it('should display goal as count when limit_count_to_goal is set and count exceeds goal', async () => {
const settings = await compileTargets('targets-limit-count-config.js');
await utils.updateSettings(settings, { ignoreReload: true, sync: true, refresh: true, revert: true });

await analyticsPage.goToTargets();
await commonPage.waitForLoaders();

// The test setup includes person contacts, so the rules engine will calculate a pass count.
// goal=1 is set low enough that it will be exceeded, so limit_count_to_goal should cap the display at 1.
const targets = await analyticsPage.getTargets();
const activePregnancies = targets.find(t => t.title === 'Active pregnancies');
expect(activePregnancies.count).to.equal('1');
});

it('should show error message for bad config', async () => {
const settings = await compileTargets('targets-error-config.js');
await utils.updateSettings(settings, { ignoreReload: true, sync: true, refresh: true, revert: true });
Expand Down
12 changes: 12 additions & 0 deletions tests/e2e/default/targets/config/targets-limit-count-config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module.exports = [
{
id: 'active-pregnancies',
translation_key: 'targets.anc.active_pregnancies.title',
type: 'count',
goal: 1,
limit_count_to_goal: true,
appliesTo: 'contacts',
appliesToType: ['person'],
date: 'now',
},
];
7 changes: 3 additions & 4 deletions webapp/src/css/targets.less
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@
position: relative;
}
.count {
display: flex;
flex-direction: column;
align-items: center;
Comment on lines +72 to +74
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Centering the goal seems cleaner than trying to pad it in an absolute position in the top right corner. See my question about this in the PR description.

text-align: center;
.number {
text-align: center;
Expand All @@ -78,11 +81,7 @@
padding-bottom: 0;
}
.goal {
position: absolute;
right: 0;
padding-right: 20px;
color: @label-color;
top: 15px;
p {
font-size: @font-small;
font-weight: 400;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,10 @@
[aggregate]="false">
</mm-analytics-targets-progress>
<div class="count" *ngIf="target.type !== 'percent'">
<div class="number">{{ getDisplayCount(target) | localizeNumber }}</div>
<div class="goal" *ngIf="target.goal >= 0">
<p>{{ 'analytics.target.monthly_goal' | translate }} {{ target.goal | localizeNumber }}</p>
</div>
<div class="number">{{ target.value?.pass | localizeNumber }}</div>
</div>
</div>
<div class="heading">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,4 +133,11 @@ export class AnalyticsTargetsComponent implements OnInit, OnDestroy {
});
});
}

getDisplayCount(target: any): number {
if (target.limit_count_to_goal && target.goal >= 0 && target.value?.pass >= target.goal) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awww, negative goals are out? is the goal positive check necessary?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah I see, it's in case someone creates a target without a goal and then sets limit_count_to_goal to true?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's in case someone creates a target without a goal and then sets limit_count_to_goal to true

Yes!

return target.goal;
}
return target.value?.pass;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { ContactTypesService } from '@mm-services/contact-types.service';
import { SettingsService } from '@mm-services/settings.service';
import { TelemetryService } from '@mm-services/telemetry.service';
import { Selectors } from '@mm-selectors/index';
import { ResourceIconsService } from '@mm-services/resource-icons.service';

describe('AnalyticsTargetsComponent', () => {
let component: AnalyticsTargetsComponent;
Expand Down Expand Up @@ -102,6 +103,7 @@ describe('AnalyticsTargetsComponent', () => {
{ provide: ContactTypesService, useValue: contactTypesService },
{ provide: SettingsService, useValue: settingsService },
{ provide: TelemetryService, useValue: telemetryService },
{ provide: ResourceIconsService, useValue: { getImg: sinon.stub().returns('') } },
]
})
.compileComponents()
Expand Down Expand Up @@ -323,4 +325,74 @@ describe('AnalyticsTargetsComponent', () => {
expect(globalActions.setTitle.notCalled).to.be.true;
expect(globalActions.setShowContent.calledOnceWithExactly(false)).to.be.true;
}));

it('should display goal as count number when limit_count_to_goal is set and count exceeds goal', fakeAsync(() => {
sinon.reset();
rulesEngineService.isEnabled.resolves(true);
rulesEngineService.fetchTargets.resolves([
{ id: 'target1', type: 'count', goal: 10, limit_count_to_goal: true, value: { pass: 15, total: 15 } },
]);

component.ngOnInit();
tick(50);
fixture.detectChanges();

expect(fixture.nativeElement.querySelector('.count .number').innerText).to.equal('10');
}));

it('should display goal as count number when limit_count_to_goal is set and count equals goal', fakeAsync(() => {
sinon.reset();
rulesEngineService.isEnabled.resolves(true);
rulesEngineService.fetchTargets.resolves([
{ id: 'target1', type: 'count', goal: 10, limit_count_to_goal: true, value: { pass: 10, total: 10 } },
]);

component.ngOnInit();
tick(50);
fixture.detectChanges();

expect(fixture.nativeElement.querySelector('.count .number').innerText).to.equal('10');
}));

it('should display actual count when limit_count_to_goal is set but count is below goal', fakeAsync(() => {
sinon.reset();
rulesEngineService.isEnabled.resolves(true);
rulesEngineService.fetchTargets.resolves([
{ id: 'target1', type: 'count', goal: 10, limit_count_to_goal: true, value: { pass: 5, total: 5 } },
]);

component.ngOnInit();
tick(50);
fixture.detectChanges();

expect(fixture.nativeElement.querySelector('.count .number').innerText).to.equal('5');
}));

it('should display actual count when limit_count_to_goal is not set and count exceeds goal', fakeAsync(() => {
sinon.reset();
rulesEngineService.isEnabled.resolves(true);
rulesEngineService.fetchTargets.resolves([
{ id: 'target1', type: 'count', goal: 10, value: { pass: 15, total: 15 } },
]);

component.ngOnInit();
tick(50);
fixture.detectChanges();

expect(fixture.nativeElement.querySelector('.count .number').innerText).to.equal('15');
}));

it('should display actual count when limit_count_to_goal is set but target has no goal', fakeAsync(() => {
sinon.reset();
rulesEngineService.isEnabled.resolves(true);
rulesEngineService.fetchTargets.resolves([
{ id: 'target1', type: 'count', goal: -1, limit_count_to_goal: true, value: { pass: 15, total: 15 } },
]);

component.ngOnInit();
tick(50);
fixture.detectChanges();

expect(fixture.nativeElement.querySelector('.count .number').innerText).to.equal('15');
}));
});
Loading