Creating a New Approver Dashboard

Peter Williams
Kilo Sage

Good day everyone,

so i created a new widget to have an all in One solution there user can approve or reject ritm items from just a simple view, for the most part its all working except the Key point, it doesnt update the approve or reject for that ritm.

 

here is my code, PLEASE Help....

HTML Template:

<div>
<h2 class="text-xl font-bold mb-4">My Pending Approvals</h2>

<div ng-if="!data.approvals || data.approvals.length === 0">No pending approvals.</div>

<form>
<table class="table w-full">
<thead>
<tr>
<th><input type="checkbox" ng-model="c.data.selectAll" ng-change="c.toggleAll()"></th>
<th ng-click="c.setSort('task.number')" style="cursor:pointer;">
RITM
<span ng-if="c.sortColumn === 'task.number'">{{ c.sortReverse ? '▼' : '▲' }}</span>
</th>
<th>Details</th>
<th ng-click="c.setSort('task.opened')" style="cursor:pointer;">
Submission Date
<span ng-if="c.sortColumn === 'task.opened'">{{ c.sortReverse ? '▼' : '▲' }}</span>
</th>
<th ng-click="c.setSort('totalAmount')" style="cursor:pointer;">
Total Amount
<span ng-if="c.sortColumn === 'totalAmount'">{{ c.sortReverse ? '▼' : '▲' }}</span>
</th>
<th>Attachments</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="item in data.approvals | orderBy: c.sortColumn : c.sortReverse">
<td><input type="checkbox" ng-model="item.selected"></td>
<td>
<strong>
<a href="/sc_req_item.do?sys_id={{ item.task.sys_id }}" target="_blank">
{{ item.task.number }}
</a>
</strong>
</td>
<td>
<div>
<strong>{{ item.task.short_description }}</strong>
<ul class="ml-4 mt-1 list-disc">
<li><strong>Voucher Type:</strong> {{ item.variables['Voucher Type'] }}</li>
<li><strong>Purpose of Expense:</strong> {{ item.variables['Purpose of Expense'] }}</li>
<li><strong>Requested For:</strong> {{ item.variables['Requested For'] }}</li>
<li><strong>Payee Name:</strong> {{ item.variables['Payee Name'] }}</li>
</ul>
</div>
</td>
<td>{{ item.task.opened }}</td>
<td>{{ item.variables['Total Amount'] }}</td>
<td>
<ul class="ml-2">
<li ng-repeat="att in item.attachments">
<a href="{{ att.url }}" target="_blank">📎 {{ att.name }}</a>
</li>
</ul>
</td>
<td>
<button type="button" class="btn btn-success btn-sm" ng-click="c.approve(item)">Approve</button>
<button type="button" class="btn btn-danger btn-sm" ng-click="c.reject(item)">Reject</button>
</td>
</tr>
</tbody>
</table>

<div class="mt-4">
<button type="button" class="btn btn-success" ng-click="c.approveSelected()" ng-disabled="!c.hasSelected()">Approve Selected</button>
<button type="button" class="btn btn-danger ml-2" ng-click="c.rejectSelected()" ng-disabled="!c.hasSelected()">Reject Selected</button>
</div>
</form>
</div>

Client Script:

function($scope, spUIActionsExecuter, spUtil, $timeout, i18n) {
var c = this;

// Sort defaults
c.sortColumn = 'task.number';
c.sortReverse = false;
c.loadingApprovals = {};

// Prepare totalAmount for sorting
if ($scope.data.approvals && $scope.data.approvals.length) {
$scope.data.approvals.forEach(function(item) {
var amt = item.variables && item.variables['Total Amount'] ? item.variables['Total Amount'] : '0';
amt = amt.replace(/[^0-9.\-]+/g, '');
item.totalAmount = parseFloat(amt) || 0;
});
}

c.setSort = function(column) {
if (c.sortColumn === column) {
c.sortReverse = !c.sortReverse;
} else {
c.sortColumn = column;
c.sortReverse = false;
}
};

c.toggleAll = function() {
var selectAll = c.data.selectAll;
$scope.data.approvals.forEach(function(item) {
item.selected = selectAll;
});
};

c.hasSelected = function() {
return $scope.data.approvals.some(function(item) {
return item.selected;
});
};

// Approve or reject a single approval by sysapproval_approver sys_id
c.updateApproval = function(approverSysId, state, esigRequired) {
if (c.loadingApprovals[approverSysId]) return; // prevent double clicks
c.loadingApprovals[approverSysId] = true;

var doUpdate = function() {
c.server.update({ approverSysId: approverSysId, op: state }).then(function(response) {
c.loadingApprovals[approverSysId] = false;
if (response && response.error) {
spUtil.addErrorMessage(response.error);
} else {
spUtil.addInfoMessage(i18n.getMessage("Approval {0} successfully.", [state]));
c.server.refresh();
}
}).catch(function(err) {
c.loadingApprovals[approverSysId] = false;
spUtil.addErrorMessage("Error updating approval: " + err.message);
});
};

if (esigRequired) {
var requestParams = {
username: $scope.data.esignature.username,
userSysId: $scope.data.esignature.userSysId
};
// Use spUIActionsExecuter if e-signature required
spUIActionsExecuter.executeFormAction(
state === 'approved' ? 'cbfe291147220100ba13a5554ee4904d' : '580f711147220100ba13a5554ee4904b',
'sysapproval_approver',
approverSysId,
[],
'',
requestParams
).then(function() {
c.loadingApprovals[approverSysId] = false;
spUtil.addInfoMessage(i18n.getMessage("Approval {0} successfully.", [state]));
c.server.refresh();
}).catch(function(err) {
c.loadingApprovals[approverSysId] = false;
spUtil.addErrorMessage("Error updating approval: " + err.message);
});
} else {
doUpdate();
}
};

// Approve all approvers of one request item
c.approve = function(item) {
item.approvers.forEach(function(approverSysId) {
var esigRequired = item.requireEsigApproval || false;
c.updateApproval(approverSysId, 'approved', esigRequired);
});
};

// Reject all approvers of one request item
c.reject = function(item) {
item.approvers.forEach(function(approverSysId) {
var esigRequired = item.requireEsigApproval || false;
c.updateApproval(approverSysId, 'rejected', esigRequired);
});
};

// Bulk approve selected
c.approveSelected = function() {
$scope.data.approvals.forEach(function(item) {
if (item.selected) {
item.approvers.forEach(function(approverSysId) {
var esigRequired = item.requireEsigApproval || false;
c.updateApproval(approverSysId, 'approved', esigRequired);
});
}
});
};

// Bulk reject selected
c.rejectSelected = function() {
$scope.data.approvals.forEach(function(item) {
if (item.selected) {
item.approvers.forEach(function(approverSysId) {
var esigRequired = item.requireEsigApproval || false;
c.updateApproval(approverSysId, 'rejected', esigRequired);
});
}
});
};

}

Server Script:

(function() {
// Debug log - remove in production if needed
gs.info('Approval Dashboard Server Script Started with input: ' + JSON.stringify(input));

// Handle approval update request
if (input && input.approverSysId && input.op) {
var op = input.op.toString();
var approverGR = new GlideRecord('sysapproval_approver');
if (approverGR.get(input.approverSysId)) {
var userApprovalAccess = gs.hasRole("approval_admin") ||
(gs.hasRole("approver_user") && approverGR.approver == gs.getUserID());

if (!userApprovalAccess) {
data.error = "You do not have permission to update this approval.";
} else {
approverGR.state = op;
var updated = approverGR.update();
if (updated) {
data.message = "Approval updated successfully";
} else {
data.error = "Failed to update approval";
}
}
} else {
data.error = "Approval record not found";
}
return; // Stop loading full list on update
}

// Load e-signature registry for tables requiring esign approval
var esigRequiredMap = {};
if (GlidePluginManager.isRegistered('com.glide.e_signature_approvals')) {
var esigRegistryGR = new GlideRecord("e_signature_registry");
esigRegistryGR.addQuery("enabled", "true");
esigRegistryGR.query();
while(esigRegistryGR.next()) {
esigRequiredMap[esigRegistryGR.getValue("table_name")] = true;
}
}

// Load approvals for current user
var approvalsByRitm = {};
var gr = new GlideRecord('sysapproval_approver');
gr.addQuery('approver', gs.getUserID());
gr.addQuery('state', 'requested');
gr.query();

while (gr.next()) {
var ritmSysId = gr.sysapproval.sys_id.toString();

if (!approvalsByRitm[ritmSysId]) {
var reqItem = new GlideRecord('sc_req_item');
if (reqItem.get(ritmSysId)) {
var item = {
task: {
sys_id: reqItem.sys_id.toString(),
number: reqItem.number.toString(),
short_description: reqItem.short_description.toString(),
opened: reqItem.opened_at ? reqItem.opened_at.toString() : ''
},
approvers: [gr.sys_id.toString()],
variables: {},
attachments: [],
requireEsigApproval: esigRequiredMap['sc_req_item'] === true // check if sc_req_item requires esig
};

var variableFieldsToShow = {
"expense_will_be_paided_by": "Voucher Type",
"purpose_of_expenditure": "Purpose of Expense",
"total_office": "Total Amount",
"requested_for": "Requested For",
"pay_to_the_order_of": "Payee Name"
};

var variables = reqItem.variables.getElements();
for (var i = 0; i < variables.length; i++) {
var v = variables[i];
var qName = v.getName();
if (variableFieldsToShow.hasOwnProperty(qName)) {
item.variables[variableFieldsToShow[qName]] = v.getDisplayValue();
}
}

var attGr = new GlideRecord('sys_attachment');
attGr.addQuery('table_sys_id', ritmSysId);
attGr.addQuery('table_name', 'sc_req_item');
attGr.query();
while (attGr.next()) {
item.attachments.push({
name: attGr.file_name.toString(),
url: '/sys_attachment.do?sys_id=' + attGr.sys_id.toString()
});
}

approvalsByRitm[ritmSysId] = item;
}
} else {
approvalsByRitm[ritmSysId].approvers.push(gr.sys_id.toString());
}
}

var approvals = [];
for (var key in approvalsByRitm) {
if (approvalsByRitm.hasOwnProperty(key)) {
approvals.push(approvalsByRitm[key]);
}
}

approvals.forEach(function(item) {
var amt = item.variables['Total Amount'] || '0';
amt = amt.replace(/[^0-9.\-]+/g, '');
item.totalAmount = parseFloat(amt) || 0;
});

data.approvals = approvals;
})();

3 REPLIES 3

wojasso
Giga Guru

Hi @PeterWilliams

The Service Catalog approval process uses records in the sysapproval_approver table. Setting only the state on these rows is not enough to drive the workflow – you need to update both the approval state and run the processor that notifies the Flow/Workflow engine.

  • When an approver clicks approve or reject, look up the sysapproval_approver record by sys_id and set both the state and approval fields to approved or rejected.
  • After updating the record, use the sn_sc.ApprovalProcessor API to process the approval. This will trigger any downstream actions (closing the RITM, moving to next approver, etc.).
  • If you need to collect an electronic signature, continue using the spUIActionsExecuter.executeFormAction call with the appropriate UI Action sys_id – it will also call the processor when complete.

Example server‑side code to approve an item:

var appr = new GlideRecord('sysapproval_approver');
if (appr.get(inputs.approverSysId)) {
  // update both fields
  appr.state = 'approved';
  appr.approval = 'approved';
  appr.update();
  // call the processor to advance the request
  var processor = new sn_sc.ApprovalProcessor();
  processor.approve(appr.sysapproval, gs.getUserID());
}

You can similarly call processor.reject() to reject the request. This approach ensures the request, request item and any associated tasks move to the correct states.



💥 Was this answer useful? 👈 If so, click 👍 Helpful 👍 or Accept as Solution 💡🛠️🧐🙌

Thank you for that line of code, i am having a hard time incorporating it into my existing code.

Any recommendations on where i am going wrong with it?

Peter Williams
Kilo Sage

does anyone see where i am going wrong in my code?