From 2a21253c13596dac53040edc418cf873c2de5cc8 Mon Sep 17 00:00:00 2001 From: Florian Ludwig Date: Fri, 9 Oct 2026 17:21:07 +0200 Subject: [PATCH] feat(admin): hint when Nextcloud does not forward notifications by email On a fresh instance the Notifications app keeps "Send email reminders about unhandled notifications" at "Never", so reminders and appointment notifications only reach the bell and the mobile app. The reminder settings now say so, link to the Notifications settings and can be collapsed, since some instances want it that way. The choice is stored for all admins. The admin and several appointment controllers lacked their psalm type imports, which hid findings and left baseline entries behind. With the imports in place: - Talk room, booking and check-in responses now go through the serializers and carry the fields their documented types promise (occupancy, isFull, waitlisted, effective booking status). - Vacation, audit and self-check-in payloads are typed at their source. - The navigation and response-list types no longer claim fields the server never sent. - The deprecated getAppValue and getJobs calls in the admin settings are replaced. Nothing is removed from any response, so older mobile clients keep working. --- l10n/de.js | 8 +- l10n/de.json | 8 +- l10n/de_DE.js | 8 +- l10n/de_DE.json | 8 +- lib/Controller/AdminController.php | 34 ++- lib/Controller/AppointmentController.php | 35 ++- lib/Controller/AuditController.php | 11 +- lib/Controller/SelfCheckinController.php | 15 +- lib/Controller/Traits/RequiresAuthTrait.php | 2 +- lib/Controller/VacationController.php | 18 +- lib/Db/AuditEvent.php | 11 +- lib/Db/Vacation.php | 5 + lib/ResponseDefinitions.php | 19 +- lib/Service/AppointmentSerializer.php | 6 +- lib/Service/AppointmentService.php | 28 +- lib/Service/ConfigService.php | 13 + lib/Service/NotificationService.php | 13 + lib/Service/PermissionService.php | 2 + lib/Service/SelfCheckinService.php | 9 +- openapi-administration.json | 78 +++++- openapi-full.json | 147 +++++++++-- openapi.json | 69 ++++- psalm-baseline.xml | 243 +----------------- src/views/AdminSettings.vue | 61 ++++- tests/unit/Controller/AdminControllerTest.php | 33 ++- .../Controller/VacationControllerTest.php | 4 +- .../unit/Service/NotificationServiceTest.php | 22 ++ 27 files changed, 570 insertions(+), 340 deletions(-) diff --git a/l10n/de.js b/l10n/de.js index 5125c426..5ad6cd32 100644 --- a/l10n/de.js +++ b/l10n/de.js @@ -1091,6 +1091,12 @@ OC.L10N.register( "Template" : "Vorlage", "All day" : "Ganztägig", "You are scheduled in for this appointment." : "Für diesen Termin bist du eingeplant.", - "Visible to all. Search to restrict …" : "Für alle sichtbar. Zum Einschränken suchen …" + "Visible to all. Search to restrict …" : "Für alle sichtbar. Zum Einschränken suchen …", + "Nextcloud does not forward notifications by email: in the Notifications settings, \"Send email reminders about unhandled notifications\" is set to \"Never\". Reminders and appointment notifications from Attendance only show up in the notification bell and the mobile app." : "Nextcloud leitet Benachrichtigungen nicht per E-Mail weiter: In den Einstellungen der Benachrichtigungs-App steht „E-Mail-Erinnerungen für unbearbeitete Benachrichtigungen senden nach“ auf „Niemals“. Erinnerungen und Termin-Benachrichtigungen von Attendance erscheinen nur in der Glocke und in der mobilen App.", + "Choose an interval there to deliver them by email as well. It applies to everyone who has not picked their own interval in their personal settings and can take a day to take effect." : "Wähle dort ein Intervall, damit sie zusätzlich per E-Mail zugestellt werden. Es gilt für alle, die in ihren persönlichen Einstellungen kein eigenes Intervall gewählt haben, und kann bis zu einem Tag dauern, bis es wirkt.", + "Open Notifications settings" : "Einstellungen der Benachrichtigungs-App öffnen", + "Hide hint" : "Hinweis ausblenden", + "Show hint" : "Hinweis anzeigen", + "Nextcloud does not forward notifications by email." : "Nextcloud leitet Benachrichtigungen nicht per E-Mail weiter." }, "nplurals=2; plural=(n != 1);"); diff --git a/l10n/de.json b/l10n/de.json index 65630852..00ca0ca2 100644 --- a/l10n/de.json +++ b/l10n/de.json @@ -1089,6 +1089,12 @@ "Template" : "Vorlage", "All day" : "Ganztägig", "You are scheduled in for this appointment." : "Für diesen Termin bist du eingeplant.", - "Visible to all. Search to restrict …" : "Für alle sichtbar. Zum Einschränken suchen …" + "Visible to all. Search to restrict …" : "Für alle sichtbar. Zum Einschränken suchen …", + "Nextcloud does not forward notifications by email: in the Notifications settings, \"Send email reminders about unhandled notifications\" is set to \"Never\". Reminders and appointment notifications from Attendance only show up in the notification bell and the mobile app." : "Nextcloud leitet Benachrichtigungen nicht per E-Mail weiter: In den Einstellungen der Benachrichtigungs-App steht „E-Mail-Erinnerungen für unbearbeitete Benachrichtigungen senden nach“ auf „Niemals“. Erinnerungen und Termin-Benachrichtigungen von Attendance erscheinen nur in der Glocke und in der mobilen App.", + "Choose an interval there to deliver them by email as well. It applies to everyone who has not picked their own interval in their personal settings and can take a day to take effect." : "Wähle dort ein Intervall, damit sie zusätzlich per E-Mail zugestellt werden. Es gilt für alle, die in ihren persönlichen Einstellungen kein eigenes Intervall gewählt haben, und kann bis zu einem Tag dauern, bis es wirkt.", + "Open Notifications settings" : "Einstellungen der Benachrichtigungs-App öffnen", + "Hide hint" : "Hinweis ausblenden", + "Show hint" : "Hinweis anzeigen", + "Nextcloud does not forward notifications by email." : "Nextcloud leitet Benachrichtigungen nicht per E-Mail weiter." },"pluralForm" :"nplurals=2; plural=(n != 1);" } \ No newline at end of file diff --git a/l10n/de_DE.js b/l10n/de_DE.js index b36ba313..acdbf8af 100644 --- a/l10n/de_DE.js +++ b/l10n/de_DE.js @@ -1091,6 +1091,12 @@ OC.L10N.register( "Template" : "Vorlage", "All day" : "Ganztägig", "You are scheduled in for this appointment." : "Für diesen Termin sind Sie eingeplant.", - "Visible to all. Search to restrict …" : "Für alle sichtbar. Zum Einschränken suchen …" + "Visible to all. Search to restrict …" : "Für alle sichtbar. Zum Einschränken suchen …", + "Nextcloud does not forward notifications by email: in the Notifications settings, \"Send email reminders about unhandled notifications\" is set to \"Never\". Reminders and appointment notifications from Attendance only show up in the notification bell and the mobile app." : "Nextcloud leitet Benachrichtigungen nicht per E-Mail weiter: In den Einstellungen der Benachrichtigungs-App steht „E-Mail-Erinnerungen für unbearbeitete Benachrichtigungen senden nach“ auf „Niemals“. Erinnerungen und Termin-Benachrichtigungen von Attendance erscheinen nur in der Glocke und in der mobilen App.", + "Choose an interval there to deliver them by email as well. It applies to everyone who has not picked their own interval in their personal settings and can take a day to take effect." : "Wählen Sie dort ein Intervall, damit sie zusätzlich per E-Mail zugestellt werden. Es gilt für alle, die in ihren persönlichen Einstellungen kein eigenes Intervall gewählt haben, und kann bis zu einem Tag dauern, bis es wirkt.", + "Open Notifications settings" : "Einstellungen der Benachrichtigungs-App öffnen", + "Hide hint" : "Hinweis ausblenden", + "Show hint" : "Hinweis anzeigen", + "Nextcloud does not forward notifications by email." : "Nextcloud leitet Benachrichtigungen nicht per E-Mail weiter." }, "nplurals=2; plural=(n != 1);"); diff --git a/l10n/de_DE.json b/l10n/de_DE.json index ba846c26..8a6856f0 100644 --- a/l10n/de_DE.json +++ b/l10n/de_DE.json @@ -1089,6 +1089,12 @@ "Template" : "Vorlage", "All day" : "Ganztägig", "You are scheduled in for this appointment." : "Für diesen Termin sind Sie eingeplant.", - "Visible to all. Search to restrict …" : "Für alle sichtbar. Zum Einschränken suchen …" + "Visible to all. Search to restrict …" : "Für alle sichtbar. Zum Einschränken suchen …", + "Nextcloud does not forward notifications by email: in the Notifications settings, \"Send email reminders about unhandled notifications\" is set to \"Never\". Reminders and appointment notifications from Attendance only show up in the notification bell and the mobile app." : "Nextcloud leitet Benachrichtigungen nicht per E-Mail weiter: In den Einstellungen der Benachrichtigungs-App steht „E-Mail-Erinnerungen für unbearbeitete Benachrichtigungen senden nach“ auf „Niemals“. Erinnerungen und Termin-Benachrichtigungen von Attendance erscheinen nur in der Glocke und in der mobilen App.", + "Choose an interval there to deliver them by email as well. It applies to everyone who has not picked their own interval in their personal settings and can take a day to take effect." : "Wählen Sie dort ein Intervall, damit sie zusätzlich per E-Mail zugestellt werden. Es gilt für alle, die in ihren persönlichen Einstellungen kein eigenes Intervall gewählt haben, und kann bis zu einem Tag dauern, bis es wirkt.", + "Open Notifications settings" : "Einstellungen der Benachrichtigungs-App öffnen", + "Hide hint" : "Hinweis ausblenden", + "Show hint" : "Hinweis anzeigen", + "Nextcloud does not forward notifications by email." : "Nextcloud leitet Benachrichtigungen nicht per E-Mail weiter." },"pluralForm" :"nplurals=2; plural=(n != 1);" } \ No newline at end of file diff --git a/lib/Controller/AdminController.php b/lib/Controller/AdminController.php index 83dfb205..e768d992 100644 --- a/lib/Controller/AdminController.php +++ b/lib/Controller/AdminController.php @@ -26,6 +26,13 @@ use OCP\IRequest; use OCP\IUserSession; +/** + * @psalm-import-type AttendanceAdminConfig from \OCA\Attendance\ResponseDefinitions + * @psalm-import-type AttendanceAdminStatus from \OCA\Attendance\ResponseDefinitions + * @psalm-import-type AttendanceGroupOption from \OCA\Attendance\ResponseDefinitions + * @psalm-import-type AttendanceTestReminderResult from \OCA\Attendance\ResponseDefinitions + * @psalm-import-type AttendanceWritableCalendar from \OCA\Attendance\ResponseDefinitions + */ class AdminController extends Controller { use DatetimeFormatTrait; @@ -104,7 +111,7 @@ public function getSettings(): DataResponse { $groupOptions = $this->permissionService->getAvailableGroups(); // Get currently configured whitelisted groups - $whitelistedGroups = $this->configService->getWhitelistedGroups(); + $whitelistedGroups = array_values($this->configService->getWhitelistedGroups()); // Get currently configured whitelisted teams with display names $whitelistedTeamIds = $this->configService->getWhitelistedTeams(); @@ -131,9 +138,8 @@ public function getSettings(): DataResponse { // Compute status: next reminder run $nextReminderRun = null; - $reminderJobs = $this->jobList->getJobs(ReminderJob::class, 1, 0); - if (!empty($reminderJobs)) { - $lastRun = $reminderJobs[0]->getLastRun(); + foreach ($this->jobList->getJobsIterator(ReminderJob::class, 1, 0) as $reminderJob) { + $lastRun = $reminderJob->getLastRun(); if ($lastRun > 0) { $nextReminderRun = gmdate('Y-m-d H:i:s', $lastRun + 86400); } @@ -141,6 +147,11 @@ public function getSettings(): DataResponse { $pushDeviceCount = $this->notificationService->countPushDevices($user->getUID()); + $notificationsApp = [ + 'emailForwardingEnabled' => $this->notificationService->isEmailForwardingEnabledByDefault(), + 'emailHintDismissed' => $this->configService->isNotificationEmailHintDismissed(), + ]; + return new DataResponse([ 'config' => [ 'whitelistedGroups' => $whitelistedGroups, @@ -149,9 +160,9 @@ public function getSettings(): DataResponse { 'responseSummaryTeamsMode' => $this->configService->getResponseSummaryTeamsMode(), 'permissions' => $permissionSettings, 'reminders' => [ - 'enabled' => $this->config->getAppValue('attendance', 'reminders_enabled', 'no') === 'yes', - 'reminderDays' => (int)$this->config->getAppValue('attendance', 'reminder_days', '7'), - 'reminderFrequency' => (int)$this->config->getAppValue('attendance', 'reminder_frequency', '0'), + 'enabled' => $this->configService->areRemindersEnabled(), + 'reminderDays' => $this->configService->getReminderDays(), + 'reminderFrequency' => $this->configService->getReminderFrequency(), 'reminderTarget' => $this->configService->getReminderTarget(), ], 'calendarSync' => [ @@ -183,6 +194,7 @@ public function getSettings(): DataResponse { 'whitelistEnabled' => $this->guestService->isGuestsWhitelistEnabled(), 'attendanceInWhitelist' => $this->guestService->isAttendanceInGuestsWhitelist(), ], + 'notificationsApp' => $notificationsApp, ], 'status' => [ 'nextAppointment' => $nextAppointment, @@ -220,6 +232,7 @@ public function getSettings(): DataResponse { * @param ?bool $allowMaybe Whether appointments offer "Maybe" where they have no override of their own * @param ?int $selfCheckinWindowMinutes Minutes before appointment start that self-check-in opens * @param ?bool $onboardingCompleted Whether the setup wizard has been walked to its end + * @param ?bool $notificationEmailHintDismissed Whether the admin collapsed the hint that notifications do not reach email * @return DataResponse, array{}>|DataResponse|DataResponse|DataResponse */ #[NoCSRFRequired] @@ -242,6 +255,7 @@ public function saveSettings( ?bool $allowMaybe = null, ?int $selfCheckinWindowMinutes = null, ?bool $onboardingCompleted = null, + ?bool $notificationEmailHintDismissed = null, ): DataResponse { // Get current user $user = $this->userSession->getUser(); @@ -340,6 +354,10 @@ public function saveSettings( $this->configService->setOnboardingCompleted($onboardingCompleted); } + if ($notificationEmailHintDismissed !== null) { + $this->configService->setNotificationEmailHintDismissed($notificationEmailHintDismissed); + } + return new DataResponse([]); } catch (\Exception $e) { return new DataResponse(['error' => $e->getMessage()], 500); @@ -351,7 +369,7 @@ public function saveSettings( * * Uses the next upcoming appointment to send a preview reminder notification. * - * @return DataResponse|DataResponse|DataResponse|DataResponse + * @return DataResponse|DataResponse|DataResponse|DataResponse|DataResponse */ #[NoCSRFRequired] #[OpenAPI(OpenAPI::SCOPE_ADMINISTRATION)] diff --git a/lib/Controller/AppointmentController.php b/lib/Controller/AppointmentController.php index adc3e227..5e3c9bab 100644 --- a/lib/Controller/AppointmentController.php +++ b/lib/Controller/AppointmentController.php @@ -33,9 +33,24 @@ use OCP\Security\ISecureRandom; /** + * @psalm-import-type AttendanceAppointmentData from \OCA\Attendance\ResponseDefinitions * @psalm-import-type AttendanceAppointmentPage from \OCA\Attendance\ResponseDefinitions + * @psalm-import-type AttendanceAppointmentWithResponse from \OCA\Attendance\ResponseDefinitions + * @psalm-import-type AttendanceBulkAppointmentItem from \OCA\Attendance\ResponseDefinitions + * @psalm-import-type AttendanceCapabilities from \OCA\Attendance\ResponseDefinitions + * @psalm-import-type AttendanceCheckinData from \OCA\Attendance\ResponseDefinitions + * @psalm-import-type AttendanceDeleteResult from \OCA\Attendance\ResponseDefinitions * @psalm-import-type AttendanceDirectoryEntry from \OCA\Attendance\ResponseDefinitions + * @psalm-import-type AttendanceExportResult from \OCA\Attendance\ResponseDefinitions * @psalm-import-type AttendanceExportSeries from \OCA\Attendance\ResponseDefinitions + * @psalm-import-type AttendanceGuestCreationResult from \OCA\Attendance\ResponseDefinitions + * @psalm-import-type AttendanceNavigationAppointment from \OCA\Attendance\ResponseDefinitions + * @psalm-import-type AttendancePushConfig from \OCA\Attendance\ResponseDefinitions + * @psalm-import-type AttendanceReminderResult from \OCA\Attendance\ResponseDefinitions + * @psalm-import-type AttendanceResponseData from \OCA\Attendance\ResponseDefinitions + * @psalm-import-type AttendanceResponseWithUser from \OCA\Attendance\ResponseDefinitions + * @psalm-import-type AttendanceUserConfig from \OCA\Attendance\ResponseDefinitions + * @psalm-import-type AttendanceUserPermissions from \OCA\Attendance\ResponseDefinitions */ class AppointmentController extends Controller { private AppointmentService $appointmentService; @@ -99,6 +114,8 @@ public function __construct( * (global manager or organizer of this appointment). Returns the error * DataResponse to send when loading or authorization fails, the * Appointment otherwise. + * + * @return \OCA\Attendance\Db\Appointment|DataResponse */ private function findManageableAppointment(int $id, string $userId, string $permissionError): \OCA\Attendance\Db\Appointment|DataResponse { try { @@ -609,7 +626,7 @@ public function createTalkRoom(int $id): DataResponse { return new DataResponse(['error' => 'The conversation could not be created'], 400); } - return new DataResponse($this->appointmentService->getAppointment($id)); + return new DataResponse($this->appointmentService->serializeAppointment($this->appointmentService->getAppointment($id))); } /** @@ -643,7 +660,7 @@ public function deleteTalkRoom(int $id): DataResponse { return new DataResponse(['error' => 'The conversation could not be deleted'], 400); } - return new DataResponse($this->appointmentService->getAppointment($id)); + return new DataResponse($this->appointmentService->serializeAppointment($this->appointmentService->getAppointment($id))); } /** @@ -733,6 +750,8 @@ public function unbook(int $id, string $userId): DataResponse { /** * Shared book/unbook implementation: permission + feature gating, then * delegate to BookingService. + * + * @return DataResponse|DataResponse */ private function setBooking(int $id, string $userId, bool $book): DataResponse { $user = $this->userSession->getUser(); @@ -759,7 +778,7 @@ private function setBooking(int $id, string $userId, bool $book): DataResponse { return new DataResponse(['error' => $e->getMessage()], 400); } - return new DataResponse($updated); + return new DataResponse($this->appointmentService->serializeResponse($updated, $appointment)); } /** @@ -845,7 +864,7 @@ public function destroy(int $id, string $scope = 'single'): DataResponse { * Get detailed responses for an appointment (requires manage appointments permission or being an organizer of it) * * @param int $id Appointment ID - * @return DataResponse, array{}>|DataResponse|DataResponse|DataResponse + * @return DataResponse, array{}>|DataResponse|DataResponse|DataResponse|DataResponse */ #[NoAdminRequired] #[NoCSRFRequired] @@ -975,7 +994,7 @@ public function widget(): DataResponse { * @param string $targetUserId User ID to check in * @param ?string $response Check-in response: yes, no, or null to keep current state * @param string $comment Optional check-in comment - * @return DataResponse|DataResponse|DataResponse|DataResponse + * @return DataResponse|DataResponse|DataResponse|DataResponse|DataResponse */ #[NoAdminRequired] #[NoCSRFRequired] @@ -993,7 +1012,7 @@ public function checkinResponse(int $appointmentId, string $targetUserId, ?strin // Verify appointment exists try { - $this->appointmentService->getAppointment($appointmentId); + $appointment = $this->appointmentService->getAppointment($appointmentId); } catch (\Exception $e) { return new DataResponse(['error' => 'Appointment not found'], 404); } @@ -1007,7 +1026,7 @@ public function checkinResponse(int $appointmentId, string $targetUserId, ?strin $user->getUID() ); - return new DataResponse($result); + return new DataResponse($this->appointmentService->serializeResponse($result, $appointment)); } catch (\Exception $e) { return new DataResponse(['error' => $e->getMessage()], 400); } @@ -1587,7 +1606,7 @@ public function sendReminders(int $id, string $target = 'non_responders'): DataR * * @param int $id Appointment ID * @param string $userId Target user ID - * @return DataResponse|DataResponse|DataResponse|DataResponse + * @return DataResponse|DataResponse|DataResponse|DataResponse|DataResponse */ #[NoAdminRequired] #[NoCSRFRequired] diff --git a/lib/Controller/AuditController.php b/lib/Controller/AuditController.php index 7841d40f..2130f50b 100644 --- a/lib/Controller/AuditController.php +++ b/lib/Controller/AuditController.php @@ -20,6 +20,9 @@ use OCP\IUserManager; use OCP\IUserSession; +/** + * @psalm-import-type AttendanceAuditPage from \OCA\Attendance\ResponseDefinitions + */ class AuditController extends Controller { private IUserSession $userSession; private IUserManager $userManager; @@ -96,10 +99,10 @@ public function index(int $id, ?string $verb = null, ?string $subject = null, in $displayNames = $this->resolveDisplayNames($events); $items = []; foreach ($events as $event) { - $serialized = $event->jsonSerialize(); - $serialized['actor'] = $this->refFor($event->getActorId(), $displayNames); - $serialized['subject'] = $this->refFor($event->getSubjectId(), $displayNames); - $items[] = $serialized; + $items[] = $event->jsonSerialize() + [ + 'actor' => $this->refFor($event->getActorId(), $displayNames), + 'subject' => $this->refFor($event->getSubjectId(), $displayNames), + ]; } return new DataResponse([ diff --git a/lib/Controller/SelfCheckinController.php b/lib/Controller/SelfCheckinController.php index b0875d36..ceb88721 100644 --- a/lib/Controller/SelfCheckinController.php +++ b/lib/Controller/SelfCheckinController.php @@ -25,6 +25,9 @@ /** * Controller for self-check-in via NFC / deep link. * All endpoints require Nextcloud authentication (logged-in user). + * + * @psalm-import-type AttendanceSelfCheckinOverview from \OCA\Attendance\ResponseDefinitions + * @psalm-import-type AttendanceSelfCheckinResult from \OCA\Attendance\ResponseDefinitions */ class SelfCheckinController extends Controller { use RequiresAuthTrait; @@ -67,9 +70,9 @@ public function __construct( #[NoCSRFRequired] #[OpenAPI(OpenAPI::SCOPE_DEFAULT)] public function getActiveAppointments(): DataResponse { - $user = $this->requireUser(); - if ($user instanceof DataResponse) { - return $user; + $user = $this->userSession->getUser(); + if (!$user) { + return new DataResponse(['error' => 'User not authenticated'], Http::STATUS_UNAUTHORIZED); } $permissionError = $this->requirePermission('self_checkin', 'Self-check-in is not enabled for your account.'); @@ -100,9 +103,9 @@ public function getActiveAppointments(): DataResponse { #[NoCSRFRequired] #[OpenAPI(OpenAPI::SCOPE_DEFAULT)] public function checkin(int $appointmentId, string $method = 'qr'): DataResponse { - $user = $this->requireUser(); - if ($user instanceof DataResponse) { - return $user; + $user = $this->userSession->getUser(); + if (!$user) { + return new DataResponse(['error' => 'User not authenticated'], Http::STATUS_UNAUTHORIZED); } $permissionError = $this->requirePermission('self_checkin', 'Self-check-in is not enabled for your account.'); diff --git a/lib/Controller/Traits/RequiresAuthTrait.php b/lib/Controller/Traits/RequiresAuthTrait.php index 46412d3e..85a075e0 100644 --- a/lib/Controller/Traits/RequiresAuthTrait.php +++ b/lib/Controller/Traits/RequiresAuthTrait.php @@ -74,7 +74,7 @@ protected function currentUserHasPermission(string $permission): bool { * * @param string $permission The permission to require * @param string $errorMessage Custom error message - * @return DataResponse|null Error response or null if permission granted + * @return DataResponse<403, array{error: string}, array{}>|null Error response or null if permission granted */ protected function requirePermission(string $permission, string $errorMessage = ''): ?DataResponse { if (!$this->currentUserHasPermission($permission)) { diff --git a/lib/Controller/VacationController.php b/lib/Controller/VacationController.php index e92d7131..e17e75c8 100644 --- a/lib/Controller/VacationController.php +++ b/lib/Controller/VacationController.php @@ -4,6 +4,7 @@ namespace OCA\Attendance\Controller; +use OCA\Attendance\Db\Vacation; use OCA\Attendance\Service\AppointmentService; use OCA\Attendance\Service\PermissionService; use OCA\Attendance\Service\VacationService; @@ -17,6 +18,11 @@ use OCP\IRequest; use OCP\IUserSession; +/** + * @psalm-import-type AttendanceVacationConflicts from \OCA\Attendance\ResponseDefinitions + * @psalm-import-type AttendanceVacationData from \OCA\Attendance\ResponseDefinitions + * @psalm-import-type AttendanceVacationEntry from \OCA\Attendance\ResponseDefinitions + */ class VacationController extends Controller { private IUserSession $userSession; private VacationService $vacationService; @@ -52,7 +58,10 @@ public function index(): DataResponse { return new DataResponse(['error' => 'User not authenticated'], Http::STATUS_UNAUTHORIZED); } - return new DataResponse($this->vacationService->getForUser($user->getUID())); + return new DataResponse(array_map( + static fn (Vacation $vacation): array => $vacation->jsonSerialize(), + $this->vacationService->getForUser($user->getUID()), + )); } /** @@ -73,7 +82,7 @@ public function create(string $startDate, string $endDate, ?string $note = null) } try { - return new DataResponse($this->vacationService->create($user->getUID(), $startDate, $endDate, $note), Http::STATUS_CREATED); + return new DataResponse($this->vacationService->create($user->getUID(), $startDate, $endDate, $note)->jsonSerialize(), Http::STATUS_CREATED); } catch (\InvalidArgumentException $e) { return new DataResponse(['error' => $e->getMessage()], Http::STATUS_BAD_REQUEST); } @@ -98,7 +107,7 @@ public function update(int $id, string $startDate, string $endDate, ?string $not } try { - return new DataResponse($this->vacationService->update($id, $user->getUID(), $startDate, $endDate, $note)); + return new DataResponse($this->vacationService->update($id, $user->getUID(), $startDate, $endDate, $note)->jsonSerialize()); } catch (DoesNotExistException $e) { return new DataResponse(['error' => 'Vacation not found'], Http::STATUS_NOT_FOUND); } catch (\InvalidArgumentException $e) { @@ -211,7 +220,8 @@ public function overview(?string $from = null, ?string $to = null): DataResponse } $fromDate = $from ?? gmdate('Y-m-d'); - $toDate = $to ?? gmdate('Y-m-d', strtotime($fromDate . ' +180 days')); + $toTimestamp = strtotime($fromDate . ' +180 days'); + $toDate = $to ?? gmdate('Y-m-d', $toTimestamp === false ? null : $toTimestamp); $candidates = $this->appointmentService->getAllWhitelistedUsers(); return new DataResponse($this->vacationService->getOverview($fromDate, $toDate, $candidates)); diff --git a/lib/Db/AuditEvent.php b/lib/Db/AuditEvent.php index 02aa96a7..f644610d 100644 --- a/lib/Db/AuditEvent.php +++ b/lib/Db/AuditEvent.php @@ -45,19 +45,28 @@ public function __construct() { $this->addType('createdAt', 'string'); } + /** + * @return array + */ public function getMetaArray(): array { $raw = $this->getMeta(); if ($raw === null || $raw === '') { return []; } $decoded = json_decode($raw, true); - return is_array($decoded) ? $decoded : []; + if (!is_array($decoded)) { + return []; + } + return array_combine(array_map('strval', array_keys($decoded)), $decoded); } public function setMetaArray(array $meta): void { $this->setMeta($meta === [] ? null : json_encode($meta, JSON_UNESCAPED_UNICODE)); } + /** + * @return array{id: int, appointmentId: int, verb: string, actorId: ?string, subjectId: ?string, meta: array, source: ?string, createdAt: ?string} + */ public function jsonSerialize(): array { return [ 'id' => $this->getId(), diff --git a/lib/Db/Vacation.php b/lib/Db/Vacation.php index 90852a44..ac6888df 100644 --- a/lib/Db/Vacation.php +++ b/lib/Db/Vacation.php @@ -22,6 +22,8 @@ * @method void setCreatedAt(string $createdAt) * @method string getUpdatedAt() * @method void setUpdatedAt(string $updatedAt) + * + * @psalm-import-type AttendanceVacationData from \OCA\Attendance\ResponseDefinitions */ class Vacation extends Entity implements JsonSerializable { protected $userId = ''; @@ -43,6 +45,9 @@ public function __construct() { $this->addType('updatedAt', 'string'); } + /** + * @return AttendanceVacationData + */ public function jsonSerialize(): array { return [ 'id' => $this->getId(), diff --git a/lib/ResponseDefinitions.php b/lib/ResponseDefinitions.php index 7d997ea5..0711a008 100644 --- a/lib/ResponseDefinitions.php +++ b/lib/ResponseDefinitions.php @@ -82,14 +82,18 @@ * bookingStatus: ?string, * userName: string, * userGroups: list, - * isGuest: bool, - * waitlisted: bool, + * isGuest?: bool, + * waitlisted?: bool, * } * @psalm-type AttendanceGuestsAppStatus = array{ * enabled: bool, * whitelistEnabled: bool, * attendanceInWhitelist: bool, * } + * @psalm-type AttendanceNotificationsAppStatus = array{ + * emailForwardingEnabled: bool, + * emailHintDismissed: bool, + * } * @psalm-type AttendanceGuestCreationResult = array{ * userId: string, * displayName: string, @@ -139,8 +143,6 @@ * name: string, * startDatetime: string, * isAllDay: bool, - * seriesId: ?string, - * seriesPosition: ?int, * userResponse: ?array{response: string}, * closedAt: ?string, * cancelledAt: ?string, @@ -226,7 +228,7 @@ * users: list, * } * @psalm-type AttendanceGroupOption = array{id: string, displayName: string} - * @psalm-type AttendanceTeamOption = array{id: string, displayName: string} + * @psalm-type AttendanceTeamOption = array{id: string, label: string, type: string} * @psalm-type AttendancePermissionSetting = array{mode: string, groups: list, categories?: list} * @psalm-type AttendancePermissionSettings = array * @psalm-type AttendanceUserPermissions = array{ @@ -322,6 +324,11 @@ * userId: ?string, * summary: bool, * } + * @psalm-type AttendanceAdminVacationCalendarConfig = array{ + * enabled: bool, + * calendarUri: ?string, + * userId: ?string, + * } * @psalm-type AttendanceWritableCalendar = array{ * uri: string, * displayName: string, @@ -340,6 +347,7 @@ * reminders: AttendanceAdminReminderConfig, * calendarSync: AttendanceAdminCalendarSyncConfig, * orgCalendar: AttendanceAdminOrgCalendarConfig, + * vacationCalendar: AttendanceAdminVacationCalendarConfig, * audit: AttendanceAdminAuditConfig, * displayOrder: string, * pushEnabled: bool, @@ -348,6 +356,7 @@ * allowMaybe: bool, * selfCheckinWindowMinutes: int, * guestsApp: AttendanceGuestsAppStatus, + * notificationsApp: AttendanceNotificationsAppStatus, * } * @psalm-type AttendanceAdminStatus = array{ * nextAppointment: ?array{name: string, startDatetime: string, isAllDay: bool}, diff --git a/lib/Service/AppointmentSerializer.php b/lib/Service/AppointmentSerializer.php index 968e96f2..2107dcc8 100644 --- a/lib/Service/AppointmentSerializer.php +++ b/lib/Service/AppointmentSerializer.php @@ -13,6 +13,8 @@ * instance default and on whether the appointment has a limit, and occupancy is * derived from the queue. Both the appointment endpoints and the check-in view * hand out this shape, so it lives here rather than in either of them. + * + * @psalm-import-type AttendanceAppointmentData from \OCA\Attendance\ResponseDefinitions */ class AppointmentSerializer { public function __construct( @@ -22,10 +24,9 @@ public function __construct( } /** - * @return array + * @return AttendanceAppointmentData */ public function serialize(Appointment $appointment): array { - /** @var array $data */ $data = $appointment->jsonSerialize(); $data['allowMaybe'] = $this->responsePolicyService->isMaybeAllowed($appointment); // Handed on to isFull() so the queue is counted once; without a limit @@ -33,6 +34,7 @@ public function serialize(Appointment $appointment): array { $occupancy = $this->capacityService->occupancy($appointment); $data['occupancy'] = $occupancy; $data['isFull'] = $this->capacityService->isFull($appointment, $occupancy); + /** @var AttendanceAppointmentData $data */ return $data; } } diff --git a/lib/Service/AppointmentService.php b/lib/Service/AppointmentService.php index 7fe74164..5180932a 100644 --- a/lib/Service/AppointmentService.php +++ b/lib/Service/AppointmentService.php @@ -18,6 +18,10 @@ * Core service for managing appointments and responses. * Delegates to specialized services for summary, visibility, and check-in operations. * + * @psalm-import-type AttendanceAppointmentData from \OCA\Attendance\ResponseDefinitions + * @psalm-import-type AttendanceNavigationAppointment from \OCA\Attendance\ResponseDefinitions + * @psalm-import-type AttendanceResponseData from \OCA\Attendance\ResponseDefinitions + * @psalm-import-type AttendanceResponseWithUser from \OCA\Attendance\ResponseDefinitions * @psalm-type ListRow = array{appointment: Appointment, isAttendee: bool, isOrganizer: bool, isPast: bool} */ class AppointmentService { @@ -1260,16 +1264,17 @@ private function serializeUserResponse(?AttendanceResponse $userResponse, ?Appoi * whether this yes holds a spot or a place in line. The standing is derived, * so it moves on its own as other people change their answers. * - * @return array + * @return AttendanceResponseData */ public function serializeResponse(AttendanceResponse $response, ?Appointment $appointment = null): array { - /** @var array $data */ $data = $response->jsonSerialize(); $data['bookingStatus'] = $this->bookingService->effectiveBookingStatus($response); $standing = $appointment === null ? ['waitlisted' => false, 'waitlistPosition' => null] : $this->capacityService->standingOf($appointment, $response->getUserId()); - return $data + $standing; + /** @var AttendanceResponseData $merged */ + $merged = $data + $standing; + return $merged; } /** @@ -1566,6 +1571,8 @@ public function getAppointmentResponses(int $appointmentId): array { /** * Get all responses for an appointment with user details. + * + * @return list */ public function getAppointmentResponsesWithUsers(int $appointmentId, string $requestingUserId): array { $appointment = $this->appointmentMapper->find($appointmentId); @@ -1598,6 +1605,7 @@ public function getAppointmentResponsesWithUsers(int $appointmentId, string $req $result[] = $responseData; } + /** @var list $result */ return $result; } @@ -1609,7 +1617,7 @@ public function getAppointmentResponsesWithUsers(int $appointmentId, string $req * * @param ?int $pastLimit Page size for the past entries, which grow without * bound; null returns all of them - * @return array{current: list>, past: list>, pastHasMore: bool} + * @return array{current: list, past: list, pastHasMore: bool} */ public function getAppointmentsForNavigation(string $userId, ?int $pastLimit = null, int $pastOffset = 0): array { $current = $this->buildNavigationData($this->ownAppointments($this->getUpcomingAppointments(), $userId), $userId); @@ -1666,23 +1674,21 @@ private function ownAppointments(array $appointments, string $userId): array { /** * @param list $own - * @return list> + * @return list */ private function buildNavigationData(array $own, string $userId): array { $responses = $this->responseMapper->findByUserForAppointments($userId, self::rowIds($own)); $result = []; foreach ($own as ['appointment' => $appointment, 'inAudience' => $inAudience]) { - $userResponse = $responses[$appointment->getId()] ?? null; + $answer = ($responses[$appointment->getId()] ?? null)?->getResponse(); $result[] = [ 'id' => $appointment->getId(), 'name' => $appointment->getName(), - 'startDatetime' => $this->formatDatetimeToUtc($appointment->getStartDatetime()), + 'startDatetime' => $this->formatDatetimeToUtc($appointment->getStartDatetime()) ?? '', 'isAllDay' => $appointment->isAllDay(), - 'userResponse' => ($userResponse && $userResponse->getResponse() !== null) - ? ['response' => $userResponse->getResponse()] - : null, + 'userResponse' => $answer !== null ? ['response' => $answer] : null, 'closedAt' => $this->formatDatetimeToUtc($appointment->getClosedAt()), 'cancelledAt' => $this->formatDatetimeToUtc($appointment->getCancelledAt()), 'inAudience' => $inAudience, @@ -2292,7 +2298,7 @@ public function getAllWhitelistedUsers(): array { * on its own — the tri-state column has to be read against the instance * default — so payloads go through here instead of jsonSerialize(). * - * @return array + * @return AttendanceAppointmentData */ public function serializeAppointment(Appointment $appointment): array { return $this->appointmentSerializer->serialize($appointment); diff --git a/lib/Service/ConfigService.php b/lib/Service/ConfigService.php index f41deff3..12c0dcfb 100644 --- a/lib/Service/ConfigService.php +++ b/lib/Service/ConfigService.php @@ -514,6 +514,19 @@ public function setMobileAppBannerEnabled(bool $enabled): void { $this->config->setAppValue(self::APP_ID, 'mobile_app_banner_enabled', $enabled ? 'yes' : 'no'); } + /** + * Whether an admin collapsed the reminder-settings hint that Nextcloud + * keeps notifications out of people's inboxes — a deliberate choice on + * some instances, so the hint must not nag. + */ + public function isNotificationEmailHintDismissed(): bool { + return $this->appConfig->getValueBool(self::APP_ID, 'notification_email_hint_dismissed', false); + } + + public function setNotificationEmailHintDismissed(bool $dismissed): void { + $this->appConfig->setValueBool(self::APP_ID, 'notification_email_hint_dismissed', $dismissed); + } + /** * Whether an admin has walked the setup wizard to its end. Only decides * which empty-instance hint admins get — the wizard stays reachable from diff --git a/lib/Service/NotificationService.php b/lib/Service/NotificationService.php index 20de2462..2d8bbe54 100644 --- a/lib/Service/NotificationService.php +++ b/lib/Service/NotificationService.php @@ -7,6 +7,7 @@ use OCA\Attendance\Db\Appointment; use OCP\Activity\IManager as IActivityManager; use OCP\App\IAppManager; +use OCP\IAppConfig; use OCP\IDBConnection; use OCP\IURLGenerator; use OCP\Notification\IManager as INotificationManager; @@ -16,6 +17,7 @@ class NotificationService { private INotificationManager $notificationManager; private IActivityManager $activityManager; private IAppManager $appManager; + private IAppConfig $appConfig; private IURLGenerator $urlGenerator; private IDBConnection $db; private LoggerInterface $logger; @@ -25,6 +27,7 @@ public function __construct( INotificationManager $notificationManager, IActivityManager $activityManager, IAppManager $appManager, + IAppConfig $appConfig, IURLGenerator $urlGenerator, IDBConnection $db, LoggerInterface $logger, @@ -32,6 +35,7 @@ public function __construct( $this->notificationManager = $notificationManager; $this->activityManager = $activityManager; $this->appManager = $appManager; + $this->appConfig = $appConfig; $this->urlGenerator = $urlGenerator; $this->db = $db; $this->logger = $logger; @@ -76,6 +80,15 @@ public function isNotificationsAppEnabled(): bool { return $this->appManager->isEnabledForUser('notifications'); } + /** + * Whether the Notifications app forwards unhandled notifications by email + * for users on the instance default. 0 is its "Never", the value a fresh + * instance ships with; users may still opt in individually. + */ + public function isEmailForwardingEnabledByDefault(): bool { + return $this->appConfig->getValueInt('notifications', 'setting_batchtime', 0) !== 0; + } + /** * Count how many push devices (from the notifications app) are registered for a user. * Returns 0 if the notifications app is not installed or the query fails. diff --git a/lib/Service/PermissionService.php b/lib/Service/PermissionService.php index 3850e056..29e0a249 100644 --- a/lib/Service/PermissionService.php +++ b/lib/Service/PermissionService.php @@ -251,6 +251,8 @@ public function currentUserHasPermission(string $permission): bool { /** * Get all available groups for permission configuration + * + * @return list */ public function getAvailableGroups(): array { $allGroups = $this->groupManager->search(''); diff --git a/lib/Service/SelfCheckinService.php b/lib/Service/SelfCheckinService.php index c61ebb53..db958e69 100644 --- a/lib/Service/SelfCheckinService.php +++ b/lib/Service/SelfCheckinService.php @@ -16,6 +16,9 @@ /** * Service for self-check-in operations. * Handles finding active appointments and performing self-check-in for users. + * + * @psalm-import-type AttendanceSelfCheckinAppointment from \OCA\Attendance\ResponseDefinitions + * @psalm-import-type AttendanceSelfCheckinNextUpcoming from \OCA\Attendance\ResponseDefinitions */ class SelfCheckinService { use DatetimeFormatTrait; @@ -57,7 +60,7 @@ public function __construct( * - the user is a target attendee (via visibility settings) * * @param string $userId The user ID - * @return array{appointments: list>, nextUpcoming: ?array{id: int, name: string, startDatetime: string, isAllDay: bool, checkinWindowStartsAt: string}} + * @return array{appointments: list, nextUpcoming: ?AttendanceSelfCheckinNextUpcoming} */ public function getOverview(string $userId): array { $windowMinutes = $this->configService->getSelfCheckinWindowMinutes(); @@ -70,7 +73,6 @@ public function getOverview(string $userId): array { continue; } - /** @var array $appointmentData */ $appointmentData = $appointment->jsonSerialize(); // Check if user is already checked in @@ -86,6 +88,7 @@ public function getOverview(string $userId): array { $appointmentData['checkinAt'] = null; } + /** @var AttendanceSelfCheckinAppointment $appointmentData */ $result[] = $appointmentData; } @@ -103,7 +106,7 @@ public function getOverview(string $userId): array { * Find the next visible appointment whose check-in window has not opened * yet, so clients can show "check-in opens at …" when nothing matches now. * - * @return ?array{id: int, name: string, startDatetime: string, isAllDay: bool, checkinWindowStartsAt: string} + * @return ?AttendanceSelfCheckinNextUpcoming */ private function findNextUpcoming(string $userId, int $windowMinutes): ?array { foreach ($this->appointmentMapper->findUpcomingOutsideWindow($windowMinutes) as $appointment) { diff --git a/openapi-administration.json b/openapi-administration.json index bc5cf9c1..68127a5f 100644 --- a/openapi-administration.json +++ b/openapi-administration.json @@ -57,6 +57,7 @@ "reminders", "calendarSync", "orgCalendar", + "vacationCalendar", "audit", "displayOrder", "pushEnabled", @@ -64,7 +65,8 @@ "bookingEnabled", "allowMaybe", "selfCheckinWindowMinutes", - "guestsApp" + "guestsApp", + "notificationsApp" ], "properties": { "whitelistedGroups": { @@ -97,6 +99,9 @@ "orgCalendar": { "$ref": "#/components/schemas/AdminOrgCalendarConfig" }, + "vacationCalendar": { + "$ref": "#/components/schemas/AdminVacationCalendarConfig" + }, "audit": { "$ref": "#/components/schemas/AdminAuditConfig" }, @@ -121,6 +126,9 @@ }, "guestsApp": { "$ref": "#/components/schemas/GuestsAppStatus" + }, + "notificationsApp": { + "$ref": "#/components/schemas/NotificationsAppStatus" } } }, @@ -212,6 +220,27 @@ } } }, + "AdminVacationCalendarConfig": { + "type": "object", + "required": [ + "enabled", + "calendarUri", + "userId" + ], + "properties": { + "enabled": { + "type": "boolean" + }, + "calendarUri": { + "type": "string", + "nullable": true + }, + "userId": { + "type": "string", + "nullable": true + } + } + }, "AudienceRef": { "type": "object", "required": [ @@ -558,6 +587,21 @@ } } }, + "NotificationsAppStatus": { + "type": "object", + "required": [ + "emailForwardingEnabled", + "emailHintDismissed" + ], + "properties": { + "emailForwardingEnabled": { + "type": "boolean" + }, + "emailHintDismissed": { + "type": "boolean" + } + } + }, "PermissionSetting": { "type": "object", "required": [ @@ -593,13 +637,17 @@ "type": "object", "required": [ "id", - "displayName" + "label", + "type" ], "properties": { "id": { "type": "string" }, - "displayName": { + "label": { + "type": "string" + }, + "type": { "type": "string" } } @@ -984,6 +1032,12 @@ "nullable": true, "default": null, "description": "Whether the setup wizard has been walked to its end" + }, + "notificationEmailHintDismissed": { + "type": "boolean", + "nullable": true, + "default": null, + "description": "Whether the admin collapsed the hint that notifications do not reach email" } } } @@ -1201,6 +1255,24 @@ } } } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + } + } + } + } + } } } } diff --git a/openapi-full.json b/openapi-full.json index 33374ed4..00f228e1 100644 --- a/openapi-full.json +++ b/openapi-full.json @@ -57,6 +57,7 @@ "reminders", "calendarSync", "orgCalendar", + "vacationCalendar", "audit", "displayOrder", "pushEnabled", @@ -64,7 +65,8 @@ "bookingEnabled", "allowMaybe", "selfCheckinWindowMinutes", - "guestsApp" + "guestsApp", + "notificationsApp" ], "properties": { "whitelistedGroups": { @@ -97,6 +99,9 @@ "orgCalendar": { "$ref": "#/components/schemas/AdminOrgCalendarConfig" }, + "vacationCalendar": { + "$ref": "#/components/schemas/AdminVacationCalendarConfig" + }, "audit": { "$ref": "#/components/schemas/AdminAuditConfig" }, @@ -121,6 +126,9 @@ }, "guestsApp": { "$ref": "#/components/schemas/GuestsAppStatus" + }, + "notificationsApp": { + "$ref": "#/components/schemas/NotificationsAppStatus" } } }, @@ -212,6 +220,27 @@ } } }, + "AdminVacationCalendarConfig": { + "type": "object", + "required": [ + "enabled", + "calendarUri", + "userId" + ], + "properties": { + "enabled": { + "type": "boolean" + }, + "calendarUri": { + "type": "string", + "nullable": true + }, + "userId": { + "type": "string", + "nullable": true + } + } + }, "AppointmentData": { "type": "object", "required": [ @@ -1310,8 +1339,6 @@ "name", "startDatetime", "isAllDay", - "seriesId", - "seriesPosition", "userResponse", "closedAt", "cancelledAt", @@ -1331,15 +1358,6 @@ "isAllDay": { "type": "boolean" }, - "seriesId": { - "type": "string", - "nullable": true - }, - "seriesPosition": { - "type": "integer", - "format": "int64", - "nullable": true - }, "userResponse": { "type": "object", "nullable": true, @@ -1365,6 +1383,21 @@ } } }, + "NotificationsAppStatus": { + "type": "object", + "required": [ + "emailForwardingEnabled", + "emailHintDismissed" + ], + "properties": { + "emailForwardingEnabled": { + "type": "boolean" + }, + "emailHintDismissed": { + "type": "boolean" + } + } + }, "OnboardingState": { "type": "object", "required": [ @@ -1536,9 +1569,7 @@ "checkinSource", "bookingStatus", "userName", - "userGroups", - "isGuest", - "waitlisted" + "userGroups" ], "properties": { "id": { @@ -2377,13 +2408,17 @@ "type": "object", "required": [ "id", - "displayName" + "label", + "type" ], "properties": { "id": { "type": "string" }, - "displayName": { + "label": { + "type": "string" + }, + "type": { "type": "string" } } @@ -4345,6 +4380,24 @@ } } } + }, + "400": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + } + } + } + } + } } } } @@ -4655,6 +4708,24 @@ } } } + }, + "404": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + } + } + } + } + } } } } @@ -5160,6 +5231,24 @@ } } } + }, + "400": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + } + } + } + } + } } } } @@ -9396,6 +9485,12 @@ "nullable": true, "default": null, "description": "Whether the setup wizard has been walked to its end" + }, + "notificationEmailHintDismissed": { + "type": "boolean", + "nullable": true, + "default": null, + "description": "Whether the admin collapsed the hint that notifications do not reach email" } } } @@ -9613,6 +9708,24 @@ } } } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + } + } + } + } + } } } } diff --git a/openapi.json b/openapi.json index 61ed95d2..86632350 100644 --- a/openapi.json +++ b/openapi.json @@ -1026,8 +1026,6 @@ "name", "startDatetime", "isAllDay", - "seriesId", - "seriesPosition", "userResponse", "closedAt", "cancelledAt", @@ -1047,15 +1045,6 @@ "isAllDay": { "type": "boolean" }, - "seriesId": { - "type": "string", - "nullable": true - }, - "seriesPosition": { - "type": "integer", - "format": "int64", - "nullable": true - }, "userResponse": { "type": "object", "nullable": true, @@ -1221,9 +1210,7 @@ "checkinSource", "bookingStatus", "userName", - "userGroups", - "isGuest", - "waitlisted" + "userGroups" ], "properties": { "id": { @@ -3980,6 +3967,24 @@ } } } + }, + "400": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + } + } + } + } + } } } } @@ -4290,6 +4295,24 @@ } } } + }, + "404": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + } + } + } + } + } } } } @@ -4795,6 +4818,24 @@ } } } + }, + "400": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + } + } + } + } + } } } } diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 3f519f68..1591840f 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,5 +1,5 @@ - + @@ -134,13 +134,6 @@ - - |DataResponse|DataResponse|DataResponse]]> - , writableCalendars: list}, array{}>|DataResponse|DataResponse|DataResponse]]> - , writableCalendars: list}, array{}>|DataResponse|DataResponse|DataResponse]]> - , writableCalendars: list}, array{}>|DataResponse|DataResponse|DataResponse]]> - , writableCalendars: list}, array{}>|DataResponse|DataResponse|DataResponse]]> - @@ -162,140 +155,8 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |DataResponse|DataResponse|DataResponse]]> - |DataResponse|DataResponse|DataResponse]]> - |DataResponse|DataResponse|DataResponse]]> - |DataResponse|DataResponse|DataResponse]]> - |DataResponse|DataResponse|DataResponse]]> - |DataResponse|DataResponse|DataResponse|DataResponse]]> - |DataResponse|DataResponse|DataResponse|DataResponse]]> - , array{}>|DataResponse|DataResponse|DataResponse|DataResponse]]> - |DataResponse|DataResponse]]> - ]]> - |DataResponse|DataResponse|DataResponse]]> - |DataResponse|DataResponse|DataResponse|DataResponse]]> - |DataResponse|DataResponse|DataResponse]]> - |DataResponse|DataResponse|DataResponse|DataResponse]]> - ]]> - |DataResponse|DataResponse|DataResponse|DataResponse]]> - |DataResponse|DataResponse|DataResponse]]> - |DataResponse|DataResponse]]> - |DataResponse|DataResponse|DataResponse]]> - |DataResponse|DataResponse|DataResponse|DataResponse]]> - |DataResponse|DataResponse|DataResponse|DataResponse]]> - |DataResponse|DataResponse|DataResponse|DataResponse]]> ]]> - |DataResponse]]> - , past: list, pastHasMore: bool}, array{}>|DataResponse]]> - , array{}>|DataResponse|DataResponse]]> - |AttendanceAppointmentPage, array{}>|DataResponse]]> - , array{}>|DataResponse|DataResponse|DataResponse]]> - ]]> @@ -305,16 +166,6 @@ - - - - - - - - - |DataResponse|DataResponse|DataResponse|DataResponse]]> - @@ -381,14 +232,6 @@ - - - - - - |DataResponse|DataResponse|DataResponse]]> - |DataResponse|DataResponse|DataResponse|DataResponse]]> - @@ -410,29 +253,6 @@ - - - - - - - - - - - - - - - - - - |DataResponse|DataResponse]]> - |DataResponse]]> - |DataResponse|DataResponse|DataResponse]]> - , array{}>|DataResponse]]> - , array{}>|DataResponse|DataResponse]]> - @@ -682,9 +502,6 @@ - - - @@ -721,8 +538,6 @@ - - @@ -1370,11 +1185,6 @@ - - - - - @@ -1448,31 +1258,15 @@ - - - - - - - - - - - - - - - - getComment()]]> @@ -1638,8 +1432,6 @@ - - getCheckinComment()]]> @@ -1704,18 +1496,14 @@ - - - - @@ -1745,12 +1533,6 @@ - - - - - - @@ -1792,7 +1574,6 @@ - @@ -1825,8 +1606,6 @@ - - @@ -1869,10 +1648,7 @@ - - - @@ -1925,14 +1701,8 @@ - - - - - - @@ -2102,8 +1872,6 @@ - - @@ -2140,11 +1908,7 @@ - - - - @@ -2165,10 +1929,6 @@ - - - - @@ -2205,7 +1965,6 @@ - diff --git a/src/views/AdminSettings.vue b/src/views/AdminSettings.vue index 7496e297..8c416dbe 100644 --- a/src/views/AdminSettings.vue +++ b/src/views/AdminSettings.vue @@ -283,6 +283,40 @@