diff --git a/app/controllers/ApplicationController.scala b/app/controllers/ApplicationController.scala index 22ce8458d..ac1660464 100644 --- a/app/controllers/ApplicationController.scala +++ b/app/controllers/ApplicationController.scala @@ -19,7 +19,6 @@ import models._ import models.formModels.{AnswerFormData, ApplicationFormData, InvitationFormData} import modules.AppConfig import org.webjars.play.WebJarsUtil -import play.api.cache.AsyncCacheApi import play.api.data.Forms._ import play.api.data._ import play.api.data.format.{Formats, Formatter} @@ -31,7 +30,6 @@ import play.twirl.api.Html import serializers.Keys import serializers.ApiModel.{ApplicationMetadata, ApplicationMetadataResult} import services._ -import views.stats.StatsData import java.nio.file.{Files, Path, Paths} import java.time.{LocalDate, ZonedDateTime} @@ -47,7 +45,6 @@ import scala.util.{Failure, Success, Try} @Singleton case class ApplicationController @Inject() ( applicationService: ApplicationService, - cache: AsyncCacheApi, config: AppConfig, eventService: EventService, fileService: FileService, @@ -569,143 +566,6 @@ case class ApplicationController @Inject() ( ).withHeaders(CACHE_CONTROL -> "no-store") } - private def statsAggregates(applications: List[Application], users: List[User]): StatsData = { - val now = Time.nowParis() - val applicationsByArea: Map[Area, List[Application]] = - applications - .groupBy(_.area) - .flatMap { case (areaId: UUID, applications: Seq[Application]) => - Area.all - .find(area => area.id === areaId) - .map(area => (area, applications)) - } - - val firstDate: ZonedDateTime = - if (applications.isEmpty) now else applications.map(_.creationDate).min - val months = Time.monthsBetween(firstDate, now) - val allApplications = applicationsByArea.flatMap(_._2).toList - val allApplicationsByArea = applicationsByArea.map { case (area, applications) => - StatsData.AreaAggregates( - area = area, - StatsData.ApplicationAggregates( - applications = applications, - months = months, - usersRelatedToApplications = users - ) - ) - }.toList - val data = StatsData( - all = StatsData.ApplicationAggregates( - applications = allApplications, - months = months, - usersRelatedToApplications = users - ), - aggregatesByArea = allApplicationsByArea - ) - data - } - - private def anonymousGroupsAndUsers( - groups: List[UserGroup] - ): Future[(List[User], List[Application])] = - for { - users <- userService.byGroupIdsAnonymous(groups.map(_.id)) - applications <- applicationService.allForUserIds(users.map(_.id), none) - } yield (users, applications) - - private def generateStats( - areaIds: List[UUID], - organisationIds: List[Organisation.Id], - groupIds: List[UUID], - creationMinDate: LocalDate, - creationMaxDate: LocalDate, - rights: Authorization.UserRights - )(implicit request: RequestWithUserData[_]): Future[Html] = { - eventService.log( - StatsShowed, - "Génère les stats pour les paramètres [Territoires '" + areaIds.mkString(",") + - "' ; Organismes '" + organisationIds.mkString(",") + - "' ; Groupes '" + groupIds.mkString(",") + - "' ; Date début '" + creationMinDate + - "' ; Date fin '" + creationMaxDate + "']" - ) - - val usersAndApplications: Future[(List[User], List[Application])] = - (areaIds, organisationIds, groupIds) match { - case (Nil, Nil, Nil) => - userService.allNoNameNoEmail.zip(applicationService.all()) - case (_ :: _, Nil, Nil) => - for { - groups <- userGroupService.byAreas(areaIds) - users <- ( - userService.byGroupIdsAnonymous(groups.map(_.id)), - applicationService.allForAreas(areaIds, None) - ).mapN(Tuple2.apply) - } yield users - case (_ :: _, _ :: _, Nil) => - for { - groups <- userGroupService - .byOrganisationIds(organisationIds) - .map(_.filter(_.areaIds.intersect(areaIds).nonEmpty)) - users <- userService.byGroupIdsAnonymous(groups.map(_.id)) - applications <- applicationService - .allForUserIds(users.map(_.id), none) - .map(_.filter(application => areaIds.contains(application.area))) - } yield (users, applications) - case (_, _ :: _, _) => - userGroupService.byOrganisationIds(organisationIds).flatMap(anonymousGroupsAndUsers) - case (_, Nil, _) => - userGroupService - .byIdsFuture(groupIds) - .flatMap(anonymousGroupsAndUsers) - .map { case (users, allApplications) => - val applications = allApplications.filter { application => - application.isWithoutInvitedGroupIdsLegacyCase || - application.invitedGroups.intersect(groupIds.toSet).nonEmpty || - users.exists(user => user.id === application.creatorUserId) - } - (users, applications) - } - } - - // Filter creation dates - def isBeforeOrEqual(d1: LocalDate, d2: LocalDate): Boolean = !d1.isAfter(d2) - - val applicationsFuture = usersAndApplications.map { case (_, applications) => - applications.filter(application => - isBeforeOrEqual(creationMinDate, application.creationDate.toLocalDate) && - isBeforeOrEqual(application.creationDate.toLocalDate, creationMaxDate) - ) - } - - // Users whose id is in the `Application` - val relatedUsersFuture: Future[List[User]] = applicationsFuture.flatMap { applications => - val ids: Set[UUID] = applications.flatMap { application => - application.creatorUserId :: application.invitedUsers.keys.toList - }.toSet - if (ids.size > 1000) { - // We don't want to send a giga-query to PG - // it fails with - // IOException - // Tried to send an out-of-range integer as a 2-byte value: 33484 - userService.all.map(_.filter(user => ids.contains(user.id))) - } else { - userService.byIdsFuture(ids.toList, includeDisabled = true) - } - } - - // Note: `users` are Users on which we make stats (count, ...) - // `relatedUsers` are Users to help Applications stats (linked orgs, ...) - for { - users <- usersAndApplications.map { case (users, _) => users } - applications <- applicationsFuture - relatedUsers <- relatedUsersFuture - } yield views.html.stats.charts(Authorization.isAdmin(rights))( - statsAggregates(applications, relatedUsers), - users - ) - } - // Handles some edge cases from browser compatibility private val localDateMapping: Mapping[LocalDate] = { val formatter = new Formatter[LocalDate] { @@ -734,9 +594,6 @@ case class ApplicationController @Inject() ( ) ) - private val statsCSP = - "connect-src 'self' https://stats.data.gouv.fr; base-uri 'none'; img-src 'self' data: stats.data.gouv.fr; form-action 'self'; frame-src 'self' *.aplus.beta.gouv.fr; style-src 'self' 'unsafe-inline' stats.data.gouv.fr; object-src 'none'; script-src 'self' 'unsafe-inline' stats.data.gouv.fr; default-src 'none'; font-src 'self'; frame-ancestors 'self'" - val statsAction = loginAction.withPublicPage(Ok(views.publicStats.page)) def stats: Action[AnyContent] = @@ -786,56 +643,33 @@ case class ApplicationController @Inject() ( userGroupService.byIdsFuture(allGroupsIds).flatMap { groups => val groupsThatCanBeFilteredBy = groups.filter(group => dropdownGroupIds.contains[UUID](group.id)) - val charts: Future[Html] = - if ( - (Authorization.isAdmin(rights) && request.getQueryString("oldstats").isEmpty) || - request.getQueryString("newstats").nonEmpty - ) { - val validQueryGroups = groups.filter(group => validQueryGroupIds.contains[UUID](group.id)) - val creatorGroupIds = validQueryGroups - .filter(group => - group.organisationId - .map(id => Organisation.organismesAidants.map(_.id).contains[Organisation.Id](id)) - .getOrElse(false) - ) - .map(_.id) - val invitedGroupIds = - validQueryGroupIds.filterNot(id => creatorGroupIds.contains[UUID](id)) - - Future.successful( - views.internalStats.charts( - views.internalStats.Filters( - startDate = creationMinDate, - endDate = creationMaxDate, - areaIds, - organisationIds, - creatorGroupIds, - invitedGroupIds, - ), - config - ) + val charts: Future[Html] = { + val validQueryGroups = groups.filter(group => validQueryGroupIds.contains[UUID](group.id)) + val creatorGroupIds = validQueryGroups + .filter(group => + group.organisationId + .map(id => Organisation.organismesAidants.map(_.id).contains[Organisation.Id](id)) + .getOrElse(false) ) - } else { - val cacheKey = - Authorization.isAdmin(rights).toString + - ".stats." + - Hash.sha256( - areaIds.toString + organisationIds.toString + validQueryGroupIds.toString + - creationMinDate.toString + creationMaxDate.toString - ) + .map(_.id) + val invitedGroupIds = + validQueryGroupIds.filterNot(id => creatorGroupIds.contains[UUID](id)) + + Future.successful( + views.internalStats.charts( + views.internalStats.Filters( + startDate = creationMinDate, + endDate = creationMaxDate, + areaIds, + organisationIds, + creatorGroupIds, + invitedGroupIds, + ), + config + ) + ) + } - cache - .getOrElseUpdate[Html](cacheKey, 1.hours)( - generateStats( - areaIds, - organisationIds, - validQueryGroupIds, - creationMinDate, - creationMaxDate, - rights - ) - ) - } charts .map { html => eventService.log( @@ -857,7 +691,7 @@ case class ApplicationController @Inject() ( creationMinDate, creationMaxDate ) - ).withHeaders(CONTENT_SECURITY_POLICY -> statsCSP) + ) } } } diff --git a/app/helper/Time.scala b/app/helper/Time.scala index e95b1339f..c3b3c5274 100644 --- a/app/helper/Time.scala +++ b/app/helper/Time.scala @@ -1,10 +1,9 @@ package helper import cats.Order -import java.time.{Instant, LocalDate, YearMonth, ZoneId, ZonedDateTime} +import java.time.{Instant, LocalDate, ZoneId, ZonedDateTime} import java.time.format.DateTimeFormatter import java.util.Locale -import scala.collection.immutable.ListMap object Time { @@ -30,12 +29,7 @@ object Time { def formatPatternFr(date: LocalDate, pattern: String): String = date.format(DateTimeFormatter.ofPattern(pattern, Locale.FRANCE)) - // Note that .atDay(1) will yield incorrect format value - def formatMonthYearAllLetters(month: YearMonth): String = - month.atDay(15).format(monthYearAllLettersFormatter) - val adminsFormatter = DateTimeFormatter.ofPattern("dd/MM/YY-HH:mm", Locale.FRANCE) - private val monthYearAllLettersFormatter = DateTimeFormatter.ofPattern("MMMM YYYY", Locale.FRANCE) // Note: we use an Instant here to make clear that we will set our own TZ def formatForAdmins(date: Instant): String = @@ -45,32 +39,6 @@ object Time { val dateWithHourFormatter = DateTimeFormatter.ofPattern("dd/MM/YYYY H'h'", Locale.FRANCE) - def weeksMap(fromDate: ZonedDateTime, toDate: ZonedDateTime): ListMap[String, String] = { - val keyFormatter = DateTimeFormatter.ofPattern("YYYY/ww", Locale.FRANCE) - val valueFormatter = DateTimeFormatter.ofPattern("E dd MMM YYYY", Locale.FRANCE) - val weekFieldISO = java.time.temporal.WeekFields.of(Locale.FRANCE).dayOfWeek() - def recursion(date: ZonedDateTime): ListMap[String, String] = - if (date.isBefore(fromDate)) { - ListMap() - } else { - recursion(date.minusWeeks(1)) + - (date.format(keyFormatter) -> date.format(valueFormatter)) - } - val toDateFirstDayOfWeek = toDate.`with`(weekFieldISO, 1) - recursion(toDateFirstDayOfWeek) - } - - def monthsBetween(fromDate: ZonedDateTime, toDate: ZonedDateTime): List[YearMonth] = { - val beginning = fromDate.withDayOfMonth(1) - def recursion(date: ZonedDateTime): Vector[YearMonth] = - if (date.isBefore(beginning)) { - Vector.empty[YearMonth] - } else { - recursion(date.minusMonths(1)) :+ YearMonth.from(date) - } - recursion(toDate).toList - } - implicit final val zonedDateTimeInstance: Order[ZonedDateTime] = new Order[ZonedDateTime] { override def compare(x: ZonedDateTime, y: ZonedDateTime): Int = x.compareTo(y) diff --git a/app/models/Application.scala b/app/models/Application.scala index 0368369a1..de4200da4 100644 --- a/app/models/Application.scala +++ b/app/models/Application.scala @@ -41,10 +41,6 @@ case class Application( personalDataWiped: Boolean = false, ) extends AgeModel { - // Legacy case, can be removed once data has been cleaned up. - val isWithoutInvitedGroupIdsLegacyCase: Boolean = - invitedGroupIdsAtCreation.isEmpty - val invitedGroups: Set[UUID] = (invitedGroupIdsAtCreation ::: answers.flatMap(_.invitedGroupIds)).toSet @@ -119,9 +115,6 @@ case class Application( def invitedUsers(users: List[User]): List[User] = invitedUsers.keys.flatMap(userId => users.find(_.id === userId)).toList - def creatorUserQualite(users: List[User]): Option[String] = - users.find(_.id === creatorUserId).map(_.qualite) - def allUserInfos = userInfos ++ answers.flatMap(_.userInfos.getOrElse(Map())) lazy val anonymousApplication = { @@ -169,13 +162,6 @@ case class Application( // TODO: remove def haveUserInvitedOn(user: User) = invitedUsers.keys.toList.contains(user.id) - // Stats - lazy val estimatedClosedDate = (closedDate, closed) match { - case (Some(date), _) => Some(date) - case (_, true) => Some(answers.lastOption.map(_.creationDate).getOrElse(creationDate)) - case _ => None - } - lazy val resolutionTimeInMinutes: Option[Int] = if (closed) { val lastDate = answers.lastOption.map(_.creationDate).orElse(closedDate).getOrElse(creationDate) Some(MINUTES.between(creationDate, lastDate).toInt) diff --git a/app/services/NotificationService.scala b/app/services/NotificationService.scala index b5288df94..6960bbf2b 100644 --- a/app/services/NotificationService.scala +++ b/app/services/NotificationService.scala @@ -84,25 +84,15 @@ class NotificationService @Inject() ( // Retrieve data val userIds = (application.invitedUsers ++ answer.invitedUsers).keys val users = userService.byIds(userIds.toList) - val (allGroups, alreadyPresentGroupIds): (List[UserGroup], Set[UUID]) = - // This legacy case can be removed once data has been fixed - if (application.isWithoutInvitedGroupIdsLegacyCase) { - ( - groupService - .byIds(users.flatMap(_.groupIds)) - .filter(_.email.nonEmpty) - .filter(_.areaIds.contains(application.area)), - users.filter(user => application.invitedUsers.contains(user.id)).flatMap(_.groupIds).toSet - ) - } else { - val allGroupIds = application.invitedGroups.union(answer.invitedGroupIds.toSet) - ( - groupService - .byIds(allGroupIds.toList) - .filter(_.email.nonEmpty), - application.invitedGroups - ) - } + val (allGroups, alreadyPresentGroupIds): (List[UserGroup], Set[UUID]) = { + val allGroupIds = application.invitedGroups.union(answer.invitedGroupIds.toSet) + ( + groupService + .byIds(allGroupIds.toList) + .filter(_.email.nonEmpty), + application.invitedGroups + ) + } // Send emails to users users diff --git a/app/services/UserService.scala b/app/services/UserService.scala index ec50a25b4..cf1fc88e2 100644 --- a/app/services/UserService.scala +++ b/app/services/UserService.scala @@ -115,19 +115,6 @@ class UserService @Inject() ( .as(simpleUser.*) }.map(_.toUser) - // Note: this function is used in the stats, - // pseudonymization is possible (removing name, etc.) - def byGroupIdsAnonymous(ids: List[UUID]): Future[List[User]] = - Future { - db.withConnection { implicit connection => - SQL(s"""SELECT $fieldsInSelect, '' as name, '' as email, '' as qualite - FROM "user" - WHERE ARRAY[{ids}]::uuid[] && group_ids""") - .on("ids" -> ids.distinct) - .as(simpleUser.*) - }.map(_.toUser) - } - def byId(id: UUID, includeDisabled: Boolean = false): Option[User] = { val results = byIds(List(id), includeDisabled) assert(results.length <= 1) diff --git a/app/views/internalStats.scala b/app/views/internalStats.scala index cc11c0dd8..643b80dd4 100644 --- a/app/views/internalStats.scala +++ b/app/views/internalStats.scala @@ -1,6 +1,7 @@ package views import cats.syntax.all._ +import helper.Time import java.time.LocalDate import java.util.UUID import models.Organisation @@ -68,6 +69,19 @@ object internalStats { config.statisticsPercentOfRelevantApplicationsUrl.map(iframe6Cols), config.statisticsNumberOfApplicationsByUsefulnessUrl.map(iframe12Cols), config.statisticsTimeToProcessApplicationsUrl.map(iframe12Cols), + (filters.endDate + .isAfter(LocalDate.now().minusDays(2))) + .some + .filter(identity) + .map(_ => + p( + "Attention, certaines demandes du ", + Time.formatPatternFr(LocalDate.now().minusDays(1), "dd/MM/YY"), + " et du ", + Time.formatPatternFr(LocalDate.now(), "dd/MM/YY"), + " peuvent ne pas être comptabilisées." + ) + ) ) } diff --git a/app/views/stats/StatsData.scala b/app/views/stats/StatsData.scala deleted file mode 100644 index 151f645f7..000000000 --- a/app/views/stats/StatsData.scala +++ /dev/null @@ -1,139 +0,0 @@ -package views.stats - -import cats.kernel.Eq -import cats.syntax.all._ -import java.time.YearMonth -import helper.Time -import models.{Application, Area, User} - -object StatsData { - - case class Label(label: String) extends AnyVal - - object Label { - implicit val Eq: Eq[Label] = (x: Label, y: Label) => x.label === y.label - } - - case class TimeSeries(points: List[(Label, Int)]) - - case class ConditionalTimeSeries(series: List[(Label, TimeSeries)], timeAxis: List[Label]) { - - lazy val conditions: List[Label] = series.map(_._1) - - /** For HTML tables */ - lazy val transpose: List[(String, List[(String, Int)])] = - timeAxis.map(timePoint => - ( - timePoint.label, - series.map { case (condition, singleTimeSeries) => - ( - condition.label, - singleTimeSeries.points - .find(t => t._1 === timePoint) - .map(_._2) - // not pretty, maybe figure out how to have some Option / NaN - .getOrElse[Int](0) - ) - } - ) - ) - - } - - case class AreaAggregates(area: Area, aggregates: ApplicationAggregates) - - case class ApplicationAggregates( - applications: List[Application], - months: List[YearMonth], - private val usersRelatedToApplications: List[User] - ) { - def count: Int = applications.size - lazy val countLast30Days: Int = applications.count(_.ageInDays <= 30) - lazy val countClosedLast30Days: Int = applications.count(a => a.ageInDays <= 30 && a.closed) - lazy val countRelevant: Int = applications.count(!_.irrelevant) - lazy val countIrrelevant: Int = applications.count(_.irrelevant) - - lazy val countIrrelevantLast30Days: Int = - applications.count(a => a.ageInDays <= 30 && a.irrelevant) - - lazy val applicationsByStatus: Map[String, List[Application]] = - applications.groupBy(_.status.show) - - lazy val applicationsGroupedByMonth: List[(String, List[Application])] = { - val grouped = applications.groupBy(application => YearMonth.from(application.creationDate)) - months.map { month => - Time.formatMonthYearAllLetters(month) -> grouped.getOrElse(month, List.empty[Application]) - } - } - - lazy val closedApplicationsGroupedByMonth: List[(String, List[Application])] = { - val grouped = - applications.groupBy(application => application.estimatedClosedDate.map(YearMonth.from)) - months.map { month => - Time.formatMonthYearAllLetters(month) -> grouped.getOrElse( - month.some, - List.empty[Application] - ) - } - } - - private def applicationsCountByMandat(mandatType: Application.MandatType): List[Int] = - applicationsGroupedByMonth.map( - _._2.count(application => application.mandatType === mandatType.some) - ) - - lazy val applicationsCountByMandatPaper: List[Int] = - applicationsGroupedByMonth.map( - _._2 - .count(application => - application.mandatType.isEmpty || - application.mandatType === Application.MandatType.Paper.some - ) - ) - - lazy val applicationsCountByMandatSms: List[Int] = - applicationsCountByMandat(Application.MandatType.Sms) - - lazy val applicationsCountByMandatPhone: List[Int] = - applicationsCountByMandat(Application.MandatType.Phone) - - // Conditional Series - - lazy val creatorQualitees: List[String] = - applicationsGroupedByMonth - .flatMap(_._2) - .flatMap(_.creatorUserQualite(usersRelatedToApplications)) - .distinct - - lazy val applicationsCountGroupedByCreatorQualiteThenByMonth: ConditionalTimeSeries = - ConditionalTimeSeries( - series = creatorQualitees.map(qualite => - ( - Label(qualite), - TimeSeries( - applicationsGroupedByMonth - .map { case (month, applications) => - ( - Label(month), - applications.count( - _.creatorUserQualite(usersRelatedToApplications).contains(qualite) - ) - ) - } - ) - ) - ), - timeAxis = months.map(month => Label(Time.formatMonthYearAllLetters(month))) - ) - - } - -} - -/** This class (and its subclasses) should have all "computation" methods, such that the template do - * not have calculations in it. - */ -case class StatsData( - all: StatsData.ApplicationAggregates, - aggregatesByArea: List[StatsData.AreaAggregates] -) diff --git a/app/views/stats/applicationsByAreaTable.scala.html b/app/views/stats/applicationsByAreaTable.scala.html deleted file mode 100644 index 0bafbd105..000000000 --- a/app/views/stats/applicationsByAreaTable.scala.html +++ /dev/null @@ -1,30 +0,0 @@ -@(data: views.stats.StatsData) - - - - - - - - - - - - - - - - @for(areaData <- data.aggregatesByArea) { - - - - - - - - - - - } - -
TerritoireTotalTotal archivéesTotal non pertinentes Oui Je ne sais pas Non?
@areaData.area.name@areaData.aggregates.applications.length@areaData.aggregates.applications.count(_.closed)@areaData.aggregates.applications.count(_.irrelevant)@areaData.aggregates.applications.count(_.usefulness.contains("Oui")) @areaData.aggregates.applications.count(_.usefulness.contains("Je ne sais pas")) @areaData.aggregates.applications.count(_.usefulness.contains("Non")) @areaData.aggregates.applications.count(_.usefulness.isEmpty) ?
diff --git a/app/views/stats/applicationsRelevantPieChart.scala.html b/app/views/stats/applicationsRelevantPieChart.scala.html deleted file mode 100644 index ed9506274..000000000 --- a/app/views/stats/applicationsRelevantPieChart.scala.html +++ /dev/null @@ -1,30 +0,0 @@ -@(data: views.stats.StatsData.ApplicationAggregates) - - - diff --git a/app/views/stats/applicationsResolutionTimeBarChart.scala.html b/app/views/stats/applicationsResolutionTimeBarChart.scala.html deleted file mode 100644 index 819950d0b..000000000 --- a/app/views/stats/applicationsResolutionTimeBarChart.scala.html +++ /dev/null @@ -1,69 +0,0 @@ -@import _root_.helper.Math - -@(data: views.stats.StatsData.ApplicationAggregates) - - - diff --git a/app/views/stats/applicationsStatusPieChart.scala.html b/app/views/stats/applicationsStatusPieChart.scala.html deleted file mode 100644 index 6b0a78af8..000000000 --- a/app/views/stats/applicationsStatusPieChart.scala.html +++ /dev/null @@ -1,32 +0,0 @@ -@(data: views.stats.StatsData.ApplicationAggregates) - - - diff --git a/app/views/stats/applicationsUsefulnessFeedbackBarChart.scala.html b/app/views/stats/applicationsUsefulnessFeedbackBarChart.scala.html deleted file mode 100644 index 129ccf43b..000000000 --- a/app/views/stats/applicationsUsefulnessFeedbackBarChart.scala.html +++ /dev/null @@ -1,48 +0,0 @@ -@(data: views.stats.StatsData.ApplicationAggregates) - - - diff --git a/app/views/stats/charts.scala.html b/app/views/stats/charts.scala.html deleted file mode 100644 index 99cda097b..000000000 --- a/app/views/stats/charts.scala.html +++ /dev/null @@ -1,96 +0,0 @@ -@import java.util.UUID - -@import _root_.helper.Time -@(isAdmin: Boolean)(data: views.stats.StatsData, users: List[User]) - - -
-
- Dernière mise à jour : @{Time.formatPatternFr(Time.nowParis(), "dd MMM YYYY - HH:mm:ss")} -
- - -
-
Total de demandes
-

@data.all.count

-
@data.all.countLast30Days sur les 30 derniers jours
-
-
-
Total d’utilisateurs
-

@users.length

-
@users.count(_.ageInDays <= 30) sur les 30 derniers jours
-
-
-
Répartition des demandes
- @applicationsStatusPieChart(data.all) -
@data.all.countClosedLast30Days archivées sur les 30 derniers jours
-
-
-
Pertinence des demandes
- @applicationsRelevantPieChart(data.all) -
@data.all.countIrrelevantLast30Days non pertinentes sur les 30 derniers jours
-
- -
-
Est-ce que la réponse vous semble utile pour l’usager ?
- @applicationsUsefulnessFeedbackBarChart(data.all) -
- -
-
Temps de résolution
- @applicationsResolutionTimeBarChart(data.all) -
- -
-
Demandes déposées et archivées
-
- @newAndClosedApplicationsBarChart(data.all, "all") -
-
- -@if(isAdmin) { -
-
Demandes déposées par type de mandat
-
- @mandatTypeBarChart(data.all, "mandat-type") -
-
-} - -@if(isAdmin) { - -
-
Demandes par territoire
-
- @applicationsByAreaTable(data) -
-
- - -
- @for(areaData <- data.aggregatesByArea) { -

@areaData.area.name

-
- @stats.newAndClosedApplicationsBarChart(areaData.aggregates, areaData.area.id.toString) -
- Détail des demandes faites par des structures aidantes -
- @conditionalTimeSeriesPlot( - areaData.aggregates.applicationsCountGroupedByCreatorQualiteThenByMonth, - s"toto-helper-admin-chart-${areaData.area.id}" - ) -
- @conditionalTimeSeriesTransposedTable( - areaData.aggregates.applicationsCountGroupedByCreatorQualiteThenByMonth, - "Mois", - "Total" - ) -
-
- Détail des demandes archivées - @closedApplicationsTable(areaData.aggregates) -
-
- } -
-} diff --git a/app/views/stats/closedApplicationsTable.scala.html b/app/views/stats/closedApplicationsTable.scala.html deleted file mode 100644 index 34fb0097b..000000000 --- a/app/views/stats/closedApplicationsTable.scala.html +++ /dev/null @@ -1,28 +0,0 @@ -@(data: views.stats.StatsData.ApplicationAggregates) - - - - - - - - - - - - - - - @for((month, applications) <- data.closedApplicationsGroupedByMonth.reverse) { - - - - - - - - - - } - -
SemaineArchivéesNon Pertinentes Oui Je ne sais pas Non?
@month@applications.length@applications.count(_.irrelevant)@applications.count(_.usefulness.contains("Oui")) @applications.count(_.usefulness.contains("Je ne sais pas")) @applications.count(_.usefulness.contains("Non")) @applications.count(_.usefulness.isEmpty) ?
diff --git a/app/views/stats/conditionalTimeSeriesPlot.scala.html b/app/views/stats/conditionalTimeSeriesPlot.scala.html deleted file mode 100644 index c891b6cf0..000000000 --- a/app/views/stats/conditionalTimeSeriesPlot.scala.html +++ /dev/null @@ -1,37 +0,0 @@ -@(allSeries: views.stats.StatsData.ConditionalTimeSeries, chartId: String) - - - diff --git a/app/views/stats/conditionalTimeSeriesTransposedTable.scala.html b/app/views/stats/conditionalTimeSeriesTransposedTable.scala.html deleted file mode 100644 index 3f08de7a7..000000000 --- a/app/views/stats/conditionalTimeSeriesTransposedTable.scala.html +++ /dev/null @@ -1,24 +0,0 @@ -@(allSeries: views.stats.StatsData.ConditionalTimeSeries, timeColumnHeader: String, sumColumnHeader: String) - - - - - - - @for(condition <- allSeries.conditions) { - - } - - - - @for((timePoint, valuesByCondition) <- allSeries.transpose.reverse) { - - - - @for((_, count) <- valuesByCondition) { - - } - - } - -
@timeColumnHeader@sumColumnHeader@condition.label
@timePoint@valuesByCondition.map(_._2).sum@count
diff --git a/app/views/stats/mandatTypeBarChart.scala.html b/app/views/stats/mandatTypeBarChart.scala.html deleted file mode 100644 index 16b2a549e..000000000 --- a/app/views/stats/mandatTypeBarChart.scala.html +++ /dev/null @@ -1,51 +0,0 @@ -@(data: views.stats.StatsData.ApplicationAggregates, chartId: String) - - - diff --git a/app/views/stats/newAndClosedApplicationsBarChart.scala.html b/app/views/stats/newAndClosedApplicationsBarChart.scala.html deleted file mode 100644 index e53a42aca..000000000 --- a/app/views/stats/newAndClosedApplicationsBarChart.scala.html +++ /dev/null @@ -1,69 +0,0 @@ -@(data: views.stats.StatsData.ApplicationAggregates, chartId: String) - - - diff --git a/app/views/stats/page.scala.html b/app/views/stats/page.scala.html index 4f753baed..d02349fb8 100644 --- a/app/views/stats/page.scala.html +++ b/app/views/stats/page.scala.html @@ -4,7 +4,6 @@ @(currentUser: User, currentUserRights: Authorization.UserRights)(formUrl: Call, result: Html, groupsThatCanBeFilteredBy: List[UserGroup], selectedAreaIds: List[UUID], selectedOrganisationIds: List[Organisation.Id], selectedGroupIds: List[UUID], creationMinDate: LocalDate, creationMaxDate: LocalDate)(implicit webJarsUtil: org.webjars.play.WebJarsUtil, flash: Flash, request: RequestHeader, mainInfos: MainInfos) @main(currentUser, currentUserRights)(s"Stats") { - @webJarsUtil.locate("Chart.bundle.min.js").script() }{
@helper.form(formUrl, "method" -> "post") { diff --git a/build.sbt b/build.sbt index a2936020a..65d9966ce 100644 --- a/build.sbt +++ b/build.sbt @@ -81,7 +81,6 @@ libraryDependencies ++= Seq( ws, jdbc, evolutions, - ehcache ) pipelineStages := Seq(digest, gzip) @@ -109,7 +108,6 @@ libraryDependencies ++= Seq( "org.webjars.bower" % "material-design-lite" % "1.3.0", "org.webjars" % "material-design-icons" % "4.0.0", "org.webjars.npm" % "roboto-fontface" % "0.10.0", - "org.webjars" % "chartjs" % "2.9.4", "org.webjars" % "font-awesome" % "6.4.2", ) diff --git a/conf/routes b/conf/routes index 62603c464..ab812d381 100644 --- a/conf/routes +++ b/conf/routes @@ -36,13 +36,9 @@ POST /mandats/sms/webhook # Stats -+ nocsp GET /stats controllers.ApplicationController.stats -+ nocsp GET /as/:userId/stats controllers.ApplicationController.statsAs(userId: java.util.UUID) -+ nocsp POST /stats controllers.ApplicationController.stats -+ nocsp POST /as/:userId/stats controllers.ApplicationController.statsAs(userId: java.util.UUID)