mirror of https://github.com/nextcloud/calendar
260 lines
7.1 KiB
Vue
260 lines
7.1 KiB
Vue
<!--
|
|
- @copyright Copyright (c) 2021 Richard Steinmetz <richard@steinmetz.cloud>
|
|
-
|
|
- @author Richard Steinmetz <richard@steinmetz.cloud>
|
|
-
|
|
- @license GNU AGPL version 3 or any later version
|
|
-
|
|
- This program is free software: you can redistribute it and/or modify
|
|
- it under the terms of the GNU Affero General Public License as
|
|
- published by the Free Software Foundation, either version 3 of the
|
|
- License, or (at your option) any later version.
|
|
-
|
|
- This program is distributed in the hope that it will be useful,
|
|
- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
- GNU Affero General Public License for more details.
|
|
-
|
|
- You should have received a copy of the GNU Affero General Public License
|
|
- along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
-
|
|
-->
|
|
|
|
<template>
|
|
<div class="resource-search">
|
|
<Multiselect class="resource-search__multiselect"
|
|
:options="matches"
|
|
:searchable="true"
|
|
:internal-search="false"
|
|
:max-height="600"
|
|
:show-no-results="true"
|
|
:show-no-options="false"
|
|
:placeholder="placeholder"
|
|
:class="{ 'showContent': inputGiven, 'icon-loading': isLoading }"
|
|
open-direction="bottom"
|
|
track-by="email"
|
|
label="displayName"
|
|
@search-change="findResources"
|
|
@select="addResource">
|
|
<template #option="{ option }">
|
|
<div class="resource-search-list-item">
|
|
<Avatar :disable-tooltip="true"
|
|
:display-name="option.displayName" />
|
|
<div class="resource-search-list-item__label resource-search-list-item__label--single-email">
|
|
<div>
|
|
{{ option.displayName }}
|
|
<span v-if="!isAvailable"
|
|
class="resource-search-list-item__label__availability">
|
|
({{ formatAvailability(option.isAvailable) }})
|
|
</span>
|
|
</div>
|
|
<div :title="option.subLine">
|
|
{{ option.subLine }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</Multiselect>
|
|
|
|
<template>
|
|
<div class="resource-search__capacity">
|
|
<ResourceSeatingCapacity :value.sync="capacity" />
|
|
<Actions class="resource-search__capacity__actions">
|
|
<ActionCheckbox :checked.sync="isAvailable">
|
|
<!-- Translators room or resource is not yet booked -->
|
|
{{ $t('calendar', 'Available') }}
|
|
</ActionCheckbox>
|
|
<ActionCheckbox :checked.sync="hasProjector">
|
|
{{ $t('calendar', 'Projector') }}
|
|
</ActionCheckbox>
|
|
<ActionCheckbox :checked.sync="hasWhiteboard">
|
|
{{ $t('calendar', 'Whiteboard') }}
|
|
</ActionCheckbox>
|
|
<ActionCheckbox :checked.sync="isAccessible">
|
|
{{ $t('calendar', 'Wheelchair accessible') }}
|
|
</ActionCheckbox>
|
|
</Actions>
|
|
</div>
|
|
<ResourceRoomType :value.sync="roomType" />
|
|
</template>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import Avatar from '@nextcloud/vue/dist/Components/NcAvatar'
|
|
import { checkResourceAvailability } from '../../../services/freeBusyService'
|
|
import Multiselect from '@nextcloud/vue/dist/Components/NcMultiselect'
|
|
import debounce from 'debounce'
|
|
import logger from '../../../utils/logger'
|
|
import { advancedPrincipalPropertySearch } from '../../../services/caldavService'
|
|
import Actions from '@nextcloud/vue/dist/Components/NcActions'
|
|
import ActionCheckbox from '@nextcloud/vue/dist/Components/NcActionCheckbox'
|
|
import ResourceSeatingCapacity from './ResourceSeatingCapacity'
|
|
import ResourceRoomType from './ResourceRoomType'
|
|
|
|
export default {
|
|
name: 'ResourceListSearch',
|
|
components: {
|
|
Avatar,
|
|
Multiselect,
|
|
ResourceSeatingCapacity,
|
|
Actions,
|
|
ActionCheckbox,
|
|
ResourceRoomType,
|
|
},
|
|
props: {
|
|
alreadyInvitedEmails: {
|
|
type: Array,
|
|
required: true,
|
|
},
|
|
calendarObjectInstance: {
|
|
type: Object,
|
|
required: true,
|
|
},
|
|
},
|
|
data() {
|
|
return {
|
|
isLoading: false,
|
|
inputGiven: false,
|
|
matches: [],
|
|
capacity: NaN,
|
|
roomType: '',
|
|
isAvailable: true,
|
|
isAccessible: false,
|
|
hasProjector: false,
|
|
hasWhiteboard: false,
|
|
}
|
|
},
|
|
computed: {
|
|
placeholder() {
|
|
return this.$t('calendar', 'Search for resources or rooms')
|
|
},
|
|
noResult() {
|
|
return this.$t('calendar', 'No match found')
|
|
},
|
|
features() {
|
|
const features = []
|
|
if (this.isAccessible) {
|
|
features.push('WHEELCHAIR-ACCESSIBLE')
|
|
}
|
|
if (this.hasProjector) {
|
|
features.push('PROJECTOR')
|
|
}
|
|
if (this.hasWhiteboard) {
|
|
features.push('WHITEBOARD')
|
|
}
|
|
return features
|
|
},
|
|
},
|
|
methods: {
|
|
findResources: debounce(async function(query) {
|
|
this.isLoading = true
|
|
let matches = []
|
|
|
|
if (query.length > 0) {
|
|
matches = await this.findResourcesFromDAV(query)
|
|
this.isLoading = false
|
|
this.inputGiven = true
|
|
} else {
|
|
this.inputGiven = false
|
|
this.isLoading = false
|
|
}
|
|
|
|
this.matches = matches
|
|
}, 500),
|
|
addResource(selectedValue) {
|
|
this.$emit('add-resource', selectedValue)
|
|
},
|
|
async findResourcesFromDAV(input) {
|
|
let results
|
|
try {
|
|
const query = { displayName: input }
|
|
query.capacity = this.capacity
|
|
query.features = this.features
|
|
query.roomType = this.roomType
|
|
results = await advancedPrincipalPropertySearch(query)
|
|
} catch (error) {
|
|
logger.debug('Could not find resources', { error })
|
|
return []
|
|
}
|
|
|
|
// Build options
|
|
let options = results
|
|
.filter(principal => {
|
|
if (!principal.email) {
|
|
return false
|
|
}
|
|
|
|
if (this.alreadyInvitedEmails.includes(principal.email)) {
|
|
return false
|
|
}
|
|
|
|
// Only include resources and rooms
|
|
if (!['RESOURCE', 'ROOM'].includes(principal.calendarUserType)) {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
})
|
|
.map(principal => {
|
|
const subLineData = []
|
|
if (principal.roomSeatingCapacity) {
|
|
subLineData.push(this.$n('calendar', '{seatingCapacity} seat', '{seatingCapacity} seats', principal.roomSeatingCapacity, {
|
|
seatingCapacity: principal.roomSeatingCapacity,
|
|
}))
|
|
}
|
|
if (principal.roomAddress) {
|
|
subLineData.push(principal.roomAddress)
|
|
}
|
|
|
|
return {
|
|
commonName: principal.displayname,
|
|
email: principal.email,
|
|
calendarUserType: principal.calendarUserType,
|
|
displayName: principal.displayname ?? principal.email,
|
|
subLine: subLineData.join(' - '),
|
|
isAvailable: true,
|
|
roomAddress: principal.roomAddress,
|
|
}
|
|
})
|
|
|
|
// Check resource availability
|
|
await checkResourceAvailability(
|
|
options,
|
|
this.$store.getters.getCurrentUserPrincipalEmail,
|
|
this.calendarObjectInstance.eventComponent.startDate,
|
|
this.calendarObjectInstance.eventComponent.endDate,
|
|
)
|
|
|
|
// Filter by availability
|
|
if (this.isAvailable) {
|
|
options = options.filter(option => option.isAvailable)
|
|
}
|
|
|
|
return options
|
|
},
|
|
/**
|
|
* Format availability of a search result
|
|
*
|
|
* @param {boolean} isAvailable The availability state
|
|
* @return {string} Human readable and localized availability
|
|
*/
|
|
formatAvailability(isAvailable) {
|
|
if (isAvailable) {
|
|
// TRANSLATORS room or resource is available due to not being booked yet
|
|
return this.$t('calendar', 'available')
|
|
}
|
|
|
|
// TRANSLATORS room or resource is unavailable due to it being already booked
|
|
return this.$t('calendar', 'unavailable')
|
|
},
|
|
},
|
|
}
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.resource-search__multiselect {
|
|
padding-bottom: 5px !important;
|
|
}
|
|
</style>
|