Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEATURE] Afficher le nombre de partages de profils dans les résultats d'un campagne collecte de profils (PIX-15266) #10541

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ class CampaignProfilesCollectionParticipationSummary {
participantExternalId,
sharedAt,
pixScore,
sharedProfileCount,
previousPixScore,
previousSharedAt,
certifiable,
Expand All @@ -17,6 +18,7 @@ class CampaignProfilesCollectionParticipationSummary {
this.participantExternalId = participantExternalId;
this.sharedAt = sharedAt;
this.pixScore = pixScore;
this.sharedProfileCount = sharedProfileCount;
this.previousPixScore = previousPixScore ?? null;
this.previousSharedAt = previousSharedAt ?? null;
this.evolution = this.#computeEvolution(this.pixScore, this.previousPixScore);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,15 @@ async function _getParticipations(qb, campaignId, filters) {
.whereNull('campaign-participations.deletedAt')
.orderBy('sharedAt', 'desc');
})
.with('participationsCount', (qb) => {
qb.select('organizationLearnerId')
.count('organizationLearnerId AS sharedProfileCount')
.from('campaign-participations')
.groupBy('organizationLearnerId')
.where('campaignId', campaignId)
.whereNotNull('campaign-participations.sharedAt')
.whereNull('campaign-participations.deletedAt');
})
.select(
'campaign-participations.id AS campaignParticipationId',
'campaign-participations.userId AS userId',
Expand All @@ -66,6 +75,7 @@ async function _getParticipations(qb, campaignId, filters) {
'campaign-participations.pixScore AS pixScore',
'previousParticipationsInfos.previousPixScore',
'previousParticipationsInfos.previousSharedAt',
'participationsCount.sharedProfileCount',
)
.distinctOn('campaign-participations.organizationLearnerId')
.from('campaign-participations')
Expand All @@ -80,6 +90,11 @@ async function _getParticipations(qb, campaignId, filters) {
'campaign-participations.organizationLearnerId',
).andOn('campaign-participations.id', '!=', 'previousParticipationsInfos.id');
})
.join(
'participationsCount',
'participationsCount.organizationLearnerId',
'campaign-participations.organizationLearnerId',
)
.where('campaign-participations.campaignId', campaignId)
.whereNull('campaign-participations.deletedAt')
.whereNotNull('campaign-participations.sharedAt')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const serialize = function ({ data, pagination }) {
'participantExternalId',
'sharedAt',
'pixScore',
'sharedProfileCount',
'previousPixScore',
'previousSharedAt',
'evolution',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ describe('Integration | Repository | Campaign Profiles Collection Participation
await databaseBuilder.commit();
});

it('should return the certification profile info and pix score', async function () {
it('should return the certification profile info, pix score and count', async function () {
// when
const results =
await campaignProfilesCollectionParticipationSummaryRepository.findPaginatedByCampaignId(campaignId);
Expand All @@ -191,6 +191,7 @@ describe('Integration | Repository | Campaign Profiles Collection Participation
participantExternalId: 'JeBu',
sharedAt,
pixScore: 46,
sharedProfileCount: 1,
certifiable: false,
certifiableCompetencesCount: 1,
}),
Expand Down Expand Up @@ -260,6 +261,151 @@ describe('Integration | Repository | Campaign Profiles Collection Participation
});
});

context('participations count', function () {
beforeEach(async function () {
databaseBuilder.factory.buildCampaignParticipation({
campaignId,
sharedAt: new Date('2020-01-02'),
createdAt: new Date('2020-01-02'),
isImproved: true,
userId: organizationLearner.userId,
organizationLearnerId: organizationLearner.id,
});

await databaseBuilder.commit();
});

describe('when participant has only one shared participation', function () {
it('should count one participation', async function () {
// when
const results =
await campaignProfilesCollectionParticipationSummaryRepository.findPaginatedByCampaignId(campaignId);

// then
expect(results.data[0].sharedProfileCount).to.equal(1);
});
});
describe('when participant has multiple participations', function () {
it('should return the count of shared participations only', async function () {
// given
databaseBuilder.factory.buildCampaignParticipation({
isImproved: true,
sharedAt: new Date('2022-01-02'),
createdAt: new Date('2022-01-02'),
status: SHARED,
campaignId,
userId: organizationLearner.userId,
organizationLearnerId: organizationLearner.id,
});

databaseBuilder.factory.buildCampaignParticipation({
isImproved: false,
sharedAt: null,
createdAt: new Date('2022-01-02'),
status: TO_SHARE,
campaignId,
userId: organizationLearner.userId,
organizationLearnerId: organizationLearner.id,
});

await databaseBuilder.commit();

// when
const results =
await campaignProfilesCollectionParticipationSummaryRepository.findPaginatedByCampaignId(campaignId);

// then
expect(results.data[0].sharedProfileCount).to.equal(2);
});

it('should not count a deleted participation', async function () {
// given deleted participation
databaseBuilder.factory.buildCampaignParticipation({
isImproved: true,
sharedAt: new Date('2022-01-02'),
createdAt: new Date('2022-01-02'),
deletedAt: new Date('2022-01-02'),
status: SHARED,
campaignId,
userId: organizationLearner.userId,
organizationLearnerId: organizationLearner.id,
});

// given not shared participation
databaseBuilder.factory.buildCampaignParticipation({
isImproved: false,
sharedAt: null,
createdAt: new Date('2022-01-02'),
status: TO_SHARE,
campaignId,
userId: organizationLearner.userId,
organizationLearnerId: organizationLearner.id,
});

await databaseBuilder.commit();

// when
const results =
await campaignProfilesCollectionParticipationSummaryRepository.findPaginatedByCampaignId(campaignId);

// then
expect(results.data[0].sharedProfileCount).to.equal(1);
});
});

describe('when there is another participant for same campaign', function () {
it('should only count shared participations for same learner', async function () {
// given second organisation learner and his participation
const secondOrganizationLearner = databaseBuilder.factory.buildOrganizationLearner({ organizationId });
databaseBuilder.factory.buildCampaignParticipation({
isImproved: false,
sharedAt: new Date('2022-01-02'),
createdAt: new Date('2022-01-02'),
status: SHARED,
campaignId,
userId: secondOrganizationLearner.userId,
organizationLearnerId: secondOrganizationLearner.id,
});

await databaseBuilder.commit();

// when
const results =
await campaignProfilesCollectionParticipationSummaryRepository.findPaginatedByCampaignId(campaignId);

// then
expect(results.data[0].sharedProfileCount).to.equal(1);
expect(results.data[1].sharedProfileCount).to.equal(1);
});
});

describe('when participant has participations to different campaigns', function () {
it('should only count shared participations for same campaign', async function () {
// given second campaign and participation
const secondCampaignId = databaseBuilder.factory.buildCampaign({ organizationId }).id;

databaseBuilder.factory.buildCampaignParticipation({
isImproved: false,
sharedAt: new Date('2022-01-02'),
createdAt: new Date('2022-01-02'),
status: SHARED,
campaignId: secondCampaignId,
userId: organizationLearner.userId,
organizationLearnerId: organizationLearner.id,
});

await databaseBuilder.commit();

// when
const results =
await campaignProfilesCollectionParticipationSummaryRepository.findPaginatedByCampaignId(campaignId);

// then
expect(results.data[0].sharedProfileCount).to.equal(1);
});
});
});

context('additionnal informations about previous participation and evolution', function () {
let userId,
organizationId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ describe('Unit | Domain | Read-Models | CampaignResults | CampaignProfilesCollec
participantExternalId: 'Sarah2024',
sharedAt: '2024-10-28',
pixScore: 20,
sharedProfileCount: 2,
previousPixScore: null,
previousSharedAt: null,
certifiable: true,
Expand Down Expand Up @@ -40,6 +41,7 @@ describe('Unit | Domain | Read-Models | CampaignResults | CampaignProfilesCollec
participantExternalId: 'Sarah2024',
certifiable: true,
certifiableCompetencesCount: 9,
sharedProfileCount: 2,
};

describe('when previous participation pixScore and shared date are undefined', function () {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ describe('Unit | Serializer | JSONAPI | campaign-profiles-collection-participati
participantExternalId: 'abo',
sharedAt: new Date(2020, 2, 2),
pixScore: 1024,
sharedProfileCount: 1,
previousPixScore: 512,
previousSharedAt: new Date(2024, 10, 29),
certifiable: true,
Expand All @@ -29,6 +30,7 @@ describe('Unit | Serializer | JSONAPI | campaign-profiles-collection-participati
'participant-external-id': 'abo',
'shared-at': new Date(2020, 2, 2),
'pix-score': 1024,
'shared-profile-count': 1,
'previous-pix-score': 512,
'previous-shared-at': new Date(2024, 10, 29),
evolution: 'increase',
Expand Down
10 changes: 10 additions & 0 deletions orga/app/components/campaign/results/profile-list.gjs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import ParticipationEvolutionIcon from './participation-evolution-icon';
{{/if}}
<col class="hide-on-mobile" />
<col class="hide-on-mobile" />
<col />
</colgroup>
<thead>
<tr>
Expand All @@ -67,6 +68,12 @@ import ParticipationEvolutionIcon from './participation-evolution-icon';
<TableHeader @align="center" class="hide-on-mobile">{{t
"pages.profiles-list.table.column.competences-certifiables"
}}</TableHeader>

{{#if @campaign.multipleSendings}}
<TableHeader @align="center" aria-label={{t "pages.profiles-list.table.column.ariaSharedProfileCount"}}>
{{t "pages.profiles-list.table.column.sharedProfileCount"}}
</TableHeader>
{{/if}}
</tr>
</thead>

Expand Down Expand Up @@ -116,6 +123,9 @@ import ParticipationEvolutionIcon from './participation-evolution-icon';
<td class="table__column--center hide-on-mobile">
{{profile.certifiableCompetencesCount}}
</td>
<td class="table__column--center">
{{profile.sharedProfileCount}}
</td>
</tr>
{{/each}}
</tbody>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export default class CampaignProfilesCollectionParticipationSummary extends Mode
@attr('string') participantExternalId;
@attr('date') sharedAt;
@attr('number') pixScore;
@attr('number') sharedProfileCount;
@attr('nullable-string') evolution;
@attr('boolean') certifiable;
@attr('number') certifiableCompetencesCount;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ module('Integration | Component | Campaign::Results::ProfileList', function (hoo
});
});
module('table headers for multiple sendings campaign', function () {
test('it should display evolution header and tooltip when campaign is multiple sendings', async function (assert) {
test('it should display evolution header and tooltip and shared profile count when campaign is multiple sendings', async function (assert) {
// given
this.campaign = store.createRecord('campaign', {
id: '1',
Expand Down Expand Up @@ -74,9 +74,12 @@ module('Integration | Component | Campaign::Results::ProfileList', function (hoo
name: t('pages.profiles-list.table.column.evolution'),
});
assert.ok(within(evolutionHeader).getByText(t('pages.profiles-list.table.evolution-tooltip.content')));
assert.ok(
screen.getByRole('columnheader', { name: t('pages.profiles-list.table.column.ariaSharedProfileCount') }),
);
});

test('it should not display evolution header if campaign is not multiple sendings', async function (assert) {
test('it should not display evolution header or shared profile count if campaign is not multiple sendings', async function (assert) {
// given
this.campaign = store.createRecord('campaign', {
id: '1',
Expand All @@ -101,6 +104,9 @@ module('Integration | Component | Campaign::Results::ProfileList', function (hoo

// then
assert.notOk(screen.queryByRole('columnheader', { name: t('pages.profiles-list.table.column.evolution') }));
assert.notOk(
screen.queryByRole('columnheader', { name: t('pages.profiles-list.table.column.ariaSharedProfileCount') }),
);
});
});

Expand Down Expand Up @@ -209,6 +215,42 @@ module('Integration | Component | Campaign::Results::ProfileList', function (hoo
assert.ok(screen.getByRole('cell', { name: t('pages.profiles-list.table.evolution.unavailable') }));
});

test('it should display number of profiles shares', async function (assert) {
// given
this.campaign = store.createRecord('campaign', {
id: '1',
name: 'campagne 1',
participationsCount: 1,
multipleSendings: true,
});
this.profiles = [
{
firstName: 'John',
lastName: 'Doe',
participantExternalId: '123',
sharedProfileCount: 3,
evolution: 'decrease',
sharedAt: new Date(2020, 1, 1),
},
];
this.profiles.meta = { rowCount: 1 };

// when
const screen = await render(
hbs`<Campaign::Results::ProfileList
@campaign={{this.campaign}}
@profiles={{this.profiles}}
@onClickParticipant={{this.noop}}
@onFilter={{this.noop}}
@selectedDivisions={{this.divisions}}
@selectedGroups={{this.groups}}
/>`,
);

// then
assert.ok(screen.getByRole('cell', { name: '3' }));
});

test('it should display the profile list with external id', async function (assert) {
// given
this.campaign = store.createRecord('campaign', {
Expand Down
4 changes: 3 additions & 1 deletion orga/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1270,6 +1270,7 @@
"title": "List of submitted profiles",
"caption": "This table contains the list of participants who have shared their profiles. It indicates for each participant their last name, first name, the sent date, the Pix score, the evolution, the certifiability status and the number of certifiable competences.",
"column": {
"ariaSharedProfileCount": "Number of profiles shared",
"certifiable": "Eligible for certification",
"competences-certifiables": "Competences eligible for certification",
"evolution": "Evolution",
Expand All @@ -1282,7 +1283,8 @@
"sending-date": {
"label": "Sent on",
"on-hold": "Pending"
}
},
"sharedProfileCount": "No. profiles shared"
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggestion: remplacer No. par Qty

},
"empty": "No profiles yet",
"evolution": {
Expand Down
Loading