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 @@