This document outlines the creation of a custom Visualforce page for comprehensive Skills Management, deployed as a custom tab within Salesforce Field Service Lightning.
Objective
Create a centralized skills management interface that allows administrators to view, assign, and manage skills across all Service Resources without using the standard Salesforce setup menus.
Architecture Overview
-
Apex Controller: Handles all SOQL queries and DML operations for Skills and Service Resources.
-
Visualforce Page: Provides the UI for searching resources, viewing assigned skills, and managing skill assignments.
-
Custom Tab: Exposes the Visualforce page in the Salesforce app navigation.
Implementation Code
1. Apex Controller (SkillsManagerController.cls)
public with sharing class SkillsManagerController {
// Public properties bound to Visualforce page
public String searchTerm { get; set; }
public List<ServiceResource> searchResults { get; set; }
public ServiceResource selectedResource { get; set; }
public List<SkillRequirement> assignedSkills { get; set; }
public List<Skill> availableSkills { get; set; }
public String selectedSkillId { get; set; }
public Integer skillLevel { get; set; }
// Constructor
public SkillsManagerController() {
searchResults = new List<ServiceResource>();
assignedSkills = new List<SkillRequirement>();
loadAvailableSkills();
}
// Search for Service Resources by name
public void searchResources() {
searchResults.clear();
if (String.isNotBlank(searchTerm)) {
String searchQuery = '%' + searchTerm + '%';
searchResults = [
SELECT Id, Name, ResourceType, IsActive
FROM ServiceResource
WHERE Name LIKE :searchQuery
ORDER BY Name
LIMIT 50
];
}
}
// Load skills assigned to selected resource
public void selectResource() {
String resourceId = ApexPages.currentPage().getParameters().get('resourceId');
if (String.isNotBlank(resourceId)) {
selectedResource = [
SELECT Id, Name, ResourceType, IsActive
FROM ServiceResource
WHERE Id = :resourceId
LIMIT 1
];
assignedSkills = [
SELECT Id, SkillId, Skill.MasterLabel, SkillLevel
FROM SkillRequirement
WHERE RelatedRecordId = :resourceId
ORDER BY Skill.MasterLabel
];
}
}
// Load all available skills from the system
private void loadAvailableSkills() {
availableSkills = [
SELECT Id, MasterLabel, DeveloperName
FROM Skill
ORDER BY MasterLabel
LIMIT 1000
];
}
// Assign new skill to selected resource
public void assignSkill() {
if (selectedResource != null && String.isNotBlank(selectedSkillId) && skillLevel > 0) {
try {
SkillRequirement newSkill = new SkillRequirement(
RelatedRecordId = selectedResource.Id,
SkillId = selectedSkillId,
SkillLevel = skillLevel
);
insert newSkill;
// Refresh assigned skills list
selectResource();
// Reset input fields
selectedSkillId = null;
skillLevel = null;
} catch (Exception e) {
ApexPages.addMessage(new ApexPages.Message(
ApexPages.Severity.ERROR,
'Error assigning skill: ' + e.getMessage()
));
}
}
}
// Remove skill from resource
public void removeSkill() {
String skillReqId = ApexPages.currentPage().getParameters().get('skillReqId');
if (String.isNotBlank(skillReqId)) {
try {
SkillRequirement skillToDelete = [
SELECT Id FROM SkillRequirement
WHERE Id = :skillReqId
LIMIT 1
];
delete skillToDelete;
// Refresh assigned skills list
selectResource();
} catch (Exception e) {
ApexPages.addMessage(new ApexPages.Message(
ApexPages.Severity.ERROR,
'Error removing skill: ' + e.getMessage()
));
}
}
}
}
2. Visualforce Page (SkillsManager.vfp)
<apex:page controller="SkillsManagerController"
sidebar="false"
showHeader="true"
tabStyle="ServiceResource"
standardStylesheets="false"
applyHtmlTag="false"
applyBodyTag="false">
<html xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>FSL Skills Manager</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- Import Salesforce Lightning Design System -->
<apex:slds />
<style>
.container {
padding: 1rem;
background: white;
min-height: 80vh;
}
.search-section, .resource-details {
margin-bottom: 2rem;
padding: 1rem;
border: 1px solid #dddbda;
border-radius: 0.25rem;
}
.skill-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem;
border-bottom: 1px solid #f3f2f2;
}
.skill-actions {
display: flex;
gap: 0.5rem;
}
</style>
</head>
<body>
<div class="container slds-scope">
<!-- Page Header -->
<div class="slds-page-header">
<div class="slds-page-header__row">
<div class="slds-page-header__col-title">
<div class="slds-media">
<div class="slds-media__figure">
<lightning-icon icon-name="standard:service_resource" size="small"></lightning-icon>
</div>
<div class="slds-media__body">
<h1 class="slds-page-header__title">FSL Skills Management</h1>
</div>
</div>
</div>
</div>
</div>
<!-- Search Section -->
<div class="search-section">
<h2 class="slds-text-heading_medium slds-m-bottom_medium">Search Service Resources</h2>
<div class="slds-grid slds-gutters">
<div class="slds-col slds-size_2-of-3">
<div class="slds-form-element">
<label class="slds-form-element__label" for="searchInput">Resource Name</label>
<div class="slds-form-element__control">
<input type="text"
id="searchInput"
class="slds-input"
placeholder="Enter resource name..."
value="{!searchTerm}"
onchange="updateSearchTerm(this.value)"/>
</div>
</div>
</div>
<div class="slds-col slds-size_1-of-3">
<div class="slds-form-element">
<label class="slds-form-element__label" style="visibility: hidden;">Search</label>
<div class="slds-form-element__control">
<button class="slds-button slds-button_brand" onclick="searchResources()">
Search Resources
</button>
</div>
</div>
</div>
</div>
<!-- Search Results -->
<apex:outputPanel rendered="{!NOT(EMPTY(searchResults))}">
<div class="slds-m-top_medium">
<h3 class="slds-text-heading_small">Search Results</h3>
<ul class="slds-list slds-list_dotted">
<apex:repeat value="{!searchResults}" var="resource">
<li class="slds-list__item">
<a href="#" onclick="selectResource('{!resource.Id}')" class="slds-text-link">
{!resource.Name} ({!resource.ResourceType})
</a>
</li>
</apex:repeat>
</ul>
</div>
</apex:outputPanel>
</div>
<!-- Resource Details Section -->
<apex:outputPanel rendered="{!selectedResource != null}">
<div class="resource-details">
<h2 class="slds-text-heading_medium">Skills for: {!selectedResource.Name}</h2>
<!-- Current Skills -->
<div class="slds-m-top_medium">
<h3 class="slds-text-heading_small">Assigned Skills</h3>
<apex:outputPanel rendered="{!EMPTY(assignedSkills)}">
<div class="slds-text-color_weak">No skills assigned to this resource.</div>
</apex:outputPanel>
<apex:outputPanel rendered="{!NOT(EMPTY(assignedSkills))}">
<div class="slds-list_vertical">
<apex:repeat value="{!assignedSkills}" var="skill">
<div class="skill-item">
<span>{!skill.Skill.MasterLabel} (Level {!skill.SkillLevel})</span>
<div class="skill-actions">
<button class="slds-button slds-button_destructive slds-button_stretch"
onclick="removeSkill('{!skill.Id}')">
Remove
</button>
</div>
</div>
</apex:repeat>
</div>
</apex:outputPanel>
</div>
<!-- Add New Skill Form -->
<div class="slds-m-top_large">
<h3 class="slds-text-heading_small">Add New Skill</h3>
<div class="slds-grid slds-gutters">
<div class="slds-col slds-size_1-of-2">
<div class="slds-form-element">
<label class="slds-form-element__label">Skill</label>
<div class="slds-form-element__control">
<select class="slds-select" onchange="updateSelectedSkill(this.value)">
<option value="">Select a skill...</option>
<apex:repeat value="{!availableSkills}" var="availSkill">
<option value="{!availSkill.Id}">{!availSkill.MasterLabel}</option>
</apex:repeat>
</select>
</div>
</div>
</div>
<div class="slds-col slds-size_1-of-4">
<div class="slds-form-element">
<label class="slds-form-element__label">Skill Level</label>
<div class="slds-form-element__control">
<input type="number"
class="slds-input"
min="1"
max="10"
placeholder="1-10"
onchange="updateSkillLevel(this.value)"/>
</div>
</div>
</div>
<div class="slds-col slds-size_1-of-4">
<div class="slds-form-element">
<label class="slds-form-element__label" style="visibility: hidden;">Add</label>
<div class="slds-form-element__control">
<button class="slds-button slds-button_brand" onclick="assignSkill()">
Assign Skill
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</apex:outputPanel>
</div>
<!-- JavaScript Functions -->
<script>
function updateSearchTerm(value) {
document.getElementById('searchInput').value = value;
}
function searchResources() {
var searchTerm = document.getElementById('searchInput').value;
window.location = '{!URLFOR($Page.SkillsManager)}?search=' + encodeURIComponent(searchTerm);
}
function selectResource(resourceId) {
window.location = '{!URLFOR($Page.SkillsManager)}?resourceId=' + resourceId;
}
function updateSelectedSkill(skillId) {
document.querySelector('select.slds-select').value = skillId;
}
function updateSkillLevel(level) {
document.querySelector('input[type="number"]').value = level;
}
function assignSkill() {
var skillId = document.querySelector('select.slds-select').value;
var skillLevel = document.querySelector('input[type="number"]').value;
if(skillId && skillLevel) {
window.location = '{!URLFOR($Page.SkillsManager)}?action=assign&skillId=' + skillId + '&level=' + skillLevel;
}
}
function removeSkill(skillReqId) {
if(confirm('Are you sure you want to remove this skill?')) {
window.location = '{!URLFOR($Page.SkillsManager)}?action=remove&skillReqId=' + skillReqId;
}
}
</script>
</body>
</html>
</apex:page>
Deployment Configuration
1. Create Custom Tab
-
Navigate to Setup → User Interface → Tabs → Visualforce Tabs
-
Click New Visualforce Tab
-
Select
SkillsManagerfrom the Visualforce Page dropdown -
Tab Name:
Skills Manager -
Tab Style: Select any appropriate icon (e.g.,
service_resource) -
Click Next and add to relevant profiles
2. Add to FSL App
-
Navigate to App Manager
-
Edit your Field Service Lightning app
-
Add the
Skills Managertab to the selected tabs -
Save changes
Technical Features
-
Search Functionality: SOQL-based search across Service Resources
-
Skill Management: Complete CRUD operations for SkillRequirement records
-
SLDS Styling: Lightning-compliant user interface
-
URL Parameter Handling: State management through URL parameters
-
Error Handling: Comprehensive try-catch blocks with user feedback
Security Considerations
-
Controller uses
with sharingfor user context enforcement -
Implicit FLS and CRUD checks through SOQL
-
Exception handling prevents sensitive data exposure
This implementation provides a fully functional skills management interface that operates independently of the Dispatcher Console, accessible via a custom tab in the FSL navigation.