2 Streamline Event Check-in with QR Codes and Rock Website or App Shared by Randy Aufrecht, Community Bible Church-San Antonio one year ago 13.0 General, Operations Beginner Overview Problem Trying to check-in 200+ people into an event in a timely manner can be difficult if you want to verify who has checked-in and who hasn’t. Solution Using QR codes sent to the participant and workflows that check the registration lets you control who has come and who has yet to come. The system sends a single QR code to the Registrar, which can be shared amongst the registrants. There is also a way to search for people who either didn’t get a qr code or lost it. Details There are 2 methods to verify Event Registrations using QR codes, via web or via app. Both work well, but the app offers an easier interface for the person doing the check-in. The Backend Registration When Creating your Registration Form, you need to add a field with the Key "CheckinDateTime" that is of type "Date Time". This is the attribute that will be used to store the check-in date and time. Make it an "Internal" field. Workflow The workflow handles everything for the web version of check-in and the final steps for the app version. It verifies which registrants are being checked-in and sets the check-in Date attribute required on the Registration Instance. Workflows do not allow you to populate single-selects or multi-select fields on the fly after the workflow has been loaded. You also are not able to do it from Attributes that may already be loaded into the workflow. Our workaround is to reiterate the workflow with new page parameters and have the workflow move through different activities based on those parameters. This workflow uses an Action "For Each" which can be found in Blue Box Moon's Workflow Stimpack. You can download it from the Rock Shop at PackageId=68. Things to modify within the workflow: "Registrar or Registrant" Attribute Inside the Values field is a "LEFT JOIN PhoneNumber pn on pn.PersonId = p.Id and pn.NumberTypeValueId = 12" (replace 12 with your mobile phone NumberTypeValueId) Redirect Action (x2): This is the page that you use for WorkflowEntry. Substitute "form/371/" with your own workflow entry page and this workflowtypeid. QR Code The QR Code presents a URL pointing to the workflow for the web version and a deep link to an app page for the app version. It contains 2 parameters, the Registration Instance Id and the Person Alias Guid Lava Shortcodes Registration Check-in Chart This shortcode is used to display a chart of the number of people checked-in vs the number of people who have not checked-in. It is used on the Registration Instance Detail page. To make this work accross multiple Registration Templates, we use the Attribute Name for CheckinDateTime, we also rely on the EntityType of "Rock.Model.RegistrationRegistrant". In our instance it was 305 in another it was 313. Adjust the SQL to match your EntityTypeId of your Registration Registrant Entity Tag Name: registrationcheckinchart Tag Type: Inline Documentation: instanceid = Registration Instance Id mobie = true for mobile app code Parameters: instanceid, mobile Enabled Lava Commands: SQL Shortcode Markup:{% sql RegistrationInstanceId:'{{ instanceid }}' return:'checkinProgress' %} SELECT COUNT(p.Id) AS TotalPeople, COUNT(CASE WHEN CheckinDateTime.[Value] IS NOT NULL AND CheckinDateTime.[Value] <> '' THEN p.Id END) AS CheckedIn FROM Registration r INNER JOIN RegistrationRegistrant rr ON r.Id = rr.RegistrationId OUTER APPLY ( SELECT av.Value, a.EntityTypeId FROM AttributeValue av INNER JOIN Attribute a ON av.AttributeId = a.Id AND a.[Key] = 'CheckinDateTime' AND a.EntityTypeId = 305 WHERE rr.Id = av.EntityId ) CheckinDateTime INNER JOIN PersonAlias rrpa ON rr.PersonAliasId = rrpa.Id INNER JOIN Person p ON rrpa.PersonId = p.[Id] WHERE r.RegistrationInstanceId = @RegistrationInstanceId AND rr.OnWaitList = 0; {% endsql %} {% for progress in checkinProgress %} {% assign pct = progress.CheckedIn | DividedBy:progress.TotalPeople,2 %} {% if mobile == 'true' %} <Label StyleClass="font-weight-bold" Text="{{ progress.CheckedIn }} of {{ progress.TotalPeople }} Checked In" /> <ProgressBar x:Name="progress-bar" WidthRequest="100" HeightRequest="10" MinimumHeightRequest="10" Progress="{{ pct }}" ProgressColor="{% if pct < 0.33 %}Red{% elseif pct > 0.66 %}Green{% else %}Yellow{% endif %}" VerticalOptions="CenterAndExpand" HorizontalOptions="Fill"> </ProgressBar> {% else %} <h5># Checked In</h5> <div class="progress w-100"> {% assign pct = pct | Times:100 %} <div class="progress-bar " role="progressbar" aria-valuenow="{{ pct }}" aria-valuemin="0" aria-valuemax="100" style="width: {{ pct }}%"> {{ progress.CheckedIn }} of {{ progress.TotalPeople }} </div> </div> {% endif %} Show Currently Checked into Instance - Website This shortcode is used to display a list of the people who have checked-in using the workflow. Tag Name: showcurrentlycheckedintoinstance Tag Type: Inline Shortcode Markup:{%- sql -%} SELECT p.NickName, p.LastName, av.Value FROM ( SELECT pa.PersonId, r.Id FROM Registration r INNER JOIN PersonAlias pa2 ON pa2.Id = r.PersonAliasId INNER JOIN Person p ON pa2.PersonId = p.Id INNER JOIN PersonAlias pa ON pa.PersonId = p.Id AND pa.Guid = TRY_CAST('{{ "Global" | PageParameter:"Person" | AsString | SanitizeSql | WithFallback:"", "0" }}' AS uniqueidentifier) WHERE r.RegistrationInstanceId = {{ 'Global' | PageParameter:"RegistrationInstanceId" | AsString | SanitizeSql | WithFallback:'', '0' }} UNION SELECT pa.PersonId, r.Id FROM Registration r INNER JOIN RegistrationRegistrant rr ON r.Id = rr.RegistrationId INNER JOIN PersonAlias pa2 ON pa2.Id = rr.PersonAliasId INNER JOIN Person p ON pa2.PersonId = p.Id INNER JOIN PersonAlias pa ON pa.PersonId = p.Id AND pa.Guid = TRY_CAST('{{ "Global" | PageParameter:"Person" | AsString | SanitizeSql | WithFallback:"", "0" }}' AS uniqueidentifier) WHERE r.RegistrationInstanceId = {{ 'Global' | PageParameter:'RegistrationInstanceId' | AsString | SanitizeSql | WithFallback:'', '0' }} ) PersonReg INNER JOIN RegistrationRegistrant rr ON PersonReg.Id = rr.RegistrationId INNER JOIN AttributeValue av ON rr.Id = av.EntityId AND av.Value IS NOT NULL AND av.Value !='' INNER JOIN Attribute a ON av.AttributeId = a.Id AND a.[Key] = 'CheckinDateTime' INNER JOIN PersonAlias rrpa ON rr.PersonAliasId = rrpa.Id INNER JOIN Person p ON rrpa.PersonId = p.Id {% endsql -%} {%- for item in results -%} {%- if forloop.first == true -%}<h3>Already Checked In:</h3><ul>{%- endif -%} <li>{{ item.NickName }} {{ item.LastName }} at {{ item.Value }}</li> {%- if forloop.last == true -%}</ul>{%- endif -%} {%- endfor -%} Show Currently Checked into Instance - Mobile App This shortcode is used to display a list of the people who have checked-in using the app. Tag Name: showcurrentlycheckedintoinstanceapp Tag Type: Inline Parameters: person, registrationinstanceid Shortcode Markup:{%- sql -%} SELECT p.NickName, p.LastName, av.Value FROM ( SELECT pa.PersonId, r.Id FROM Registration r INNER JOIN PersonAlias pa2 ON pa2.Id = r.PersonAliasId INNER JOIN Person p ON pa2.PersonId = p.Id INNER JOIN PersonAlias pa ON pa.PersonId = p.Id AND pa.Guid = TRY_CAST('{{ person | AsString | SanitizeSql | WithFallback:"", "0" }}' AS uniqueidentifier) WHERE r.RegistrationInstanceId = {{ registrationinstanceid | AsString | SanitizeSql | WithFallback:'', '0' }} UNION SELECT pa.PersonId, r.Id FROM Registration r INNER JOIN RegistrationRegistrant rr ON r.Id = rr.RegistrationId INNER JOIN PersonAlias pa2 ON pa2.Id = rr.PersonAliasId INNER JOIN Person p ON pa2.PersonId = p.Id INNER JOIN PersonAlias pa ON pa.PersonId = p.Id AND pa.Guid = TRY_CAST('{{ person | AsString | SanitizeSql | WithFallback:"", "0" }}' AS uniqueidentifier) WHERE r.RegistrationInstanceId = {{ registrationinstanceid | AsString | SanitizeSql | WithFallback:'', '0' }} ) PersonReg INNER JOIN RegistrationRegistrant rr ON PersonReg.Id = rr.RegistrationId INNER JOIN AttributeValue av ON rr.Id = av.EntityId AND av.Value IS NOT NULL AND av.Value !='' INNER JOIN Attribute a ON av.AttributeId = a.Id AND a.[Key] = 'CheckinDateTime' INNER JOIN PersonAlias rrpa ON rr.PersonAliasId = rrpa.Id INNER JOIN Person p ON rrpa.PersonId = p.Id {% endsql -%} {%- for item in results -%} {%- if forloop.first == true -%}<Label Text="Already Checked In:" StyleClass="h3, mb-12" />{%- endif -%} <Label Text="{{ item.NickName }} {{ item.LastName }} at {{ item.Value }}" StyleClass="ml-12" /> {%- endfor -%}item. Nickname Show Registrant This shortcode is used to display the number of registrants the person is connected to with the Registration Instance. Tag Name: showregistrant Tag Type: Inline Parameters: person, registrationinstanceid Shortcode Markup:{%- sql -%} SELECT DISTINCT rr.Id AS Value , p.NickName + ' ' + p.LastName AS TEXT FROM ( SELECT pa.PersonId , r.Id FROM Registration r INNER JOIN PersonAlias pa2 ON pa2.Id = r.PersonAliasId INNER JOIN Person p ON pa2.PersonId = p.Id INNER JOIN PersonAlias pa ON pa.PersonId = p.Id AND pa.Guid = TRY_CAST('{{ person | AsString | SanitizeSql | WithFallback:"", "0" }}' AS UNIQUEIDENTIFIER) WHERE r.RegistrationInstanceId = {{ regInstId | AsString | SanitizeSql | WithFallback: '', '0' }} UNION SELECT pa.PersonId , r.Id FROM Registration r INNER JOIN RegistrationRegistrant rr ON r.Id = rr.RegistrationId INNER JOIN PersonAlias pa2 ON pa2.Id = rr.PersonAliasId INNER JOIN Person p ON pa2.PersonId = p.Id INNER JOIN PersonAlias pa ON pa.PersonId = p.Id AND pa.Guid = TRY_CAST('{{ person | AsString | SanitizeSql | WithFallback:"", "0" }}' AS UNIQUEIDENTIFIER) WHERE r.RegistrationInstanceId = {{ reginstid | AsString | SanitizeSql | WithFallback: '', '0' }} ) PersonReg INNER JOIN RegistrationRegistrant rr ON PersonReg.Id = rr.RegistrationId INNER JOIN PersonAlias rrpa ON rr.PersonAliasId = rrpa.Id INNER JOIN Person p ON rrpa.PersonId = p.[Id] {%- endsql -%} {{ results | Size }} The Frontend Website Pages The url from the QR code should point to a Workflow Entry page, or a deep link with a fallback to your Workflow Entry page you use for your public forms for example we use /form/371 to point to the correct workflow, then the url has the appropriate parameters ?RegistrationInstanceId= and &Person= Mobie App Pages Because the mobile app doesn’t allow for redirecting back to your workflow Entry Page like we can on the website, we need to create more pages to handle the check-in Scan Registration Checkin This page displays buttons to scan the qr code through the app, or search for a registrar through the search page Block: Content PageParameter Received: RegistrationInstanceId Enabled Lava Commands: SQL, Rock Entity Dynamic Content: Yes Content:{%- assign regInstId = PageParameter.RegistrationInstanceId -%} <StackLayout Padding="0" Spacing="30"> {% if regInstId != null and regInstId != empty %} {%- registrationinstance id:'{{ regInstId }}' securityenabled:'false'-%} {%- assign regName = registrationinstance.Name -%} {%- endregistrationinstance -%} <Label StyleClass= "h3" Text="{{ regName | Escape }} Registration Check-in" /> {[ registrationcheckinchart instanceid:'{{ regInstId }}' mobile:'true' ]} {% endif %} <Rock:ScanCode x:Name="scanner" Command="{Binding PushPage}" Mode="Manual"> <Rock:ScanCode.CommandParameter> <Rock:PushPageParameters PageGuid="a0084277-d6e6-46d0-ae02-b3cd1e125a7f"> //- PageGuid from Mobile App Page 3 Registration Check-in <Rock:Parameter Name="Url" Value="{Binding Source={x:Reference scanner}, Path=Value}" /> </Rock:PushPageParameters> </Rock:ScanCode.CommandParameter> </Rock:ScanCode> <Button Text="Enter Phone Number" StyleClass="btn, btn-primary, mb-12" Command="{Binding PushPage}"> <Button.CommandParameter> <Rock:PushPageParameters PageGuid="fb73faa0-945e-4d32-8363-a460aaf2f81f"> //- PageGuid from Mobile App Page 2 People Search for Registration Check-in <Rock:Parameter Name="RegistrationInstanceId" Value="{{ regInstId | Escape }}" /> </Rock:PushPageParameters> </Button.CommandParameter> </Button> </StackLayout> People Search for Registration Check-in Allows you to search for registrants/registrars tied to the registration instance. This block does not allow for PageParameters, so you must hardcode the Registration Instance Id for every Event. NOTE: You must hardcode the Registration Instance Id of the event into this block. At this time the Search Block does not read the PageParameter. Block: Search Search Component: Person Phone Show Search Label: No Search Placeholder Text: 4 Digits of Phone Number Result Item Template: Custom {%- assign regInstId = 1228 -%} {% assign itemPhone = Item | PhoneNumber:'Mobile' %} {% assign itemName = Item.FullName %} {% assign itemText = Item.Email %} {% capture reg %}{[ showregistrant person:'{{ Item.PrimaryAlias.Guid }}' reginstid:'{{regInstId}}']}{% endcapture %} {%- assign reg = reg | AsInteger -%} <StackLayout Spacing="0"> {% if reg > 0 %} <StackLayout Orientation="Horizontal" StyleClass="search-result-content"> <StackLayout.GestureRecognizers> <TapGestureRecognizer Command="{Binding PushPage}"> <TapGestureRecognizer.CommandParameter> <Rock:PushPageParameters PageGuid="a0084277-d6e6-46d0-ae02-b3cd1e125a7f"> //-PageGuid from Mobile App Page 3 Registration Check-in <Rock:Parameter Name="Person" Value="{{ Item.PrimaryAlias.Guid }}" /> <Rock:Parameter Name="RegistrationInstanceId" Value="{{regInstId}}" /> </Rock:PushPageParameters> </TapGestureRecognizer.CommandParameter> </TapGestureRecognizer> </StackLayout.GestureRecognizers> <StackLayout Spacing="0" HorizontalOptions="FillAndExpand" VerticalOptions="Center"> <Label StyleClass="search-result-name" Text="{{ itemName | Escape }}" HorizontalOptions="FillAndExpand" /> {% if itemText != null and itemText != '' %} <Label StyleClass="search-result-text">{{ itemText | XamlWrap }}</Label> {% endif %} {% if itemPhone != null and itemPhone != '' %} <Label StyleClass="search-result-text">{{ itemPhone | XamlWrap }}</Label> {% endif %} </StackLayout> <Rock:Icon IconClass="chevron-right" VerticalOptions="Center" StyleClass="search-result-detail-arrow" /> </StackLayout> <Rock:Divider /> {%- endif -%} </StackLayout> Max Results: 300 Registration Checkin From the QR Code, this page displays the Registrant and the Registrars tied to the Person and Registration Instance. From here the user selects who is checking in and it goes to the next page. Block: Content PageParameter Received: Url (from scan code) or RegistrationInstanceId, Person (from search) {%- assign url = PageParameter.Url -%} {%- if url != null and url != empty -%} {%- assign urlsegments = url | Url:'segments'%} {%- assign regInstId = urlsegments[3] | Remove:'/' -%} {%- assign Person = urlsegments[4] | Remove:'/' | PersonByAliasGuid -%} {%- else -%} {%- assign regInstId = PageParameter.RegistrationInstanceId -%} {%- if PageParameter.Person != null and PageParameter.Person != empty -%} {%- assign Person = PageParameter.Person | PersonByAliasGuid -%} {%- endif -%} {%- endif -%} {%- registrationinstance id:'{{ regInstId }}' -%} {%- assign regName = registrationinstance.Name -%} {%- endregistrationinstance -%} {%- sql -%} SELECT DISTINCT rr.Id AS Value , p.NickName + ' ' + p.LastName AS TEXT FROM ( SELECT pa.PersonId , r.Id FROM Registration r INNER JOIN PersonAlias pa2 ON pa2.Id = r.PersonAliasId INNER JOIN Person p ON pa2.PersonId = p.Id INNER JOIN PersonAlias pa ON pa.PersonId = p.Id AND pa.Guid = TRY_CAST('{{ Person.PrimaryAlias.Guid | AsString | SanitizeSql | WithFallback:"", "0" }}' AS UNIQUEIDENTIFIER) WHERE r.RegistrationInstanceId = {{ regInstId | AsString | SanitizeSql | WithFallback: '' , '0' }} UNION SELECT pa.PersonId , r.Id FROM Registration r INNER JOIN RegistrationRegistrant rr ON r.Id = rr.RegistrationId INNER JOIN PersonAlias pa2 ON pa2.Id = rr.PersonAliasId INNER JOIN Person p ON pa2.PersonId = p.Id INNER JOIN PersonAlias pa ON pa.PersonId = p.Id AND pa.Guid = TRY_CAST('{{ Person.PrimaryAlias.Guid | AsString | SanitizeSql | WithFallback:"", "0" }}' AS UNIQUEIDENTIFIER) WHERE r.RegistrationInstanceId = {{ regInstId | AsString | SanitizeSql | WithFallback: '', '0' }} ) PersonReg INNER JOIN RegistrationRegistrant rr ON PersonReg.Id = rr.RegistrationId OUTER APPLY ( SELECT av.Value FROM AttributeValue av INNER JOIN Attribute a ON av.AttributeId = a.Id AND a.[Key] = 'CheckinDateTime' WHERE rr.Id = av.EntityId ) CheckinDateTime INNER JOIN PersonAlias rrpa ON rr.PersonAliasId = rrpa.Id INNER JOIN Person p ON rrpa.PersonId = p.[Id] WHERE CheckinDateTime.[Value] IS NULL OR CheckinDateTime.[Value] = '' {%- endsql -%} <StackLayout StyleClass="section" Spacing="24" xmlns:clr="clr-namespace:System;assembly=mscorlib"> <Label StyleClass= "h3" Text="{{ regName }} Registration Check-in" /> {[ showcurrentlycheckedintoinstanceapp person:'{{ Person.PrimaryAlias.Guid }}' registrationinstanceid:'{{ regInstId }}']} {%- assign size = results | Size -%} {%- if size > 0 -%} <Rock:FieldContainer> <Rock:CheckBoxList x:Name="cbGroupMembers" StyleClass="neue-bold, mb-12" Label="Who is Checking in?"> {%- if size == 1 -%} <Rock:CheckBoxList.SelectedValues> <clr:String>{{ results[0].Value }}</clr:String> </Rock:CheckBoxList.SelectedValues> {%- endif -%} {%- for Member in results -%} <Rock:Parameter Name="{{ Member.TEXT | Escape }}" Value="{{ Member.Value }}" /> {%- endfor -%} </Rock:CheckBoxList> </Rock:FieldContainer> <Button Text="Submit" StyleClass="btn, btn-primary" Command="{Binding PushPage}"> <Button.CommandParameter> <Rock:PushPageParameters PageGuid="216e2091-ed1a-4394-bdca-30b785412289"> //- PageGuid for Workflow Entry Page <Rock:Parameter Name="RegistrationInstanceId" Value="{{ regInstId }}" /> <Rock:Parameter Name="Person" Value="{{Person.PrimaryAlias.Guid}}" /> <Rock:Parameter Name="Registrants" Value="{Binding Source={x:Reference cbGroupMembers}, Path=SelectedValueText}" /> <Rock:Parameter Name="Mobile" Value="true" /> <Rock:Parameter Name="WorkflowTypeGuid" Value="8deec6e6-cc9e-426d-b850-3c5b7e57bea3" /> </Rock:PushPageParameters> </Button.CommandParameter> </Button> {%- else -%} <Label Text="No one is available to be checked in." /> <Button Text="Cancel" StyleClass="btn, btn-default" Command="{Binding ShowPage}" CommandParameter="c308ba5d-148f-4614-9893-5b6078a6f3dc" > <Button.CommandParameter> <Rock:ReplacePageParameters PageGuid="c308ba5d-148f-4614-9893-5b6078a6f3dc"> //- PageGuid from Page 1 Scan Registration Checkin <Rock:Parameter Name="RegistrationInstanceId" Value="{{ regInstId }}" /> </Rock:ReplacePageParameters> </Button.CommandParameter> </Button> {%- endif -%} </StackLayout> Workflow Entry Page You can use the basic Workflow Entry Page you use for all workflows here. We use the same Page for all our workflows and have lava in the Completion Xaml field that determines what confirmation messages display. Completion Xaml: {%- assign regInstId = Workflow | Attribute:'RegistrationInstanceId' -%} {%- assign registrants = Workflow | Attribute:'Registrants','RawValue' -%} {%- assign size = registrants | Split:',' | Size -%} <StackLayout Padding="0" Spacing="10"> {% if regInstId != null and regInstId != empty %} {%- registrationinstance id:'{{ regInstId }}' securityenabled:'false'-%} {%- assign regName = registrationinstance.Name -%} {%- endregistrationinstance -%} <Label StyleClass= "h3" Text="{{ regName }} Registration Check-in" /> {[ registrationcheckinchart instanceid:'{{ regInstId }}' mobile:'true' ]} {% endif %} {%- if size > 0 -%} <StackLayout Spacing="0" StyleClass="mb-24"> <Label StyleClass="h3" Text="Checked In" /> {%- registrationregistrant ids:'{{ registrants }}' securityenabled:'false' -%} {%- for registrant in registrationregistrantItems -%} <Label StyleClass="ml-12" Text="{{ registrant.Person.FullName }}" /> {%- endfor -%} {%- endregistrationregistrant -%} </StackLayout> {%- endif -%} <Rock:ScanCode x:Name="scanner" Command="{Binding ReplacePage}" Mode="Manual"> <Rock:ScanCode.CommandParameter> <Rock:ReplacePageParameters PageGuid="a0084277-d6e6-46d0-ae02-b3cd1e125a7f"> //- PageGuid from Page 3 <Rock:Parameter Name="Url" Value="{Binding Source={x:Reference scanner}, Path=Value}" /> </Rock:ReplacePageParameters> </Rock:ScanCode.CommandParameter> </Rock:ScanCode> <Button Text="Enter Phone Number" StyleClass="btn, btn-primary, mb-12" Command="{Binding ReplacePage}"> <Button.CommandParameter> <Rock:ReplacePageParameters PageGuid="fb73faa0-945e-4d32-8363-a460aaf2f81f"> //- PageGuid from Page 2 <Rock:Parameter Name="RegistrationInstanceId" Value="{{ regInstId | Escape }}" /> </Rock:ReplacePageParameters> </Button.CommandParameter> </Button> </StackLayout> Download File