diff --git a/app/Enums/CustomStatusCodes.php b/app/Enums/CustomStatusCodes.php index 7b0aac282..982ebbd95 100644 --- a/app/Enums/CustomStatusCodes.php +++ b/app/Enums/CustomStatusCodes.php @@ -18,6 +18,7 @@ enum CustomStatusCodes: int case ROOM_TYPE_INVALID = 467; case FEATURE_DISABLED = 468; case MEETING_ATTENDANCE_NOT_ENDED = 469; + case MEETING_ATTENDANCE_DISABLED = 470; case EMAIL_CHANGE_THROTTLE = 471; case JOIN_FAILED = 472; case ROOM_ALREADY_RUNNING = 474; diff --git a/app/Http/Controllers/api/v1/MeetingController.php b/app/Http/Controllers/api/v1/MeetingController.php index 80652e577..00599fd69 100644 --- a/app/Http/Controllers/api/v1/MeetingController.php +++ b/app/Http/Controllers/api/v1/MeetingController.php @@ -148,7 +148,7 @@ public function attendance(Meeting $meeting) // check if attendance recording is enabled for this meeting if (! $meeting->record_attendance) { Log::info('Failed to show attendace for meeting {meeting} of room {room}; attendance is disabled', ['room' => $meeting->room->getLogLabel(), 'meeting' => $meeting->id]); - abort(CustomStatusCodes::FEATURE_DISABLED->value, __('app.errors.meeting_attendance_disabled')); + abort(CustomStatusCodes::MEETING_ATTENDANCE_DISABLED->value, __('app.errors.meeting_attendance_disabled')); } // check if meeting is ended diff --git a/lang/en/app.php b/lang/en/app.php index ca1fe5310..bff9ce3ee 100644 --- a/lang/en/app.php +++ b/lang/en/app.php @@ -35,8 +35,8 @@ 'errors' => [ 'attendance_agreement_missing' => 'Consent to record attendance is required.', 'join_failed' => 'Joining the room has failed because a connection error has occurred.', - 'meeting_attendance_disabled' => 'The logging of attendance is unavailable.', - 'meeting_attendance_not_ended' => 'The logging of attendance is not yet completed for this room.', + 'meeting_attendance_disabled' => 'Attendance logging was not active at this meeting.', + 'meeting_attendance_not_ended' => 'The attendance logs are not yet available for this meeting as it has not yet ended.', 'meeting_statistics_disabled' => 'The usage data is unavailable.', 'membership_disabled' => 'Membership failed! Membership for this room is currently not available.', 'no_room_access' => 'You does not have the necessary permissions, to edit this room.', diff --git a/package-lock.json b/package-lock.json index f9b04ad83..18697f47c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -58,6 +58,7 @@ "@eslint/json": "^0.7.0", "@intlify/eslint-plugin-vue-i18n": "^3.0.0", "cypress": "^13.16.0", + "cypress-set-device-pixel-ratio": "^1.0.7", "eslint": "^9.15.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-cypress": "^4.1.0", @@ -6065,6 +6066,25 @@ "node": "^16.0.0 || ^18.0.0 || >=20.0.0" } }, + "node_modules/cypress-set-device-pixel-ratio": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cypress-set-device-pixel-ratio/-/cypress-set-device-pixel-ratio-1.0.7.tgz", + "integrity": "sha512-J5Mo+ZyLsok29LwWNj0qydqecZJxdETQNJuN83geu+LCk1Z69K+rItJEnoLRwJB9Wv/mVxCKuP/JklOMdh2Feg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "cypress": ">=12.0.0", + "cypress-wait-until": ">=1.7.2" + } + }, + "node_modules/cypress-wait-until": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/cypress-wait-until/-/cypress-wait-until-3.0.2.tgz", + "integrity": "sha512-iemies796dD5CgjG5kV0MnpEmKSH+s7O83ZoJLVzuVbZmm4lheMsZqAVT73hlMx4QlkwhxbyUzhOBUOZwoOe0w==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/cypress/node_modules/proxy-from-env": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz", diff --git a/package.json b/package.json index 39af92601..520d3eaa5 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "@eslint/json": "^0.7.0", "@intlify/eslint-plugin-vue-i18n": "^3.0.0", "cypress": "^13.16.0", + "cypress-set-device-pixel-ratio": "^1.0.7", "eslint": "^9.15.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-cypress": "^4.1.0", diff --git a/resources/js/components/RoomTabHistory.vue b/resources/js/components/RoomTabHistory.vue index 4d6ae17cb..f44356ba9 100644 --- a/resources/js/components/RoomTabHistory.vue +++ b/resources/js/components/RoomTabHistory.vue @@ -144,6 +144,8 @@ :start="item.start" :end="item.end" :room-name="props.room.name" + @feature-disabled="loadData()" + @not-found="loadData()" /> diff --git a/resources/js/components/RoomTabHistoryAttendanceButton.vue b/resources/js/components/RoomTabHistoryAttendanceButton.vue index 69d9de9e2..312fc6ba1 100644 --- a/resources/js/components/RoomTabHistoryAttendanceButton.vue +++ b/resources/js/components/RoomTabHistoryAttendanceButton.vue @@ -57,7 +57,7 @@ scroll-height="400px" :value="attendance" data-key="id" - :loading="isLoadingAction" + :loading="isLoadingAction || loadingError" row-hover :global-filter-fields="['name']" :pt="{ @@ -101,14 +101,19 @@ /> + + + - {{ - $t("meetings.attendance.no_data") - }} - {{ - $t("meetings.attendance.no_data_filtered") - }} + + {{ + $t("meetings.attendance.no_data") + }} + {{ + $t("meetings.attendance.no_data_filtered") + }} + @@ -160,6 +165,7 @@ import { computed, ref } from "vue"; import { useApi } from "../composables/useApi.js"; import "chartjs-adapter-date-fns"; import { useSettingsStore } from "../stores/settings.js"; +import env from "../env.js"; const props = defineProps({ roomId: { @@ -188,8 +194,11 @@ const props = defineProps({ }, }); +const emit = defineEmits(["notFound", "notEnded", "attendanceDisabled"]); + const modalVisible = ref(false); const isLoadingAction = ref(false); +const loadingError = ref(false); const attendance = ref([]); const filters = ref({ global: { value: null, matchMode: "contains" }, @@ -199,12 +208,14 @@ const api = useApi(); const settingsStore = useSettingsStore(); function showModal() { + attendance.value = []; modalVisible.value = true; loadData(); } function loadData() { isLoadingAction.value = true; + loadingError.value = false; api .call("meetings/" + props.meetingId + "/attendance") @@ -212,6 +223,34 @@ function loadData() { attendance.value = response.data.data; }) .catch((error) => { + loadingError.value = true; + + if (error.response) { + // meeting is still running, therefore attendance is not yet available + if (error.response.status === env.HTTP_MEETING_ATTENDANCE_NOT_ENDED) { + emit("notEnded"); + modalVisible.value = false; + } + + // attendance was not enabled for this meeting + if (error.response.status === env.HTTP_MEETING_ATTENDANCE_DISABLED) { + emit("attendanceDisabled"); + modalVisible.value = false; + } + + // meeting not found + if (error.response.status === env.HTTP_MEETING_ATTENDANCE_NOT_ENDED) { + emit("notEnded"); + modalVisible.value = false; + } + + // meeting not found + if (error.response.status === env.HTTP_NOT_FOUND) { + emit("notFound"); + modalVisible.value = false; + } + } + // error during stats loading api.error(error, { redirectOnUnauthenticated: false }); }) diff --git a/resources/js/components/RoomTabHistoryStatisticButton.vue b/resources/js/components/RoomTabHistoryStatisticButton.vue index c833e4197..a96cc2936 100644 --- a/resources/js/components/RoomTabHistoryStatisticButton.vue +++ b/resources/js/components/RoomTabHistoryStatisticButton.vue @@ -51,12 +51,18 @@ + + + { + loadingError.value = true; + + if (error.response) { + // feature disabled + if (error.response.status === env.HTTP_FEATURE_DISABLED) { + emit("featureDisabled"); + modalVisible.value = false; + } + + // meeting not found + if (error.response.status === env.HTTP_NOT_FOUND) { + emit("notFound"); + modalVisible.value = false; + } + } + // error during stats loading api.error(error, { redirectOnUnauthenticated: false }); }) diff --git a/resources/js/env.js b/resources/js/env.js index c2425302f..cd67f4d5f 100644 --- a/resources/js/env.js +++ b/resources/js/env.js @@ -12,6 +12,9 @@ export default { HTTP_ROOM_LIMIT_EXCEEDED: 463, HTTP_ROLE_DELETE_LINKED_USERS: 464, HTTP_ROLE_UPDATE_PERMISSION_LOST: 465, + HTTP_FEATURE_DISABLED: 468, + HTTP_MEETING_ATTENDANCE_NOT_ENDED: 469, + HTTP_MEETING_ATTENDANCE_DISABLED: 470, HTTP_EMAIL_CHANGE_THROTTLE: 471, HTTP_JOIN_FAILED: 472, HTTP_ROOM_ALREADY_RUNNING: 474, diff --git a/tests/Frontend/e2e/RoomsViewHistoryMeetingActions.cy.js b/tests/Frontend/e2e/RoomsViewHistoryMeetingActions.cy.js index 3239f6122..6f4ade571 100644 --- a/tests/Frontend/e2e/RoomsViewHistoryMeetingActions.cy.js +++ b/tests/Frontend/e2e/RoomsViewHistoryMeetingActions.cy.js @@ -7,6 +7,8 @@ describe("Rooms view history meeting actions", function () { }); it("show stats", function () { + cy.setDevicePixelRatio(2); + cy.visit("/rooms/abc-def-123#tab=history"); cy.wait("@roomRequest"); @@ -58,7 +60,7 @@ describe("Rooms view history meeting actions", function () { cy.get('[data-test="chart"] > canvas').should( "have.attr", "width", - 1163, + 2326, ); cy.get('[data-test="chart"] > canvas').then(($canvas) => { diff --git a/tests/Frontend/fixtures/files/statsGraph.png b/tests/Frontend/fixtures/files/statsGraph.png index aac986226..7329be71a 100644 Binary files a/tests/Frontend/fixtures/files/statsGraph.png and b/tests/Frontend/fixtures/files/statsGraph.png differ diff --git a/tests/Frontend/support/e2e.js b/tests/Frontend/support/e2e.js index 08c3b79c5..30bf12736 100644 --- a/tests/Frontend/support/e2e.js +++ b/tests/Frontend/support/e2e.js @@ -17,3 +17,4 @@ import "./commands/generalCommands.js"; import "./commands/roomCommands.js"; import "./commands/interceptCommands.js"; import "@cypress/code-coverage/support"; +import "cypress-set-device-pixel-ratio";