Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion l10n/de.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);");
8 changes: 7 additions & 1 deletion l10n/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -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);"
}
8 changes: 7 additions & 1 deletion l10n/de_DE.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);");
8 changes: 7 additions & 1 deletion l10n/de_DE.json
Original file line number Diff line number Diff line change
Expand Up @@ -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);"
}
34 changes: 26 additions & 8 deletions lib/Controller/AdminController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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();
Expand All @@ -131,16 +138,20 @@ 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);
}
}

$pushDeviceCount = $this->notificationService->countPushDevices($user->getUID());

$notificationsApp = [
'emailForwardingEnabled' => $this->notificationService->isEmailForwardingEnabledByDefault(),
'emailHintDismissed' => $this->configService->isNotificationEmailHintDismissed(),
];

return new DataResponse([
'config' => [
'whitelistedGroups' => $whitelistedGroups,
Expand All @@ -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' => [
Expand Down Expand Up @@ -183,6 +194,7 @@ public function getSettings(): DataResponse {
'whitelistEnabled' => $this->guestService->isGuestsWhitelistEnabled(),
'attendanceInWhitelist' => $this->guestService->isAttendanceInGuestsWhitelist(),
],
'notificationsApp' => $notificationsApp,
],
'status' => [
'nextAppointment' => $nextAppointment,
Expand Down Expand Up @@ -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<Http::STATUS_OK, array<string, mixed>, array{}>|DataResponse<Http::STATUS_UNAUTHORIZED, array{error: string}, array{}>|DataResponse<Http::STATUS_FORBIDDEN, array{error: string}, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR, array{error: string}, array{}>
*/
#[NoCSRFRequired]
Expand All @@ -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();
Expand Down Expand Up @@ -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);
Expand All @@ -351,7 +369,7 @@ public function saveSettings(
*
* Uses the next upcoming appointment to send a preview reminder notification.
*
* @return DataResponse<Http::STATUS_OK, AttendanceTestReminderResult, array{}>|DataResponse<Http::STATUS_UNAUTHORIZED, array{error: string}, array{}>|DataResponse<Http::STATUS_FORBIDDEN, array{error: string}, array{}>|DataResponse<Http::STATUS_NOT_FOUND, array{error: string}, array{}>
* @return DataResponse<Http::STATUS_OK, AttendanceTestReminderResult, array{}>|DataResponse<Http::STATUS_UNAUTHORIZED, array{error: string}, array{}>|DataResponse<Http::STATUS_FORBIDDEN, array{error: string}, array{}>|DataResponse<Http::STATUS_NOT_FOUND, array{error: string}, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR, array{error: string}, array{}>
*/
#[NoCSRFRequired]
#[OpenAPI(OpenAPI::SCOPE_ADMINISTRATION)]
Expand Down
35 changes: 27 additions & 8 deletions lib/Controller/AppointmentController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<Http::STATUS_NOT_FOUND|Http::STATUS_FORBIDDEN, array{error: string}, array{}>
*/
private function findManageableAppointment(int $id, string $userId, string $permissionError): \OCA\Attendance\Db\Appointment|DataResponse {
try {
Expand Down Expand Up @@ -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)));
}

/**
Expand Down Expand Up @@ -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)));
}

/**
Expand Down Expand Up @@ -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<Http::STATUS_OK, AttendanceResponseData, array{}>|DataResponse<Http::STATUS_UNAUTHORIZED|Http::STATUS_FORBIDDEN|Http::STATUS_BAD_REQUEST|Http::STATUS_NOT_FOUND, array{error: string}, array{}>
*/
private function setBooking(int $id, string $userId, bool $book): DataResponse {
$user = $this->userSession->getUser();
Expand All @@ -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));
}

/**
Expand Down Expand Up @@ -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<Http::STATUS_OK, list<AttendanceResponseWithUser>, array{}>|DataResponse<Http::STATUS_UNAUTHORIZED, array{error: string}, array{}>|DataResponse<Http::STATUS_FORBIDDEN, array{error: string}, array{}>|DataResponse<Http::STATUS_NOT_FOUND, array{error: string}, array{}>
* @return DataResponse<Http::STATUS_OK, list<AttendanceResponseWithUser>, array{}>|DataResponse<Http::STATUS_UNAUTHORIZED, array{error: string}, array{}>|DataResponse<Http::STATUS_FORBIDDEN, array{error: string}, array{}>|DataResponse<Http::STATUS_NOT_FOUND, array{error: string}, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{error: string}, array{}>
*/
#[NoAdminRequired]
#[NoCSRFRequired]
Expand Down Expand Up @@ -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<Http::STATUS_OK, AttendanceResponseData, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{error: string}, array{}>|DataResponse<Http::STATUS_UNAUTHORIZED, array{error: string}, array{}>|DataResponse<Http::STATUS_FORBIDDEN, array{error: string}, array{}>
* @return DataResponse<Http::STATUS_OK, AttendanceResponseData, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{error: string}, array{}>|DataResponse<Http::STATUS_UNAUTHORIZED, array{error: string}, array{}>|DataResponse<Http::STATUS_FORBIDDEN, array{error: string}, array{}>|DataResponse<Http::STATUS_NOT_FOUND, array{error: string}, array{}>
*/
#[NoAdminRequired]
#[NoCSRFRequired]
Expand All @@ -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);
}
Expand All @@ -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);
}
Expand Down Expand Up @@ -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<Http::STATUS_OK, AttendanceReminderResult, array{}>|DataResponse<Http::STATUS_UNAUTHORIZED, array{error: string}, array{}>|DataResponse<Http::STATUS_FORBIDDEN, array{error: string}, array{}>|DataResponse<Http::STATUS_NOT_FOUND, array{error: string}, array{}>
* @return DataResponse<Http::STATUS_OK, AttendanceReminderResult, array{}>|DataResponse<Http::STATUS_UNAUTHORIZED, array{error: string}, array{}>|DataResponse<Http::STATUS_FORBIDDEN, array{error: string}, array{}>|DataResponse<Http::STATUS_NOT_FOUND, array{error: string}, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{error: string}, array{}>
*/
#[NoAdminRequired]
#[NoCSRFRequired]
Expand Down
Loading
Loading