Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
31 changes: 31 additions & 0 deletions Widgets/VolunteerTracker/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Volunteer Tracker Widget

The Volunteer Tracker is a Domino’s Pizza Tracker–inspired widget that helps volunteers see exactly where they are in the application process. Just like watching your pizza go from “Order Placed” to “Out for Delivery,” this tracker moves you from “Application Received” to “Ready to Serve!”—minus the melted cheese (unless you’re volunteering in the kitchen 🍕).

## Features

- **Domino’s-Style Progress Bar**: Big, bold, and colorful stages that light up as you complete each requirement.
- **Stages Include**:
1. **Received** – Your application is in!
2. **Leader Review** – Waiting for approval from your leader.
3. **Background Check** – Show if a background check is required.
4. **Additional Requirements** – Tracks milestones, certifications, and forms.
5. **Ready to Serve!** – All steps complete and you’re good to go.
- **Helpful Links**: Direct links to complete background checks or forms if they’re still pending.
- **Contact Info**: Displays the opportunity leader’s name and email for quick help.
- **No Auth Required**: Just add ?response=[Response_ID]&cid=[Contact_GUID] to the URL this widget is on to load the tracker.

## Configuration

- Modify the stored procedure parameters to reflect your church's setup:
- **@BGCWebUrl** - Public web address for completing background checks. The Background_Check_GUID will be appended.
- **@FormWebUrl** - Public web location of a Custom Form widget. The Form_GUID will be appended.
- **@CheckTypeID** - The default/fallback Background_Check_Type_ID for your church.
- **@EnforceVolunteerGroup** - If set to 1, then the widget will _only_ work with Groups with a Type that is set as a Volunteer Group.
- Create any "Requirement" records for the Group Role associated with the Opportunity, if needed. This widget will show all distinct requirements associated with the Group Role.
- If no Background Check is set in a requirement record, but the Group Role requires a Background Check, then the default/fallback @CheckTypeID will be used.
- The "Leader Review" step utilizes the "Response Result" field on the Response record. Blank/Null will leave it in the "Pending" state, "Not Placed" will change the tracker to the "closed" state, and any other "Response Result" will advance the tracker to the next stage.

## Screenshots

<img src="./Assets/screenshot-volunteertracker.png" width="800" />
17 changes: 17 additions & 0 deletions Widgets/VolunteerTracker/StoredProc/StoredProc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Stored Procedure Definition

Use this folder to include any sql scripts designed to install Stored Procedures required for the Csutom Widget to work. Please follow the pattern of including a fully installable script including required MinistryPlatform metadata. Examplesa are included to aid in quickly developing installable Stored Procedure scripts

## Installation Notes

All examples in this repository will install the stored procedure, then check API_Procudures for the relevant metadata that defines the stored procedure. Additionally, install scripts will give the Administrators security role access to the script.

## Standard Parameters

The Custom Widget API will attempt to pass @UserName when the website user is logged into widgets. It is highly recommend that you include the @UserName parameter in all Custom Widget Stored Procedures. If @UserName is not required, you should default this parameter to null. When NOT NULL, the absense of this parameter will cause the stored procedure to fail.

Example:

```
@UserName nvarchar(75) = null
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
DROP PROCEDURE IF EXISTS [dbo].[api_custom_VolunteerTrackerWidget_JSON]
GO

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO

-- =============================================
-- api_custom_VolunteerTrackerWidget_JSON
-- =============================================
-- Description: Returns the status of requirements for a response to a volunteer opportunity.
-- Created at MP Dev Lab 2025 at Perimeter Church in Atlanta, Georgia.
-- Last Modified: 8/22/2025
-- Stephan Swinford
-- =============================================

CREATE PROCEDURE [dbo].[api_custom_VolunteerTrackerWidget_JSON]
@DomainID INT,
@Username NVARCHAR(75) = NULL,
@BGCWebUrl NVARCHAR(255) = 'https://yourchurch.ministryplatform.com/portal/backgroundcheck.aspx?background=',
@FormWebUrl NVARCHAR(255) = 'https://www.yourchurch.org/form?id=',
@CheckTypeID INT = 1, --Default Background Check Type ID
@EnforceVolunteerGroup INT = 0, --Enforce whether associated opportunity group must be a 'volunteer group' or not
@ContactGUID NVARCHAR(75),
@ResponseID INT

AS
BEGIN
SET NOCOUNT ON;

-- Verify that the Contact_GUID associated with the Reponse matches the provided Contact GUID
IF NOT EXISTS (
SELECT 1
FROM Contacts C
JOIN Participants P ON P.Contact_ID = C.Contact_ID
JOIN Responses R ON R.Participant_ID = P.Participant_ID
WHERE R.Response_ID = @ResponseID
AND C.Contact_GUID = @ContactGUID
)
BEGIN
SELECT [JsonResult] = (SELECT 'Invalid data in URL. Please check your URL and try again.' AS ErrorMessage FOR JSON PATH, WITHOUT_ARRAY_WRAPPER);
RETURN;
END

SELECT [JsonResult] =
(
SELECT

/* 1) Volunteer Information -> object (embedded without escaping) */
JSON_QUERY(
COALESCE(
(
SELECT TOP 1
C.Nickname,
C.Last_Name,
C2.Nickname AS Opportunity_Contact_Nickname,
C2.Last_Name AS Opportunity_Contact_Last_Name,
C2.Email_Address AS Opportunity_Contact_Email,
O.Opportunity_Title,
R.Response_Date,
CASE WHEN R.Closed = 1 AND ISNULL(RR.Response_Result_ID, 2) = 2 THEN 1 ELSE 0 END AS Closed,
ISNULL(RR.Response_Result, 'Pending') AS [Leader_Review],
CASE WHEN @EnforceVolunteerGroup = 0 THEN 1 ELSE GT.Volunteer_Group END AS [Is_Volunteer_Group],
R.Response_ID,
C.Contact_GUID
FROM Contacts C
JOIN Responses R ON R.Response_ID = @ResponseID
JOIN Opportunities O ON O.Opportunity_ID = R.Opportunity_ID
JOIN Contacts C2 ON C2.Contact_ID = O.Contact_Person
LEFT JOIN Groups G ON G.Group_ID = O.Add_to_Group
LEFT JOIN Group_Types GT ON GT.Group_Type_ID = G.Group_Type_ID
LEFT JOIN Response_Results RR ON RR.Response_Result_ID = R.Response_Result_ID
WHERE R.Response_ID = @ResponseID
AND C.Contact_GUID = @ContactGUID
ORDER BY C.Contact_ID DESC
FOR JSON PATH, WITHOUT_ARRAY_WRAPPER
),
N'{}' -- fallback to an empty object if no match
)
) AS volunteer_information,

/* 2) Background Check Requirements -> array */
(
SELECT DISTINCT
BCT.Background_Check_Type AS Background_Check_Type,
CASE WHEN (EXISTS
(
SELECT 1
FROM Background_Checks BC
WHERE BC.Background_Check_Type_ID = BCT.Background_Check_Type_ID
AND BC.Contact_ID = P.Contact_ID
AND BC.All_Clear = 1
AND BC.Background_Check_Expires > GETDATE()
)
OR (PR.Background_Check_Type_ID IS NULL AND GR.Background_Check_Required=1 AND EXISTS
(
SELECT 1
FROM Background_Checks BC
WHERE BC.Contact_ID = P.Contact_ID
AND BC.All_Clear = 1
AND BC.Background_Check_Expires > GETDATE()
))
)
THEN 'Completed'
WHEN (EXISTS
(
SELECT 1
FROM Background_Checks BC
WHERE BC.Background_Check_Type_ID = BCT.Background_Check_Type_ID
AND BC.Contact_ID = P.Contact_ID
AND BC.Background_Check_Started >= GetDate()-60
AND BC.Background_Check_Submitted IS NOT NULL
)
OR (PR.Background_Check_Type_ID IS NULL AND GR.Background_Check_Required=1 AND EXISTS
(
SELECT 1
FROM Background_Checks BC
WHERE BC.Contact_ID = P.Contact_ID
AND BC.Background_Check_Started >= GetDate()-60
AND BC.Background_Check_Submitted IS NOT NULL
))
) THEN 'Submitted'
ELSE 'Needed' END AS Background_Check_Status,
ISNULL((
SELECT TOP 1
CONCAT(@BGCWebUrl, BC.Background_Check_GUID)
FROM Background_Checks BC
WHERE BC.Background_Check_Type_ID = BCT.Background_Check_Type_ID
AND BC.Contact_ID = P.Contact_ID
AND BC.Background_Check_Started >= GetDate()-60
AND BC.Background_Check_Submitted IS NULL
ORDER BY BC.Background_Check_Started DESC
), '') AS Background_Check_URL
FROM Participation_Requirements PR
INNER JOIN Group_Roles GR ON GR.Group_Role_ID = PR.Group_Role_ID
INNER JOIN Opportunities O ON O.Group_Role_ID = GR.Group_Role_ID
INNER JOIN Responses R ON R.Opportunity_ID = O.Opportunity_ID
LEFT JOIN Background_Check_Types BCT ON BCT.Background_Check_Type_ID = ISNULL(PR.Background_Check_Type_ID, @CheckTypeID)
INNER JOIN Participants P ON P.Participant_ID = R.Participant_ID
INNER JOIN Contacts C ON C.Contact_ID = P.Contact_ID
WHERE R.Response_ID = @ResponseID
AND C.Contact_GUID = @ContactGUID
FOR JSON PATH
) AS background_checks,

/* 3) Milestone Requirements -> array */
(
SELECT DISTINCT
M.Milestone_Title,
CASE WHEN EXISTS
(
SELECT 1
FROM Participant_Milestones PM
WHERE PM.Milestone_ID = M.Milestone_ID
AND PM.Participant_ID = P.Participant_ID
AND (DATEADD(MONTH, ISNULL(M.Expires_in_Months, 0), PM.Date_Accomplished) > GETDATE()
OR M.Expires_in_Months IS NULL)
)
THEN 'Completed' ELSE 'Needed' END AS Milestone_Status
FROM Participation_Requirements PR
INNER JOIN Group_Roles GR ON GR.Group_Role_ID = PR.Group_Role_ID
INNER JOIN Opportunities O ON O.Group_Role_ID = GR.Group_Role_ID
INNER JOIN Responses R ON R.Opportunity_ID = O.Opportunity_ID
INNER JOIN Milestones M ON M.Milestone_ID = PR.Milestone_ID
INNER JOIN Participants P ON P.Participant_ID = R.Participant_ID
INNER JOIN Contacts C ON C.Contact_ID = P.Contact_ID
WHERE R.Response_ID = @ResponseID
AND C.Contact_GUID = @ContactGUID
FOR JSON PATH
) AS milestones,

/* 4) Certification Requirements -> array */
(
SELECT DISTINCT
CT.Certification_Type,
CASE WHEN EXISTS
(
SELECT 1
FROM Participant_Certifications PC
WHERE PC.Certification_Type_ID = CT.Certification_Type_ID
AND PC.Participant_ID = P.Participant_ID
AND ISNULL(PC.Certification_Expires, GETDATE()) > GETDATE()
)
THEN 'Completed' ELSE 'Needed' END AS Certification_Status
FROM Participation_Requirements PR
INNER JOIN Group_Roles GR ON GR.Group_Role_ID = PR.Group_Role_ID
INNER JOIN Opportunities O ON O.Group_Role_ID = GR.Group_Role_ID
INNER JOIN Responses R ON R.Opportunity_ID = O.Opportunity_ID
INNER JOIN Certification_Types CT ON CT.Certification_Type_ID = PR.Certification_Type_ID
INNER JOIN Participants P ON P.Participant_ID = R.Participant_ID
INNER JOIN Contacts C ON C.Contact_ID = P.Contact_ID
WHERE R.Response_ID = @ResponseID
AND C.Contact_GUID = @ContactGUID
FOR JSON PATH
) AS certifications,

/* 5) Form Requirements -> array */
(
SELECT DISTINCT
F.Form_Title,
CONCAT(@FormWebUrl, F.Form_GUID) AS Form_URL,
CASE WHEN EXISTS
(
SELECT 1
FROM Form_Responses FR
WHERE FR.Form_ID = F.Form_ID
AND FR.Contact_ID = P.Contact_ID
AND DATEADD(MONTH, ISNULL(F.Months_Till_Expires, 1), FR.Response_Date) > GETDATE()
)
THEN 'Completed' ELSE 'Needed' END AS Form_Status
FROM Participation_Requirements PR
INNER JOIN Group_Roles GR ON GR.Group_Role_ID = PR.Group_Role_ID
INNER JOIN Opportunities O ON O.Group_Role_ID = GR.Group_Role_ID
INNER JOIN Responses R ON R.Opportunity_ID = O.Opportunity_ID
INNER JOIN Forms F ON F.Form_ID = PR.Custom_Form_ID
INNER JOIN Participants P ON P.Participant_ID = R.Participant_ID
INNER JOIN Contacts C ON C.Contact_ID = P.Contact_ID
WHERE R.Response_ID = @ResponseID
AND C.Contact_GUID = @ContactGUID
FOR JSON PATH
) AS forms

FOR JSON PATH, WITHOUT_ARRAY_WRAPPER
);
END
GO


-- =============================================
-- SP MetaData Install
-- =============================================
DECLARE @spName NVARCHAR(128) = 'api_custom_VolunteerTrackerWidget_JSON'
DECLARE @spDescription NVARCHAR(500) = 'Returns the status of requirements for a response to a volunteer opportunity.'

IF NOT EXISTS (
SELECT API_Procedure_ID FROM dp_API_Procedures WHERE Procedure_Name = @spName
)
BEGIN
INSERT INTO dp_API_Procedures (Procedure_Name, Description)
VALUES (@spName, @spDescription)
END

-- Grant to Administrators Role
DECLARE @AdminRoleID INT = (
SELECT Role_ID FROM dp_Roles WHERE Role_Name = 'Administrators'
)

IF NOT EXISTS (
SELECT *
FROM dp_Role_API_Procedures RP
INNER JOIN dp_API_Procedures AP ON AP.API_Procedure_ID = RP.API_Procedure_ID
WHERE AP.Procedure_Name = @spName AND RP.Role_ID = @AdminRoleID
)
BEGIN
INSERT INTO dp_Role_API_Procedures (Domain_ID, API_Procedure_ID, Role_ID)
VALUES (
1,
(SELECT API_Procedure_ID FROM dp_API_Procedures WHERE Procedure_Name = @spName),
@AdminRoleID
)
END
GO
Loading