Ticket Transitions

Summary

As of EngageIP 8.5.25.0 you can configure Ticket Transitions which allow you to organize and simplify how tickets can progress from one stage to the next (for example to move a new order from sales, through provisioning and ultimately to order completion and billing). Ticket statuses, categories and which group the ticket is assigned to can be automatically updated at each stage of the ticket transition based on the configuration you have set.

Requirements

  • The EngageIP Event Manager Service must be running for Ticket Transitions to function

  • Roles need to be setup properly along with Ticket Groups and Categories. See the Ticket Overview article for more information

Configuration

First add the following Custom Code to enable Ticket Transition functionality:

  1. In the AdminPortal load the Setup page

  2. Click on Custom Code

  3. Click Add

  4. Enter a Name and then paste in the code below

    //Reference: Castle.MonoRail.Framework //Reference: Newtonsoft.Json //Reference: Logisense.Utility //Reference: System.Data using Castle.MonoRail.Framework; using Logisense.Boss.Logic; using Logisense.Boss.Logic.DomainModel; using Logisense.Boss.Utility; using System; using System.Collections.Generic; using System.Data; using System.Linq; using Newtonsoft.Json.Linq; namespace LogisenseKB.EventHandlers { public class CreatePackageQuestionAction : BaseTicketType, IAction { public string Name { get { return "CreatePackageQuestionAction"; } } public string Description { get { return "Triggered by a new package being added to a system, which will add 2 package profile questions"; } } public string EventCode { get { return "Package.Create"; } } public int OwnerID { get; set; } public void Run(ScriptContext context) { if (!ProfileQuestion.SearchByQuestion(TicketTypeForCreateUserPackage).Any()) { Logisense.Boss.Logic.Core.PackageProfileQuestion.GetNewQuestion(TicketTypeForCreateUserPackage, Logisense.Boss.Logic.Core.DataType.TEXT_NAME, OwnerID); } if (!ProfileQuestion.SearchByQuestion(TicketTypeForCancelUserPackage).Any()) { Logisense.Boss.Logic.Core.PackageProfileQuestion.GetNewQuestion(TicketTypeForCancelUserPackage, Logisense.Boss.Logic.Core.DataType.TEXT_NAME, OwnerID); } } } public class InitializeTicketWhenCreateAction : BaseTicketType, IAction { public string Name { get { return "InitializeTicketWhenCreateAction"; } } public string Description { get { return "Triggered by a new user package being added to a system"; } } public string EventCode { get { return "UserPackage.Create"; } } public int OwnerID { get; set; } public void Run(ScriptContext context) { var userPackage = UserPackage.GetByID(Convert.ToInt32(context["UserPackageID"])); var user = userPackage.GetUser(); var owner = user.GetActingOwner(); var ticketType = GetTicketType(TicketTypeForCreateUserPackage, GetPackageAttributeProfileAnswers(userPackage), owner); if (ticketType != default(TicketType)) { var ticketName = string.Format("Auto Generated Ticket for User Package: '{0}'", userPackage.Name); InitializeTicket(userPackage, user, owner, ticketName, ticketType); } } } public class InitializeTicketWhenCancelAction : BaseTicketType, IAction { public string Name { get { return "InitializeTicketWhenCancelAction"; } } public string Description { get { return "Triggered by a user package was cancelled"; } } public string EventCode { get { return "UserPackage.Update"; } } public int OwnerID { get; set; } public void Run(ScriptContext context) { var userPackage = UserPackage.GetByID(Convert.ToInt32(context["UserPackageID"])); var oldUserPackage = UserPackage.Deserialize((string)context["OldRow"]); var user = userPackage.GetUser(); var owner = user.GetActingOwner(); var newStatusType = userPackage.GetStatusType().Name; if (newStatusType == Logisense.Boss.Logic.Core.StatusType.CanceledName && oldUserPackage.GetStatusType().Name != newStatusType) { var packageAttributeProfileAnswers = GetPackageAttributeProfileAnswers(userPackage); var ticketTypeForCancelUserPackage = GetTicketType(TicketTypeForCancelUserPackage, packageAttributeProfileAnswers, owner); if (ticketTypeForCancelUserPackage != default(TicketType)) { var ticketTypeForCreateUserPackage = GetTicketType(TicketTypeForCreateUserPackage, packageAttributeProfileAnswers, owner); var ticketName = string.Format("Auto Generated Ticket for Cancelling the User Package: '{0}'", userPackage.Name); if (ticketTypeForCreateUserPackage != default(TicketType)) { var ticketQuery = new TicketQuery() { UserPackageID = userPackage.ID, TicketTypeID = ticketTypeForCreateUserPackage.ID, }; var existingTicket = Ticket.GetCollection(ref ticketQuery).FirstOrDefault(); if (existingTicket != default(Ticket)) { //if the user package has existing tickets with the ticketTypeForCreateUserPackage if (existingTicket.GetTicketStatus().GetTicketStatusType().Name == Logisense.Boss.Logic.Core.TicketStatusType.OpenName) { //if there is an existing open ticket, update ticket type to it EditExistingTicket((Logisense.Boss.Logic.Core.Ticket)existingTicket, ticketTypeForCancelUserPackage, owner); } else { //else the ticket has been closed, create a new one InitializeTicket(userPackage, user, owner, ticketName, ticketTypeForCancelUserPackage); } } } else { //else initialize a new ticket InitializeTicket(userPackage, user, owner, ticketName, ticketTypeForCancelUserPackage); } } } } private static void EditExistingTicket(Logisense.Boss.Logic.Core.Ticket ticket, TicketType ticketType, Owner owner) { ticket.TicketTypeID = ticketType.ID; if (!ticket.ValidateTicketTypeAndTransitions()) { Logisense.Boss.Logic.Core.Alert.Create(Logisense.Boss.Logic.Core.TicketTypeTransition.invalidTicketTypeMsg, owner.ID); } else { ticket.RawUpdate(); } } } public class BaseTicketType { protected static string TicketType { get { return "TicketType"; } } public static string TicketTypeForCreateUserPackage { get { return "Ticket Type for Creating a User Package"; } } public static string TicketTypeForCancelUserPackage { get { return "Ticket Type for Cancelling a User Package"; } } protected static void InitializeTicket(UserPackage userPackage, User user, Owner owner, string ticketName, TicketType ticketType) { var ticketCategory = owner.GetTicketCategoryCollection().FirstOrDefault(); var ticketGroup = owner.GetTicketGroupCollection().FirstOrDefault(); var ticketPriority = owner.GetTicketPriorityCollection().FirstOrDefault(); var ticketStatus = owner.GetTicketStatusCollection().FirstOrDefault(); var ticketCategoryId = ticketCategory != null ? ticketCategory.ID : int.MinValue; var ticketGroupId = ticketGroup != null ? ticketGroup.ID : int.MinValue; var ticketPriorityId = ticketPriority != null ? ticketPriority.ID : int.MinValue; var ticketStatusId = ticketStatus != null ? ticketStatus.ID : int.MinValue; var ticket = Ticket.GetNew(); ticket.Name = ticketName; ticket.Title = ticketName; ticket.TicketTypeID = ticketType.ID; ticket.OpenedBy_UserID = user.ID; ticket.RelatedTo_UserID = ticket.OpenedBy_UserID; ticket.TicketCategoryID = ticketCategoryId; ticket.TicketGroupID = ticketGroupId; ticket.TicketPriorityID = ticketPriorityId; ticket.TicketStatusID = ticketStatusId; ticket.UserID = user.ID; ticket.CreatedDate = DateTime.Now; ticket.UserPackageID = userPackage.ID; var coreTicket = (Logisense.Boss.Logic.Core.Ticket)ticket; if (coreTicket.ValidateTicketTypeAndTransitions()) { coreTicket.Create(ticketName, string.Empty, null, false); } else { Logisense.Boss.Logic.Core.Alert.Create(Logisense.Boss.Logic.Core.TicketTypeTransition.invalidTicketTypeMsg, owner.ID); } } protected static PackageAttributeProfileAnswer[] GetPackageAttributeProfileAnswers(UserPackage userPackage) { var packageAttributeProfileAnswers = userPackage.GetPackage().GetPackageAttributeProfileAnswerCollection(); return packageAttributeProfileAnswers; } protected static TicketType GetTicketType(string profileQuestion, IEnumerable<PackageAttributeProfileAnswer> packageAttributeProfileAnswers, Owner owner) { var answer = packageAttributeProfileAnswers.FirstOrDefault(x => x.GetPackageProfileQuestion().GetProfileQuestion().Name == profileQuestion); return answer != null ? owner.GetTicketTypeCollection().FirstOrDefault(x => x.Name == answer.GetProfileAnswer().Value) : default(TicketType); } } public class TicketTypeTransitionInstaller : Logisense.Boss.Logic.Core.ICustomCodeInstaller { public void Install(int ownerId) { var owner = (Logisense.Boss.Logic.Core.Owner)Owner.GetByID(ownerId); SetupReversedTicketTypeTransitions(); SetupPageExtensions(owner); } public static void SetupReversedTicketTypeTransitions() { //This SQL query will only create reversed TicketTypeTransitions that do not already exist (based on Current_TicketStatusID and Allowed_TicketStatusID //appearing in alternate order in a second row for each TicketTypeID). //NOTE: This CustomCode relies on reversed transitions being named [TransitionName] + "_Reversed", reversed transitions missing //"_Reversed" from their names will not be handled correctly and incorrect behaviour of TicketStatuses will be observed. var createdTransitions = SQLHelper.ExecuteDataTable(@" INSERT INTO TicketTypeTransition (Name, Current_TicketStatusID, Allowed_TicketStatusID, TicketGroupID, AssignedTo_UserID, TicketTypeID) OUTPUT INSERTED.ID AS TransitionID, INSERTED.Name AS TransitionName SELECT ttt1.Name + '_Reversed', ttt1.Allowed_TicketStatusID, ttt1.Current_TicketStatusID, ttt3.TicketGroupID, ttt3.AssignedTo_UserID, ttt1.TicketTypeID FROM TicketTypeTransition AS ttt1 LEFT JOIN TicketTypeTransition AS ttt2 ON --Searching for missing reversed transitions ttt2.Allowed_TicketStatusID = ttt1.Current_TicketStatusID AND ttt2.Current_TicketStatusID = ttt1.Allowed_TicketStatusID AND ttt2.TicketTypeID = ttt1.TicketTypeID LEFT JOIN TicketTypeTransition AS ttt3 ON --Searching for transitions previous to the current one, the reversed transitions will use their TicketGroupID/AssignTo_UserID ttt3.Allowed_TicketStatusID = ttt1.Current_TicketStatusID AND ttt3.TicketTypeID = ttt1.TicketTypeID WHERE ttt2.ID IS NULL GROUP BY ttt1.Name, ttt1.Allowed_TicketStatusID, ttt1.Current_TicketStatusID, ttt3.TicketGroupID, ttt3.AssignedTo_UserID, ttt1.TicketTypeID;"); foreach (DataRow transition in createdTransitions.Rows) { Log.Information("Created TicketTypeTransition '{0}' (ID: {1})", transition["TransitionName"], transition["TransitionID"]); } } private static void SetupPageExtensions(Owner owner) { var script = string.Format(@" var ticketId = document.getElementById('Ticket.id').value; setLoadSession(); ByPassIfTicketTypeHasNoTransition(); // Description: if the ticket has ticket type with transition configured, update input values and the logic behind the select button function ByPassIfTicketTypeHasNoTransition(){{ var query = 'AJAXBypassIfTicketTypeHasNoTransition.rails?ticketId=' + ticketId; new Ajax.Request(query, {{ asynchronous: false, evalScripts: true, onSuccess: function (response) {{ if(response.responseJSON.None == false){{ //action starts as Load when the page loads and changes to Previous or Next based on what button was clicked var action = '{2}'; if (sessionStorage.previousTicketTypeTransition == 'true') {{ action = '{0}'; }} else if (sessionStorage.nextTicketTypeTransition == 'true') {{ action = '{1}'; }} //TS-1697 Editing the ticket without clicking the next or previous button shouldn't populate inputs var ajaxUpdateInput = action != '{2}'; updateTicketField(action, 'TicketStatus', ajaxUpdateInput); updateTicketField(action, 'User', ajaxUpdateInput); updateTicketField(action, 'TicketGroup', ajaxUpdateInput); UpdateButton(ajaxUpdateInput); new Form.Element.Observer('Ticket.TicketStatus', 0.100, function (element, value) {{ //These need a separate action since they're changed by the TicketStatus dropdown updateTicketField('{3}', 'User', true); updateTicketField('{3}', 'TicketGroup', true); UpdateButton(false); }}); }} }} }}) }} function updateTicketField(action, field, updateInput) {{ var ticketField = field != 'TicketGroup' ? 'Ticket.' + field : 'TicketGroup.TicketGroup'; if (updateInput === true){{ ajaxUpdateInput(action, ticketField, ticketField); }} if (ticketField == 'Ticket.TicketStatus'){{ jQuery('#TicketStatus_select').attr('href', ""javascript: ajaxLaunchCustomTicketStatusSelectWindow();""); }} }} function UpdateButton(multipleTransitionsDisableButtons) {{ var ticketStatusName = document.getElementById('Ticket.TicketStatus').value; var query = 'AJAXUpdateButton.rails?ticketId=' + ticketId + '&ticketStatusName=' + ticketStatusName; new Ajax.Request(query, {{ asynchronous: false, evalScripts: true, onSuccess: function (response) {{ if ($('nextTicketTypeTransition') != null){{ $('nextTicketTypeTransition').remove(); }} if ($('previousTicketTypeTransition') != null){{ $('previousTicketTypeTransition').remove(); }} if (response){{ clearSession(); if (response.responseJSON.Previous || response.responseJSON.MultiplePrevious && !multipleTransitionsDisableButtons){{ jQuery(""#SaveButton"").before(""<input name='previousTicketTypeTransition' id='previousTicketTypeTransition' type='submit' value='{0}' onclick='setPreviousSession(); return thisCheckvalidation(); '> ""); }} if (response.responseJSON.Next || response.responseJSON.MultipleNext && !multipleTransitionsDisableButtons){{ jQuery(""#SaveButton"").before(""<input name='nextTicketTypeTransition' id='nextTicketTypeTransition' type='submit' value='{1}' onclick='setNextSession(); return thisCheckvalidation(); '> ""); }} }} }} }}); }} function showInvalidError(){{ if ($('errorMsgDiv') != null){{ $('errorMsgDiv').remove(); }} jQuery(""#EditForm"").before(""<div class='error' id='errorMsgDiv'>Multiple valid transitions were found, please choose one status from the drop down list.</div> ""); }} function thisCheckvalidation(){{ return checkvalidation(document.getElementById('EditForm'), 0); }} function setPreviousSession(){{ sessionStorage.previousTicketTypeTransition = true; sessionStorage.removeItem(""nextTicketTypeTransition""); sessionStorage.removeItem(""load""); }} function setNextSession(){{ sessionStorage.nextTicketTypeTransition = true; sessionStorage.removeItem(""previousTicketTypeTransition""); sessionStorage.removeItem(""load""); }} function setLoadSession(){{ if (sessionStorage.previousTicketTypeTransition != 'true' && sessionStorage.nextTicketTypeTransition != 'true') {{ sessionStorage.load = true; }} }} function clearSession(){{ sessionStorage.removeItem(""nextTicketTypeTransition""); sessionStorage.removeItem(""previousTicketTypeTransition""); sessionStorage.removeItem(""load""); }} function ajaxUpdateInput(action, target, selector) {{ var ticketStatusName = document.getElementById('Ticket.TicketStatus').value; var ticketStatusId = document.getElementById('Ticket.TicketStatusID').value; var query = 'AJAXUpdateInput.rails?ticketId=' + ticketId + '&currentTicketStatusId=' + ticketStatusId + '&newTicketStatusName=' + ticketStatusName + '&action=' + action + '&target=' + target; new Ajax.Request(query, {{ asynchronous: true, evalScripts: true, onSuccess: function (response) {{ if (response){{ if (response.responseJSON.Message == '{4}') {{ //if restictive document.getElementById(selector).value = response.responseJSON.Value; }} else if (response.responseJSON.Message == '{5}') {{ //if invalid document.getElementById(selector).value = ''; showInvalidError(); }} }} }} }}); }} // Description: restrict selection lists function ajaxLaunchCustomTicketStatusSelectWindow() {{ //We only create a custom select list for TicketStatus, everything else should use the standard select list var field = 'TicketStatus'; var target = 'Ticket.' + field; var d = document.getElementById(target + 'selectdiv'); var element = document.getElementById(target); d.innerHTML = 'Loading' + '...'; d.style.marginLeft = jQuery('#Ticket\\.TicketStatus').css('margin-left'); d.style.width = element.offsetWidth + 'px'; d.style.display = 'block'; window.currentlyVisiblePopup = d; window.currentlyVisiblePopup.style.visibility = 'visible'; var query = 'AJAXCustomSelect.rails?ticketId=' + ticketId + '&target=' + target; new Ajax.Request(query, {{ asynchronous: true, evalScripts: true, onSuccess: function (response) {{ if (response.responseText == 'no match') {{ // empty the selection d.innerHTML = ''; }} else if (response.responseText == '') {{ // non restricted selection LaunchSelectWindow(field, 'Name', target, '', 0); }} else {{ // restricted selection d.innerHTML = response.responseText; }} }} }}); }}", TransitionAction.Previous, TransitionAction.Next, TransitionAction.Load, TransitionAction.ManualChange, GetFieldMessage.Restrictive, GetFieldMessage.Invalid); const string PAGE_EXTENSION_NAME = "TicketTypeTransitionPageExtension"; try { var pageExtensionQuery = new PageExtensionQuery { OwnerID = owner.ID, Name = PAGE_EXTENSION_NAME }; if (!PageExtension.GetCollection(ref pageExtensionQuery).Any()) { var pageExtension = new Logisense.Boss.Logic.Core.PageExtension() { OwnerID = owner.ID, Name = PAGE_EXTENSION_NAME, Page = "ticket/edit", Script = script }; pageExtension.Create(); } } catch (Exception ex) { throw new ApplicationException(string.Format("Error while creating/updating page extension: '{0}'", PAGE_EXTENSION_NAME), ex); } } } public enum TransitionAction { Previous, Next, Load, ManualChange } public enum GetFieldMessage { NonRestrictive, Restrictive, Invalid } public class AJAXUpdateButton : IDynamicAction { public void Execute(Controller controller) { var ticketId = controller.Params["ticketId"]; var ticketStatusName = controller.Params["ticketStatusName"]; var next = false; var multipleNext = false; var previous = false; var multiplePrevious = false; if (!string.IsNullOrWhiteSpace(ticketId) && ticketId != int.MinValue.ToString()) { var ticket = (Logisense.Boss.Logic.Core.Ticket)Ticket.GetByID(Convert.ToInt32(ticketId)); var ticketTypeTransitions = ticket.GetTicketType().GetTicketTypeTransitionCollection(); var countNextTransitions = ticketTypeTransitions.Count(x => x.GetCurrent_TicketStatus().Name == ticketStatusName && !x.Name.Contains("_Reversed")); var countPreviousTransitions = ticketTypeTransitions.Count(x => x.GetCurrent_TicketStatus().Name == ticketStatusName && x.Name.Contains("_Reversed")); if (countNextTransitions == 1) { next = true; } else if (countNextTransitions > 1) { multipleNext = true; } if (countPreviousTransitions == 1) { previous = true; } else if (countPreviousTransitions > 1) { multiplePrevious = true; } } var json = new JObject { {"Next", next}, {"MultipleNext", multipleNext}, {"Previous", previous}, {"MultiplePrevious", multiplePrevious}, {"None", true} }; controller.Response.ContentType = "application/json"; controller.RenderText(json.ToString()); } } public class AJAXCustomSelect : BaseTicketType, IDynamicAction { public void Execute(Controller controller) { var ticketId = controller.Params["ticketId"]; var noRestriction = false; var results = GetFieldSelection(ticketId, ref noRestriction); if (!results.Any()) { // render "no match": empty the ticket status' selection if no previous or next status has been found // render empty string: load the non restricted selections var noMatch = !noRestriction ? "no match" : string.Empty; controller.RenderText(noMatch); } else { //Target will always be Ticket.TicketStatus var target = controller.Params["target"]; var listResults = new ListResults { objects = results.Values.ToArray(), RowCount = results.Count }; controller.PropertyBag["results"] = listResults; controller.PropertyBag["target"] = target; //Select.vm requires that $table be set, it should be last portion of the target controller.PropertyBag["table"] = target.Split('.').Last(); controller.RenderSharedView("select"); } } public class ListResults { private object[] _objects; public object[] objects { get { return _objects; } set { _objects = value; } } private int _RowCount; public int RowCount { get { return _RowCount; } set { _RowCount = value; } } } /// <summary> /// Get a dictionary of restricted value for ticket field /// </summary> /// <param name="ticketId">string: ID of the ticket to act on</param> /// <param name="noRestriction">ref bool: output true means the ticket type doesn't restrict ticket status</param> /// <returns></returns> private static Dictionary<string, object> GetFieldSelection(string ticketId, ref bool noRestriction) { var ticket = (Logisense.Boss.Logic.Core.Ticket)Ticket.GetByID(Convert.ToInt32(ticketId)); var results = new Dictionary<string, object>(); if (ticket.TicketTypeID != int.MinValue) { var ticketType = ticket.GetTicketType(); var transitionCollection = ticketType.GetTicketTypeTransitionCollection(); if (!transitionCollection.Any()) { noRestriction = true; } else { //Get all transitions that start on the current status to populate the TicketStatus dropdown var possibleTransitions = transitionCollection.Where(x => x.Current_TicketStatusID == ticket.TicketStatusID).ToList(); foreach (var transition in possibleTransitions) { if (transition.Allowed_TicketStatusID != int.MinValue) { var item = transition.GetAllowed_TicketStatus(); AddToDictionary(ref results, item, item.Name); } if (transition.Current_TicketStatusID != int.MinValue) { var item = transition.GetCurrent_TicketStatus(); AddToDictionary(ref results, item, item.Name); } } } } return results; } private static void AddToDictionary(ref Dictionary<string, object> dictionary, object item, string name) { if (item != null & !dictionary.ContainsKey(name)) { dictionary.Add(name, item); } } } public class AJAXUpdateInput : BaseTicketType, IDynamicAction { public void Execute(Controller controller) { var ticketId = controller.Params["ticketId"]; var newTicketStatusName = controller.Params["newTicketStatusName"]; var currentTicketStatusId = controller.Params["currentTicketStatusId"]; var action = controller.Params["action"]; var target = controller.Params["target"]; var fieldValue = GetFieldValue(action, target, ticketId, GetCorrectTicketStatusId(target, currentTicketStatusId, newTicketStatusName)); controller.Response.ContentType = "application/json"; controller.RenderText(fieldValue); } /// <summary> /// Get the value of a ticket field which is restricted by the ticket type transition /// </summary> /// <param name="action">string: "previous" or "next"</param> /// <param name="target">string: the id of the ticket field that needs to be restricted</param> /// <param name="ticketId">string: ID of the ticket to act on</param> /// <param name="ticketStatusId">string: ticketStatusId from the form</param> /// <returns>string: the value of restricted ticket field</returns> private static string GetFieldValue(string action, string target, string ticketId, int ticketStatusId) { var ticket = (Logisense.Boss.Logic.Core.Ticket)Ticket.GetByID(Convert.ToInt32(ticketId)); var returnString = string.Empty; // if returnString is empty, no restriction on this field var jsonMessage = GetFieldMessage.NonRestrictive; if (ticket.TicketTypeID != int.MinValue) { var ticketType = ticket.GetTicketType(); var transitionCollection = ticketType.GetTicketTypeTransitionCollection(); if (transitionCollection.Any()) { var allowedTransitions = ticket.GetNextTicketTypeTransitions(ticket.TicketStatusID, transitionCollection); //ticket.GetNextTicketTypeTransitions() will return both a forward and return transition for the Allowed_TicketStatusID, differentiate using "_Reversed" if (action == TransitionAction.Next.ToString()) { allowedTransitions = allowedTransitions.Where(x => !x.Name.Contains("_Reversed")).ToList(); } else if (action == TransitionAction.Previous.ToString()) { allowedTransitions = allowedTransitions.Where(x => x.Name.Contains("_Reversed")).ToList(); } else if (action == TransitionAction.ManualChange.ToString()) { //This will only happen when the form observer on TicketStatus triggers an update for User and TicketGroup allowedTransitions = allowedTransitions.Where(x => x.Allowed_TicketStatusID == ticketStatusId).ToList(); if (ticketStatusId == ticket.TicketStatusID) { //TicketStatus was moved back to the original value, since TicketGroup and User are //associated to the allowed TicketStatus just grab the first transition that matches allowedTransitions = transitionCollection.Where(x => x.Allowed_TicketStatusID == ticketStatusId).Take(1).ToList(); } } var firstPageLoad = action == TransitionAction.Load.ToString(); if (allowedTransitions.Count == 1 || firstPageLoad) { //If the page is being loaded for the first time we don't care what transitions exist just yet, we're going to load in ticket details instead var ticketTransition = !firstPageLoad ? allowedTransitions.Single() : default(TicketTypeTransition); // get field value switch (target) { case "Ticket.User": if (firstPageLoad) { returnString = ticket.GetUser().Name; } else if (ticketTransition != null && ticketTransition.AssignedTo_UserID != int.MinValue) { returnString = ticketTransition.GetAssignedTo_User().Name; } break; case "Ticket.TicketStatus": if (firstPageLoad) { returnString = ticket.GetTicketStatus().Name; } else if (ticketTransition != null) { //If the TicketStatusID from the form doesn't match the ticket it means the form has already transitioned forward, //don't update the TicketStatus returnString = ticket.TicketStatusID != ticketStatusId ? ticketTransition.GetCurrent_TicketStatus().Name : ticketTransition.GetAllowed_TicketStatus().Name; } break; case "TicketGroup.TicketGroup": if (firstPageLoad) { returnString = ViewTicketGroupWithOwner.GetByID(ticket.TicketGroupID).Name; } else if (ticketTransition != null && ticketTransition.TicketGroupID != int.MinValue) { returnString = ViewTicketGroupWithOwner.GetByID(ticketTransition.TicketGroupID).Name; } break; default: returnString = string.Empty; break; } if (returnString != string.Empty) { jsonMessage = GetFieldMessage.Restrictive; } } else if (allowedTransitions.Count > 1) { jsonMessage = GetFieldMessage.Invalid; } } } var json = new JObject { { "Message", jsonMessage.ToString() }, { "Value", returnString } }; return json.ToString(); } private static int GetCorrectTicketStatusId(string target, string currentTicketStatusId, string newTicketStatusName) { var ticketStatusId = Convert.ToInt32(currentTicketStatusId); if (ticketStatusId > 0 && target != "Ticket.TicketStatus") { //TicketStatus requires currentTicketStatusId (related to ticket), but TicketGroup and User require newTicketStatusName (current value in form) TicketStatus ticketStatus; try { ticketStatus = TicketStatus.GetByID(ticketStatusId); } catch (RecordNotFoundException) { //Couldn't find a TicketStatus for the passed in ID, all we can really do is pass back the original ID return ticketStatusId; } var ticketStatusQuery = new TicketStatusQuery { OwnerID = ticketStatus.OwnerID, Name = newTicketStatusName }; ticketStatus = TicketStatus.GetCollection(ref ticketStatusQuery).SingleOrDefault(); if (ticketStatus != null) { ticketStatusId = ticketStatus.ID; } } return ticketStatusId; } } public class AJAXBypassIfTicketTypeHasNoTransition : IDynamicAction { public void Execute(Controller controller) { var ticketId = controller.Params["ticketId"]; var none = true; if (!string.IsNullOrWhiteSpace(ticketId) && ticketId != int.MinValue.ToString()) { var ticket = Ticket.GetByID(Convert.ToInt32(ticketId)); if (ticket.TicketTypeID != int.MinValue) { var ticketTypeTransitions = ticket.GetTicketType().GetTicketTypeTransitionCollection(); if (ticketTypeTransitions.Any()) { none = false; } } } var json = new JObject { { "None", none } }; controller.Response.ContentType = "application/json"; controller.RenderText(json.ToString()); } } }
  5. Click Save


Next configure Ticket Types, Categories and Transitions:

  1. In the AdminPortal load the Setup page

  2. Click on Ticket Types

  3. Click Add to create a new type

  4. Enter a Name for the Ticket Type (for example: New Order)

  5. Click Save

  6. On the Ticket Types page click on the Name of your new Type to edit it

  7. Under the Categories heading click Add

  8. Specify a Ticket Category. If no categories exist click the + icon to the right of this field to create them. If you are unfamiliar with ticket categories see the Ticket Overview article

  9. On the edit Ticket Type page and Transitions, under Transitions click on Add

  10. Enter a Name for the first stage of in the process flow (for example, "New Order").  Required fields are in red:

    1. Select a Current Ticket Status, if you need to add new Statuses for Ticket Transition functionality click the + to the right of this field
      Note: For ease of use it is best to use the "Open" ticket status for the first transition in the process flow. For transitions to function a ticket must be created with both the proper Ticket Type field value populated (matching a Ticket Type you have setup) and the proper Status field value populated (matching this Transition > Current Ticket Status configuration). By setting the Current Ticket Status here to "Open" you will not require the user to change the Status field when creating a new ticket in EngageIP as the default status is "Open" on ticket creation

    2. In the Allowed Ticket Status list select the status that the Current Ticket Status above is permitted to transition to (for example if a Current Ticket Status of "New Order" is permitted to transition to the next phase status of "Provisioning", you would setup a Provisioning status and add it under this Allowed Ticket Status field). This field is used to chain the ticket transitions together so that you can migrate tickets backward and forward through the stages of a process

    3. Optional: Select the Ticket Group that the ticket will be assigned to when the ticket transitions into the next status (i.e. the Allowed Ticket Status). If you leave this blank the 'Ticket Group' field will maintain it's current value / the value assigned to it from the previous transition

    4. Optional: Select the Assigned To User the ticket will be assigned to when the ticket transitions into the next status (i.e. the Allowed Ticket Status). If you leave this blank the 'Assigned To User' field will maintain it's current value / the value assigned to it from the previous transition

    5. Optional: you can trigger an Action when the user clicks the Next button to transition a ticket to the next status. For example you can add a Set User Status: 'Enabled' at the end of Order Completion, or to Send Email if you want to notify the customer that an activity is completed/scheduled (e.g. notify the customer their account is now activated, or provide an email confirmation of a verbally communicated service installation date). In addition to the stock actions in EngageIP you can add Actions (found on the Setup page / Actions) by creating your own scripts, using scripts on this Knowledge Base (see the Action Scripts article) or contacting your LogiSense account Representative to discuss what Action script you need us to create for you

  11. Once you have configured the Transition details click Save and repeat the process above to add new transitions until you have all the stages of the process captured

  12. Click Save on the Ticket Type add window when you are finished defining your transition procedure

Sample Configuration

When Transition configuration is complete you can use the 'Transitions' section on your newly created Ticket Type to quickly reference how the Transitions will operate.

Following the configuration above, if you opened a ticket which has it's Ticket Type field set as "New Order" and it's Status field set as "Open", clicking the Next button would transition (update) the ticket as follows:

  1. The ticket Status 'Open' would change to 'New Order Provisioning'

  2. The Ticket Group would change from it's current assigned group to 'Provisioning'

  3. The 'Assigned to User' would remain the same as it was set to on the ticket (or remain empty if it was never set)

  4. No Action would be executed

  5. Click Next on the ticket again and the following transition will be applied: Status: New Order Provisioning >> New Order Installation, Ticket Group: Provisioning >> Installer, Assigned To User: remains the same, Action: no action triggered

Usage

To test transitions do the following (ideally in a test environment):

  1. Load a test account in the AdminPortal and click on Tickets

  2. Add a test ticket

  3. Select the Ticket Type you setup and set the Status field to the status you configured for your first transition under the Ticket Type (the 'Current Ticket Status' value)

  4. Ensure the Category is set/limited to the Ticket Type Categories you setup

  5. Set the ticket Priority and enter a comment

  6. Click Save

  7. Click on the Ticket number to open it back up

  8. When you scroll down you should now see a Next button next to Save

  9. Click Next and ensure the Ticket Status, Ticket Group and Assigned To match the transition configuration you have set

  10. You will now see a Previous button, click Next to ensure all your transitions work to the last transition, and then Previous to ensure you can progress backwards as well

  11. As you click next and previous ticket changes will be noted in the Ticket History

Troubleshooting

Error running action 'xxxxxxxxxx' for transition 'xxxxxxxx': Script Engine Execution Exception, Index was outside the bounds of the array

This indicates that the value you entered for an action on a transition is invalid, check the value entered and ensure it is correct (no typo's, the value exists in EngageIP).