HR Leave · Australia

04 — Service Portal (Studio From Scratch)

Widgets hrlx_my_leaves + hrlx_balance, page hrlx_home, roles, portal URL

Scopex_786686_hr_leave
sys_ide5f5bc9493a303507520703efaba104d
Instance labdev398980
URL token{BASE}
Quick open Visual map Service Portal Widgets HR Leave Home

Visual map — Ch.04 · Service Portal — self-service

See in 10 seconds

Widgets hrlx_my_leaves + hrlx_balance → page hrlx_home

You build

Portal suffix hrlx

Track

Development

W1My leaves
→
W2Balance
→
PAGEhrlx_home
→
ROLEemployee
✓ GlideRecordSecure✓ Cancel on server✓ Role on widget✗ ng-bind-html on reason✗ Show team on employee page

Full visual path (all chapters): VISUAL_FROM_SCRATCH_00_16.html

Studio-only from scratch: Portal widgets use u_* fields on req_v2 and bal_v2. No external build tools.

Also open: Properties + Direct Links · Schema · Studio Create All Objects · Previous: 03 Service Catalog

How portal objects relate

Portal (url_suffix hrlx)
  └── Page hrlx_home
        ├── Part 1 Widget hrlx_my_leaves (req_v2 list + cancel)
        └── Part 2 Widget hrlx_balance (bal_v2 cards)
Property x_786686_hr_leave.config.portal_page_id = hrlx_home

Rule 0 — Before any object (Application + Update Set + Studio)

Repeat these checks before creating or editing any application file. This section is also section 00 Update Set — create the capture set first.

  1. Application picker — top-right globe / application menu → select HR Leave (x_786686_hr_leave). Never leave Global selected when creating scoped artifacts.
  2. Update Set — All → System Update Sets → Local Update Sets → New:
    FieldValue
    NameHR Leave – Build
    ApplicationHR Leave
    StateIn Progress
    DescriptionCaptures all HR Leave lab artifacts
    After submit: open the record → click Make Current. Direct list: {BASE}/sys_update_set_list.do
  3. Open Studio — {BASE}/$studio.do → confirm header shows HR Leave.

If artifacts land in Global scope, delete them and recreate under HR Leave. ACLs and events created globally will not behave correctly for this app.

Verify Update Set: Create a test Script Include in Studio → open HR Leave – Build → Related List Customer Updates shows the new file.


Part 1: Widget — hrlx_my_leaves

Purpose

Lists current user's leave requests with cancel action for draft/submitted rows.

Navigation path

Direct URL

Prerequisites

Form fields

FieldValue
IDhrlx_my_leaves
NameHRLX My Leaves
Client controllerHrlxMyLeavesCtrl
ApplicationHR Leave

Step-by-step create

  1. SP → Widgets → New → ID hrlx_my_leaves.
  2. Paste HTML template, CSS, Client script, Server script from sections below.
  3. Save.

Full script — hrlx_my_leaves HTML template

<div class="hrlx-my-leaves">
  <h3>My Leave Requests</h3>

  <table class="table table-striped" ng-if="data.leaves.length">
    <thead>
      <tr>
        <th>Number</th>
        <th>Type</th>
        <th>Start</th>
        <th>End</th>
        <th>Days</th>
        <th>State</th>
        <th></th>
      </tr>
    </thead>
    <tbody>
      <tr ng-repeat="item in data.leaves">
        <td>{{item.number}}</td>
        <td>{{item.type}}</td>
        <td>{{item.start}}</td>
        <td>{{item.end}}</td>
        <td>{{item.days}}</td>
        <td><span class="label label-default">{{item.state}}</span></td>
        <td>
          <button class="btn btn-xs btn-danger"
                  ng-if="item.state === 'submitted' || item.state === 'draft'"
                  ng-click="c.cancelLeave(item)">Cancel</button>
        </td>
      </tr>
    </tbody>
  </table>

  <p ng-if="!data.leaves.length" class="text-muted">No leave requests yet.</p>
</div>

Full script — hrlx_my_leaves CSS

.hrlx-my-leaves {
  padding: 16px;

  h3 {
    margin-top: 0;
    margin-bottom: 16px;
    font-weight: 600;
  }

  .label {
    text-transform: capitalize;
  }
}

Full script — hrlx_my_leaves Client script

/**
 * Widget: hrlx_my_leaves — Client Controller
 * Application: Custom · Scope: x_786686_hr_leave · sys_id: 83d5298c932b8f107520703efaba1072
 */
function HrlxMyLeavesCtrl() {
    var c = this;

    c.cancelLeave = function (item) {
        if (!confirm('Cancel leave ' + item.number + '?')) return;
        c.data.action = 'cancel';
        c.data.sys_id = item.sys_id;
        c.server.update().then(function () {
            c.data.action = '';
            c.data.sys_id = '';
        });
    };
}

Full script — hrlx_my_leaves Server script

/**
 * Widget: hrlx_my_leaves — Server Script
 * Application: Custom · Scope: x_786686_hr_leave · sys_id: 83d5298c932b8f107520703efaba1072
 * Shows the current user's leave requests (secure).
 */
(function () {
    data.leaves = [];

    var gr = new GlideRecordSecure('x_786686_hr_leave_req_v2');
    gr.addQuery('u_requested_for_user', gs.getUserID());
    gr.orderByDesc('sys_created_on');
    gr.setLimit(50);
    gr.query();

    while (gr.next()) {
        data.leaves.push({
            sys_id: gr.getUniqueValue(),
            number: gr.getValue('u_number'),
            type: gr.getDisplayValue('u_leave_type'),
            start: gr.getValue('u_start_date'),
            end: gr.getValue('u_end_date'),
            days: gr.getValue('u_days_count'),
            state: gr.getValue('u_state')
        });
    }

    // Cancel action from client
    if (input && input.action === 'cancel' && input.sys_id) {
        var c = new GlideRecord('x_786686_hr_leave_req_v2');
        if (c.get(input.sys_id) && c.getValue('u_requested_for_user') === gs.getUserID()) {
            c.u_state = 'cancelled';
            c.update();
        }
    }
})();

Verify checklist

Demo / test

After Part 3 page built: employee1 sees own requests.


Part 2: Widget — hrlx_balance

Purpose

Read-only balance cards for current year from bal_v2.

Navigation path

Direct URL

Prerequisites

Form fields

FieldValue
IDhrlx_balance
NameHRLX Balance
Client controllerHrlxBalanceCtrl
ApplicationHR Leave

Step-by-step create

  1. SP → Widgets → New → ID hrlx_balance.
  2. Paste HTML, CSS, Client, Server from below.
  3. Save.

Full script — hrlx_balance HTML template

<div class="hrlx-balance">
  <h3>Leave Balance {{data.year}}</h3>

  <div class="hrlx-cards" ng-if="data.balances.length">
    <div class="hrlx-card" ng-repeat="b in data.balances">
      <div class="hrlx-card-type">{{b.type}}</div>
      <div class="hrlx-card-remaining">{{b.remaining}}</div>
      <div class="hrlx-card-meta">used {{b.used}} / {{b.entitled}}</div>
    </div>
  </div>

  <p ng-if="!data.balances.length" class="text-muted">No balance records found.</p>
</div>

Full script — hrlx_balance CSS

.hrlx-balance {
  padding: 16px;

  h3 { margin-top: 0; font-weight: 600; }

  .hrlx-cards {
    display: flex;
    flex-wrap: wrap;
    gap: 12px;
  }

  .hrlx-card {
    flex: 1 1 120px;
    border: 1px solid #e0e0e0;
    border-radius: 8px;
    padding: 12px;
    text-align: center;
  }

  .hrlx-card-type { text-transform: capitalize; color: #707070; }
  .hrlx-card-remaining { font-size: 28px; font-weight: 700; }
  .hrlx-card-meta { font-size: 12px; color: #909090; }
}

Full script — hrlx_balance Client script

/**
 * Widget: hrlx_balance — Client Controller
 * Application: Custom · Scope: x_786686_hr_leave · sys_id: 83d5298c932b8f107520703efaba1072
 */
function HrlxBalanceCtrl() {
    var c = this;
    // No client actions needed; data is read-only.
}

Full script — hrlx_balance Server script

/**
 * Widget: hrlx_balance — Server Script
 * Application: Custom · Scope: x_786686_hr_leave · sys_id: 83d5298c932b8f107520703efaba1072
 * Shows the current user's remaining leave balance for the current year.
 */
(function () {
    data.balances = [];
    data.year = new GlideDateTime().getYearLocalTime();

    // find employee record of current user
    var emp = new GlideRecord('x_786686_hr_leave_employee_v2');
    emp.addQuery('u_user', gs.getUserID());
    emp.query();
    if (!emp.next()) return;

    var bal = new GlideRecordSecure('x_786686_hr_leave_bal_v2');
    bal.addQuery('u_employee_ref', emp.getUniqueValue());
    bal.addQuery('u_year', data.year);
    bal.query();
    while (bal.next()) {
        data.balances.push({
            type: bal.getDisplayValue('u_leave_type'),
            entitled: parseInt(bal.getValue('u_entitled') || 0, 10),
            used: parseInt(bal.getValue('u_used') || 0, 10),
            remaining: parseInt(bal.getValue('u_remaining') || 0, 10)
        });
    }
})();

Verify checklist

Demo / test

employee1 with balance row → remaining displays correctly.


Part 3: Portal Page — hrlx_home

Purpose

HR Leave home page with balance widget on top and my leaves below.

Navigation path

Direct URL

Prerequisites

Form fields — Portal (sp_portal)

FieldValue
TitleHR Leave Portal
URL suffixhrlx
ApplicationHR Leave

Page (sp_page)

FieldValue
IDhrlx_home
TitleHR Leave Home
ApplicationHR Leave
Rolesx_786686_hr_leave.employee

Step-by-step create

  1. Portals → New OR configure dedicated portal url_suffix hrlx.
  2. Pages → New → ID hrlx_home → drag hrlx_balance (top), hrlx_my_leaves (below) on 12-column layout.
  3. Page security → role employee.
  4. Set property x_786686_hr_leave.config.portal_page_id = hrlx_home (02 Administration Part 5).
  5. Save. Open {BASE}/hrlx?id=hrlx_home as employee1.

Verify checklist

Demo / test

Login employee1 → hrlx?id=hrlx_home → requests + balance visible.

Service Portal verify checklist

Complete after Parts 1–3.

  1. Two widgets scoped to HR Leave.
  2. Page hrlx_home published with role restriction.
  3. Portal URL opens for employee1.