From 89338d06dc6a09c7c6c907dd0743053b85e5306f Mon Sep 17 00:00:00 2001 From: Lionel Date: Mon, 16 Sep 2024 17:58:26 +0200 Subject: [PATCH] [FEATURE] affiche les lots de place dans orga (PIX-14009) (#10103) --- ...ild-place-lots.js => build-places-lots.js} | 6 +- .../data/team-prescription/data-builder.js | 4 +- .../prod/create-organization-places-lot.js | 6 +- .../organization-place-controller.js | 2 +- .../domain/read-models/PlacesLot.js | 7 ++ .../create-organization-places-lot.js | 4 +- ...t.js => delete-organization-places-lot.js} | 4 +- .../usecases/get-organization-places-lots.js | 2 +- .../organization-places-lot-repository.js | 8 +- .../organization-places-lots-serializer.js | 2 +- .../organization-repository_test.js | 2 +- .../get-organization-places-lots_test.js | 2 +- ...organization-places-lot-repository_test.js | 6 +- .../DataOrganizationPlacesStatistics_test.js | 1 + .../read-models/PlaceStatistics_test.js | 17 +++- .../unit/domain/read-models/PlacesLot_test.js | 64 +++++++++----- .../create-organization-places-lot_test.js | 8 +- ...=> delete-organization-places-lot_test.js} | 8 +- .../find-organization-places-lot_test.js | 4 +- ...on-places-lot-managment-serializer_test.js | 6 +- ...rganization-places-lots-serializer_test.js | 4 +- orga/app/adapters/organization-places-lot.js | 12 +++ .../components/places/places-lot-table.gjs | 57 +++++++++++++ orga/app/models/organization-places-lot.js | 14 ++++ orga/app/routes/authenticated/places.js | 11 ++- orga/app/styles/components/places/index.scss | 1 + .../styles/components/places/places-lots.scss | 11 +++ orga/app/templates/authenticated/places.hbs | 7 +- .../places/places-lot-table-test.gjs | 83 +++++++++++++++++++ .../adapters/organization-places-lot-test.js | 20 +++++ .../models/organization-places-lot-test.js | 22 +++++ .../unit/routes/authenticated/places-test.js | 21 ++++- orga/translations/en.json | 20 ++++- orga/translations/fr.json | 20 ++++- orga/translations/nl.json | 20 ++++- 35 files changed, 415 insertions(+), 71 deletions(-) rename api/db/seeds/data/team-prescription/{build-place-lots.js => build-places-lots.js} (92%) rename api/src/prescription/organization-place/domain/usecases/{delete-organization-place-lot.js => delete-organization-places-lot.js} (50%) rename api/tests/prescription/organization-place/unit/domain/usecases/{delete-organization-place-lot_test.js => delete-organization-places-lot_test.js} (78%) create mode 100644 orga/app/adapters/organization-places-lot.js create mode 100644 orga/app/components/places/places-lot-table.gjs create mode 100644 orga/app/models/organization-places-lot.js create mode 100644 orga/app/styles/components/places/places-lots.scss create mode 100644 orga/tests/integration/components/places/places-lot-table-test.gjs create mode 100644 orga/tests/unit/adapters/organization-places-lot-test.js create mode 100644 orga/tests/unit/models/organization-places-lot-test.js diff --git a/api/db/seeds/data/team-prescription/build-place-lots.js b/api/db/seeds/data/team-prescription/build-places-lots.js similarity index 92% rename from api/db/seeds/data/team-prescription/build-place-lots.js rename to api/db/seeds/data/team-prescription/build-places-lots.js index b97cec531f3..98cbe0bfb74 100644 --- a/api/db/seeds/data/team-prescription/build-place-lots.js +++ b/api/db/seeds/data/team-prescription/build-places-lots.js @@ -7,7 +7,7 @@ import { import { REAL_PIX_SUPER_ADMIN_ID } from '../common/constants.js'; import { PRO_ORGANIZATION_ID } from '../common/constants.js'; -function _buildPlaceLotsForProOrganization(databaseBuilder) { +function _buildPlacesLotsForProOrganization(databaseBuilder) { databaseBuilder.factory.buildOrganizationPlace({ organizationId: PRO_ORGANIZATION_ID, category: T3, @@ -57,7 +57,7 @@ function _buildPlaceLotsForProOrganization(databaseBuilder) { }); } -export function buildPlaceLots(databaseBuilder) { - _buildPlaceLotsForProOrganization(databaseBuilder); +export function buildPlacesLots(databaseBuilder) { + _buildPlacesLotsForProOrganization(databaseBuilder); return databaseBuilder.commit(); } diff --git a/api/db/seeds/data/team-prescription/data-builder.js b/api/db/seeds/data/team-prescription/data-builder.js index c4091c4b2d4..d614ba76088 100644 --- a/api/db/seeds/data/team-prescription/data-builder.js +++ b/api/db/seeds/data/team-prescription/data-builder.js @@ -1,6 +1,6 @@ import { buildCampaigns } from './build-campaigns.js'; import { buildOrganizationLearners } from './build-learners.js'; -import { buildPlaceLots } from './build-place-lots.js'; +import { buildPlacesLots } from './build-places-lots.js'; import { buildQuests } from './build-quests.js'; import { buildTargetProfiles } from './build-target-profiles.js'; @@ -8,7 +8,7 @@ async function teamPrescriptionDataBuilder({ databaseBuilder }) { await buildTargetProfiles(databaseBuilder); await buildCampaigns(databaseBuilder); await buildOrganizationLearners(databaseBuilder); - await buildPlaceLots(databaseBuilder); + await buildPlacesLots(databaseBuilder); await buildQuests(databaseBuilder); } diff --git a/api/scripts/prod/create-organization-places-lot.js b/api/scripts/prod/create-organization-places-lot.js index cc1578fcc7d..cbead6cbb8b 100644 --- a/api/scripts/prod/create-organization-places-lot.js +++ b/api/scripts/prod/create-organization-places-lot.js @@ -22,7 +22,7 @@ async function prepareOrganizationPlacesLot(organizationPlacesLotData, log = tru const activationDateInCorrectFormat = activationDate?.split('/').reverse().join('-'); const expirationDateInCorrectFormat = expirationDate?.split('/').reverse().join('-'); - const organizationPlaceLot = new OrganizationPlacesLotForManagement({ + const organizationPlacesLot = new OrganizationPlacesLotForManagement({ createdBy, organizationId, count, @@ -33,9 +33,9 @@ async function prepareOrganizationPlacesLot(organizationPlacesLotData, log = tru }); _log( - `Lot de ${organizationPlaceLot.count} places ${organizationPlaceLot.category} pour l'organisation ${organizationPlaceLot.organizationId} ===> ✔\n`, + `Lot de ${organizationPlacesLot.count} places ${organizationPlacesLot.category} pour l'organisation ${organizationPlacesLot.organizationId} ===> ✔\n`, ); - return organizationPlaceLot; + return organizationPlacesLot; }, ); diff --git a/api/src/prescription/organization-place/application/organization-place-controller.js b/api/src/prescription/organization-place/application/organization-place-controller.js index eb596768240..261bb87c35e 100644 --- a/api/src/prescription/organization-place/application/organization-place-controller.js +++ b/api/src/prescription/organization-place/application/organization-place-controller.js @@ -30,7 +30,7 @@ const deleteOrganizationPlacesLot = async function (request, h) { const organizationPlaceId = request.params.placeId; const userId = request.auth.credentials.userId; - await usecases.deleteOrganizationPlaceLot({ organizationPlaceId, userId }); + await usecases.deleteOrganizationPlacesLot({ organizationPlaceId, userId }); return h.response(null).code(204); }; diff --git a/api/src/prescription/organization-place/domain/read-models/PlacesLot.js b/api/src/prescription/organization-place/domain/read-models/PlacesLot.js index 237f531c1ea..a11a674cc72 100644 --- a/api/src/prescription/organization-place/domain/read-models/PlacesLot.js +++ b/api/src/prescription/organization-place/domain/read-models/PlacesLot.js @@ -4,6 +4,7 @@ const Joi = BaseJoi.extend(JoiDate); import { validateEntity } from '../../../../shared/domain/validators/entity-validator.js'; const validationSchema = Joi.object({ + id: Joi.number().required(), count: Joi.number().required().allow(null), organizationId: Joi.number(), activationDate: Joi.date().required(), @@ -18,11 +19,13 @@ const statuses = { }; export class PlacesLot { + #id; #activationDate; #expirationDate; #deletedAt; constructor(params = {}) { validateEntity(validationSchema, params); + this.#id = params.id; this.count = params.count; this.organizationId = params.organizationId; this.#activationDate = params.activationDate; @@ -30,6 +33,10 @@ export class PlacesLot { this.#deletedAt = params.deletedAt; } + get id() { + return this.#id; + } + get isActive() { return this.status === statuses.ACTIVE && !this.#deletedAt; } diff --git a/api/src/prescription/organization-place/domain/usecases/create-organization-places-lot.js b/api/src/prescription/organization-place/domain/usecases/create-organization-places-lot.js index 37cafe2273e..b436f07e7e8 100644 --- a/api/src/prescription/organization-place/domain/usecases/create-organization-places-lot.js +++ b/api/src/prescription/organization-place/domain/usecases/create-organization-places-lot.js @@ -9,13 +9,13 @@ const createOrganizationPlacesLot = async function ({ }) { await organizationRepository.get(organizationId); - const organizationPlaceLot = new OrganizationPlacesLotForManagement({ + const organizationPlacesLot = new OrganizationPlacesLotForManagement({ ...organizationPlacesLotData, organizationId, createdBy, }); - const id = await organizationPlacesLotRepository.create(organizationPlaceLot); + const id = await organizationPlacesLotRepository.create(organizationPlacesLot); return await organizationPlacesLotRepository.get(id); }; diff --git a/api/src/prescription/organization-place/domain/usecases/delete-organization-place-lot.js b/api/src/prescription/organization-place/domain/usecases/delete-organization-places-lot.js similarity index 50% rename from api/src/prescription/organization-place/domain/usecases/delete-organization-place-lot.js rename to api/src/prescription/organization-place/domain/usecases/delete-organization-places-lot.js index 895ea8597e7..184ede09c9a 100644 --- a/api/src/prescription/organization-place/domain/usecases/delete-organization-place-lot.js +++ b/api/src/prescription/organization-place/domain/usecases/delete-organization-places-lot.js @@ -1,6 +1,6 @@ -const deleteOrganizationPlaceLot = async function ({ organizationPlaceId, userId, organizationPlacesLotRepository }) { +const deleteOrganizationPlacesLot = async function ({ organizationPlaceId, userId, organizationPlacesLotRepository }) { await organizationPlacesLotRepository.get(organizationPlaceId); await organizationPlacesLotRepository.remove({ id: organizationPlaceId, deletedBy: userId }); }; -export { deleteOrganizationPlaceLot }; +export { deleteOrganizationPlacesLot }; diff --git a/api/src/prescription/organization-place/domain/usecases/get-organization-places-lots.js b/api/src/prescription/organization-place/domain/usecases/get-organization-places-lots.js index 8d353fc22e8..18fb15eb0e0 100644 --- a/api/src/prescription/organization-place/domain/usecases/get-organization-places-lots.js +++ b/api/src/prescription/organization-place/domain/usecases/get-organization-places-lots.js @@ -4,7 +4,7 @@ * @typedef {object} payload * @property {number} organizationId * @property {organizationPlacesLotRepository} organizationPlacesLotRepository - * @returns {Promise>} + * @returns {Promise>} */ /** diff --git a/api/src/prescription/organization-place/infrastructure/repositories/organization-places-lot-repository.js b/api/src/prescription/organization-place/infrastructure/repositories/organization-places-lot-repository.js index 3f48417693e..23ef648e71f 100644 --- a/api/src/prescription/organization-place/infrastructure/repositories/organization-places-lot-repository.js +++ b/api/src/prescription/organization-place/infrastructure/repositories/organization-places-lot-repository.js @@ -32,7 +32,7 @@ const findByOrganizationId = async function (organizationId) { const findAllByOrganizationId = async function (organizationId) { const knexConn = DomainTransaction.getConnection(); const placesLots = await knexConn('organization-places') - .select('count', 'activationDate', 'expirationDate', 'deletedAt') + .select('id', 'count', 'activationDate', 'expirationDate', 'deletedAt') .where({ organizationId }); return placesLots.map((e) => new PlacesLot(e)); }; @@ -40,7 +40,7 @@ const findAllByOrganizationId = async function (organizationId) { const findAllByOrganizationIds = async function (organizationIds) { const knexConn = DomainTransaction.getConnection(); const placesLots = await knexConn('organization-places') - .select('count', 'organizationId', 'activationDate', 'expirationDate', 'deletedAt') + .select('id', 'count', 'organizationId', 'activationDate', 'expirationDate', 'deletedAt') .whereIn('organizationId', organizationIds); return placesLots.map((e) => new PlacesLot(e)); @@ -51,7 +51,7 @@ const findAllByOrganizationIds = async function (organizationIds) { const findAllNotDeletedByOrganizationId = async function (organizationId) { const knexConn = DomainTransaction.getConnection(); const placesLots = await knexConn('organization-places') - .select('count', 'activationDate', 'expirationDate', 'deletedAt') + .select('id', 'count', 'activationDate', 'expirationDate', 'deletedAt') .where({ organizationId }) .whereNull('deletedAt') .orderBy( @@ -64,7 +64,7 @@ const findAllNotDeletedByOrganizationId = async function (organizationId) { .orderBy('activationDate', 'desc') .orderBy('createdAt', 'desc'); - return placesLots.map((placeLot) => new PlacesLot(placeLot)); + return placesLots.map((placesLot) => new PlacesLot(placesLot)); }; const get = async function (id) { diff --git a/api/src/prescription/organization-place/infrastructure/serializers/jsonapi/organization-places-lots-serializer.js b/api/src/prescription/organization-place/infrastructure/serializers/jsonapi/organization-places-lots-serializer.js index 8eb8d4df04f..ab8c6fe1f3b 100644 --- a/api/src/prescription/organization-place/infrastructure/serializers/jsonapi/organization-places-lots-serializer.js +++ b/api/src/prescription/organization-place/infrastructure/serializers/jsonapi/organization-places-lots-serializer.js @@ -3,7 +3,7 @@ import jsonapiSerializer from 'jsonapi-serializer'; const { Serializer } = jsonapiSerializer; const serialize = function (places) { - return new Serializer('organization-place', { + return new Serializer('organization-places-lot', { attributes: ['organizationId', 'count', 'activationDate', 'expirationDate', 'status'], }).serialize(places); }; diff --git a/api/tests/integration/infrastructure/repositories/organization-repository_test.js b/api/tests/integration/infrastructure/repositories/organization-repository_test.js index 89a1b9b66a3..6c1d8ff149f 100644 --- a/api/tests/integration/infrastructure/repositories/organization-repository_test.js +++ b/api/tests/integration/infrastructure/repositories/organization-repository_test.js @@ -1096,7 +1096,7 @@ describe('Integration | Repository | Organization', function () { expect(organizationsWithPlaces[0].type).to.equal(firstOrganization.type); }); - it('should return only once an organization with many placeLots', async function () { + it('should return only once an organization with many placesLots', async function () { // given const superAdminUserId = databaseBuilder.factory.buildUser().id; diff --git a/api/tests/prescription/organization-place/acceptance/application/get-organization-places-lots_test.js b/api/tests/prescription/organization-place/acceptance/application/get-organization-places-lots_test.js index 1408021c40d..27b36ff234f 100644 --- a/api/tests/prescription/organization-place/acceptance/application/get-organization-places-lots_test.js +++ b/api/tests/prescription/organization-place/acceptance/application/get-organization-places-lots_test.js @@ -44,7 +44,7 @@ describe('Acceptance | Route | Get Organizations Places Lots', function () { const response = await server.inject(options); // then - expect(response.result.data[0].type).to.equal('organization-places'); + expect(response.result.data[0].type).to.equal('organization-places-lots'); expect(response.statusCode).to.equal(200); }); }); diff --git a/api/tests/prescription/organization-place/integration/infrastructure/repositories/organization-places-lot-repository_test.js b/api/tests/prescription/organization-place/integration/infrastructure/repositories/organization-places-lot-repository_test.js index 339e05e617c..ca1d68b0b7b 100644 --- a/api/tests/prescription/organization-place/integration/infrastructure/repositories/organization-places-lot-repository_test.js +++ b/api/tests/prescription/organization-place/integration/infrastructure/repositories/organization-places-lot-repository_test.js @@ -218,7 +218,7 @@ describe('Integration | Repository | Organization Places Lot', function () { await databaseBuilder.commit(); }); - it('should return array of PlaceLot model', async function () { + it('should return array of PlacesLot model', async function () { databaseBuilder.factory.buildOrganizationPlace({ organizationId }); await databaseBuilder.commit(); @@ -227,7 +227,7 @@ describe('Integration | Repository | Organization Places Lot', function () { expect(places[0]).to.be.instanceOf(PlacesLot); }); - it('should return empty array if there is no placelots', async function () { + it('should return empty array if there is no placeslots', async function () { await databaseBuilder.commit(); const places = await organizationPlacesLotRepository.findAllByOrganizationId(organizationId); @@ -235,7 +235,7 @@ describe('Integration | Repository | Organization Places Lot', function () { expect(places).to.be.empty; }); - it('should return placelots if there are places for given organizationId', async function () { + it('should return placeslots if there are places for given organizationId', async function () { databaseBuilder.factory.buildOrganizationPlace({ organizationId, count: 7, diff --git a/api/tests/prescription/organization-place/unit/domain/read-models/DataOrganizationPlacesStatistics_test.js b/api/tests/prescription/organization-place/unit/domain/read-models/DataOrganizationPlacesStatistics_test.js index 16aa9d24411..a9d9e325c95 100644 --- a/api/tests/prescription/organization-place/unit/domain/read-models/DataOrganizationPlacesStatistics_test.js +++ b/api/tests/prescription/organization-place/unit/domain/read-models/DataOrganizationPlacesStatistics_test.js @@ -25,6 +25,7 @@ describe('Unit | Domain | ReadModels | DataOrganizationPlacesStatistics', functi const placeStatistics = new PlaceStatistics({ placesLots: [ new PlacesLot({ + id: 1, count: 10, expirationDate: new Date('2022-05-02'), activationDate: new Date('2019-04-01'), diff --git a/api/tests/prescription/organization-place/unit/domain/read-models/PlaceStatistics_test.js b/api/tests/prescription/organization-place/unit/domain/read-models/PlaceStatistics_test.js index 2f89714b309..aafb3d946d9 100644 --- a/api/tests/prescription/organization-place/unit/domain/read-models/PlaceStatistics_test.js +++ b/api/tests/prescription/organization-place/unit/domain/read-models/PlaceStatistics_test.js @@ -52,6 +52,7 @@ describe('Unit | Domain | ReadModels | PlaceStatistics', function () { const statistics = new PlaceStatistics({ placesLots: [ new PlacesLot({ + id: 1, count: 1, expirationDate: new Date('2021-05-02'), activationDate: new Date('2021-04-01'), @@ -68,12 +69,14 @@ describe('Unit | Domain | ReadModels | PlaceStatistics', function () { const statistics = new PlaceStatistics({ placesLots: [ new PlacesLot({ + id: 1, count: 1, expirationDate: new Date('2021-05-02'), activationDate: new Date('2021-04-01'), deletedAt: null, }), new PlacesLot({ + id: 2, count: 1, expirationDate: new Date('2021-05-02'), activationDate: new Date('2021-04-01'), @@ -118,6 +121,7 @@ describe('Unit | Domain | ReadModels | PlaceStatistics', function () { const statistics = new PlaceStatistics({ placesLots: [ new PlacesLot({ + id: 1, count: 1, expirationDate: new Date('2021-05-02'), activationDate: new Date('2021-04-01'), @@ -134,6 +138,7 @@ describe('Unit | Domain | ReadModels | PlaceStatistics', function () { const statistics = new PlaceStatistics({ placesLots: [ new PlacesLot({ + id: 1, count: 2, expirationDate: new Date('2021-05-02'), activationDate: new Date('2021-04-01'), @@ -149,7 +154,13 @@ describe('Unit | Domain | ReadModels | PlaceStatistics', function () { it('should return 0 when there are more participant than total places', function () { const statistics = new PlaceStatistics({ placesLots: [ - { count: 2, expirationDate: new Date('2021-05-02'), activationDate: new Date('2021-04-01'), deletedAt: null }, + { + id: 1, + count: 2, + expirationDate: new Date('2021-05-02'), + activationDate: new Date('2021-04-01'), + deletedAt: null, + }, ], placeRepartition: { totalUnRegisteredParticipant: 0, totalRegisteredParticipant: 3 }, }); @@ -169,6 +180,7 @@ describe('Unit | Domain | ReadModels | PlaceStatistics', function () { const statistics = new PlaceStatistics({ placesLots: [ new PlacesLot({ + id: 1, count: 3, expirationDate: new Date('2021-05-02'), activationDate: new Date('2021-04-01'), @@ -184,18 +196,21 @@ describe('Unit | Domain | ReadModels | PlaceStatistics', function () { const statistics = new PlaceStatistics({ placesLots: [ new PlacesLot({ + id: 1, count: 1, expirationDate: new Date('2021-05-02'), activationDate: new Date('2021-04-01'), deletedAt: null, }), new PlacesLot({ + id: 2, count: 10, expirationDate: new Date('2021-05-02'), activationDate: new Date('2021-04-01'), deletedAt: null, }), new PlacesLot({ + id: 3, count: 10, expirationDate: new Date('2020-05-02'), activationDate: new Date('2019-04-01'), diff --git a/api/tests/prescription/organization-place/unit/domain/read-models/PlacesLot_test.js b/api/tests/prescription/organization-place/unit/domain/read-models/PlacesLot_test.js index 9ea79c41f77..fb522867cd0 100644 --- a/api/tests/prescription/organization-place/unit/domain/read-models/PlacesLot_test.js +++ b/api/tests/prescription/organization-place/unit/domain/read-models/PlacesLot_test.js @@ -15,121 +15,145 @@ describe('Unit | Domain | ReadModels | PlacesLot', function () { describe('#count', function () { it('should return count', function () { - const placeLot = new PlacesLot({ + const placesLot = new PlacesLot({ + id: 1, count: 10, activationDate: new Date(), expirationDate: new Date(), deletedAt: null, }); - expect(placeLot.count).to.equal(10); + expect(placesLot.count).to.equal(10); }); }); describe('#isActive', function () { it('should return true when place lots is active', function () { - const placeLots = new PlacesLot({ + const placesLot = new PlacesLot({ + id: 1, count: 1, activationDate: new Date('2021-04-01'), expirationDate: new Date('2021-12-31'), deletedAt: null, }); - expect(placeLots.isActive).to.be.true; + expect(placesLot.isActive).to.be.true; }); it('should return false when place lots is expired', function () { - const placeLots = new PlacesLot({ + const placesLot = new PlacesLot({ + id: 1, count: 1, activationDate: new Date('2021-04-20'), expirationDate: new Date('2021-04-30'), deletedAt: null, }); - expect(placeLots.isActive).to.be.false; + expect(placesLot.isActive).to.be.false; }); it('should return false when place lots is not active yet', function () { - const placeLots = new PlacesLot({ + const placesLot = new PlacesLot({ + id: 1, count: 1, expirationDate: new Date('2021-07-07'), activationDate: new Date('2021-06-06'), deletedAt: null, }); - expect(placeLots.isActive).to.be.false; + expect(placesLot.isActive).to.be.false; }); it('should return 0 when there are only deleted place lots', function () { - const placeLots = new PlacesLot({ + const placesLot = new PlacesLot({ + id: 1, count: 1, expirationDate: new Date('2021-07-07'), activationDate: new Date('2021-06-06'), deletedAt: new Date('2021-05-05'), }); - expect(placeLots.isActive).to.be.false; + expect(placesLot.isActive).to.be.false; + }); + }); + + describe('#id', function () { + it('should return id', function () { + const placesLot = new PlacesLot({ + id: 1, + count: 1, + activationDate: new Date('2021-04-01'), + expirationDate: new Date('2021-12-31'), + deletedAt: null, + }); + + expect(placesLot.expirationDate).to.deep.equal(new Date('2021-12-31')); }); }); describe('#activationDate', function () { it('should return activation date', function () { - const placeLots = new PlacesLot({ + const placesLot = new PlacesLot({ + id: 1, count: 1, activationDate: new Date('2021-04-01'), expirationDate: new Date('2021-12-31'), deletedAt: null, }); - expect(placeLots.activationDate).to.deep.equal(new Date('2021-04-01')); + expect(placesLot.activationDate).to.deep.equal(new Date('2021-04-01')); }); }); describe('#expirationDate', function () { it('should return activation date', function () { - const placeLots = new PlacesLot({ + const placesLot = new PlacesLot({ + id: 1, count: 1, activationDate: new Date('2021-04-01'), expirationDate: new Date('2021-12-31'), deletedAt: null, }); - expect(placeLots.expirationDate).to.deep.equal(new Date('2021-12-31')); + expect(placesLot.expirationDate).to.deep.equal(new Date('2021-12-31')); }); }); describe('#status', function () { it('should return Active status', function () { - const placeLots = new PlacesLot({ + const placesLot = new PlacesLot({ + id: 1, count: 1, activationDate: new Date('2021-04-01'), expirationDate: new Date('2021-12-31'), deletedAt: null, }); - expect(placeLots.status).to.equal('ACTIVE'); + expect(placesLot.status).to.equal('ACTIVE'); }); it('should return Expired status', function () { - const placeLots = new PlacesLot({ + const placesLot = new PlacesLot({ + id: 1, count: 1, activationDate: new Date('2021-04-01'), expirationDate: new Date('2021-04-30'), deletedAt: null, }); - expect(placeLots.status).to.equal('EXPIRED'); + expect(placesLot.status).to.equal('EXPIRED'); }); it('should return Pending status', function () { - const placeLots = new PlacesLot({ + const placesLot = new PlacesLot({ + id: 1, count: 1, activationDate: new Date('2021-07-01'), expirationDate: new Date('2021-09-30'), deletedAt: null, }); - expect(placeLots.status).to.equal('PENDING'); + expect(placesLot.status).to.equal('PENDING'); }); }); }); diff --git a/api/tests/prescription/organization-place/unit/domain/usecases/create-organization-places-lot_test.js b/api/tests/prescription/organization-place/unit/domain/usecases/create-organization-places-lot_test.js index 24b807ca26e..ed3808c378a 100644 --- a/api/tests/prescription/organization-place/unit/domain/usecases/create-organization-places-lot_test.js +++ b/api/tests/prescription/organization-place/unit/domain/usecases/create-organization-places-lot_test.js @@ -32,7 +32,7 @@ describe('Unit | UseCase | create-organization-places-lot', function () { reference: 'ABC123', }; - const organizationPlaceLotId = 12; + const organizationPlacesLotId = 12; const expectedOrganizationPlacesLotData = new OrganizationPlacesLotForManagement({ ...organizationPlacesLotData, @@ -44,10 +44,12 @@ describe('Unit | UseCase | create-organization-places-lot', function () { expectedOrganizationPlacesLotData, ); - organizationPlacesLotRepository.create.withArgs(expectedOrganizationPlacesLotData).resolves(organizationPlaceLotId); + organizationPlacesLotRepository.create + .withArgs(expectedOrganizationPlacesLotData) + .resolves(organizationPlacesLotId); organizationRepository.get.withArgs(organizationId).resolves(organization); organizationPlacesLotRepository.get - .withArgs(organizationPlaceLotId) + .withArgs(organizationPlacesLotId) .resolves(expectedOrganizatonPlacesLotManagement); //when diff --git a/api/tests/prescription/organization-place/unit/domain/usecases/delete-organization-place-lot_test.js b/api/tests/prescription/organization-place/unit/domain/usecases/delete-organization-places-lot_test.js similarity index 78% rename from api/tests/prescription/organization-place/unit/domain/usecases/delete-organization-place-lot_test.js rename to api/tests/prescription/organization-place/unit/domain/usecases/delete-organization-places-lot_test.js index 49f5f85de54..37958e2fd30 100644 --- a/api/tests/prescription/organization-place/unit/domain/usecases/delete-organization-place-lot_test.js +++ b/api/tests/prescription/organization-place/unit/domain/usecases/delete-organization-places-lot_test.js @@ -1,8 +1,8 @@ -import { deleteOrganizationPlaceLot } from '../../../../../../src/prescription/organization-place/domain/usecases/delete-organization-place-lot.js'; +import { deleteOrganizationPlacesLot } from '../../../../../../src/prescription/organization-place/domain/usecases/delete-organization-places-lot.js'; import { NotFoundError } from '../../../../../../src/shared/domain/errors.js'; import { catchErr, expect, sinon } from '../../../../../test-helper.js'; -describe('Unit | UseCase | delete-organization-place-lot', function () { +describe('Unit | UseCase | delete-organization-places-lot', function () { it('should delete the organization place lot', async function () { // given const organizationPlacesLotRepository = { @@ -11,7 +11,7 @@ describe('Unit | UseCase | delete-organization-place-lot', function () { }; // when - await deleteOrganizationPlaceLot({ + await deleteOrganizationPlacesLot({ organizationPlaceId: 999, userId: 666, organizationPlacesLotRepository, @@ -31,7 +31,7 @@ describe('Unit | UseCase | delete-organization-place-lot', function () { organizationPlacesLotRepository.get.withArgs(999).rejects(new NotFoundError()); // when - const response = await catchErr(deleteOrganizationPlaceLot)({ + const response = await catchErr(deleteOrganizationPlacesLot)({ organizationPlaceId: 999, userId: 666, organizationPlacesLotRepository, diff --git a/api/tests/prescription/organization-place/unit/domain/usecases/find-organization-places-lot_test.js b/api/tests/prescription/organization-place/unit/domain/usecases/find-organization-places-lot_test.js index 530c3861ad5..a60bd869d2a 100644 --- a/api/tests/prescription/organization-place/unit/domain/usecases/find-organization-places-lot_test.js +++ b/api/tests/prescription/organization-place/unit/domain/usecases/find-organization-places-lot_test.js @@ -1,4 +1,4 @@ -import { findOrganizationPlacesLot as findOrganizationPlaceLot } from '../../../../../../src/prescription/organization-place/domain/usecases/find-organization-places-lot.js'; +import { findOrganizationPlacesLot } from '../../../../../../src/prescription/organization-place/domain/usecases/find-organization-places-lot.js'; import { expect, sinon } from '../../../../../test-helper.js'; describe('Unit | Domain | Use Cases | find-organization-places', function () { @@ -12,7 +12,7 @@ describe('Unit | Domain | Use Cases | find-organization-places', function () { organizationPlacesLotRepository.findByOrganizationId.withArgs(organizationId).resolves(expectedOrganizationPlaces); // when - const organizationPlace = await findOrganizationPlaceLot({ + const organizationPlace = await findOrganizationPlacesLot({ organizationId, organizationPlacesLotRepository, }); diff --git a/api/tests/prescription/organization-place/unit/infrastructure/serializers/jsonapi/organization-places-lot-managment-serializer_test.js b/api/tests/prescription/organization-place/unit/infrastructure/serializers/jsonapi/organization-places-lot-managment-serializer_test.js index e5c05562197..32f57753ceb 100644 --- a/api/tests/prescription/organization-place/unit/infrastructure/serializers/jsonapi/organization-places-lot-managment-serializer_test.js +++ b/api/tests/prescription/organization-place/unit/infrastructure/serializers/jsonapi/organization-places-lot-managment-serializer_test.js @@ -1,7 +1,7 @@ import { FREE_RATE } from '../../../../../../../src/prescription/organization-place/domain/constants/organization-places-categories.js'; import * as organizationPlacesLotCategories from '../../../../../../../src/prescription/organization-place/domain/constants/organization-places-categories.js'; import { OrganizationPlacesLotManagement } from '../../../../../../../src/prescription/organization-place/domain/read-models/OrganizationPlacesLotManagement.js'; -import * as organizationPlaceLotManagementSerializer from '../../../../../../../src/prescription/organization-place/infrastructure/serializers/jsonapi/organization-places-lot-management-serializer.js'; +import * as organizationPlacesLotManagementSerializer from '../../../../../../../src/prescription/organization-place/infrastructure/serializers/jsonapi/organization-places-lot-management-serializer.js'; import { domainBuilder, expect } from '../../../../../../test-helper.js'; describe('Unit | Serializer | JSONAPI | organization-places-lot-management-serializer', function () { @@ -63,7 +63,7 @@ describe('Unit | Serializer | JSONAPI | organization-places-lot-management-seria }; // when - const json = organizationPlaceLotManagementSerializer.serialize(organizationPlaces); + const json = organizationPlacesLotManagementSerializer.serialize(organizationPlaces); // then expect(json).to.deep.equal(expectedJSON); @@ -98,7 +98,7 @@ describe('Unit | Serializer | JSONAPI | organization-places-lot-management-seria it('should convert JSON API data into an organization place set object', function () { //when - const organizationPlaceSet = organizationPlaceLotManagementSerializer.deserialize(jsonOrganizationPlacesSet); + const organizationPlaceSet = organizationPlacesLotManagementSerializer.deserialize(jsonOrganizationPlacesSet); //then expect(organizationPlaceSet).to.be.deep.equal(expectedJsonOrganizationPlacesSet); diff --git a/api/tests/prescription/organization-place/unit/infrastructure/serializers/jsonapi/organization-places-lots-serializer_test.js b/api/tests/prescription/organization-place/unit/infrastructure/serializers/jsonapi/organization-places-lots-serializer_test.js index 3cad2b7bd85..9b073657fe1 100644 --- a/api/tests/prescription/organization-place/unit/infrastructure/serializers/jsonapi/organization-places-lots-serializer_test.js +++ b/api/tests/prescription/organization-place/unit/infrastructure/serializers/jsonapi/organization-places-lots-serializer_test.js @@ -25,7 +25,7 @@ describe('Unit | Serializer | JSONAPI | organization-places-lot-management-seria const expectedJSON = { data: [ { - type: 'organization-places', + type: 'organization-places-lots', id: organizationPlaces[0].id.toString(), attributes: { count: organizationPlaces[0].count, @@ -35,7 +35,7 @@ describe('Unit | Serializer | JSONAPI | organization-places-lot-management-seria }, }, { - type: 'organization-places', + type: 'organization-places-lots', id: organizationPlaces[1].id.toString(), attributes: { count: organizationPlaces[1].count, diff --git a/orga/app/adapters/organization-places-lot.js b/orga/app/adapters/organization-places-lot.js new file mode 100644 index 00000000000..f15c51d5cee --- /dev/null +++ b/orga/app/adapters/organization-places-lot.js @@ -0,0 +1,12 @@ +import ApplicationAdapter from './application'; + +export default class PlacesLotAdapter extends ApplicationAdapter { + urlForQuery(query) { + if (query.filter.organizationId) { + const { organizationId } = query.filter; + delete query.filter.organizationId; + return `${this.host}/${this.namespace}/organizations/${organizationId}/places-lots`; + } + return super.urlForQuery(...arguments); + } +} diff --git a/orga/app/components/places/places-lot-table.gjs b/orga/app/components/places/places-lot-table.gjs new file mode 100644 index 00000000000..686c09e09a5 --- /dev/null +++ b/orga/app/components/places/places-lot-table.gjs @@ -0,0 +1,57 @@ +import PixTag from '@1024pix/pix-ui/components/pix-tag'; +import dayjs from 'dayjs'; +import { t } from 'ember-intl'; +import { eq, gt } from 'ember-truth-helpers'; + +import { STATUSES } from '../../models/organization-places-lot'; +import Header from '../table/header'; +import EmptyState from '../ui/empty-state.js'; + +function displayDate(date) { + return dayjs(date).format('DD/MM/YYYY'); +} + +function emptyCell(value) { + return value ? value : '-'; +} + + diff --git a/orga/app/models/organization-places-lot.js b/orga/app/models/organization-places-lot.js new file mode 100644 index 00000000000..633ab69a98b --- /dev/null +++ b/orga/app/models/organization-places-lot.js @@ -0,0 +1,14 @@ +import Model, { attr } from '@ember-data/model'; + +export default class PlacesLot extends Model { + @attr('number') count; + @attr('date') activationDate; + @attr('date') expirationDate; + @attr('string') status; +} + +export const STATUSES = { + PENDING: 'PENDING', + EXPIRED: 'EXPIRED', + ACTIVE: 'ACTIVE', +}; diff --git a/orga/app/routes/authenticated/places.js b/orga/app/routes/authenticated/places.js index b94eb835351..84b9c6565b8 100644 --- a/orga/app/routes/authenticated/places.js +++ b/orga/app/routes/authenticated/places.js @@ -12,7 +12,14 @@ export default class AuthenticatedPlacesRoute extends Route { } } - model() { - return this.modelFor('authenticated'); + async model() { + const placesLots = await this.store.query('organization-places-lot', { + filter: { organizationId: this.currentUser.organization.id }, + }); + const statistics = await this.modelFor('authenticated'); + return { + statistics, + placesLots, + }; } } diff --git a/orga/app/styles/components/places/index.scss b/orga/app/styles/components/places/index.scss index 8ec5b9ccafe..b910f09c3de 100644 --- a/orga/app/styles/components/places/index.scss +++ b/orga/app/styles/components/places/index.scss @@ -2,3 +2,4 @@ @import 'statistics'; @import 'place-info'; @import 'capacity-alert'; +@import 'places-lots'; diff --git a/orga/app/styles/components/places/places-lots.scss b/orga/app/styles/components/places/places-lots.scss new file mode 100644 index 00000000000..3b7e5e3d98e --- /dev/null +++ b/orga/app/styles/components/places/places-lots.scss @@ -0,0 +1,11 @@ +.places-lots{ + &_title { + @extend %pix-title-xs; + + margin: var(--pix-spacing-8x) 0 var(--pix-spacing-4x); + } + + &_empty-state { + margin-top:var(--pix-spacing-8x); + } +} diff --git a/orga/app/templates/authenticated/places.hbs b/orga/app/templates/authenticated/places.hbs index 2721694c0cc..d15f9f7f5b0 100644 --- a/orga/app/templates/authenticated/places.hbs +++ b/orga/app/templates/authenticated/places.hbs @@ -2,6 +2,7 @@ - - - \ No newline at end of file + + + + \ No newline at end of file diff --git a/orga/tests/integration/components/places/places-lot-table-test.gjs b/orga/tests/integration/components/places/places-lot-table-test.gjs new file mode 100644 index 00000000000..ea0351ef5b2 --- /dev/null +++ b/orga/tests/integration/components/places/places-lot-table-test.gjs @@ -0,0 +1,83 @@ +import { render } from '@1024pix/ember-testing-library'; +import { t } from 'ember-intl/test-support'; +import PlacesLotTable from 'pix-orga/components/places/places-lot-table'; +import { module, test } from 'qunit'; + +import setupIntlRenderingTest from '../../../helpers/setup-intl-rendering'; + +module('Integration | Component | Places | PlacesLotTable', function (hooks) { + setupIntlRenderingTest(hooks); + let placesLot, store; + const count = 123; + const activationDate = new Date('2021-09-23'); + const expirationDate = new Date('2022-09-23'); + const status = 'PENDING'; + + hooks.beforeEach(function () { + // given + store = this.owner.lookup('service:store'); + placesLot = store.createRecord('organization-places-lot', { + count, + activationDate, + expirationDate, + status, + }); + }); + test('it should render a table with a title and a caption', async function (assert) { + // when + const placesLots = [placesLot]; + const screen = await render(); + + // then + assert.ok(screen.getByRole('heading', t('pages.places.places-lots.table.title'))); + assert.ok(screen.getByText(t('pages.places.places-lots.table.caption'))); + }); + test('it should render a table with a correct headers', async function (assert) { + // when + const placesLots = [placesLot]; + const screen = await render(); + + // then + assert.ok(screen.getByRole('columnheader', { name: t('pages.places.places-lots.table.headers.count') })); + assert.ok(screen.getByRole('columnheader', { name: t('pages.places.places-lots.table.headers.activation-date') })); + assert.ok(screen.getByRole('columnheader', { name: t('pages.places.places-lots.table.headers.expiration-date') })); + assert.ok(screen.getByRole('columnheader', { name: t('pages.places.places-lots.table.headers.status') })); + }); + test('it should render a table with a place lots infos', async function (assert) { + // when + const placesLots = [placesLot]; + const screen = await render(); + + // then + assert.ok(screen.getByRole('cell', { name: placesLot.count })); + assert.ok(screen.getByRole('cell', { name: '23/09/2021' })); + assert.ok(screen.getByRole('cell', { name: '23/09/2022' })); + assert.ok(screen.getByRole('cell', { name: t('pages.places.places-lots.statuses.pending') })); + }); + test('it should render a empty cell with a dash', async function (assert) { + // when + const placesLots = [ + store.createRecord('organization-places-lot', { + count: null, + activationDate, + expirationDate: null, + status, + }), + ]; + const screen = await render(); + + // then + assert.ok(screen.getByRole('cell', { name: '-' })); + assert.ok(screen.getByRole('cell', { name: '23/09/2021' })); + assert.ok(screen.getByRole('cell', { name: '-' })); + assert.ok(screen.getByRole('cell', { name: t('pages.places.places-lots.statuses.pending') })); + }); + test('it should render an emty state if there is no places lots', async function (assert) { + // when + const placesLots = []; + const screen = await render(); + + // then + assert.ok(screen.getByText(t('pages.places.places-lots.table.empty-state'))); + }); +}); diff --git a/orga/tests/unit/adapters/organization-places-lot-test.js b/orga/tests/unit/adapters/organization-places-lot-test.js new file mode 100644 index 00000000000..ce0111dd89d --- /dev/null +++ b/orga/tests/unit/adapters/organization-places-lot-test.js @@ -0,0 +1,20 @@ +import { setupTest } from 'ember-qunit'; +import { module, test } from 'qunit'; + +module('Unit | Adapters | organization-places-lot', function (hooks) { + setupTest(hooks); + + module('#urlForFindAll', function () { + test('should build findAll url from organization-places-lot wit organizationId', async function (assert) { + // given + const adapter = this.owner.lookup('adapter:organization-places-lot'); + const organizationId = 4; + + // when + const url = await adapter.urlForQuery({ filter: { organizationId } }); + + // then + assert.true(url.endsWith(`/organizations/${organizationId}/places-lots`)); + }); + }); +}); diff --git a/orga/tests/unit/models/organization-places-lot-test.js b/orga/tests/unit/models/organization-places-lot-test.js new file mode 100644 index 00000000000..73144d2ffcc --- /dev/null +++ b/orga/tests/unit/models/organization-places-lot-test.js @@ -0,0 +1,22 @@ +import { setupTest } from 'ember-qunit'; +import { module, test } from 'qunit'; + +module('Unit | Model | organization-places-lot', function (hooks) { + setupTest(hooks); + + test('it should return the right data in the PlacesLot model', function (assert) { + const store = this.owner.lookup('service:store'); + const activationDate = new Date('2020-01-21'); + const expirationDate = new Date('2020-01-21'); + const model = store.createRecord('organization-places-lot', { + count: 123, + activationDate, + expirationDate, + status: 'PENDING', + }); + assert.strictEqual(model.count, 123); + assert.strictEqual(model.activationDate, activationDate); + assert.strictEqual(model.expirationDate, expirationDate); + assert.strictEqual(model.status, 'PENDING'); + }); +}); diff --git a/orga/tests/unit/routes/authenticated/places-test.js b/orga/tests/unit/routes/authenticated/places-test.js index 915d12bc057..1b019c10f38 100644 --- a/orga/tests/unit/routes/authenticated/places-test.js +++ b/orga/tests/unit/routes/authenticated/places-test.js @@ -7,19 +7,32 @@ module('Unit | Route | authenticated/places', function (hooks) { setupTest(hooks); module('model', function () { - test('that modelFor called authenticated', async function (assert) { + test('fetch both places lots and statistics', async function (assert) { // given const route = this.owner.lookup('route:authenticated/places'); + const store = this.owner.lookup('service:store'); + + const organizationId = Symbol('organization-id'); + const statistics = Symbol('statistics'); + const placesLots = Symbol('placesLots'); + + route.currentUser = { organization: { id: organizationId } }; const modelForStub = sinon.stub(route, 'modelFor'); + const query = sinon.stub(store, 'query'); - modelForStub.resolves(); + modelForStub.withArgs('authenticated').resolves(statistics); + query + .withArgs('organization-places-lot', { + filter: { organizationId }, + }) + .resolves(placesLots); // when - await route.model(); + const result = await route.model(); // then - assert.ok(modelForStub.calledWithExactly('authenticated')); + assert.deepEqual(result, { statistics, placesLots }); }); }); diff --git a/orga/translations/en.json b/orga/translations/en.json index 479ca8f8cc9..5ff03d687d6 100644 --- a/orga/translations/en.json +++ b/orga/translations/en.json @@ -1123,7 +1123,25 @@ }, "places": { "title": "Seats", - "before-date": "on" + "before-date": "on", + "places-lots": { + "statuses": { + "active": "Active", + "expired": "Expired", + "pending": "Pending" + }, + "table": { + "title": "Seats purchase history", + "caption": "Table listing your organisation's seat purchases. For each purchase, it shows: the number of tickets, the activation date, the expiry date and the status (active, coming soon or expired). It is ordered according to the status of the purchases and their expiry date.", + "empty-state":"No seats yet!", + "headers": { + "activationDate": "Activation date", + "count": "Seats count", + "expirationDate": "Expiration date", + "status": "Status" + } + } + } }, "preselect-target-profile": { "title": "Topic selection", diff --git a/orga/translations/fr.json b/orga/translations/fr.json index 3b76a745bb6..6ad7ebc8321 100644 --- a/orga/translations/fr.json +++ b/orga/translations/fr.json @@ -1131,7 +1131,25 @@ }, "places": { "title": "Places", - "before-date": "au" + "before-date": "au", + "places-lots": { + "statuses": { + "active": "Actif", + "expired": "Expiré", + "pending": "À venir" + }, + "table": { + "title": "Historique d'achat de places", + "caption": "Tableau listant les achats de places de votre organisation. Pour chaque achat, il indique : le nombre de places, la date d’activation, la date d’expiration ainsi que le statut (actif, à venir ou expiré). Il est ordonné selon le statut des achats et leur date d’expiration.", + "empty-state":"Aucune place pour le moment !", + "headers": { + "activation-date": "Date d'activation", + "count": "Nombre de places", + "expiration-date": "Date d'expiration", + "status": "Statut" + } + } + } }, "preselect-target-profile": { "title": "Sélection des sujets", diff --git a/orga/translations/nl.json b/orga/translations/nl.json index a99b2a4dd5c..8c47b8581a1 100644 --- a/orga/translations/nl.json +++ b/orga/translations/nl.json @@ -1156,7 +1156,25 @@ }, "places": { "before-date": "op", - "title": "Plaatsen" + "title": "Plaatsen", + "places-lots": { + "table": { + "title": "Seats purchase history", + "caption": "Table listing your organisation's seat purchases. For each purchase, it shows: the number of tickets, the activation date, the expiry date and the status (active, coming soon or expired). It is ordered according to the status of the purchases and their expiry date.", + "empty-state":"No seats yet!", + "headers": { + "count": "Seats count", + "activationDate": "Activation date", + "expirationDate": "Expiration date", + "status": "Status" + } + }, + "statuses": { + "pending": "Pending", + "active": "Active", + "expired": "Expired" + } + } }, "preselect-target-profile": { "download": "Downloaden selectie onderwerpen ({ numberOfTubesSelected }) (JSON, { fileSize }kB)",