diff --git a/src/components/Editor/Resources/ResourceList.vue b/src/components/Editor/Resources/ResourceList.vue index 8b62d56688..5c81d8dd78 100644 --- a/src/components/Editor/Resources/ResourceList.vue +++ b/src/components/Editor/Resources/ResourceList.vue @@ -4,238 +4,613 @@ --> - diff --git a/src/components/Editor/Resources/ResourceListItem.vue b/src/components/Editor/Resources/ResourceListItem.vue deleted file mode 100644 index a2564a45ac..0000000000 --- a/src/components/Editor/Resources/ResourceListItem.vue +++ /dev/null @@ -1,309 +0,0 @@ - - - - - - - diff --git a/src/components/Editor/Resources/ResourceListSearch.vue b/src/components/Editor/Resources/ResourceListSearch.vue deleted file mode 100644 index 4b331fa0eb..0000000000 --- a/src/components/Editor/Resources/ResourceListSearch.vue +++ /dev/null @@ -1,294 +0,0 @@ - - - - - - - diff --git a/src/components/Editor/Resources/ResourceRoomCard.vue b/src/components/Editor/Resources/ResourceRoomCard.vue new file mode 100644 index 0000000000..91946de0c8 --- /dev/null +++ b/src/components/Editor/Resources/ResourceRoomCard.vue @@ -0,0 +1,191 @@ + + + + + + + diff --git a/src/models/principal.js b/src/models/principal.js index 1e50eaeed5..fe4cddd8aa 100644 --- a/src/models/principal.js +++ b/src/models/principal.js @@ -50,6 +50,14 @@ function getDefaultPrincipalObject(props) { principalId: null, // The url of the default calendar for invitations scheduleDefaultCalendarUrl: null, + // Room-specific properties (only for calendar-rooms) + roomSeatingCapacity: null, + roomType: null, + roomAddress: null, + roomFeatures: null, + roomBuildingName: null, + roomBuildingAddress: null, + roomNumber: null, ...props, } } @@ -91,6 +99,38 @@ function mapDavToPrincipal(dav) { const url = dav.principalUrl const userId = dav.userId + // Extract room-specific properties from DAV object, trimming string values defensively + const roomSeatingCapacity = dav.roomSeatingCapacity ?? null + const roomType = (dav.roomType ?? '').toString().trim() || null + const roomFeatures = (dav.roomFeatures ?? '').toString().trim() || null + // Strip leading/trailing whitespace and commas from building address to handle empty + // building-name fields, e.g. ", Science Park 140, 1098 XG, Amsterdam" → "Science Park 140, 1098 XG, Amsterdam" + const rawBuildingAddress = dav.roomBuildingAddress ?? null + const roomBuildingAddress = rawBuildingAddress + ? rawBuildingAddress.replace(/^[\s,]+|[\s,]+$/g, '').trim() || null + : null + // Derive building name from address (everything before first comma): "Poppodium, Kerkstraat 10" → "Poppodium" + const roomBuildingName = roomBuildingAddress ? roomBuildingAddress.split(',')[0].trim() || null : null + // Room number (floor.room format, e.g. "2.17") is stored in room-building-room-number + const roomNumber = (dav.roomBuildingRoomNumber ?? '').toString().trim() || null + + // Construct roomAddress for event LOCATION field from available properties + // Format: "Street (Building, Room X.XX)" — street-first for map/navigation apps + let roomAddress = null + if (roomBuildingAddress) { + const commaIdx = roomBuildingAddress.indexOf(',') + if (commaIdx > 0) { + const building = roomBuildingAddress.substring(0, commaIdx).trim() + const street = roomBuildingAddress.substring(commaIdx + 1).trim() + const detail = roomNumber ? building + ', Room ' + roomNumber : building + roomAddress = street + ' (' + detail + ')' + } else { + roomAddress = roomNumber + ? roomBuildingAddress + ' (Room ' + roomNumber + ')' + : roomBuildingAddress + } + } + return getDefaultPrincipalObject({ id, calendarUserType, @@ -107,6 +147,13 @@ function mapDavToPrincipal(dav) { principalId, userId, scheduleDefaultCalendarUrl, + roomSeatingCapacity, + roomType, + roomAddress, + roomFeatures, + roomBuildingName, + roomBuildingAddress, + roomNumber, }) } diff --git a/src/models/principal.ts b/src/models/principal.ts new file mode 100644 index 0000000000..ca7f0c41a6 --- /dev/null +++ b/src/models/principal.ts @@ -0,0 +1,15 @@ +export interface RoomPrincipal { + id: string | null + displayname: string | null + emailAddress: string | null + calendarUserType: string + isAvailable: boolean + roomSeatingCapacity: string | null + roomType: string | null + roomAddress: string | null + roomFeatures: string | null + roomNumber: string | null + roomFloor: string | null + roomBuildingName: string | null + roomBuildingAddress: string | null +} diff --git a/src/models/resourceProps.js b/src/models/resourceProps.js index 4e11b46b1a..1c72e46360 100644 --- a/src/models/resourceProps.js +++ b/src/models/resourceProps.js @@ -13,7 +13,12 @@ import { translate as t } from '@nextcloud/l10n' export function getAllRoomTypes() { return [ { value: 'meeting-room', label: t('calendar', 'Meeting room') }, + { value: 'board-room', label: t('calendar', 'Board room') }, + { value: 'conference-room', label: t('calendar', 'Conference room') }, { value: 'lecture-hall', label: t('calendar', 'Lecture hall') }, + { value: 'rehearsal-room', label: t('calendar', 'Rehearsal room') }, + { value: 'studio', label: t('calendar', 'Studio') }, + { value: 'outdoor-area', label: t('calendar', 'Outdoor area') }, { value: 'seminar-room', label: t('calendar', 'Seminar room') }, { value: 'other', label: t('calendar', 'Other') }, ] @@ -29,3 +34,35 @@ export function formatRoomType(value) { const option = getAllRoomTypes().find((option) => option.value === value) return option?.label ?? null } + +/** + * Short labels for known facility types. + * Evaluated lazily (inside a function) so that t() is not called at module-import + * time, which would break test mocks. + * + * @return {object} + */ +function getFacilityLabels() { + return { + projector: t('calendar', 'Projector'), + beamer: t('calendar', 'Projector'), + whiteboard: t('calendar', 'Whiteboard'), + video_conference: t('calendar', 'Video'), + videoconference: t('calendar', 'Video'), + wheelchair_accessible: t('calendar', 'Wheelchair accessible'), + 'wheelchair-accessible': t('calendar', 'Wheelchair accessible'), + audio: t('calendar', 'Audio'), + display: t('calendar', 'Display'), + } +} + +/** + * Get a human-readable label for a facility + * + * @param {string} facility The facility identifier + * @return {string} + */ +export function formatFacility(facility) { + const lower = facility.toLowerCase().trim() + return getFacilityLabels()[lower] || facility.charAt(0).toUpperCase() + facility.slice(1) +} diff --git a/tests/javascript/unit/models/principal.test.js b/tests/javascript/unit/models/principal.test.js index 12d65a3769..5d333dfc12 100644 --- a/tests/javascript/unit/models/principal.test.js +++ b/tests/javascript/unit/models/principal.test.js @@ -23,6 +23,13 @@ describe('Test suite: Principal model (models/principal.js)', () => { isCalendarRoom: false, principalId: null, scheduleDefaultCalendarUrl: null, + roomSeatingCapacity: null, + roomType: null, + roomAddress: null, + roomFeatures: null, + roomBuildingName: null, + roomBuildingAddress: null, + roomNumber: null, }) }) @@ -47,6 +54,13 @@ describe('Test suite: Principal model (models/principal.js)', () => { principalId: 'bar', otherProp: 'foo', scheduleDefaultCalendarUrl: null, + roomSeatingCapacity: null, + roomType: null, + roomAddress: null, + roomFeatures: null, + roomBuildingName: null, + roomBuildingAddress: null, + roomNumber: null, }) }) @@ -81,6 +95,13 @@ describe('Test suite: Principal model (models/principal.js)', () => { isCalendarRoom: false, principalId: 'jane.doe', userId: 'legacy-jane-doe-uid', + roomSeatingCapacity: null, + roomType: null, + roomAddress: null, + roomFeatures: null, + roomBuildingName: null, + roomBuildingAddress: null, + roomNumber: null, }) }) @@ -115,6 +136,13 @@ describe('Test suite: Principal model (models/principal.js)', () => { isCalendarRoom: false, principalId: 'jane.doe', userId: null, + roomSeatingCapacity: null, + roomType: null, + roomAddress: null, + roomFeatures: null, + roomBuildingName: null, + roomBuildingAddress: null, + roomNumber: null, }) }) @@ -149,6 +177,13 @@ describe('Test suite: Principal model (models/principal.js)', () => { isCalendarRoom: false, principalId: 'CGAH82BAS285H', userId: null, + roomSeatingCapacity: null, + roomType: null, + roomAddress: null, + roomFeatures: null, + roomBuildingName: null, + roomBuildingAddress: null, + roomNumber: null, }) }) @@ -183,6 +218,13 @@ describe('Test suite: Principal model (models/principal.js)', () => { isCalendarRoom: false, principalId: 'projector-123', userId: null, + roomSeatingCapacity: null, + roomType: null, + roomAddress: null, + roomFeatures: null, + roomBuildingName: null, + roomBuildingAddress: null, + roomNumber: null, }) }) @@ -217,9 +259,90 @@ describe('Test suite: Principal model (models/principal.js)', () => { isCalendarRoom: true, principalId: 'room-123', userId: null, + roomSeatingCapacity: null, + roomType: null, + roomAddress: null, + roomFeatures: null, + roomBuildingName: null, + roomBuildingAddress: null, + roomNumber: null, }) }) + it('should properly map a calendar-room-principal with room properties', () => { + const dav = { + addressBookHomes: undefined, + calendarHomes: [], + calendarUserAddressSet: [], + calendarUserType: 'ROOM', + displayname: 'Conference Room A', + email: 'conf-a@example.com', + principalScheme: 'principal:principals/calendar-rooms/conf-a', + principalUrl: '/remote.php/dav/principals/calendar-rooms/conf-a/', + scheduleInbox: null, + scheduleOutbox: null, + url: '/remote.php/dav/principals/calendar-rooms/conf-a/', + userId: null, + roomSeatingCapacity: 20, + roomType: 'conference-room', + roomFeatures: 'PROJECTOR,WHITEBOARD', + roomBuildingAddress: 'Building A, Main Street 1', + roomBuildingRoomNumber: '2.17', + } + + expect(mapDavToPrincipal(dav)).toEqual({ + id: 'L3JlbW90ZS5waHAvZGF2L3ByaW5jaXBhbHMvY2FsZW5kYXItcm9vbXMvY29uZi1hLw==', + dav, + calendarUserType: 'ROOM', + principalScheme: 'principal:principals/calendar-rooms/conf-a', + emailAddress: 'conf-a@example.com', + displayname: 'Conference Room A', + url: '/remote.php/dav/principals/calendar-rooms/conf-a/', + isUser: false, + isGroup: false, + isCircle: false, + isCalendarResource: false, + isCalendarRoom: true, + principalId: 'conf-a', + userId: null, + roomSeatingCapacity: 20, + roomType: 'conference-room', + roomFeatures: 'PROJECTOR,WHITEBOARD', + roomBuildingName: 'Building A', + roomBuildingAddress: 'Building A, Main Street 1', + roomNumber: '2.17', + roomAddress: 'Main Street 1 (Building A, Room 2.17)', + }) + }) + + it('should strip leading commas and whitespace from roomBuildingAddress', () => { + const dav = { + addressBookHomes: undefined, + calendarHomes: [], + calendarUserAddressSet: [], + calendarUserType: 'ROOM', + displayname: 'AMS 0.11', + email: 'ams-011@example.com', + principalScheme: 'principal:principals/calendar-rooms/ams-011', + principalUrl: '/remote.php/dav/principals/calendar-rooms/ams-011/', + scheduleInbox: null, + scheduleOutbox: null, + url: '/remote.php/dav/principals/calendar-rooms/ams-011/', + userId: null, + roomSeatingCapacity: 1, + roomType: 'meeting-room', + roomFeatures: ' ', + roomBuildingAddress: ', Science Park 140, 1098 XG, Amsterdam', + roomBuildingRoomNumber: '0.11', + } + + const result = mapDavToPrincipal(dav) + expect(result.roomBuildingAddress).toBe('Science Park 140, 1098 XG, Amsterdam') + expect(result.roomBuildingName).toBe('Science Park 140') + expect(result.roomFeatures).toBe(null) + expect(result.roomAddress).toBe('1098 XG, Amsterdam (Science Park 140, Room 0.11)') + }) + it('should properly map a principal from an unknown backend to principal-object', () => { const dav = { addressBookHomes: undefined, @@ -251,6 +374,13 @@ describe('Test suite: Principal model (models/principal.js)', () => { isCalendarRoom: false, principalId: null, userId: null, + roomSeatingCapacity: null, + roomType: null, + roomAddress: null, + roomFeatures: null, + roomBuildingName: null, + roomBuildingAddress: null, + roomNumber: null, }) }) })