From d0bfb35d20de07ca68d480ec09ccdd0cb73b32f1 Mon Sep 17 00:00:00 2001 From: eletallbetagouv <107104509+eletallbetagouv@users.noreply.github.com> Date: Mon, 3 Feb 2025 14:04:39 +0200 Subject: [PATCH] TRELLO-2853 add endpoint to get most active users of company --- app/controllers/CompanyAccessController.scala | 6 +++++ app/loader/SignalConsoApplicationLoader.scala | 3 ++- app/models/access/UserWithAccessLevel.scala | 23 +++++++++++++++++++ .../CompanyAccessOrchestrator.scala | 21 ++++++++++++++++- app/repositories/event/EventRepository.scala | 18 +++++++++++++++ .../event/EventRepositoryInterface.scala | 1 + conf/routes | 2 +- 7 files changed, 71 insertions(+), 3 deletions(-) diff --git a/app/controllers/CompanyAccessController.scala b/app/controllers/CompanyAccessController.scala index 9f124deb..e673e2d9 100644 --- a/app/controllers/CompanyAccessController.scala +++ b/app/controllers/CompanyAccessController.scala @@ -54,6 +54,12 @@ class CompanyAccessController( .map(userWithAccessLevel => Ok(Json.toJson(userWithAccessLevel))) } + def listAccessesMostActive(siret: String) = withCompanyAccess(siret).async { implicit request => + companyAccessOrchestrator + .listAccessesMostActive(request.company, request.identity) + .map(mostActive => Ok(Json.toJson(mostActive))) + } + def countAccesses(siret: String) = withCompanyAccess(siret).async { implicit request => companyAccessOrchestrator .listAccesses(request.company, request.identity) diff --git a/app/loader/SignalConsoApplicationLoader.scala b/app/loader/SignalConsoApplicationLoader.scala index a78dfa0e..40671a47 100644 --- a/app/loader/SignalConsoApplicationLoader.scala +++ b/app/loader/SignalConsoApplicationLoader.scala @@ -371,6 +371,7 @@ class SignalConsoComponents( companyRepository, accessTokenRepository, companyActivationAttemptRepository, + eventRepository, proAccessTokenOrchestrator ) @@ -962,8 +963,8 @@ class SignalConsoComponents( eventsController, reportFileController, adminController, - statisticController, companyAccessController, + statisticController, authController, accountController, blacklistedEmailsController, diff --git a/app/models/access/UserWithAccessLevel.scala b/app/models/access/UserWithAccessLevel.scala index 95d3b450..167a8144 100644 --- a/app/models/access/UserWithAccessLevel.scala +++ b/app/models/access/UserWithAccessLevel.scala @@ -32,3 +32,26 @@ object UserWithAccessLevel { .withFieldConst(_.isHeadOffice, isHeadOffice) .transform } + +case class UserWithAccessLevelAndNbResponse( + userId: UUID, + firstName: String, + lastName: String, + email: EmailAddress, + level: String, + editable: Boolean, + isHeadOffice: Boolean, + nbResponses: Int +) + +object UserWithAccessLevelAndNbResponse { + + implicit val UserWithAccessLevelAndNbResponsesWrites: Writes[UserWithAccessLevelAndNbResponse] = + Json.writes[UserWithAccessLevelAndNbResponse] + + def build(access: UserWithAccessLevel, nbResponses: Int): UserWithAccessLevelAndNbResponse = + access + .into[UserWithAccessLevelAndNbResponse] + .withFieldConst(_.nbResponses, nbResponses) + .transform +} diff --git a/app/orchestrators/CompanyAccessOrchestrator.scala b/app/orchestrators/CompanyAccessOrchestrator.scala index a866bf83..3b34bf21 100644 --- a/app/orchestrators/CompanyAccessOrchestrator.scala +++ b/app/orchestrators/CompanyAccessOrchestrator.scala @@ -7,6 +7,8 @@ import controllers.error.AppError.TooMuchCompanyActivationAttempts import models.AccessToken import models.User import models.access.ActivationLinkRequest +import models.access.UserWithAccessLevel +import models.access.UserWithAccessLevelAndNbResponse import java.time.OffsetDateTime.now import cats.implicits.catsSyntaxApplicativeId @@ -19,7 +21,6 @@ import models.UserRole.DGCCRF import models.UserRole.Professionnel import models.UserRole.ReadOnlyAdmin import models.UserRole.SuperAdmin -import models.access.UserWithAccessLevel import models.access.UserWithAccessLevel.toApi import models.company.AccessLevel import models.company.Company @@ -29,6 +30,9 @@ import repositories.accesstoken.AccessTokenRepositoryInterface import repositories.company.CompanyRepositoryInterface import repositories.companyaccess.CompanyAccessRepositoryInterface import repositories.companyactivationattempt.CompanyActivationAttemptRepositoryInterface +import repositories.event.EventFilter +import repositories.event.EventRepositoryInterface +import utils.Constants.ActionEvent.REPORT_PRO_RESPONSE import utils.SIREN import utils.SIRET @@ -42,6 +46,7 @@ class CompanyAccessOrchestrator( val companyRepository: CompanyRepositoryInterface, val accessTokenRepository: AccessTokenRepositoryInterface, val companyActivationAttemptRepository: CompanyActivationAttemptRepositoryInterface, + val eventsRepository: EventRepositoryInterface, val accessesOrchestrator: ProAccessTokenOrchestrator )(implicit val ec: ExecutionContext) { @@ -133,6 +138,20 @@ class CompanyAccessOrchestrator( } yield filteredHeadOfficeAccess.getOrElse(List.empty) ++ subsidiaryUserAccess } + def listAccessesMostActive(company: Company, user: User): Future[List[UserWithAccessLevelAndNbResponse]] = + for { + accesses <- listAccesses(company, user) + nbResponsesByUserIds <- eventsRepository.countCompanyEventsByUsers( + companyId = company.id, + usersIds = accesses.map(_.userId), + EventFilter(action = Some(REPORT_PRO_RESPONSE)) + ) + accessesWithNbResponses = accesses.map(access => + UserWithAccessLevelAndNbResponse.build(access, nbResponsesByUserIds.getOrElse(access.userId, 0)) + ) + mostActive = accessesWithNbResponses.sortBy(-_.nbResponses).take(3) + } yield mostActive + private def getHeadOffice(company: Company): Future[Option[Company]] = companyRepository .findHeadOffices(List(SIREN.fromSIRET(company.siret)), openOnly = false) diff --git a/app/repositories/event/EventRepository.scala b/app/repositories/event/EventRepository.scala index 50448da7..bc0d750c 100644 --- a/app/repositories/event/EventRepository.scala +++ b/app/repositories/event/EventRepository.scala @@ -128,6 +128,24 @@ class EventRepository( .result } + override def countCompanyEventsByUsers( + companyId: UUID, + usersIds: List[UUID], + filter: EventFilter + ): Future[Map[UUID, Int]] = + db.run { + getRawEvents(filter) + .filter(_.companyId === companyId) + .filter( + _.userId inSetBind usersIds + ) + .groupBy(_.userId) + .map { case (userId, events) => + userId.get -> events.length + } + .result + }.map(_.toMap) + override def getReportResponseReviews(companyId: Option[UUID]): Future[Seq[Event]] = db.run( table diff --git a/app/repositories/event/EventRepositoryInterface.scala b/app/repositories/event/EventRepositoryInterface.scala index 1bf64467..4530aebd 100644 --- a/app/repositories/event/EventRepositoryInterface.scala +++ b/app/repositories/event/EventRepositoryInterface.scala @@ -32,6 +32,7 @@ trait EventRepositoryInterface extends CRUDRepositoryInterface[Event] { def getCompanyEventsWithUsers(companyId: UUID, filter: EventFilter): Future[List[(Event, Option[User])]] + def countCompanyEventsByUsers(companyId: UUID, usersIds: List[UUID], filter: EventFilter): Future[Map[UUID, Int]] def getReportResponseReviews(companyId: Option[UUID]): Future[Seq[Event]] def fetchEventsOfReports(reports: List[Report]): Future[Map[UUID, List[Event]]] diff --git a/conf/routes b/conf/routes index 7dd8b031..c854287e 100644 --- a/conf/routes +++ b/conf/routes @@ -102,7 +102,7 @@ GET /api/albert/classification/:reportId controller ############################### GET /api/companies/:siret/events controllers.EventsController.getCompanyEvents(siret: SIRET, eventType: Option[String]) -# various stats for the company page +GET /api/accesses/:siret/most-active controllers.CompanyAccessController.listAccessesMostActive(siret: String) GET /api/stats/reports/delay/responsed controllers.StatisticController.getDelayReportResponseInHours(companyId: Option[java.util.UUID]) GET /api/stats/reports/tags controllers.StatisticController.getReportsTagsDistribution(companyId: Option[java.util.UUID]) GET /api/stats/reports/status controllers.StatisticController.getReportsStatusDistribution(companyId: Option[java.util.UUID])