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

  1. Apex Controller: Handles all SOQL queries and DML operations for Skills and Service Resources.

  2. Visualforce Page: Provides the UI for searching resources, viewing assigned skills, and managing skill assignments.

  3. 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

  1. Navigate to Setup → User Interface → Tabs → Visualforce Tabs

  2. Click New Visualforce Tab

  3. Select SkillsManager from the Visualforce Page dropdown

  4. Tab Name: Skills Manager

  5. Tab Style: Select any appropriate icon (e.g., service_resource)

  6. Click Next and add to relevant profiles

2. Add to FSL App

  1. Navigate to App Manager

  2. Edit your Field Service Lightning app

  3. Add the Skills Manager tab to the selected tabs

  4. Save changes


Technical Features

Security Considerations

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.

Leave a Reply

Your email address will not be published. Required fields are marked *