{{ t("attendance", "Inquiry closed") }}
@@ -321,7 +329,7 @@
@@ -467,6 +475,7 @@ import {
} from '@nextcloud/vue'
import { computed, nextTick, ref, watch } from 'vue'
import BellRingIcon from 'vue-material-design-icons/BellRing.vue'
+import AccountStarIcon from 'vue-material-design-icons/AccountStar.vue'
import CalendarRefreshIcon from 'vue-material-design-icons/CalendarRefresh.vue'
import CalendarRemoveIcon from 'vue-material-design-icons/CalendarRemove.vue'
import CalendarSyncIcon from 'vue-material-design-icons/CalendarSync.vue'
@@ -548,9 +557,7 @@ const emit = defineEmits([
'openDetail',
])
-const { capabilities } = usePermissions()
-
-const currentUserUid = window.OC?.getCurrentUser?.()?.uid || window.OC?.currentUser || null
+const { capabilities, permissions } = usePermissions()
const localComment = ref(props.appointment.userResponse?.comment || '')
const commentExpanded = ref(false)
@@ -575,10 +582,14 @@ const userResponse = computed(() => {
const isClosed = computed(() => Boolean(props.appointment.closedAt))
-const canToggleClosed = computed(() => {
- if (props.canManageAppointments) return true
- return Boolean(currentUserUid) && props.appointment.createdBy === currentUserUid
-})
+// Per-appointment permissions from the server; global props act as fallback
+// so older payloads (without myPermissions) keep working.
+const myPermissions = computed(() => props.appointment.myPermissions || {})
+const canManageThis = computed(() => props.canManageAppointments || myPermissions.value.canEdit === true)
+const canSeeResponsesThis = computed(() => props.canSeeResponseOverview || myPermissions.value.canSeeResponses === true)
+const canSeeCommentsThis = computed(() => props.canSeeComments || myPermissions.value.canSeeComments === true)
+const canSeeAuditLogThis = computed(() => props.canSeeAuditLog
+ || myPermissions.value.canSeeAuditLog === true)
const isCancelled = computed(() => Boolean(props.appointment.cancelledAt))
@@ -606,9 +617,14 @@ const pendingChipText = computed(() =>
// TRANSLATORS: Check-in status chip — no check-in recorded for the person yet. See "{count} attended".
t('attendance', '{count} pending', { count: props.appointment.checkinSummary?.notCheckedIn ?? 0 }))
+const organizerNames = computed(() => (props.appointment.organizers || [])
+ .map((organizer) => organizer.label || organizer.id)
+ .filter(Boolean)
+ .join(', '))
+
// Cancelling is a manager/creator action gated behind the server capability, so
// instances (and older servers) that don't offer it never show the UI.
-const canCancel = computed(() => capabilities.cancelling && canToggleClosed.value)
+const canCancel = computed(() => capabilities.cancelling && canManageThis.value)
const formattedClosedAt = computed(() => props.appointment.closedAt ? formatDateTime(props.appointment.closedAt) : '')
@@ -1076,13 +1092,14 @@ function handleCommentInputEvent() {
}
:deep(h1) {
- font-size: 1.5em;
+ font-size: 1.75em;
+ font-weight: 700;
}
:deep(h2) {
- font-size: 1.3em;
+ font-size: 1.45em;
}
:deep(h3) {
- font-size: 1.15em;
+ font-size: 1.2em;
}
:deep(h4) {
font-size: 1.05em;
@@ -1255,6 +1272,15 @@ function handleCommentInputEvent() {
font-size: 0.9em;
}
+.organizer-info {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ margin-bottom: 12px;
+ color: var(--color-text-maxcontrast);
+ font-size: 0.9em;
+}
+
.response-section--readonly {
display: flex;
flex-direction: column;
diff --git a/src/components/common/MarkdownEditor.vue b/src/components/common/MarkdownEditor.vue
index a2896d7a..cc49dfd8 100644
--- a/src/components/common/MarkdownEditor.vue
+++ b/src/components/common/MarkdownEditor.vue
@@ -207,6 +207,18 @@ onBeforeUnmount(() => {
pre, span {
cursor: text;
}
+
+ // Tame EasyMDE's viewport-based heading sizes (calc(1.375rem + 1.5vw)
+ // and friends) — match the scale used when rendering descriptions.
+ .cm-header-1 {
+ font-size: 1.75em;
+ }
+ .cm-header-2 {
+ font-size: 1.45em;
+ }
+ .cm-header-3 {
+ font-size: 1.2em;
+ }
}
.CodeMirror-placeholder {
@@ -236,6 +248,12 @@ onBeforeUnmount(() => {
cursor: pointer;
color: var(--color-main-text);
+ // EasyMDE appends "1"/"2"/"3" to the heading buttons for its
+ // FontAwesome icons — our SVG icons already carry the digit.
+ &::after {
+ content: none;
+ }
+
svg {
width: 20px;
height: 20px;
diff --git a/src/composables/usePermissions.js b/src/composables/usePermissions.js
index f1f2757e..84f72ced 100644
--- a/src/composables/usePermissions.js
+++ b/src/composables/usePermissions.js
@@ -5,6 +5,7 @@ import { reactive, readonly } from 'vue'
const state = reactive({
permissions: {
canManageAppointments: false,
+ canCreateAppointments: false,
canCheckin: false,
canSeeResponseOverview: false,
canSeeComments: false,
@@ -20,6 +21,7 @@ const state = reactive({
auditLog: false,
cancelling: false,
bookingEnabled: false,
+ organizers: false,
},
config: {
displayOrder: 'name_first',
@@ -62,6 +64,7 @@ export function usePermissions() {
])
state.permissions.canManageAppointments = permissionsRes.data.canManageAppointments || false
+ state.permissions.canCreateAppointments = permissionsRes.data.canCreateAppointments || false
state.permissions.canCheckin = permissionsRes.data.canCheckin || false
state.permissions.canSeeResponseOverview = permissionsRes.data.canSeeResponseOverview || false
state.permissions.canSeeComments = permissionsRes.data.canSeeComments || false
@@ -76,6 +79,7 @@ export function usePermissions() {
state.capabilities.auditLog = capabilitiesRes.data.auditLog === true
state.capabilities.cancelling = capabilitiesRes.data.cancelling === true
state.capabilities.bookingEnabled = capabilitiesRes.data.bookingEnabled === true
+ state.capabilities.organizers = capabilitiesRes.data.organizers === true
state.config.displayOrder = configRes.data.displayOrder || 'name_first'
state.config.mobileAppBannerEnabled = configRes.data.mobileAppBannerEnabled !== false
@@ -87,6 +91,7 @@ export function usePermissions() {
state.error = error
state.permissions.canManageAppointments = false
+ state.permissions.canCreateAppointments = false
state.permissions.canCheckin = false
state.permissions.canSeeResponseOverview = false
state.permissions.canSeeComments = false
@@ -99,6 +104,7 @@ export function usePermissions() {
state.capabilities.auditLog = false
state.capabilities.cancelling = false
state.capabilities.bookingEnabled = false
+ state.capabilities.organizers = false
state.config.displayOrder = 'name_first'
state.config.mobileAppBannerEnabled = true
state.config.hasPushDevice = false
@@ -118,6 +124,7 @@ export function usePermissions() {
state.loading = false
state.error = null
state.permissions.canManageAppointments = false
+ state.permissions.canCreateAppointments = false
state.permissions.canCheckin = false
state.permissions.canSeeResponseOverview = false
state.permissions.canSeeComments = false
diff --git a/src/views/AdminSettings.vue b/src/views/AdminSettings.vue
index 84899674..5a6e0b1d 100644
--- a/src/views/AdminSettings.vue
+++ b/src/views/AdminSettings.vue
@@ -76,6 +76,22 @@
+
+
{{ t('attendance', 'Create own appointments') }}
+
+ {{ t('attendance', 'Groups that can create appointments and manage them as organizers, in addition to the groups above. If no group is selected, only the groups above can create appointments.') }}
+
+
+
+ {{ n('attendance', '%n group selected', '%n groups selected', selectedCreateAppointmentsRoles.length, { n: selectedCreateAppointmentsRoles.length }) }}
+
+
+
{{ t('attendance', 'Check-in access') }}
@@ -634,6 +650,7 @@ const teamSearchResults = ref([])
const isSearchingTeams = ref(false)
const teamsAvailable = ref(false)
const selectedManageAppointmentsRoles = ref([])
+const selectedCreateAppointmentsRoles = ref([])
const selectedCheckinRoles = ref([])
const selectedSeeResponseOverviewRoles = ref([])
const selectedSeeCommentsRoles = ref([])
@@ -771,6 +788,9 @@ async function loadSettings() {
selectedSelfCheckinRoles.value = (config.permissions.self_checkin || [])
.map((id) => groups.find((group) => group.id === id))
.filter((group) => group !== undefined)
+ selectedCreateAppointmentsRoles.value = (config.permissions.create_appointments || [])
+ .map((id) => groups.find((group) => group.id === id))
+ .filter((group) => group !== undefined)
}
selfCheckinWindowMinutes.value = config.selfCheckinWindowMinutes ?? 30
@@ -863,6 +883,7 @@ async function saveSettings() {
PERMISSION_SEE_RESPONSE_OVERVIEW: selectedSeeResponseOverviewRoles.value.map((g) => g.id),
PERMISSION_SEE_COMMENTS: selectedSeeCommentsRoles.value.map((g) => g.id),
PERMISSION_SELF_CHECKIN: selectedSelfCheckinRoles.value.map((g) => g.id),
+ PERMISSION_CREATE_APPOINTMENTS: selectedCreateAppointmentsRoles.value.map((g) => g.id),
},
reminders: {
enabled: remindersEnabled.value,
diff --git a/src/views/AppointmentDetail.vue b/src/views/AppointmentDetail.vue
index e8e17327..1a788ae5 100644
--- a/src/views/AppointmentDetail.vue
+++ b/src/views/AppointmentDetail.vue
@@ -100,6 +100,11 @@ const showDeleteDialog = ref(false)
const { permissions, capabilities, config, loadPermissions } = usePermissions()
const canSeeAuditTimeline = computed(() => {
+ // Per-appointment verdict from the server (knows the audit visibility
+ // mode); the global flags remain as fallback for the initial render.
+ if (appointment.value?.myPermissions) {
+ return appointment.value.myPermissions.canSeeAuditLog === true
+ }
if (!capabilities.auditLog) return false
return permissions.canManageAppointments || permissions.canSeeResponseOverview
})
diff --git a/src/views/AppointmentForm.vue b/src/views/AppointmentForm.vue
index 5ac87d84..6045c527 100644
--- a/src/views/AppointmentForm.vue
+++ b/src/views/AppointmentForm.vue
@@ -388,6 +388,42 @@
+
+