diff options
Diffstat (limited to 'www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCaseType.js')
-rw-r--r-- | www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCaseType.js | 648 |
1 files changed, 648 insertions, 0 deletions
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCaseType.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCaseType.js new file mode 100644 index 00000000..a1ff51ae --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCaseType.js @@ -0,0 +1,648 @@ +(function(angular, $, _) { + + var crmCaseType = angular.module('crmCaseType', CRM.angRequires('crmCaseType')); + + // Note: This template will be passed to cloneDeep(), so don't put any funny stuff in here! + var newCaseTypeTemplate = { + title: "", + name: "", + is_active: "1", + weight: "1", + definition: { + activityTypes: [ + {name: 'Open Case', max_instances: 1}, + {name: 'Email'}, + {name: 'Follow up'}, + {name: 'Meeting'}, + {name: 'Phone Call'} + ], + activitySets: [ + { + name: 'standard_timeline', + label: 'Standard Timeline', + timeline: '1', // Angular won't bind checkbox correctly with numeric 1 + activityTypes: [ + {name: 'Open Case', status: 'Completed' } + ] + } + ], + caseRoles: [ + { name: 'Case Coordinator', creator: '1', manager: '1'} + ] + } + }; + + crmCaseType.config(['$routeProvider', + function($routeProvider) { + $routeProvider.when('/caseType', { + templateUrl: '~/crmCaseType/list.html', + controller: 'CaseTypeListCtrl', + resolve: { + caseTypes: function($route, crmApi) { + return crmApi('CaseType', 'get', {options: {limit: 0}}); + } + } + }); + $routeProvider.when('/caseType/:id', { + templateUrl: '~/crmCaseType/edit.html', + controller: 'CaseTypeCtrl', + resolve: { + apiCalls: function($route, crmApi) { + var reqs = {}; + reqs.actStatuses = ['OptionValue', 'get', { + option_group_id: 'activity_status', + sequential: 1, + options: {limit: 0} + }]; + reqs.caseStatuses = ['OptionValue', 'get', { + option_group_id: 'case_status', + sequential: 1, + options: {limit: 0} + }]; + reqs.actTypes = ['OptionValue', 'get', { + option_group_id: 'activity_type', + sequential: 1, + options: { + sort: 'name', + limit: 0 + } + }]; + reqs.defaultAssigneeTypes = ['OptionValue', 'get', { + option_group_id: 'activity_default_assignee', + sequential: 1, + options: { + limit: 0 + } + }]; + reqs.relTypes = ['RelationshipType', 'get', { + sequential: 1, + is_active: 1, + options: { + sort: 'label_a_b', + limit: 0 + } + }]; + if ($route.current.params.id !== 'new') { + reqs.caseType = ['CaseType', 'getsingle', { + id: $route.current.params.id + }]; + } + return crmApi(reqs); + } + } + }); + } + ]); + + // Add a new record by name. + // Ex: <crmAddName crm-options="['Alpha','Beta','Gamma']" crm-var="newItem" crm-on-add="callMyCreateFunction(newItem)" /> + crmCaseType.directive('crmAddName', function() { + return { + restrict: 'AE', + template: '<input class="add-activity crm-action-menu fa-plus" type="hidden" />', + link: function(scope, element, attrs) { + + var input = $('input', element); + + scope._resetSelection = function() { + $(input).select2('close'); + $(input).select2('val', ''); + scope[attrs.crmVar] = ''; + }; + + $(input).crmSelect2({ + data: function () { + return { results: scope[attrs.crmOptions] }; + }, + createSearchChoice: function(term) { + return {id: term, text: term + ' (' + ts('new') + ')'}; + }, + createSearchChoicePosition: 'bottom', + placeholder: attrs.placeholder + }); + $(input).on('select2-selecting', function(e) { + scope[attrs.crmVar] = e.val; + scope.$evalAsync(attrs.crmOnAdd); + scope.$evalAsync('_resetSelection()'); + e.preventDefault(); + }); + } + }; + }); + + crmCaseType.directive('crmEditableTabTitle', function($timeout) { + return { + restrict: 'AE', + link: function(scope, element, attrs) { + element.addClass('crm-editable crm-editable-enabled'); + var titleLabel = $(element).find('span'); + var penIcon = $('<i class="crm-i fa-pencil crm-editable-placeholder"></i>').prependTo(element); + var saveButton = $('<button type="button"><i class="crm-i fa-check"></i></button>').appendTo(element); + var cancelButton = $('<button type="cancel"><i class="crm-i fa-times"></i></button>').appendTo(element); + $('button', element).wrapAll('<div class="crm-editable-form" style="display:none" />'); + var buttons = $('.crm-editable-form', element); + titleLabel.on('click', startEditMode); + penIcon.on('click', startEditMode); + + function detectEscapeKeyPress (event) { + var isEscape = false; + + if ("key" in event) { + isEscape = (event.key == "Escape" || event.key == "Esc"); + } else { + isEscape = (event.keyCode == 27); + } + + return isEscape; + } + + function detectEnterKeyPress (event) { + var isEnter = false; + + if ("key" in event) { + isEnter = (event.key == "Enter"); + } else { + isEnter = (event.keyCode == 13); + } + + return isEnter; + } + + function startEditMode () { + if (titleLabel.is(":focus")) { + return; + } + + penIcon.hide(); + buttons.show(); + + saveButton.click(function () { + updateTextValue(); + stopEditMode(); + }); + + cancelButton.click(function () { + revertTextValue(); + stopEditMode(); + }); + + $(element).addClass('crm-editable-editing'); + + titleLabel + .attr("contenteditable", "true") + .focus() + .focusout(function (event) { + $timeout(function () { + revertTextValue(); + stopEditMode(); + }, 500); + }) + .keydown(function(event) { + event.stopImmediatePropagation(); + + if(detectEscapeKeyPress(event)) { + revertTextValue(); + stopEditMode(); + } else if(detectEnterKeyPress(event)) { + event.preventDefault(); + updateTextValue(); + stopEditMode(); + } + }); + } + + function stopEditMode () { + titleLabel.removeAttr("contenteditable").off("focusout"); + titleLabel.off("keydown"); + saveButton.off("click"); + cancelButton.off("click"); + $(element).removeClass('crm-editable-editing'); + + penIcon.show(); + buttons.hide(); + } + + function revertTextValue () { + titleLabel.text(scope.activitySet.label); + } + + function updateTextValue () { + var updatedTitle = titleLabel.text(); + + scope.$evalAsync(function () { + scope.activitySet.label = updatedTitle; + }); + } + } + }; + }); + + crmCaseType.controller('CaseTypeCtrl', function($scope, crmApi, apiCalls, crmUiHelp) { + var defaultAssigneeDefaultValue, ts; + + (function init () { + + ts = $scope.ts = CRM.ts(null); + $scope.hs = crmUiHelp({file: 'CRM/Case/CaseType'}); + $scope.locks = { caseTypeName: true, activitySetName: true }; + $scope.workflows = { timeline: 'Timeline', sequence: 'Sequence' }; + defaultAssigneeDefaultValue = _.find(apiCalls.defaultAssigneeTypes.values, { is_default: '1' }) || {}; + + storeApiCallsResults(); + initCaseType(); + initCaseTypeDefinition(); + initSelectedStatuses(); + })(); + + /// Stores the api calls results in the $scope object + function storeApiCallsResults() { + $scope.activityStatuses = apiCalls.actStatuses.values; + $scope.caseStatuses = _.indexBy(apiCalls.caseStatuses.values, 'name'); + $scope.activityTypes = _.indexBy(apiCalls.actTypes.values, 'name'); + $scope.activityTypeOptions = _.map(apiCalls.actTypes.values, formatActivityTypeOption); + $scope.defaultAssigneeTypes = apiCalls.defaultAssigneeTypes.values; + $scope.relationshipTypeOptions = getRelationshipTypeOptions(false); + $scope.defaultRelationshipTypeOptions = getRelationshipTypeOptions(true); + // stores the default assignee values indexed by their option name: + $scope.defaultAssigneeTypeValues = _.chain($scope.defaultAssigneeTypes) + .indexBy('name').mapValues('value').value(); + } + + // Returns the relationship type options. If the relationship is + // bidirectional (Ex: Spouse of) it adds a single option otherwise it adds + // two options representing the relationship type directions (Ex: Employee + // of, Employer of). + // + // The default relationship field needs values that are IDs with direction, + // while the role field needs values that are names (with implicit + // direction). + // + // At any rate, the labels should follow the convention in the UI of + // describing case roles from the perspective of the client, while the + // values must follow the convention in the XML of describing case roles + // from the perspective of the non-client. + function getRelationshipTypeOptions($isDefault) { + return _.transform(apiCalls.relTypes.values, function(result, relType) { + var isBidirectionalRelationship = relType.label_a_b === relType.label_b_a; + if ($isDefault) { + result.push({ + label: relType.label_b_a, + value: relType.id + '_a_b' + }); + + if (!isBidirectionalRelationship) { + result.push({ + label: relType.label_a_b, + value: relType.id + '_b_a' + }); + } + } + // TODO The ids below really should use names not labels see + // https://lab.civicrm.org/dev/core/issues/774 + else { + result.push({ + text: relType.label_b_a, + id: relType.label_a_b + }); + + if (!isBidirectionalRelationship) { + result.push({ + text: relType.label_a_b, + id: relType.label_b_a + }); + } + } + }, []); + } + + /// initializes the case type object + function initCaseType() { + var isNewCaseType = !apiCalls.caseType; + + if (isNewCaseType) { + $scope.caseType = _.cloneDeep(newCaseTypeTemplate); + } else { + $scope.caseType = apiCalls.caseType; + } + } + + /// initializes the case type definition object + function initCaseTypeDefinition() { + $scope.caseType.definition = $scope.caseType.definition || []; + $scope.caseType.definition.activityTypes = $scope.caseType.definition.activityTypes || []; + $scope.caseType.definition.activitySets = $scope.caseType.definition.activitySets || []; + $scope.caseType.definition.caseRoles = $scope.caseType.definition.caseRoles || []; + $scope.caseType.definition.statuses = $scope.caseType.definition.statuses || []; + $scope.caseType.definition.timelineActivityTypes = $scope.caseType.definition.timelineActivityTypes || []; + $scope.caseType.definition.restrictActivityAsgmtToCmsUser = $scope.caseType.definition.restrictActivityAsgmtToCmsUser || 0; + $scope.caseType.definition.activityAsgmtGrps = $scope.caseType.definition.activityAsgmtGrps || []; + + _.each($scope.caseType.definition.activitySets, function (set) { + _.each(set.activityTypes, function (type, name) { + var isDefaultAssigneeTypeUndefined = _.isUndefined(type.default_assignee_type); + var typeDefinition = $scope.activityTypes[type.name]; + type.label = (typeDefinition && typeDefinition.label) || type.name; + + if (isDefaultAssigneeTypeUndefined) { + type.default_assignee_type = defaultAssigneeDefaultValue.value; + } + }); + }); + + // go lookup and add client-perspective labels for $scope.caseType.definition.caseRoles + _.each($scope.caseType.definition.caseRoles, function (set) { + _.each($scope.relationshipTypeOptions, function (relTypes) { + if (relTypes.text == set.name) { + set.displaylabel = relTypes.id; + } + }); + }); + } + + /// initializes the selected statuses + function initSelectedStatuses() { + $scope.selectedStatuses = {}; + + _.each(apiCalls.caseStatuses.values, function (status) { + $scope.selectedStatuses[status.name] = !$scope.caseType.definition.statuses.length || $scope.caseType.definition.statuses.indexOf(status.name) > -1; + }); + } + + $scope.addActivitySet = function(workflow) { + var activitySet = {}; + activitySet[workflow] = '1'; + activitySet.activityTypes = []; + + var offset = 1; + var names = _.pluck($scope.caseType.definition.activitySets, 'name'); + while (_.contains(names, workflow + '_' + offset)) offset++; + activitySet.name = workflow + '_' + offset; + activitySet.label = (offset == 1 ) ? $scope.workflows[workflow] : ($scope.workflows[workflow] + ' #' + offset); + + $scope.caseType.definition.activitySets.push(activitySet); + _.defer(function() { + $('.crmCaseType-acttab').tabs('refresh').tabs({active: -1}); + }); + }; + + function formatActivityTypeOption(type) { + return {id: type.name, text: type.label, icon: type.icon}; + } + + function addActivityToSet(activitySet, activityTypeName) { + activitySet.activityTypes = activitySet.activityTypes || []; + var activity = { + name: activityTypeName, + label: $scope.activityTypes[activityTypeName].label, + status: 'Scheduled', + reference_activity: 'Open Case', + reference_offset: '1', + reference_select: 'newest', + default_assignee_type: $scope.defaultAssigneeTypeValues.NONE + }; + activitySet.activityTypes.push(activity); + if(typeof activitySet.timeline !== "undefined" && activitySet.timeline == "1") { + $scope.caseType.definition.timelineActivityTypes.push(activity); + } + } + + function resetTimelineActivityTypes() { + $scope.caseType.definition.timelineActivityTypes = []; + angular.forEach($scope.caseType.definition.activitySets, function(activitySet) { + angular.forEach(activitySet.activityTypes, function(activityType) { + $scope.caseType.definition.timelineActivityTypes.push(activityType); + }); + }); + } + + function createActivity(name, callback) { + CRM.loadForm(CRM.url('civicrm/admin/options/activity_type', {action: 'add', reset: 1, label: name, component_id: 7})) + .on('crmFormSuccess', function(e, data) { + $scope.activityTypes[data.optionValue.name] = data.optionValue; + $scope.activityTypeOptions.push(formatActivityTypeOption(data.optionValue)); + callback(data.optionValue); + $scope.$digest(); + }); + } + + // Add a new activity entry to an activity-set + $scope.addActivity = function(activitySet, activityType) { + if ($scope.activityTypes[activityType]) { + addActivityToSet(activitySet, activityType); + } else { + createActivity(activityType, function(newActivity) { + addActivityToSet(activitySet, newActivity.name); + }); + } + }; + + /// Add a new top-level activity-type entry + $scope.addActivityType = function(activityType) { + var names = _.pluck($scope.caseType.definition.activityTypes, 'name'); + if (!_.contains(names, activityType)) { + // Add an activity type that exists + if ($scope.activityTypes[activityType]) { + $scope.caseType.definition.activityTypes.push({name: activityType}); + } else { + createActivity(activityType, function(newActivity) { + $scope.caseType.definition.activityTypes.push({name: newActivity.name}); + }); + } + } + }; + + /// Clears the activity's default assignee values for relationship and contact + $scope.clearActivityDefaultAssigneeValues = function(activity) { + activity.default_assignee_relationship = null; + activity.default_assignee_contact = null; + }; + + // TODO roleName passed to addRole is a misnomer, its passed as the + // label HOWEVER it should be saved to xml as the name see + // https://lab.civicrm.org/dev/core/issues/774 + + /// Add a new role + $scope.addRole = function(roles, roleName) { + var names = _.pluck($scope.caseType.definition.caseRoles, 'name'); + if (!_.contains(names, roleName)) { + var matchingRoles = _.filter($scope.relationshipTypeOptions, {id: roleName}); + if (matchingRoles.length) { + var matchingRole = matchingRoles.shift(); + roles.push({name: roleName, displaylabel: matchingRole.text}); + } else { + CRM.loadForm(CRM.url('civicrm/admin/reltype', {action: 'add', reset: 1, label_a_b: roleName})) + .on('crmFormSuccess', function(e, data) { + var newType = _.values(data.relationshipType)[0]; + roles.push({name: newType.label_b_a, displaylabel: newType.label_a_b}); + // Assume that the case role should be A-B but add both directions as options. + $scope.relationshipTypeOptions.push({id: newType.label_a_b, text: newType.label_a_b}); + if (newType.label_a_b != newType.label_b_a) { + $scope.relationshipTypeOptions.push({id: newType.label_b_a, text: newType.label_b_a}); + } + $scope.$digest(); + }); + } + } + }; + + $scope.onManagerChange = function(managerRole) { + angular.forEach($scope.caseType.definition.caseRoles, function(caseRole) { + if (caseRole != managerRole) { + caseRole.manager = '0'; + } + }); + }; + + $scope.removeItem = function(array, item) { + var idx = _.indexOf(array, item); + if (idx != -1) { + array.splice(idx, 1); + resetTimelineActivityTypes(); + } + }; + + $scope.isForkable = function() { + return !$scope.caseType.id || $scope.caseType.is_forkable; + }; + + $scope.newStatus = function() { + CRM.loadForm(CRM.url('civicrm/admin/options/case_status', {action: 'add', reset: 1})) + .on('crmFormSuccess', function(e, data) { + $scope.caseStatuses[data.optionValue.name] = data.optionValue; + $scope.selectedStatuses[data.optionValue.name] = true; + $scope.$digest(); + }); + }; + + $scope.isNewActivitySetAllowed = function(workflow) { + switch (workflow) { + case 'timeline': + return true; + case 'sequence': + return 0 === _.where($scope.caseType.definition.activitySets, {sequence: '1'}).length; + default: + CRM.console('warn', 'Denied access to unrecognized workflow: (' + workflow + ')'); + return false; + } + }; + + $scope.isActivityRemovable = function(activitySet, activity) { + return true; + }; + + $scope.isValidName = function(name) { + return !name || name.match(/^[a-zA-Z0-9_]+$/); + }; + + $scope.getWorkflowName = function(activitySet) { + var result = 'Unknown'; + _.each($scope.workflows, function(value, key) { + if (activitySet[key]) result = value; + }); + return result; + }; + + /** + * Determine which HTML partial to use for a particular + * + * @return string URL of the HTML partial + */ + $scope.activityTableTemplate = function(activitySet) { + if (activitySet.timeline) { + return '~/crmCaseType/timelineTable.html'; + } else if (activitySet.sequence) { + return '~/crmCaseType/sequenceTable.html'; + } else { + return ''; + } + }; + + $scope.dump = function() { + console.log($scope.caseType); + }; + + $scope.save = function() { + // Add selected statuses + var selectedStatuses = []; + _.each($scope.selectedStatuses, function(v, k) { + if (v) selectedStatuses.push(k); + }); + // Ignore if ALL or NONE selected + $scope.caseType.definition.statuses = selectedStatuses.length == _.size($scope.selectedStatuses) ? [] : selectedStatuses; + + if ($scope.caseType.definition.activityAsgmtGrps) { + $scope.caseType.definition.activityAsgmtGrps = $scope.caseType.definition.activityAsgmtGrps.toString().split(","); + } + + function dropDisplaylabel (v) { + delete v.displaylabel; + } + + // strip out labels from $scope.caseType.definition.caseRoles + _.map($scope.caseType.definition.caseRoles, dropDisplaylabel); + + var result = crmApi('CaseType', 'create', $scope.caseType, true); + result.then(function(data) { + if (data.is_error === 0 || data.is_error == '0') { + $scope.caseType.id = data.id; + window.location.href = '#/caseType'; + } + }); + }; + + $scope.$watchCollection('caseType.definition.activitySets', function() { + _.defer(function() { + $('.crmCaseType-acttab').tabs('refresh'); + }); + }); + + var updateCaseTypeName = function () { + if (!$scope.caseType.id && $scope.locks.caseTypeName) { + // Should we do some filtering? Lowercase? Strip whitespace? + var t = $scope.caseType.title ? $scope.caseType.title : ''; + $scope.caseType.name = t.replace(/ /g, '_').replace(/[^a-zA-Z0-9_]/g, '').toLowerCase(); + } + }; + $scope.$watch('locks.caseTypeName', updateCaseTypeName); + $scope.$watch('caseType.title', updateCaseTypeName); + + if (!$scope.isForkable()) { + CRM.alert(ts('The CiviCase XML file for this case-type prohibits editing the definition.')); + } + + }); + + crmCaseType.controller('CaseTypeListCtrl', function($scope, crmApi, caseTypes) { + var ts = $scope.ts = CRM.ts(null); + + $scope.caseTypes = caseTypes.values; + $scope.toggleCaseType = function (caseType) { + caseType.is_active = (caseType.is_active == '1') ? '0' : '1'; + crmApi('CaseType', 'create', caseType, true) + .catch(function (data) { + caseType.is_active = (caseType.is_active == '1') ? '0' : '1'; // revert + $scope.$digest(); + }); + }; + $scope.deleteCaseType = function (caseType) { + crmApi('CaseType', 'delete', {id: caseType.id}, { + error: function (data) { + CRM.alert(data.error_message, ts('Error'), 'error'); + } + }) + .then(function (data) { + delete caseTypes.values[caseType.id]; + }); + }; + $scope.revertCaseType = function (caseType) { + caseType.definition = 'null'; + caseType.is_forked = '0'; + crmApi('CaseType', 'create', caseType, true) + .catch(function (data) { + caseType.is_forked = '1'; // restore + $scope.$digest(); + }); + }; + }); + +})(angular, CRM.$, CRM._); |