Permissions Matrix Report

CalMHSA has worked with AI to create a workflow to transform the existing “Staff Roles and Permissions Report 2” into a more visually helpful matrix. Previously, CalMHSA had done this work manually and has shared these point-in-time reports with counties. However, due to the labor intensity of creating this matrix, this was not something that CalMHSA could produce on a regular basis.

This process uses a macro-enabled Excel workbook, which you can download here.

How to Use the Macro-Enabled Workbook

  1. Download the Data from the EHR
    1. Log in to SmartCare
    2. Run the report “Staff User Roles and Permissions 2” with the following parameters:
      • Role Id: <All Roles>
      • Permission Template Type: <All Template Types>
      • Permission Status: Granted/Denied
      • Include All Permissions Outside of Role: N
      • Staff: <All>
    3. Export the report to CSV. Note where this file is downloaded to and what it’s named. Your CSV should have these columns:
      • Column A: StaffName1 (will be ignored)
      • Column B: RoleName
      • Column C: PermissionTemplateTypeName
      • Column D: PermissionItemName
      • Column E: Denied (will be ignored)
      • Column F: Granted
  2. Open the Macro-Enabled Workbook
    1. Since this workbook contains macros, Excel may show security warnings when opening. This is normal – just click “Enable Content” to use the macro functionality.
  3. Import your CSV file
    1. Go to Data and select From Text/CSV
    2. Select your EHR permissions CSV file
    3. Click Transform Data (not just “Load”)
    4. In Power Query Editor, in the Transform ribbon, select Use First Row as Headers
    5. Navigate back to the Home ribbon and click Close & Load
    6. Confirm the data has pulled in appropriately.
  4. Run the macro
    1. Make sure the sheet with your imported data is the active sheet
    2. Make sure macros are enabled
    3. Press Alt + F8 to open the Macro dialog
    4. Select “TransformPermissionsToMatrix”
    5. Click Run
  5. Review the results
    1. The macro will create two new sheets:
      • CleanData: Contains the processed data (removes column A and column E, sorts the data alphabetically by column B, then C, then D)
      • Matrix: Contains your final permissions matrix

Tips for Success

  1. Always work with a copy of your original CSV data
  2. The macro may take several minutes to process large datasets (100,000+ rows)
  3. Watch the status bar for progress updates during processing
  4. Review the final matrix to ensure accuracy before using it for analysis

Troubleshooting

  1. If macros don’t run: Check that macros are enabled in Trust Center settings
  2. If you get security warnings: Click “Enable Content” when prompted
  3. If the macro stops with an error: Make sure your data has the expected column structure
  4. If results look incorrect: Verify that your CSV was imported with proper headers

Sub TransformPermissionsToMatrix()
‘ This macro transforms EHR permissions data into a matrix format
‘ using a clean copy approach instead of modifying the original data

Application.ScreenUpdating = False
Application.Calculation = xlCalculationManual
Application.DisplayStatusBar = True
Application.StatusBar = “Initializing…”

‘ Get the source data sheet
Dim dataSheet As Worksheet
Set dataSheet = ActiveSheet

‘ Create a new sheet for the clean data
Dim cleanSheet As Worksheet, matrixSheet As Worksheet

‘ Check if the sheets already exist, if so, delete them
On Error Resume Next
Set cleanSheet = Worksheets(“CleanData”)
If Not cleanSheet Is Nothing Then
cleanSheet.Delete
End If

Set matrixSheet = Worksheets(“Matrix”)
If Not matrixSheet Is Nothing Then
matrixSheet.Delete
End If
On Error GoTo 0

‘ Add new sheets
Set cleanSheet = Worksheets.Add(After:=dataSheet)
cleanSheet.Name = “CleanData”

Set matrixSheet = Worksheets.Add(After:=cleanSheet)
matrixSheet.Name = “Matrix”

‘ Define the source column positions in the original data
Dim srcRoleCol As Long, srcTemplateCol As Long, srcPermItemCol As Long, srcGrantedCol As Long
srcRoleCol = 2 ‘ RoleName (Column B)
srcTemplateCol = 3 ‘ PermissionTemplateTypeName (Column C)
srcPermItemCol = 4 ‘ PermissionItemName (Column D)
srcGrantedCol = 6 ‘ Granted (Column F)

‘ Define target column positions in the clean sheet
Dim tgtRoleCol As Long, tgtTemplateCol As Long, tgtPermItemCol As Long, tgtGrantedCol As Long
tgtRoleCol = 1 ‘ RoleName (Column A in clean sheet)
tgtTemplateCol = 2 ‘ PermissionTemplateTypeName (Column B in clean sheet)
tgtPermItemCol = 3 ‘ PermissionItemName (Column C in clean sheet)
tgtGrantedCol = 4 ‘ Granted (Column D in clean sheet)

‘ Get the last row with data in the source sheet
Dim lastRow As Long
lastRow = dataSheet.UsedRange.Rows.Count

Application.StatusBar = “Copying needed columns to clean sheet…”

‘ Set up headers in the clean sheet
cleanSheet.Cells(1, tgtRoleCol).Value = “RoleName”
cleanSheet.Cells(1, tgtTemplateCol).Value = “PermissionTemplateTypeName”
cleanSheet.Cells(1, tgtPermItemCol).Value = “PermissionItemName”
cleanSheet.Cells(1, tgtGrantedCol).Value = “Granted”

‘ Copy data to clean sheet
Dim r As Long

For r = 2 To lastRow
‘ Update progress every 1000 rows
If r Mod 1000 = 0 Then
Application.StatusBar = “Copying row ” & r & ” of ” & lastRow & ” (” & Format(r / lastRow, “0%”) & “)”
DoEvents
End If

‘ Copy each cell to the clean sheet
cleanSheet.Cells(r, tgtRoleCol).Value = dataSheet.Cells(r, srcRoleCol).Value
cleanSheet.Cells(r, tgtTemplateCol).Value = dataSheet.Cells(r, srcTemplateCol).Value
cleanSheet.Cells(r, tgtPermItemCol).Value = dataSheet.Cells(r, srcPermItemCol).Value
cleanSheet.Cells(r, tgtGrantedCol).Value = dataSheet.Cells(r, srcGrantedCol).Value
Next r

Application.StatusBar = “Sorting data…”

‘ Sort the clean data by RoleName, PermissionTemplateTypeName, PermissionItemName
With cleanSheet.Sort
.SortFields.Clear
.SortFields.Add key:=cleanSheet.Range(cleanSheet.Cells(1, tgtRoleCol), cleanSheet.Cells(lastRow, tgtRoleCol)), _
SortOn:=xlSortOnValues, Order:=xlAscending
.SortFields.Add key:=cleanSheet.Range(cleanSheet.Cells(1, tgtTemplateCol), cleanSheet.Cells(lastRow, tgtTemplateCol)), _
SortOn:=xlSortOnValues, Order:=xlAscending
.SortFields.Add key:=cleanSheet.Range(cleanSheet.Cells(1, tgtPermItemCol), cleanSheet.Cells(lastRow, tgtPermItemCol)), _
SortOn:=xlSortOnValues, Order:=xlAscending

.SetRange cleanSheet.Range(cleanSheet.Cells(1, 1), cleanSheet.Cells(lastRow, tgtGrantedCol))
.Header = xlYes
.Apply
End With

Application.StatusBar = “Collecting unique values…”

‘ Get unique roles, templates, and permission items
Dim roleDict As Object, templateDict As Object, permItemDict As Object
Set roleDict = CreateObject(“Scripting.Dictionary”)
Set templateDict = CreateObject(“Scripting.Dictionary”)
Set permItemDict = CreateObject(“Scripting.Dictionary”)

‘ Validate clean data
Dim validCount As Long
validCount = 0

For r = 2 To lastRow
Dim role As String, template As String, permItem As String, granted As String

role = cleanSheet.Cells(r, tgtRoleCol).Value
template = cleanSheet.Cells(r, tgtTemplateCol).Value
permItem = cleanSheet.Cells(r, tgtPermItemCol).Value
granted = cleanSheet.Cells(r, tgtGrantedCol).Value

‘ Skip if any critical fields are empty
If Len(role) > 0 And Len(template) > 0 And Len(permItem) > 0 Then
validCount = validCount + 1

‘ Add unique values to dictionaries
If Not roleDict.Exists(role) Then
roleDict.Add role, Nothing
End If

Dim templatePermKey As String
templatePermKey = template & “|” & permItem

If Not templateDict.Exists(template) Then
templateDict.Add template, Nothing
End If

If Not permItemDict.Exists(templatePermKey) Then
permItemDict.Add templatePermKey, Nothing
End If
End If
Next r

‘ Check if we found any valid data
If validCount = 0 Or roleDict.Count = 0 Or permItemDict.Count = 0 Then
MsgBox “No valid data found for processing. Please check that the source data has RoleName, PermissionTemplateTypeName, PermissionItemName, and Granted columns.”, vbExclamation

‘ Show debugging info
Dim debugMsg As String
debugMsg = “Debugging information:” & vbCrLf & vbCrLf
debugMsg = debugMsg & “Total rows in source: ” & lastRow & vbCrLf
debugMsg = debugMsg & “Valid rows found: ” & validCount & vbCrLf
debugMsg = debugMsg & “Unique roles found: ” & roleDict.Count & vbCrLf
debugMsg = debugMsg & “Unique templates found: ” & templateDict.Count & vbCrLf
debugMsg = debugMsg & “Unique permission combinations found: ” & permItemDict.Count & vbCrLf & vbCrLf

‘ Show headers from source data
debugMsg = debugMsg & “Source data headers:” & vbCrLf
For c = 1 To 6
debugMsg = debugMsg & ” Column ” & c & “: [” & dataSheet.Cells(1, c).Value & “]” & vbCrLf
Next c
debugMsg = debugMsg & vbCrLf

‘ Show sample of first few data rows from source
debugMsg = debugMsg & “Sample data from source sheet:” & vbCrLf
For r = 2 To Application.Min(4, lastRow)
debugMsg = debugMsg & “Row ” & r & “:” & vbCrLf
debugMsg = debugMsg & ” Col B (RoleName): [” & dataSheet.Cells(r, srcRoleCol).Value & “]” & vbCrLf
debugMsg = debugMsg & ” Col C (Template): [” & dataSheet.Cells(r, srcTemplateCol).Value & “]” & vbCrLf
debugMsg = debugMsg & ” Col D (PermItem): [” & dataSheet.Cells(r, srcPermItemCol).Value & “]” & vbCrLf
debugMsg = debugMsg & ” Col F (Granted): [” & dataSheet.Cells(r, srcGrantedCol).Value & “]” & vbCrLf & vbCrLf
Next r

‘ Show sample of first few data rows from clean sheet
debugMsg = debugMsg & “Sample data from clean sheet:” & vbCrLf
For r = 2 To Application.Min(4, lastRow)
debugMsg = debugMsg & “Row ” & r & “:” & vbCrLf
debugMsg = debugMsg & ” RoleName: [” & cleanSheet.Cells(r, tgtRoleCol).Value & “]” & vbCrLf
debugMsg = debugMsg & ” Template: [” & cleanSheet.Cells(r, tgtTemplateCol).Value & “]” & vbCrLf
debugMsg = debugMsg & ” PermItem: [” & cleanSheet.Cells(r, tgtPermItemCol).Value & “]” & vbCrLf
debugMsg = debugMsg & ” Granted: [” & cleanSheet.Cells(r, tgtGrantedCol).Value & “]” & vbCrLf & vbCrLf
Next r

MsgBox debugMsg, vbInformation, “Debug Information”

Application.StatusBar = False
Application.ScreenUpdating = True
Application.Calculation = xlCalculationAutomatic
Exit Sub
End If

Application.StatusBar = “Building matrix…”

‘ Set up matrix sheet headers
matrixSheet.Cells(1, 1).Value = “PermissionTemplateTypeName”
matrixSheet.Cells(1, 2).Value = “PermissionItemName”

‘ Add role headers
Dim roleArray() As String
ReDim roleArray(0 To roleDict.Count – 1)

Dim roleKeys As Variant, i As Long
roleKeys = roleDict.Keys
For i = 0 To UBound(roleKeys)
roleArray(i) = roleKeys(i)
Next i

‘ Sort roles alphabetically
Call BubbleSort(roleArray)

‘ Set role names as column headers
For i = 0 To UBound(roleArray)
matrixSheet.Cells(1, i + 3).Value = roleArray(i)
Next i

‘ Create a dictionary to store unique template+permItem combinations
Dim uniquePermissions As Object, permIndex As Long
Set uniquePermissions = CreateObject(“Scripting.Dictionary”)
permIndex = 2 ‘ Start at row 2 in matrix

‘ Collect all unique template+permItem combinations and their values for each role
Dim permStorage As Object
Set permStorage = CreateObject(“Scripting.Dictionary”)

Application.StatusBar = “Processing permissions data…”

‘ First pass to collect all unique permissions and role values
For r = 2 To lastRow
Dim currRole As String, currTemplate As String, currPermItem As String, currGranted As String

currRole = cleanSheet.Cells(r, tgtRoleCol).Value
currTemplate = cleanSheet.Cells(r, tgtTemplateCol).Value
currPermItem = cleanSheet.Cells(r, tgtPermItemCol).Value
currGranted = cleanSheet.Cells(r, tgtGrantedCol).Value

‘ Skip empty rows
If Len(currRole) > 0 And Len(currTemplate) > 0 And Len(currPermItem) > 0 Then
Dim permKey As String, storageKey As String
permKey = currTemplate & “|” & currPermItem
storageKey = permKey & “|” & currRole

‘ Add to unique permissions dictionary if not exists
If Not uniquePermissions.Exists(permKey) Then
uniquePermissions.Add permKey, permIndex
permIndex = permIndex + 1
End If

‘ Store the granted value
permStorage(storageKey) = currGranted
End If
Next r

‘ Create sorted array of unique permissions for consistent ordering
Dim permKeys As Variant
permKeys = uniquePermissions.Keys

Dim permArray() As String
ReDim permArray(0 To UBound(permKeys))

For i = 0 To UBound(permKeys)
permArray(i) = permKeys(i)
Next i

‘ Sort permissions
Call BubbleSort(permArray)

Application.StatusBar = “Building matrix rows…”

‘ Output template and permission item values
For i = 0 To UBound(permArray)
Dim parts As Variant
parts = Split(permArray(i), “|”)

‘ Add template and permission item to matrix
matrixSheet.Cells(i + 2, 1).Value = parts(0) ‘ Template
matrixSheet.Cells(i + 2, 2).Value = parts(1) ‘ Permission Item
Next i

Application.StatusBar = “Filling in permission values…”

‘ Now fill in the granted values for each role
For i = 0 To UBound(permArray)
Dim permissionKey As String
permissionKey = permArray(i)

For j = 0 To UBound(roleArray)
Dim roleName As String
roleName = roleArray(j)

‘ Create the storage key
storageKey = permissionKey & “|” & roleName

‘ Check if this combination exists and add the value
If permStorage.Exists(storageKey) Then
matrixSheet.Cells(i + 2, j + 3).Value = permStorage(storageKey)
End If
Next j

‘ Update progress for large datasets
If i Mod 100 = 0 Then
Application.StatusBar = “Filling in values: ” & Format(i / UBound(permArray), “0%”) & ” complete…”
DoEvents
End If
Next i

Application.StatusBar = “Applying formatting…”

‘ Format the matrix
With matrixSheet.Range(matrixSheet.Cells(1, 1), matrixSheet.Cells(1, UBound(roleArray) + 3))
.Interior.Color = RGB(200, 200, 200)
.Font.Bold = True
End With

‘ Get the used range of the matrix
Dim matrixRange As Range
Set matrixRange = matrixSheet.Range(matrixSheet.Cells(1, 1), _
matrixSheet.Cells(UBound(permArray) + 2, UBound(roleArray) + 3))

‘ Apply conditional formatting for Y/N values
Dim dataRange As Range
Set dataRange = matrixSheet.Range(matrixSheet.Cells(2, 3), _
matrixSheet.Cells(UBound(permArray) + 2, UBound(roleArray) + 3))

With dataRange.FormatConditions.Add(Type:=xlCellValue, Operator:=xlEqual, Formula1:=”Y”)
.Interior.Color = RGB(200, 255, 200) ‘ Light green
End With

With dataRange.FormatConditions.Add(Type:=xlCellValue, Operator:=xlEqual, Formula1:=”N”)
.Interior.Color = RGB(255, 200, 200) ‘ Light red
End With

‘ Add filter to header row
matrixSheet.Range(matrixSheet.Rows(1).Address).AutoFilter

‘ Apply borders
matrixRange.Borders.LineStyle = xlContinuous

‘ Autofit columns
matrixSheet.Columns.AutoFit

‘ Freeze panes
matrixSheet.Range(“C2”).Select
ActiveWindow.FreezePanes = True

‘ Make the matrix sheet active
matrixSheet.Activate

‘ Reset application settings
Application.StatusBar = False
Application.ScreenUpdating = True
Application.Calculation = xlCalculationAutomatic

‘ Success message
MsgBox “Permissions matrix created successfully.” & vbCrLf & vbCrLf & _
“• ” & roleDict.Count & ” unique roles” & vbCrLf & _
“• ” & uniquePermissions.Count & ” unique permissions” & vbCrLf & _
“• ” & permStorage.Count & ” data points processed”, _
vbInformation, “Matrix Creation Complete”
End Sub

‘ Helper function to sort an array of strings
Sub BubbleSort(arr() As String)
Dim i As Long, j As Long
Dim temp As String

For i = LBound(arr) To UBound(arr) – 1
For j = i + 1 To UBound(arr)
If arr(i) > arr(j) Then
temp = arr(i)
arr(i) = arr(j)
arr(j) = temp
End If
Next j
Next i
End Sub