HR Leave · Australia

Service Portals & Employee Center Complete Guide — دليل البورتالات الشامل

ServiceNow Australia · Custom app · Arabic + English readable · scripts copy/paste ready

App typeCustom
Scopex_786686_hr_leave
sys_idc84db0e8839f0b509f02b0d6feaad3c9
Replace{BASE} = your instance
Instance URLs Service Portal Widgets Employee Center

Service Portals & Employee Center Complete Guide — دليل البورتالات الشامل

Platform: ServiceNow Australia (Sep 2026)
دليل شامل لبناء وتخصيص Service Portal و Employee Center


Table of Contents

  1. Service Portal Overview
  2. Portal Configuration
  3. Pages and Layouts
  4. Widgets Development
  5. Angular Providers
  6. CSS and Theming
  7. Employee Center
  8. HR Leave Portal Widget
  9. Mobile Responsiveness
  10. 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