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 les statistiques dans Pix Orga (PIX-15455) #10751

Merged
merged 6 commits into from
Dec 11, 2024
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
@@ -1,5 +1,6 @@
import { NoProfileRewardsFoundError } from '../../../profile/domain/errors.js';
import { usecases } from '../domain/usecases/index.js';
import * as analysisByTubesSerializer from '../infrastructure/serializers/jsonapi/analysis-by-tubes-serializer.js';

const getAttestationZipForDivisions = async function (request, h) {
const organizationId = request.params.organizationId;
Expand All @@ -17,10 +18,11 @@ const getAttestationZipForDivisions = async function (request, h) {
}
};

const getAnalysisByTubes = async function (request, h) {
const getAnalysisByTubes = async function (request, h, dependencies = { analysisByTubesSerializer }) {
const organizationId = request.params.organizationId;
const result = await usecases.getAnalysisByTubes({ organizationId });
return h.response(result).code(200);
const serializedResult = dependencies.analysisByTubesSerializer.serialize(result);
return h.response(serializedResult).code(200);
};

const organizationLearnersController = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { randomUUID } from 'node:crypto';

import jsonapiSerializer from 'jsonapi-serializer';

const { Serializer } = jsonapiSerializer;

const serialize = function (analysisByTubes) {
return new Serializer('analysis-by-tubes', {
transform(analysisByTubes) {
return {
id: randomUUID(),
...analysisByTubes,
};
},
attributes: ['data'],
}).serialize(analysisByTubes);
};

export { serialize };
Original file line number Diff line number Diff line change
Expand Up @@ -109,11 +109,16 @@ describe('Prescription | Organization Learner | Acceptance | Application | Organ
const response = await server.inject(options);

// then
const expectedResult = {
data: {
attributes: {
data: 'expected-data',
},
type: 'analysis-by-tubes',
},
};
expect(response.statusCode).to.equal(200);
expect(response.result).to.deep.equal({
data: expectedData,
status: 'success',
});
expect(response.result.data).to.deep.includes(expectedResult.data);
});
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,13 +98,19 @@ describe('Unit | Application | Organization-Learner | organization-learners-cont
});
});

describe('#getLearnersLevelsByTubes', function () {
describe('#getAnalysisByTubes', function () {
it('should return data', async function () {
// given
const organizationId = 123;

const useCaseResult = Symbol('use-case-result');
sinon.stub(usecases, 'getAnalysisByTubes');
usecases.getAnalysisByTubes.withArgs({ organizationId }).resolves();
usecases.getAnalysisByTubes.withArgs({ organizationId }).resolves(useCaseResult);
const analysisByTubesSerializer = {
serialize: sinon.stub(),
};
const expectedResult = Symbol('expected-result');
analysisByTubesSerializer.serialize.withArgs(useCaseResult).returns(expectedResult);

const request = {
params: {
Expand All @@ -113,9 +119,12 @@ describe('Unit | Application | Organization-Learner | organization-learners-cont
};

// when
const response = await organizationLearnersController.getAnalysisByTubes(request, hFake);
const response = await organizationLearnersController.getAnalysisByTubes(request, hFake, {
analysisByTubesSerializer,
});

// then
expect(response.source).to.equal(expectedResult);
expect(response.statusCode).to.equal(200);
});
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import * as serializer from '../../../../../../../src/prescription/organization-learner/infrastructure/serializers/jsonapi/analysis-by-tubes-serializer.js';
import { expect } from '../../../../../../test-helper.js';

describe('Unit | Serializer | JSONAPI | analysis-by-tubes-serializer', function () {
describe('#serialize', function () {
it('should convert an analysis-by-tubes object into JSON API data', function () {
// given
const analysisByTubes = { data: [{}] };
const expectedType = 'analysis-by-tubes';
const expectedData = analysisByTubes.data;

// when
const json = serializer.serialize(analysisByTubes);

// then
expect(json.data.type).to.equal(expectedType);
expect(json.data.attributes.data).to.deep.equal(expectedData);
expect(json.data.id).to.exist;
});
});
});
10 changes: 10 additions & 0 deletions orga/app/adapters/analysis-by-tube.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import ApplicationAdapter from './application';

export default class AnalysisByTubeAdapter extends ApplicationAdapter {
urlForQueryRecord(query) {
const { organizationId } = query;
delete query.organizationId;

return `${this.host}/${this.namespace}/organizations/${organizationId}/organization-learners-level-by-tubes`;
}
}
12 changes: 6 additions & 6 deletions orga/app/components/layout/sidebar.gjs
Original file line number Diff line number Diff line change
Expand Up @@ -122,18 +122,18 @@ export default class SidebarMenu extends Component {
{{t "navigation.main.team"}}
</PixNavigationButton>

{{#if this.shouldDisplayStatisticsEntry}}
<PixNavigationButton @route="authenticated.statistics" @icon="monitoring">
{{t "navigation.main.statistics"}}
</PixNavigationButton>
{{/if}}

{{#if this.shouldDisplayPlacesEntry}}
<PixNavigationButton @route="authenticated.places" @icon="seat">
{{t "navigation.main.places"}}
</PixNavigationButton>
{{/if}}

{{#if this.shouldDisplayStatisticsEntry}}
<PixNavigationButton @route="authenticated.statistics" @icon="monitoring">
{{t "navigation.main.statistics"}}
</PixNavigationButton>
{{/if}}

{{#if this.documentationUrl}}
<PixNavigationButton href={{this.documentationUrl}} @target="_blank" rel="noopener noreferrer" @icon="book">
{{t "navigation.main.documentation"}}
Expand Down
69 changes: 69 additions & 0 deletions orga/app/components/statistics/cover-rate-gauge.gjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { guidFor } from '@ember/object/internals';
import { htmlSafe } from '@ember/template';
import Component from '@glimmer/component';
import { t } from 'ember-intl';

const MAX_REACHABLE_LEVEL = 7;

export default class CoverRateGauge extends Component {
get id() {
return guidFor(this);
}

get userLevel() {
return this.formatNumber(this.args.userLevel);
}

get tubeLevel() {
return this.formatNumber(this.args.tubeLevel);
}

formatNumber = (str) => {
const num = Number(str);
const oneDigitNum = num.toFixed(1);
if (oneDigitNum.toString().endsWith('0')) {
return Math.ceil(num);
}
return oneDigitNum;
};

getGaugeSizeStyle = (level, { withExtraPercentage }) => {
const gaugeSize = (level / MAX_REACHABLE_LEVEL) * 100;
return htmlSafe(`width: calc(${gaugeSize}% + ${withExtraPercentage ? 5 : 0}%)`);
xav-car marked this conversation as resolved.
Show resolved Hide resolved
};

<template>
<div class="cover-rate-gauge">
VincentHardouin marked this conversation as resolved.
Show resolved Hide resolved
<div class="cover-rate-gauge__container">
<div
class="cover-rate-gauge__level cover-rate-gauge__level--tube-level"
style={{this.getGaugeSizeStyle this.tubeLevel withExtraPercentage=true}}
>
{{this.tubeLevel}}
</div>
<div class="cover-rate-gauge__background">
<label for={{this.id}} class="screen-reader-only">{{t
"pages.statistics.gauge.label"
userLevel=this.userLevel
tubeLevel=this.tubeLevel
}}</label>
<progress
class="cover-rate-gauge__progress"
id={{this.id}}
max={{this.tubeLevel}}
value={{this.userLevel}}
style={{this.getGaugeSizeStyle this.tubeLevel withExtraPercentage=false}}
>
{{this.userLevel}}
</progress>
</div>
<div
class="cover-rate-gauge__level cover-rate-gauge__level--user-level"
style={{this.getGaugeSizeStyle this.userLevel withExtraPercentage=true}}
>
{{this.userLevel}}
</div>
</div>
</div>
</template>
}
47 changes: 47 additions & 0 deletions orga/app/components/statistics/index.gjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { t } from 'ember-intl';

import Header from '../table/header';
import CoverRateGauge from './cover-rate-gauge';
import TagLevel from './tag-level';

const analysisByTubes = (model) => {
return model.data.sort(
(a, b) => a.competence_code.localeCompare(b.competence_code) || a.sujet.localeCompare(b.sujet),
);
};

<template>
<div class="statistics-page__header">
<h1 class="page-title">{{t "pages.statistics.title"}}</h1>
</div>

<section class="statistics-page__section">
<table class="panel">
<caption class="screen-reader-only">{{t "pages.statistics.table.caption"}}</caption>
<thead>
<tr>
<Header @size="wide" scope="col">{{t "pages.statistics.table.headers.skills"}}</Header>
<Header @size="medium" scope="col">{{t "pages.statistics.table.headers.topics"}}</Header>
<Header @size="medium" @align="center" scope="col">{{t "pages.statistics.table.headers.positioning"}}</Header>
<Header @align="center" @size="medium" scope="col">{{t
"pages.statistics.table.headers.reached-level"
}}</Header>
</tr>
</thead>
<tbody>
{{#each (analysisByTubes @model) as |line|}}
<tr>
<td>{{line.competence_code}} {{line.competence}}</td>
<td>{{line.sujet}}</td>
<td>
<CoverRateGauge @userLevel={{line.niveau_par_user}} @tubeLevel={{line.niveau_par_sujet}} />
</td>
<td class="table__column--center">
<TagLevel @level={{line.niveau_par_user}} />
</td>
</tr>
{{/each}}
</tbody>
</table>
</section>
</template>
23 changes: 23 additions & 0 deletions orga/app/components/statistics/tag-level.gjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import PixTag from '@1024pix/pix-ui/components/pix-tag';
import Component from '@glimmer/component';
import { t } from 'ember-intl';

const MAX_LEVEL = {
novice: 3,
independent: 4,
advanced: 6,
};

export default class TagLevel extends Component {
get category() {
const parsedLevel = Math.ceil(parseFloat(this.args.level));
if (parsedLevel < MAX_LEVEL.novice) return 'pages.statistics.level.novice';
if (parsedLevel < MAX_LEVEL.independent) return 'pages.statistics.level.independent';
if (parsedLevel < MAX_LEVEL.advanced) return 'pages.statistics.level.advanced';
return 'pages.statistics.level.expert';
}

<template>
<PixTag>{{t this.category}}</PixTag>
</template>
}
5 changes: 5 additions & 0 deletions orga/app/models/analysis-by-tube.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import Model, { attr } from '@ember-data/model';

export default class AnalysisByTube extends Model {
@attr() data;
}
5 changes: 5 additions & 0 deletions orga/app/routes/authenticated/statistics.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,15 @@ import { service } from '@ember/service';
export default class AuthenticatedStatisticsRoute extends Route {
@service currentUser;
@service router;
@service store;

beforeModel() {
if (!this.currentUser.canAccessStatisticsPage) {
this.router.replaceWith('application');
}
}

model() {
return this.store.queryRecord('analysis-by-tube', { organizationId: this.currentUser.organization.id });
}
}
2 changes: 2 additions & 0 deletions orga/app/styles/app.scss
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@
@import 'components/sup-organization-participant/replace-students-modal';
@import 'components/import-banner';
@import 'components/import-information-banner';
@import 'components/statistics';
@import 'components/statistics/cover-rate-gauge';

// pages
@import 'pages/login';
Expand Down
13 changes: 13 additions & 0 deletions orga/app/styles/components/statistics.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
.statistics-page__header {
display: flex;
gap: var(--pix-spacing-2x);
align-items: baseline;
}

.statistics-page__section {
tbody > tr {
&:nth-child(odd) {
background-color: var(--pix-neutral-20);
}
}
}
Loading
Loading