├── v2 ├── Sample Files │ ├── LocationList.csv │ ├── SubLocationList.csv │ └── RoomAccounts.csv ├── Flows │ ├── AddMeetingBotv2.zip │ └── SetupRoomMeetingsFlowv2.zip ├── Documentation │ └── Images │ │ ├── VRApp1.png │ │ ├── VRApp2.png │ │ ├── VRApp3.png │ │ ├── ConfigApp1.png │ │ ├── ConfigApp2.png │ │ ├── TeamsPolicy.png │ │ ├── AppSetupPolicy.png │ │ ├── CallingPolicy.png │ │ ├── MeetingPolicy1.png │ │ ├── MeetingPolicy2.png │ │ ├── MessagingPolicy.png │ │ ├── PatientJoinApp.png │ │ ├── PatientJoinApp2.png │ │ ├── LiveEventsPolicy.png │ │ └── AppPermissionPolicy.png ├── PowerApps │ ├── PatientJoin_20200429035431.zip │ ├── VirtualRounding_20200429035512.zip │ └── PowerApps.md ├── Scripts │ ├── RunningConfig.json │ ├── CreateRooms.ps1 │ └── SetupSPO.ps1 ├── LICENSE └── README.md ├── v1 ├── Flows │ ├── SetupRoomMeetingsFlow.zip │ └── JoinBotsToMeetings(v1)_20200415141920.zip ├── Sample Files │ ├── SubLocationList.csv │ ├── LocationList.csv │ └── RoomAccounts.csv ├── Documentation │ └── Images │ │ ├── TeamsPolicy.png │ │ ├── AppSetupPolicy.png │ │ ├── CallingPolicy.png │ │ ├── MeetingPolicy1.png │ │ ├── MeetingPolicy2.png │ │ ├── MessagingPolicy.png │ │ ├── LiveEventsPolicy.png │ │ └── AppPermissionPolicy.png ├── LICENSE ├── Scripts │ ├── RunningConfig.json │ ├── SharePointViewFormatting.json │ ├── CreateRooms.ps1 │ └── CreateTeamsAndSPO.ps1 └── README.md ├── LICENSE └── README.md /v2/Sample Files/LocationList.csv: -------------------------------------------------------------------------------- 1 | LocationName,MembersGroupName 2 | Building 1, 3 | Building 2, 4 | Building 3, 5 | -------------------------------------------------------------------------------- /v2/Flows/AddMeetingBotv2.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartterHealth/Virtual-Rounding/HEAD/v2/Flows/AddMeetingBotv2.zip -------------------------------------------------------------------------------- /v1/Flows/SetupRoomMeetingsFlow.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartterHealth/Virtual-Rounding/HEAD/v1/Flows/SetupRoomMeetingsFlow.zip -------------------------------------------------------------------------------- /v1/Sample Files/SubLocationList.csv: -------------------------------------------------------------------------------- 1 | LocationName,LocationSubname 2 | Building 1,Floor 2 3 | Building 1,Floor 3 4 | Building 1,Floor 1 5 | -------------------------------------------------------------------------------- /v2/Documentation/Images/VRApp1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartterHealth/Virtual-Rounding/HEAD/v2/Documentation/Images/VRApp1.png -------------------------------------------------------------------------------- /v2/Documentation/Images/VRApp2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartterHealth/Virtual-Rounding/HEAD/v2/Documentation/Images/VRApp2.png -------------------------------------------------------------------------------- /v2/Documentation/Images/VRApp3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartterHealth/Virtual-Rounding/HEAD/v2/Documentation/Images/VRApp3.png -------------------------------------------------------------------------------- /v2/Documentation/Images/ConfigApp1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartterHealth/Virtual-Rounding/HEAD/v2/Documentation/Images/ConfigApp1.png -------------------------------------------------------------------------------- /v2/Documentation/Images/ConfigApp2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartterHealth/Virtual-Rounding/HEAD/v2/Documentation/Images/ConfigApp2.png -------------------------------------------------------------------------------- /v2/Flows/SetupRoomMeetingsFlowv2.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartterHealth/Virtual-Rounding/HEAD/v2/Flows/SetupRoomMeetingsFlowv2.zip -------------------------------------------------------------------------------- /v1/Documentation/Images/TeamsPolicy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartterHealth/Virtual-Rounding/HEAD/v1/Documentation/Images/TeamsPolicy.png -------------------------------------------------------------------------------- /v2/Documentation/Images/TeamsPolicy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartterHealth/Virtual-Rounding/HEAD/v2/Documentation/Images/TeamsPolicy.png -------------------------------------------------------------------------------- /v1/Documentation/Images/AppSetupPolicy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartterHealth/Virtual-Rounding/HEAD/v1/Documentation/Images/AppSetupPolicy.png -------------------------------------------------------------------------------- /v1/Documentation/Images/CallingPolicy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartterHealth/Virtual-Rounding/HEAD/v1/Documentation/Images/CallingPolicy.png -------------------------------------------------------------------------------- /v1/Documentation/Images/MeetingPolicy1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartterHealth/Virtual-Rounding/HEAD/v1/Documentation/Images/MeetingPolicy1.png -------------------------------------------------------------------------------- /v1/Documentation/Images/MeetingPolicy2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartterHealth/Virtual-Rounding/HEAD/v1/Documentation/Images/MeetingPolicy2.png -------------------------------------------------------------------------------- /v1/Documentation/Images/MessagingPolicy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartterHealth/Virtual-Rounding/HEAD/v1/Documentation/Images/MessagingPolicy.png -------------------------------------------------------------------------------- /v2/Documentation/Images/AppSetupPolicy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartterHealth/Virtual-Rounding/HEAD/v2/Documentation/Images/AppSetupPolicy.png -------------------------------------------------------------------------------- /v2/Documentation/Images/CallingPolicy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartterHealth/Virtual-Rounding/HEAD/v2/Documentation/Images/CallingPolicy.png -------------------------------------------------------------------------------- /v2/Documentation/Images/MeetingPolicy1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartterHealth/Virtual-Rounding/HEAD/v2/Documentation/Images/MeetingPolicy1.png -------------------------------------------------------------------------------- /v2/Documentation/Images/MeetingPolicy2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartterHealth/Virtual-Rounding/HEAD/v2/Documentation/Images/MeetingPolicy2.png -------------------------------------------------------------------------------- /v2/Documentation/Images/MessagingPolicy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartterHealth/Virtual-Rounding/HEAD/v2/Documentation/Images/MessagingPolicy.png -------------------------------------------------------------------------------- /v2/Documentation/Images/PatientJoinApp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartterHealth/Virtual-Rounding/HEAD/v2/Documentation/Images/PatientJoinApp.png -------------------------------------------------------------------------------- /v2/Documentation/Images/PatientJoinApp2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartterHealth/Virtual-Rounding/HEAD/v2/Documentation/Images/PatientJoinApp2.png -------------------------------------------------------------------------------- /v2/PowerApps/PatientJoin_20200429035431.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartterHealth/Virtual-Rounding/HEAD/v2/PowerApps/PatientJoin_20200429035431.zip -------------------------------------------------------------------------------- /v1/Documentation/Images/LiveEventsPolicy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartterHealth/Virtual-Rounding/HEAD/v1/Documentation/Images/LiveEventsPolicy.png -------------------------------------------------------------------------------- /v2/Documentation/Images/LiveEventsPolicy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartterHealth/Virtual-Rounding/HEAD/v2/Documentation/Images/LiveEventsPolicy.png -------------------------------------------------------------------------------- /v2/Sample Files/SubLocationList.csv: -------------------------------------------------------------------------------- 1 | LocationName,LocationSubname 2 | Building 1,Floor 1 3 | Building 1,Floor 4 4 | Building 2,Floor 1 5 | Building 3,ED 6 | -------------------------------------------------------------------------------- /v1/Documentation/Images/AppPermissionPolicy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartterHealth/Virtual-Rounding/HEAD/v1/Documentation/Images/AppPermissionPolicy.png -------------------------------------------------------------------------------- /v2/Documentation/Images/AppPermissionPolicy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartterHealth/Virtual-Rounding/HEAD/v2/Documentation/Images/AppPermissionPolicy.png -------------------------------------------------------------------------------- /v2/PowerApps/VirtualRounding_20200429035512.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartterHealth/Virtual-Rounding/HEAD/v2/PowerApps/VirtualRounding_20200429035512.zip -------------------------------------------------------------------------------- /v1/Flows/JoinBotsToMeetings(v1)_20200415141920.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartterHealth/Virtual-Rounding/HEAD/v1/Flows/JoinBotsToMeetings(v1)_20200415141920.zip -------------------------------------------------------------------------------- /v1/Sample Files/LocationList.csv: -------------------------------------------------------------------------------- 1 | LocationName,MembersGroupName 2 | Building 1,Building 1 Doctors 3 | Building 2,Building 2 Doctors 4 | Building 3,Building 3 Doctors 5 | -------------------------------------------------------------------------------- /v1/Sample Files/RoomAccounts.csv: -------------------------------------------------------------------------------- 1 | AccountName,AccountUPN,AccountPassword,AccountLocation,AccountSubLocation 2 | Room 123,room123@consoto.com,p@ssword1,Building 1,Floor 1 3 | -------------------------------------------------------------------------------- /v2/Sample Files/RoomAccounts.csv: -------------------------------------------------------------------------------- 1 | AccountName,AccountUPN,AccountPassword,AccountLocation,AccountSubLocation 2 | Room 123,room123@fritzium.com,p@ssword1,Building 1,Floor 1 3 | Room 404,room404@fritzium.com,p@ssword1,Building 1,Floor 4 4 | Room 121,room121@fritzium.com,p@ssword1,Building 2,Floor 1 5 | ED Room 1,edroom1@fritzium.com,p@ssword1,Building 3,ED 6 | -------------------------------------------------------------------------------- /v2/Scripts/RunningConfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "LocationCsvPaths": 3 | { 4 | "Rooms": "C:\\GitHub\\Virtual-Rounding\\v2\\Sample Files\\RoomAccounts.csv" 5 | }, 6 | "TenantInfo": 7 | { 8 | "TenantName": "Fritzium.onmicrosoft.com", 9 | "SPOBaseUrl": "https://fritzium.sharepoint.com/", 10 | "SPOMasterSiteName": "VirtualRounding", 11 | "SPOMasterListName": "Virtual Rounding", 12 | "RoomsADGroup": "Virtual Rounding Accounts", 13 | "MFARequired": false, 14 | "GlobalAdminUPN": "admin@Fritzium.onmicrosoft.com" 15 | }, 16 | "TeamsInfo": 17 | { 18 | "meetingPolicyName": "Virtual Rounding", 19 | "messagingPolicyName": "Virtual Rounding", 20 | "liveEventPolicyName": "Virtual Rounding", 21 | "appPermissionPolicyName": "Virtual Rounding", 22 | "appSetupPolicyName": "Virtual Rounding", 23 | "callingPolicyName": "Virtual Rounding", 24 | "teamsPolicyName": "Virtual Rounding" 25 | } 26 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 SmartterHealth 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /v1/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 SmartterHealth 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /v2/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 SmartterHealth 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /v1/Scripts/RunningConfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "ClientCredential": 3 | { 4 | "Id": "", 5 | "Secret": "" 6 | }, 7 | "LocationCsvPaths": 8 | { 9 | "Locations": "C:\\Scripts\\Locations.csv. Replace with Path of the first CSV file. (Columns expected: LocationName, MembersGroupName). LOCATION NAMES SHOULD MATCH AccountLocations FROM CreateRooms SCRIPT", 10 | "SubLocations": "C:\\Scripts\\Sublocations.csv Replace with path of the Subocation CSV file. (Columns expected: SubLocationName, LocationName) SUBLOCATION NAMES SHOULD MATCH AccountSubLocations FROM CreateRooms SCRIPT", 11 | "Rooms": "C:\\Scripts\\Rooms.csv Replace with the path of the Rooms CSV file. Columns expected: AccountName, AccountUPN, AccountPassword, AccountLocation, AccountSubLocation" 12 | }, 13 | "TenantInfo": 14 | { 15 | "TenantName": "something.onmicrosoft.com", 16 | "SPOBaseUrl": "https://something.sharepoint.com/", 17 | "RoomsADGroup": "Rounding Patient Rooms", 18 | "MFARequired": false, 19 | "GlobalAdminUPN": "admin@contoso.com" 20 | }, 21 | "ViewJson": 22 | { 23 | "SPViewJsonFilePath": "C:\\Scripts\\SharePointViewFormatting.json" 24 | }, 25 | "GroupConfiguration": 26 | { 27 | "RoundingTeamSuffix":"Virtual Rounding" 28 | }, 29 | "TimeZoneInfo": 30 | { 31 | "UTCOffset": "-4", 32 | "TimeZoneName": "Eastern Standard Time" 33 | }, 34 | "TeamsInfo": 35 | { 36 | "meetingPolicyName": "Virtual Rounding Room", 37 | "messagingPolicyName": "Virtual Rounding Room", 38 | "liveEventPolicyName": "Virtual Rounding Room", 39 | "appPermissionPolicyName": "Virtual Rounding Room", 40 | "appSetupPolicyName": "Virtual Rounding Room", 41 | "callingPolicyName": "Virtual Rounding Room", 42 | "teamsPolicyName": "Virtual Rounding Room" 43 | } 44 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Virtual Rounding using Microsoft Teams 2 | 3 | For deployment assistance, questions or comments, please fill out [this form](https://forms.office.com/Pages/ResponsePage.aspx?id=v4j5cvGGr0GRqy180BHbR6mlTNdIzWRKq7zcu5h9FqNUMVoxSU0yS0hCSVhKMkxRREZaVE1IRU8wVy4u). Someone from Microsoft will reach out as soon as possible. 4 | 5 | ## Overview 6 | 7 | This is the Virtual Rounding solution referenced in the Microsoft Health & Life Sciences [blog post](https://aka.ms/teamsvirtualrounding). Please see that blog post for more an overview of the use case. This repository serves as the technical documentation. 8 | 9 | There are now two active versions of Virtual Rounding, Version 1 (v1) and Version 2 (v2). Both are available, however all future enhancements will be to v2 moving forward. v1 will be updated with bug fixes only as they are identified. Please see below for a comparison and links to each reference architecture: 10 | 11 | # [Version 1](/v1) 12 | * Join buttons exposed through SharePoint lists pinned as tabs in Teams 13 | 14 | # [Version 2](/v2) 15 | * Join buttons exposed through SharePoint lists pinned as tabs in Teams, or through Power Apps (additional licensing required) 16 | * Ability to invite family/friends 17 | * Unified SharePoint List that contains all rooms and links 18 | - With views for each sublocation/location still added as tabs in Teams (optional) 19 | * PowerApps for: 20 | - One click join for Patient Rooms 21 | - Meeting Join and Configuration for Providers 22 | 23 | ## Disclaimer 24 | 25 | _This solution is a sample and may be used with Microsoft Teams for dissemination of reference information only. This solution is not intended or made available for use as a medical device, clinical support, diagnostic tool, or other technology intended to be used in the diagnosis, cure, mitigation, treatment, or prevention of disease or other conditions, and no license or right is granted by Microsoft to use this solution for such purposes. This solution is not designed or intended to be a substitute for professional medical advice, diagnosis, treatment, or judgement and should not be used as such. Customer bears the sole risk and responsibility for any use. Microsoft does not warrant that the solution or any materials provided in connection therewith will be sufficient for any medical purposes or meet the health or medical requirements of any person._ -------------------------------------------------------------------------------- /v1/Scripts/SharePointViewFormatting.json: -------------------------------------------------------------------------------- 1 | { 2 | "schema": "https://developer.microsoft.com/json-schemas/sp/view-formatting.schema.json", 3 | "hideSelection": true, 4 | "hideListHeader": true, 5 | "rowFormatter": { 6 | "elmType": "div", 7 | "style": { 8 | "float": "left", 9 | "display": "flex", 10 | "flex-wrap": "wrap", 11 | "flex-direction": "column", 12 | "align-items": "center", 13 | "justify-content": "space-around", 14 | "min-width": "100px", 15 | "min-height": "110px", 16 | "border-radius": "8px", 17 | "margin-right": "10px", 18 | "margin-bottom": "10px", 19 | "box-shadow": "2px 2px 4px darkgrey", 20 | "background-color": "#0078d4" 21 | }, 22 | "attributes": { 23 | "class": "ms-bgColor-neutralLighterAlt ms-bgColor-neutralLight--hover ms-fontColor-themePrimary--hover" 24 | }, 25 | "children": [ 26 | { 27 | "elmType": "div", 28 | "children": [ 29 | { 30 | "elmType": "a", 31 | "style": { 32 | "font-size": "50px", 33 | "text-decoration": "none", 34 | "color": "white" 35 | }, 36 | "attributes": { 37 | "iconName": "Medical", 38 | "href": "=[$MeetingLink]", 39 | "target": "_blank" 40 | } 41 | } 42 | ] 43 | }, 44 | { 45 | "elmType": "div", 46 | "style": { 47 | "text-align": "center", 48 | "margin-bottom": "0px" 49 | }, 50 | "children": [ 51 | { 52 | "elmType": "a", 53 | "style": { 54 | "font-weight": "500", 55 | "font-size": "1.1rem", 56 | "color": "white", 57 | "text-decoration": "none" 58 | }, 59 | "txtContent": "[$Title]", 60 | "attributes": { 61 | "href": "=[$MeetingLink]", 62 | "target": "_blank" 63 | } 64 | } 65 | ] 66 | }, 67 | { 68 | "elmType": "div", 69 | "style": { 70 | "text-align": "center", 71 | "margin-top": "0px" 72 | 73 | }, 74 | "children": [ 75 | { 76 | "elmType": "a", 77 | "style": { 78 | "font-weight": "250", 79 | "font-size": "1.0rem", 80 | "color": "white", 81 | "text-decoration": "none" 82 | }, 83 | "txtContent": "[$RoomLocation]", 84 | "attributes": { 85 | "href": "=[$MeetingLink]", 86 | "target": "_blank" 87 | } 88 | } 89 | ] 90 | } 91 | ] 92 | } 93 | } -------------------------------------------------------------------------------- /v1/Scripts/CreateRooms.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | DISCLAIMER: 3 | ---------------------------------------------------------------- 4 | This sample is provided as is and is not meant for use on a production environment. 5 | It is provided only for illustrative purposes. The end user must test and modify the 6 | sample to suit their target environment. 7 | 8 | Microsoft can make no representation concerning the content of this sample. Microsoft 9 | is providing this information only as a convenience to you. This is to inform you that 10 | Microsoft has not tested the sample and therefore cannot make any representations 11 | regarding the quality, safety, or suitability of any code or information found here.   12 | ---------------------------------------------------------------- 13 | #> 14 | 15 | <# 16 | INSTRUCTIONS: 17 | Please see https://aka.ms/virtualroundingcode 18 | #> 19 | 20 | #-------------------Configurable Variables---------------------# 21 | $configFilePath = ".\Scripts\RunningConfig.json" 22 | 23 | #--------------System Variables (DO NOT MODIFY)----------------# 24 | $configFile = Get-Content -Path $configFilePath | ConvertFrom-Json 25 | 26 | $roomListCsvFilePath = $configFile.LocationCsvPaths.Rooms 27 | 28 | $roomsGroupName = $configFile.TenantInfo.RoomsADGroup 29 | 30 | $meetingPolicy = $configFile.TeamsInfo.meetingPolicyName 31 | $messagingPolicy = $configFile.TeamsInfo.messagingPolicyName 32 | $liveEventsPolicy = $configFile.TeamsInfo.liveEventPolicyName 33 | $appPermissionPolicy = $configFile.TeamsInfo.appPermissionPolicyName 34 | $appSetupPolicy = $configFile.TeamsInfo.appSetupPolicyName 35 | $callingPolicy = $configFile.TeamsInfo.callingPolicyName 36 | $teamsPolicy = $configFile.TeamsInfo.teamsPolicyName 37 | 38 | $useMFA = $configFile.TenantInfo.MFARequired 39 | $adminUPN = $configFile.TenantInfo.GlobalAdminUPN 40 | 41 | #-------------------------Script Setup-------------------------# 42 | Function Test-Existence { 43 | [CmdletBinding()] 44 | param( 45 | $value, 46 | $errorMsg 47 | ) 48 | try{ 49 | if($value){ 50 | return $true 51 | } 52 | else{ 53 | throw $errorMsg 54 | } 55 | } 56 | catch{ 57 | Write-Error $_ 58 | } 59 | } 60 | #-------------------------Script Setup-------------------------# 61 | if (!$useMFA) {$creds = Get-Credential -Message 'Please sign in to your Global Admin account:' -UserName $adminUPN} 62 | 63 | Test-Existence((Get-Module AzureAD-Preview),'The AzureAD Module is not installed. Please see https://aka.ms/virtualroundingcode for more details.') -ErrorAction Stop 64 | Import-Module AzureAD 65 | if ($useMFA) {Connect-AzureAD -ErrorAction Stop} 66 | else {Connect-AzureAD -Credential $creds -ErrorAction Stop} 67 | 68 | Test-Existence((Get-Module SkypeOnlineConnector),'The SkypeOnlineConnector Module is not installed. Please see https://aka.ms/virtualroundingcode for more details.') -ErrorAction Stop 69 | Import-Module SkypeOnlineConnector 70 | 71 | $roomsGroupID = (Get-AzureADGroup -Filter "DisplayName eq '$roomsGroupName'").objectID 72 | $existingGroupMembers = Get-AzureADGroupMember -ObjectId $roomsGroupID 73 | $accountList = Import-Csv -Path $roomListCsvFilePath 74 | 75 | #-----------Create user accounts and apply licensing-----------# 76 | foreach ($account in $accountList){ 77 | $PasswordProfile = New-Object -TypeName Microsoft.Open.AzureAD.Model.PasswordProfile 78 | $PasswordProfile.Password = $account.AccountPassword 79 | $PasswordProfile.ForceChangePasswordNextLogin = $false 80 | $upnParts = $account.AccountUPN.Split("@") 81 | $mailnickname = ($upnParts)[0] 82 | $upn = $account.AccountUPN.tostring() 83 | $userCheck = $null 84 | $userCheck = Get-AzureADUser -Filter "UserPrincipalName eq '$upn'" 85 | #Test for Account 86 | if ($null -eq $userCheck) { 87 | #Create Account 88 | New-AzureADUser -AccountEnabled $true -DisplayName $account.AccountName -UserPrincipalName $upn -Department $account.AccountLocation -UsageLocation "US" -PasswordProfile $PasswordProfile -JobTitle $account.AccountSubLocation -MailNickName $mailnickname 89 | #Add Account to License Group 90 | $userObjectID = (Get-AzureADUser -ObjectId $upn).ObjectID 91 | try {Add-AzureADGroupMember -ObjectId $roomsGroupID -RefObjectId $userObjectID} 92 | catch { 93 | if ($existingGroupMembers -contains $userObjectID) {Write-Host "$upn is already a member of $roomsGroupName group. Continuing..." -ForegroundColor DarkYellow} 94 | else {Write-Host "Unable to add $upn to $roomsGroupName group. The script will need to be restarted." -ErrorAction Stop -ForegroundColor Red} 95 | } 96 | Write-Host "Created $upn and added to $roomsGroupName group." -ForegroundColor Green 97 | } 98 | else { 99 | throw "$upn already exists in tenant. Skipped adding to Azure AD Group. WARNING: Teams policies will be applied to this account. Cancel run if this is unexpected." 100 | } 101 | } 102 | #Wait for licensing application and Teams/Exchange provisioning 103 | Write-Host "Script will now pause for 15 minutes to allow for licensing application and Teams/Exchange provisioning of new accounts" -ForegroundColor Green 104 | Start-Sleep -Seconds 900 #15 minutes 105 | 106 | #---------------------Apply Teams Policies---------------------# 107 | #Connect to Skype for Business Online PowerShell 108 | if ($useMFA) {$skypeSession = New-CsOnlineSession -UserName $adminUPN -ErrorAction Stop} 109 | else {$skypeSession = New-CsOnlineSession -Credential $creds -ErrorAction Stop} 110 | Import-PSSession $skypeSession -ErrorAction Stop 111 | 112 | foreach ($account in $accountList){ 113 | $upn = $account.AccountUPN 114 | #Check if account is ready 115 | $user = Get-CsOnlineUser -Identity $upn -ErrorAction SilentlyContinue 116 | while ($null -eq $user){ 117 | Write-Host "$upn is not ready for Teams Policies. Would you like to wait 15 more minutes (w), skip this user (s), or cancel the script (c)? (Default is Wait)" -ForegroundColor Yellow 118 | $readHost = Read-Host " ( w / s / c )" 119 | Switch ($readHost){ 120 | W {Write-host "Wait 15 more minutes"; Start-Sleep -Seconds 900} 121 | S {Write-Host "Skip $upn"; $skip = $true; Continue} 122 | C {Write-Host "Cancel Script"; break} 123 | Default {Write-Host "Wait 15 more minutes"; Start-Sleep -Seconds 900} 124 | } 125 | if($skip){Continue} 126 | $user = Get-CsOnlineUser -Identity $upn -ErrorAction SilentlyContinue 127 | } 128 | if($skip){Continue} 129 | Grant-CsTeamsAppPermissionPolicy -Identity $upn -PolicyName $appPermissionPolicy 130 | Grant-CsTeamsAppSetupPolicy -Identity $upn -PolicyName $appSetupPolicy 131 | Grant-CsTeamsCallingPolicy -Identity $upn -PolicyName $callingPolicy 132 | Grant-CsTeamsMeetingBroadcastPolicy -Identity $upn -PolicyName $liveEventsPolicy 133 | Grant-CsTeamsMeetingPolicy -Identity $upn -PolicyName $meetingPolicy 134 | Grant-CsTeamsMessagingPolicy -Identity $upn -PolicyName $messagingPolicy 135 | Grant-CsTeamsChannelsPolicy -Identity $upn -PolicyName $teamsPolicy 136 | Grant-CsTeamsUpgradePolicy -Identity $upn -PolicyName UpgradeToTeams #Sets account to Teams Only mode 137 | } -------------------------------------------------------------------------------- /v2/Scripts/CreateRooms.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | DISCLAIMER: 3 | ---------------------------------------------------------------- 4 | This sample is provided as is and is not meant for use on a production environment. 5 | It is provided only for illustrative purposes. The end user must test and modify the 6 | sample to suit their target environment. 7 | 8 | Microsoft can make no representation concerning the content of this sample. Microsoft 9 | is providing this information only as a convenience to you. This is to inform you that 10 | Microsoft has not tested the sample and therefore cannot make any representations 11 | regarding the quality, safety, or suitability of any code or information found here.   12 | ---------------------------------------------------------------- 13 | #> 14 | 15 | <# 16 | INSTRUCTIONS: 17 | Please see https://aka.ms/virtualroundingcode 18 | #> 19 | 20 | #-------------------Configurable Variables---------------------# 21 | $configFilePath = ".\GitHub\Virtual-Rounding\v2\Scripts\RunningConfig.json" 22 | 23 | #--------------System Variables (DO NOT MODIFY)----------------# 24 | $configFile = Get-Content -Path $configFilePath | ConvertFrom-Json 25 | 26 | $roomListCsvFilePath = $configFile.LocationCsvPaths.Rooms 27 | 28 | $roomsGroupName = $configFile.TenantInfo.RoomsADGroup 29 | 30 | $meetingPolicy = $configFile.TeamsInfo.meetingPolicyName 31 | $messagingPolicy = $configFile.TeamsInfo.messagingPolicyName 32 | $liveEventsPolicy = $configFile.TeamsInfo.liveEventPolicyName 33 | $appPermissionPolicy = $configFile.TeamsInfo.appPermissionPolicyName 34 | $appSetupPolicy = $configFile.TeamsInfo.appSetupPolicyName 35 | $callingPolicy = $configFile.TeamsInfo.callingPolicyName 36 | $teamsPolicy = $configFile.TeamsInfo.teamsPolicyName 37 | 38 | $useMFA = $configFile.TenantInfo.MFARequired 39 | $adminUPN = $configFile.TenantInfo.GlobalAdminUPN 40 | 41 | #-------------------------Script Setup-------------------------# 42 | Function Test-Existence { 43 | [CmdletBinding()] 44 | param( 45 | $value, 46 | $errorMsg 47 | ) 48 | try{ 49 | if($value){ 50 | return $true 51 | } 52 | else{ 53 | throw $errorMsg 54 | } 55 | } 56 | catch{ 57 | Write-Error $_ 58 | } 59 | } 60 | #-------------------------Script Setup-------------------------# 61 | if (!$useMFA) {$creds = Get-Credential -Message 'Please sign in to your Global Admin account:' -UserName $adminUPN} 62 | 63 | Test-Existence((Get-Module AzureAD),'The AzureAD Module is not installed. Please see https://aka.ms/virtualroundingcode for more details.') -ErrorAction Stop 64 | Import-Module AzureAD 65 | if ($useMFA) {Connect-AzureAD -ErrorAction Stop} 66 | else {Connect-AzureAD -Credential $creds -ErrorAction Stop} 67 | 68 | Test-Existence((Get-Module SkypeOnlineConnector),'The SkypeOnlineConnector Module is not installed. Please see https://aka.ms/virtualroundingcode for more details.') -ErrorAction Stop 69 | Import-Module SkypeOnlineConnector 70 | 71 | $roomsGroupID = (Get-AzureADGroup -Filter "DisplayName eq '$roomsGroupName'").objectID 72 | $existingGroupMembers = Get-AzureADGroupMember -ObjectId $roomsGroupID 73 | $accountList = Import-Csv -Path $roomListCsvFilePath -ErrorAction Stop 74 | 75 | #-----------Create user accounts and apply licensing-----------# 76 | foreach ($account in $accountList){ 77 | $PasswordProfile = New-Object -TypeName Microsoft.Open.AzureAD.Model.PasswordProfile 78 | $PasswordProfile.Password = $account.AccountPassword 79 | $PasswordProfile.ForceChangePasswordNextLogin = $false 80 | $upnParts = $account.AccountUPN.Split("@") 81 | $mailnickname = ($upnParts)[0] 82 | $upn = $account.AccountUPN.tostring() 83 | $userCheck = $null 84 | $userCheck = Get-AzureADUser -Filter "UserPrincipalName eq '$upn'" 85 | #Test for Account 86 | if ($null -eq $userCheck) { 87 | #Create Account 88 | New-AzureADUser -AccountEnabled $true -DisplayName $account.AccountName -UserPrincipalName $upn -Department $account.AccountLocation -UsageLocation "US" -PasswordProfile $PasswordProfile -JobTitle $account.AccountSubLocation -MailNickName $mailnickname 89 | #Add Account to License Group 90 | $userObjectID = (Get-AzureADUser -ObjectId $upn).ObjectID 91 | try {Add-AzureADGroupMember -ObjectId $roomsGroupID -RefObjectId $userObjectID} 92 | catch { 93 | if ($existingGroupMembers -contains $userObjectID) {Write-Host "$upn is already a member of $roomsGroupName group. Continuing..." -ForegroundColor DarkYellow} 94 | else {Write-Host "Unable to add $upn to $roomsGroupName group. The script will need to be restarted." -ErrorAction Stop -ForegroundColor Red} 95 | } 96 | Write-Host "Created $upn and added to $roomsGroupName group." -ForegroundColor Green 97 | } 98 | else { 99 | throw "$upn already exists in tenant. Skipped adding to Azure AD Group. WARNING: Teams policies will be applied to this account. Cancel run if this is unexpected." 100 | } 101 | } 102 | #Wait for licensing application and Teams/Exchange provisioning 103 | Write-Host "Script will now pause for 15 minutes to allow for licensing application and Teams/Exchange provisioning of new accounts. Check https://admin.microsoft.com/AdminPortal/Home#/teamsprovisioning to verify status." -ForegroundColor Green 104 | Start-Sleep -Seconds 900 #15 minutes 105 | 106 | #---------------------Apply Teams Policies---------------------# 107 | #Connect to Skype for Business Online PowerShell 108 | if ($useMFA) {$skypeSession = New-CsOnlineSession -UserName $adminUPN -ErrorAction Stop} 109 | else {$skypeSession = New-CsOnlineSession -Credential $creds -ErrorAction Stop} 110 | Import-PSSession $skypeSession -ErrorAction Stop 111 | 112 | foreach ($account in $accountList){ 113 | $upn = $account.AccountUPN 114 | #Check if account is ready 115 | $user = Get-CsOnlineUser -Identity $upn -ErrorAction SilentlyContinue 116 | while ($null -eq $user){ 117 | Write-Host "$upn is not ready for Teams Policies. Would you like to wait 15 more minutes (w), skip this user (s), or cancel the script (c)? (Default is Wait)" -ForegroundColor Yellow 118 | $readHost = Read-Host " ( w / s / c )" 119 | Switch ($readHost){ 120 | W {Write-host "Wait 15 more minutes"; Start-Sleep -Seconds 900} 121 | S {Write-Host "Skip $upn"; $skip = $true; Continue} 122 | C {Write-Host "Cancel Script"; break} 123 | Default {Write-Host "Waiting 15 more minutes" -ForegroundColor Green; Start-Sleep -Seconds 900} 124 | } 125 | if($skip){Continue} 126 | $user = Get-CsOnlineUser -Identity $upn -ErrorAction SilentlyContinue 127 | } 128 | if($skip){Continue} 129 | Grant-CsTeamsAppPermissionPolicy -Identity $upn -PolicyName $appPermissionPolicy 130 | Grant-CsTeamsAppSetupPolicy -Identity $upn -PolicyName $appSetupPolicy 131 | Grant-CsTeamsCallingPolicy -Identity $upn -PolicyName $callingPolicy 132 | Grant-CsTeamsMeetingBroadcastPolicy -Identity $upn -PolicyName $liveEventsPolicy 133 | Grant-CsTeamsMeetingPolicy -Identity $upn -PolicyName $meetingPolicy 134 | Grant-CsTeamsMessagingPolicy -Identity $upn -PolicyName $messagingPolicy 135 | Grant-CsTeamsChannelsPolicy -Identity $upn -PolicyName $teamsPolicy 136 | Grant-CsTeamsUpgradePolicy -Identity $upn -PolicyName UpgradeToTeams #Sets account to Teams Only mode 137 | } 138 | 139 | Write-Host "Script Complete." -ForegroundColor Green -------------------------------------------------------------------------------- /v2/Scripts/SetupSPO.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | DISCLAIMER: 3 | ---------------------------------------------------------------- 4 | This sample is provided as is and is not meant for use on a production environment. 5 | It is provided only for illustrative purposes. The end user must test and modify the 6 | sample to suit their target environment. 7 | 8 | Microsoft can make no representation concerning the content of this sample. Microsoft 9 | is providing this information only as a convenience to you. This is to inform you that 10 | Microsoft has not tested the sample and therefore cannot make any representations 11 | regarding the quality, safety, or suitability of any code or information found here.   12 | #> 13 | 14 | <# 15 | INSTRUCTIONS: 16 | Please see https://aka.ms/virtualroundingcode 17 | #> 18 | 19 | #--------------------------Variables---------------------------# 20 | $configFilePath = ".\GitHub\Virtual-Rounding\v2\Scripts\RunningConfig.json" 21 | $configFile = Get-Content -Path $configFilePath | ConvertFrom-Json 22 | 23 | $sharepointBaseUrl = $configFile.TenantInfo.SPOBaseUrl 24 | $sharepointMasterSiteName = $configFile.TenantInfo.SPOMasterSiteName 25 | $sharepointMasterListName = $configFile.TenantInfo.SPOMasterListName 26 | 27 | $useMFA = $configFile.TenantInfo.MFARequired 28 | $adminUPN = $configFile.TenantInfo.GlobalAdminUPN 29 | 30 | #--------------------------Functions---------------------------# 31 | Function Check-Module { 32 | [CmdletBinding()] 33 | param( 34 | $value, 35 | $errorMsg 36 | ) 37 | try { 38 | if ($value) { 39 | return $true 40 | } 41 | else { 42 | throw $errorMsg 43 | } 44 | } 45 | catch { 46 | Write-Error $_ 47 | } 48 | } 49 | 50 | Function Ask-User { 51 | [CmdletBinding()] 52 | param( 53 | $prompt 54 | ) 55 | Write-Host ($prompt + " (Default is Yes)") -ForegroundColor Yellow 56 | $readHost = Read-Host " ( y / n )" 57 | Switch ($readHost) { 58 | Y { return $true } 59 | N { return $false } 60 | Default { return $true } 61 | } 62 | } 63 | #-------------------------Script Setup-------------------------# 64 | if (!$useMFA) { $creds = Get-Credential -Message 'Please sign in to your Global Admin account:' -UserName $adminUPN } 65 | 66 | Check-Module((Get-Module SharePointPnPPowerShellOnline), 'The SharePointPnPPowerShellOnline Module is not installed. Please see https://aka.ms/virtualroundingcode for more details.') -ErrorAction Stop 67 | Import-Module SharePointPnPPowerShellOnline 68 | 69 | #-----------------Create Site and List-----------------# 70 | Write-Host "Connecting to SharePoint Online" -ForegroundColor Green 71 | if ($useMFA) { Connect-PnPOnline -Url $sharepointBaseUrl -UseWebLogin } 72 | else { Connect-PnPOnline -Url $sharepointBaseUrl -Credential $creds } 73 | 74 | $existingSite = Get-PnPSiteSearchQueryResults -Query "Title:$sharepointMasterSiteName" 75 | if ($existingSite) { $existingSiteTrue = $true } 76 | while ($existingSiteTrue -eq $true) { 77 | $userOption = Ask-User("An existing site already exists with the name of '$sharepointMasterSiteName'. Would you like to cancel or specify a new site name?") 78 | if ($userOption -eq $false) { Write-Host "Script stopping by user request" -ForegroundColor Red -ErrorAction Stop } 79 | else { 80 | $sharepointMasterSiteName = Read-Host "New Site Name:" 81 | $existingSite = Get-PnPSiteSearchQueryResults -Query "Title:$sharepointMasterSiteName" 82 | if (!$existingSite) { $existingSiteTrue = $false } 83 | } 84 | } 85 | $sharepointMasterSiteNameShort = $sharepointMasterSiteName.replace(" ", "") 86 | $SharePointMasterSiteURL = $sharepointBaseUrl + "sites/" + $sharepointMasterSiteNameShort 87 | 88 | Write-Host "Creating Site '$sharepointMasterSiteName'" -ForegroundColor Green 89 | New-PnPTenantSite -Title $sharepointMasterSiteName -Url $SharePointMasterSiteURL -Owner $adminUPN -TimeZone 11 -ErrorAction Stop 90 | 91 | Write-Host "Connecting to Site '$sharepointMasterSiteName'" -ForegroundColor Green 92 | $siteReady = $false 93 | while ($siteReady -eq $false) { 94 | try { 95 | if ($useMFA) { Connect-PnPOnline -Url $SharePointMasterSiteURL -UseWebLogin } 96 | else { Connect-PnPOnline -Url $SharePointMasterSiteURL -Credential $creds } 97 | $siteReady = $true 98 | } 99 | catch { 100 | $userOption2 = Ask-User("Unable to connect to SharePoint Site. This is commonly a result of a provisioning delay. Would you like to have the script pause for 5 minutes and try again?") 101 | if ($userOption2 -eq $false) { Write-Host "Script stopping by user request" -ForegroundColor Red -ErrorAction Stop } 102 | if ($userOption2 -eq $true) { Write-Host "Pausing for 5 minutes to wait for SharePoint Site Provisioning." -ForegroundColor Green; Start-Sleep -Seconds 300 } 103 | } 104 | } 105 | 106 | Write-Host "Setting up Site Columns & Content Type" -ForegroundColor Green 107 | Add-PnPField -Type Text -InternalName "RoomLocation" -DisplayName "Room Location" -Group "VirtualRounding" 108 | Add-PnPField -Type Text -InternalName "RoomSubLocation" -DisplayName "Room SubLocation" -Group "VirtualRounding" 109 | Add-PnPField -Type URL -InternalName "MeetingLink" -DisplayName "Meeting Link" -Group "VirtualRounding" 110 | Add-PnPField -Type Text -InternalName "EventID" -DisplayName "EventID" -Group "VirtualRounding" 111 | Add-PnPField -Type Text -InternalName "Share Externally" -DisplayName "Share Externally" -Group "VirtualRounding" 112 | Add-PnPField -Type Text -InternalName "Reset Room" -DisplayName "Reset Room" -Group "VirtualRounding" 113 | Add-PnPField -Type DateTime -InternalName "LastReset" -DisplayName "Last Reset" -Group "VirtualRounding" 114 | Add-PnPField -Type Number -InternalName "SharedWith" -DisplayName "Shared With" -Group "VirtualRounding" 115 | Add-PnPField -Type DateTime -InternalName "LastShare" -DisplayName "Last Share" -Group "VirtualRounding" 116 | Add-PnPField -Type Text -InternalName "RoomUPN" -DisplayName "Room UPN" -Group "VirtualRounding" 117 | Add-PnPField -Type Text -InternalName "Patient Name" -DisplayName "Patient Name" -Group "VirtualRounding" 118 | Add-PnPContentType -Name "VirtualRoundingRoom" -Group "VirtualRounding" | Out-Null 119 | Start-Sleep -Seconds 5 120 | $contentType = $null 121 | while (!$contentType) { 122 | try { 123 | $contentType = Get-PnPContentType -Identity "VirtualRoundingRoom" 124 | } 125 | catch { 126 | Write-Host "Content Type is not provisioned yet. Waiting 1 minute for provisioning. This will repeat each minute until ready." -ForegroundColor Yellow 127 | Start-Sleep 60 128 | } 129 | } 130 | Add-PnPFieldToContentType -Field "RoomLocation" -ContentType $contentType 131 | Add-PnPFieldToContentType -Field "RoomSubLocation" -ContentType $contentType 132 | Add-PnPFieldToContentType -Field "MeetingLink" -ContentType $contentType 133 | Add-PnPFieldToContentType -Field "EventID" -ContentType $contentType 134 | Add-PnPFieldToContentType -Field "Share Externally" -ContentType $contentType 135 | Add-PnPFieldToContentType -Field "Reset Room" -ContentType $contentType 136 | Add-PnPFieldToContentType -Field "LastReset" -ContentType $contentType 137 | Add-PnPFieldToContentType -Field "SharedWith" -ContentType $contentType 138 | Add-PnPFieldToContentType -Field "LastShare" -ContentType $contentType 139 | Add-PnPFieldToContentType -Field "RoomUPN" -ContentType $contentType 140 | Add-PnPFieldToContentType -Field "Patient Name" -ContentType $contentType 141 | 142 | Write-Host "Creating SharePoint List '$sharepointMasterListName'." -ForegroundColor Green 143 | $listShortName = $sharepointMasterListName.replace(" ","") 144 | $listUrl = "Lists/$listShortName" 145 | New-PnPList -Title $sharepointMasterListName -Url $listUrl -Template GenericList 146 | Start-Sleep -Seconds 5 147 | while (!$list) { 148 | try { 149 | $list = Get-PnPList -Identity $listUrl 150 | } 151 | catch { 152 | Write-Host "List is not provisioned yet. Waiting 1 minute for provisioning. This will repeat each minute until ready." -ForegroundColor Yellow 153 | Start-Sleep 60 154 | } 155 | } 156 | 157 | Write-Host "Enabling Content Types for SharePoint List '$sharepointMasterListName'." -ForegroundColor Green 158 | Set-PnPList -Identity $list -EnableContentTypes $true 159 | Write-Host "Adding 'VirtualRoundingRoom' Content Type to SharePoint List '$sharepointMasterListName'." -ForegroundColor Green 160 | Add-PnPContentTypeToList -List $list -ContentType $contentType -DefaultContentType -ErrorAction SilentlyContinue | Out-Null #Bug in PnP cmdlet, so SilentlyContinue required 161 | $newContentType = $null 162 | while (!$newContentType) { 163 | try { 164 | $newContentType = Get-PnPContentType -List $list | Where-Object { $_.Name -eq "VirtualRoundingRoom" } 165 | } 166 | catch { 167 | Write-Host "Content Type is not added to list yet. Waiting 1 minute for provisioning. This will repeat each minute until ready." -ForegroundColor Yellow 168 | Start-Sleep 60 169 | } 170 | } 171 | 172 | Disconnect-PnPOnline 173 | 174 | Write-Host "Script Complete." -ForegroundColor Green -------------------------------------------------------------------------------- /v2/PowerApps/PowerApps.md: -------------------------------------------------------------------------------- 1 | # Virtual Rounding Power Apps Deployment Instructions 2 | 3 | Special thanks to KiZAN for their help with these instructions. 4 | 5 | ## SharePoint List Permissions 6 | 7 | 1. Update the Virtual Rounding list so that all Providers have "Edit" access to the list items. 8 | 2. If you desire for Patients to be able to conduct "Invites" from their devices, Room Accounts will need "Edit" access to their list items. Note that doing this could be a PHI exposure avenue if Patient devices are not locked down to Kiosk mode; as users could leverage the Web Browser to navigate to the SharePoint site and view other rooms' data. 9 | 10 | ## Initial Provider App Upload 11 | 12 | 1. Navigate to the Power Apps portal signed in as a user with a Premium Power Automate license assigned to them. Click on "Import" and Upload the VirtualRounding\_xxxxx.zip file. 13 | 2. Set "Import Setup" for App as "Create as new" 14 | 15 | [![](RackMultipart20200512-4-8g3p57_html_7302a85831e601e4.png)](https://github.com/justinkobel/Virtual-Rounding/blob/master/v2/Documentation/Images/ProviderAppImport-CreateAsNew.png) 16 | 17 | 1. Each of the Flows within the "Related Resources" should be configured with "Create as new". Note, that if you don't see "Set Patient Name", do not worry, this has been removed in v2.1 of the Power App. 18 | 19 | [![](RackMultipart20200512-4-8g3p57_html_b2778d02b680759a.png)](https://github.com/justinkobel/Virtual-Rounding/blob/master/v2/Documentation/Images/ProviderAppImport-CreateFlowsAsNew.png) 20 | 21 | 1. Create a SharePoint Connection and Mail Connection (if one doesn't already exist) respectively. 22 | 2. Click "Import" 23 | 3. You should see a success indicator upon completion. 24 | 25 | [![](RackMultipart20200512-4-8g3p57_html_f77fe3f487575dc2.png)](https://github.com/justinkobel/Virtual-Rounding/blob/master/v2/Documentation/Images/ProviderAppImport-CreateSuccess.png) 26 | 27 | 1. Open the uploaded App in "Edit" mode, and click on the "Data" icon on the left hand side of the interface. 28 | 2. Expand the previous "Virtual Rounding" data source and click "Remove. Expect to see a lot of warnings pop up in your Health indicator. 29 | 30 | [![](RackMultipart20200512-4-8g3p57_html_f582701e96e29e14.png)](https://github.com/justinkobel/Virtual-Rounding/blob/master/v2/Documentation/Images/ProviderAppImport-RemoveConnection.png) 31 | 32 | 1. In the "Data sources" pane, expand out "Connectors", and click on your SharePoint connector. 33 | 34 | [![](RackMultipart20200512-4-8g3p57_html_2c53c1dbfdb8c321.png)](https://github.com/justinkobel/Virtual-Rounding/blob/master/v2/Documentation/Images/ProviderAppImport-AddConnectionStep1.png) 35 | 36 | 1. Connect to your Virtual Rounding site (the v2 one) and select the Virtual Rounding list. If this list was imported as "Virtual Rounding", you're good to go! If you chose an alternative name; you will need to replace references to 'Virtual Rounding' in the Power App formulas with the name of your list. 37 | 2. Share the App "File... Share within the Power Apps studio" with any Providers who will need access to it (this should be via AAD group for larger deployments). You will probably want to un-select the "Send email" prompt until you are ready to share it with the users. 38 | 39 | ## Updating Power Automate Flows 40 | 41 | 1. Navigate to [Flow](https://flow.microsoft.com/) and sign into the system. 42 | 2. Click "Reset Meeting Link" and edit this flow. 43 | 3. On the first "Get item" step, update the step to refer to your Virtual Rounding site/list. Type-ahead on this will likely not work since the Flow was imported, click the "Limit Columns by View" to confirm that you got the URL and List Names correct 44 | 45 | [![](RackMultipart20200512-4-8g3p57_html_6cdcf4c9ffc03990.png)](https://github.com/justinkobel/Virtual-Rounding/blob/master/v2/Documentation/Images/ResetMeetingLink-GetItem.png) 46 | 47 | 1. Populate the client Secret, Application Id, Directory Id, Hours off UTC and Set Time Zone steps with the proper information. 48 | 2. Expand the "Update Item" activity. Replace the Site Address and List Name with your site's information. 49 | 3. Ensure the columns below are populated with the correct information below. There may be "left over" column names like FamilyInvited or "Room\_x0020\_UPN", which you can ignore (and will eventually decide to go away from the editor) 50 | 51 | [![](RackMultipart20200512-4-8g3p57_html_1cbafc8bb18b6aa0.png)](https://github.com/justinkobel/Virtual-Rounding/blob/master/v2/Documentation/Images/ResetMeetingLink-UpdateItem.png) 52 | 53 | 1. Save the Flow, and run a test from the Power App to confirm that patient data, meeting invites, etc. are refreshed inside of the underlying SharePoint Virtual Rounding list. 54 | 2. Click the "Share Meeting Link" Flow and edit the flow. 55 | 3. On the first "Get item" step, update the step to refer to your Virtual Rounding site/list. (See step 4) 56 | 4. Confirm that the "Send an email notification" has the proper message content for your organization. (Some organizations prefer to replace this step with a "Send As" action within Exchange Online, or another mass-mailing platform. The default action will have a "From" address of Send Grid and "Power Apps and Flow" that will frequently wind up in users' Spam folders.) 57 | 5. Update the "Update Item" action like in step 9 above. The formula for "Shared With" should be: add(body('Get\_item')?['SharedWith'], 1) 58 | 6. Click "Save" and test this Flow from the Power App (remember, check your Spam folder) 59 | 60 | If you start to get errors on Flows when testing them, remember sometimes it's easier to create a new one (and use the awesome new "Copy to clipboard" for each Flow action), than try to get a Flow with messed up connections patched up. 61 | 62 | ## Patient App Upload 63 | 64 | 1. Navigate to the Power Apps portal signed in as a user with a Premium Power Automate license assigned to them. Click on "Import" and Upload the PatientJoin\_xxxxx.zip file. 65 | 2. Set "Import Setup" for App as "Create as new" 66 | 3. For Share Meeting Link, select "Create as New", otherwise we'll overwrite all of our hard work down on the previous steps to get the Meeting Link flow matched into your environment. In doing so, give it a name to denote it's temporary and able to be deleted in following steps. 67 | 68 | [![](RackMultipart20200512-4-8g3p57_html_3e1c61e45f84a387.png)](https://github.com/justinkobel/Virtual-Rounding/blob/master/v2/Documentation/Images/PatientJoin-ImportMeetingLinkFlow.png) 69 | 70 | 1. Update the SharePoint and Mail connections just like the Provider app. 71 | 72 | [![](RackMultipart20200512-4-8g3p57_html_6ec73d4032c3d98d.png)](https://github.com/justinkobel/Virtual-Rounding/blob/master/v2/Documentation/Images/PatientJoin-ImportApp.png) 73 | 74 | 1. Click "Import" 75 | 2. You should see a success indicator upon completion. 76 | 77 | [![](RackMultipart20200512-4-8g3p57_html_f77fe3f487575dc2.png)](https://github.com/justinkobel/Virtual-Rounding/blob/master/v2/Documentation/Images/ProviderAppImport-CreateSuccess.png) 78 | 79 | 1. Open the uploaded App in "Edit" mode, and click on the "Data" icon on the left hand side of the interface. 80 | 2. Expand the previous "Virtual Rounding" data source and click "Remove. Expect to see a lot of warnings pop up in your Health indicator. 81 | 82 | [![](RackMultipart20200512-4-8g3p57_html_f582701e96e29e14.png)](https://github.com/justinkobel/Virtual-Rounding/blob/master/v2/Documentation/Images/ProviderAppImport-RemoveConnection.png) 83 | 84 | 1. In the "Data sources" pane, expand out "Connectors", and click on your SharePoint connector. 85 | 86 | [![](RackMultipart20200512-4-8g3p57_html_2c53c1dbfdb8c321.png)](https://github.com/justinkobel/Virtual-Rounding/blob/master/v2/Documentation/Images/ProviderAppImport-AddConnectionStep1.png) 87 | 88 | 1. Connect to your Virtual Rounding site (the v2 one) and select the Virtual Rounding list. If this list was imported as "Virtual Rounding", you're good to go! If you chose an alternative name; you will need to replace references to 'Virtual Rounding' in the Power App formulas with the name of your list. 89 | 2. [what about deleting the other flow] 90 | 3. Share the App with your VirtualRoundingRooms group you used to track all of your Rooms in AAD. You will want to un-select the "Send email" prompt, as users will not be receiving email on their devices. 91 | 92 | ## Branding Updates (Optional) 93 | 94 | You will likely elect to replace logos within the Apps with your organization's logo, and also adjust the app's color scheme. 95 | 96 | ## Embedding the App in Teams for Your Providers and Rooms 97 | 98 | If you wish to embed these Apps below directly in Teams, follow the below steps. Otherwise, the apps may be interacted with via "traditional" Power Apps desktop applications, web browser links, mobile apps, etc. for providers and rooms. 99 | 100 | 1. On the Providers and Patients Power App, click "Edit Settings", and enable "Preload app for enhanced performance" 101 | 102 | [![](RackMultipart20200512-4-8g3p57_html_426a4440f03757f9.png)](https://github.com/justinkobel/Virtual-Rounding/blob/master/v2/Documentation/Images/PowerApps-PreloadingEnable.png) 103 | 104 | 1. For the Providers and Patients App, click "Add to Teams". This will generate a .zip file for each. 105 | 106 | ![](RackMultipart20200512-4-8g3p57_html_49ac0cb03196381.gif) 107 | 108 | 1. Navigate to the "Teams Admin Center... Teams Apps... Manage apps" 109 | 2. Click "Upload new app" and upload each .zip file you just generated. If you are unable to upload apps from here, check your "Org-wide app settings" and confirm that "Custom Apps" are enabled. 110 | 3. Navigate to "Permission Policies" for your App Permission Policies, and enable the "Patient Join" app to be published under "Tenant apps" 111 | 112 | [![](RackMultipart20200512-4-8g3p57_html_96d4aa34f75b9aa1.png)](https://github.com/justinkobel/Virtual-Rounding/blob/master/v2/Documentation/Images/PermissionPolicy-PatientJoin.png) 113 | 114 | 1. Go to your Virtual Rounding Room "Setup Policy" that you configured during the setup policy, and add the "Patient Join" app as a Power App to the profile. 115 | 2. If you wish to publish the Provider Power App, follow similar steps as above, but do not target the "Rooms" policies, but instead "Provider" policies you will need to create to support this scenario. -------------------------------------------------------------------------------- /v2/README.md: -------------------------------------------------------------------------------- 1 | # Virtual Rounding using Microsoft Teams v2 2 | _Updated 4/15/2020_ 3 | 4 | Version 2 Scripts, Flows, and PowerApps are all ready for use. 5 | 6 | Version 2 includes the following improved features: 7 | * Ability to invite family/friends 8 | * Unified SharePoint List that contains all rooms and links 9 | - With views for each sublocation/location still added as tabs in Teams (optional) 10 | * PowerApps for: 11 | - One click join for Patient Rooms 12 | >![Teams Policy](/v2/Documentation/Images/PatientJoinApp.png) 13 | >![Teams Policy](/v2/Documentation/Images/PatientJoinApp2.png) 14 | - Meeting Join and Configuration for Providers 15 | >![Teams Policy](/v2/Documentation/Images/VRApp1.png) 16 | >![Teams Policy](/v2/Documentation/Images/VRApp2.png) 17 | >![Teams Policy](/v2/Documentation/Images/VRApp3.png) 18 | 19 | For deployment assistance, questions or comments, please fill out [this form](https://forms.office.com/Pages/ResponsePage.aspx?id=v4j5cvGGr0GRqy180BHbR6mlTNdIzWRKq7zcu5h9FqNUMVoxSU0yS0hCSVhKMkxRREZaVE1IRU8wVy4u). Someone from Microsoft will reach out as soon as possible. 20 | 21 | ## Overview 22 | 23 | This is the Virtual Rounding solution referenced in the Microsoft Health & Life Sciences [blog post](https://aka.ms/teamsvirtualrounding). Please see that blog post for more an overview of the use case. This repository serves as the technical documentation. 24 | 25 | ## Disclaimer 26 | 27 | _This solution is a sample and may be used with Microsoft Teams for dissemination of reference information only. This solution is not intended or made available for use as a medical device, clinical support, diagnostic tool, or other technology intended to be used in the diagnosis, cure, mitigation, treatment, or prevention of disease or other conditions, and no license or right is granted by Microsoft to use this solution for such purposes. This solution is not designed or intended to be a substitute for professional medical advice, diagnosis, treatment, or judgement and should not be used as such. Customer bears the sole risk and responsibility for any use. Microsoft does not warrant that the solution or any materials provided in connection therewith will be sufficient for any medical purposes or meet the health or medical requirements of any person._ 28 | 29 | ## Solution Design 30 | 31 | A device will be deployed in each patient room needed. These will be referred to as "patient rooms" in this documentation. That device will be locked down in Kiosk mode to the Microsoft Teams application. 32 | 33 | Each patient room will have an associated Office 365 account, with only Microsoft Teams and Exchange Online licensing applied. Custom Teams Policies will be applied to the accounts to limit the capabilities within the Teams application & meetings, including: 34 | 35 | - Disable Chat 36 | - Disable Calling 37 | - Disable Organization browsing 38 | - Disable Meeting & Live Event creation 39 | - Disable Discovery of Private Teams 40 | - Disable Installation/Adding Apps 41 | - Hide all apps except the Patient Join PowerApp 42 | - Disable Meeting Features: Meet Now, Cloud Recording, Transcription, Screen Sharing, PowerPoint Sharing, Whiteboard, Shared Notes, Anonymous user admission, Meeting Chat 43 | 44 | Each patient room will have an ongoing Teams meeting running for a long period of time (months or longer), and that meeting will be reused for that room as patients flow in and out of rooms. As noted in the known limitations, there is a 24 hour timeout; Please see that section for guidance. 45 | 46 | Doctors will not be directly invited to any meetings, but instead have access to a Team or Teams with a list of meetings pinned as a tab (from SharePoint). Doctors will be able to join a Patient Room meeting via the Join URL hyperlink in the list. 47 | 48 | ## Known Limitations & Warnings 49 | 50 | - Patient Room accounts will be able to browse and join public Teams. Limit the presence of those in your directory or deploy Information Barriers to prevent this. 51 | - Patient Room accounts can technically create Teams if this is not already restricted. Consider implementing restrictions to Team creation to these accounts to limit this ability if that is a concern. 52 | - While Patients Room accounts are prevented from exposing PHI during meetings (no chat, whiteboard, or shared notes access), Doctors do not have those same limitations (unless you choose to apply custom meeting policies to Doctors as well). Ensure Doctors have proper training or documentation to _not_ use those features of put PHI in them. Any content posted in those features will be visible to the next patient in the room. 53 | - If a patient goes to the show participants list, they are technically able to invite other users from your directory. There is no current workaround for this besides training and patient supervision. 54 | - If a patient taps/clicks on the doctor's name while in the meeting, they can see Azure AD profile information for that Doctor. There is no current workaround for this besides training and patient supervision. Some hospitals have used generic workstations with generic Teams logins to get around this for the doctors. 55 | - This solution is built with cloud only Azure AD Accounts in mind for the Patient Room accounts. Any variation from that will have to be coded manually. 56 | - This solution relies on familiarity with PowerShell, Azure AD Admin, Teams Admin, and Power Automate (formerly known as Microsoft Flow). and may require customization for your specific environment. Please fill out [this form](https://forms.office.com/Pages/ResponsePage.aspx?id=v4j5cvGGr0GRqy180BHbR6mlTNdIzWRKq7zcu5h9FqNUMVoxSU0yS0hCSVhKMkxRREZaVE1IRU8wVy4u) to contact us if you need assistance. 57 | - A single participant cannot be joined to a meeting for more than 24 hours. The devices will need to rejoin the meetings at least once every 24 hours. To avoid service interruptions, consider training floor staff that is already in the room once daily to end and rejoin the meeting. 58 | 59 | ## Prerequisites 60 | 61 | ### Admin Access: 62 | 63 | Access to a Global Administrator account (for Application Consent) 64 | 65 | ### Licensing: 66 | 67 | Appropriate licensing for this solution may vary depending on your current agreement and configuration of the virtual rounding solution. Please contact your Microsoft account team for accurate licensing requirement information. 68 | 69 | Depending on the use of agreement, the following is one example of a licensing solution for virtual rounding: 70 | 71 | - Microsoft Teams License for each Patient Room account - minimum of F3 required. 72 | - Power Automate per flow license for all users who will access the app (not Patient Room accounts). 73 | - Version 1 of Virtual Rounding is still supported and maintained and does not require this additional licensing. 74 | - Optional: EM+S licenses for management of Patient Room devices and identities 75 | 76 | # Configuration 77 | 78 | 79 | ## Create Teams Policies 80 | 81 | Create Policies in the Microsoft Teams Admin Center matching the below policies. The screenshots below are recommended configuration, but you should configure to your organization's policy/needs. 82 | 83 | ### Teams Policy 84 | ![Teams Policy](/v2/Documentation/Images/TeamsPolicy.png) 85 | ### Meeting Policy 86 | ![Meeting Policy1](/v2/Documentation/Images/MeetingPolicy1.png) 87 | ![Meeting Policy2](/v2/Documentation/Images/MeetingPolicy2.png) 88 | ### Live Events Policy 89 | ![Live Events Policy](/v2/Documentation/Images/LiveEventsPolicy.png) 90 | ### Messaging Policy 91 | ![Messaging Policy](/v2/Documentation/Images/MessagingPolicy.png) 92 | ### App Permission Policy 93 | ![App Permission Policy](/v2/Documentation/Images/AppPermissionPolicy.png) 94 | ### App Setup Policy 95 | ![App Setup Policy](/v2/Documentation/Images/AppSetupPolicy.png) 96 | ### Calling Policy 97 | ![Calling Policy](/v2/Documentation/Images/CallingPolicy.png) 98 | 99 | ## Application Registration 100 | 101 | For various steps in this process we will need to call the Microsoft Graph. To do that, an app registration is required in Azure AD. This will require a Global Administrator account. 102 | 103 | 1. Navigate to [https://aad.portal.azure.com/#blade/Microsoft\_AAD\_IAM/ActiveDirectoryMenuBlade/RegisteredApps](https://aad.portal.azure.com/#blade/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/RegisteredApps) and sign in as a Global Administrator. 104 | 2. Click New Registration. 105 | 3. Provide an application name, select "Accounts in this organizational directory only", and leave Redirect URI blank. Click Register. 106 | 4. Note down Application and Directory IDs to use later. 107 | 5. From the left menu, click "API permissions" to grant some permissions to the application. 108 | 6. Click "+ Add a permission". 109 | 7. Select "Microsoft Graph". 110 | 8. Select Application permissions. 111 | 9. Add the following permissions: Calendars.ReadWrite, Group.ReadWrite.All, OnlineMeetings.ReadWrite.All 112 | 10. Click "Grant admin consent for …" 113 | 11. From the left menu, click "Certificates & secrets". 114 | 12. Under "Client secrets", click "+ New client secret". 115 | 13. Provide a description and select an expiry time for the secret and click "Add". 116 | 14. Note down the secret Value. 117 | 118 | ## SharePoint Site/List Account Setup 119 | 120 | In this repository is a PowerShell script (SetupSPO.ps1) that: 121 | 122 | 1. Creates a SharePoint Site 123 | 2. Creates a list 124 | 3. Adds custom columns and content type to list. 125 | 126 | Before running this script, you will need the following: 127 | 128 | - A Global Admin Account 129 | - SharePoint Online PnP PowerShell: [https://docs.microsoft.com/en-us/powershell/sharepoint/sharepoint-pnp/sharepoint-pnp-cmdlets?view=sharepoint-ps](https://docs.microsoft.com/en-us/powershell/sharepoint/sharepoint-pnp/sharepoint-pnp-cmdlets?view=sharepoint-ps) 130 | 131 | ### Script 132 | 133 | All variables and supporting files will need to be specified in the RunningConfig.json file you can find in this repository. The only varialbe the script needs configured manually is the location of that JSON configuration file. 134 | 135 | Once the above is ready, you can run SetupSPO.ps1. As with all open source scripts, please test and review before running in your production environment. 136 | 137 | When prompted, sign in with global administrator credentials. 138 | 139 | 140 | ## Patient Room Account Setup 141 | 142 | In this repository is a PowerShell script (CreateRooms.ps1) that: 143 | 144 | 1. Creates the accounts 145 | 2. Adds the account to a group (for tracking and group based licensing) 146 | 3. Applies Custom Teams Policies 147 | 148 | Before running this script, you will need the following: 149 | 150 | - An Azure AD Security Group 151 | - This group should be empty, and will only be used for the patient room accounts. If provisioning manually, ensure all users are added to this group. It will be used later for licensing and in the flows. 152 | - You will also need to setup Group Based Licensing for the Azure AD Security Group. Please assign an Office 365 license to the group and disable all assignment options except for Microsoft Teams, Skype for Business and Exchange Online. Detailed instructions for Group Based Licensing can be found here: [https://docs.microsoft.com/en-us/azure/active-directory/users-groups-roles/licensing-groups-assign](https://docs.microsoft.com/en-us/azure/active-directory/users-groups-roles/licensing-groups-assign). 153 | - A CSV file with the desired Patient Room account information 154 | - Columns: 155 | - AccountName 156 | - Desired Room Name. Ensure this is easily identifiable to your clinical staff. 157 | - AccountUPN 158 | - Desired UserPrincipalName (email address) of the room. 159 | - This will only be used for login to the Teams application on the device. 160 | - Do not use a domain that is federated (ADFS, Ping, etc), as this will be a cloud only account. 161 | - Must not conflict with any other accounts in your directory. 162 | - AccountPassword 163 | - Must comply with your Azure AD Password Policies 164 | - AccountLocation 165 | - Building or Location of the room. This will be used to categorize the rooms later on and will be tied to a Team name (a suffix can be added) (see _Team/List/Tab_ sections below). 166 | - This will be filled into the "Department" field of the user. 167 | - AccountSubLocation 168 | - Sub Location of the room (example: Floor 1). This will be used to categorize the rooms later on and must be tied to either a Channel name (see _Team/List/Tab_ sections below). 169 | - This will be filled into the "Job Title" field of the user. 170 | - Sample file available (RoomAccounts.csv) 171 | - Azure AD PowerShell Module v2: [https://docs.microsoft.com/en-us/powershell/azure/active-directory/install-adv2?view=azureadps-2.0](https://docs.microsoft.com/en-us/powershell/azure/active-directory/install-adv2?view=azureadps-2.0) 172 | - Skype for Business Online PowerShell: [https://docs.microsoft.com/en-us/office365/enterprise/powershell/manage-skype-for-business-online-with-office-365-powershell](https://docs.microsoft.com/en-us/office365/enterprise/powershell/manage-skype-for-business-online-with-office-365-powershell) 173 | 174 | ### Script 175 | 176 | All variables and supporting files will need to be specified in the RunningConfig.json file you can find in this repository. The only varialbe the script needs configured manually is the location of that JSON configuration file. 177 | 178 | Once the above is ready, you can run CreateRooms.ps1. As with all open source scripts, please test and review before running in your production environment. 179 | 180 | When prompted, sign in with administrator credentials that are able to create Azure AD accounts and assign Teams policies. 181 | 182 | ## Team/Channel/Tab Creation (Optional) 183 | 184 | If you chose to use Teams and Channels to for providers to join/configure meetings/rooms, please see this link. 185 | 186 | ## Patient Room Meeting Setup 187 | 188 | ## Meeting Creation 189 | 190 | To create the meetings, we will use Power Automate. Power Automate offers a simple way to call the Microsoft Graph API, and the ability to run on a regular basis if we need in the future. 191 | 192 | Prerequisites: 193 | 194 | - A Power Automate Premium license will be required for this piece (P1, P2, Per User or Per App all work). 195 | - An account with the Power Automate license applied to it, used for creating the Flows (ideally a service account). 196 | - SetupMeetingsFlow.zip from this repository 197 | - Get the Group GUID/ObjectID for your Azure AD Group used in _Patient Room Account Setup_ (find in the group properties in the Azure AD Portal) 198 | 199 | Instructions: 200 | 201 | 1. Login to flow.microsoft.com 202 | 2. Click on "My flows". 203 | 3. Click "Import". 204 | 4. Upload SetupMeetingsFlow.zip 205 | 5. Update all variables, the SharePoint Site base URL in the final step of the flow, and the Group ID. 206 | 207 | Once it's been at least 3 hours since you've created the room accounts, you can run the Flow to create all the meeting links. Ideally, wait at least 24 hours. This is to ensure the Teams Policies properly apply to the room accounts before a meeting is created. 208 | 209 | # Power Apps 210 | 211 | ## Patient Join App 212 | Upload the Patient Join App to PowerApps. The connection to the master SharePoint list in both the PowerApp and the associated flow will need to be updated for proper functionality. 213 | 214 | This app then should be published installed on the device. 215 | 216 | These instructions will be defined in more detail soon. Please work with a Microsoft Partner in the meantime. 217 | 218 | ## Virtual Rounding App 219 | Upload the Patient Join App to PowerApps. The connection to the master SharePoint list in the PowerApp and the associated flows will need to be updated for proper functionality. 220 | 221 | This app then should be published in Teams and made available to providers. 222 | 223 | These instructions will be defined in more detail soon. Please work with a Microsoft Partner in the meantime. 224 | 225 | # Security Controls 226 | 227 | ## Mobile Device Management 228 | 229 | We strongly recommend managing the devices with Intune MDM and enabling kiosk mode. More detailed instructions will be added here. 230 | 231 | ## Conditional Access Policies 232 | 233 | We strongly recommend applying a conditional access policy to the Azure AD Group used in _Patient Room Account Setup_ (contains all Patient Room accounts). This policy should limit sign ins to either Intune Managed Devices or specific trusted IPs. This is to limit the risk of the account becoming compromised and a third party logging into an ongoing patient meeting. 234 | -------------------------------------------------------------------------------- /v1/Scripts/CreateTeamsAndSPO.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | DISCLAIMER: 3 | ---------------------------------------------------------------- 4 | This sample is provided as is and is not meant for use on a production environment. 5 | It is provided only for illustrative purposes. The end user must test and modify the 6 | sample to suit their target environment. 7 | Microsoft can make no representation concerning the content of this sample. Microsoft 8 | is providing this information only as a convenience to you. This is to inform you that 9 | Microsoft has not tested the sample and therefore cannot make any representations 10 | regarding the quality, safety, or suitability of any code or information found here.   11 | #> 12 | 13 | <# 14 | INSTRUCTIONS: 15 | Please see https://aka.ms/virtualroundingcode 16 | #> 17 | 18 | #--------------------------Variables---------------------------# 19 | $configFilePath = ".\Scripts\RunningConfig.json" 20 | $configFile = Get-Content -Path $configFilePath | ConvertFrom-Json 21 | 22 | $locationsCsvFile = $configFile.LocationCsvPaths.Locations 23 | $subLocationsCsvFile = $configFile.LocationCsvPaths.SubLocations 24 | $groupOwner = $configFile.TenantInfo.MeetingSchedulingUser 25 | $spviewJsonFilePath = $configFile.ViewJson.SPViewJsonFilePath 26 | $teamNameSuffix = $configFile.GroupConfiguration.RoundingTeamSuffix 27 | $clientId = $configFile.ClientCredential.Id 28 | $clientSecret = $configFile.ClientCredential.Secret 29 | $tenantName = $configFile.TenantInfo.TenantName 30 | $sharepointBaseUrl = $configFile.TenantInfo.SPOBaseUrl 31 | 32 | $useMFA = $configFile.TenantInfo.MFARequired 33 | $adminUPN = $configFile.TenantInfo.GlobalAdminUPN 34 | 35 | #--------------------------Functions---------------------------# 36 | Function Test-Existence { 37 | [CmdletBinding()] 38 | param( 39 | $value, 40 | $errorMsg 41 | ) 42 | try{ 43 | if($value){ 44 | return $true 45 | } 46 | else{ 47 | throw $errorMsg 48 | } 49 | } 50 | catch{ 51 | Write-Error $_ 52 | } 53 | } 54 | 55 | Function Ask-User { 56 | [CmdletBinding()] 57 | param( 58 | $prompt 59 | ) 60 | Write-Host ($prompt + " (Default is Yes)") -ForegroundColor Yellow 61 | $readHost = Read-Host " ( y / n )" 62 | Switch ($readHost){ 63 | Y {return $true} 64 | N {return $false} 65 | Default {return $true} 66 | } 67 | } 68 | #-------------------------Script Setup-------------------------# 69 | if (!$useMFA) {$creds = Get-Credential -Message 'Please sign in to your Global Admin account:' -UserName $adminUPN} 70 | 71 | Test-Existence((Get-Module AzureAD-Preview),'The AzureAD Module is not installed. Please see https://aka.ms/virtualroundingcode for more details.') -ErrorAction Stop 72 | Import-Module AzureAD 73 | if ($useMFA) {Connect-AzureAD -ErrorAction Stop} 74 | else {Connect-AzureAD -Credential $creds -ErrorAction Stop} 75 | 76 | $groupOwner = $adminUPN 77 | 78 | Test-Existence((Get-Module MicrosofTeams),'The MicrosoftTeams Module is not installed. Please see https://aka.ms/virtualroundingcode for more details.') -ErrorAction Stop 79 | Import-Module MicrosoftTeams 80 | if ($useMFA) {Connect-MicrosoftTeams -ErrorAction Stop} 81 | else {Connect-MicrosoftTeams -Credential $creds -ErrorAction Stop} 82 | 83 | Test-Existence((Get-Module SharePointPnPPowerShellOnline),'The SharePointPnPPowerShellOnline Module is not installed. Please see https://aka.ms/virtualroundingcode for more details.') -ErrorAction Stop 84 | Import-Module SharePointPnPPowerShellOnline -WarningAction SilentlyContinue #Always outputs warning due to unapproved verbs 85 | 86 | #Import CSV of Teams 87 | $locationsList = Import-Csv -Path $locationsCsvFile 88 | 89 | #Import CSV of Channels/Lists 90 | $subLocationsList = Import-Csv -Path $subLocationsCsvFile 91 | 92 | #Import JSON formatting file 93 | $jsonContent = Get-Content $spviewJsonFilePath 94 | $jsonContent | ConvertFrom-Json | Out-Null 95 | 96 | #Prepare Microsoft Graph API Calls 97 | $ReqTokenBody = @{ 98 | Grant_Type = "client_credentials" 99 | Scope = "https://graph.microsoft.com/.default" 100 | client_Id = $clientID 101 | Client_Secret = $clientSecret 102 | } 103 | $TokenResponse = Invoke-RestMethod -Uri "https://login.microsoftonline.com/$TenantName/oauth2/v2.0/token" -Method POST -Body $ReqTokenBody 104 | 105 | #-----------------Create Teams and add Members-----------------# 106 | foreach ($location in $locationsList) { 107 | #Create Team with policies 108 | $teamName = $location.LocationName + " " + $teamNameSuffix 109 | $teamShortName = $location.LocationName.replace(' ', '') 110 | #check for existing Team/Site 111 | $existingTeamName = Get-Team -DisplayName $teamName 112 | $existingTeamMail = Get-Team -MailNickName $teamShortName 113 | if ($existingTeamName -or $existingTeamMail) { 114 | $userOption = Ask-User("Existing team found for '$teamName' or '$teamShortName'. Would you like to continue and use this existing team?") 115 | if ($userOption -eq $false) { Write-Host "Script stopping by user request" -ForegroundColor Red -ErrorAction Stop } 116 | } 117 | else { 118 | Write-Host "Creating '$teamName' team." -ForegroundColor Green 119 | New-Team -DisplayName $teamName -Visibility Private -Owner $groupOwner -AllowAddRemoveApps $false -AllowCreateUpdateChannels $false -AllowCreateUpdateRemoveConnectors $false -AllowCreateUpdateRemoveTabs $false -AllowDeleteChannels $false -MailNickName $teamShortName -ErrorAction Stop 120 | #Add members to team 121 | $teamID = (Get-AzureADGroup -Filter "DisplayName eq '$teamName'").ObjectID 122 | #If team has not provisioned yet to AzureAD, keep checking every minute 123 | while (!$teamID) { 124 | Write-Host "Team '$teamName' needs more time to provision before members can be added. Trying agin in 60 seconds." -ForegroundColor Green 125 | Start-sleep -Seconds 60 126 | $teamID = (Get-AzureADGroup -Filter "DisplayName eq '$teamName'").ObjectID 127 | } 128 | $groupName = $location.MembersGroupName 129 | $groupID = (Get-AzureADGroup -Filter "DisplayName eq '$groupName'").ObjectID 130 | #Ask for new Group Name if none or more than one were found 131 | while (!($groupID) -or ($groupID.count -gt 1)){ 132 | $userOption = Ask-User("Unable to find Azure AD Group with the exact namne of '$groupName', or found multiple. Would you like to cancel or specify a new group name?") 133 | if ($userOption -eq $false) { Write-Host "Script stopping by user request" -ForegroundColor Red -ErrorAction Stop } 134 | else { 135 | $groupName = Read-Host "Exact Group Name of users to be added to '$teamName' Team:" 136 | $groupID = (Get-AzureADGroup -Filter "DisplayName eq '$groupName'").ObjectID 137 | } 138 | } 139 | $groupMembers = Get-AzureADGroupMember -ObjectId $groupID 140 | Write-Host "Adding Group Membership to '$teamName' team." -ForegroundColor Green 141 | foreach ($member in $groupMembers) { 142 | Add-AzureADGroupMember -ObjectId $teamID -RefObjectId $member.ObjectID -ErrorAction SilentlyContinue #silentlycontinue if user is already in the Team 143 | } 144 | 145 | $teamSpoUrl = $sharepointBaseUrl + "sites/" + $teamShortName 146 | $siteReady = $false 147 | Write-Host "Connecting to SharePoint Online: $teamSpoUrl" -ForegroundColor Green 148 | while ($siteReady -eq $false) { 149 | try { 150 | if ($useMFA) { Connect-PnPOnline -Url $teamSpoUrl -UseWebLogin } 151 | else { Connect-PnPOnline -Url $teamSpoUrl -Credential $creds } 152 | $siteReady = $true 153 | } 154 | catch { 155 | $userOption2 = Ask-User("Unable to connect to SharePoint Site. This is commonly a result of a provisioning delay. Would you like to have the script pause for 5 minutes and try again?") 156 | if ($userOption2 -eq $false) { Write-Host "Script stopping by user request" -ForegroundColor Red -ErrorAction Stop } 157 | if ($userOption2 -eq $true) { Write-Host "Pausing for 5 minutes to wait for SharePoint Site Provisioning." -ForegroundColor Green; Start-Sleep -Seconds 300 } 158 | } 159 | } 160 | #--------------------Setup Site Colums---------------------# 161 | Write-Host "Setting up Site Columns & Content Type" -ForegroundColor Green 162 | Add-PnPField -Type Text -InternalName "RoomLocation" -DisplayName "Room Location" -Group "VirtualRounding" 163 | Add-PnPField -Type Text -InternalName "RoomSubLocation" -DisplayName "Room SubLocation" -Group "VirtualRounding" 164 | Add-PnPField -Type URL -InternalName "MeetingLink" -DisplayName "Meeting Link" -Group "VirtualRounding" 165 | Add-PnPField -Type Text -InternalName "EventID" -DisplayName "EventID" -Group "VirtualRounding" 166 | Add-PnPContentType -Name "VirtualRoundingRoom" -Group "VirtualRounding" | Out-Null 167 | Start-Sleep -Seconds 5 168 | $contentType = $null 169 | while (!$contentType) { 170 | try { 171 | $contentType = Get-PnPContentType -Identity "VirtualRoundingRoom" 172 | } 173 | catch { 174 | Write-Host "Content Type is not provisioned yet. Waiting 1 minute for provisioning. This will repeat each minute until ready." -ForegroundColor Yellow 175 | Start-Sleep 60 176 | } 177 | } 178 | Write-Host "Adding Columns to ContentType" -ForegroundColor Green 179 | Add-PnPFieldToContentType -Field "RoomLocation" -ContentType $contentType 180 | Add-PnPFieldToContentType -Field "RoomSubLocation" -ContentType $contentType 181 | Add-PnPFieldToContentType -Field "MeetingLink" -ContentType $contentType 182 | Add-PnPFieldToContentType -Field "EventID" -ContentType $contentType 183 | Write-Host "Disconnecting SharePoint Online" -ForegroundColor Green 184 | Disconnect-PnPOnline 185 | } 186 | } 187 | 188 | foreach ($sublocation in $sublocationsList) { 189 | #--------------Create Lists and Add Columns----------------# 190 | $sublocationName = $sublocation.LocationSubName 191 | $sublocationShortName = $sublocationName.replace('-','') 192 | $teamName = $sublocation.LocationName + " " + $teamNameSuffix 193 | $teamID = (Get-AzureADGroup -Filter "DisplayName eq '$teamName'").ObjectID 194 | $teamShortName = $sublocation.LocationName.replace(' ','') 195 | $teamSpoUrl = $sharepointBaseUrl + "sites/" + $teamShortName 196 | 197 | Write-Host "Connecting to SharePoint Online: $teamSpoUrl" -ForegroundColor Green 198 | if ($useMFA) { Connect-PnPOnline -Url $teamSpoUrl -UseWebLogin } 199 | else { Connect-PnPOnline -Url $teamSpoUrl -Credential $creds } 200 | 201 | $contentType = Get-PnPContentType -Identity "VirtualRoundingRoom" 202 | 203 | $list = Get-PnPList -Identity ("Lists/" + $sublocationShortName) 204 | if ($list) { 205 | $userOption = Ask-User("An existing list for '$sublocationName' already exists. Would you like to use this existing list?") 206 | if ($userOption -eq $false) { Write-Host "Script stopping by user request" -ForegroundColor Red -ErrorAction Stop } 207 | } 208 | else { 209 | Write-Host "Creating SharePoint List '$sublocationName' in the '$teamName' Team." -ForegroundColor Green 210 | New-PnPList -Title $sublocationName -Url "Lists/$sublocationshortName" -Template GenericList -ErrorAction SilentlyContinue | Out-Null #Bug in PnP cmdlet, so SilentlyContinue required 211 | Start-Sleep -Seconds 5 212 | while(!$list){ 213 | try { 214 | $list = Get-PnPList -Identity ("Lists/" + $sublocationShortName) 215 | } 216 | catch { 217 | Write-Host "List is not provisioned yet. Waiting 1 minute for provisioning. This will repeat each minute until ready." -ForegroundColor Yellow 218 | Start-Sleep 60 219 | } 220 | } 221 | } 222 | Write-Host "Enabling Content Types for SharePoint List '$sublocationName' in the '$teamName' Team." -ForegroundColor Green 223 | Set-PnPList -Identity $sublocationShortName -EnableContentTypes $true -ErrorAction SilentlyContinue | Out-Null #Bug in PnP cmdlet, so SilentlyContinue required 224 | Write-Host "Adding 'VirtualRoundingRoom' Content Type to SharePoint List '$sublocationName' in the '$teamName' Team." -ForegroundColor Green 225 | Add-PnPContentTypeToList -List $list -ContentType $contentType -DefaultContentType -ErrorAction SilentlyContinue | Out-Null #Bug in PnP cmdlet, so SilentlyContinue required 226 | $newContentType = $null 227 | while(!$newContentType){ 228 | try { 229 | $newContentType = Get-PnPContentType -List $list | Where-Object{$_.Name -eq "VirtualRoundingRoom"} 230 | } 231 | catch { 232 | Write-Host "Content Type is not added to list yet. Waiting 1 minute for provisioning. This will repeat each minute until ready." -ForegroundColor Yellow 233 | Start-Sleep 60 234 | } 235 | } 236 | Write-Host "Adding 'Meetings' View to SharePoint List '$sublocationName' in the '$teamName' Team." -ForegroundColor Green 237 | Add-PnPView -List $list -Title Meetings -SetAsDefault -Fields Title, RoomLocation, MeetingLink -ErrorAction SilentlyContinue | Out-Null #Bug in PnP cmdlet, so siletlycontinue required 238 | Write-Host "Pausing for 20 seconds for provisioning." -ForegroundColor Green 239 | Start-Sleep -Seconds 20 240 | $view = $null 241 | while(!$view){ 242 | try { 243 | $view = Get-PnPView -List $list -Identity Meetings 244 | } 245 | catch { 246 | Write-Host "The view is not ready yet. Waiting 1 minute for provisioning. This will repeat each minute until ready." -ForegroundColor Yellow 247 | Start-Sleep 60 248 | } 249 | } 250 | $view.CustomFormatter = $jsonContent 251 | $view.Update() 252 | $view.Context.ExecuteQuery() 253 | $viewUrl = ($teamSpoUrl + "/Lists/" + $sublocationShortName + "/Meetings.aspx") 254 | $viewUrlEncoded = [System.Web.HTTPUtility]::UrlEncode($viewUrl) 255 | $viewUrl = $viewUrl.replace(" ","%20") #Needs to be after the encoding step otherwise encoding will encode the '%' symbol 256 | Write-Host "Disconnecting SharePoint Online" -ForegroundColor Green 257 | Disconnect-PnPOnline 258 | #-------------------Create Channels------------------------# 259 | #Create Channel 260 | Write-Host "Creating channel '$sublocationName' in the '$teamName' Team." -ForegroundColor Green 261 | $channelApiUrl = ("https://graph.microsoft.com/beta/teams/" + $teamID + "/channels") 262 | $channelBody = @" 263 | {"displayName": "$sublocationName"} 264 | "@ 265 | $newChannel = Invoke-RestMethod -Headers @{Authorization = "Bearer $($Tokenresponse.access_token)" } -Uri $channelApiUrl -Body $channelBody -Method Post -ContentType 'application/json' 266 | #Add SPO List as Tab (need to make it non-hidden) 267 | Write-Host "Adding 'Join a Room' tab to channel '$sublocationName' in the '$teamName' Team." -ForegroundColor Green 268 | $tabApiUrl = ("https://graph.microsoft.com/beta/teams/" + $teamID + "/channels/" + $newChannel.id + "/tabs") 269 | $tabBody = @" 270 | { 271 | "displayName": "Join a Room", 272 | "teamsApp@odata.bind" : "https://graph.microsoft.com/v1.0/appCatalogs/teamsApps/2a527703-1f6f-4559-a332-d8a7d288cd88", 273 | "configuration": { 274 | "entityId": "sharepointtab_0.8309667588452743", 275 | "contentUrl": "$teamSpoUrl/_layouts/15/teamslogon.aspx?spfx=true&dest=$viewUrlEncoded", 276 | "websiteUrl": "$viewUrl", 277 | "removeUrl": null 278 | } 279 | } 280 | "@ 281 | Invoke-RestMethod -Headers @{Authorization = "Bearer $($Tokenresponse.access_token)" } -Uri $tabApiUrl -Body $tabBody -Method Post -ContentType 'application/json' 282 | #Get Wiki Tab 283 | Write-Host "Removing 'Wiki' tab from channel '$sublocationName' in the '$teamName' Team." -ForegroundColor Green 284 | $tabs = Invoke-RestMethod -Headers @{Authorization = "Bearer $($Tokenresponse.access_token)" } -Uri $tabApiUrl -Method GET -ContentType 'application/json' 285 | $wikiID = ($tabs.value | Where-Object { $_.name -eq "Wiki" }).id 286 | #Delete Wiki Tab 287 | $wikiApiUrl = ("https://graph.microsoft.com/beta/teams/" + $teamID + "/channels/" + $newChannel.id + "/tabs/" + $wikiID) 288 | Invoke-RestMethod -Headers @{Authorization = "Bearer $($Tokenresponse.access_token)" } -Uri $wikiApiUrl -Method DELETE -ContentType 'application/json' 289 | } -------------------------------------------------------------------------------- /v1/README.md: -------------------------------------------------------------------------------- 1 | # Virtual Rounding using Microsoft Teams 2 | 3 | _Version: 1.1 4 | Updated 4/8/2020_ 5 | 6 | For Microsoft customers with deployment assistance, questions or comments, please fill out [this form](https://forms.office.com/Pages/ResponsePage.aspx?id=v4j5cvGGr0GRqy180BHbR6mlTNdIzWRKq7zcu5h9FqNUMVoxSU0yS0hCSVhKMkxRREZaVE1IRU8wVy4u). Someone from Microsoft will reach out as soon as possible. If you are a Microsoft Partner, please reach out to your partner team to connect with us. The form is for customers only. 7 | 8 | ## Changelog 9 | ### Version 1.1 10 | Date: 4/8/2020 11 | * All scripts now have added delays after crucial steps to ensure provisioning of resources, and extra catches to ensure more time is given for provisioning when necessary. 12 | * All scripts no longer need direct modification for variables. A single JSON file is used for all variables, and scripts shouldn't need modifications unless you have desired customizations. 13 | * Bug identified causing meetings to end in the following situation: 14 | + Room sitting in meeting -> Provider Joins for a certain period of time -> Provider leaves meeting -> Meeting ends 30 minutes later if no other providers join (only one user in the meeting) 15 | + A new part of the Virtual Rounding solution has been added to solve this bug. There is now a free meeting bot you can deploy to always be joined to the meeting and serve as a constant second meeting participant to ensure the 30 minute timer does not apply. 16 | + Please note that there are new API permissions required in the Azure AD App registration to support this solution. 17 | 18 | ## Overview 19 | 20 | This is the Virtual Rounding solution referenced in the Microsoft Health & Life Sciences [blog post](https://aka.ms/teamsvirtualrounding). Please see that blog post for more an overview of the use case. This repository serves as the technical documentation. 21 | 22 | ## Disclaimer 23 | 24 | _This solution is a sample and may be used with Microsoft Teams for dissemination of reference information only. This solution is not intended or made available for use as a medical device, clinical support, diagnostic tool, or other technology intended to be used in the diagnosis, cure, mitigation, treatment, or prevention of disease or other conditions, and no license or right is granted by Microsoft to use this solution for such purposes. This solution is not designed or intended to be a substitute for professional medical advice, diagnosis, treatment, or judgement and should not be used as such. Customer bears the sole risk and responsibility for any use. Microsoft does not warrant that the solution or any materials provided in connection therewith will be sufficient for any medical purposes or meet the health or medical requirements of any person._ 25 | 26 | ## Solution Design 27 | 28 | A device will be deployed in each patient room needed. These will be referred to as "patient rooms" in this documentation. That device will be locked down in Kiosk mode to the Microsoft Teams application. 29 | 30 | Each patient room will have an associated Office 365 account, with only Microsoft Teams and Exchange Online licensing applied. Custom Teams Policies will be applied to the accounts to limit the capabilities within the Teams application & meetings, including: 31 | 32 | - Disable Chat 33 | - Disable Calling 34 | - Disable Organization browsing 35 | - Disable Meeting & Live Event creation 36 | - Disable Discovery of Private Teams 37 | - Disable Installation/Adding Apps 38 | - Hide all apps except Calendar 39 | - Disable Meeting Features: Meet Now, Cloud Recording, Transcription, Screen Sharing, PowerPoint Sharing, Whiteboard, Shared Notes, Anonymous user admission, Meeting Chat 40 | 41 | Each patient room will have an ongoing Teams meeting running for a long period of time (months or longer), and that meeting will be reused for that room as patients flow in and out of rooms. As noted in the known limitations, there is a 24 hour timeout; Please see that section for guidance. 42 | 43 | Doctors will not be directly invited to any meetings, but instead have access to a Team or Teams with a list of meetings pinned as a tab (from SharePoint). Doctors will be able to join a Patient Room meeting via the Join URL hyperlink in the list. 44 | 45 | ## Known Limitations & Warnings 46 | 47 | - Patient Room accounts will be able to browse and join public Teams. Limit the presence of those in your directory or deploy Information Barriers to prevent this. 48 | - Patient Room accounts can technically create Teams if this is not already restricted. Consider implementing restrictions to Team creation to these accounts to limit this ability if that is a concern. 49 | - While Patients Room accounts are prevented from exposing PHI during meetings (no chat, whiteboard, or shared notes access), Doctors do not have those same limitations (unless you choose to apply custom meeting policies to Doctors as well). Ensure Doctors have proper training or documentation to _not_ use those features of put PHI in them. Any content posted in those features will be visible to the next patient in the room. 50 | - If a patient goes to the show participants list, they are technically able to invite other users from your directory. There is no current workaround for this besides training and patient supervision. 51 | - If a patient taps/clicks on the doctor's name while in the meeting, they can see Azure AD profile information for that Doctor. There is no current workaround for this besides training and patient supervision. Some hospitals have used generic workstations with generic Teams logins to get around this for the doctors. 52 | - This solution is built with cloud only Azure AD Accounts in mind for the Patient Room accounts. Any variation from that will have to be coded manually. 53 | - This solution relies on familiarity with PowerShell, Azure AD Admin, Teams Admin, and Power Automate (formerly known as Microsoft Flow). and may require customization for your specific environment. Please fill out [this form](https://forms.office.com/Pages/ResponsePage.aspx?id=v4j5cvGGr0GRqy180BHbR6mlTNdIzWRKq7zcu5h9FqNUMVoxSU0yS0hCSVhKMkxRREZaVE1IRU8wVy4u) to contact us if you need assistance. 54 | - A single participant cannot be joined to a meeting for more than 24 hours. The devices will need to rejoin the meetings at least once every 24 hours. To avoid service interruptions, consider training floor staff that is already in the room once daily to end and rejoin the meeting. 55 | 56 | ## Prerequisites 57 | 58 | - Access to a Global Administrator account (for Application Consent) 59 | - A service account with a Power Automate Premium license 60 | - If not available, PowerShell can be used instead of Power Automate (this will require customization not available in this repository) 61 | - Enough Office 365 Licenses for each Patient Room account (any License SKU that includes Microsoft Teams and Exchange Online [plan 1 or 2]; If needed, please contact your Microsoft Account team for the E1 trial available during COVID-19) 62 | - Optional: EM+S licenses for management of Patient Room devices and identities 63 | 64 | # Configuration 65 | 66 | All configuration steps below assume that you would like to set this up at scale with a large amount of accounts. If you would like to test out the solution or POC with a smaller amount of user accounts, no scripting or Power Automate is needed. Simply follow along but skip running scripts/flows and instead manually complete the same steps that are listed for each script/flow. 67 | 68 | ## Create Teams Policies 69 | 70 | Create Policies in the Microsoft Teams Admin Center matching the below policies. The screenshots below are recommended configuration, but you should configure to your organization's policy/needs. 71 | 72 | ### Teams Policy 73 | ![Teams Policy](/Documentation/Images/TeamsPolicy.png) 74 | ### Meeting Policy 75 | ![Meeting Policy1](/Documentation/Images/MeetingPolicy1.png) 76 | ![Meeting Policy2](/Documentation/Images/MeetingPolicy2.png) 77 | ### Live Events Policy 78 | ![Live Events Policy](/Documentation/Images/LiveEventsPolicy.png) 79 | ### Messaging Policy 80 | ![Messaging Policy](/Documentation/Images/MessagingPolicy.png) 81 | ### App Permission Policy 82 | ![App Permission Policy](/Documentation/Images/AppPermissionPolicy.png) 83 | ### App Setup Policy 84 | ![App Setup Policy](/Documentation/Images/AppSetupPolicy.png) 85 | ### Calling Policy 86 | ![Calling Policy](/Documentation/Images/CallingPolicy.png) 87 | 88 | ## Application Registration 89 | 90 | For various steps in this process we will need to call the Microsoft Graph. To do that, an app registration is required in Azure AD. This will require a Global Administrator account. 91 | 92 | 1. Navigate to [https://aad.portal.azure.com/#blade/Microsoft\_AAD\_IAM/ActiveDirectoryMenuBlade/RegisteredApps](https://aad.portal.azure.com/#blade/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/RegisteredApps) and sign in as a Global Administrator. 93 | 2. Click New Registration. 94 | 3. Provide an application name, select "Accounts in this organizational directory only", and leave Redirect URI blank. Click Register. 95 | 4. Note down Application and Directory IDs to use later. 96 | 5. From the left menu, click "API permissions" to grant some permissions to the application. 97 | 6. Click "+ Add a permission". 98 | 7. Select "Microsoft Graph". 99 | 8. Select Application permissions. 100 | 9. Add the following permissions: Calendars.ReadWrite, Calls.InitiateGroupCall.All, Calls.JoinGroupCall.All, Group.ReadWrite.All, OnlineMeetings.ReadWrite.All 101 | 10. Click "Grant admin consent for …" 102 | 11. From the left menu, click "Certificates & secrets". 103 | 12. Under "Client secrets", click "+ New client secret". 104 | 13. Provide a description and select an expiry time for the secret and click "Add". 105 | 14. Note down the secret Value. 106 | 107 | ## Patient Room Account Setup 108 | 109 | In this repository is a PowerShell script that: 110 | 111 | 1. Creates the accounts 112 | 2. Adds the account to a group (for tracking and group based licensing) 113 | 3. Applies Custom Teams Policies 114 | 115 | Before running this script, you will need the following: 116 | 117 | - An Azure AD Security Group 118 | - This group should be empty, and will only be used for the patient room accounts. If provisioning manually, ensure all users are added to this group. It will be used later for licensing and in the flows. 119 | - You will also need to setup Group Based Licensing for the Azure AD Security Group. Please assign an Office 365 license to the group and disable all assignment options except for Microsoft Teams, Skype for Business and Exchange Online. Detailed instructions for Group Based Licensing can be found here: [https://docs.microsoft.com/en-us/azure/active-directory/users-groups-roles/licensing-groups-assign](https://docs.microsoft.com/en-us/azure/active-directory/users-groups-roles/licensing-groups-assign). 120 | - A CSV file with the desired Patient Room account information 121 | - Columns: 122 | - AccountName 123 | - Desired Room Name. Ensure this is easily identifiable to your clinical staff. 124 | - AccountUPN 125 | - Desired UserPrincipalName (email address) of the room. 126 | - This will only be used for login to the Teams application on the device. 127 | - Do not use a domain that is federated (ADFS, Ping, etc), as this will be a cloud only account. 128 | - Must not conflict with any other accounts in your directory. 129 | - AccountPassword 130 | - Must comply with your Azure AD Password Policies 131 | - AccountLocation 132 | - Building or Location of the room. This will be used to categorize the rooms later on and will be tied to a Team name (a suffix can be added) (see _Team/List/Tab_ sections below). 133 | - This will be filled into the "Department" field of the user. 134 | - AccountSubLocation 135 | - Sub Location of the room (example: Floor 1). This will be used to categorize the rooms later on and must be tied to either a Channel name (see _Team/List/Tab_ sections below). 136 | - This will be filled into the "Job Title" field of the user. 137 | - Sample file available (RoomAccounts.csv) 138 | - Azure AD PowerShell Module v2: [https://docs.microsoft.com/en-us/powershell/azure/active-directory/install-adv2?view=azureadps-2.0](https://docs.microsoft.com/en-us/powershell/azure/active-directory/install-adv2?view=azureadps-2.0) 139 | - Skype for Business Online PowerShell: [https://docs.microsoft.com/en-us/office365/enterprise/powershell/manage-skype-for-business-online-with-office-365-powershell](https://docs.microsoft.com/en-us/office365/enterprise/powershell/manage-skype-for-business-online-with-office-365-powershell) 140 | 141 | ### Script 142 | 143 | All variables and supporting files will need to be specified in the RunningConfig.json file you can find in this repository. The only varialbe the script needs configured manually is the location of that JSON configuration file. 144 | 145 | Once the above is ready, you can run CreateRooms.ps1. As with all open source scripts, please test and review before running in your production environment. 146 | 147 | When prompted, sign in with administrator credentials that are able to create Azure AD accounts and assign Teams policies. 148 | 149 | ## Team/List/Tab Creation 150 | 151 | Depending on your setup, you may want one Team or multiple Teams for Doctors to use to navigate and join the Patient Room meetings. 152 | 153 | We recommend a single Team with Channels for each location involved so that Doctors are able to join any room at any location during a Time of crisis. This is the method that will be covered and supported in this guide. 154 | 155 | If doctor's should only be able to join rooms at specific locations (hospitals/clinics), we recommend separate teams per location, or a single Team with _Private_ Channels for each location involved. This guide does not cover that method at this time however, and you will need to adapt to your needs. This will be added to the guide at a different time. 156 | 157 | In SharePoint, we will be leveraging SharePoint lists to store and surface the meeting join links. This guide will cover creating multiple SharePoint lists, one for each location, and having them added as Tabs to the associated channel. Each list will also get a custom view applied. 158 | 159 | ### Script 160 | 161 | In this repository is a PowerShell Script (CreateTeamsAndSPO.ps1) that: 162 | 163 | 1. Creates the Team 164 | 2. Sets Team Settings: 165 | i. Visibility: Private 166 | ii. Disables member capabilities: Add/Remove Apps, Create/Update/Remove Channels, Create/Update/Remove Connectors, Create/Update/Remove Tabs 167 | 3. Adds members to Team 168 | 4. Creates SharePoint Lists in the associated SharePoint site 169 | 5. Adds columns and custom view to lists 170 | i. Columns: Title (exists by default), RoomLocation, RoomSubLocation, MeetingLink, EventID(skip if provisioning manually). 171 | ii. View: (Create a new view)[https://support.office.com/en-us/article/Create-change-or-delete-a-view-of-a-list-or-library-27AE65B8-BC5B-4949-B29B-4EE87144A9C9] and then (add in the JSON)[https://support.microsoft.com/en-us/office/formatting-list-views-f737fb8b-afb7-45b9-b9b5-4505d4427dd1?ui=en-us&rs=en-us&ad=us] from SharePointViewFormatting.json 172 | 6. Creates Channels and pins the SharePoint list as a Tab 173 | 7. Removes Wiki Tabs 174 | 175 | Before running this script, you will need the following: 176 | 177 | - Azure AD Security Groups 178 | - You will specify security groups to copy membership from to the individual Teams in the below CSV (_MembersGroupName_). 179 | - These groups should contain the provider's accounts that you want to be added to the Teams as members. Do not include any of the room accounts you created earlier. They should _not_ be members of the Team. 180 | - The App ID and Client Secret from the Azure AD App Registration (earlier step in this guide). 181 | - A CSV file with the desired Team(s) information 182 | - Columns: 183 | - LocationName 184 | - Location Name. This must match the location names used for AccountLocation in _Patient Room Account Setup_. Ensure all Location Names from that earlier script are represented. 185 | - You will be able to add a suffix to this to make a more readable Team name by using a variable in the script 186 | - MembersGroupName 187 | - Name of an Azure AD Group (or synced AD Group) containing the members to be added to the Team. 188 | - These groups should contain the provider's accounts that you want to be added to the Teams as members. Do not include any of the room accounts you created earlier. They should _not_ be members of the Team. 189 | - Sample file available (LocationList.csv) 190 | - A second CSV file with the desired Channel(s)/List(s) 191 | - Columns: 192 | - LocationSubName 193 | - Sub Location Name. This must match the location names used for AccountSubLocation in _Patient Room Account Setup_. Ensure all Sub Location Names from that earlier script are represented. 194 | - LocationName 195 | - Location Name. This must match the location names used for AccountLocation in _Patient Room Account Setup_. Ensure all Location Names from that earlier script are represented. 196 | - Sample file available (SubLocationList.csv) 197 | - Azure AD PowerShell Module v2: [https://docs.microsoft.com/en-us/powershell/azure/active-directory/install-adv2?view=azureadps-2.0](https://docs.microsoft.com/en-us/powershell/azure/active-directory/install-adv2?view=azureadps-2.0) 198 | - Microsoft Teams PowerShell: [https://www.powershellgallery.com/packages/MicrosoftTeams/](https://www.powershellgallery.com/packages/MicrosoftTeams/) 199 | - SharePoint Online PnP PowerShell: [https://docs.microsoft.com/en-us/powershell/sharepoint/sharepoint-pnp/sharepoint-pnp-cmdlets?view=sharepoint-ps](https://docs.microsoft.com/en-us/powershell/sharepoint/sharepoint-pnp/sharepoint-pnp-cmdlets?view=sharepoint-ps) 200 | 201 | All variables and supporting files will need to be specified in the RunningConfig.json file you can find in this repository. The only varialbe the script needs configured manually is the location of that JSON configuration file. 202 | 203 | Once the above is ready, you can run CreateTeamsAndSPO.ps1. As with all open source scripts, please test and review before running in your production environment. 204 | 205 | ## Patient Room Meeting Setup 206 | 207 | ## Meeting Creation 208 | 209 | To create the meetings, we will use Power Automate. Power Automate offers a simple way to call the Microsoft Graph API, and the ability to run on a regular basis if we need in the future. 210 | 211 | Prerequisites: 212 | 213 | - A Power Automate Premium license will be required for this piece (P1, P2, Per User or Per App all work). 214 | - An account with the Power Automate license applied to it, used for creating the Flows (ideally a service account). 215 | - SetupMeetingsFlow.zip from this repository 216 | - Get the Group GUID/ObjectID for your Azure AD Group used in _Patient Room Account Setup_ (find in the group properties in the Azure AD Portal) 217 | 218 | Instructions: 219 | 220 | 1. Login to flow.microsoft.com 221 | 2. Click on "My flows". 222 | 3. Click "Import". 223 | 4. Upload SetupMeetingsFlow.zip 224 | 5. Update all variables, the SharePoint Site base URL in the final step of the flow, and the Group ID. 225 | 226 | Once it's been at least 3 hours since you've created the room accounts, you can run the Flow to create all the meeting links. Ideally, wait at least 24 hours. This is to ensure the Teams Policies properly apply to the room accounts before a meeting is created. 227 | 228 | ## Meeting Bot 229 | This section covering the meeting bot is _draft_, and we recommend reaching out to your Microsoft Partner or account team for assistance with this. We will finalize this section over the next 48 hours as we continue to build. 230 | 231 | A meeting bot can be used to get around the 30 minute timeout issue mentioned in the changelog at the top of this page. The meeting bot will sit in each meeting and serve as a second meeting participant to avoid the 30 minute timeout (which starts as soon as a meeting is down to one participant). The bot is subject to the same 30 minute and 24 hour timeouts that standard accounts have. Therefore, it is crucial that patient device not hang up the meeting, as that would leave the bot as the lone participant in the meeting, starting the 30 minute timer. 232 | The meeting bot is joined into a meeting using a Graph API call, which can be automated using Power Automate or PowerShell to ensure it rejoins every 24 hours, and potentially sooner depending on your needs. The below will outline the basics of the bot setup process. Ensure you have updated your Azure AD App Registration with the newly added API permissions before starting. 233 | 234 | ### Bot Configuration 235 | 1. Go to https://dev.botframework.com/bots/new 236 | 2. Fill out all the pertinent information, ensuring to use the app ID from your Azure AD App registration. 237 | 3. Add Microsoft Teams as a channel 238 | 4. Select the calling tab, and select the checkbox to _Enable calling_. For your webhook, enter any _valid_ https URL. We will never be calling this bot, so this field won't be relevant, but it is required to enter something. 239 | 5. In Microsoft Teams, select Apps from the left pane and then select App Studio. 240 | 6. From the top pane, click Manifest editor and then Create a new app from the left pane. 241 | 7. In the App details tab, provide the basic information. 242 | 8. Navigate to the Capabilities section, and select the Bots tab. Then select Set Up in the right pane. 243 | 9. Fill in the desired bot name 244 | 10. Select the Select from one of my existing bots option, and find your bot from above in the dropdown. 245 | 11. Check all options under Calling Bot and Scope and press Save 246 | 12. Use app studio to deploy the bot to your tenant. 247 | 248 | ### Adding the bot to a Teams meeting 249 | A Graph API call using your Azure AD App Registration (Client ID, Client Secret, Tenant ID) will allow us to add the bot to an existing scheduled meeting. 250 | To get the items that the API call will need, get your meeting join link, which should look like this: 251 | 252 | `https://teams.microsoft.com/l/meetup-join/19%3ameeting_YWNiYzA2NTctOGIzMy00MzRhLTkyNmUtZGY4NzM2YTFhNmEz%40thread.v2/0?context=%7b%22Tid%22%3a%226be58f7f-c45d-43f9-89e4-b97ec2a06d8e%22%2c%22Oid%22%3a%22ac2ea2ab-9845-4308-a99c-8fdc6548ceac%22%7d` 253 | 254 | Decoding that URI, we get this: 255 | 256 | `https://teams.microsoft.com/l/meetup-join/19:meeting_YWNiYzA2NTctOGIzMy00MzRhLTkyNmUtZGY4NzM2YTFhNmEz@thread.v2/0?context={"Tid":"6be58f7f-c45d-43f9-89e4-b97ec2a06d8e","Oid":"ac2ea2ab-9845-4308-a99c-8fdc6548ceac"}` 257 | 258 | The two items we need from the decoded uri are: 259 | 260 | - threadId: `19:meeting_YWNiYzA2NTctOGIzMy00MzRhLTkyNmUtZGY4NzM2YTFhNmEz@thread.v2` 261 | - organizerId `ac2ea2ab-9845-4308-a99c-8fdc6548ceac` 262 | 263 | Using that information, call the graph API using the below to add the bot to the meeting: 264 | 265 | Call: `POST https://graph.microsoft.com/beta/communications/calls` 266 | 267 | Body: 268 | ```json 269 | { 270 | "@odata.type": "#microsoft.graph.call", 271 | "callbackUri": "INSERT URI FROM STEP 4 ABOVE", 272 | "tenantId": "INSERT TENANTID HERE", 273 | "meetingInfo": { 274 | "@odata.type": "#microsoft.graph.organizerMeetingInfo", 275 | "organizer": { 276 | "@odata.type": "#microsoft.graph.identitySet", 277 | "user": { 278 | "@odata.type": "#microsoft.graph.identity", 279 | "id": "INSERT ORGANIZERID HERE", 280 | "tenantId": "INSERT TENANTID HERE" 281 | } 282 | }, 283 | "allowConversationWithoutHost": true 284 | }, 285 | "mediaConfig": { 286 | "@odata.type": "#microsoft.graph.serviceHostedMediaConfig" 287 | }, 288 | "chatInfo": { 289 | "@odata.type": "#microsoft.graph.chatInfo", 290 | "threadId": "INSERT THREADID HERE", 291 | "messageId": "0" 292 | } 293 | } 294 | ``` 295 | ### Flow for automating adding bot to meetings 296 | We have built a flow for you to use to continuously add the bot to all meetings. This will ensure the bot is not outside of the meeting for more than 25 minutes, ensuring the patient room will not be kicked out either. Again, the only reason the bot will be kicked out is after 24 hours, or if the patient room is not joined 30 minutes. The flow will solve both of those. 297 | 298 | Prerequisites: 299 | 300 | - A Power Automate Premium license will be required for this piece (P1, P2, Per User or Per App all work). 301 | - An account with the Power Automate license applied to it, used for creating the Flows (ideally a service account). 302 | - SetupMeetingsFlow.zip from this repository 303 | - Get the Group GUID/ObjectID for your Azure AD Group used in _Patient Room Account Setup_ (find in the group properties in the Azure AD Portal) 304 | 305 | Instructions: 306 | 307 | 1. Login to flow.microsoft.com 308 | 2. Click on "My flows". 309 | 3. Click "Import". 310 | 4. Upload AddBotToMeetingsFlow.zip 311 | 5. Update all variables. 312 | 313 | ## Meeting Updating 314 | 315 | If there is an error with a meeting link, there is a flow that can be manually run to update the link. Please note when this happens, someone will need to end the meeting on the patient room device and join the new meeting. 316 | Please check back here soon for the details of the flow for this purpose. 317 | 318 | # Security Controls 319 | 320 | ## Mobile Device Management 321 | 322 | We strongly recommend managing the devices with Intune MDM and enabling kiosk mode. More detailed instructions will be added here. 323 | 324 | ## Conditional Access Policies 325 | 326 | We strongly recommend applying a conditional access policy to the Azure AD Group used in _Patient Room Account Setup_ (contains all Patient Room accounts). This policy should limit sign ins to either Intune Managed Devices or specific trusted IPs. This is to limit the risk of the account becoming compromised and a third party logging into an ongoing patient meeting. 327 | --------------------------------------------------------------------------------