From 0c6cfcfd9149837398185e3ce51afd5046c9f8c9 Mon Sep 17 00:00:00 2001 From: Aurora Virmasalo <93916893+VirmasaloA@users.noreply.github.com> Date: Wed, 11 Dec 2024 09:26:28 +0200 Subject: [PATCH 1/4] CSCEXAM-1390 Fix question bank table accessibility (#1178) * Fix question bank table accessibility * Remove excess depricated @if --- package-lock.json | 88 ++++++++--------- package.json | 2 +- .../app/question/library/library.component.ts | 25 +++-- .../results/library-results.component.html | 19 ++-- .../search/library-search.component.html | 94 +++++++++++-------- 5 files changed, 126 insertions(+), 102 deletions(-) diff --git a/package-lock.json b/package-lock.json index 07053981d..b9844a1d8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -61,7 +61,7 @@ "eslint-plugin-deprecation": "^3.0.0", "eslint-plugin-no-relative-import-paths": "^1.5.5", "karma": "^6.4.4", - "lefthook": "^1.6.1", + "lefthook": "^1.8.4", "prettier": "^3.3.3", "prettier-plugin-java": "^2.6.4", "prettier-plugin-organize-imports": "^4.0.0", @@ -9332,31 +9332,31 @@ } }, "node_modules/lefthook": { - "version": "1.7.16", - "resolved": "https://registry.npmjs.org/lefthook/-/lefthook-1.7.16.tgz", - "integrity": "sha512-ApzyQnsa3t4YBdbI+nvGVXOuNTNP02Fh+haoDj4f/Sjjlo3S+19OmPdFtJAKcz1ziOL52iq1q3rTLIOtOXNDZg==", + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/lefthook/-/lefthook-1.8.4.tgz", + "integrity": "sha512-XNyMaTWNRuADOaocYiHidgNkNDz8SCekpdNJ7lqceFcBT2zjumnb28/o7IMaNROpLBZdQkLkJXSeaQWGqn3kog==", "dev": true, "hasInstallScript": true, "bin": { "lefthook": "bin/index.js" }, "optionalDependencies": { - "lefthook-darwin-arm64": "1.7.16", - "lefthook-darwin-x64": "1.7.16", - "lefthook-freebsd-arm64": "1.7.16", - "lefthook-freebsd-x64": "1.7.16", - "lefthook-linux-arm64": "1.7.16", - "lefthook-linux-x64": "1.7.16", - "lefthook-openbsd-arm64": "1.7.16", - "lefthook-openbsd-x64": "1.7.16", - "lefthook-windows-arm64": "1.7.16", - "lefthook-windows-x64": "1.7.16" + "lefthook-darwin-arm64": "1.8.4", + "lefthook-darwin-x64": "1.8.4", + "lefthook-freebsd-arm64": "1.8.4", + "lefthook-freebsd-x64": "1.8.4", + "lefthook-linux-arm64": "1.8.4", + "lefthook-linux-x64": "1.8.4", + "lefthook-openbsd-arm64": "1.8.4", + "lefthook-openbsd-x64": "1.8.4", + "lefthook-windows-arm64": "1.8.4", + "lefthook-windows-x64": "1.8.4" } }, "node_modules/lefthook-darwin-arm64": { - "version": "1.7.16", - "resolved": "https://registry.npmjs.org/lefthook-darwin-arm64/-/lefthook-darwin-arm64-1.7.16.tgz", - "integrity": "sha512-T7VRvfWpCE4IaT2jQnZIT4VVJ+Igj3/DE9Mzlt/Zk8yVn7E9hm+DvvAvj3mOlQtFq1tn5IlJZ6sgogjxpjtgMg==", + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/lefthook-darwin-arm64/-/lefthook-darwin-arm64-1.8.4.tgz", + "integrity": "sha512-OS5MsU0gvd8LYSpuQCHtmDUqwNrJ/LjCO0LGC1wNepY4OkuVl9DfX+rQ506CVUQYZiGVcwy2/qPOOBjNzA5+wQ==", "cpu": [ "arm64" ], @@ -9367,9 +9367,9 @@ ] }, "node_modules/lefthook-darwin-x64": { - "version": "1.7.16", - "resolved": "https://registry.npmjs.org/lefthook-darwin-x64/-/lefthook-darwin-x64-1.7.16.tgz", - "integrity": "sha512-SmwulM8QOcqvOYZMrTtVdvIqv6hKk2rBT3JzEyyOJ2kLiBDrfJs/r5dlkLuM06jOqxF5M9WvArb3dUHBkkmewQ==", + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/lefthook-darwin-x64/-/lefthook-darwin-x64-1.8.4.tgz", + "integrity": "sha512-QLRsqK9aTMRcVW8qz4pzI2OWnGCEcaEPJlIiFjwstYsS+wfkooxOS0UkfVMjy+QoGgEcki+cxF/FoY7lE7DDtw==", "cpu": [ "x64" ], @@ -9380,9 +9380,9 @@ ] }, "node_modules/lefthook-freebsd-arm64": { - "version": "1.7.16", - "resolved": "https://registry.npmjs.org/lefthook-freebsd-arm64/-/lefthook-freebsd-arm64-1.7.16.tgz", - "integrity": "sha512-YUPpiGVB1i9/OrDwm3tKSYmiip01eqa1gDY79MHj/lj7qLVkqzQsohx+xs/XroZZ/U6CdmLpOHQPXIIZ5FAL6Q==", + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/lefthook-freebsd-arm64/-/lefthook-freebsd-arm64-1.8.4.tgz", + "integrity": "sha512-chnQ1m/Cmn9c0sLdk5HL2SToE5LBJv5uQMdH1IGRRcw+nEqWqrMnDXvM75caiJAyjmUGvPH3czKTJDzTFV1E+A==", "cpu": [ "arm64" ], @@ -9393,9 +9393,9 @@ ] }, "node_modules/lefthook-freebsd-x64": { - "version": "1.7.16", - "resolved": "https://registry.npmjs.org/lefthook-freebsd-x64/-/lefthook-freebsd-x64-1.7.16.tgz", - "integrity": "sha512-4KflUJXY7sO7Ikzk9J+6GjzQZaGos67qehi0I343MhpRtCXGOLQWbVT4IMUFZjF+pybu5HfrX6VNcGDF6mTubQ==", + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/lefthook-freebsd-x64/-/lefthook-freebsd-x64-1.8.4.tgz", + "integrity": "sha512-KQi+WBUdnGLnK0rHOR58kbMH5TDVN1ZjZLu66Pv9FCG7Y7shR1qtaTXu+wmxdRhMvaLeQIXRsUEPjNRC66yMmA==", "cpu": [ "x64" ], @@ -9406,9 +9406,9 @@ ] }, "node_modules/lefthook-linux-arm64": { - "version": "1.7.16", - "resolved": "https://registry.npmjs.org/lefthook-linux-arm64/-/lefthook-linux-arm64-1.7.16.tgz", - "integrity": "sha512-QUiqaBNM3dGHKXNVdcZnYtGb/Jz4qnVrW6L5DMHLJ4ns/w09z3lduYJo/1EZg9QW8+0HD6IYwA3laYRnH+WXtg==", + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/lefthook-linux-arm64/-/lefthook-linux-arm64-1.8.4.tgz", + "integrity": "sha512-CXNcqIskLwTwQARidGdFqmNxpvOU3jsWPK4KA7pq2+QmlWJ64w98ebMvNBoUmRUCXqzmUm7Udf/jpfz2fobewQ==", "cpu": [ "arm64" ], @@ -9419,9 +9419,9 @@ ] }, "node_modules/lefthook-linux-x64": { - "version": "1.7.16", - "resolved": "https://registry.npmjs.org/lefthook-linux-x64/-/lefthook-linux-x64-1.7.16.tgz", - "integrity": "sha512-jw18PlmZv+5EW0o/7A8r4Y/+Yc4pFh49xpECEts3m+LuWSeMyaubfOVaBuYEoBLC8ksSGDkZ2ocgXtlc81hvFQ==", + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/lefthook-linux-x64/-/lefthook-linux-x64-1.8.4.tgz", + "integrity": "sha512-pVNITkFBxUCEtamWSM/res2Gd48+m9YKbNyIBndAuZVC5pKV5aGKZy2DNq6PWUPYiUDPx+7hoAtCJg/tlAiqhw==", "cpu": [ "x64" ], @@ -9432,9 +9432,9 @@ ] }, "node_modules/lefthook-openbsd-arm64": { - "version": "1.7.16", - "resolved": "https://registry.npmjs.org/lefthook-openbsd-arm64/-/lefthook-openbsd-arm64-1.7.16.tgz", - "integrity": "sha512-OBuelWygkF37AC44Qj1v+XP6TT4xH8xtvJoa55VstpY0KcA++q1hLDyMjeAake4y6r2JDV3cPxgGjuVoexLorg==", + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/lefthook-openbsd-arm64/-/lefthook-openbsd-arm64-1.8.4.tgz", + "integrity": "sha512-l+i/Dg5X36kYzhpMGSPE3rMbWy1KSytbLB9lY1PmxYb6LRH6iQTYIoxvLabVUwSBPSq8HtIFa50+bvC5+scfVA==", "cpu": [ "arm64" ], @@ -9445,9 +9445,9 @@ ] }, "node_modules/lefthook-openbsd-x64": { - "version": "1.7.16", - "resolved": "https://registry.npmjs.org/lefthook-openbsd-x64/-/lefthook-openbsd-x64-1.7.16.tgz", - "integrity": "sha512-wvXlaWQVndvEJKCUMdC2DhBdeSzN+RaNfUEXfPgh+5rg5a3DACHXt1A+QLP7eNDz4+87mzKltED9QtlS1VNQdg==", + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/lefthook-openbsd-x64/-/lefthook-openbsd-x64-1.8.4.tgz", + "integrity": "sha512-CqhDDPPX8oHzMLgNi/Reba823DRzj+eMNWQ8axvSiIG+zmG1w20xZH5QSs/mD3tjrND90yfDd90mWMt181qPyA==", "cpu": [ "x64" ], @@ -9458,9 +9458,9 @@ ] }, "node_modules/lefthook-windows-arm64": { - "version": "1.7.16", - "resolved": "https://registry.npmjs.org/lefthook-windows-arm64/-/lefthook-windows-arm64-1.7.16.tgz", - "integrity": "sha512-6TpgtEDbWqQ6I0BQUGNNzAH8xnlMFOOvDUl0NA1eMo40PgTBL/avNgjP84HQ4hKfFuSzN7s9yrwAlkQdZ9KqUg==", + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/lefthook-windows-arm64/-/lefthook-windows-arm64-1.8.4.tgz", + "integrity": "sha512-dvpvorICmVjmw29Aiczg7DcaSzkd86bEBomiGq4UsAEk3+7ExLrlWJDLFsI6xLjMKmTxy+F7eXb2uDtuFC1N4g==", "cpu": [ "arm64" ], @@ -9471,9 +9471,9 @@ ] }, "node_modules/lefthook-windows-x64": { - "version": "1.7.16", - "resolved": "https://registry.npmjs.org/lefthook-windows-x64/-/lefthook-windows-x64-1.7.16.tgz", - "integrity": "sha512-EDUQff+mmW7ygWTzIrVxJG0L4mEPt5yOR8OzxqUmIcaqDEhIrtVYIbA/F63iRJmWDC2DBJiHoX4zX44KnZgMRQ==", + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/lefthook-windows-x64/-/lefthook-windows-x64-1.8.4.tgz", + "integrity": "sha512-e+y8Jt4/7PnoplhOuK48twjGVJEsU4T3J5kxD4mWfl6Cbit0YSn4bme9nW41eqCqTUqOm+ky29XlfnPHFX5ZNA==", "cpu": [ "x64" ], diff --git a/package.json b/package.json index ab973dfff..e36363d10 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,7 @@ "eslint-plugin-deprecation": "^3.0.0", "eslint-plugin-no-relative-import-paths": "^1.5.5", "karma": "^6.4.4", - "lefthook": "^1.6.1", + "lefthook": "^1.8.4", "prettier": "^3.3.3", "prettier-plugin-java": "^2.6.4", "prettier-plugin-organize-imports": "^4.0.0", diff --git a/ui/src/app/question/library/library.component.ts b/ui/src/app/question/library/library.component.ts index c519b332d..5addcdbb1 100644 --- a/ui/src/app/question/library/library.component.ts +++ b/ui/src/app/question/library/library.component.ts @@ -27,10 +27,14 @@ import { LibraryTagsDialogComponent } from './tags/library-tags-dialog.component
- -
@@ -45,6 +49,7 @@ import { LibraryTagsDialogComponent } from './tags/library-tags-dialog.component popoverTitle="{{ 'i18n_instructions' | translate }}" triggers="mouseenter:mouseleave" class="ms-2" + tabindex="0" > @@ -72,38 +77,38 @@ import { LibraryTagsDialogComponent } from './tags/library-tags-dialog.component
  • - {{ 'i18n_add_question_owner' | translate }} + {{ 'i18n_add_question_owner' | translate }}
  • - {{ 'i18n_tag_questions' | translate }} + {{ 'i18n_tag_questions' | translate }}
  • - {{ 'i18n_transfer_questions' | translate }} + {{ 'i18n_transfer_questions' | translate }}
  • - {{ 'i18n_export_questions' | translate }} + {{ 'i18n_export_questions' | translate }}
  • diff --git a/ui/src/app/question/library/results/library-results.component.html b/ui/src/app/question/library/results/library-results.component.html index 12845b785..c505a8aa8 100644 --- a/ui/src/app/question/library/results/library-results.component.html +++ b/ui/src/app/question/library/results/library-results.component.html @@ -125,12 +125,13 @@
    @if (!disableLinks) { - {{ question.id }} + {{ question.id }} + } @if (disableLinks) { #{{ question.id }} @@ -184,9 +185,9 @@ @if (question.attachment) { - + } {{ printTags(question) }} @@ -211,25 +212,25 @@ @if (question.allowedToRemove) { - + } - + {{ question.examSectionQuestions.length }} diff --git a/ui/src/app/question/library/search/library-search.component.html b/ui/src/app/question/library/search/library-search.component.html index 261d60138..e7c320b47 100644 --- a/ui/src/app/question/library/search/library-search.component.html +++ b/ui/src/app/question/library/search/library-search.component.html @@ -26,17 +26,18 @@ placeholder="{{ 'i18n_search_course_hint' | translate }}" aria-describedby="search-icon-1" /> -
    -
    - -
    -
    + - @for (course of filteredCourses; track course) { -
  • - {{ formatCourse(course) }} {{ course.name }} -
  • - } +
    @@ -64,16 +65,18 @@
    - @for (exam of filteredExams; track exam) { -
  • - {{ formatCourse(exam) }} {{ exam.name }} - @if (exam.period) { - ({{ exam.period }}) - } - -
  • - } + @@ -103,15 +106,23 @@ - @for (tag of filteredTags; track tag) { -
  • - @if (tag.usage && tag.usage > 0) { - - } -
  • - } + @@ -141,13 +152,19 @@ - @for (section of filteredSections; track section) { -
  • - -
  • - } + + @@ -176,17 +193,18 @@ -
  • -
  • +
    From 5629c1dbbbf78b212fc3369184bc9eb21f1f3c96 Mon Sep 17 00:00:00 2001 From: Matti Lupari Date: Thu, 12 Dec 2024 06:58:23 +0200 Subject: [PATCH 2/4] CSCEXAM-1398 Encode query parameters in course search --- app/impl/ExternalCourseHandlerImpl.scala | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/impl/ExternalCourseHandlerImpl.scala b/app/impl/ExternalCourseHandlerImpl.scala index f3262d4d1..ab35acd5b 100644 --- a/app/impl/ExternalCourseHandlerImpl.scala +++ b/app/impl/ExternalCourseHandlerImpl.scala @@ -7,7 +7,7 @@ package impl import io.ebean.DB import miscellaneous.config.ConfigReader import miscellaneous.scala.DbApiHelper -import models._ +import models.* import models.exam.{Course, Grade, GradeScale} import models.facility.Organisation import models.user.User @@ -18,15 +18,15 @@ import play.api.Logging import play.api.libs.json.{JsValue, Json} import play.api.libs.ws.{WSClient, WSResponse} import play.mvc.Http -import validators.ExternalCourseValidator.{CourseUnitInfo, GradeScale => ExtGradeScale} +import validators.ExternalCourseValidator.{CourseUnitInfo, GradeScale as ExtGradeScale} -import java.net._ +import java.net.* import java.nio.charset.StandardCharsets import java.text.SimpleDateFormat import javax.inject.Inject import scala.collection.immutable.TreeSet import scala.concurrent.{ExecutionContext, Future} -import scala.jdk.CollectionConverters._ +import scala.jdk.CollectionConverters.* class ExternalCourseHandlerImpl @Inject ( private val wsClient: WSClient, @@ -229,7 +229,7 @@ class ExternalCourseHandlerImpl @Inject ( val path = configReader.getString(configPath.getOrElse("")) if (!path.contains(COURSE_CODE_PLACEHOLDER)) throw new RuntimeException("exam.integration.courseUnitInfo.url is malformed") - val url = path.replace(COURSE_CODE_PLACEHOLDER, courseCode) + val url = path.replace(COURSE_CODE_PLACEHOLDER, URLEncoder.encode(courseCode, StandardCharsets.UTF_8)) URI.create(url).toURL private def parseUrl(user: User) = From 9bccb960136f8a07472e36632a46fe258767da7f Mon Sep 17 00:00:00 2001 From: Matti Lupari Date: Thu, 12 Dec 2024 06:19:56 +0200 Subject: [PATCH 3/4] CSCEXAM-459 External reservation statistics view --- app/controllers/admin/ReportController.java | 47 +++++-- .../impl/ExternalCalendarController.java | 4 + app/models/enrolment/Reservation.java | 26 +++- conf/evolutions/default/135.sql | 11 ++ conf/routes | 1 + .../categories/exam-statistics.component.ts | 12 +- .../iop-reservation-statistics.component.ts | 123 ++++++++++++++++++ .../statistics/statistics.component.html | 13 +- .../statistics/statistics.component.ts | 3 + .../statistics/statistics.service.ts | 3 + ui/src/app/reservation/reservation.model.ts | 2 + ui/src/assets/i18n/en.json | 6 +- ui/src/assets/i18n/fi.json | 6 +- ui/src/assets/i18n/sv.json | 24 ++-- 14 files changed, 241 insertions(+), 40 deletions(-) create mode 100644 conf/evolutions/default/135.sql create mode 100644 ui/src/app/administrative/statistics/categories/iop-reservation-statistics.component.ts diff --git a/app/controllers/admin/ReportController.java b/app/controllers/admin/ReportController.java index 5e4657f15..7c6adeda0 100644 --- a/app/controllers/admin/ReportController.java +++ b/app/controllers/admin/ReportController.java @@ -28,6 +28,7 @@ import javax.inject.Inject; import miscellaneous.excel.ExcelBuilder; import models.enrolment.ExamEnrolment; +import models.enrolment.Reservation; import models.exam.Course; import models.exam.Exam; import models.facility.ExamRoom; @@ -228,21 +229,46 @@ public Result getPublishedExams(Optional dept, Optional start, O @Restrict({ @Group("ADMIN") }) public Result getReservations(Optional dept, Optional start, Optional end) { ExpressionList query = DB.find(ExamEnrolment.class).where(); - query = - applyFilters( - query, - "exam.course", - "reservation.startAt", - dept.orElse(null), - start.orElse(null), - end.orElse(null) - ); + query = applyFilters( + query, + "exam.course", + "reservation.startAt", + dept.orElse(null), + start.orElse(null), + end.orElse(null) + ); Set enrolments = query.findSet(); long noShows = enrolments.stream().filter(ExamEnrolment::isNoShow).count(); long appearances = enrolments.size() - noShows; return ok(Json.newObject().put("noShows", noShows).put("appearances", appearances)); } + @Restrict({ @Group("ADMIN") }) + public Result getIopReservations(Optional dept, Optional start, Optional end) { + ExpressionList query = DB.find(Reservation.class) + .fetch("externalReservation") + .fetch("enrolment") + .where() + .or() + .isNotNull("externalRef") + .isNotNull("externalReservation.orgName") + .endOr(); + query = applyFilters( + query, + "examEnrolment.exam.course", + "startAt", + dept.orElse(null), + start.orElse(null), + end.orElse(null) + ); + Set reservations = query + .findSet() + .stream() + .filter(r -> r.getExternalOrgName() != null || (r.getExternalReservation() != null)) + .collect(Collectors.toSet()); + return ok(reservations); + } + @Restrict({ @Group("ADMIN") }) public Result getResponses(Optional dept, Optional start, Optional end) { ExpressionList query = DB.find(Exam.class).where().isNotNull("parent").isNotNull("course"); @@ -272,8 +298,7 @@ public Result getResponses(Optional dept, Optional start, Option ) ) .count(); - JsonNode node = Json - .newObject() + JsonNode node = Json.newObject() .put("aborted", aborted) .put("assessed", assessed) .put("unAssessed", unAssessed); diff --git a/app/controllers/iop/transfer/impl/ExternalCalendarController.java b/app/controllers/iop/transfer/impl/ExternalCalendarController.java index 596c9e440..420a98215 100644 --- a/app/controllers/iop/transfer/impl/ExternalCalendarController.java +++ b/app/controllers/iop/transfer/impl/ExternalCalendarController.java @@ -109,6 +109,8 @@ public Result provideReservation(Http.Request request) { DateTime start = ISODateTimeFormat.dateTimeParser().parseDateTime(node.get("start").asText()); DateTime end = ISODateTimeFormat.dateTimeParser().parseDateTime(node.get("end").asText()); String userEppn = node.get("user").asText(); + String orgRef = node.get("orgRef").asText(); + String orgName = node.get("orgName").asText(); if (start.isBeforeNow() || end.isBefore(start)) { return badRequest("invalid dates"); } @@ -133,6 +135,8 @@ public Result provideReservation(Http.Request request) { reservation.setStartAt(start); reservation.setMachine(machine.get()); reservation.setExternalUserRef(userEppn); + reservation.setExternalOrgRef(orgRef); + reservation.setExternalOrgName(orgName); reservation.save(); PathProperties pp = PathProperties.parse("(*, machine(*, room(*, mailAddress(*))))"); diff --git a/app/models/enrolment/Reservation.java b/app/models/enrolment/Reservation.java index 2991a41fa..7908c5783 100644 --- a/app/models/enrolment/Reservation.java +++ b/app/models/enrolment/Reservation.java @@ -55,6 +55,8 @@ public class Reservation extends GeneratedIdentityModel implements Comparable{{ 'i18n_most_popular_exams' | translate }} -
    - @if (exams.length > 0) { + @if (exams.length > 0) { +
    @@ -46,16 +46,14 @@ import { StatisticsService } from 'src/app/administrative/statistics/statistics. {{ 'i18n_total' | translate }}
    - @if (exams) { - {{ totalExams }} - } + {{ totalExams }}
    - } -
    +
    + } `, selector: 'xm-exam-statistics', standalone: true, diff --git a/ui/src/app/administrative/statistics/categories/iop-reservation-statistics.component.ts b/ui/src/app/administrative/statistics/categories/iop-reservation-statistics.component.ts new file mode 100644 index 000000000..9472c9ea9 --- /dev/null +++ b/ui/src/app/administrative/statistics/categories/iop-reservation-statistics.component.ts @@ -0,0 +1,123 @@ +// SPDX-FileCopyrightText: 2024 The members of the EXAM Consortium +// +// SPDX-License-Identifier: EUPL-1.2 + +import { KeyValuePipe } from '@angular/common'; +import type { OnInit } from '@angular/core'; +import { Component, Input } from '@angular/core'; +import { TranslateModule } from '@ngx-translate/core'; +import { groupBy } from 'ramda'; +import { QueryParams } from 'src/app/administrative/administrative.model'; +import { StatisticsService } from 'src/app/administrative/statistics/statistics.service'; +import { Reservation } from 'src/app/reservation/reservation.model'; + +@Component({ + template: ` +
    +
    + +
    +
    + @if (grouped) { +
    +
    + + + + + + + + + + + + @for (rg of grouped | keyvalue; track rg) { + + + + + + + + } + + + + + + + + + + +
    {{ 'i18n_faculty_name' | translate }}{{ 'i18n_outbound_reservations' | translate }} + {{ 'i18n_outbound_reservations' | translate }} - + {{ 'i18n_unused_reservation' | translate }} + {{ 'i18n_inbound_reservations' | translate }} + {{ 'i18n_inbound_reservations' | translate }} - + {{ 'i18n_unused_reservation' | translate }} +
    {{ rg.key }}{{ outgoingTo(rg.key) }}{{ outgoingNoShowsTo(rg.key) }}{{ incomingFrom(rg.key) }}{{ incomingNoShowsFrom(rg.key) }}
    + {{ 'i18n_total' | translate }} + + {{ totalOutgoing() }} + + {{ totalOutgoingNoShows() }} + + {{ totalIncoming() }} + + {{ totalIncomingNoShows() }} +
    +
    +
    + } + `, + selector: 'xm-iop-reservation-statistics', + standalone: true, + imports: [KeyValuePipe, TranslateModule], +}) +export class IopReservationStatisticsComponent implements OnInit { + @Input() queryParams: QueryParams = {}; + + reservations: Reservation[] = []; + grouped!: Record; + + constructor(private Statistics: StatisticsService) {} + + ngOnInit() { + this.listReservations(); + } + + listReservations = () => + this.Statistics.listIopReservations$(this.queryParams).subscribe((resp) => { + const byOrg = groupBy((r: Reservation) => r.externalOrgName || r.externalReservation?.orgName || ''); + this.grouped = byOrg(resp) as Record; + }); + + incomingFrom = (org: keyof typeof this.grouped): number => + this.grouped[org].filter((r) => r.externalOrgRef && !r.enrolment?.noShow).length; + incomingNoShowsFrom = (org: keyof typeof this.grouped): number => + this.grouped[org].filter((r) => r.externalOrgRef && r.enrolment?.noShow === true).length; + outgoingTo = (org: keyof typeof this.grouped): number => + this.grouped[org].filter((r) => r.externalReservation?.orgName && !r.enrolment?.noShow).length; + outgoingNoShowsTo = (org: keyof typeof this.grouped): number => + this.grouped[org].filter((r) => r.externalReservation?.orgName && r.enrolment?.noShow === true).length; + totalIncoming = () => + Object.keys(this.grouped) + .map((k) => this.incomingFrom(k)) + .reduce((a, b) => a + b); + totalOutgoing = () => + Object.keys(this.grouped) + .map((k) => this.outgoingTo(k)) + .reduce((a, b) => a + b); + totalIncomingNoShows = () => + Object.keys(this.grouped) + .map((k) => this.incomingNoShowsFrom(k)) + .reduce((a, b) => a + b); + totalOutgoingNoShows = () => + Object.keys(this.grouped) + .map((k) => this.outgoingNoShowsTo(k)) + .reduce((a, b) => a + b); +} diff --git a/ui/src/app/administrative/statistics/statistics.component.html b/ui/src/app/administrative/statistics/statistics.component.html index cec286037..af849731a 100644 --- a/ui/src/app/administrative/statistics/statistics.component.html +++ b/ui/src/app/administrative/statistics/statistics.component.html @@ -23,6 +23,9 @@
  • {{ 'i18n_reservations' | translate }}
  • +
  • + {{ 'i18n_iop_reservations' | translate }} +
  • @@ -60,12 +63,7 @@ > {{ 'i18n_choose' | translate }}  -
    +