diff --git a/api/db/database-builder/factory/prescription/organization-learners/build-organization-learner-feature.js b/api/db/database-builder/factory/prescription/organization-learners/build-organization-learner-feature.js index b01276a070..a232c22697 100644 --- a/api/db/database-builder/factory/prescription/organization-learners/build-organization-learner-feature.js +++ b/api/db/database-builder/factory/prescription/organization-learners/build-organization-learner-feature.js @@ -1,8 +1,8 @@ import _ from 'lodash'; import { databaseBuffer } from '../../../database-buffer.js'; -import { buildOrganizationLearner } from '../../build-organization-learner.js'; import { buildFeature } from '../../build-feature.js'; +import { buildOrganizationLearner } from '../../build-organization-learner.js'; const buildOrganizationLearnerFeature = function ({ id = databaseBuffer.getNextId(), diff --git a/api/db/seeds/data/common/feature-builder.js b/api/db/seeds/data/common/feature-builder.js index a2d556da0e..afbc53c727 100644 --- a/api/db/seeds/data/common/feature-builder.js +++ b/api/db/seeds/data/common/feature-builder.js @@ -6,8 +6,8 @@ import { FEATURE_COMPUTE_ORGANIZATION_LEARNER_CERTIFICABILITY_ID, FEATURE_LEARNER_IMPORT_ID, FEATURE_MISSIONS_MANAGEMENT_ID, - FEATURE_ORALIZATION_ID, FEATURE_MULTIPLE_SENDING_ASSESSMENT_ID, + FEATURE_ORALIZATION_ID, FEATURE_PLACES_MANAGEMENT_ID, } from './constants.js'; diff --git a/api/src/prescription/organization-learner/domain/usecases/find-paginated-filtered-participants.js b/api/src/prescription/organization-learner/domain/usecases/find-paginated-filtered-participants.js index 7efc51bf03..a62178e8bf 100644 --- a/api/src/prescription/organization-learner/domain/usecases/find-paginated-filtered-participants.js +++ b/api/src/prescription/organization-learner/domain/usecases/find-paginated-filtered-participants.js @@ -1,3 +1,5 @@ +import { ORGANIZATION_FEATURE } from '../../../../shared/domain/constants.js'; + const findPaginatedFilteredParticipants = async function ({ organizationId, filters, @@ -7,15 +9,13 @@ const findPaginatedFilteredParticipants = async function ({ organizationParticipantRepository, organizationLearnerImportFormatRepository, organizationFeaturesAPI, + organizationLearnerFeatureRepository, }) { const organizationFeatures = await organizationFeaturesAPI.getAllFeaturesFromOrganization(organizationId); if (organizationFeatures.hasLearnersImportFeature) { const importFormat = await organizationLearnerImportFormatRepository.get(organizationId); - // TODO récupérer le featureId qui nous intéresse, pour l'oralization activé - // TODO Branger le organizationLearnerFeatureRepository pour récupérer les organizationLearners concerné par la feature - const { organizationParticipants, meta } = await organizationParticipantRepository.findPaginatedFilteredImportedParticipants({ organizationId, @@ -26,23 +26,62 @@ const findPaginatedFilteredParticipants = async function ({ page, }); - // TODO Si oralization activée pour l'orga, décorer la liste des organizationPArticipants avec l'info - meta.headingCustomColumns = importFormat.columnsToDisplay; - if (organizationFeatures.hasOralizationFeature) { - meta.headingCustomColumns.push('ORALIZATION'); - } meta.customFilters = importFormat.filtersToDisplay; + if (organizationFeatures.hasOralizationFeature) { + return await _addOralizationInformations({ + organizationId, + meta, + organizationParticipants, + organizationLearnerFeatureRepository, + }); + } return { organizationParticipants, meta }; - } else { - return organizationParticipantRepository.findPaginatedFilteredParticipants({ - organizationId, - filters, - sort, - page, - }); } + + return organizationParticipantRepository.findPaginatedFilteredParticipants({ + organizationId, + filters, + sort, + page, + }); }; +async function _addOralizationInformations({ + organizationId, + meta, + organizationParticipants, + organizationLearnerFeatureRepository, +}) { + meta.headingCustomColumns.push('ORALIZATION'); + return { + meta, + organizationParticipants: await _addOralizationInformationToParticipants({ + organizationId, + organizationParticipants, + organizationLearnerFeatureRepository, + }), + }; +} + +async function _addOralizationInformationToParticipants({ + organizationId, + organizationParticipants, + organizationLearnerFeatureRepository, +}) { + const learnersWithOralizationFeature = await organizationLearnerFeatureRepository.getLearnersByFeature({ + organizationId, + featureKey: ORGANIZATION_FEATURE.ORALIZATION, + }); + return organizationParticipants?.map((participant) => { + const hasOralization = learnersWithOralizationFeature.some( + (learnerWithOralization) => learnerWithOralization.id === participant.id, + ); + participant.extraColumns['ORALIZATION'] = hasOralization; + + return participant; + }); +} + export { findPaginatedFilteredParticipants }; diff --git a/api/src/prescription/organization-learner/domain/usecases/index.js b/api/src/prescription/organization-learner/domain/usecases/index.js index 3d2d17797c..edcaf0f287 100644 --- a/api/src/prescription/organization-learner/domain/usecases/index.js +++ b/api/src/prescription/organization-learner/domain/usecases/index.js @@ -19,6 +19,7 @@ import * as groupRepository from '../../../campaign/infrastructure/repositories/ import * as organizationLearnerImportFormatRepository from '../../../learner-management/infrastructure/repositories/organization-learner-import-format-repository.js'; import { repositories } from '../../infrastructure/repositories/index.js'; import * as organizationLearnerActivityRepository from '../../infrastructure/repositories/organization-learner-activity-repository.js'; +import * as organizationLearnerFeatureRepository from '../../infrastructure/repositories/organization-learner-feature-repository.js'; import * as organizationParticipantRepository from '../../infrastructure/repositories/organization-participant-repository.js'; import * as registrationOrganizationLearnerRepository from '../../infrastructure/repositories/registration-organization-learner-repository.js'; import * as scoOrganizationParticipantRepository from '../../infrastructure/repositories/sco-organization-participant-repository.js'; @@ -34,6 +35,7 @@ const dependencies = { organizationParticipantRepository, organizationLearnerActivityRepository, organizationLearnerRepository: repositories.organizationLearnerRepository, + organizationLearnerFeatureRepository, organizationLearnerImportFormatRepository, organizationFeaturesAPI, campaignRepository, diff --git a/api/src/prescription/organization-learner/infrastructure/repositories/organization-learner-feature-repository.js b/api/src/prescription/organization-learner/infrastructure/repositories/organization-learner-feature-repository.js index dff2ee7666..dd67a78aae 100644 --- a/api/src/prescription/organization-learner/infrastructure/repositories/organization-learner-feature-repository.js +++ b/api/src/prescription/organization-learner/infrastructure/repositories/organization-learner-feature-repository.js @@ -1,13 +1,18 @@ import { DomainTransaction } from '../../../../../lib/infrastructure/DomainTransaction.js'; import { OrganizationLearner } from '../../domain/read-models/OrganizationLearner.js'; -async function getLearnersByFeature({ organizationId, featureId }) { +async function getLearnersByFeature({ organizationId, featureKey }) { const knexConn = DomainTransaction.getConnection(); const rawOrganizationLearnerFeatures = await knexConn - .select('*') - .from('organization-learner-features') - .join('organization-learners', 'organization-learners.id', 'organization-learner-features.organizationLearnerId') - .where({ featureId, organizationId }); + .select('organization-learners.*') + .from('organization-learners') + .join( + 'organization-learner-features', + 'organization-learner-features.organizationLearnerId', + 'organization-learners.id', + ) + .join('features', 'features.id', 'organization-learner-features.featureId') + .where({ key: featureKey, organizationId }); return rawOrganizationLearnerFeatures.map( (rawOrganizationLearnerFeature) => new OrganizationLearner(rawOrganizationLearnerFeature), diff --git a/api/tests/organizational-entities/unit/application/api/OrganizationFeaturesDTO_test.js b/api/tests/organizational-entities/unit/application/api/OrganizationFeaturesDTO_test.js index 2ff7865c0f..755d956a03 100644 --- a/api/tests/organizational-entities/unit/application/api/OrganizationFeaturesDTO_test.js +++ b/api/tests/organizational-entities/unit/application/api/OrganizationFeaturesDTO_test.js @@ -18,6 +18,7 @@ describe('Unit | Organizational Entities | application | API | OrganizationFeatu expect(organizationFeature.hasLearnersImportFeature).to.be.false; }); }); + describe('#hasOralizationFeature', function () { it('should return true', function () { const organizationFeature = new OrganizationFeaturesDTO({ diff --git a/api/tests/prescription/organization-learner/integration/infrastructure/organization-learner-feature-repository_test.js b/api/tests/prescription/organization-learner/integration/infrastructure/organization-learner-feature-repository_test.js index eb44152533..8e074726dc 100644 --- a/api/tests/prescription/organization-learner/integration/infrastructure/organization-learner-feature-repository_test.js +++ b/api/tests/prescription/organization-learner/integration/infrastructure/organization-learner-feature-repository_test.js @@ -1,18 +1,29 @@ -import * as organizationLearnerFeatureRepository from '../../../../../src/prescription/organization-learner/infrastructure/repositories/organization-learner-feature-repository.js'; import { OrganizationLearner } from '../../../../../src/prescription/organization-learner/domain/read-models/OrganizationLearner.js'; - -import { expect, databaseBuilder, domainBuilder } from '../../../../test-helper.js'; +import * as organizationLearnerFeatureRepository from '../../../../../src/prescription/organization-learner/infrastructure/repositories/organization-learner-feature-repository.js'; +import { databaseBuilder, expect } from '../../../../test-helper.js'; describe('Prescription | OrganizationLearner | Integration | Infrastructure | OrganizationLearnerFeatureRepository', function () { describe('#getLearnerByFeature', function () { + it('returns empty array when feature is unknown', async function () { + const organizationId = databaseBuilder.factory.buildOrganization().id; + const featureKey = 'AN_UNKNOWN_FEATURE'; + + await databaseBuilder.commit(); + + const result = await organizationLearnerFeatureRepository.getLearnersByFeature({ organizationId, featureKey }); + + expect(result).to.deep.equal([]); + }); + it('returns empty array when no learner', async function () { const organizationId = databaseBuilder.factory.buildOrganization().id; - const featureId = databaseBuilder.factory.buildFeature({ key: 'A_FEATURE' }).id; + const featureKey = 'A_FEATURE'; + const featureId = databaseBuilder.factory.buildFeature({ key: featureKey }).id; databaseBuilder.factory.buildOrganizationFeature({ organizationId, featureId }); await databaseBuilder.commit(); - const result = await organizationLearnerFeatureRepository.getLearnersByFeature({ organizationId, featureId }); + const result = await organizationLearnerFeatureRepository.getLearnersByFeature({ organizationId, featureKey }); expect(result).to.deep.equal([]); }); @@ -20,8 +31,9 @@ describe('Prescription | OrganizationLearner | Integration | Infrastructure | Or it('returns learner that have linked feature', async function () { const organizationId = databaseBuilder.factory.buildOrganization().id; const otherOrganizationId = databaseBuilder.factory.buildOrganization().id; + const featureKey = 'A_FEATURE'; - const featureId = databaseBuilder.factory.buildFeature({ key: 'A_FEATURE' }).id; + const featureId = databaseBuilder.factory.buildFeature({ key: featureKey }).id; const otherFeatureId = databaseBuilder.factory.buildFeature({ key: 'OTHER_FEATURE' }).id; databaseBuilder.factory.buildOrganizationFeature({ organizationId, featureId }); @@ -51,7 +63,7 @@ describe('Prescription | OrganizationLearner | Integration | Infrastructure | Or const learner = new OrganizationLearner({ ...organizationLearner }); - const result = await organizationLearnerFeatureRepository.getLearnersByFeature({ organizationId, featureId }); + const result = await organizationLearnerFeatureRepository.getLearnersByFeature({ organizationId, featureKey }); expect(result).to.deep.equal([learner]); }); diff --git a/api/tests/prescription/organization-learner/unit/domain/usecases/find-paginated-filtered-participants_test.js b/api/tests/prescription/organization-learner/unit/domain/usecases/find-paginated-filtered-participants_test.js index 78d210fa53..86322f0f7d 100644 --- a/api/tests/prescription/organization-learner/unit/domain/usecases/find-paginated-filtered-participants_test.js +++ b/api/tests/prescription/organization-learner/unit/domain/usecases/find-paginated-filtered-participants_test.js @@ -1,5 +1,6 @@ +import { OrganizationParticipant } from '../../../../../../src/prescription/organization-learner/domain/read-models/OrganizationParticipant.js'; import { findPaginatedFilteredParticipants } from '../../../../../../src/prescription/organization-learner/domain/usecases/find-paginated-filtered-participants.js'; -import { expect, sinon } from '../../../../../test-helper.js'; +import { domainBuilder, expect, sinon } from '../../../../../test-helper.js'; describe('Unit | UseCases | find-paginated-participants', function () { let organizationId, @@ -12,7 +13,8 @@ describe('Unit | UseCases | find-paginated-participants', function () { extraColumns, organizationLearnerImportFormatRepository, organizationParticipantRepository, - organizationFeaturesAPI; + organizationFeaturesAPI, + organizationLearnerFeatureRepository; beforeEach(function () { organizationId = Symbol('organizationId'); @@ -36,6 +38,10 @@ describe('Unit | UseCases | find-paginated-participants', function () { organizationFeaturesAPI = { getAllFeaturesFromOrganization: sinon.stub(), }; + + organizationLearnerFeatureRepository = { + getLearnersByFeature: sinon.stub(), + }; }); it('should call findPaginatedFilteredParticipants when import not enabled', async function () { @@ -122,10 +128,77 @@ describe('Unit | UseCases | find-paginated-participants', function () { sort, organizationParticipantRepository, organizationLearnerImportFormatRepository, + organizationLearnerFeatureRepository, organizationFeaturesAPI, }); expect(result.meta.headingCustomColumns).to.be.deep.equals(['ORALIZATION']); }); + it('should add oralization value to participants', async function () { + // given + organizationFeaturesAPI.getAllFeaturesFromOrganization + .withArgs(organizationId) + .resolves({ hasLearnersImportFeature: true, hasOralizationFeature: true }); + + organizationLearnerImportFormatRepository.get + .withArgs(organizationId) + .resolves({ columnsToDisplay: [], extraColumns, filtersToDisplay }); + const learnerWithOralization = domainBuilder.buildOrganizationLearner({ id: 1 }); + + const participantWithOralization = new OrganizationParticipant({ + id: learnerWithOralization.id, + FAKE_COLUMN: 'fake value', + }); + const participantWithoutOralization = new OrganizationParticipant({ + id: 2, + FAKE_COLUMN: 'fake value', + }); + organizationParticipantRepository.findPaginatedFilteredImportedParticipants.resolves({ + organizationParticipants: [participantWithOralization, participantWithoutOralization], + meta: {}, + }); + organizationLearnerFeatureRepository.getLearnersByFeature.resolves([learnerWithOralization]); + + // when + const result = await findPaginatedFilteredParticipants({ + organizationId, + filters, + extraFilters, + page, + sort, + organizationParticipantRepository, + organizationLearnerImportFormatRepository, + organizationFeaturesAPI, + organizationLearnerFeatureRepository, + }); + + // then + expect( + organizationParticipantRepository.findPaginatedFilteredImportedParticipants, + ).to.have.been.calledWithExactly({ + organizationId, + extraColumns, + page, + sort, + filters, + extraFilters, + }); + + const expectedParticipantWithOralization = new OrganizationParticipant({ + id: participantWithOralization.id, + FAKE_COLUMN: 'fake value', + ORALIZATION: true, + }); + const expectedParticipantWithoutOralization = new OrganizationParticipant({ + id: participantWithoutOralization.id, + FAKE_COLUMN: 'fake value', + ORALIZATION: false, + }); + + expect(result.organizationParticipants).to.have.deep.members([ + expectedParticipantWithOralization, + expectedParticipantWithoutOralization, + ]); + }); }); }); diff --git a/api/tests/tooling/domain-builder/factory/build-organization-participant.js b/api/tests/tooling/domain-builder/factory/build-organization-participant.js new file mode 100644 index 0000000000..7c5c42ccd4 --- /dev/null +++ b/api/tests/tooling/domain-builder/factory/build-organization-participant.js @@ -0,0 +1,35 @@ +import { OrganizationParticipant } from '../../../../src/prescription/organization-learner/domain/read-models/OrganizationParticipant.js'; + +function buildOrganizationParticipant({ + id = 345, + firstName = 'first-name', + lastName = 'last-name', + participationCount = 0, + lastParticipationDate = null, + campaignName = null, + campaignType = null, + participationStatus = null, + isCertifiableFromCampaign = false, + certifiableAtFromCampaign = false, + isCertifiableFromLearner = false, + certifiableAtFromLearner = false, + extraColumns, +} = {}) { + return new OrganizationParticipant({ + id, + firstName, + lastName, + participationCount, + lastParticipationDate, + campaignName, + campaignType, + participationStatus, + isCertifiableFromCampaign, + certifiableAtFromCampaign, + isCertifiableFromLearner, + certifiableAtFromLearner, + ...extraColumns, + }); +} + +export { buildOrganizationParticipant }; diff --git a/orga/app/components/organization-participant/list.gjs b/orga/app/components/organization-participant/list.gjs index cc895f86a8..d933da13d3 100644 --- a/orga/app/components/organization-participant/list.gjs +++ b/orga/app/components/organization-participant/list.gjs @@ -4,6 +4,7 @@ import { guidFor } from '@ember/object/internals'; import { inject as service } from '@ember/service'; import Component from '@glimmer/component'; import { tracked } from '@glimmer/tracking'; +import dayjs from 'dayjs'; import { t } from 'ember-intl'; import InElement from '../in-element'; @@ -19,6 +20,7 @@ export default class List extends Component { @tracked showDeletionModal = false; @service currentUser; + @service intl; get showCheckbox() { return this.currentUser.isAdminInOrganization && !this.currentUser.hasLearnerImportFeature; @@ -44,12 +46,59 @@ export default class List extends Component { return Boolean(this.args.participants.length); } + displayDate(date) { + return dayjs(date).format('DD/MM/YYYY'); + } + get customColumns() { if (!this.currentUser.hasLearnerImportFeature || !this.args.participants.meta) return []; return this.args.participants.meta.headingCustomColumns; } + getExtraColumnRowValue(extraColumnName, participant) { + if (extraColumnName === 'ORALIZATION') { + return this.intl.t( + `pages.organization-participants.table.row-value.oralization.${participant.extraColumns[extraColumnName]}`, + ); + } else { + if (dayjs(participant.extraColumns[extraColumnName]).isValid()) { + return this.displayDate(participant.extraColumns[extraColumnName]); + } + } + return participant.extraColumns[extraColumnName]; + } + + get extraColumnRowInfo() { + if (!this.currentUser.hasLearnerImportFeature || !this.args.participants.meta) return []; + + //TODO Faire la popote pour créer un tableau d'object ou chacun des objet contiennent les trad (déjà translate) / la colonne / + + // this.args.participants.meta.headingCustomColumns => ['COMMON_DIVISION', 'ORALIZATION] + // on a acces aux participants + + // faire un truc du genre dégueu mais un peu plus caché :) + + return this.args.participants.map((participant) => { + const extraInfos = Object.keys(participant.extraColumns).reduce((accumulator, extraColumnName) => { + return [ + ...accumulator, + { hearderName: extraColumnName, value: this.getExtraColumnRowValue(extraColumnName, participant) }, + ]; + }, []); + + return { + participant, + extraInfos, + }; + }); + } + + @action + participantExtraInfoRow(participant) { + return this.extraColumnRowInfo.find((info) => info.participant.id === participant.id); + } + get customFilters() { if (!this.currentUser.hasLearnerImportFeature || !this.args.participants.meta) return []; @@ -177,7 +226,7 @@ export default class List extends Component { @isParticipantSelected={{isParticipantSelected}} @onToggleParticipant={{fn this.addStopPropagationOnFunction toggleParticipant}} @onClickLearner={{fn @onClickLearner participant.id}} - @customRows={{this.customColumns}} + @customRows={{this.participantExtraInfoRow participant}} @hideCertifiableDate={{@hasComputeOrganizationLearnerCertificabilityEnabled}} @hasOrganizationParticipantPage={{@hasOrganizationParticipantPage}} /> diff --git a/orga/app/components/organization-participant/table-row.gjs b/orga/app/components/organization-participant/table-row.gjs index 76cfc02960..39e1a113bd 100644 --- a/orga/app/components/organization-participant/table-row.gjs +++ b/orga/app/components/organization-participant/table-row.gjs @@ -65,8 +65,10 @@ export default class TableRow extends Component { {{/if}} {{@participant.firstName}} - {{#each @customRows as |key|}} - {{this.getCustomRowData @participant.extraColumns key}} + {{#each @customRows.extraInfos as |extraRowInfos|}} + + {{extraRowInfos.value}} + {{/each}} {{#unless this.currentUser.canAccessMissionsPage}} diff --git a/orga/app/controllers/authenticated/missions/mission/activities.js b/orga/app/controllers/authenticated/missions/mission/activities.js index 2ca91f0152..70c8c5463c 100644 --- a/orga/app/controllers/authenticated/missions/mission/activities.js +++ b/orga/app/controllers/authenticated/missions/mission/activities.js @@ -13,9 +13,9 @@ export default class MissionActivitiesController extends Controller { @tracked divisions = []; @tracked statuses = []; @tracked statusOptions = [ - { value: 'completed', label: this.translateStatusOptionKey('completed') }, - { value: 'started', label: this.translateStatusOptionKey('started') }, { value: 'not-started', label: this.translateStatusOptionKey('not-started') }, + { value: 'started', label: this.translateStatusOptionKey('started') }, + { value: 'completed', label: this.translateStatusOptionKey('completed') }, ]; @tracked name = ''; diff --git a/orga/app/controllers/authenticated/missions/mission/results.js b/orga/app/controllers/authenticated/missions/mission/results.js index ffb053c271..d0ab3fbd5d 100644 --- a/orga/app/controllers/authenticated/missions/mission/results.js +++ b/orga/app/controllers/authenticated/missions/mission/results.js @@ -13,11 +13,11 @@ export default class MissionResultsController extends Controller { @tracked divisions = []; @tracked results = []; @tracked resultOptions = [ - { value: 'reached', label: this.translateResultOptionKey('reached') }, + { value: 'no-result', label: this.translateResultOptionKey('no-result') }, { value: 'not-reached', label: this.translateResultOptionKey('not-reached') }, - { value: 'exceeded', label: this.translateResultOptionKey('exceeded') }, { value: 'partially-reached', label: this.translateResultOptionKey('partially-reached') }, - { value: 'no-result', label: this.translateResultOptionKey('no-result') }, + { value: 'reached', label: this.translateResultOptionKey('reached') }, + { value: 'exceeded', label: this.translateResultOptionKey('exceeded') }, ]; @tracked name = ''; diff --git a/orga/translations/en.json b/orga/translations/en.json index cf662b1cbf..6d2f3016e2 100644 --- a/orga/translations/en.json +++ b/orga/translations/en.json @@ -1105,7 +1105,13 @@ }, "description": "Table of participants, sorted by name.", "empty": "No participants", - "row-title": "Participant" + "row-title": "Participant", + "row-value": { + "oralization": { + "false": "Deactivated", + "true": "Activated" + } + } } }, "organization-participants-import": { diff --git a/orga/translations/fr.json b/orga/translations/fr.json index 20f21d3934..be0b1c1edc 100644 --- a/orga/translations/fr.json +++ b/orga/translations/fr.json @@ -1111,7 +1111,13 @@ }, "description": "Tableau des participants trié par nom.", "empty": "Aucun participant", - "row-title": "Participant" + "row-title": "Participant", + "row-value": { + "oralization": { + "false": "Désactivée", + "true": "Activée" + } + } } }, "organization-participants-import": { diff --git a/orga/translations/nl.json b/orga/translations/nl.json index 431d27f029..9683fd951b 100644 --- a/orga/translations/nl.json +++ b/orga/translations/nl.json @@ -1157,6 +1157,12 @@ "label": "Aantal deelnames" } }, + "row-value": { + "oralization": { + "true": "Geactiveerd", + "false": "Gedeactiveerd" + } + }, "description": "Tabel met deelnemers gesorteerd op naam.", "empty": "Geen deelnemers", "row-title": "Deelnemer"