Recipe - Streamline Event Check-in with QR Codes and Rock Website or App
Skill level: Beginner
Organization: Community Bible Church-San Antonio
Requires Rock: 1.13.0
{# strip images & classes from the HTML but otherwise leave structure #}
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 related file (Registration_Checkin_202306081605.zip)
Screenshots
- /GetImage.ashx?guid=947f746b-0223-4040-895c-beef317f0ce1