diff --git a/CareTogetherCMS.sln b/CareTogetherCMS.sln
index a94538ae..7e849d13 100644
--- a/CareTogetherCMS.sln
+++ b/CareTogetherCMS.sln
@@ -1,10 +1,10 @@
-
-Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 17
-VisualStudioVersion = 17.0.32014.148
-MinimumVisualStudioVersion = 10.0.40219.1
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "deploy", "deploy", "{B04053C5-CA17-41B1-BC52-8B5393090D45}"
- ProjectSection(SolutionItems) = preProject
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.0.32014.148
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "deploy", "deploy", "{B04053C5-CA17-41B1-BC52-8B5393090D45}"
+ ProjectSection(SolutionItems) = preProject
.github\workflows\cd-prod-dotnet.yml = .github\workflows\cd-prod-dotnet.yml
.github\workflows\cd-prod-react.yml = .github\workflows\cd-prod-react.yml
.github\workflows\ci-pr-dotnet.yml = .github\workflows\ci-pr-dotnet.yml
@@ -13,198 +13,198 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "deploy", "deploy", "{B04053
.github\workflows\playwright.yml = .github\workflows\playwright.yml
EndProjectSection
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CareTogether.Api", "src\CareTogether.Api\CareTogether.Api.csproj", "{67F0E369-C927-4278-A7D0-DDFBE029003F}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{465BA74E-CEF3-4608-8809-8C7A2E979DAC}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "b2c", "b2c", "{7787CCCA-D8C1-4E53-8EBA-1A07B5DEA5AB}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CustomPolicies", "CustomPolicies", "{E477275E-8530-49B4-ADBB-D61DD5E79BD6}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Dev", "Dev", "{7B8B0240-E608-4349-89AD-AB5E410BD273}"
- ProjectSection(SolutionItems) = preProject
- src\b2c\CustomPolicies\Dev\PasswordReset.xml = src\b2c\CustomPolicies\Dev\PasswordReset.xml
- src\b2c\CustomPolicies\Dev\ProfileEdit.xml = src\b2c\CustomPolicies\Dev\ProfileEdit.xml
- src\b2c\CustomPolicies\Dev\readme.md = src\b2c\CustomPolicies\Dev\readme.md
- src\b2c\CustomPolicies\Dev\SignUpOrSignin.xml = src\b2c\CustomPolicies\Dev\SignUpOrSignin.xml
- src\b2c\CustomPolicies\Dev\TrustFrameworkBase.xml = src\b2c\CustomPolicies\Dev\TrustFrameworkBase.xml
- src\b2c\CustomPolicies\Dev\TrustFrameworkExtensions.xml = src\b2c\CustomPolicies\Dev\TrustFrameworkExtensions.xml
- EndProjectSection
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Prod", "Prod", "{2DC7B8F3-2B29-4EC9-BFFF-CC49C817469C}"
- ProjectSection(SolutionItems) = preProject
- src\b2c\CustomPolicies\Prod\PasswordReset.xml = src\b2c\CustomPolicies\Prod\PasswordReset.xml
- src\b2c\CustomPolicies\Prod\ProfileEdit.xml = src\b2c\CustomPolicies\Prod\ProfileEdit.xml
- src\b2c\CustomPolicies\Prod\readme.md = src\b2c\CustomPolicies\Prod\readme.md
- src\b2c\CustomPolicies\Prod\SignUpOrSignin.xml = src\b2c\CustomPolicies\Prod\SignUpOrSignin.xml
- src\b2c\CustomPolicies\Prod\TrustFrameworkBase.xml = src\b2c\CustomPolicies\Prod\TrustFrameworkBase.xml
- src\b2c\CustomPolicies\Prod\TrustFrameworkExtensions.xml = src\b2c\CustomPolicies\Prod\TrustFrameworkExtensions.xml
- EndProjectSection
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CareTogether.Core", "src\CareTogether.Core\CareTogether.Core.csproj", "{82B69D6F-DA25-452B-883D-71B9AE91DF23}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CareTogether.Core.Test", "test\CareTogether.Core.Test\CareTogether.Core.Test.csproj", "{B2CA1F10-216E-404E-A3C2-70423D5BCF5D}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{42D43EAF-EE89-479A-B304-3B5DF56A3494}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{4911A81B-E06B-4090-9252-4359580C3C8E}"
- ProjectSection(SolutionItems) = preProject
- .github\CODEOWNERS = .github\CODEOWNERS
- LICENSE = LICENSE
- README.md = README.md
- swagger.json = swagger.json
- EndProjectSection
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CareTogether.TestData", "test\CareTogether.TestData\CareTogether.TestData.csproj", "{7DC7327F-6F01-4561-943D-D385FB5637E1}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Timelines", "src\Timelines\Timelines.csproj", "{97B1250A-5BD6-4C12-8D9B-63EEBAFB8DC9}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Timelines.Test", "test\Timelines.Test\Timelines.Test.csproj", "{3C0F8F56-17BB-4BF7-8D59-51D8A5F635CA}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CareTogether.AppHost", "src\CareTogether.AppHost\CareTogether.AppHost.csproj", "{7AB05778-70B4-4B95-BE67-D07802D0B57B}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CareTogether.ServiceDefaults", "src\CareTogether.ServiceDefaults\CareTogether.ServiceDefaults.csproj", "{3C6180D9-8BFB-45CE-AD0F-967EBDE7C0F4}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CareTogether.AppHost.Tests", "test\CareTogether.AppHost.Tests\CareTogether.AppHost.Tests.csproj", "{E0AB6E03-C1C4-490F-A56A-8EE45C0C7220}"
-EndProject
-Global
- GlobalSection(SolutionConfigurationPlatforms) = preSolution
- Debug|Any CPU = Debug|Any CPU
- Debug|x64 = Debug|x64
- Debug|x86 = Debug|x86
- Release|Any CPU = Release|Any CPU
- Release|x64 = Release|x64
- Release|x86 = Release|x86
- EndGlobalSection
- GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {67F0E369-C927-4278-A7D0-DDFBE029003F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {67F0E369-C927-4278-A7D0-DDFBE029003F}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {67F0E369-C927-4278-A7D0-DDFBE029003F}.Debug|x64.ActiveCfg = Debug|Any CPU
- {67F0E369-C927-4278-A7D0-DDFBE029003F}.Debug|x64.Build.0 = Debug|Any CPU
- {67F0E369-C927-4278-A7D0-DDFBE029003F}.Debug|x86.ActiveCfg = Debug|Any CPU
- {67F0E369-C927-4278-A7D0-DDFBE029003F}.Debug|x86.Build.0 = Debug|Any CPU
- {67F0E369-C927-4278-A7D0-DDFBE029003F}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {67F0E369-C927-4278-A7D0-DDFBE029003F}.Release|Any CPU.Build.0 = Release|Any CPU
- {67F0E369-C927-4278-A7D0-DDFBE029003F}.Release|x64.ActiveCfg = Release|Any CPU
- {67F0E369-C927-4278-A7D0-DDFBE029003F}.Release|x64.Build.0 = Release|Any CPU
- {67F0E369-C927-4278-A7D0-DDFBE029003F}.Release|x86.ActiveCfg = Release|Any CPU
- {67F0E369-C927-4278-A7D0-DDFBE029003F}.Release|x86.Build.0 = Release|Any CPU
- {82B69D6F-DA25-452B-883D-71B9AE91DF23}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {82B69D6F-DA25-452B-883D-71B9AE91DF23}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {82B69D6F-DA25-452B-883D-71B9AE91DF23}.Debug|x64.ActiveCfg = Debug|Any CPU
- {82B69D6F-DA25-452B-883D-71B9AE91DF23}.Debug|x64.Build.0 = Debug|Any CPU
- {82B69D6F-DA25-452B-883D-71B9AE91DF23}.Debug|x86.ActiveCfg = Debug|Any CPU
- {82B69D6F-DA25-452B-883D-71B9AE91DF23}.Debug|x86.Build.0 = Debug|Any CPU
- {82B69D6F-DA25-452B-883D-71B9AE91DF23}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {82B69D6F-DA25-452B-883D-71B9AE91DF23}.Release|Any CPU.Build.0 = Release|Any CPU
- {82B69D6F-DA25-452B-883D-71B9AE91DF23}.Release|x64.ActiveCfg = Release|Any CPU
- {82B69D6F-DA25-452B-883D-71B9AE91DF23}.Release|x64.Build.0 = Release|Any CPU
- {82B69D6F-DA25-452B-883D-71B9AE91DF23}.Release|x86.ActiveCfg = Release|Any CPU
- {82B69D6F-DA25-452B-883D-71B9AE91DF23}.Release|x86.Build.0 = Release|Any CPU
- {B2CA1F10-216E-404E-A3C2-70423D5BCF5D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {B2CA1F10-216E-404E-A3C2-70423D5BCF5D}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {B2CA1F10-216E-404E-A3C2-70423D5BCF5D}.Debug|x64.ActiveCfg = Debug|Any CPU
- {B2CA1F10-216E-404E-A3C2-70423D5BCF5D}.Debug|x64.Build.0 = Debug|Any CPU
- {B2CA1F10-216E-404E-A3C2-70423D5BCF5D}.Debug|x86.ActiveCfg = Debug|Any CPU
- {B2CA1F10-216E-404E-A3C2-70423D5BCF5D}.Debug|x86.Build.0 = Debug|Any CPU
- {B2CA1F10-216E-404E-A3C2-70423D5BCF5D}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {B2CA1F10-216E-404E-A3C2-70423D5BCF5D}.Release|Any CPU.Build.0 = Release|Any CPU
- {B2CA1F10-216E-404E-A3C2-70423D5BCF5D}.Release|x64.ActiveCfg = Release|Any CPU
- {B2CA1F10-216E-404E-A3C2-70423D5BCF5D}.Release|x64.Build.0 = Release|Any CPU
- {B2CA1F10-216E-404E-A3C2-70423D5BCF5D}.Release|x86.ActiveCfg = Release|Any CPU
- {B2CA1F10-216E-404E-A3C2-70423D5BCF5D}.Release|x86.Build.0 = Release|Any CPU
- {7DC7327F-6F01-4561-943D-D385FB5637E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {7DC7327F-6F01-4561-943D-D385FB5637E1}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {7DC7327F-6F01-4561-943D-D385FB5637E1}.Debug|x64.ActiveCfg = Debug|Any CPU
- {7DC7327F-6F01-4561-943D-D385FB5637E1}.Debug|x64.Build.0 = Debug|Any CPU
- {7DC7327F-6F01-4561-943D-D385FB5637E1}.Debug|x86.ActiveCfg = Debug|Any CPU
- {7DC7327F-6F01-4561-943D-D385FB5637E1}.Debug|x86.Build.0 = Debug|Any CPU
- {7DC7327F-6F01-4561-943D-D385FB5637E1}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {7DC7327F-6F01-4561-943D-D385FB5637E1}.Release|Any CPU.Build.0 = Release|Any CPU
- {7DC7327F-6F01-4561-943D-D385FB5637E1}.Release|x64.ActiveCfg = Release|Any CPU
- {7DC7327F-6F01-4561-943D-D385FB5637E1}.Release|x64.Build.0 = Release|Any CPU
- {7DC7327F-6F01-4561-943D-D385FB5637E1}.Release|x86.ActiveCfg = Release|Any CPU
- {7DC7327F-6F01-4561-943D-D385FB5637E1}.Release|x86.Build.0 = Release|Any CPU
- {97B1250A-5BD6-4C12-8D9B-63EEBAFB8DC9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {97B1250A-5BD6-4C12-8D9B-63EEBAFB8DC9}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {97B1250A-5BD6-4C12-8D9B-63EEBAFB8DC9}.Debug|x64.ActiveCfg = Debug|Any CPU
- {97B1250A-5BD6-4C12-8D9B-63EEBAFB8DC9}.Debug|x64.Build.0 = Debug|Any CPU
- {97B1250A-5BD6-4C12-8D9B-63EEBAFB8DC9}.Debug|x86.ActiveCfg = Debug|Any CPU
- {97B1250A-5BD6-4C12-8D9B-63EEBAFB8DC9}.Debug|x86.Build.0 = Debug|Any CPU
- {97B1250A-5BD6-4C12-8D9B-63EEBAFB8DC9}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {97B1250A-5BD6-4C12-8D9B-63EEBAFB8DC9}.Release|Any CPU.Build.0 = Release|Any CPU
- {97B1250A-5BD6-4C12-8D9B-63EEBAFB8DC9}.Release|x64.ActiveCfg = Release|Any CPU
- {97B1250A-5BD6-4C12-8D9B-63EEBAFB8DC9}.Release|x64.Build.0 = Release|Any CPU
- {97B1250A-5BD6-4C12-8D9B-63EEBAFB8DC9}.Release|x86.ActiveCfg = Release|Any CPU
- {97B1250A-5BD6-4C12-8D9B-63EEBAFB8DC9}.Release|x86.Build.0 = Release|Any CPU
- {3C0F8F56-17BB-4BF7-8D59-51D8A5F635CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {3C0F8F56-17BB-4BF7-8D59-51D8A5F635CA}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {3C0F8F56-17BB-4BF7-8D59-51D8A5F635CA}.Debug|x64.ActiveCfg = Debug|Any CPU
- {3C0F8F56-17BB-4BF7-8D59-51D8A5F635CA}.Debug|x64.Build.0 = Debug|Any CPU
- {3C0F8F56-17BB-4BF7-8D59-51D8A5F635CA}.Debug|x86.ActiveCfg = Debug|Any CPU
- {3C0F8F56-17BB-4BF7-8D59-51D8A5F635CA}.Debug|x86.Build.0 = Debug|Any CPU
- {3C0F8F56-17BB-4BF7-8D59-51D8A5F635CA}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {3C0F8F56-17BB-4BF7-8D59-51D8A5F635CA}.Release|Any CPU.Build.0 = Release|Any CPU
- {3C0F8F56-17BB-4BF7-8D59-51D8A5F635CA}.Release|x64.ActiveCfg = Release|Any CPU
- {3C0F8F56-17BB-4BF7-8D59-51D8A5F635CA}.Release|x64.Build.0 = Release|Any CPU
- {3C0F8F56-17BB-4BF7-8D59-51D8A5F635CA}.Release|x86.ActiveCfg = Release|Any CPU
- {3C0F8F56-17BB-4BF7-8D59-51D8A5F635CA}.Release|x86.Build.0 = Release|Any CPU
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CareTogether.Api", "src\CareTogether.Api\CareTogether.Api.csproj", "{67F0E369-C927-4278-A7D0-DDFBE029003F}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{465BA74E-CEF3-4608-8809-8C7A2E979DAC}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "b2c", "b2c", "{7787CCCA-D8C1-4E53-8EBA-1A07B5DEA5AB}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CustomPolicies", "CustomPolicies", "{E477275E-8530-49B4-ADBB-D61DD5E79BD6}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Dev", "Dev", "{7B8B0240-E608-4349-89AD-AB5E410BD273}"
+ ProjectSection(SolutionItems) = preProject
+ src\b2c\CustomPolicies\Dev\PasswordReset.xml = src\b2c\CustomPolicies\Dev\PasswordReset.xml
+ src\b2c\CustomPolicies\Dev\ProfileEdit.xml = src\b2c\CustomPolicies\Dev\ProfileEdit.xml
+ src\b2c\CustomPolicies\Dev\readme.md = src\b2c\CustomPolicies\Dev\readme.md
+ src\b2c\CustomPolicies\Dev\SignUpOrSignin.xml = src\b2c\CustomPolicies\Dev\SignUpOrSignin.xml
+ src\b2c\CustomPolicies\Dev\TrustFrameworkBase.xml = src\b2c\CustomPolicies\Dev\TrustFrameworkBase.xml
+ src\b2c\CustomPolicies\Dev\TrustFrameworkExtensions.xml = src\b2c\CustomPolicies\Dev\TrustFrameworkExtensions.xml
+ EndProjectSection
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Prod", "Prod", "{2DC7B8F3-2B29-4EC9-BFFF-CC49C817469C}"
+ ProjectSection(SolutionItems) = preProject
+ src\b2c\CustomPolicies\Prod\PasswordReset.xml = src\b2c\CustomPolicies\Prod\PasswordReset.xml
+ src\b2c\CustomPolicies\Prod\ProfileEdit.xml = src\b2c\CustomPolicies\Prod\ProfileEdit.xml
+ src\b2c\CustomPolicies\Prod\readme.md = src\b2c\CustomPolicies\Prod\readme.md
+ src\b2c\CustomPolicies\Prod\SignUpOrSignin.xml = src\b2c\CustomPolicies\Prod\SignUpOrSignin.xml
+ src\b2c\CustomPolicies\Prod\TrustFrameworkBase.xml = src\b2c\CustomPolicies\Prod\TrustFrameworkBase.xml
+ src\b2c\CustomPolicies\Prod\TrustFrameworkExtensions.xml = src\b2c\CustomPolicies\Prod\TrustFrameworkExtensions.xml
+ EndProjectSection
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CareTogether.Core", "src\CareTogether.Core\CareTogether.Core.csproj", "{82B69D6F-DA25-452B-883D-71B9AE91DF23}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CareTogether.Core.Test", "test\CareTogether.Core.Test\CareTogether.Core.Test.csproj", "{B2CA1F10-216E-404E-A3C2-70423D5BCF5D}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{42D43EAF-EE89-479A-B304-3B5DF56A3494}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{4911A81B-E06B-4090-9252-4359580C3C8E}"
+ ProjectSection(SolutionItems) = preProject
+ .github\CODEOWNERS = .github\CODEOWNERS
+ LICENSE = LICENSE
+ README.md = README.md
+ swagger.json = swagger.json
+ EndProjectSection
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CareTogether.TestData", "test\CareTogether.TestData\CareTogether.TestData.csproj", "{7DC7327F-6F01-4561-943D-D385FB5637E1}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Timelines", "src\Timelines\Timelines.csproj", "{97B1250A-5BD6-4C12-8D9B-63EEBAFB8DC9}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Timelines.Test", "test\Timelines.Test\Timelines.Test.csproj", "{3C0F8F56-17BB-4BF7-8D59-51D8A5F635CA}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CareTogether.AppHost", "src\CareTogether.AppHost\CareTogether.AppHost.csproj", "{7AB05778-70B4-4B95-BE67-D07802D0B57B}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CareTogether.ServiceDefaults", "src\CareTogether.ServiceDefaults\CareTogether.ServiceDefaults.csproj", "{3C6180D9-8BFB-45CE-AD0F-967EBDE7C0F4}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CareTogether.AppHost.Tests", "test\CareTogether.AppHost.Tests\CareTogether.AppHost.Tests.csproj", "{E0AB6E03-C1C4-490F-A56A-8EE45C0C7220}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
+ Release|Any CPU = Release|Any CPU
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {67F0E369-C927-4278-A7D0-DDFBE029003F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {67F0E369-C927-4278-A7D0-DDFBE029003F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {67F0E369-C927-4278-A7D0-DDFBE029003F}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {67F0E369-C927-4278-A7D0-DDFBE029003F}.Debug|x64.Build.0 = Debug|Any CPU
+ {67F0E369-C927-4278-A7D0-DDFBE029003F}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {67F0E369-C927-4278-A7D0-DDFBE029003F}.Debug|x86.Build.0 = Debug|Any CPU
+ {67F0E369-C927-4278-A7D0-DDFBE029003F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {67F0E369-C927-4278-A7D0-DDFBE029003F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {67F0E369-C927-4278-A7D0-DDFBE029003F}.Release|x64.ActiveCfg = Release|Any CPU
+ {67F0E369-C927-4278-A7D0-DDFBE029003F}.Release|x64.Build.0 = Release|Any CPU
+ {67F0E369-C927-4278-A7D0-DDFBE029003F}.Release|x86.ActiveCfg = Release|Any CPU
+ {67F0E369-C927-4278-A7D0-DDFBE029003F}.Release|x86.Build.0 = Release|Any CPU
+ {82B69D6F-DA25-452B-883D-71B9AE91DF23}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {82B69D6F-DA25-452B-883D-71B9AE91DF23}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {82B69D6F-DA25-452B-883D-71B9AE91DF23}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {82B69D6F-DA25-452B-883D-71B9AE91DF23}.Debug|x64.Build.0 = Debug|Any CPU
+ {82B69D6F-DA25-452B-883D-71B9AE91DF23}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {82B69D6F-DA25-452B-883D-71B9AE91DF23}.Debug|x86.Build.0 = Debug|Any CPU
+ {82B69D6F-DA25-452B-883D-71B9AE91DF23}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {82B69D6F-DA25-452B-883D-71B9AE91DF23}.Release|Any CPU.Build.0 = Release|Any CPU
+ {82B69D6F-DA25-452B-883D-71B9AE91DF23}.Release|x64.ActiveCfg = Release|Any CPU
+ {82B69D6F-DA25-452B-883D-71B9AE91DF23}.Release|x64.Build.0 = Release|Any CPU
+ {82B69D6F-DA25-452B-883D-71B9AE91DF23}.Release|x86.ActiveCfg = Release|Any CPU
+ {82B69D6F-DA25-452B-883D-71B9AE91DF23}.Release|x86.Build.0 = Release|Any CPU
+ {B2CA1F10-216E-404E-A3C2-70423D5BCF5D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B2CA1F10-216E-404E-A3C2-70423D5BCF5D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B2CA1F10-216E-404E-A3C2-70423D5BCF5D}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {B2CA1F10-216E-404E-A3C2-70423D5BCF5D}.Debug|x64.Build.0 = Debug|Any CPU
+ {B2CA1F10-216E-404E-A3C2-70423D5BCF5D}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {B2CA1F10-216E-404E-A3C2-70423D5BCF5D}.Debug|x86.Build.0 = Debug|Any CPU
+ {B2CA1F10-216E-404E-A3C2-70423D5BCF5D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B2CA1F10-216E-404E-A3C2-70423D5BCF5D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B2CA1F10-216E-404E-A3C2-70423D5BCF5D}.Release|x64.ActiveCfg = Release|Any CPU
+ {B2CA1F10-216E-404E-A3C2-70423D5BCF5D}.Release|x64.Build.0 = Release|Any CPU
+ {B2CA1F10-216E-404E-A3C2-70423D5BCF5D}.Release|x86.ActiveCfg = Release|Any CPU
+ {B2CA1F10-216E-404E-A3C2-70423D5BCF5D}.Release|x86.Build.0 = Release|Any CPU
+ {7DC7327F-6F01-4561-943D-D385FB5637E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7DC7327F-6F01-4561-943D-D385FB5637E1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7DC7327F-6F01-4561-943D-D385FB5637E1}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {7DC7327F-6F01-4561-943D-D385FB5637E1}.Debug|x64.Build.0 = Debug|Any CPU
+ {7DC7327F-6F01-4561-943D-D385FB5637E1}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {7DC7327F-6F01-4561-943D-D385FB5637E1}.Debug|x86.Build.0 = Debug|Any CPU
+ {7DC7327F-6F01-4561-943D-D385FB5637E1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7DC7327F-6F01-4561-943D-D385FB5637E1}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7DC7327F-6F01-4561-943D-D385FB5637E1}.Release|x64.ActiveCfg = Release|Any CPU
+ {7DC7327F-6F01-4561-943D-D385FB5637E1}.Release|x64.Build.0 = Release|Any CPU
+ {7DC7327F-6F01-4561-943D-D385FB5637E1}.Release|x86.ActiveCfg = Release|Any CPU
+ {7DC7327F-6F01-4561-943D-D385FB5637E1}.Release|x86.Build.0 = Release|Any CPU
+ {97B1250A-5BD6-4C12-8D9B-63EEBAFB8DC9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {97B1250A-5BD6-4C12-8D9B-63EEBAFB8DC9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {97B1250A-5BD6-4C12-8D9B-63EEBAFB8DC9}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {97B1250A-5BD6-4C12-8D9B-63EEBAFB8DC9}.Debug|x64.Build.0 = Debug|Any CPU
+ {97B1250A-5BD6-4C12-8D9B-63EEBAFB8DC9}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {97B1250A-5BD6-4C12-8D9B-63EEBAFB8DC9}.Debug|x86.Build.0 = Debug|Any CPU
+ {97B1250A-5BD6-4C12-8D9B-63EEBAFB8DC9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {97B1250A-5BD6-4C12-8D9B-63EEBAFB8DC9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {97B1250A-5BD6-4C12-8D9B-63EEBAFB8DC9}.Release|x64.ActiveCfg = Release|Any CPU
+ {97B1250A-5BD6-4C12-8D9B-63EEBAFB8DC9}.Release|x64.Build.0 = Release|Any CPU
+ {97B1250A-5BD6-4C12-8D9B-63EEBAFB8DC9}.Release|x86.ActiveCfg = Release|Any CPU
+ {97B1250A-5BD6-4C12-8D9B-63EEBAFB8DC9}.Release|x86.Build.0 = Release|Any CPU
+ {3C0F8F56-17BB-4BF7-8D59-51D8A5F635CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3C0F8F56-17BB-4BF7-8D59-51D8A5F635CA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3C0F8F56-17BB-4BF7-8D59-51D8A5F635CA}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {3C0F8F56-17BB-4BF7-8D59-51D8A5F635CA}.Debug|x64.Build.0 = Debug|Any CPU
+ {3C0F8F56-17BB-4BF7-8D59-51D8A5F635CA}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {3C0F8F56-17BB-4BF7-8D59-51D8A5F635CA}.Debug|x86.Build.0 = Debug|Any CPU
+ {3C0F8F56-17BB-4BF7-8D59-51D8A5F635CA}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {3C0F8F56-17BB-4BF7-8D59-51D8A5F635CA}.Release|Any CPU.Build.0 = Release|Any CPU
+ {3C0F8F56-17BB-4BF7-8D59-51D8A5F635CA}.Release|x64.ActiveCfg = Release|Any CPU
+ {3C0F8F56-17BB-4BF7-8D59-51D8A5F635CA}.Release|x64.Build.0 = Release|Any CPU
+ {3C0F8F56-17BB-4BF7-8D59-51D8A5F635CA}.Release|x86.ActiveCfg = Release|Any CPU
+ {3C0F8F56-17BB-4BF7-8D59-51D8A5F635CA}.Release|x86.Build.0 = Release|Any CPU
{7AB05778-70B4-4B95-BE67-D07802D0B57B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7AB05778-70B4-4B95-BE67-D07802D0B57B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7AB05778-70B4-4B95-BE67-D07802D0B57B}.Debug|x64.ActiveCfg = Debug|Any CPU
- {7AB05778-70B4-4B95-BE67-D07802D0B57B}.Debug|x64.Build.0 = Debug|Any CPU
- {7AB05778-70B4-4B95-BE67-D07802D0B57B}.Debug|x86.ActiveCfg = Debug|Any CPU
- {7AB05778-70B4-4B95-BE67-D07802D0B57B}.Debug|x86.Build.0 = Debug|Any CPU
- {7AB05778-70B4-4B95-BE67-D07802D0B57B}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {7AB05778-70B4-4B95-BE67-D07802D0B57B}.Release|Any CPU.Build.0 = Release|Any CPU
- {7AB05778-70B4-4B95-BE67-D07802D0B57B}.Release|x64.ActiveCfg = Release|Any CPU
- {7AB05778-70B4-4B95-BE67-D07802D0B57B}.Release|x64.Build.0 = Release|Any CPU
- {7AB05778-70B4-4B95-BE67-D07802D0B57B}.Release|x86.ActiveCfg = Release|Any CPU
- {7AB05778-70B4-4B95-BE67-D07802D0B57B}.Release|x86.Build.0 = Release|Any CPU
- {3C6180D9-8BFB-45CE-AD0F-967EBDE7C0F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {3C6180D9-8BFB-45CE-AD0F-967EBDE7C0F4}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {3C6180D9-8BFB-45CE-AD0F-967EBDE7C0F4}.Debug|x64.ActiveCfg = Debug|Any CPU
- {3C6180D9-8BFB-45CE-AD0F-967EBDE7C0F4}.Debug|x64.Build.0 = Debug|Any CPU
- {3C6180D9-8BFB-45CE-AD0F-967EBDE7C0F4}.Debug|x86.ActiveCfg = Debug|Any CPU
- {3C6180D9-8BFB-45CE-AD0F-967EBDE7C0F4}.Debug|x86.Build.0 = Debug|Any CPU
- {3C6180D9-8BFB-45CE-AD0F-967EBDE7C0F4}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {3C6180D9-8BFB-45CE-AD0F-967EBDE7C0F4}.Release|Any CPU.Build.0 = Release|Any CPU
- {3C6180D9-8BFB-45CE-AD0F-967EBDE7C0F4}.Release|x64.ActiveCfg = Release|Any CPU
- {3C6180D9-8BFB-45CE-AD0F-967EBDE7C0F4}.Release|x64.Build.0 = Release|Any CPU
- {3C6180D9-8BFB-45CE-AD0F-967EBDE7C0F4}.Release|x86.ActiveCfg = Release|Any CPU
- {3C6180D9-8BFB-45CE-AD0F-967EBDE7C0F4}.Release|x86.Build.0 = Release|Any CPU
- {E0AB6E03-C1C4-490F-A56A-8EE45C0C7220}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {E0AB6E03-C1C4-490F-A56A-8EE45C0C7220}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {E0AB6E03-C1C4-490F-A56A-8EE45C0C7220}.Debug|x64.ActiveCfg = Debug|Any CPU
- {E0AB6E03-C1C4-490F-A56A-8EE45C0C7220}.Debug|x64.Build.0 = Debug|Any CPU
- {E0AB6E03-C1C4-490F-A56A-8EE45C0C7220}.Debug|x86.ActiveCfg = Debug|Any CPU
- {E0AB6E03-C1C4-490F-A56A-8EE45C0C7220}.Debug|x86.Build.0 = Debug|Any CPU
- {E0AB6E03-C1C4-490F-A56A-8EE45C0C7220}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {E0AB6E03-C1C4-490F-A56A-8EE45C0C7220}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7AB05778-70B4-4B95-BE67-D07802D0B57B}.Debug|x64.Build.0 = Debug|Any CPU
+ {7AB05778-70B4-4B95-BE67-D07802D0B57B}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {7AB05778-70B4-4B95-BE67-D07802D0B57B}.Debug|x86.Build.0 = Debug|Any CPU
+ {7AB05778-70B4-4B95-BE67-D07802D0B57B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7AB05778-70B4-4B95-BE67-D07802D0B57B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7AB05778-70B4-4B95-BE67-D07802D0B57B}.Release|x64.ActiveCfg = Release|Any CPU
+ {7AB05778-70B4-4B95-BE67-D07802D0B57B}.Release|x64.Build.0 = Release|Any CPU
+ {7AB05778-70B4-4B95-BE67-D07802D0B57B}.Release|x86.ActiveCfg = Release|Any CPU
+ {7AB05778-70B4-4B95-BE67-D07802D0B57B}.Release|x86.Build.0 = Release|Any CPU
+ {3C6180D9-8BFB-45CE-AD0F-967EBDE7C0F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3C6180D9-8BFB-45CE-AD0F-967EBDE7C0F4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3C6180D9-8BFB-45CE-AD0F-967EBDE7C0F4}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {3C6180D9-8BFB-45CE-AD0F-967EBDE7C0F4}.Debug|x64.Build.0 = Debug|Any CPU
+ {3C6180D9-8BFB-45CE-AD0F-967EBDE7C0F4}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {3C6180D9-8BFB-45CE-AD0F-967EBDE7C0F4}.Debug|x86.Build.0 = Debug|Any CPU
+ {3C6180D9-8BFB-45CE-AD0F-967EBDE7C0F4}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {3C6180D9-8BFB-45CE-AD0F-967EBDE7C0F4}.Release|Any CPU.Build.0 = Release|Any CPU
+ {3C6180D9-8BFB-45CE-AD0F-967EBDE7C0F4}.Release|x64.ActiveCfg = Release|Any CPU
+ {3C6180D9-8BFB-45CE-AD0F-967EBDE7C0F4}.Release|x64.Build.0 = Release|Any CPU
+ {3C6180D9-8BFB-45CE-AD0F-967EBDE7C0F4}.Release|x86.ActiveCfg = Release|Any CPU
+ {3C6180D9-8BFB-45CE-AD0F-967EBDE7C0F4}.Release|x86.Build.0 = Release|Any CPU
+ {E0AB6E03-C1C4-490F-A56A-8EE45C0C7220}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E0AB6E03-C1C4-490F-A56A-8EE45C0C7220}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E0AB6E03-C1C4-490F-A56A-8EE45C0C7220}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {E0AB6E03-C1C4-490F-A56A-8EE45C0C7220}.Debug|x64.Build.0 = Debug|Any CPU
+ {E0AB6E03-C1C4-490F-A56A-8EE45C0C7220}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {E0AB6E03-C1C4-490F-A56A-8EE45C0C7220}.Debug|x86.Build.0 = Debug|Any CPU
+ {E0AB6E03-C1C4-490F-A56A-8EE45C0C7220}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E0AB6E03-C1C4-490F-A56A-8EE45C0C7220}.Release|Any CPU.Build.0 = Release|Any CPU
{E0AB6E03-C1C4-490F-A56A-8EE45C0C7220}.Release|x64.ActiveCfg = Release|Any CPU
{E0AB6E03-C1C4-490F-A56A-8EE45C0C7220}.Release|x64.Build.0 = Release|Any CPU
{E0AB6E03-C1C4-490F-A56A-8EE45C0C7220}.Release|x86.ActiveCfg = Release|Any CPU
{E0AB6E03-C1C4-490F-A56A-8EE45C0C7220}.Release|x86.Build.0 = Release|Any CPU
- EndGlobalSection
- GlobalSection(SolutionProperties) = preSolution
- HideSolutionNode = FALSE
- EndGlobalSection
- GlobalSection(NestedProjects) = preSolution
- {67F0E369-C927-4278-A7D0-DDFBE029003F} = {465BA74E-CEF3-4608-8809-8C7A2E979DAC}
- {7787CCCA-D8C1-4E53-8EBA-1A07B5DEA5AB} = {465BA74E-CEF3-4608-8809-8C7A2E979DAC}
- {E477275E-8530-49B4-ADBB-D61DD5E79BD6} = {7787CCCA-D8C1-4E53-8EBA-1A07B5DEA5AB}
- {7B8B0240-E608-4349-89AD-AB5E410BD273} = {E477275E-8530-49B4-ADBB-D61DD5E79BD6}
- {2DC7B8F3-2B29-4EC9-BFFF-CC49C817469C} = {E477275E-8530-49B4-ADBB-D61DD5E79BD6}
- {82B69D6F-DA25-452B-883D-71B9AE91DF23} = {465BA74E-CEF3-4608-8809-8C7A2E979DAC}
- {B2CA1F10-216E-404E-A3C2-70423D5BCF5D} = {42D43EAF-EE89-479A-B304-3B5DF56A3494}
- {7DC7327F-6F01-4561-943D-D385FB5637E1} = {42D43EAF-EE89-479A-B304-3B5DF56A3494}
- {97B1250A-5BD6-4C12-8D9B-63EEBAFB8DC9} = {465BA74E-CEF3-4608-8809-8C7A2E979DAC}
- {3C0F8F56-17BB-4BF7-8D59-51D8A5F635CA} = {42D43EAF-EE89-479A-B304-3B5DF56A3494}
- {7AB05778-70B4-4B95-BE67-D07802D0B57B} = {465BA74E-CEF3-4608-8809-8C7A2E979DAC}
- {3C6180D9-8BFB-45CE-AD0F-967EBDE7C0F4} = {465BA74E-CEF3-4608-8809-8C7A2E979DAC}
- {E0AB6E03-C1C4-490F-A56A-8EE45C0C7220} = {42D43EAF-EE89-479A-B304-3B5DF56A3494}
- EndGlobalSection
- GlobalSection(ExtensibilityGlobals) = postSolution
- SolutionGuid = {E09CB79A-6E5B-4BAD-B356-64D949892A7E}
- EndGlobalSection
-EndGlobal
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {67F0E369-C927-4278-A7D0-DDFBE029003F} = {465BA74E-CEF3-4608-8809-8C7A2E979DAC}
+ {7787CCCA-D8C1-4E53-8EBA-1A07B5DEA5AB} = {465BA74E-CEF3-4608-8809-8C7A2E979DAC}
+ {E477275E-8530-49B4-ADBB-D61DD5E79BD6} = {7787CCCA-D8C1-4E53-8EBA-1A07B5DEA5AB}
+ {7B8B0240-E608-4349-89AD-AB5E410BD273} = {E477275E-8530-49B4-ADBB-D61DD5E79BD6}
+ {2DC7B8F3-2B29-4EC9-BFFF-CC49C817469C} = {E477275E-8530-49B4-ADBB-D61DD5E79BD6}
+ {82B69D6F-DA25-452B-883D-71B9AE91DF23} = {465BA74E-CEF3-4608-8809-8C7A2E979DAC}
+ {B2CA1F10-216E-404E-A3C2-70423D5BCF5D} = {42D43EAF-EE89-479A-B304-3B5DF56A3494}
+ {7DC7327F-6F01-4561-943D-D385FB5637E1} = {42D43EAF-EE89-479A-B304-3B5DF56A3494}
+ {97B1250A-5BD6-4C12-8D9B-63EEBAFB8DC9} = {465BA74E-CEF3-4608-8809-8C7A2E979DAC}
+ {3C0F8F56-17BB-4BF7-8D59-51D8A5F635CA} = {42D43EAF-EE89-479A-B304-3B5DF56A3494}
+ {7AB05778-70B4-4B95-BE67-D07802D0B57B} = {465BA74E-CEF3-4608-8809-8C7A2E979DAC}
+ {3C6180D9-8BFB-45CE-AD0F-967EBDE7C0F4} = {465BA74E-CEF3-4608-8809-8C7A2E979DAC}
+ {E0AB6E03-C1C4-490F-A56A-8EE45C0C7220} = {42D43EAF-EE89-479A-B304-3B5DF56A3494}
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {E09CB79A-6E5B-4BAD-B356-64D949892A7E}
+ EndGlobalSection
+EndGlobal
diff --git a/src/caretogether-pwa/src/AppRoutes.tsx b/src/caretogether-pwa/src/AppRoutes.tsx
index 023c32ac..9ec95470 100644
--- a/src/caretogether-pwa/src/AppRoutes.tsx
+++ b/src/caretogether-pwa/src/AppRoutes.tsx
@@ -33,7 +33,7 @@ import { InboxScreen } from './Inbox/InboxScreen';
import { FamilyScreenRoute } from './Families/FamilyScreenRoute';
import { ClientsScreenRoute } from './V1Cases/ClientsScreenRoute';
import { V1Referrals } from './V1Referrals/V1Referrals';
-import { Volunteers } from './Volunteers/Volunteers';
+import { VolunteersScreenRoute } from './Volunteers/VolunteersScreenRoute';
import { Communities } from './Communities/Communities';
import { Reports } from './Reports/Reports';
import { Settings } from './Settings/SettingsRoutes';
@@ -191,7 +191,7 @@ function AuthorizedLocationContextWrapper({
} />
} />
} />
- } />
+ } />
} />
} />
} />
diff --git a/src/caretogether-pwa/src/Shell/shellLayoutConstants.ts b/src/caretogether-pwa/src/Shell/shellLayoutConstants.ts
index 75fa5d59..39cb34eb 100644
--- a/src/caretogether-pwa/src/Shell/shellLayoutConstants.ts
+++ b/src/caretogether-pwa/src/Shell/shellLayoutConstants.ts
@@ -1,11 +1,11 @@
-const CHAT_WIDGET_SAFE_HEIGHT = 96;
-const MOBILE_BOTTOM_NAV_HEIGHT = 56;
+const CHAT_WIDGET_SAFE_HEIGHT = 90;
const DESKTOP_APP_BAR_HEIGHT = 48;
const TABLET_APP_BAR_HEIGHT = 64;
const MOBILE_APP_BAR_HEIGHT = 56;
export const EXPANDED_DRAWER_WIDTH = 190;
export const COLLAPSED_DRAWER_WIDTH = 48;
+export const MOBILE_BOTTOM_NAV_HEIGHT = 56;
export const DESKTOP_BOTTOM_SAFE_AREA = CHAT_WIDGET_SAFE_HEIGHT;
export const MOBILE_BOTTOM_SAFE_AREA =
diff --git a/src/caretogether-pwa/src/Utilities/wideTablePageSx.ts b/src/caretogether-pwa/src/Utilities/wideTablePageSx.ts
index 8186edcd..0134ba1b 100644
--- a/src/caretogether-pwa/src/Utilities/wideTablePageSx.ts
+++ b/src/caretogether-pwa/src/Utilities/wideTablePageSx.ts
@@ -1,18 +1,20 @@
-const FEATUREBASE_CHAT_WIDGET_SAFE_HEIGHT = 96;
-const MOBILE_BOTTOM_NAVIGATION_HEIGHT = 56;
-const MOBILE_BOTTOM_SAFE_HEIGHT =
- FEATUREBASE_CHAT_WIDGET_SAFE_HEIGHT + MOBILE_BOTTOM_NAVIGATION_HEIGHT;
+import {
+ DESKTOP_BOTTOM_SAFE_AREA,
+ MOBILE_BOTTOM_NAV_HEIGHT,
+ MOBILE_BOTTOM_SAFE_AREA,
+ SHELL_APP_BAR_HEIGHT,
+} from '../Shell/shellLayoutConstants';
function wideTablePageSx(hasFeaturebaseChat: boolean) {
const bottomOffset = hasFeaturebaseChat
? {
- xs: `${MOBILE_BOTTOM_SAFE_HEIGHT}px`,
- sm: `${MOBILE_BOTTOM_SAFE_HEIGHT}px`,
- md: `${FEATUREBASE_CHAT_WIDGET_SAFE_HEIGHT}px`,
+ xs: `${MOBILE_BOTTOM_SAFE_AREA}px`,
+ sm: `${MOBILE_BOTTOM_SAFE_AREA}px`,
+ md: `${DESKTOP_BOTTOM_SAFE_AREA}px`,
}
: {
- xs: `${MOBILE_BOTTOM_NAVIGATION_HEIGHT}px`,
- sm: `${MOBILE_BOTTOM_NAVIGATION_HEIGHT}px`,
+ xs: `${MOBILE_BOTTOM_NAV_HEIGHT}px`,
+ sm: `${MOBILE_BOTTOM_NAV_HEIGHT}px`,
md: '0px',
};
@@ -21,9 +23,9 @@ function wideTablePageSx(hasFeaturebaseChat: boolean) {
flexDirection: 'column',
flexWrap: 'nowrap',
height: {
- xs: `calc(100dvh - 56px - ${bottomOffset.xs})`,
- sm: `calc(100dvh - 64px - ${bottomOffset.sm})`,
- md: `calc(100dvh - 48px - ${bottomOffset.md})`,
+ xs: `calc(100dvh - ${SHELL_APP_BAR_HEIGHT.xs} - ${bottomOffset.xs})`,
+ sm: `calc(100dvh - ${SHELL_APP_BAR_HEIGHT.sm} - ${bottomOffset.sm})`,
+ md: `calc(100dvh - ${SHELL_APP_BAR_HEIGHT.md} - ${bottomOffset.md})`,
},
// ShellRootLayout always reserves Featurebase space; reclaim it when chat is hidden.
minHeight: 0,
@@ -36,7 +38,7 @@ function wideTablePageSx(hasFeaturebaseChat: boolean) {
return {
...sx,
- marginBottom: `-${FEATUREBASE_CHAT_WIDGET_SAFE_HEIGHT}px`,
+ marginBottom: `-${DESKTOP_BOTTOM_SAFE_AREA}px`,
} as const;
}
diff --git a/src/caretogether-pwa/src/V1Cases/ClientsBrowserToolbarV2.tsx b/src/caretogether-pwa/src/V1Cases/ClientsBrowserToolbarV2.tsx
index 909fa95b..76e6ec46 100644
--- a/src/caretogether-pwa/src/V1Cases/ClientsBrowserToolbarV2.tsx
+++ b/src/caretogether-pwa/src/V1Cases/ClientsBrowserToolbarV2.tsx
@@ -4,7 +4,6 @@ import {
Sort as SortIcon,
} from '@mui/icons-material';
import {
- Box,
Button,
FormControl,
InputAdornment,
@@ -15,13 +14,8 @@ import {
TextField,
} from '@mui/material';
import type { SelectChangeEvent } from '@mui/material/Select';
-import type {
- AssignedIndividualVolunteer,
- Person,
-} from '../GeneratedClient';
-import type {
- AssignmentFilterSelectionsByRole,
-} from '../FunctionAssignments/assignmentRoleColumns';
+import type { AssignedIndividualVolunteer, Person } from '../GeneratedClient';
+import type { AssignmentFilterSelectionsByRole } from '../FunctionAssignments/assignmentRoleColumns';
import { AssignmentRoleFilters } from '../FunctionAssignments/AssignmentRoleFilters';
import { PartneringFamiliesSortMode } from './PartneringFamilies/sortPartneringFamilies';
import { ArrangementsFilter } from './PartneringFamilies/types';
@@ -88,14 +82,16 @@ export function ClientsBrowserToolbarV2({
assignmentRoles.length > 0 &&
!!assignmentPersonLookup &&
!!onAssignmentFilterChange;
- const shouldRenderMoreFilters =
- customFieldCount > 0 && !!onMoreFiltersClick;
+ const shouldRenderMoreFilters = customFieldCount > 0 && !!onMoreFiltersClick;
return (
)}
-
);
}
diff --git a/src/caretogether-pwa/src/V1Cases/ClientsScreenV2.tsx b/src/caretogether-pwa/src/V1Cases/ClientsScreenV2.tsx
index 0f09bd06..e822d981 100644
--- a/src/caretogether-pwa/src/V1Cases/ClientsScreenV2.tsx
+++ b/src/caretogether-pwa/src/V1Cases/ClientsScreenV2.tsx
@@ -160,17 +160,15 @@ export function ClientsScreenV2() {
-
+
Clients
+ {roles.map((role) => (
+
+ ))}
+
+ );
+}
+
+export function VolunteerApprovalRolesCellV2({ roles }: Props) {
+ return (
+
+
+ Family:
+
+
+
+ Individual:
+
+
+
+ );
+}
diff --git a/src/caretogether-pwa/src/Volunteers/VolunteerApprovalTab/VolunteerApprovalTableItem.tsx b/src/caretogether-pwa/src/Volunteers/VolunteerApprovalTab/VolunteerApprovalTableItem.tsx
index 4f061a7e..0913c14f 100644
--- a/src/caretogether-pwa/src/Volunteers/VolunteerApprovalTab/VolunteerApprovalTableItem.tsx
+++ b/src/caretogether-pwa/src/Volunteers/VolunteerApprovalTab/VolunteerApprovalTableItem.tsx
@@ -2,7 +2,6 @@ import Grid from '../../Generic/GridLegacyCompat';
import {
Box,
Checkbox,
- Chip,
TableCell,
TableRow,
Typography,
@@ -17,7 +16,7 @@ import { VolunteerRoleApprovalStatusChip } from '../VolunteerRoleApprovalStatusC
import { familyLastName } from './familyLastName';
import { LazyLoadMountTrigger } from '../../Utilities/LazyLoadMountTrigger';
import { LazyLoad } from '../../Utilities/reactLazyLoadInterop';
-import { sortByPolicyOrder } from '../../Generic/sortByPolicyOrder';
+import { renderVolunteerCustomFieldValue } from './volunteerCustomFieldPresentation';
type VolunteerApprovalTableItemProps = {
volunteerFamily: CombinedFamilyInfo;
@@ -43,35 +42,6 @@ function getRowGroupHeight(
return 39 + (activeAdultsCount + activeChildrenCount) * 33;
}
-function renderCustomFieldValue(
- value: unknown,
- validValues?: string[]
-): React.ReactNode {
- if (value === null || typeof value === 'undefined') {
- return '';
- }
- if (value === true) {
- return 'Yes';
- }
- if (value === false) {
- return 'No';
- }
- if (Array.isArray(value)) {
- const sortedValue =
- validValues && validValues.length > 0
- ? sortByPolicyOrder(value.map(String), validValues)
- : value;
- return (
-
- {sortedValue.map((item) => (
-
- ))}
-
- );
- }
- return String(value);
-}
-
function VolunteerApprovalPlaceholderRow(
props: Pick<
VolunteerApprovalTableItemProps,
@@ -330,7 +300,7 @@ function VolunteerApprovalTableRows(props: VolunteerApprovalTableItemProps) {
familyCustomField?.value ?? volunteerFamilyCustomField?.value;
return (
- {renderCustomFieldValue(
+ {renderVolunteerCustomFieldValue(
familyCustomFieldValue,
fieldPolicy?.validValues
)}
diff --git a/src/caretogether-pwa/src/Volunteers/VolunteerApprovalTab/VolunteerFilter.tsx b/src/caretogether-pwa/src/Volunteers/VolunteerApprovalTab/VolunteerFilter.tsx
index b4e7f55c..563d0a22 100644
--- a/src/caretogether-pwa/src/Volunteers/VolunteerApprovalTab/VolunteerFilter.tsx
+++ b/src/caretogether-pwa/src/Volunteers/VolunteerApprovalTab/VolunteerFilter.tsx
@@ -1,13 +1,6 @@
-import {
- Checkbox,
- FormControl,
- InputBase,
- ListItemText,
- MenuItem,
- Select,
- SelectChangeEvent,
-} from '@mui/material';
-import { FilterList as FilterListIcon } from '@mui/icons-material';
+import { Checkbox, ListItemText, Menu, MenuItem } from '@mui/material';
+import { useState } from 'react';
+import { VolunteerBrowserFilterButtonV2 } from '../VolunteerBrowserFilterButtonV2';
import { filterOption } from './filterOption';
type VolunteerFilterProps = {
label: string;
@@ -20,55 +13,34 @@ export function VolunteerFilter({
options,
setSelected,
}: VolunteerFilterProps) {
+ const [anchorEl, setAnchorEl] = useState(null);
const selectedCount = options.filter((option) => option.selected).length;
- const displayText =
- selectedCount === options.length
- ? label
- : `${label} (${selectedCount}/${options.length})`;
- const handleChange = (event: SelectChangeEvent) => {
- const allOptionKeysPlusSelectedOptionValue = event.target.value;
- setSelected(allOptionKeysPlusSelectedOptionValue);
- };
+ function handleOptionClick(option: filterOption) {
+ setSelected(options.map((option) => option.key).concat(option.value ?? ''));
+ }
return (
-
-
-
+
+ >
);
}
diff --git a/src/caretogether-pwa/src/Volunteers/VolunteerApprovalTab/volunteerApprovalRolePresentation.ts b/src/caretogether-pwa/src/Volunteers/VolunteerApprovalTab/volunteerApprovalRolePresentation.ts
new file mode 100644
index 00000000..b4185062
--- /dev/null
+++ b/src/caretogether-pwa/src/Volunteers/VolunteerApprovalTab/volunteerApprovalRolePresentation.ts
@@ -0,0 +1,58 @@
+import {
+ CombinedFamilyInfo,
+ DateOnlyTimelineOfRoleApprovalStatus,
+} from '../../GeneratedClient';
+import { filterOption } from './filterOption';
+
+export type VolunteerApprovalRoleChipPresentation = {
+ roleName: string;
+ status?: DateOnlyTimelineOfRoleApprovalStatus;
+};
+
+export type VolunteerApprovalRolesPresentation = {
+ familyRoles: VolunteerApprovalRoleChipPresentation[];
+ individualRoles: VolunteerApprovalRoleChipPresentation[];
+};
+
+export function buildVolunteerApprovalRolesPresentation(
+ family: CombinedFamilyInfo,
+ roleFilters: Pick[]
+): VolunteerApprovalRolesPresentation {
+ const familyRoles = roleFilters.map((roleFilter) => ({
+ roleName: roleFilter.key,
+ status:
+ family.volunteerFamilyInfo?.familyRoleApprovals?.[roleFilter.key]
+ ?.effectiveRoleApprovalStatus,
+ }));
+ const individualRolesByName = new Map<
+ string,
+ VolunteerApprovalRoleChipPresentation
+ >();
+
+ family.family?.adults?.forEach((adult) => {
+ const adultId = adult.item1?.id;
+
+ if (!adultId) {
+ return;
+ }
+
+ Object.entries(
+ family.volunteerFamilyInfo?.individualVolunteers?.[adultId]
+ ?.approvalStatusByRole ?? {}
+ ).forEach(([roleName, roleApprovalStatus]) => {
+ if (individualRolesByName.has(roleName)) {
+ return;
+ }
+
+ individualRolesByName.set(roleName, {
+ roleName,
+ status: roleApprovalStatus.effectiveRoleApprovalStatus,
+ });
+ });
+ });
+
+ return {
+ familyRoles,
+ individualRoles: Array.from(individualRolesByName.values()),
+ };
+}
diff --git a/src/caretogether-pwa/src/Volunteers/VolunteerApprovalTab/volunteerApprovalRoleStatusFilters.ts b/src/caretogether-pwa/src/Volunteers/VolunteerApprovalTab/volunteerApprovalRoleStatusFilters.ts
new file mode 100644
index 00000000..75731145
--- /dev/null
+++ b/src/caretogether-pwa/src/Volunteers/VolunteerApprovalTab/volunteerApprovalRoleStatusFilters.ts
@@ -0,0 +1,263 @@
+import { CombinedFamilyInfo, VolunteerInfo } from '../../GeneratedClient';
+import { checkStatusEquivalence } from './checkStatusEquivalence';
+import { notAppliedLabel } from './catchAllLabel';
+import { filterOption } from './filterOption';
+import { filterType } from './filterType';
+
+function selectedFamilyRoleKeys(roleFilters: filterOption[]) {
+ return roleFilters
+ .filter(
+ (filterOption) =>
+ filterOption.selected && filterOption.type !== filterType.Individual
+ )
+ .map((filterOption) => filterOption.key);
+}
+
+function selectedIndividualRoleKeys(roleFilters: filterOption[]) {
+ return roleFilters
+ .filter(
+ (filterOption) =>
+ filterOption.selected && filterOption.type !== filterType.Family
+ )
+ .map((filterOption) => filterOption.key);
+}
+
+function selectedStatusKeys(statusFilters: filterOption[]) {
+ return statusFilters
+ .filter((filterOption) => filterOption.selected)
+ .map((filterOption) => filterOption.value);
+}
+
+function getFamilyMembers(family: CombinedFamilyInfo) {
+ return (
+ (family.volunteerFamilyInfo?.individualVolunteers &&
+ Object.entries(family.volunteerFamilyInfo?.individualVolunteers)) ||
+ []
+ );
+}
+
+function familyHasNotAppliedForAnyRoles(family: CombinedFamilyInfo) {
+ const familyRoleApprovals =
+ family.volunteerFamilyInfo?.familyRoleApprovals ?? {};
+
+ const familyHasAppliedRole = Object.values(familyRoleApprovals).some(
+ (roleApproval) => roleApproval.currentStatus != null
+ );
+
+ if (familyHasAppliedRole) {
+ return false;
+ }
+
+ return getFamilyMembers(family).every(([, volunteer]) => {
+ const individualRoleApprovals = volunteer.approvalStatusByRole ?? {};
+ return Object.values(individualRoleApprovals).every(
+ (roleApproval) => roleApproval.currentStatus == null
+ );
+ });
+}
+
+function familyHasNoValidStatuses(
+ family: CombinedFamilyInfo,
+ roleFilters: filterOption[]
+) {
+ return roleFilters
+ .filter((filterOption) => filterOption.key !== notAppliedLabel)
+ .every(
+ (filterOption) =>
+ family.volunteerFamilyInfo?.familyRoleApprovals?.[filterOption.key] ===
+ undefined
+ );
+}
+
+function familyHasSpecificRoleInValidStatus(
+ family: CombinedFamilyInfo,
+ roleName: string,
+ statusFilters: filterOption[]
+) {
+ return statusFilters
+ .filter((filterOption) => filterOption.key !== notAppliedLabel)
+ .some((status) =>
+ checkStatusEquivalence(
+ status.value,
+ family.volunteerFamilyInfo?.familyRoleApprovals?.[roleName]
+ ?.currentStatus
+ )
+ );
+}
+
+function familyMeetsFilterCriteria(
+ family: CombinedFamilyInfo,
+ roleFilters: filterOption[],
+ statusFilters: filterOption[]
+) {
+ const familyRoleKeys = selectedFamilyRoleKeys(roleFilters);
+ const individualRoleKeys = selectedIndividualRoleKeys(roleFilters);
+ const statusKeys = selectedStatusKeys(statusFilters);
+
+ if (!familyRoleKeys.length) {
+ if (!statusKeys.length) {
+ return individualRoleKeys.length === 0;
+ }
+ return statusKeys.some((status) =>
+ status === notAppliedLabel
+ ? familyHasNoValidStatuses(family, roleFilters)
+ : roleFilters.some((roleFilter) =>
+ checkStatusEquivalence(
+ status,
+ family.volunteerFamilyInfo?.familyRoleApprovals?.[roleFilter.key]
+ ?.currentStatus
+ )
+ )
+ );
+ }
+ return familyRoleKeys.some((roleName) => {
+ if (roleName === notAppliedLabel) {
+ return familyHasNotAppliedForAnyRoles(family);
+ }
+
+ const familyHasRole =
+ family.volunteerFamilyInfo?.familyRoleApprovals?.[roleName] !==
+ undefined;
+ if (!familyHasRole) {
+ return familyHasRole;
+ }
+ if (statusKeys.length === 0) {
+ return familyHasSpecificRoleInValidStatus(
+ family,
+ roleName,
+ statusFilters
+ );
+ }
+ return statusKeys.some((status) =>
+ checkStatusEquivalence(
+ status,
+ family.volunteerFamilyInfo?.familyRoleApprovals?.[roleName]
+ ?.currentStatus
+ )
+ );
+ });
+}
+
+function familyMemberHasNoValidStatuses(
+ volunteer: VolunteerInfo,
+ roleFilters: filterOption[]
+) {
+ return roleFilters
+ .filter((filterOption) => filterOption.key !== notAppliedLabel)
+ .every((filterOption) =>
+ checkStatusEquivalence(
+ volunteer.approvalStatusByRole?.[filterOption.key]?.currentStatus,
+ null
+ )
+ );
+}
+
+function familyMemberHasSpecificRoleInValidStatus(
+ volunteer: VolunteerInfo,
+ roleName: string,
+ statusFilters: filterOption[]
+) {
+ return statusFilters
+ .filter((filterOption) => filterOption.key !== notAppliedLabel)
+ .some((status) =>
+ checkStatusEquivalence(
+ status.value,
+ volunteer.approvalStatusByRole?.[roleName]?.currentStatus
+ )
+ );
+}
+
+function familyMemberHasARoleInSelectedStatus(
+ volunteer: VolunteerInfo,
+ status: string,
+ roleFilters: filterOption[]
+) {
+ return status === notAppliedLabel
+ ? familyMemberHasNoValidStatuses(volunteer, roleFilters)
+ : roleFilters.some((roleFilter) =>
+ checkStatusEquivalence(
+ volunteer.approvalStatusByRole?.[roleFilter.key]?.currentStatus,
+ status
+ )
+ );
+}
+
+function familyMembersMeetFilterCriteria(
+ family: CombinedFamilyInfo,
+ roleFilters: filterOption[],
+ statusFilters: filterOption[]
+) {
+ const familyMembers = getFamilyMembers(family);
+ const familyRoleKeys = selectedFamilyRoleKeys(roleFilters);
+ const individualRoleKeys = selectedIndividualRoleKeys(roleFilters);
+ const statusKeys = selectedStatusKeys(statusFilters);
+
+ if (!individualRoleKeys.length) {
+ if (!statusKeys.length) {
+ return !familyRoleKeys.length;
+ }
+ return statusKeys.some(
+ (status) =>
+ familyMembers.filter(([, volunteer]) =>
+ familyMemberHasARoleInSelectedStatus(
+ volunteer,
+ status ? status : notAppliedLabel,
+ roleFilters
+ )
+ ).length > 0
+ );
+ }
+ return individualRoleKeys.some((roleName) => {
+ if (roleName === notAppliedLabel) {
+ return familyHasNotAppliedForAnyRoles(family);
+ }
+
+ return familyMembers.some(([, volunteer]) => {
+ if (!statusKeys.length) {
+ return familyMemberHasSpecificRoleInValidStatus(
+ volunteer,
+ roleName,
+ statusFilters
+ );
+ }
+ return statusKeys.some((status) =>
+ checkStatusEquivalence(
+ status,
+ volunteer.approvalStatusByRole?.[roleName]?.currentStatus
+ )
+ );
+ });
+ });
+}
+
+export function familyOrFamilyMembersMeetRoleStatusFilterCriteria(
+ family: CombinedFamilyInfo,
+ roleFilters: filterOption[],
+ statusFilters: filterOption[]
+) {
+ const familyMeetsRoleCriteria = familyMeetsFilterCriteria(
+ family,
+ roleFilters,
+ statusFilters
+ );
+ const familyMembersMeetRoleCriteria = familyMembersMeetFilterCriteria(
+ family,
+ roleFilters,
+ statusFilters
+ );
+ const familyRolesSelected = selectedFamilyRoleKeys(roleFilters).length > 0;
+ const individualRolesSelected =
+ selectedIndividualRoleKeys(roleFilters).length > 0;
+ const statusesSelected = selectedStatusKeys(statusFilters).length > 0;
+ let result = true;
+ if (familyRolesSelected && individualRolesSelected) {
+ result = familyMeetsRoleCriteria || familyMembersMeetRoleCriteria;
+ } else if (familyRolesSelected) {
+ result = familyMeetsRoleCriteria;
+ } else if (individualRolesSelected) {
+ result = familyMembersMeetRoleCriteria;
+ } else if (statusesSelected) {
+ result = familyMeetsRoleCriteria || familyMembersMeetRoleCriteria;
+ }
+ return result;
+}
diff --git a/src/caretogether-pwa/src/Volunteers/VolunteerApprovalTab/volunteerCustomFieldPresentation.tsx b/src/caretogether-pwa/src/Volunteers/VolunteerApprovalTab/volunteerCustomFieldPresentation.tsx
new file mode 100644
index 00000000..4bb99a3c
--- /dev/null
+++ b/src/caretogether-pwa/src/Volunteers/VolunteerApprovalTab/volunteerCustomFieldPresentation.tsx
@@ -0,0 +1,32 @@
+import { Box, Chip } from '@mui/material';
+import { ReactNode } from 'react';
+import { sortByPolicyOrder } from '../../Generic/sortByPolicyOrder';
+
+export function renderVolunteerCustomFieldValue(
+ value: unknown,
+ validValues?: string[]
+): ReactNode {
+ if (value === null || typeof value === 'undefined') {
+ return '';
+ }
+ if (value === true) {
+ return 'Yes';
+ }
+ if (value === false) {
+ return 'No';
+ }
+ if (Array.isArray(value)) {
+ const sortedValue =
+ validValues && validValues.length > 0
+ ? sortByPolicyOrder(value.map(String), validValues)
+ : value;
+ return (
+
+ {sortedValue.map((item) => (
+
+ ))}
+
+ );
+ }
+ return String(value);
+}
diff --git a/src/caretogether-pwa/src/Volunteers/VolunteerApprovalTab/volunteerMissingRequirementsPresentation.ts b/src/caretogether-pwa/src/Volunteers/VolunteerApprovalTab/volunteerMissingRequirementsPresentation.ts
new file mode 100644
index 00000000..5be76b2f
--- /dev/null
+++ b/src/caretogether-pwa/src/Volunteers/VolunteerApprovalTab/volunteerMissingRequirementsPresentation.ts
@@ -0,0 +1,161 @@
+import { CombinedFamilyInfo } from '../../GeneratedClient';
+import { personNameString } from '../../Families/PersonName';
+import { notAppliedLabel } from './catchAllLabel';
+import { filterOption } from './filterOption';
+import { filterType } from './filterType';
+
+export const missingRequirementFilterValue = '__missing__';
+export const completeRequirementFilterValue = '__complete__';
+
+export type RequirementFilterValue =
+ | typeof completeRequirementFilterValue
+ | typeof missingRequirementFilterValue
+ | string;
+
+export type VolunteerMissingRequirementGroup = {
+ label: string;
+ requirements: string[];
+};
+
+function selectedRoleFilters(roleFilters: filterOption[]) {
+ return roleFilters.filter(
+ (roleFilter) => roleFilter.selected && roleFilter.key !== notAppliedLabel
+ );
+}
+
+function selectedFamilyRoleNames(roleFilters: filterOption[]) {
+ return selectedRoleFilters(roleFilters)
+ .filter((roleFilter) => roleFilter.type !== filterType.Individual)
+ .map((roleFilter) => roleFilter.key);
+}
+
+function selectedIndividualRoleNames(roleFilters: filterOption[]) {
+ return selectedRoleFilters(roleFilters)
+ .filter((roleFilter) => roleFilter.type !== filterType.Family)
+ .map((roleFilter) => roleFilter.key);
+}
+
+function requirementNames(
+ missingRequirements:
+ | NonNullable<
+ CombinedFamilyInfo['volunteerFamilyInfo']
+ >['missingRequirements']
+ | undefined
+) {
+ return Array.from(
+ new Set(
+ (missingRequirements ?? [])
+ .map((requirement) => requirement.item1)
+ .filter((requirement): requirement is string => Boolean(requirement))
+ )
+ );
+}
+
+function shouldIncludeFamilyRequirements(
+ family: CombinedFamilyInfo,
+ familyRoleNames: string[],
+ individualRoleNames: string[]
+) {
+ if (!familyRoleNames.length && !individualRoleNames.length) {
+ return true;
+ }
+
+ return familyRoleNames.some(
+ (roleName) =>
+ family.volunteerFamilyInfo?.familyRoleApprovals?.[roleName] !== undefined
+ );
+}
+
+function shouldIncludeIndividualRequirements(
+ approvalStatusByRole:
+ | NonNullable<
+ NonNullable<
+ CombinedFamilyInfo['volunteerFamilyInfo']
+ >['individualVolunteers']
+ >[string]['approvalStatusByRole']
+ | undefined,
+ familyRoleNames: string[],
+ individualRoleNames: string[]
+) {
+ if (!familyRoleNames.length && !individualRoleNames.length) {
+ return true;
+ }
+
+ return individualRoleNames.some(
+ (roleName) => approvalStatusByRole?.[roleName] !== undefined
+ );
+}
+
+export function buildVolunteerMissingRequirementGroups(
+ family: CombinedFamilyInfo,
+ roleFilters: filterOption[]
+): VolunteerMissingRequirementGroup[] {
+ const familyRoleNames = selectedFamilyRoleNames(roleFilters);
+ const individualRoleNames = selectedIndividualRoleNames(roleFilters);
+ const familyRequirements = shouldIncludeFamilyRequirements(
+ family,
+ familyRoleNames,
+ individualRoleNames
+ )
+ ? requirementNames(family.volunteerFamilyInfo?.missingRequirements)
+ : [];
+ const familyGroups: VolunteerMissingRequirementGroup[] =
+ familyRequirements.length > 0
+ ? [{ label: 'Family', requirements: familyRequirements }]
+ : [];
+ const individualGroups = Object.entries(
+ family.volunteerFamilyInfo?.individualVolunteers ?? {}
+ ).flatMap(([personId, volunteer]) => {
+ const requirements = shouldIncludeIndividualRequirements(
+ volunteer.approvalStatusByRole,
+ familyRoleNames,
+ individualRoleNames
+ )
+ ? requirementNames(volunteer.missingRequirements)
+ : [];
+
+ if (!requirements.length) {
+ return [];
+ }
+
+ const person = family.family?.adults?.find(
+ (adult) => adult.item1?.id === personId
+ )?.item1;
+
+ return [
+ {
+ label: person ? personNameString(person) : 'Volunteer',
+ requirements,
+ },
+ ];
+ });
+
+ return familyGroups.concat(individualGroups);
+}
+
+export function familyHasMissingRequirements(
+ family: CombinedFamilyInfo,
+ roleFilters: filterOption[],
+ selectedRequirement: RequirementFilterValue | undefined
+) {
+ if (!selectedRequirement) {
+ return true;
+ }
+
+ const groups = buildVolunteerMissingRequirementGroups(family, roleFilters);
+ const hasMissingRequirements = groups.some(
+ (group) => group.requirements.length > 0
+ );
+
+ if (selectedRequirement === missingRequirementFilterValue) {
+ return hasMissingRequirements;
+ }
+
+ if (selectedRequirement === completeRequirementFilterValue) {
+ return !hasMissingRequirements;
+ }
+
+ return groups.some((group) =>
+ group.requirements.includes(selectedRequirement)
+ );
+}
diff --git a/src/caretogether-pwa/src/Volunteers/VolunteerBrowserFilterButtonV2.tsx b/src/caretogether-pwa/src/Volunteers/VolunteerBrowserFilterButtonV2.tsx
new file mode 100644
index 00000000..0b5b8f87
--- /dev/null
+++ b/src/caretogether-pwa/src/Volunteers/VolunteerBrowserFilterButtonV2.tsx
@@ -0,0 +1,44 @@
+import {
+ ArrowDropDown as ArrowDropDownIcon,
+ FilterList as FilterListIcon,
+} from '@mui/icons-material';
+import { Button } from '@mui/material';
+
+type VolunteerBrowserFilterButtonV2Props = {
+ activeCount?: number;
+ label: string;
+ selectedLabel?: string;
+ totalCount?: number;
+ size?: 'small' | 'medium';
+ onClick: (event: React.MouseEvent) => void;
+};
+
+export function VolunteerBrowserFilterButtonV2({
+ activeCount,
+ label,
+ selectedLabel,
+ totalCount,
+ size,
+ onClick,
+}: VolunteerBrowserFilterButtonV2Props) {
+ const displayText =
+ selectedLabel ?? `${label} (${activeCount ?? 0}/${totalCount ?? 0})`;
+
+ return (
+ }
+ endIcon={}
+ onClick={onClick}
+ size={size}
+ sx={{
+ borderColor: 'divider',
+ color: 'text.primary',
+ minHeight: 34,
+ px: 1.25,
+ }}
+ >
+ {displayText}
+
+ );
+}
diff --git a/src/caretogether-pwa/src/Volunteers/VolunteersBrowserV2.tsx b/src/caretogether-pwa/src/Volunteers/VolunteersBrowserV2.tsx
new file mode 100644
index 00000000..c40e7ffd
--- /dev/null
+++ b/src/caretogether-pwa/src/Volunteers/VolunteersBrowserV2.tsx
@@ -0,0 +1,281 @@
+import { Box, Stack, Typography } from '@mui/material';
+import type { GridFilterModel, GridRowSelectionModel } from '@mui/x-data-grid';
+import { useMemo, useState } from 'react';
+import { useFeatureFlagEnabled } from 'posthog-js/react';
+import { useRecoilValue } from 'recoil';
+import { EmailAddress, Permission } from '../GeneratedClient';
+import { useAppNavigate } from '../Hooks/useAppNavigate';
+import { useGlobalSnackBar } from '../Hooks/useGlobalSnackBar';
+import { useLoadable } from '../Hooks/useLoadable';
+import { useSidePanel } from '../Hooks/useSidePanel';
+import { v2Typography } from '../Families/v2Typography';
+import { selectedLocationContextState } from '../Model/Data';
+import { organizationConfigurationQuery } from '../Model/ConfigurationModel';
+import { useAllVolunteerFamiliesPermissions } from '../Model/SessionModel';
+import { BulkSmsSideSheet } from './BulkSmsSideSheet';
+import { CreateVolunteerFamilyDrawer } from './CreateVolunteerFamilyDrawer';
+import { VolunteerAssignmentFiltersSidePanel } from './VolunteerApprovalTab/VolunteerAssignmentFiltersSidePanel';
+import { VolunteerCustomFieldFiltersSidePanel } from './VolunteerApprovalTab/VolunteerCustomFieldFiltersSidePanel';
+import { VolunteersDataGridV2 } from './VolunteersDataGridV2';
+import { VolunteersToolbarV2 } from './VolunteersToolbarV2';
+import { useVolunteersBrowserViewModel } from './useVolunteersBrowserViewModel';
+import { filterOption } from './VolunteerApprovalTab/filterOption';
+import {
+ gridFilterModelFromVolunteerFilters,
+ volunteerFiltersFromGridFilterModel,
+} from './volunteersGridFilterAdapter';
+
+function selectedFilterValues(filters: filterOption[]) {
+ return filters
+ .filter((filter) => filter.selected && filter.value !== undefined)
+ .map((filter) => filter.value!);
+}
+
+export function VolunteersBrowserV2() {
+ const appNavigate = useAppNavigate();
+ const permissions = useAllVolunteerFamiliesPermissions();
+ const updateTestFamilyFlagEnabled = useFeatureFlagEnabled(
+ 'updateTestFamilyFlag'
+ );
+ const { setAndShowGlobalSnackBar } = useGlobalSnackBar();
+ const { locationId } = useRecoilValue(selectedLocationContextState);
+ const organizationConfiguration = useLoadable(organizationConfigurationQuery);
+ const [createVolunteerFamilyDrawerOpen, setCreateVolunteerFamilyDrawerOpen] =
+ useState(false);
+ const [smsMode, setSmsMode] = useState(false);
+ const [selectedFamilyIds, setSelectedFamilyIds] = useState([]);
+
+ const {
+ activeAssignmentFilterCount,
+ activeCustomFieldFilterCount,
+ arrangementTypes,
+ assignmentFilters,
+ customFieldCount,
+ customFieldFilters,
+ customFields,
+ getCustomFieldFilterOptionsForField,
+ loading,
+ requirementFilter,
+ requirementFilterOptions,
+ roleFilters,
+ rows,
+ searchValue,
+ setAssignmentFilter,
+ setCustomFieldFilter,
+ setRequirementFilter,
+ setRoleFilterValues,
+ setSearchValue,
+ setStatusFilterValues,
+ statusFilters,
+ visibleVolunteerFamilies,
+ } = useVolunteersBrowserViewModel();
+ const {
+ SidePanel: AssignmentFiltersSidePanel,
+ openSidePanel: openAssignmentFiltersSidePanel,
+ closeSidePanel: closeAssignmentFiltersSidePanel,
+ } = useSidePanel();
+ const {
+ SidePanel: CustomFieldFiltersSidePanel,
+ openSidePanel: openCustomFieldFiltersSidePanel,
+ closeSidePanel: closeCustomFieldFiltersSidePanel,
+ } = useSidePanel();
+ const canCreateVolunteerFamily =
+ permissions(Permission.EditFamilyInfo) &&
+ permissions(Permission.ActivateVolunteerFamily);
+ const smsSourcePhoneNumbers = organizationConfiguration?.locations?.find(
+ (location) => location.id === locationId
+ )?.smsSourcePhoneNumbers;
+ const canUseBulkEmail = permissions(Permission.SendBulkSms);
+ const canUseBulkSms =
+ permissions(Permission.SendBulkSms) &&
+ Boolean(smsSourcePhoneNumbers && smsSourcePhoneNumbers.length > 0);
+ const visibleRowIds = useMemo(() => rows.map((row) => row.id), [rows]);
+ const rowSelectionModel = useMemo(
+ () => ({
+ type: 'include',
+ ids: new Set(
+ selectedFamilyIds.filter((id) => visibleRowIds.includes(id))
+ ),
+ }),
+ [selectedFamilyIds, visibleRowIds]
+ );
+ const selectedVolunteerCount = rowSelectionModel.ids.size;
+ const selectedVolunteerFamilies = useMemo(() => {
+ const selectedFamilyIdSet = new Set(selectedFamilyIds);
+
+ return visibleVolunteerFamilies.filter((family) =>
+ selectedFamilyIdSet.has(family.family!.id!)
+ );
+ }, [selectedFamilyIds, visibleVolunteerFamilies]);
+ const filterModel = useMemo(
+ () =>
+ gridFilterModelFromVolunteerFilters({
+ logicOperator: 'and',
+ requirementFilter,
+ roleFilters: selectedFilterValues(roleFilters),
+ statusFilters: selectedFilterValues(statusFilters),
+ }),
+ [requirementFilter, roleFilters, statusFilters]
+ );
+
+ function selectedFamilyContactEmails() {
+ return selectedVolunteerFamilies
+ .map((family) => {
+ const primaryContactPerson = family.family?.adults?.find(
+ (adult) =>
+ adult.item1?.id === family.family?.primaryFamilyContactPersonId
+ );
+ const preferredEmailAddress =
+ primaryContactPerson?.item1?.emailAddresses?.find(
+ (email) =>
+ email.id === primaryContactPerson.item1?.preferredEmailAddressId
+ );
+ return preferredEmailAddress;
+ })
+ .filter((email) => typeof email !== 'undefined') as EmailAddress[];
+ }
+
+ function copyEmailAddresses() {
+ const emailAddresses = selectedFamilyContactEmails();
+ navigator.clipboard.writeText(
+ emailAddresses.map((email) => email.address).join('; ')
+ );
+ setAndShowGlobalSnackBar(
+ `Found and copied ${emailAddresses.length} email addresses for ${selectedVolunteerFamilies.length} selected families to clipboard`
+ );
+ }
+
+ function clearSelection() {
+ setSelectedFamilyIds([]);
+ }
+
+ function handleRowSelectionModelChange(model: GridRowSelectionModel) {
+ const visibleRowIdSet = new Set(visibleRowIds);
+
+ if (model.type === 'exclude') {
+ setSelectedFamilyIds(visibleRowIds.filter((id) => !model.ids.has(id)));
+ return;
+ }
+
+ setSelectedFamilyIds(
+ Array.from(model.ids)
+ .map(String)
+ .filter((id) => visibleRowIdSet.has(id))
+ );
+ }
+
+ function handleAssignmentFilterChange(
+ arrangementType: string,
+ selectedValues: Parameters[1]
+ ) {
+ clearSelection();
+ setAssignmentFilter(arrangementType, selectedValues);
+ }
+
+ function handleCustomFieldFilterChange(
+ fieldName: string,
+ selectedValues: Parameters[1]
+ ) {
+ clearSelection();
+ setCustomFieldFilter(fieldName, selectedValues);
+ }
+
+ function handleSearchChange(value: string) {
+ clearSelection();
+ setSearchValue(value);
+ }
+
+ function handleFilterModelChange(model: GridFilterModel) {
+ const filters = volunteerFiltersFromGridFilterModel(model);
+
+ clearSelection();
+ setRoleFilterValues(filters.roleFilters);
+ setStatusFilterValues(filters.statusFilters);
+ setRequirementFilter(
+ filters.requirementFilter as Parameters[0]
+ );
+ }
+
+ return (
+
+
+
+ Volunteers
+
+
+ Review volunteer families.
+
+
+ setCreateVolunteerFamilyDrawerOpen(true)}
+ onCustomFieldFiltersClick={openCustomFieldFiltersSidePanel}
+ onToggleBulkSms={() => setSmsMode(!smsMode)}
+ />
+
+
+
+
+
+
+
+ appNavigate.family(row.id)}
+ onRowSelectionModelChange={handleRowSelectionModelChange}
+ requirementFilterOptions={requirementFilterOptions}
+ roleFilters={roleFilters}
+ rowSelectionModel={rowSelectionModel}
+ rows={rows}
+ statusFilters={statusFilters}
+ updateTestFamilyFlagEnabled={updateTestFamilyFlagEnabled}
+ />
+
+ {createVolunteerFamilyDrawerOpen && (
+ {
+ setCreateVolunteerFamilyDrawerOpen(false);
+ if (!volunteerFamilyId) return;
+ appNavigate.family(volunteerFamilyId);
+ }}
+ />
+ )}
+ {smsMode && (
+ setSmsMode(false)}
+ />
+ )}
+
+ );
+}
diff --git a/src/caretogether-pwa/src/Volunteers/VolunteersDataGridV2.tsx b/src/caretogether-pwa/src/Volunteers/VolunteersDataGridV2.tsx
new file mode 100644
index 00000000..b8be14aa
--- /dev/null
+++ b/src/caretogether-pwa/src/Volunteers/VolunteersDataGridV2.tsx
@@ -0,0 +1,325 @@
+import {
+ Box,
+ Chip,
+ Stack,
+ Tooltip,
+ Typography,
+ useTheme,
+} from '@mui/material';
+import { Check as CheckIcon } from '@mui/icons-material';
+import {
+ DataGrid,
+ GridColDef,
+ GridFilterModel,
+ GridRowSelectionModel,
+} from '@mui/x-data-grid';
+import { useMemo } from 'react';
+import { CustomField } from '../GeneratedClient';
+import { familyLastName } from '../Families/FamilyUtils';
+import { TestFamilyBadge } from '../Families/TestFamilyBadge';
+import { v2DataGridStyles } from '../Families/v2DataGridStyles';
+import { v2Typography } from '../Families/v2Typography';
+import { filterOption } from './VolunteerApprovalTab/filterOption';
+import { VolunteerApprovalRolesCellV2 } from './VolunteerApprovalRolesCellV2';
+import { renderVolunteerCustomFieldValue } from './VolunteerApprovalTab/volunteerCustomFieldPresentation';
+import {
+ completeRequirementFilterValue,
+ missingRequirementFilterValue,
+} from './VolunteerApprovalTab/volunteerMissingRequirementsPresentation';
+import { VolunteerBrowserRowV2 } from './useVolunteersBrowserViewModel';
+
+const MAX_VISIBLE_MISSING_REQUIREMENTS = 4;
+const BULK_ACTION_SELECTION_TOOLTIP = 'Select families to enable bulk actions.';
+
+function MissingRequirementsCell({ row }: { row: VolunteerBrowserRowV2 }) {
+ const missingRequirements = row.missingRequirementGroups.flatMap(
+ (group) => group.requirements
+ );
+
+ if (!missingRequirements.length) {
+ return (
+ }
+ label="Complete"
+ size="small"
+ variant="outlined"
+ />
+ );
+ }
+
+ let visibleCount = 0;
+ const visibleGroups = row.missingRequirementGroups.flatMap((group) => {
+ if (visibleCount >= MAX_VISIBLE_MISSING_REQUIREMENTS) {
+ return [];
+ }
+
+ const requirements = group.requirements.slice(
+ 0,
+ MAX_VISIBLE_MISSING_REQUIREMENTS - visibleCount
+ );
+ visibleCount += requirements.length;
+
+ return [{ ...group, requirements }];
+ });
+ const hiddenCount = missingRequirements.length - visibleCount;
+ const tooltipTitle = row.missingRequirementGroups
+ .map((group) => `${group.label}:\n${group.requirements.join('\n')}`)
+ .join('\n');
+
+ return (
+ {tooltipTitle}}
+ >
+
+ {visibleGroups.map((group) => (
+
+
+ {group.label}
+
+ {group.requirements.map((requirement) => (
+
+ • {requirement}
+
+ ))}
+
+ ))}
+ {hiddenCount > 0 && (
+
+ +{hiddenCount} more
+
+ )}
+
+
+ );
+}
+
+type VolunteersDataGridV2Props = {
+ customFields: CustomField[];
+ filterModel: GridFilterModel;
+ loading?: boolean;
+ onFilterModelChange: (model: GridFilterModel) => void;
+ onRowClick: (row: VolunteerBrowserRowV2) => void;
+ onRowSelectionModelChange: (model: GridRowSelectionModel) => void;
+ requirementFilterOptions: string[];
+ roleFilters: filterOption[];
+ rowSelectionModel: GridRowSelectionModel;
+ rows: VolunteerBrowserRowV2[];
+ statusFilters: filterOption[];
+ updateTestFamilyFlagEnabled?: boolean;
+};
+
+function displayValues(values: string[]) {
+ return values.length > 0 ? values.join(', ') : '-';
+}
+
+function filterValueOptions(filters: filterOption[]) {
+ return filters
+ .filter((filter) => filter.value !== undefined)
+ .map((filter) => ({
+ label: filter.key,
+ value: filter.value!,
+ }));
+}
+
+function requirementValueOptions(requirementNames: string[]) {
+ return [
+ { label: 'Missing', value: missingRequirementFilterValue },
+ { label: 'Complete', value: completeRequirementFilterValue },
+ ].concat(
+ requirementNames.map((requirementName) => ({
+ label: requirementName,
+ value: requirementName,
+ }))
+ );
+}
+
+function compareByFamilyLastName(
+ firstRow: VolunteerBrowserRowV2 | null,
+ secondRow: VolunteerBrowserRowV2 | null
+) {
+ const firstFamilyName = firstRow ? familyLastName(firstRow.sourceFamily) : '';
+ const secondFamilyName = secondRow
+ ? familyLastName(secondRow.sourceFamily)
+ : '';
+ const familyNameComparison = firstFamilyName.localeCompare(secondFamilyName);
+
+ return familyNameComparison !== 0
+ ? familyNameComparison
+ : (firstRow?.id ?? '').localeCompare(secondRow?.id ?? '');
+}
+
+function buildColumns(
+ customFields: CustomField[],
+ roleFilters: filterOption[],
+ statusFilters: filterOption[],
+ requirementFilterOptions: string[],
+ updateTestFamilyFlagEnabled?: boolean
+): GridColDef[] {
+ const fixedColumns: GridColDef[] = [
+ {
+ field: 'family',
+ headerName: 'Family Name',
+ flex: 1,
+ minWidth: 220,
+ filterable: false,
+ sortComparator: (_value1, _value2, cellParams1, cellParams2) =>
+ compareByFamilyLastName(
+ cellParams1.api.getRow(
+ cellParams1.id
+ ) as VolunteerBrowserRowV2 | null,
+ cellParams2.api.getRow(cellParams2.id) as VolunteerBrowserRowV2 | null
+ ),
+ renderCell: ({ row }) => (
+
+
+ {row.family}
+
+ {updateTestFamilyFlagEnabled && (
+
+ )}
+
+ ),
+ },
+ {
+ field: 'primaryContact',
+ headerName: 'Primary Contact',
+ flex: 0.7,
+ minWidth: 160,
+ filterable: false,
+ renderCell: ({ row }) => (
+
+ {row.primaryContact}
+
+ ),
+ },
+ {
+ field: 'roles',
+ headerName: 'Roles',
+ flex: 1.5,
+ minWidth: 360,
+ type: 'singleSelect',
+ valueGetter: (_value, row) => row.roleFilterValues.join(', '),
+ valueOptions: filterValueOptions(roleFilters),
+ renderCell: ({ row }) => ,
+ },
+ {
+ field: 'status',
+ headerName: 'Status',
+ flex: 0.6,
+ minWidth: 160,
+ type: 'singleSelect',
+ valueGetter: (_value, row) => row.statusFilterValues.join(', '),
+ valueOptions: filterValueOptions(statusFilters),
+ renderCell: ({ row }) => (
+
+ {displayValues(row.statusLabels)}
+
+ ),
+ },
+ {
+ field: 'missingRequirements',
+ headerName: 'Missing Requirements',
+ flex: 1,
+ minWidth: 280,
+ type: 'singleSelect',
+ valueGetter: (_value, row) => row.requirementFilterValues.join(', '),
+ valueOptions: requirementValueOptions(requirementFilterOptions),
+ renderCell: ({ row }) => ,
+ },
+ ];
+
+ const customFieldColumns: GridColDef[] =
+ customFields.map((customField) => ({
+ field: `customField:${customField.name}`,
+ headerName: customField.name,
+ flex: 0.7,
+ minWidth: 160,
+ filterable: false,
+ renderCell: ({ row }) =>
+ renderVolunteerCustomFieldValue(
+ row.customFieldValues[customField.name],
+ customField.validValues
+ ),
+ }));
+
+ return fixedColumns.concat(customFieldColumns);
+}
+
+export function VolunteersDataGridV2({
+ customFields,
+ filterModel,
+ loading = false,
+ onFilterModelChange,
+ onRowClick,
+ onRowSelectionModelChange,
+ requirementFilterOptions,
+ roleFilters,
+ rowSelectionModel,
+ rows,
+ statusFilters,
+ updateTestFamilyFlagEnabled,
+}: VolunteersDataGridV2Props) {
+ const theme = useTheme();
+ const columns = useMemo(
+ () =>
+ buildColumns(
+ customFields,
+ roleFilters,
+ statusFilters,
+ requirementFilterOptions,
+ updateTestFamilyFlagEnabled
+ ),
+ [
+ customFields,
+ requirementFilterOptions,
+ roleFilters,
+ statusFilters,
+ updateTestFamilyFlagEnabled,
+ ]
+ );
+
+ return (
+
+ 88}
+ getRowHeight={() => 'auto'}
+ columnHeaderHeight={42}
+ checkboxSelection
+ disableRowSelectionOnClick
+ disableRowSelectionExcludeModel
+ filterMode="server"
+ filterModel={filterModel}
+ onRowClick={({ row }) => onRowClick(row)}
+ onFilterModelChange={onFilterModelChange}
+ onRowSelectionModelChange={onRowSelectionModelChange}
+ pageSizeOptions={[25, 50, 100]}
+ rowSelectionModel={rowSelectionModel}
+ slotProps={{
+ baseCheckbox: {
+ slotProps: {
+ htmlInput: {
+ title: BULK_ACTION_SELECTION_TOOLTIP,
+ },
+ },
+ },
+ }}
+ initialState={{
+ pagination: {
+ paginationModel: { pageSize: 100 },
+ },
+ sorting: {
+ sortModel: [{ field: 'family', sort: 'asc' }],
+ },
+ }}
+ />
+
+ );
+}
diff --git a/src/caretogether-pwa/src/Volunteers/VolunteersScreenRoute.tsx b/src/caretogether-pwa/src/Volunteers/VolunteersScreenRoute.tsx
new file mode 100644
index 00000000..a16003ac
--- /dev/null
+++ b/src/caretogether-pwa/src/Volunteers/VolunteersScreenRoute.tsx
@@ -0,0 +1,36 @@
+import { useEffect, useState } from 'react';
+import { useFeatureFlagEnabled, usePostHog } from 'posthog-js/react';
+import { FAMILY_SCREEN_V2_EARLY_ACCESS_FEATURE_FLAG } from '../featureFlags';
+import { ProgressBackdrop } from '../Shell/ProgressBackdrop';
+import { Volunteers } from './Volunteers';
+import { VolunteersScreenV2 } from './VolunteersScreenV2';
+
+export function VolunteersScreenRoute() {
+ const posthog = usePostHog();
+ const earlyAccessEnabled = useFeatureFlagEnabled(
+ FAMILY_SCREEN_V2_EARLY_ACCESS_FEATURE_FLAG
+ );
+ const [featureFlagsLoaded, setFeatureFlagsLoaded] = useState(
+ () => posthog.featureFlags.hasLoadedFlags
+ );
+
+ useEffect(() => {
+ setFeatureFlagsLoaded(posthog.featureFlags.hasLoadedFlags);
+
+ return posthog.onFeatureFlags(() => {
+ setFeatureFlagsLoaded(true);
+ });
+ }, [posthog]);
+
+ if (!featureFlagsLoaded) {
+ return (
+
+ Loading...
+
+ );
+ }
+
+ const showVolunteersScreenV2 = earlyAccessEnabled === true;
+
+ return showVolunteersScreenV2 ? : ;
+}
diff --git a/src/caretogether-pwa/src/Volunteers/VolunteersScreenV2.tsx b/src/caretogether-pwa/src/Volunteers/VolunteersScreenV2.tsx
new file mode 100644
index 00000000..016b9dff
--- /dev/null
+++ b/src/caretogether-pwa/src/Volunteers/VolunteersScreenV2.tsx
@@ -0,0 +1,59 @@
+import { Box, Stack } from '@mui/material';
+import {
+ Navigate,
+ Route,
+ Routes,
+ useLocation,
+ useParams,
+} from 'react-router-dom';
+import { Permission } from '../GeneratedClient';
+import { useGlobalPermissions } from '../Model/SessionModel';
+import { useScreenTitle } from '../Shell/ShellScreenTitle';
+import { wideTablePageSx } from '../Utilities/wideTablePageSx';
+import { VolunteersBrowserV2 } from './VolunteersBrowserV2';
+
+function volunteersBasePathFromPathname(pathname: string) {
+ const volunteersPathMatch = pathname.match(/^(.*\/volunteers)(?:\/.*)?$/);
+
+ return volunteersPathMatch?.[1] ?? pathname;
+}
+
+function VolunteerFamilyRedirect() {
+ const { familyId } = useParams<{ familyId: string }>();
+
+ return ;
+}
+
+export function VolunteersScreenV2() {
+ useScreenTitle('Volunteers');
+ const location = useLocation();
+ const globalPermissions = useGlobalPermissions();
+ const volunteersBasePath = volunteersBasePathFromPathname(location.pathname);
+ const hasFeaturebaseChat = globalPermissions(Permission.AccessSupportScreen);
+
+ return (
+
+
+
+ } />
+ }
+ />
+ }
+ />
+ }
+ />
+
+
+
+ );
+}
diff --git a/src/caretogether-pwa/src/Volunteers/VolunteersToolbarV2.tsx b/src/caretogether-pwa/src/Volunteers/VolunteersToolbarV2.tsx
new file mode 100644
index 00000000..5967b1c6
--- /dev/null
+++ b/src/caretogether-pwa/src/Volunteers/VolunteersToolbarV2.tsx
@@ -0,0 +1,161 @@
+import {
+ Add as AddIcon,
+ Email as EmailIcon,
+ FilterList as FilterListIcon,
+ Search as SearchIcon,
+ Sms as SmsIcon,
+} from '@mui/icons-material';
+import {
+ Button,
+ IconButton,
+ InputAdornment,
+ Stack,
+ TextField,
+ Tooltip,
+ Typography,
+} from '@mui/material';
+
+type VolunteersToolbarV2Props = {
+ activeAssignmentFilterCount: number;
+ activeCustomFieldFilterCount: number;
+ arrangementTypeCount: number;
+ customFieldCount: number;
+ canCreateVolunteerFamily: boolean;
+ canUseBulkEmail: boolean;
+ canUseBulkSms: boolean;
+ searchValue: string;
+ selectedVolunteerCount: number;
+ smsMode: boolean;
+ onSearchChange: (value: string) => void;
+ onAssignmentFiltersClick: () => void;
+ onCopyEmailAddresses: () => void;
+ onCreateVolunteerFamily: () => void;
+ onCustomFieldFiltersClick: () => void;
+ onToggleBulkSms: () => void;
+};
+
+export function VolunteersToolbarV2({
+ activeAssignmentFilterCount,
+ activeCustomFieldFilterCount,
+ arrangementTypeCount,
+ canCreateVolunteerFamily,
+ canUseBulkEmail,
+ canUseBulkSms,
+ customFieldCount,
+ searchValue,
+ selectedVolunteerCount,
+ smsMode,
+ onSearchChange,
+ onAssignmentFiltersClick,
+ onCopyEmailAddresses,
+ onCreateVolunteerFamily,
+ onCustomFieldFiltersClick,
+ onToggleBulkSms,
+}: VolunteersToolbarV2Props) {
+ const bulkActionsDisabled = selectedVolunteerCount === 0;
+ const bulkActionsTooltip = bulkActionsDisabled
+ ? 'Select one or more families to enable bulk actions.'
+ : '';
+
+ return (
+
+ {canCreateVolunteerFamily && (
+ }
+ onClick={onCreateVolunteerFamily}
+ sx={{ flexShrink: 0, whiteSpace: 'nowrap' }}
+ >
+ Add Volunteer Family
+
+ )}
+ {arrangementTypeCount > 0 && (
+ }
+ sx={{ flexShrink: 0, justifyContent: 'flex-start', minHeight: 40 }}
+ variant="outlined"
+ >
+ Assignments ({activeAssignmentFilterCount}/{arrangementTypeCount})
+
+ )}
+ {customFieldCount > 0 && (
+ }
+ sx={{ flexShrink: 0, justifyContent: 'flex-start', minHeight: 40 }}
+ variant="outlined"
+ >
+ Custom fields ({activeCustomFieldFilterCount}/{customFieldCount})
+
+ )}
+ onSearchChange(event.target.value)}
+ sx={{ width: { xs: '100%', sm: 260 } }}
+ slotProps={{
+ input: {
+ startAdornment: (
+
+
+
+ ),
+ },
+ }}
+ />
+
+
+ {selectedVolunteerCount} selected
+
+ {canUseBulkEmail && (
+
+
+
+
+
+
+
+ )}
+ {canUseBulkSms && (
+
+
+
+
+
+
+
+ )}
+
+
+ );
+}
diff --git a/src/caretogether-pwa/src/Volunteers/useVolunteersBrowserViewModel.ts b/src/caretogether-pwa/src/Volunteers/useVolunteersBrowserViewModel.ts
new file mode 100644
index 00000000..6300eae4
--- /dev/null
+++ b/src/caretogether-pwa/src/Volunteers/useVolunteersBrowserViewModel.ts
@@ -0,0 +1,528 @@
+import { useCallback, useEffect, useMemo, useState } from 'react';
+import { useRecoilState, useRecoilValue } from 'recoil';
+import {
+ CombinedFamilyInfo,
+ CustomField,
+ RoleApprovalStatus,
+} from '../GeneratedClient';
+import { familyNameString } from '../Families/FamilyName';
+import { filterFamiliesByText } from '../Families/FamilyUtils';
+import { personNameString } from '../Families/PersonName';
+import { matchesCustomFieldFilters } from '../Generic/CustomFieldsFilter/matchesCustomFieldFilters';
+import {
+ CustomFieldFilterOption,
+ CustomFieldFilterSelectionsByField,
+ CustomFieldFilterValue,
+} from '../Generic/CustomFieldsFilter/types';
+import { useCustomFieldFilters } from '../Generic/CustomFieldsFilter/useCustomFieldFilters';
+import { useLoadable } from '../Hooks/useLoadable';
+import {
+ allApprovalAndOnboardingRequirementsData,
+ policyData,
+} from '../Model/ConfigurationModel';
+import { volunteerFamiliesData } from '../Model/VolunteersModel';
+import {
+ AssignmentFilterSelectionsByArrangementType,
+ AssignmentFilterValue,
+ matchesAssignmentFilters,
+} from './VolunteerApprovalTab/assignmentFilters';
+import { filterOption } from './VolunteerApprovalTab/filterOption';
+import { roleFiltersState } from './VolunteerApprovalTab/roleFiltersState';
+import { statusFiltersState } from './VolunteerApprovalTab/statusFiltersState';
+import {
+ buildVolunteerApprovalRolesPresentation,
+ VolunteerApprovalRolesPresentation,
+} from './VolunteerApprovalTab/volunteerApprovalRolePresentation';
+import { familyOrFamilyMembersMeetRoleStatusFilterCriteria } from './VolunteerApprovalTab/volunteerApprovalRoleStatusFilters';
+import {
+ buildVolunteerMissingRequirementGroups,
+ completeRequirementFilterValue,
+ familyHasMissingRequirements,
+ missingRequirementFilterValue,
+ RequirementFilterValue,
+ VolunteerMissingRequirementGroup,
+} from './VolunteerApprovalTab/volunteerMissingRequirementsPresentation';
+
+export type VolunteerBrowserRowV2 = {
+ customFieldValues: Record;
+ id: string;
+ family: string;
+ requirementFilterValues: string[];
+ missingRequirementGroups: VolunteerMissingRequirementGroup[];
+ primaryContact: string;
+ roleFilterValues: string[];
+ roles: VolunteerApprovalRolesPresentation;
+ sourceFamily: CombinedFamilyInfo;
+ statusFilterValues: string[];
+ statusLabels: string[];
+};
+
+type VolunteersBrowserViewModel = {
+ activeAssignmentFilterCount: number;
+ activeCustomFieldFilterCount: number;
+ arrangementTypes: string[];
+ assignmentFilters: AssignmentFilterSelectionsByArrangementType;
+ customFieldCount: number;
+ customFieldFilters: CustomFieldFilterSelectionsByField;
+ customFields: CustomField[];
+ empty: boolean;
+ getCustomFieldFilterOptionsForField: (
+ field: CustomField
+ ) => CustomFieldFilterOption[];
+ loading: boolean;
+ requirementFilter: RequirementFilterValue | undefined;
+ requirementFilterOptions: string[];
+ roleFilters: filterOption[];
+ rows: VolunteerBrowserRowV2[];
+ searchValue: string;
+ setAssignmentFilter: (
+ arrangementType: string,
+ selectedValues: AssignmentFilterValue[]
+ ) => void;
+ setCustomFieldFilter: (
+ fieldName: string,
+ selectedValues: CustomFieldFilterValue[]
+ ) => void;
+ setRequirementFilter: (value: RequirementFilterValue | undefined) => void;
+ setRoleFilterValues: (values: string[]) => void;
+ setSearchValue: (value: string) => void;
+ setStatusFilterValues: (values: string[]) => void;
+ statusFilters: filterOption[];
+ totalVolunteerFamilies: number;
+ visibleVolunteerFamilies: CombinedFamilyInfo[];
+};
+
+type CustomFieldValuesByFamily = Map>;
+
+function primaryContactName(family: CombinedFamilyInfo) {
+ const primaryContact = family.family?.adults?.find(
+ (adult) => adult.item1?.id === family.family?.primaryFamilyContactPersonId
+ )?.item1;
+
+ return primaryContact ? personNameString(primaryContact) : '';
+}
+
+function customFieldValuesForFamily(
+ family: CombinedFamilyInfo,
+ customFields: CustomField[],
+ customFieldValuesByFamily: CustomFieldValuesByFamily
+) {
+ const valuesByName = customFieldValuesByFamily.get(family) ?? {};
+
+ return Object.fromEntries(
+ customFields.map((field) => [field.name, valuesByName[field.name]])
+ );
+}
+
+function familyRoleNames(family: CombinedFamilyInfo) {
+ return Object.keys(family.volunteerFamilyInfo?.familyRoleApprovals ?? {});
+}
+
+function individualRoleNames(family: CombinedFamilyInfo) {
+ return Object.values(
+ family.volunteerFamilyInfo?.individualVolunteers ?? {}
+ ).flatMap((volunteer) =>
+ Object.keys(volunteer.approvalStatusByRole ?? {})
+ );
+}
+
+function roleFilterValuesForFamily(
+ family: CombinedFamilyInfo,
+ roleFilters: filterOption[]
+) {
+ const roleNames = new Set(familyRoleNames(family).concat(individualRoleNames(family)));
+
+ return roleFilters
+ .filter(
+ (roleFilter) =>
+ roleFilter.value !== undefined && roleNames.has(roleFilter.key)
+ )
+ .map((roleFilter) => roleFilter.value!);
+}
+
+function statusValue(status: RoleApprovalStatus | null | undefined) {
+ return status === null || status === undefined ? '0' : status.toString();
+}
+
+function statusFilterValuesForFamily(family: CombinedFamilyInfo) {
+ return Array.from(
+ new Set(
+ Object.values(family.volunteerFamilyInfo?.familyRoleApprovals ?? {})
+ .map((approval) => statusValue(approval.currentStatus))
+ .concat(
+ Object.values(
+ family.volunteerFamilyInfo?.individualVolunteers ?? {}
+ ).flatMap((volunteer) =>
+ Object.values(volunteer.approvalStatusByRole ?? {}).map(
+ (approval) => statusValue(approval.currentStatus)
+ )
+ )
+ )
+ )
+ );
+}
+
+function statusLabelsForFamily(
+ family: CombinedFamilyInfo,
+ statusFilters: filterOption[]
+) {
+ const statusLabelsByValue = new Map(
+ statusFilters
+ .filter((statusFilter) => statusFilter.value !== undefined)
+ .map((statusFilter) => [statusFilter.value!, statusFilter.key])
+ );
+
+ return statusFilterValuesForFamily(family)
+ .map((value) => statusLabelsByValue.get(value))
+ .filter((label): label is string => Boolean(label));
+}
+
+function requirementFilterValuesForGroups(
+ missingRequirementGroups: VolunteerMissingRequirementGroup[]
+) {
+ const missingRequirements = missingRequirementGroups.flatMap(
+ (group) => group.requirements
+ );
+
+ if (!missingRequirements.length) {
+ return [completeRequirementFilterValue];
+ }
+
+ return Array.from(new Set([missingRequirementFilterValue, ...missingRequirements]));
+}
+
+function toVolunteerBrowserRow(
+ family: CombinedFamilyInfo,
+ customFields: CustomField[],
+ roleFilters: filterOption[],
+ statusFilters: filterOption[],
+ customFieldValuesByFamily: CustomFieldValuesByFamily
+) {
+ const missingRequirementGroups = buildVolunteerMissingRequirementGroups(
+ family,
+ roleFilters
+ );
+
+ return {
+ customFieldValues: customFieldValuesForFamily(
+ family,
+ customFields,
+ customFieldValuesByFamily
+ ),
+ id: family.family!.id!,
+ family: familyNameString(family),
+ requirementFilterValues:
+ requirementFilterValuesForGroups(missingRequirementGroups),
+ missingRequirementGroups,
+ primaryContact: primaryContactName(family),
+ roleFilterValues: roleFilterValuesForFamily(family, roleFilters),
+ roles: buildVolunteerApprovalRolesPresentation(family, roleFilters),
+ sourceFamily: family,
+ statusFilterValues: statusFilterValuesForFamily(family),
+ statusLabels: statusLabelsForFamily(family, statusFilters),
+ };
+}
+
+function volunteerCustomFields(
+ policyCustomFields: CustomField[] | undefined,
+ volunteerCustomFields: CustomField[] | undefined
+) {
+ return (policyCustomFields ?? []).concat(volunteerCustomFields ?? []);
+}
+
+function buildCustomFieldValuesByFamily(volunteerFamilies: CombinedFamilyInfo[]) {
+ return new Map(
+ volunteerFamilies.map((family) => [
+ family,
+ customFieldValuesByName(family),
+ ])
+ );
+}
+
+function customFieldValuesByName(family: CombinedFamilyInfo) {
+ const valuesByName: Record = {};
+
+ family.volunteerFamilyInfo?.completedCustomFields?.forEach((customField) => {
+ valuesByName[customField.customFieldName] = customField.value;
+ });
+ family.family?.completedCustomFields?.forEach((customField) => {
+ if (customField.value === undefined || customField.value === null) {
+ return;
+ }
+
+ valuesByName[customField.customFieldName] = customField.value;
+ });
+
+ return valuesByName;
+}
+
+function customFieldValueFromLookup(
+ customFieldValuesByFamily: CustomFieldValuesByFamily,
+ family: CombinedFamilyInfo,
+ fieldName: string
+) {
+ return customFieldValuesByFamily.get(family)?.[fieldName];
+}
+
+function customFieldIsBlank(value: unknown) {
+ return value === undefined || value === null;
+}
+
+function sourceVolunteerFamilies(
+ volunteerFamilies: CombinedFamilyInfo[] | null | undefined
+) {
+ return volunteerFamilies ?? [];
+}
+
+function applySearchStage(
+ volunteerFamilies: CombinedFamilyInfo[],
+ searchValue: string
+) {
+ return filterFamiliesByText(volunteerFamilies, searchValue);
+}
+
+function applyFilterStage(
+ volunteerFamilies: CombinedFamilyInfo[],
+ roleFilters: filterOption[],
+ statusFilters: filterOption[],
+ assignmentFilters: AssignmentFilterSelectionsByArrangementType,
+ customFields: CustomField[],
+ customFieldFilters: CustomFieldFilterSelectionsByField,
+ customFieldValuesByFamily: CustomFieldValuesByFamily,
+ requirementFilter: RequirementFilterValue | undefined
+) {
+ return volunteerFamilies.filter(
+ (family) =>
+ familyOrFamilyMembersMeetRoleStatusFilterCriteria(
+ family,
+ roleFilters,
+ statusFilters
+ ) &&
+ familyHasMissingRequirements(
+ family,
+ roleFilters,
+ requirementFilter
+ ) &&
+ matchesAssignmentFilters(family, assignmentFilters) &&
+ matchesCustomFieldFilters({
+ item: family,
+ customFields,
+ selectedValuesByField: customFieldFilters,
+ isBlank: (item, fieldName) =>
+ customFieldIsBlank(
+ customFieldValueFromLookup(
+ customFieldValuesByFamily,
+ item,
+ fieldName
+ )
+ ),
+ getValue: (item, fieldName) =>
+ customFieldValueFromLookup(
+ customFieldValuesByFamily,
+ item,
+ fieldName
+ ),
+ })
+ );
+}
+
+function mapRows(
+ volunteerFamilies: CombinedFamilyInfo[],
+ customFields: CustomField[],
+ roleFilters: filterOption[],
+ statusFilters: filterOption[],
+ customFieldValuesByFamily: CustomFieldValuesByFamily
+) {
+ return volunteerFamilies.map((family) =>
+ toVolunteerBrowserRow(
+ family,
+ customFields,
+ roleFilters,
+ statusFilters,
+ customFieldValuesByFamily
+ )
+ );
+}
+
+function withSelectedFilterValues(
+ filters: filterOption[],
+ selectedValues: string[]
+) {
+ const selectedValueSet = new Set(selectedValues);
+
+ return filters.map((filter) => ({
+ ...filter,
+ selected:
+ filter.value !== undefined && selectedValueSet.has(filter.value),
+ }));
+}
+
+export function useVolunteersBrowserViewModel(): VolunteersBrowserViewModel {
+ const volunteerFamilies = useLoadable(volunteerFamiliesData);
+ const requirementNames = useLoadable(allApprovalAndOnboardingRequirementsData);
+ const policy = useRecoilValue(policyData);
+ const [roleFilters, setRoleFilters] = useRecoilState(roleFiltersState);
+ const [statusFilters, setStatusFilters] = useRecoilState(statusFiltersState);
+ const [assignmentFilters, setAssignmentFilters] =
+ useState({});
+ const [searchValue, setSearchValue] = useState('');
+ const [requirementFilter, setRequirementFilter] =
+ useState();
+ const loading = volunteerFamilies == null;
+ const sourceFamilies = sourceVolunteerFamilies(volunteerFamilies);
+ const arrangementTypes = useMemo(
+ () =>
+ Array.from(
+ new Set(
+ (policy.referralPolicy?.arrangementPolicies ?? [])
+ .map((arrangementPolicy) => arrangementPolicy.arrangementType)
+ .filter(
+ (arrangementType): arrangementType is string => !!arrangementType
+ )
+ )
+ ),
+ [policy.referralPolicy?.arrangementPolicies]
+ );
+ const activeAssignmentFilterCount = Object.values(assignmentFilters).filter(
+ (selectedValues) => selectedValues.length > 0
+ ).length;
+ const customFields = useMemo(
+ () =>
+ volunteerCustomFields(
+ policy.customFamilyFields,
+ policy.volunteerPolicy?.customFields
+ ),
+ [policy.customFamilyFields, policy.volunteerPolicy?.customFields]
+ );
+ const customFieldValuesByFamily = useMemo(
+ () => buildCustomFieldValuesByFamily(sourceFamilies),
+ [sourceFamilies]
+ );
+ const getCustomFieldValue = useCallback(
+ (family: CombinedFamilyInfo, fieldName: string) =>
+ customFieldValueFromLookup(customFieldValuesByFamily, family, fieldName),
+ [customFieldValuesByFamily]
+ );
+ const isCustomFieldBlank = useCallback(
+ (family: CombinedFamilyInfo, fieldName: string) =>
+ customFieldIsBlank(getCustomFieldValue(family, fieldName)),
+ [getCustomFieldValue]
+ );
+ const {
+ selectedValuesByField: customFieldFilters,
+ setSelectedValuesForField: setCustomFieldFilter,
+ getOptionsForField: getCustomFieldFilterOptionsForField,
+ } = useCustomFieldFilters({
+ customFields,
+ items: sourceFamilies,
+ isBlank: isCustomFieldBlank,
+ getValue: getCustomFieldValue,
+ });
+ const customFieldCount = customFields.length;
+ const activeCustomFieldFilterCount = Object.values(
+ customFieldFilters
+ ).filter((selectedValues) => selectedValues.length > 0).length;
+
+ useEffect(() => {
+ setAssignmentFilters((currentFilters) => {
+ const validFilters = Object.fromEntries(
+ Object.entries(currentFilters).filter(([arrangementType]) =>
+ arrangementTypes.includes(arrangementType)
+ )
+ );
+
+ return Object.keys(validFilters).length ===
+ Object.keys(currentFilters).length
+ ? currentFilters
+ : validFilters;
+ });
+ }, [arrangementTypes]);
+
+ function setAssignmentFilter(
+ arrangementType: string,
+ selectedValues: AssignmentFilterValue[]
+ ) {
+ setAssignmentFilters((previous) => ({
+ ...previous,
+ [arrangementType]: selectedValues,
+ }));
+ }
+
+ function setRoleFilterValues(values: string[]) {
+ setRoleFilters(withSelectedFilterValues(roleFilters, values));
+ }
+
+ function setStatusFilterValues(values: string[]) {
+ setStatusFilters(withSelectedFilterValues(statusFilters, values));
+ }
+
+ const visibleVolunteerFamilies = useMemo(() => {
+ const searchedFamilies = applySearchStage(sourceFamilies, searchValue);
+ const filteredFamilies = applyFilterStage(
+ searchedFamilies,
+ roleFilters,
+ statusFilters,
+ assignmentFilters,
+ customFields,
+ customFieldFilters,
+ customFieldValuesByFamily,
+ requirementFilter
+ );
+
+ return filteredFamilies;
+ }, [
+ assignmentFilters,
+ customFieldFilters,
+ customFieldValuesByFamily,
+ customFields,
+ requirementFilter,
+ roleFilters,
+ searchValue,
+ sourceFamilies,
+ statusFilters,
+ ]);
+ const rows = useMemo(
+ () =>
+ mapRows(
+ visibleVolunteerFamilies,
+ customFields,
+ roleFilters,
+ statusFilters,
+ customFieldValuesByFamily
+ ),
+ [
+ customFieldValuesByFamily,
+ customFields,
+ roleFilters,
+ statusFilters,
+ visibleVolunteerFamilies,
+ ]
+ );
+
+ return {
+ activeAssignmentFilterCount,
+ activeCustomFieldFilterCount,
+ arrangementTypes,
+ assignmentFilters,
+ customFieldCount,
+ customFieldFilters,
+ customFields,
+ empty: !loading && rows.length === 0,
+ getCustomFieldFilterOptionsForField,
+ loading,
+ requirementFilter,
+ requirementFilterOptions: requirementNames ?? [],
+ roleFilters,
+ rows,
+ searchValue,
+ setAssignmentFilter,
+ setCustomFieldFilter,
+ setRequirementFilter,
+ setRoleFilterValues,
+ setSearchValue,
+ setStatusFilterValues,
+ statusFilters,
+ totalVolunteerFamilies: sourceFamilies.length,
+ visibleVolunteerFamilies,
+ };
+}
diff --git a/src/caretogether-pwa/src/Volunteers/volunteersGridFilterAdapter.ts b/src/caretogether-pwa/src/Volunteers/volunteersGridFilterAdapter.ts
new file mode 100644
index 00000000..08c2630f
--- /dev/null
+++ b/src/caretogether-pwa/src/Volunteers/volunteersGridFilterAdapter.ts
@@ -0,0 +1,109 @@
+import {
+ GridLogicOperator,
+ type GridFilterModel,
+} from '@mui/x-data-grid';
+
+export const VOLUNTEER_ROLES_FILTER_FIELD = 'roles';
+export const VOLUNTEER_STATUS_FILTER_FIELD = 'status';
+export const VOLUNTEER_REQUIREMENTS_FILTER_FIELD = 'missingRequirements';
+
+export type VolunteersGridFilterLogicOperator = 'and' | 'or';
+
+export type VolunteersGridFilters = {
+ logicOperator: VolunteersGridFilterLogicOperator;
+ requirementFilter: string | undefined;
+ roleFilters: string[];
+ statusFilters: string[];
+};
+
+const MULTI_VALUE_FILTER_OPERATOR = 'isAnyOf';
+const SINGLE_VALUE_FILTER_OPERATOR = 'is';
+
+function stringValuesFromFilterValue(value: unknown): string[] {
+ if (Array.isArray(value)) {
+ return value.map(String).filter(Boolean);
+ }
+
+ return typeof value === 'string' && value ? [value] : [];
+}
+
+function stringValueFromFilterValue(value: unknown): string | undefined {
+ return typeof value === 'string' && value ? value : undefined;
+}
+
+function filterValueForField(
+ filterModel: GridFilterModel,
+ field: string
+): unknown {
+ return filterModel.items.find((item) => item.field === field)?.value;
+}
+
+function logicOperatorFromGridFilterModel(
+ filterModel: GridFilterModel
+): VolunteersGridFilterLogicOperator {
+ return filterModel.logicOperator === GridLogicOperator.Or ? 'or' : 'and';
+}
+
+function gridLogicOperatorFromVolunteerFilters(
+ logicOperator: VolunteersGridFilterLogicOperator
+) {
+ return logicOperator === 'or' ? GridLogicOperator.Or : GridLogicOperator.And;
+}
+
+export function volunteerFiltersFromGridFilterModel(
+ filterModel: GridFilterModel
+): VolunteersGridFilters {
+ return {
+ logicOperator: logicOperatorFromGridFilterModel(filterModel),
+ requirementFilter: stringValueFromFilterValue(
+ filterValueForField(filterModel, VOLUNTEER_REQUIREMENTS_FILTER_FIELD)
+ ),
+ roleFilters: stringValuesFromFilterValue(
+ filterValueForField(filterModel, VOLUNTEER_ROLES_FILTER_FIELD)
+ ),
+ statusFilters: stringValuesFromFilterValue(
+ filterValueForField(filterModel, VOLUNTEER_STATUS_FILTER_FIELD)
+ ),
+ };
+}
+
+export function gridFilterModelFromVolunteerFilters({
+ logicOperator,
+ requirementFilter,
+ roleFilters,
+ statusFilters,
+}: VolunteersGridFilters): GridFilterModel {
+ const items: GridFilterModel['items'] = [];
+
+ if (roleFilters.length > 0) {
+ items.push({
+ field: VOLUNTEER_ROLES_FILTER_FIELD,
+ id: VOLUNTEER_ROLES_FILTER_FIELD,
+ operator: MULTI_VALUE_FILTER_OPERATOR,
+ value: roleFilters,
+ });
+ }
+
+ if (statusFilters.length > 0) {
+ items.push({
+ field: VOLUNTEER_STATUS_FILTER_FIELD,
+ id: VOLUNTEER_STATUS_FILTER_FIELD,
+ operator: MULTI_VALUE_FILTER_OPERATOR,
+ value: statusFilters,
+ });
+ }
+
+ if (requirementFilter) {
+ items.push({
+ field: VOLUNTEER_REQUIREMENTS_FILTER_FIELD,
+ id: VOLUNTEER_REQUIREMENTS_FILTER_FIELD,
+ operator: SINGLE_VALUE_FILTER_OPERATOR,
+ value: requirementFilter,
+ });
+ }
+
+ return {
+ items,
+ logicOperator: gridLogicOperatorFromVolunteerFilters(logicOperator),
+ };
+}