The Zurich release has arrived! Interested in new features and functionalities? Click here for more

Closing Work Orders Tasks in the Past

Piyus Padhy2000
Tera Contributor

 

During a recent Field Service Management (FSM) implementation, there was a requirement for the ability to close Work Order Tasks with back-dated Actual Start and End times. By default, FSM tracks work order task closing time as the current time the agent clicked on "Close Complete" on Work Order Task. But in some industries (e.g., telecom, utilities, or onsite repair), technicians may complete jobs offline and then later want to log them “in the past” when syncing back into the system.

 

To fulfil the use case, I created a UI Button "Closed Complete (Backdate)" that allows agent to input any past "Actual work start" and "Actual work end" which in turns closes the Work Order Tasks in the Past.

 

 

What are the key benefits of this method

 

  • Supports technicians who complete work offline and update records later.
  •  Ensures accurate reporting on SLA adherence and labor hours.
  •  Provides flexibility without bypassing closure validations.

 

 

To achieve this functionality I created a UI Action called Close Complete (Backdate)

The UI was created for both CSM/FSM Workspace and Native UI , there the scripts may differ accordingly

To achieve the pop-up feature in Native UI I created a separate UI Page

To achieve the calculation of Actual time worked based on the time input was done through a Work Task Flows

 

Codes for all the configurations 

 

UI Action

Name:-Close Complete (Backdate)

Table:-Work Order Task [wm_task]

Order:-500

Action Name:-close_complete_back_date

Show insert:-
Show update:-
Client:-
List v2 Compatible:-
Form button:-
Application:-Global
Onclick:-retroAssignment();
Condition:-(new global.StateFlow().validFlow(current, '74b073de833a6650ea3145647daad307', 'manual')) 

UI Action (Script):-

 

function retroAssignment() {
var taskSysId = g_form.getUniqueValue();
var dialogClass;
try {
dialogClass = GlideModal;
} catch (e) {
dialogClass = GlideDialogWindow;
}
/* Display the UI Page */
var dialog = new dialogClass("KAP_service_management_task_back_date_closed_complete");
dialog.setWidth("550");
dialog.setTitle("Close Complete");
dialog.setPreference("sys_id", taskSysId);
dialog.setPreference("work_notes", g_form.getValue("work_notes"));
dialog.setPreference("start_work", g_form.getValue("work_start"));
dialog.setPreference("end_work", g_form.getValue("work_end"));
dialog.setPreference("state_flow_id", "74b073de833a6650ea3145647daad307");
dialog.render();
});
}
if (typeof window == 'undefined') {
new global.StateFlow().processFlow(current, '74b073de833a6650ea3145647daad307', 'manual');
action.setRedirectURL(current);
}

 

UI Action (Workspace Client Script):-

 

function onClick(g_form) {
// Get current task's sys_id
var taskSysId = g_form.getUniqueValue();
g_form.setMandatory("work_start", true);
g_form.setMandatory("work_end", true);
var emptyFields = [];
if (!g_form.getValue("work_start"))
emptyFields.push("Actual work start");
if (!g_form.getValue("work_end"))
emptyFields.push("Actual work end");

if (emptyFields.length > 0) {
var errorMessage = "The following mandatory fields are not filled in:\n\n" + emptyFields.join(", ");
g_form.addErrorMessage(errorMessage);
return;
}
var fields = [{
type: 'textarea',
name: 'work_notes',
label: getMessage('Work notes'),
mandatory: true
}];
// Show modal and handle user input
g_modal.showFields({
title: "Close Complete",
fields: fields,
size: 'md'
}).then(function(fieldValues) {
var workNotesValue = fieldValues.updatedFields[0].value;
// Set captured work notes into the form
g_form.setValue('work_notes', workNotesValue);
g_form.submit('close_complete_back_date');
});
 
}

 

UI Page:-

Name:-KAP_service_management_task_back_date_closed_complete

Category:-HTML Editor

 

UI Page HTML:-

 

<g:ui_form>
<j:set var="task_id" value="${RP.getWindowProperties().get('sys_id')}" />
<input type="hidden" name="cancelled" id="cancelled" value="false" />
<input type="hidden" name="task_id" id="task_id" value="${RP.getWindowProperties().get('sys_id')}" />
<input type="hidden" name="state_flow" id="state_flow" value="${RP.getWindowProperties().get('state_flow_id')}" />
<j:set var="cancelled" value="false" />
<j:set var="jvar_work_notes" value="${RP.getWindowProperties().get('work_notes')}" />
<j:set var="jvar_work_start" value="${RP.getWindowProperties().get('start_work')}" />
<j:set var="jvar_work_end" value="${RP.getWindowProperties().get('end_work')}" />

<style>
#task_close_complete .left {
max-width: 60px;
}

#task_close_complete #yesno {
width: 98%;
}

HTML[data-doctype=true] #task_close_complete #yesno {
width: 100%;
}

#task_close_complete tr {
margin-bottom: 12px;
display: table-row;
}

#task_close_complete td {
padding-bottom: 12px;
}
</style>

<table id="task_close_complete" width="100%">
<colgroup>
<col />
<col />
</colgroup>
<!-- Actual Start Date Section-->
<tr>
<td class="left" valign="top">
<label class="control-label">
<span class="icon icon-required required-marker" mandatory="true" style="display:inline !important;"></span>
<span for="work_start_update">${gs.getMessage("Actual Start Date")}</span>
</label>
</td>
<td align="right">
<g:ui_date_time wrap="soft" rows="5" query="" id="work_start_update" style="width:98%; overflow:auto;" name="work_start_update" />
</td>
</tr>
<tr id="work_start_error" style="display: none;">
<td colspan="4">
<div class="notification-danger">
<div>
<img src='images/outputmsg_error.gifx' alt='' />${gs.getMessage("Please provide Actual work start")}
</div>
</div>
</td>
</tr>
<!-- Actual End Date Section-->
<tr>
<td class="left" valign="top">
<label class="control-label">
<span class="icon icon-required required-marker" mandatory="true" style="display:inline !important;"></span>
<span for="work_end_update">${gs.getMessage("Actual End Date")}</span>
</label>
</td>
<td align="right">
<g:ui_date_time wrap="soft" rows="5" query="" id="work_end_update" style="width:98%; overflow:auto;" name="work_end_update" />
</td>
</tr>
<tr id="work_end_error" style="display: none;">
<td colspan="4">
<div class="notification-danger">
<div>
<img src='images/outputmsg_error.gifx' alt='' />${gs.getMessage("Please provide Actual work end")}
</div>
</div>
</td>
</tr>
<!-- Actual Start Date Validation Section-->
<tr id="work_startdatevalid_error" style="display: none;">
<td colspan="4">
<div class="notification-danger">
<div>
<img src='images/outputmsg_error.gifx' alt='' />${gs.getMessage("Actual Start date should be before Actual work end")}
</div>
</div>
</td>
</tr>
<!-- Work Notes Section -->
<tr>
<td class="left" valign="top">
<label class="control-label">
<span class="icon icon-required required-marker" mandatory="true" style="display:inline !important;"></span>
<span for="work_notes_update">${gs.getMessage("Reason for the complete closure")}</span>
</label>
</td>
<td align="right">
<textarea wrap="soft" autocomplete="off" rows="5" id="work_notes_update" data-length="4000" style="width:98%; overflow:auto;" name="work_notes_update">${jvar_work_notes}</textarea>
</td>
</tr>
<tr>
<td colspan="2" id="work_notes_error" style="display: none">
<div class="notification-danger">
<div>
<img src='images/outputmsg_error.gifx' alt='' />${gs.getMessage("Please provide the reason for the complete closure")}
</div>
</div>
</td>
</tr>
<!-- Buttons -->
<tr>
<td colspan="2" align="right">
<g:dialog_buttons_ok_cancel ok="return submitChanges()" cancel="return submitCancel()" />
</td>
</tr>
</table>

<style>
.notification-danger {
background-color: #f8d7da;
padding: 8px 12px;
border: 1px solid #f5c2c7;
border-radius: 4px;
color: #842029;
margin: 6px 0;
}

.notification-danger img {
margin-right: 6px;
vertical-align: middle;
}
</style>
</g:ui_form>

 

UI Page Client Script

 

$('work_start_update').value = '';
$('work_end_update').value = '';

/* Handle cancel action: flag as cancelled and close the modal */
function submitCancel() {
var c = gel('cancelled');
c.value = "true";
GlideDialogWindow.get().destroy();
return false;
}

function submitChanges() {

var taskId = gel('task_id').value;
var workStart = gel('work_start_update').value;
var workEnd = gel('work_end_update').value;

/* Validate Start Date is not empty */
if (!workStart) {
document.getElementById('work_start_error').style.display = "";
return false;
} else {
document.getElementById('work_start_error').style.display = "none";
}

/* Validate End Date is not empty */
if (!workEnd) {
document.getElementById('work_end_error').style.display = "";
return false;
} else {
document.getElementById('work_end_error').style.display = "none";
}

/* Date validity check: Start < End */
var startDate = new Date(workStart);
var endDate = new Date(workEnd);

if (startDate > endDate) {
document.getElementById('work_startdatevalid_error').style.display = "";
return false;
} else {
document.getElementById('work_startdatevalid_error').style.display = "none";
}

/* Validate work notes field */
var notes = gel('work_notes_update').value.replace(/^\s+|\s+$/g, '');
if (notes.length < 1) {
document.getElementById('work_notes_error').style.display = "";
return false;
} else {
document.getElementById('work_notes_error').style.display = "none";
}

g_form.clearValue("work_notes");

/* Check if the user is being tracked and store geolocation for audit */
GlideAjax.disableSessionMessages();
var ajax = new GlideAjax('GeolocationAJAX');
ajax.addParam('sysparm_name', 'checkTracking');
ajax.addParam('sysparm_usersysid', g_user.userID);
ajax.getXML(function(response) {
var isUserTracked = response.responseXML.documentElement.getAttribute("answer");
var result = isUserTracked.split(',');
if (result[0] == 'true') {
navigator.geolocation.getCurrentPosition(
function(position) {
var ajax = new GlideAjax('GeolocationAJAX');
ajax.addParam('sysparm_name', 'storeGeolocationHistory');
ajax.addParam('sysparm_usersysid', g_user.userID);
ajax.addParam('sysparm_latitude', position.coords.latitude);
ajax.addParam('sysparm_longitude', position.coords.longitude);
ajax.addParam('sysparm_action', 'Close Complete');
ajax.addParam('sysparm_task', g_form.getUniqueValue());
ajax.getXML();
});
}
});

var ok_button = document.getElementById("ok_button");
/* hide submit button to prevent multi clicks
ok_button.style.display = "none"; */
return true;
}

 

UI Page Processing Script

 

if ('false' == cancelled) {

var wot_task = new GlideRecord("wm_task");

if (wot_task.get(task_id)) {

wot_task.work_notes = work_notes_update;
wot_task.work_start = work_start_update;
wot_task.work_end = work_end_update;

new global.StateFlow().processFlow(wot_task, state_flow, 'manual');

response.sendRedirect("wm_task.do?sys_id=" + wot_task.sys_id);
}
}

 

Work Task Flow:-

Number:-SF0010006

Table:-Work Order Task [wm_task]

Ending state:-Closed Complete

Name:-Close Complete (Backdate)

 

Manual:-

Condition-Assigned to is Dynamic me and Active is true

 

Manual Script:-

 

function close_complete_back_date(current) {
var startWork = current.work_start;
var endWork = current.work_end;
current.substate = "6"; /* Update the Substate to Closed Complete */
current.window_start = current.work_start; /* Update the Window Start with the Actual work start */
current.expected_start = current.work_start; /* Update the Window Start with the Actual work start */
var estimated_duration = gs.dateDiff(startWork.getValue(), endWork.getValue(), false); /* Calculate the Actual duration */
current.estimated_work_duration = estimated_duration; /* Update the Estimated Duration with the Actual Duration */
}
close_complete_back_date(current);
 
Output of the UI Configurations
 
Native UI:-
 
PiyusPadhy2000_0-1758274811462.png

 

PiyusPadhy2000_1-1758275021096.png

 

PiyusPadhy2000_2-1758275082746.png

 

 

PiyusPadhy2000_3-1758275107078.png

 

 

CSM Workspace:-

 

PiyusPadhy2000_4-1758275257667.png

 

PiyusPadhy2000_5-1758275283510.png

 

PiyusPadhy2000_6-1758275306688.png

 

PiyusPadhy2000_7-1758275354060.png

 

PiyusPadhy2000_8-1758275409703.png
0 REPLIES 0