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 @@ -695,6 +695,12 @@ OC.L10N.register(
"{actor} cancelled the appointment" : "{actor} hat diesen Termin abgesagt",
"{actor} reactivated the appointment" : "{actor} hat diesen Termin reaktiviert",
"{actor} performed {verb}" : "{actor} hat {verb} durchgeführt",
"Guests" : "Gäste"
"Guests" : "Gäste",
"Heading 1" : "Überschrift 1",
"Heading 2" : "Überschrift 2",
"Heading 3" : "Überschrift 3",
"Show details" : "Details anzeigen",
"Some users may appear in the section \"Others\" in the response summary because they are not configured for tracking." : "Einige Personen erscheinen möglicherweise im Abschnitt \"Andere\" in der Antwortübersicht, weil sie nicht für die Erfassung konfiguriert sind.",
"year" : "Jahr"
},
"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 @@ -693,6 +693,12 @@
"{actor} cancelled the appointment" : "{actor} hat diesen Termin abgesagt",
"{actor} reactivated the appointment" : "{actor} hat diesen Termin reaktiviert",
"{actor} performed {verb}" : "{actor} hat {verb} durchgeführt",
"Guests" : "Gäste"
"Guests" : "Gäste",
"Heading 1" : "Überschrift 1",
"Heading 2" : "Überschrift 2",
"Heading 3" : "Überschrift 3",
"Show details" : "Details anzeigen",
"Some users may appear in the section \"Others\" in the response summary because they are not configured for tracking." : "Einige Personen erscheinen möglicherweise im Abschnitt \"Andere\" in der Antwortübersicht, weil sie nicht für die Erfassung konfiguriert sind.",
"year" : "Jahr"
},"pluralForm" :"nplurals=2; plural=(n != 1);"
}
7 changes: 6 additions & 1 deletion l10n/de_DE.js
Original file line number Diff line number Diff line change
Expand Up @@ -696,6 +696,11 @@ OC.L10N.register(
"{actor} cancelled the appointment" : "{actor} hat diesen Termin abgesagt",
"{actor} reactivated the appointment" : "{actor} hat diesen Termin reaktiviert",
"{actor} performed {verb}" : "{actor} hat {verb} durchgeführt",
"Guests" : "Gäste"
"Guests" : "Gäste",
"Heading 1" : "Überschrift 1",
"Heading 2" : "Überschrift 2",
"Heading 3" : "Überschrift 3",
"Show details" : "Details anzeigen",
"Some users may appear in the section \"Others\" in the response summary because they are not configured for tracking." : "Einige Personen erscheinen möglicherweise im Abschnitt \"Andere\" in der Antwortübersicht, weil sie nicht für die Erfassung konfiguriert sind."
},
"nplurals=2; plural=(n != 1);");
7 changes: 6 additions & 1 deletion l10n/de_DE.json
Original file line number Diff line number Diff line change
Expand Up @@ -694,6 +694,11 @@
"{actor} cancelled the appointment" : "{actor} hat diesen Termin abgesagt",
"{actor} reactivated the appointment" : "{actor} hat diesen Termin reaktiviert",
"{actor} performed {verb}" : "{actor} hat {verb} durchgeführt",
"Guests" : "Gäste"
"Guests" : "Gäste",
"Heading 1" : "Überschrift 1",
"Heading 2" : "Überschrift 2",
"Heading 3" : "Überschrift 3",
"Show details" : "Details anzeigen",
"Some users may appear in the section \"Others\" in the response summary because they are not configured for tracking." : "Einige Personen erscheinen möglicherweise im Abschnitt \"Andere\" in der Antwortübersicht, weil sie nicht für die Erfassung konfiguriert sind."
},"pluralForm" :"nplurals=2; plural=(n != 1);"
}
1 change: 1 addition & 0 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@
@navigateToUpcoming="setView('current')"
@navigateToUnanswered="setView('unanswered')"
@showAuditLog="openAuditLog"
@openDetail="navigateToAppointment"
@clearSearch="searchQuery = ''" />

<!-- Loading state while routing is determined -->
Expand Down
168 changes: 106 additions & 62 deletions src/components/appointment/AppointmentCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,62 +2,49 @@
<div class="appointment-card" data-test="appointment-card">
<div class="appointment-header">
<div class="appointment-title-block">
<template v-if="displayOrder === 'date_first'">
<h3 data-test="appointment-title" class="appointment-date-title">
{{
formatDateRange(
appointment.startDatetime,
appointment.endDatetime,
)
}}
<a
v-if="calendarLink"
:href="calendarLink"
target="_blank"
rel="noopener noreferrer"
class="calendar-link"
:title="t('attendance', 'Imported from calendar')">
<CalendarSyncIcon :size="14" />
</a>
<span
v-if="appointment.seriesId"
class="series-indicator"
:title="t('attendance', 'Part of a recurring series')">
<RepeatIcon :size="14" />
</span>
</h3>
<span class="appointment-date-subtitle">
{{ appointment.name }}
</span>
</template>
<template v-else>
<h3 data-test="appointment-title">
{{ appointment.name }}
<span
v-if="appointment.seriesId"
class="series-indicator"
:title="t('attendance', 'Part of a recurring series')">
<RepeatIcon :size="14" />
</span>
</h3>
<span class="appointment-date-subtitle">
{{
formatDateRange(
appointment.startDatetime,
appointment.endDatetime,
)
}}
<a
v-if="calendarLink"
:href="calendarLink"
target="_blank"
rel="noopener noreferrer"
class="calendar-link"
:title="t('attendance', 'Imported from calendar')">
<CalendarSyncIcon :size="14" />
</a>
<h3
data-test="appointment-title"
:class="{ 'appointment-date-title': displayOrder === 'date_first' }">
<a
v-if="isListVariant"
:href="detailUrl"
class="title-link"
data-test="appointment-title-link"
@click.prevent="emit('openDetail', appointment.id)">
{{ titleText }}
<ChevronRightIcon :size="18" class="title-chevron" />
</a>
<template v-else>
{{ titleText }}
</template>
<a
v-if="displayOrder === 'date_first' && calendarLink"
:href="calendarLink"
target="_blank"
rel="noopener noreferrer"
class="calendar-link"
:title="t('attendance', 'Imported from calendar')">
<CalendarSyncIcon :size="14" />
</a>
<span
v-if="appointment.seriesId"
class="series-indicator"
:title="t('attendance', 'Part of a recurring series')">
<RepeatIcon :size="14" />
</span>
</template>
</h3>
<span class="appointment-date-subtitle">
{{ subtitleText }}
<a
v-if="displayOrder !== 'date_first' && calendarLink"
:href="calendarLink"
target="_blank"
rel="noopener noreferrer"
class="calendar-link"
:title="t('attendance', 'Imported from calendar')">
<CalendarSyncIcon :size="14" />
</a>
</span>
<NcChip
v-if="isCancelled"
class="cancelled-badge"
Expand Down Expand Up @@ -188,7 +175,7 @@

<!-- eslint-disable vue/no-v-html -- sanitized with DOMPurify -->
<div
v-if="appointment.description"
v-if="!isListVariant && appointment.description"
class="appointment-description"
v-html="renderedDescription" />
<!-- eslint-enable vue/no-v-html -->
Expand All @@ -213,6 +200,18 @@
</a>
</div>

<div v-if="isListVariant" class="details-link-row">
<NcButton
variant="tertiary"
data-test="button-show-details"
@click="emit('openDetail', appointment.id)">
<template #icon>
<ChevronRightIcon :size="20" />
</template>
{{ t("attendance", "Show details") }}
</NcButton>
</div>

<!-- Read-only response chip while the inquiry is closed -->
<div
v-if="isClosed"
Expand Down Expand Up @@ -489,6 +488,7 @@ 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'
import CheckIcon from 'vue-material-design-icons/Check.vue'
import ChevronRightIcon from 'vue-material-design-icons/ChevronRight.vue'
import ClockIcon from 'vue-material-design-icons/Clock.vue'
import CloseIcon from 'vue-material-design-icons/Close.vue'
import CloseCircle from 'vue-material-design-icons/CloseCircle.vue'
Expand All @@ -508,7 +508,7 @@ import ShareVariantIcon from 'vue-material-design-icons/ShareVariant.vue'
import ResponseSummary from './ResponseSummary.vue'
import { useAppointmentResponse, useResponseCooldown } from '../../composables/useAppointmentResponse.js'
import { usePermissions } from '../../composables/usePermissions.js'
import { formatClosedLabel } from '../../utils/appointment.js'
import { appointmentDetailUrl, formatClosedLabel } from '../../utils/appointment.js'
import { copyToClipboard } from '../../utils/clipboard.js'
import { formatDateRange, formatDateTime } from '../../utils/datetime.js'
import { renderMarkdown, sanitizeHtml } from '../../utils/markdown.js'
Expand Down Expand Up @@ -543,6 +543,13 @@ const props = defineProps({
type: Boolean,
default: false,
},
// 'detail' renders the full card (description, no navigation);
// 'list' hides the description and links to the detail page instead.
variant: {
type: String,
default: 'detail',
validator: (value) => ['detail', 'list'].includes(value),
},
})

const emit = defineEmits([
Expand All @@ -555,6 +562,7 @@ const emit = defineEmits([
'updateComment',
'closedToggled',
'showAuditLog',
'openDetail',
])

const { capabilities } = usePermissions()
Expand Down Expand Up @@ -609,6 +617,16 @@ const renderedDescription = computed(() => {
return sanitizeHtml(html)
})

const isListVariant = computed(() => props.variant === 'list')

const dateRangeText = computed(() => formatDateRange(props.appointment.startDatetime, props.appointment.endDatetime))

const titleText = computed(() => (props.displayOrder === 'date_first' ? dateRangeText.value : props.appointment.name))

const subtitleText = computed(() => (props.displayOrder === 'date_first' ? props.appointment.name : dateRangeText.value))

const detailUrl = computed(() => appointmentDetailUrl(props.appointment.id))

const calendarLink = computed(() => {
if (
!props.appointment.calendarUri
Expand Down Expand Up @@ -655,9 +673,7 @@ watch(
)

function copyShareLink() {
const appointmentUrl
= window.location.origin
+ generateUrl(`/apps/attendance/appointment/${props.appointment.id}`)
const appointmentUrl = window.location.origin + detailUrl.value
return copyToClipboard(appointmentUrl, {
successMessage: t('attendance', 'Link copied to clipboard'),
})
Expand Down Expand Up @@ -905,6 +921,30 @@ function handleCommentInputEvent() {
color: var(--color-main-text);
}

.title-link {
display: inline-flex;
align-items: center;
color: inherit;
text-decoration: none;

&:hover,
&:focus-visible {
text-decoration: underline;

.title-chevron {
transform: translateX(2px);
}
}
}

.title-chevron {
display: inline-flex;
align-items: center;
margin-left: 2px;
color: var(--color-text-maxcontrast);
transition: transform 0.15s ease;
}

.appointment-date-subtitle {
display: block;
font-size: 15px;
Expand Down Expand Up @@ -935,6 +975,10 @@ function handleCommentInputEvent() {
}
}

.details-link-row {
margin-bottom: 15px;
}

.appointment-description {
color: var(--color-text-lighter);
margin-bottom: 15px;
Expand Down Expand Up @@ -1107,8 +1151,8 @@ function handleCommentInputEvent() {
}
}

.appointment-description + .response-section,
.attachment-chips + .response-section {
// Any content block between the header and the response section gets a divider
:not(.appointment-header) + .response-section {
border-top: 1px solid var(--color-border);
padding-top: 15px;
}
Expand Down
46 changes: 1 addition & 45 deletions src/components/checkin/CheckinAppointmentInfo.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,13 @@
{{ formatDateRange(appointment.startDatetime, appointment.endDatetime) }}
</p>
</template>
<!-- eslint-disable-next-line vue/no-v-html -- sanitized with DOMPurify -->
<div v-if="appointment.description" class="appointment-description" v-html="renderedDescription" />
</div>
</template>

<script setup>
import { computed } from 'vue'
import { formatDateRange } from '../../utils/datetime.js'
import { renderMarkdown, sanitizeHtml } from '../../utils/markdown.js'

const props = defineProps({
defineProps({
appointment: {
type: Object,
required: true,
Expand All @@ -32,11 +28,6 @@ const props = defineProps({
default: 'name_first',
},
})

const renderedDescription = computed(() => {
if (!props.appointment?.description) return ''
return sanitizeHtml(renderMarkdown(props.appointment.description, true))
})
</script>

<style scoped lang="scss">
Expand All @@ -56,40 +47,5 @@ const renderedDescription = computed(() => {
color: var(--color-text-maxcontrast);
font-size: 14px;
}

.appointment-description {
margin: 15px 0 0 0;
color: var(--color-main-text);

:deep(strong) {
font-weight: bold;
color: var(--color-main-text);
}

:deep(em) {
font-style: italic;
}

:deep(del) {
text-decoration: line-through;
}

:deep(a) {
color: var(--color-primary-element);
text-decoration: none;

&:hover {
text-decoration: underline;
}
}

:deep(code) {
background-color: var(--color-background-dark);
padding: 2px 6px;
border-radius: var(--border-radius-small);
font-family: monospace;
font-size: 0.9em;
}
}
}
</style>
Loading
Loading