Service Portals & Employee Center Complete Guide — دليل البورتالات الشامل
Platform: ServiceNow Australia (Sep 2026)
دليل شامل لبناء وتخصيص Service Portal و Employee Center
Table of Contents
- Service Portal Overview
- Portal Configuration
- Pages and Layouts
- Widgets Development
- Angular Providers
- CSS and Theming
- Employee Center
- HR Leave Portal Widget
- Mobile Responsiveness
- Security and Performance
1. Service Portal Overview
Direct URLs
| Component | URL |
|---|---|
| Portal List | {BASE}/sp_portal_list.do |
| Pages | {BASE}/sp_page_list.do |
| Widgets | {BASE}/sp_widget_list.do |
| Themes | {BASE}/sp_theme_list.do |
| Designer | {BASE}/sp_config |
Portal Architecture
Portal
├── Theme (CSS, Colors, Fonts)
├── Header/Footer Widgets
├── Pages
│ ├── Containers
│ │ ├── Rows
│ │ │ └── Columns
│ │ │ └── Widgets
└── Angular Application
├── Providers (Services)
└── Dependencies (Libraries)
2. Portal Configuration
Create Portal
1. Navigate: All > Service Portal > Portals
2. Click New
3. Fill in:
- Title: HR Leave Portal
- URL suffix: hr_leave
- Homepage: Leave Request Home (sp_page)
- Theme: HR Leave Theme
- Logo: Upload logo
4. Save
Portal Properties
// Key portal properties
glide.sp.portal_id = 'hr_leave';
glide.sp.enable_service_catalog = 'true';
glide.sp.enable_knowledge = 'true';
3. Pages and Layouts
Create Page
Direct URL: {BASE}/sp_page_list.do
1. Navigate: Service Portal > Pages > New
2. Fill in:
- Title: Leave Request Home
- ID: hr_leave_home
- Layout: 12 columns
3. Save and open in Designer
Page Layout JSON
{
"rows": [
{
"columns": [
{
"size": 12,
"widgets": [
{ "name": "HR Leave Banner", "options": {} }
]
}
]
},
{
"columns": [
{
"size": 8,
"widgets": [
{ "name": "HR Leave Request Form", "options": {} }
]
},
{
"size": 4,
"widgets": [
{ "name": "HR Leave Balance", "options": {} }
]
}
]
}
]
}
4. Widgets Development
Direct URL: {BASE}/sp_widget_list.do
Widget Structure
Widget
├── Name / ID
├── HTML Template (AngularJS)
├── CSS (SCSS)
├── Client Controller (JS)
├── Server Script (JS)
├── Link Function (optional)
└── Options Schema (JSON)
Complete Widget: Leave Request Form
HTML Template:
<div class="hr-leave-form">
<h2>{{::options.title}}</h2>
<div ng-if="c.data.success" class="alert alert-success">
<i class="fa fa-check"></i> Request submitted successfully!
<a href="?id=leave_detail&sys_id={{c.data.newRequest.sys_id}}">
View Request {{c.data.newRequest.number}}
</a>
</div>
<form ng-if="!c.data.success" ng-submit="c.submitRequest()">
<!-- Employee Display -->
<div class="form-group">
<label>Employee - الموظف</label>
<p class="form-control-static">{{c.data.employee.name}}</p>
</div>
<!-- Leave Type -->
<div class="form-group">
<label for="leaveType">Leave Type - نوع الإجازة *</label>
<select id="leaveType" class="form-control"
ng-model="c.request.leave_type" required>
<option value="">-- Select --</option>
<option ng-repeat="type in c.data.leaveTypes"
value="{{type.value}}">{{type.label}}</option>
</select>
</div>
<!-- Date Range -->
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="startDate">Start Date - تاريخ البدء *</label>
<input type="date" id="startDate" class="form-control"
ng-model="c.request.start_date"
ng-change="c.calculateDuration()" required>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="endDate">End Date - تاريخ النهاية *</label>
<input type="date" id="endDate" class="form-control"
ng-model="c.request.end_date"
ng-change="c.calculateDuration()" required>
</div>
</div>
</div>
<!-- Duration Display -->
<div class="alert alert-info" ng-if="c.request.duration > 0">
<strong>Duration - المدة:</strong> {{c.request.duration}} days
<span ng-if="c.request.duration > c.data.balance" class="text-danger">
<br><i class="fa fa-warning"></i> Exceeds available balance ({{c.data.balance}} days)
</span>
</div>
<!-- Reason -->
<div class="form-group">
<label for="reason">Reason - السبب</label>
<textarea id="reason" class="form-control" rows="3"
ng-model="c.request.reason"
placeholder="Optional reason for leave request"></textarea>
</div>
<!-- Submit -->
<div class="form-group">
<button type="submit" class="btn btn-primary btn-lg"
ng-disabled="c.submitting || c.request.duration > c.data.balance">
<i class="fa fa-paper-plane"></i> Submit Request - إرسال الطلب
</button>
</div>
</form>
<!-- Loading -->
<div ng-if="c.submitting" class="text-center">
<i class="fa fa-spinner fa-spin fa-3x"></i>
<p>Submitting request...</p>
</div>
</div>
CSS (SCSS):
.hr-leave-form {
padding: 20px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
h2 {
color: #333;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 2px solid #007bff;
}
.form-group {
margin-bottom: 15px;
label {
font-weight: 600;
color: #555;
}
}
.alert {
border-radius: 4px;
}
.btn-primary {
background: linear-gradient(135deg, #007bff, #0056b3);
border: none;
padding: 12px 30px;
&:hover {
background: linear-gradient(135deg, #0056b3, #003d80);
}
}
}
/* RTL Support */
[dir="rtl"] .hr-leave-form {
text-align: right;
label {
text-align: right;
}
}
Client Controller:
api.controller = function($scope, spUtil, spModal) {
var c = this;
c.request = {
leave_type: '',
start_date: null,
end_date: null,
reason: '',
duration: 0
};
c.submitting = false;
// Calculate duration when dates change
c.calculateDuration = function() {
if (c.request.start_date && c.request.end_date) {
var start = new Date(c.request.start_date);
var end = new Date(c.request.end_date);
if (end >= start) {
var diff = Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1;
c.request.duration = diff;
} else {
c.request.duration = 0;
spUtil.addErrorMessage('End date must be after start date');
}
}
};
// Submit request
c.submitRequest = function() {
if (!c.request.leave_type || !c.request.start_date || !c.request.end_date) {
spUtil.addErrorMessage('Please fill all required fields');
return;
}
if (c.request.duration > c.data.balance) {
spUtil.addErrorMessage('Request exceeds available balance');
return;
}
c.submitting = true;
c.server.get({
action: 'submitRequest',
request: c.request
}).then(function(response) {
c.submitting = false;
if (response.data.success) {
c.data.success = true;
c.data.newRequest = response.data.newRequest;
spUtil.addInfoMessage('Leave request submitted successfully!');
} else {
spUtil.addErrorMessage(response.data.error || 'Failed to submit');
}
});
};
// Refresh balance when leave type changes
$scope.$watch('c.request.leave_type', function(newVal) {
if (newVal) {
c.server.get({
action: 'getBalance',
leave_type: newVal
}).then(function(response) {
c.data.balance = response.data.balance;
});
}
});
};
Server Script:
(function() {
// Get current user info
data.employee = {
sys_id: gs.getUserID(),
name: gs.getUserDisplayName(),
email: gs.getUser().getEmail()
};
// Get leave types
data.leaveTypes = [
{ value: 'annual', label: 'Annual Leave - إجازة سنوية' },
{ value: 'sick', label: 'Sick Leave - إجازة مرضية' },
{ value: 'emergency', label: 'Emergency Leave - إجازة طارئة' },
{ value: 'unpaid', label: 'Unpaid Leave - إجازة بدون راتب' }
];
// Get initial balance
data.balance = getBalance(gs.getUserID(), 'annual');
// Handle actions
if (input && input.action) {
if (input.action === 'getBalance') {
data.balance = getBalance(gs.getUserID(), input.leave_type);
}
if (input.action === 'submitRequest') {
var result = submitLeaveRequest(input.request);
data.success = result.success;
data.newRequest = result.record;
data.error = result.error;
}
}
function getBalance(employeeId, leaveType) {
var util = new x_786686_hr_leave.LeaveBalanceUtil();
return util.getBalance(employeeId, leaveType);
}
function submitLeaveRequest(req) {
try {
var gr = new GlideRecord('x_786686_hr_leave_request');
gr.initialize();
gr.employee = gs.getUserID();
gr.leave_type = req.leave_type;
gr.start_date = req.start_date;
gr.end_date = req.end_date;
gr.reason = req.reason || '';
var sysId = gr.insert();
if (sysId) {
gr.get(sysId);
return {
success: true,
record: {
sys_id: sysId,
number: gr.number.toString()
}
};
}
return { success: false, error: 'Insert failed' };
} catch (e) {
return { success: false, error: e.message };
}
}
})();
Options Schema:
{
"title": {
"label": "Widget Title",
"type": "string",
"default": "Submit Leave Request"
},
"show_balance": {
"label": "Show Balance",
"type": "boolean",
"default": true
}
}
5. Angular Providers
Direct URL: {BASE}/sp_angular_provider_list.do
Create Service Provider
// Name: hrLeaveService
// Type: factory
function hrLeaveService($http, $q, spUtil) {
var API_BASE = '/api/x_786686_hr_leave/hr_leave';
return {
getRequests: function(filters) {
return $http.get(API_BASE + '/requests', { params: filters })
.then(function(response) {
return response.data.result;
});
},
getRequest: function(sysId) {
return $http.get(API_BASE + '/requests/' + sysId)
.then(function(response) {
return response.data.result;
});
},
createRequest: function(request) {
return $http.post(API_BASE + '/requests', request)
.then(function(response) {
return response.data.result;
});
},
getBalance: function(leaveType) {
return $http.get(API_BASE + '/balance', {
params: { leave_type: leaveType }
}).then(function(response) {
return response.data.result.balance;
});
},
getCalendar: function(month, year) {
return $http.get(API_BASE + '/calendar', {
params: { month: month, year: year }
}).then(function(response) {
return response.data.result;
});
}
};
}
Use in Widget
// Client Controller
api.controller = function($scope, hrLeaveService, spUtil) {
var c = this;
c.loading = true;
hrLeaveService.getRequests().then(function(requests) {
c.data.requests = requests;
c.loading = false;
}).catch(function(error) {
spUtil.addErrorMessage('Failed to load requests');
c.loading = false;
});
c.submitRequest = function() {
hrLeaveService.createRequest(c.request).then(function(result) {
spUtil.addInfoMessage('Request ' + result.number + ' created');
c.data.requests.unshift(result);
});
};
};
6. CSS and Theming
Direct URL: {BASE}/sp_theme_list.do
Theme Variables
// Theme CSS Variables
$theme-color-primary: #007bff;
$theme-color-secondary: #6c757d;
$theme-color-success: #28a745;
$theme-color-danger: #dc3545;
$theme-color-warning: #ffc107;
$theme-color-info: #17a2b8;
$navbar-bg: #2c3e50;
$navbar-color: #ffffff;
$footer-bg: #f8f9fa;
$font-family-base: 'Segoe UI', Tahoma, Arial, sans-serif;
$font-family-arabic: 'Cairo', 'Amiri', sans-serif;
// Apply
body {
font-family: $font-family-base;
}
[lang="ar"], .arabic {
font-family: $font-family-arabic;
}
.navbar {
background: $navbar-bg;
.navbar-brand {
color: $navbar-color;
}
}
// Buttons
.btn-primary {
background: $theme-color-primary;
border-color: darken($theme-color-primary, 5%);
&:hover {
background: darken($theme-color-primary, 10%);
}
}
// Cards
.card {
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
transition: transform 0.2s, box-shadow 0.2s;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.12);
}
}
7. Employee Center
Direct URL: {BASE}/sn_hr_core_menu_item_list.do
Employee Center Configuration
**Setup Steps:**
1. Navigate: Employee Center > Administration > Setup
2. Configure:
- Portal: Employee Center
- Homepage: esc_dashboard
- Theme: Employee Center Default
**Add HR Leave Topic:**
1. Navigate: Employee Center > Taxonomy > Topics
2. Create Topic:
- Name: Leave Management
- Description: Request and manage your leave
- Icon: fa-calendar
- Category: HR Services
3. Add Content Items:
- Submit Leave Request (link to widget)
- View My Requests (link to list)
- Leave Balance (link to widget)
- Leave Policies (knowledge article)
Content Block Widget
// Server Script for Employee Center Content
(function() {
var userId = gs.getUserID();
// Quick stats for dashboard
data.stats = {
pending: getCount('state=2'),
approved: getCount('state=3^start_date>=' + getToday()),
balance: getBalance(userId, 'annual')
};
// Recent requests
var gr = new GlideRecord('x_786686_hr_leave_request');
gr.addQuery('employee', userId);
gr.orderByDesc('sys_created_on');
gr.setLimit(5);
gr.query();
data.recent = [];
while (gr.next()) {
data.recent.push({
sys_id: gr.getUniqueValue(),
number: gr.number.toString(),
type: gr.leave_type.getDisplayValue(),
dates: gr.start_date.getDisplayValue() + ' - ' + gr.end_date.getDisplayValue(),
state: gr.state.getDisplayValue(),
state_class: getStateClass(gr.state.toString())
});
}
function getCount(query) {
var ga = new GlideAggregate('x_786686_hr_leave_request');
ga.addQuery('employee', userId);
ga.addEncodedQuery(query);
ga.addAggregate('COUNT');
ga.query();
return ga.next() ? parseInt(ga.getAggregate('COUNT')) : 0;
}
function getBalance(empId, type) {
return new x_786686_hr_leave.LeaveBalanceUtil().getBalance(empId, type);
}
function getToday() {
return new GlideDateTime().getDate().toString();
}
function getStateClass(state) {
var classes = {'1': 'default', '2': 'warning', '3': 'success', '6': 'danger', '7': 'info'};
return 'label-' + (classes[state] || 'default');
}
})();
8. HR Leave Portal Widget
Leave Balance Widget
HTML:
<div class="leave-balance-widget">
<div class="balance-header">
<i class="fa fa-calendar-check-o fa-2x"></i>
<h3>Leave Balance - رصيد الإجازات</h3>
</div>
<div class="balance-grid">
<div class="balance-card" ng-repeat="item in c.data.balances">
<div class="balance-type">{{item.label}}</div>
<div class="balance-value">
<span class="available">{{item.available}}</span>
<span class="total">/ {{item.total}}</span>
</div>
<div class="balance-bar">
<div class="bar-fill" ng-style="{'width': item.percentage + '%'}"></div>
</div>
</div>
</div>
<div class="balance-footer">
<a href="?id=leave_request" class="btn btn-primary btn-block">
<i class="fa fa-plus"></i> Request Leave - طلب إجازة
</a>
</div>
</div>
Server Script:
(function() {
var userId = gs.getUserID();
var util = new x_786686_hr_leave.LeaveBalanceUtil();
var year = new GlideDateTime().getYearLocalTime();
data.balances = [
{
type: 'annual',
label: 'Annual - سنوية',
available: util.getBalance(userId, 'annual'),
total: getAllocation(userId, 'annual'),
percentage: 0
},
{
type: 'sick',
label: 'Sick - مرضية',
available: util.getBalance(userId, 'sick'),
total: getAllocation(userId, 'sick'),
percentage: 0
},
{
type: 'emergency',
label: 'Emergency - طارئة',
available: util.getBalance(userId, 'emergency'),
total: getAllocation(userId, 'emergency'),
percentage: 0
}
];
// Calculate percentages
for (var i = 0; i < data.balances.length; i++) {
var b = data.balances[i];
b.percentage = b.total > 0 ? Math.round((b.available / b.total) * 100) : 0;
}
function getAllocation(empId, type) {
var gr = new GlideRecord('x_786686_hr_leave_allocation');
gr.addQuery('employee', empId);
gr.addQuery('leave_type', type);
gr.addQuery('year', year);
gr.query();
return gr.next() ? parseInt(gr.days_allocated) || 0 : 0;
}
})();
Team Calendar Widget
HTML:
<div class="team-calendar">
<div class="calendar-header">
<button ng-click="c.prevMonth()" class="btn btn-sm">
<i class="fa fa-chevron-left"></i>
</button>
<h4>{{c.currentMonth}} {{c.currentYear}}</h4>
<button ng-click="c.nextMonth()" class="btn btn-sm">
<i class="fa fa-chevron-right"></i>
</button>
</div>
<div class="calendar-grid">
<div class="day-header" ng-repeat="day in c.dayNames">{{day}}</div>
<div class="day-cell" ng-repeat="day in c.calendarDays"
ng-class="{'today': day.isToday, 'other-month': !day.currentMonth}">
<span class="day-number">{{day.number}}</span>
<div class="events">
<div class="event" ng-repeat="event in day.events"
ng-style="{'background': event.color}"
title="{{event.title}}">
{{event.short}}
</div>
</div>
</div>
</div>
</div>
9. Mobile Responsiveness
Responsive CSS
// Breakpoints
$mobile: 576px;
$tablet: 768px;
$desktop: 992px;
// Mobile-first styles
.hr-leave-form {
padding: 15px;
@media (min-width: $tablet) {
padding: 25px;
}
@media (min-width: $desktop) {
padding: 35px;
}
}
// Stack columns on mobile
.balance-grid {
display: grid;
grid-template-columns: 1fr;
gap: 15px;
@media (min-width: $tablet) {
grid-template-columns: repeat(2, 1fr);
}
@media (min-width: $desktop) {
grid-template-columns: repeat(3, 1fr);
}
}
// Touch-friendly buttons
.btn {
min-height: 44px;
padding: 12px 20px;
@media (min-width: $tablet) {
min-height: auto;
padding: 8px 16px;
}
}
// Hide on mobile
.desktop-only {
display: none;
@media (min-width: $tablet) {
display: block;
}
}
// Show on mobile only
.mobile-only {
display: block;
@media (min-width: $tablet) {
display: none;
}
}
10. Security and Performance
Security Best Practices
// Server Script - Always validate user access
(function() {
// Check user is authenticated
if (!gs.isLoggedIn()) {
data.error = 'Not authenticated';
return;
}
// Only return user's own data (unless admin)
var userId = gs.getUserID();
var isAdmin = gs.hasRole('x_786686_hr_leave.hr_admin');
if (input && input.employee_id) {
if (input.employee_id !== userId && !isAdmin) {
data.error = 'Access denied';
return;
}
}
// Validate input
if (input && input.sys_id) {
if (!/^[a-f0-9]{32}$/i.test(input.sys_id)) {
data.error = 'Invalid ID format';
return;
}
}
})();
Performance Optimization
// Server Script - Efficient queries
(function() {
// Use setLimit for lists
var gr = new GlideRecord('x_786686_hr_leave_request');
gr.addQuery('employee', gs.getUserID());
gr.setLimit(data.options.limit || 10);
gr.setFields('sys_id,number,leave_type,start_date,end_date,state');
gr.orderByDesc('sys_created_on');
gr.query();
data.requests = [];
while (gr.next()) {
data.requests.push({
sys_id: gr.getUniqueValue(),
number: gr.number.toString(),
type: gr.leave_type.getDisplayValue(),
dates: gr.start_date.getDisplayValue() + ' - ' + gr.end_date.getDisplayValue(),
state: gr.state.toString()
});
}
// Use GlideAggregate for counts
var ga = new GlideAggregate('x_786686_hr_leave_request');
ga.addQuery('employee', gs.getUserID());
ga.addAggregate('COUNT');
ga.groupBy('state');
ga.query();
data.counts = {};
while (ga.next()) {
data.counts[ga.state.toString()] = parseInt(ga.getAggregate('COUNT'));
}
})();
Direct URLs Reference
| Page | URL |
|---|---|
| Portals | {BASE}/sp_portal_list.do |
| Pages | {BASE}/sp_page_list.do |
| Widgets | {BASE}/sp_widget_list.do |
| Themes | {BASE}/sp_theme_list.do |
| CSS | {BASE}/sp_css_list.do |
| Angular Providers | {BASE}/sp_angular_provider_list.do |
| Designer | {BASE}/sp_config |
| Employee Center | {BASE}/$esc.do |
Next: Workspaces Guide | AI Guide