1 (function(angular
, $, _
) {
3 var crmCaseType
= angular
.module('crmCaseType', CRM
.angRequires('crmCaseType'));
5 // Note: This template will be passed to cloneDeep(), so don't put any funny stuff in here!
6 var newCaseTypeTemplate
= {
13 {name
: 'Open Case', max_instances
: 1},
21 name
: 'standard_timeline',
22 label
: 'Standard Timeline',
23 timeline
: '1', // Angular won't bind checkbox correctly with numeric 1
25 {name
: 'Open Case', status
: 'Completed' }
30 { name
: 'Case Coordinator', creator
: '1', manager
: '1'}
35 crmCaseType
.config(['$routeProvider',
36 function($routeProvider
) {
37 $routeProvider
.when('/caseType', {
38 templateUrl
: '~/crmCaseType/list.html',
39 controller
: 'CaseTypeListCtrl',
41 caseTypes: function($route
, crmApi
) {
42 return crmApi('CaseType', 'get', {options
: {limit
: 0}});
46 $routeProvider
.when('/caseType/:id', {
47 templateUrl
: '~/crmCaseType/edit.html',
48 controller
: 'CaseTypeCtrl',
50 apiCalls: function($route
, crmApi
) {
52 reqs
.actStatuses
= ['OptionValue', 'get', {
53 option_group_id
: 'activity_status',
57 reqs
.caseStatuses
= ['OptionValue', 'get', {
58 option_group_id
: 'case_status',
65 reqs
.actTypes
= ['OptionValue', 'get', {
66 option_group_id
: 'activity_type',
73 reqs
.defaultAssigneeTypes
= ['OptionValue', 'get', {
74 option_group_id
: 'activity_default_assignee',
80 reqs
.relTypes
= ['RelationshipType', 'get', {
88 if ($route
.current
.params
.id
!== 'new') {
89 reqs
.caseType
= ['CaseType', 'getsingle', {
90 id
: $route
.current
.params
.id
100 // Add a new record by name.
101 // Ex: <crmAddName crm-options="['Alpha','Beta','Gamma']" crm-var="newItem" crm-on-add="callMyCreateFunction(newItem)" />
102 crmCaseType
.directive('crmAddName', function() {
105 template
: '<input class="add-activity crm-action-menu fa-plus" type="hidden" />',
106 link: function(scope
, element
, attrs
) {
108 var input
= $('input', element
);
110 scope
._resetSelection = function() {
111 $(input
).select2('close');
112 $(input
).select2('val', '');
113 scope
[attrs
.crmVar
] = '';
116 $(input
).crmSelect2({
118 return { results
: scope
[attrs
.crmOptions
] };
120 createSearchChoice: function(term
) {
121 return {id
: term
, text
: term
+ ' (' + ts('new') + ')'};
123 createSearchChoicePosition
: 'bottom',
124 placeholder
: attrs
.placeholder
126 $(input
).on('select2-selecting', function(e
) {
127 scope
[attrs
.crmVar
] = e
.val
;
128 scope
.$evalAsync(attrs
.crmOnAdd
);
129 scope
.$evalAsync('_resetSelection()');
136 crmCaseType
.directive('crmEditableTabTitle', function($timeout
) {
139 link: function(scope
, element
, attrs
) {
140 element
.addClass('crm-editable crm-editable-enabled');
141 var titleLabel
= $(element
).find('span');
142 var penIcon
= $('<i class="crm-i fa-pencil crm-editable-placeholder"></i>').prependTo(element
);
143 var saveButton
= $('<button type="button"><i class="crm-i fa-check"></i></button>').appendTo(element
);
144 var cancelButton
= $('<button type="cancel"><i class="crm-i fa-times"></i></button>').appendTo(element
);
145 $('button', element
).wrapAll('<div class="crm-editable-form" style="display:none" />');
146 var buttons
= $('.crm-editable-form', element
);
147 titleLabel
.on('click', startEditMode
);
148 penIcon
.on('click', startEditMode
);
150 function detectEscapeKeyPress (event
) {
151 var isEscape
= false;
153 if ("key" in event
) {
154 isEscape
= (event
.key
== "Escape" || event
.key
== "Esc");
156 isEscape
= (event
.keyCode
== 27);
162 function detectEnterKeyPress (event
) {
165 if ("key" in event
) {
166 isEnter
= (event
.key
== "Enter");
168 isEnter
= (event
.keyCode
== 13);
174 function startEditMode () {
175 if (titleLabel
.is(":focus")) {
182 saveButton
.click(function () {
187 cancelButton
.click(function () {
192 $(element
).addClass('crm-editable-editing');
195 .attr("contenteditable", "true")
197 .focusout(function (event
) {
198 $timeout(function () {
203 .keydown(function(event
) {
204 event
.stopImmediatePropagation();
206 if(detectEscapeKeyPress(event
)) {
209 } else if(detectEnterKeyPress(event
)) {
210 event
.preventDefault();
217 function stopEditMode () {
218 titleLabel
.removeAttr("contenteditable").off("focusout");
219 titleLabel
.off("keydown");
220 saveButton
.off("click");
221 cancelButton
.off("click");
222 $(element
).removeClass('crm-editable-editing');
228 function revertTextValue () {
229 titleLabel
.text(scope
.activitySet
.label
);
232 function updateTextValue () {
233 var updatedTitle
= titleLabel
.text();
235 scope
.$evalAsync(function () {
236 scope
.activitySet
.label
= updatedTitle
;
243 crmCaseType
.controller('CaseTypeCtrl', function($scope
, crmApi
, apiCalls
, crmUiHelp
) {
244 var defaultAssigneeDefaultValue
, ts
;
248 ts
= $scope
.ts
= CRM
.ts(null);
249 $scope
.hs
= crmUiHelp({file
: 'CRM/Case/CaseType'});
250 $scope
.locks
= { caseTypeName
: true, activitySetName
: true };
251 $scope
.workflows
= { timeline
: 'Timeline', sequence
: 'Sequence' };
252 defaultAssigneeDefaultValue
= _
.find(apiCalls
.defaultAssigneeTypes
.values
, { is_default
: '1' }) || {};
254 storeApiCallsResults();
256 initCaseTypeDefinition();
257 initSelectedStatuses();
260 /// Stores the api calls results in the $scope object
261 function storeApiCallsResults() {
262 $scope
.activityStatuses
= apiCalls
.actStatuses
.values
;
263 $scope
.caseStatuses
= _
.indexBy(apiCalls
.caseStatuses
.values
, 'name');
264 $scope
.activityTypes
= _
.indexBy(apiCalls
.actTypes
.values
, 'name');
265 $scope
.activityTypeOptions
= _
.map(apiCalls
.actTypes
.values
, formatActivityTypeOption
);
266 $scope
.defaultAssigneeTypes
= apiCalls
.defaultAssigneeTypes
.values
;
267 $scope
.relationshipTypeOptions
= getRelationshipTypeOptions(false);
268 $scope
.defaultRelationshipTypeOptions
= getRelationshipTypeOptions(true);
269 // stores the default assignee values indexed by their option name:
270 $scope
.defaultAssigneeTypeValues
= _
.chain($scope
.defaultAssigneeTypes
)
271 .indexBy('name').mapValues('value').value();
274 // Returns the relationship type options. If the relationship is
275 // bidirectional (Ex: Spouse of) it adds a single option otherwise it adds
276 // two options representing the relationship type directions (Ex: Employee
279 // The default relationship field needs values that are IDs with direction,
280 // while the role field needs values that are names (with implicit
283 // At any rate, the labels should follow the convention in the UI of
284 // describing case roles from the perspective of the client, while the
285 // values must follow the convention in the XML of describing case roles
286 // from the perspective of the non-client.
287 function getRelationshipTypeOptions($isDefault
) {
288 return _
.transform(apiCalls
.relTypes
.values
, function(result
, relType
) {
289 var isBidirectionalRelationship
= relType
.label_a_b
=== relType
.label_b_a
;
292 label
: relType
.label_b_a
,
293 value
: relType
.id
+ '_a_b'
296 if (!isBidirectionalRelationship
) {
298 label
: relType
.label_a_b
,
299 value
: relType
.id
+ '_b_a'
303 // TODO The ids below really should use names not labels see
304 // https://lab.civicrm.org/dev/core/issues/774
307 text
: relType
.label_b_a
,
308 id
: relType
.label_a_b
311 if (!isBidirectionalRelationship
) {
313 text
: relType
.label_a_b
,
314 id
: relType
.label_b_a
321 /// initializes the case type object
322 function initCaseType() {
323 var isNewCaseType
= !apiCalls
.caseType
;
326 $scope
.caseType
= _
.cloneDeep(newCaseTypeTemplate
);
328 $scope
.caseType
= apiCalls
.caseType
;
332 /// initializes the case type definition object
333 function initCaseTypeDefinition() {
334 $scope
.caseType
.definition
= $scope
.caseType
.definition
|| [];
335 $scope
.caseType
.definition
.activityTypes
= $scope
.caseType
.definition
.activityTypes
|| [];
336 $scope
.caseType
.definition
.activitySets
= $scope
.caseType
.definition
.activitySets
|| [];
337 $scope
.caseType
.definition
.caseRoles
= $scope
.caseType
.definition
.caseRoles
|| [];
338 $scope
.caseType
.definition
.statuses
= $scope
.caseType
.definition
.statuses
|| [];
339 $scope
.caseType
.definition
.timelineActivityTypes
= $scope
.caseType
.definition
.timelineActivityTypes
|| [];
340 $scope
.caseType
.definition
.restrictActivityAsgmtToCmsUser
= $scope
.caseType
.definition
.restrictActivityAsgmtToCmsUser
|| 0;
341 $scope
.caseType
.definition
.activityAsgmtGrps
= $scope
.caseType
.definition
.activityAsgmtGrps
|| [];
343 _
.each($scope
.caseType
.definition
.activitySets
, function (set) {
344 _
.each(set.activityTypes
, function (type
, name
) {
345 var isDefaultAssigneeTypeUndefined
= _
.isUndefined(type
.default_assignee_type
);
346 var typeDefinition
= $scope
.activityTypes
[type
.name
];
347 type
.label
= (typeDefinition
&& typeDefinition
.label
) || type
.name
;
349 if (isDefaultAssigneeTypeUndefined
) {
350 type
.default_assignee_type
= defaultAssigneeDefaultValue
.value
;
355 // go lookup and add client-perspective labels for $scope.caseType.definition.caseRoles
356 _
.each($scope
.caseType
.definition
.caseRoles
, function (set) {
357 _
.each($scope
.relationshipTypeOptions
, function (relationshipTypeOption
) {
358 if (relationshipTypeOption
.text
== set.name
) {
359 // relationshipTypeOption.id here corresponds to one of the civicrm_relationship_type.label database fields, not civicrm_relationship_type.id
360 set.displayLabel
= relationshipTypeOption
.id
;
366 /// initializes the selected statuses
367 function initSelectedStatuses() {
368 $scope
.selectedStatuses
= {};
370 _
.each(apiCalls
.caseStatuses
.values
, function (status
) {
371 $scope
.selectedStatuses
[status
.name
] = !$scope
.caseType
.definition
.statuses
.length
|| $scope
.caseType
.definition
.statuses
.indexOf(status
.name
) > -1;
375 $scope
.addActivitySet = function(workflow
) {
376 var activitySet
= {};
377 activitySet
[workflow
] = '1';
378 activitySet
.activityTypes
= [];
381 var names
= _
.pluck($scope
.caseType
.definition
.activitySets
, 'name');
382 while (_
.contains(names
, workflow
+ '_' + offset
)) offset
++;
383 activitySet
.name
= workflow
+ '_' + offset
;
384 activitySet
.label
= (offset
== 1 ) ? $scope
.workflows
[workflow
] : ($scope
.workflows
[workflow
] + ' #' + offset
);
386 $scope
.caseType
.definition
.activitySets
.push(activitySet
);
388 $('.crmCaseType-acttab').tabs('refresh').tabs({active
: -1});
392 function formatActivityTypeOption(type
) {
393 return {id
: type
.name
, text
: type
.label
, icon
: type
.icon
};
396 function addActivityToSet(activitySet
, activityTypeName
) {
397 activitySet
.activityTypes
= activitySet
.activityTypes
|| [];
399 name
: activityTypeName
,
400 label
: $scope
.activityTypes
[activityTypeName
].label
,
402 reference_activity
: 'Open Case',
403 reference_offset
: '1',
404 reference_select
: 'newest',
405 default_assignee_type
: $scope
.defaultAssigneeTypeValues
.NONE
407 activitySet
.activityTypes
.push(activity
);
408 if(typeof activitySet
.timeline
!== "undefined" && activitySet
.timeline
== "1") {
409 $scope
.caseType
.definition
.timelineActivityTypes
.push(activity
);
413 function resetTimelineActivityTypes() {
414 $scope
.caseType
.definition
.timelineActivityTypes
= [];
415 angular
.forEach($scope
.caseType
.definition
.activitySets
, function(activitySet
) {
416 angular
.forEach(activitySet
.activityTypes
, function(activityType
) {
417 $scope
.caseType
.definition
.timelineActivityTypes
.push(activityType
);
422 function createActivity(name
, callback
) {
423 CRM
.loadForm(CRM
.url('civicrm/admin/options/activity_type', {action
: 'add', reset
: 1, label
: name
, component_id
: 7}))
424 .on('crmFormSuccess', function(e
, data
) {
425 $scope
.activityTypes
[data
.optionValue
.name
] = data
.optionValue
;
426 $scope
.activityTypeOptions
.push(formatActivityTypeOption(data
.optionValue
));
427 callback(data
.optionValue
);
432 // Add a new activity entry to an activity-set
433 $scope
.addActivity = function(activitySet
, activityType
) {
434 if ($scope
.activityTypes
[activityType
]) {
435 addActivityToSet(activitySet
, activityType
);
437 createActivity(activityType
, function(newActivity
) {
438 addActivityToSet(activitySet
, newActivity
.name
);
443 /// Add a new top-level activity-type entry
444 $scope
.addActivityType = function(activityType
) {
445 var names
= _
.pluck($scope
.caseType
.definition
.activityTypes
, 'name');
446 if (!_
.contains(names
, activityType
)) {
447 // Add an activity type that exists
448 if ($scope
.activityTypes
[activityType
]) {
449 $scope
.caseType
.definition
.activityTypes
.push({name
: activityType
});
451 createActivity(activityType
, function(newActivity
) {
452 $scope
.caseType
.definition
.activityTypes
.push({name
: newActivity
.name
});
458 /// Clears the activity's default assignee values for relationship and contact
459 $scope
.clearActivityDefaultAssigneeValues = function(activity
) {
460 activity
.default_assignee_relationship
= null;
461 activity
.default_assignee_contact
= null;
464 // TODO roleName passed to addRole is a misnomer, its passed as the
465 // label HOWEVER it should be saved to xml as the name see
466 // https://lab.civicrm.org/dev/core/issues/774
469 $scope
.addRole = function(roles
, roleName
) {
470 var names
= _
.pluck($scope
.caseType
.definition
.caseRoles
, 'name');
471 if (!_
.contains(names
, roleName
)) {
472 var matchingRoles
= _
.filter($scope
.relationshipTypeOptions
, {id
: roleName
});
473 if (matchingRoles
.length
) {
474 var matchingRole
= matchingRoles
.shift();
475 roles
.push({name
: roleName
, displayLabel
: matchingRole
.text
});
477 CRM
.loadForm(CRM
.url('civicrm/admin/reltype', {action
: 'add', reset
: 1, label_a_b
: roleName
}))
478 .on('crmFormSuccess', function(e
, data
) {
479 var newType
= _
.values(data
.relationshipType
)[0];
480 $scope
.$apply(function() {
481 $scope
.addRoleOnTheFly(roles
, newType
);
488 $scope
.addRoleOnTheFly = function(roles
, newType
) {
489 roles
.push({name
: newType
.label_b_a
, displayLabel
: newType
.label_a_b
});
490 // Assume that the case role should be A-B but add both directions as options.
491 $scope
.relationshipTypeOptions
.push({id
: newType
.label_a_b
, text
: newType
.label_b_a
});
492 if (newType
.label_a_b
!= newType
.label_b_a
) {
493 $scope
.relationshipTypeOptions
.push({id
: newType
.label_b_a
, text
: newType
.label_a_b
});
497 $scope
.onManagerChange = function(managerRole
) {
498 angular
.forEach($scope
.caseType
.definition
.caseRoles
, function(caseRole
) {
499 if (caseRole
!= managerRole
) {
500 caseRole
.manager
= '0';
505 $scope
.removeItem = function(array
, item
) {
506 var idx
= _
.indexOf(array
, item
);
508 array
.splice(idx
, 1);
509 resetTimelineActivityTypes();
513 $scope
.isForkable = function() {
514 return !$scope
.caseType
.id
|| $scope
.caseType
.is_forkable
;
517 $scope
.newStatus = function() {
518 CRM
.loadForm(CRM
.url('civicrm/admin/options/case_status', {action
: 'add', reset
: 1}))
519 .on('crmFormSuccess', function(e
, data
) {
520 $scope
.caseStatuses
[data
.optionValue
.name
] = data
.optionValue
;
521 $scope
.selectedStatuses
[data
.optionValue
.name
] = true;
526 $scope
.isNewActivitySetAllowed = function(workflow
) {
531 return 0 === _
.where($scope
.caseType
.definition
.activitySets
, {sequence
: '1'}).length
;
533 CRM
.console('warn', 'Denied access to unrecognized workflow: (' + workflow
+ ')');
538 $scope
.isActivityRemovable = function(activitySet
, activity
) {
542 $scope
.isValidName = function(name
) {
543 return !name
|| name
.match(/^[a-zA-Z0-9_]+$/);
546 $scope
.getWorkflowName = function(activitySet
) {
547 var result
= 'Unknown';
548 _
.each($scope
.workflows
, function(value
, key
) {
549 if (activitySet
[key
]) result
= value
;
555 * Determine which HTML partial to use for a particular
557 * @return string URL of the HTML partial
559 $scope
.activityTableTemplate = function(activitySet
) {
560 if (activitySet
.timeline
) {
561 return '~/crmCaseType/timelineTable.html';
562 } else if (activitySet
.sequence
) {
563 return '~/crmCaseType/sequenceTable.html';
569 $scope
.dump = function() {
570 console
.log($scope
.caseType
);
573 $scope
.save = function() {
574 // Add selected statuses
575 var selectedStatuses
= [];
576 _
.each($scope
.selectedStatuses
, function(v
, k
) {
577 if (v
) selectedStatuses
.push(k
);
579 // Ignore if ALL or NONE selected
580 $scope
.caseType
.definition
.statuses
= selectedStatuses
.length
== _
.size($scope
.selectedStatuses
) ? [] : selectedStatuses
;
582 if ($scope
.caseType
.definition
.activityAsgmtGrps
) {
583 $scope
.caseType
.definition
.activityAsgmtGrps
= $scope
.caseType
.definition
.activityAsgmtGrps
.toString().split(",");
586 function dropDisplaylabel (v
) {
587 delete v
.displayLabel
;
590 // strip out labels from $scope.caseType.definition.caseRoles
591 _
.map($scope
.caseType
.definition
.caseRoles
, dropDisplaylabel
);
593 var result
= crmApi('CaseType', 'create', $scope
.caseType
, true);
594 result
.then(function(data
) {
595 if (data
.is_error
=== 0 || data
.is_error
== '0') {
596 $scope
.caseType
.id
= data
.id
;
597 window
.location
.href
= '#/caseType';
602 $scope
.$watchCollection('caseType.definition.activitySets', function() {
604 $('.crmCaseType-acttab').tabs('refresh');
608 var updateCaseTypeName = function () {
609 if (!$scope
.caseType
.id
&& $scope
.locks
.caseTypeName
) {
610 // Should we do some filtering? Lowercase? Strip whitespace?
611 var t
= $scope
.caseType
.title
? $scope
.caseType
.title
: '';
612 $scope
.caseType
.name
= t
.replace(/ /g, '_').replace(/[^a
-zA
-Z0
-9_
]/g
, '').toLowerCase();
615 $scope
.$watch('locks.caseTypeName', updateCaseTypeName
);
616 $scope
.$watch('caseType.title', updateCaseTypeName
);
618 if (!$scope
.isForkable()) {
619 CRM
.alert(ts('The CiviCase XML file for this case-type prohibits editing the definition.'));
624 crmCaseType
.controller('CaseTypeListCtrl', function($scope
, crmApi
, caseTypes
) {
625 var ts
= $scope
.ts
= CRM
.ts(null);
627 $scope
.caseTypes
= caseTypes
.values
;
628 $scope
.toggleCaseType = function (caseType
) {
629 caseType
.is_active
= (caseType
.is_active
== '1') ? '0' : '1';
630 crmApi('CaseType', 'create', caseType
, true)
631 .catch(function (data
) {
632 caseType
.is_active
= (caseType
.is_active
== '1') ? '0' : '1'; // revert
636 $scope
.deleteCaseType = function (caseType
) {
637 crmApi('CaseType', 'delete', {id
: caseType
.id
}, {
638 error: function (data
) {
639 CRM
.alert(data
.error_message
, ts('Error'), 'error');
642 .then(function (data
) {
643 delete caseTypes
.values
[caseType
.id
];
646 $scope
.revertCaseType = function (caseType
) {
647 caseType
.definition
= 'null';
648 caseType
.is_forked
= '0';
649 crmApi('CaseType', 'create', caseType
, true)
650 .catch(function (data
) {
651 caseType
.is_forked
= '1'; // restore
657 })(angular
, CRM
.$, CRM
._
);