Merge pull request #19736 from colemanw/empty
[civicrm-core.git] / ang / crmCaseType.js
1 (function(angular, $, _) {
2
3 var crmCaseType = angular.module('crmCaseType', CRM.angRequires('crmCaseType'));
4
5 // Note: This template will be passed to cloneDeep(), so don't put any funny stuff in here!
6 var newCaseTypeTemplate = {
7 title: "",
8 name: "",
9 is_active: "1",
10 weight: "1",
11 definition: {
12 activityTypes: [
13 {name: 'Open Case', max_instances: 1},
14 {name: 'Email'},
15 {name: 'Follow up'},
16 {name: 'Meeting'},
17 {name: 'Phone Call'}
18 ],
19 activitySets: [
20 {
21 name: 'standard_timeline',
22 label: 'Standard Timeline',
23 timeline: '1', // Angular won't bind checkbox correctly with numeric 1
24 activityTypes: [
25 {name: 'Open Case', status: 'Completed' }
26 ]
27 }
28 ],
29 caseRoles: [
30 { name: 'Case Coordinator', creator: '1', manager: '1'}
31 ]
32 }
33 };
34
35 crmCaseType.config(['$routeProvider',
36 function($routeProvider) {
37 $routeProvider.when('/caseType', {
38 templateUrl: '~/crmCaseType/list.html',
39 controller: 'CaseTypeListCtrl',
40 resolve: {
41 caseTypes: function($route, crmApi) {
42 return crmApi('CaseType', 'get', {options: {limit: 0}});
43 }
44 }
45 });
46 $routeProvider.when('/caseType/:id', {
47 templateUrl: '~/crmCaseType/edit.html',
48 controller: 'CaseTypeCtrl',
49 resolve: {
50 apiCalls: function($route, crmApi) {
51 var reqs = {};
52 reqs.actStatuses = ['OptionValue', 'get', {
53 option_group_id: 'activity_status',
54 sequential: 1,
55 options: {limit: 0}
56 }];
57 reqs.caseStatuses = ['OptionValue', 'get', {
58 option_group_id: 'case_status',
59 sequential: 1,
60 options: {
61 sort: 'weight',
62 limit: 0
63 }
64 }];
65 reqs.actTypes = ['OptionValue', 'get', {
66 option_group_id: 'activity_type',
67 sequential: 1,
68 options: {
69 sort: 'name',
70 limit: 0
71 }
72 }];
73 reqs.defaultAssigneeTypes = ['OptionValue', 'get', {
74 option_group_id: 'activity_default_assignee',
75 sequential: 1,
76 options: {
77 limit: 0
78 }
79 }];
80 reqs.relTypes = ['RelationshipType', 'get', {
81 sequential: 1,
82 options: {
83 sort: 'label_a_b',
84 limit: 0
85 }
86 }];
87 if ($route.current.params.id !== 'new') {
88 reqs.caseType = ['CaseType', 'getsingle', {
89 id: $route.current.params.id
90 }];
91 }
92 return crmApi(reqs);
93 }
94 }
95 });
96 }
97 ]);
98
99 // Add a new record by name.
100 // Ex: <crmAddName crm-options="['Alpha','Beta','Gamma']" crm-var="newItem" crm-on-add="callMyCreateFunction(newItem)" />
101 crmCaseType.directive('crmAddName', function() {
102 return {
103 restrict: 'AE',
104 template: '<input class="add-activity crm-action-menu fa-plus" type="hidden" />',
105 link: function(scope, element, attrs) {
106
107 var input = $('input', element);
108
109 scope._resetSelection = function() {
110 $(input).select2('close');
111 $(input).select2('val', '');
112 scope[attrs.crmVar] = '';
113 };
114
115 $(input).crmSelect2({
116 data: function () {
117 return { results: scope[attrs.crmOptions] };
118 },
119 createSearchChoice: function(term) {
120 return {id: term, text: term + ' (' + ts('new') + ')'};
121 },
122 createSearchChoicePosition: 'bottom',
123 placeholder: attrs.placeholder
124 });
125 $(input).on('select2-selecting', function(e) {
126 scope[attrs.crmVar] = e.val;
127 scope.$evalAsync(attrs.crmOnAdd);
128 scope.$evalAsync('_resetSelection()');
129 e.preventDefault();
130 });
131 }
132 };
133 });
134
135 crmCaseType.directive('crmEditableTabTitle', function($timeout) {
136 return {
137 restrict: 'AE',
138 link: function(scope, element, attrs) {
139 element.addClass('crm-editable crm-editable-enabled');
140 var titleLabel = $(element).find('span');
141 var penIcon = $('<i class="crm-i fa-pencil crm-editable-placeholder" aria-hidden="true"></i>').prependTo(element);
142 var saveButton = $('<button type="button"><i class="crm-i fa-check" aria-hidden="true"></i></button>').appendTo(element);
143 var cancelButton = $('<button type="cancel"><i class="crm-i fa-times" aria-hidden="true"></i></button>').appendTo(element);
144 $('button', element).wrapAll('<div class="crm-editable-form" style="display:none" />');
145 var buttons = $('.crm-editable-form', element);
146 titleLabel.on('click', startEditMode);
147 penIcon.on('click', startEditMode);
148
149 function detectEscapeKeyPress (event) {
150 var isEscape = false;
151
152 if ("key" in event) {
153 isEscape = (event.key == "Escape" || event.key == "Esc");
154 } else {
155 isEscape = (event.keyCode == 27);
156 }
157
158 return isEscape;
159 }
160
161 function detectEnterKeyPress (event) {
162 var isEnter = false;
163
164 if ("key" in event) {
165 isEnter = (event.key == "Enter");
166 } else {
167 isEnter = (event.keyCode == 13);
168 }
169
170 return isEnter;
171 }
172
173 function startEditMode () {
174 if (titleLabel.is(":focus")) {
175 return;
176 }
177
178 penIcon.hide();
179 buttons.show();
180
181 saveButton.click(function () {
182 updateTextValue();
183 stopEditMode();
184 });
185
186 cancelButton.click(function () {
187 revertTextValue();
188 stopEditMode();
189 });
190
191 $(element).addClass('crm-editable-editing');
192
193 titleLabel
194 .attr("contenteditable", "true")
195 .focus()
196 .focusout(function (event) {
197 $timeout(function () {
198 revertTextValue();
199 stopEditMode();
200 }, 500);
201 })
202 .keydown(function(event) {
203 event.stopImmediatePropagation();
204
205 if(detectEscapeKeyPress(event)) {
206 revertTextValue();
207 stopEditMode();
208 } else if(detectEnterKeyPress(event)) {
209 event.preventDefault();
210 updateTextValue();
211 stopEditMode();
212 }
213 });
214 }
215
216 function stopEditMode () {
217 titleLabel.removeAttr("contenteditable").off("focusout");
218 titleLabel.off("keydown");
219 saveButton.off("click");
220 cancelButton.off("click");
221 $(element).removeClass('crm-editable-editing');
222
223 penIcon.show();
224 buttons.hide();
225 }
226
227 function revertTextValue () {
228 titleLabel.text(scope.activitySet.label);
229 }
230
231 function updateTextValue () {
232 var updatedTitle = titleLabel.text();
233
234 scope.$evalAsync(function () {
235 scope.activitySet.label = updatedTitle;
236 });
237 }
238 }
239 };
240 });
241
242 crmCaseType.controller('CaseTypeCtrl', function($scope, crmApi, apiCalls, crmUiHelp) {
243 var defaultAssigneeDefaultValue, ts;
244
245 (function init () {
246
247 ts = $scope.ts = CRM.ts(null);
248 $scope.hs = crmUiHelp({file: 'CRM/Case/CaseType'});
249 $scope.locks = { caseTypeName: true, activitySetName: true };
250 $scope.workflows = { timeline: 'Timeline', sequence: 'Sequence' };
251 defaultAssigneeDefaultValue = _.find(apiCalls.defaultAssigneeTypes.values, { is_default: '1' }) || {};
252
253 storeApiCallsResults();
254 initCaseType();
255 initCaseTypeDefinition();
256 initSelectedStatuses();
257 })();
258
259 /// Stores the api calls results in the $scope object
260 function storeApiCallsResults() {
261 $scope.activityStatuses = apiCalls.actStatuses.values;
262 $scope.caseStatuses = _.indexBy(apiCalls.caseStatuses.values, 'name');
263 $scope.activityTypes = _.indexBy(apiCalls.actTypes.values, 'name');
264 $scope.activityTypeOptions = _.map(apiCalls.actTypes.values, formatActivityTypeOption);
265 $scope.defaultAssigneeTypes = apiCalls.defaultAssigneeTypes.values;
266 // for dropdown lists, only include enabled choices
267 $scope.relationshipTypeOptions = getRelationshipTypeOptions(true);
268 // for comparisons, include disabled
269 $scope.relationshipTypeOptionsAll = getRelationshipTypeOptions(false);
270 // stores the default assignee values indexed by their option name:
271 $scope.defaultAssigneeTypeValues = _.chain($scope.defaultAssigneeTypes)
272 .indexBy('name').mapValues('value').value();
273 }
274
275 // Returns the relationship type options. If the relationship is
276 // bidirectional (Ex: Spouse of) it adds a single option otherwise it adds
277 // two options representing the relationship type directions (Ex: Employee
278 // of, Employer of).
279 //
280 // The relationship dropdown needs to be given IDs with direction,
281 // while the role name in the xml needs values that are names (with
282 // implicit direction).
283 //
284 // At any rate, the labels should follow the convention in the UI of
285 // describing case roles from the perspective of the client, while the
286 // names must follow the convention in the XML of describing case roles
287 // from the perspective of the non-client.
288 //
289 // @param onlyActive bool
290 // If true, only include enabled relationship types.
291 // @return array[object]
292 // object: {
293 // xmlName: The name corresponding to what's stored in xml/caseRoles.
294 // text: The text in dropdowns, i.e. <option value="id">text</option>
295 // It's called text because that is what select2 is expecting.
296 // id: The id value in dropdowns, i.e. <option value="id">text</option>
297 // Is the concatenation of id+direction, e.g. 2_a_b.
298 // It's called id because that is what select2 is expecting.
299 // }
300 function getRelationshipTypeOptions(onlyActive) {
301 var relationshipTypesToUse;
302 if (onlyActive) {
303 relationshipTypesToUse = _.filter(apiCalls.relTypes.values, {is_active: "1"});
304 } else {
305 relationshipTypesToUse = apiCalls.relTypes.values;
306 }
307 return _.transform(relationshipTypesToUse, function(result, relType) {
308 var isBidirectionalRelationship = relType.label_a_b === relType.label_b_a;
309
310 // The order here of a's and b's here is important regarding
311 // unidirectional and bidirectional. Because the xml spec DOES support
312 // direction for activity auto-assignees, if this is changed you might
313 // end up with activity assignees in existing xml that then come
314 // through as blank in the dropdown on the timelines tab if it's a
315 // bidirectional relationship. E.g. if spouse is stored in an existing
316 // xml definition as 2_a_b, but we only push 2_b_a, then it won't match
317 // in the dropdown.
318 //
319 // This has some implications for when we add a new type on the fly
320 // later, but it works out ok as long as we also do it in the same
321 // direction there. See notes in addRoleOnTheFly().
322 result.push({
323 // This is what we want to store in the caseRoles.name field,
324 // which corresponds to the xml file, when we send it back to
325 // the server.
326 xmlName: relType.name_a_b,
327 // This has to be called text because select2 is expecting it.
328 // And yes it's the opposite direction from name.
329 text: relType.label_b_a,
330 // This has to be called id because select2 is expecting it.
331 id: relType.id + '_a_b'
332 });
333
334 if (!isBidirectionalRelationship) {
335 result.push({
336 xmlName: relType.name_b_a,
337 text: relType.label_a_b,
338 id: relType.id + '_b_a'
339 });
340 }
341 }, []);
342 }
343
344 /// initializes the case type object
345 function initCaseType() {
346 var isNewCaseType = !apiCalls.caseType;
347
348 if (isNewCaseType) {
349 $scope.caseType = _.cloneDeep(newCaseTypeTemplate);
350 } else {
351 $scope.caseType = apiCalls.caseType;
352 }
353 }
354
355 /// initializes the case type definition object
356 function initCaseTypeDefinition() {
357 $scope.caseType.definition = $scope.caseType.definition || [];
358 $scope.caseType.definition.activityTypes = $scope.caseType.definition.activityTypes || [];
359 $scope.caseType.definition.activitySets = $scope.caseType.definition.activitySets || [];
360 $scope.caseType.definition.caseRoles = $scope.caseType.definition.caseRoles || [];
361 $scope.caseType.definition.statuses = $scope.caseType.definition.statuses || [];
362 $scope.caseType.definition.timelineActivityTypes = $scope.caseType.definition.timelineActivityTypes || [];
363 $scope.caseType.definition.restrictActivityAsgmtToCmsUser = $scope.caseType.definition.restrictActivityAsgmtToCmsUser || 0;
364 $scope.caseType.definition.activityAsgmtGrps = $scope.caseType.definition.activityAsgmtGrps || [];
365
366 _.each($scope.caseType.definition.activitySets, function (set) {
367 _.each(set.activityTypes, function (type, name) {
368 var isDefaultAssigneeTypeUndefined = _.isUndefined(type.default_assignee_type);
369 var typeDefinition = $scope.activityTypes[type.name];
370 type.label = (typeDefinition && typeDefinition.label) || type.name;
371
372 if (isDefaultAssigneeTypeUndefined) {
373 type.default_assignee_type = defaultAssigneeDefaultValue.value;
374 }
375 });
376 });
377
378 // Go lookup and add client-perspective labels for
379 // $scope.caseType.definition.caseRoles, since the xml doesn't have them
380 // and we need to display them in the roles table.
381 _.each($scope.caseType.definition.caseRoles, function (set) {
382 _.each($scope.relationshipTypeOptionsAll, function (relationshipTypeOption) {
383 if (relationshipTypeOption.xmlName == set.name) {
384 // relationshipTypeOption.text here corresponds to one of the
385 // apiCalls.relTypes label fields (i.e. civicrm_relationship_type
386 // label database fields). It has to be called text because
387 // it's used in select2 which expects it to be called text.
388 set.displayLabel = relationshipTypeOption.text;
389 // break out of inner `each` loop
390 return false;
391 }
392 });
393 });
394 }
395
396 /// initializes the selected statuses
397 function initSelectedStatuses() {
398 $scope.selectedStatuses = {};
399
400 _.each(apiCalls.caseStatuses.values, function (status) {
401 $scope.selectedStatuses[status.name] = !$scope.caseType.definition.statuses.length || $scope.caseType.definition.statuses.indexOf(status.name) > -1;
402 });
403 }
404
405 $scope.addActivitySet = function(workflow) {
406 var activitySet = {};
407 activitySet[workflow] = '1';
408 activitySet.activityTypes = [];
409
410 var offset = 1;
411 var names = _.pluck($scope.caseType.definition.activitySets, 'name');
412 while (_.contains(names, workflow + '_' + offset)) offset++;
413 activitySet.name = workflow + '_' + offset;
414 activitySet.label = (offset == 1 ) ? $scope.workflows[workflow] : ($scope.workflows[workflow] + ' #' + offset);
415
416 $scope.caseType.definition.activitySets.push(activitySet);
417 _.defer(function() {
418 $('.crmCaseType-acttab').tabs('refresh').tabs({active: -1});
419 });
420 };
421
422 function formatActivityTypeOption(type) {
423 return {id: type.name, text: type.label, icon: type.icon};
424 }
425
426 function addActivityToSet(activitySet, activityTypeName) {
427 activitySet.activityTypes = activitySet.activityTypes || [];
428 var activity = {
429 name: activityTypeName,
430 label: $scope.activityTypes[activityTypeName].label,
431 status: 'Scheduled',
432 reference_activity: 'Open Case',
433 reference_offset: '1',
434 reference_select: 'newest',
435 default_assignee_type: $scope.defaultAssigneeTypeValues.NONE
436 };
437 activitySet.activityTypes.push(activity);
438 if(typeof activitySet.timeline !== "undefined" && activitySet.timeline == "1") {
439 $scope.caseType.definition.timelineActivityTypes.push(activity);
440 }
441 }
442
443 function resetTimelineActivityTypes() {
444 $scope.caseType.definition.timelineActivityTypes = [];
445 angular.forEach($scope.caseType.definition.activitySets, function(activitySet) {
446 angular.forEach(activitySet.activityTypes, function(activityType) {
447 $scope.caseType.definition.timelineActivityTypes.push(activityType);
448 });
449 });
450 }
451
452 function createActivity(name, callback) {
453 CRM.loadForm(CRM.url('civicrm/admin/options/activity_type', {action: 'add', reset: 1, label: name, component_id: 7}))
454 .on('crmFormSuccess', function(e, data) {
455 $scope.activityTypes[data.optionValue.name] = data.optionValue;
456 $scope.activityTypeOptions.push(formatActivityTypeOption(data.optionValue));
457 callback(data.optionValue);
458 $scope.$digest();
459 });
460 }
461
462 // Add a new activity entry to an activity-set
463 $scope.addActivity = function(activitySet, activityType) {
464 if ($scope.activityTypes[activityType]) {
465 addActivityToSet(activitySet, activityType);
466 } else {
467 createActivity(activityType, function(newActivity) {
468 addActivityToSet(activitySet, newActivity.name);
469 });
470 }
471 };
472
473 /// Add a new top-level activity-type entry
474 $scope.addActivityType = function(activityType) {
475 var names = _.pluck($scope.caseType.definition.activityTypes, 'name');
476 if (!_.contains(names, activityType)) {
477 // Add an activity type that exists
478 if ($scope.activityTypes[activityType]) {
479 $scope.caseType.definition.activityTypes.push({name: activityType});
480 } else {
481 createActivity(activityType, function(newActivity) {
482 $scope.caseType.definition.activityTypes.push({name: newActivity.name});
483 });
484 }
485 }
486 };
487
488 /// Clears the activity's default assignee values for relationship and contact
489 $scope.clearActivityDefaultAssigneeValues = function(activity) {
490 activity.default_assignee_relationship = null;
491 activity.default_assignee_contact = null;
492 };
493
494 // Add a new role.
495 // Called from the select2 dropdown when a selection is made.
496 //
497 // @param roles array
498 // The roles currently in the table.
499 // @param roleIdOrLabel string
500 // The trick here is that since you can add roles on the fly, the
501 // roleIdOrLabel parameter can be two different types of things. It can be
502 // the id, like '2_a_b' if it's an existing choice that was selected, or
503 // it can be a LABEL if they typed something that isn't in the list. If
504 // the latter the select2 has no choice but to give us a label because
505 // there is no id yet.
506 $scope.addRole = function(roles, roleIdOrLabel) {
507 var matchingRole;
508 // First check does what we've been given match up to any relationship
509 // type, based on id, which is the id from the select2 (i.e. html
510 // <option value="id">)
511 var matchingRoles = _.filter($scope.relationshipTypeOptions, {id: roleIdOrLabel});
512 if (matchingRoles.length) {
513 matchingRole = matchingRoles.shift();
514 }
515 // If found, is the corresponding machine name in the list of existing
516 // roles for the case type. Unfortunately, caseRoles only stores name,
517 // which doesn't indicate the id or direction, because the xml spec
518 // doesn't support those.
519 var names = _.pluck($scope.caseType.definition.caseRoles, 'name');
520 if (matchingRole) {
521 // If it's not in the table already, add it, otherwise do nothing since
522 // don't want to add it twice.
523 if (!_.contains(names, matchingRole.xmlName)) {
524 roles.push({name: matchingRole.xmlName, displayLabel: matchingRole.text});
525 }
526 } else {
527 // Not a known relationship type, so create on-the-fly.
528 // At this point roleIdOrLabel must be the new label they just typed.
529 CRM.loadForm(CRM.url('civicrm/admin/reltype', {action: 'add', reset: 1, label_a_b: roleIdOrLabel}))
530 .on('crmFormSuccess', function(e, data) {
531 var newType = _.values(data.relationshipType)[0];
532 $scope.$apply(function() {
533 $scope.addRoleOnTheFly(roles, newType);
534 });
535 });
536 }
537 };
538
539 // Add a newly created relationship type as a role to the table and
540 // update the list of options.
541 //
542 // @param roles array
543 // The roles currently in the table.
544 // @param newType array
545 // The array returned from the api call that created the new type
546 // earlier.
547 $scope.addRoleOnTheFly = function(roles, newType) {
548 // Add it to the roles table. Assume they want the A-B direction since
549 // that's what they would have typed.
550 // Name and label are opposites here because name represents the value
551 // in the xml here which historically is the opposite.
552 roles.push({name: newType.name_b_a, displayLabel: newType.label_a_b});
553
554 // But now add both directions as option choices for future dropdown
555 // selections.
556 // Note that to keep in line with the original population on init,
557 // we're pushing a different direction here than we just added to the
558 // table, but there's only two possibilities:
559 // 1. Labels are the same, and since it's a new type name_a_b and
560 // name_b_a will therefore be the same, and so it doesn't matter which
561 // name is stored in caseRoles.
562 // 2. Labels are different, in which case we're also going to push the
563 // other direction below.
564 // So either way we're covered.
565 // See also note in getRelationshipTypeOptions().
566 var newRelTypeOption = {
567 xmlName: newType.name_a_b,
568 // Yes text is the opposite direction from name here.
569 text: newType.label_b_a,
570 id: newType.id + '_a_b'
571 };
572 $scope.relationshipTypeOptions.push(newRelTypeOption);
573 $scope.relationshipTypeOptionsAll.push(newRelTypeOption);
574 // Add the other direction if different.
575 if (newType.label_a_b != newType.label_b_a) {
576 newRelTypeOption = {
577 xmlName: newType.name_b_a,
578 text: newType.label_a_b,
579 id: newType.id + '_b_a'
580 };
581 $scope.relationshipTypeOptions.push(newRelTypeOption);
582 $scope.relationshipTypeOptionsAll.push(newRelTypeOption);
583 }
584 };
585
586 $scope.onManagerChange = function(managerRole) {
587 angular.forEach($scope.caseType.definition.caseRoles, function(caseRole) {
588 if (caseRole != managerRole) {
589 caseRole.manager = '0';
590 }
591 });
592 };
593
594 $scope.removeItem = function(array, item) {
595 var idx = _.indexOf(array, item);
596 if (idx != -1) {
597 array.splice(idx, 1);
598 resetTimelineActivityTypes();
599 }
600 };
601
602 $scope.isForkable = function() {
603 return !$scope.caseType.id || $scope.caseType.is_forkable;
604 };
605
606 $scope.newStatus = function() {
607 CRM.loadForm(CRM.url('civicrm/admin/options/case_status', {action: 'add', reset: 1}))
608 .on('crmFormSuccess', function(e, data) {
609 $scope.caseStatuses[data.optionValue.name] = data.optionValue;
610 $scope.selectedStatuses[data.optionValue.name] = true;
611 $scope.$digest();
612 });
613 };
614
615 $scope.isNewActivitySetAllowed = function(workflow) {
616 switch (workflow) {
617 case 'timeline':
618 return true;
619 case 'sequence':
620 return 0 === _.where($scope.caseType.definition.activitySets, {sequence: '1'}).length;
621 default:
622 CRM.console('warn', 'Denied access to unrecognized workflow: (' + workflow + ')');
623 return false;
624 }
625 };
626
627 $scope.isActivityRemovable = function(activitySet, activity) {
628 return true;
629 };
630
631 $scope.isValidName = function(name) {
632 return !name || name.match(/^[a-zA-Z0-9_]+$/);
633 };
634
635 $scope.getWorkflowName = function(activitySet) {
636 var result = 'Unknown';
637 _.each($scope.workflows, function(value, key) {
638 if (activitySet[key]) result = value;
639 });
640 return result;
641 };
642
643 /**
644 * Determine which HTML partial to use for a particular
645 *
646 * @return string URL of the HTML partial
647 */
648 $scope.activityTableTemplate = function(activitySet) {
649 if (activitySet.timeline) {
650 return '~/crmCaseType/timelineTable.html';
651 } else if (activitySet.sequence) {
652 return '~/crmCaseType/sequenceTable.html';
653 } else {
654 return '';
655 }
656 };
657
658 $scope.dump = function() {
659 console.log($scope.caseType);
660 };
661
662 $scope.save = function() {
663 // Add selected statuses
664 var selectedStatuses = [];
665 _.each($scope.selectedStatuses, function(v, k) {
666 if (v) selectedStatuses.push(k);
667 });
668 // Ignore if ALL or NONE selected
669 $scope.caseType.definition.statuses = selectedStatuses.length == _.size($scope.selectedStatuses) ? [] : selectedStatuses;
670
671 if ($scope.caseType.definition.activityAsgmtGrps) {
672 $scope.caseType.definition.activityAsgmtGrps = $scope.caseType.definition.activityAsgmtGrps.toString().split(",");
673 }
674
675 function dropDisplaylabel (v) {
676 delete v.displayLabel;
677 }
678
679 // strip out labels from $scope.caseType.definition.caseRoles
680 _.map($scope.caseType.definition.caseRoles, dropDisplaylabel);
681
682 var result = crmApi('CaseType', 'create', $scope.caseType, true);
683 result.then(function(data) {
684 if (data.is_error === 0 || data.is_error == '0') {
685 $scope.caseType.id = data.id;
686 window.location.href = '#/caseType';
687 }
688 });
689 };
690
691 $scope.$watchCollection('caseType.definition.activitySets', function() {
692 _.defer(function() {
693 $('.crmCaseType-acttab').tabs('refresh');
694 });
695 });
696
697 var updateCaseTypeName = function () {
698 if (!$scope.caseType.id && $scope.locks.caseTypeName) {
699 // Should we do some filtering? Lowercase? Strip whitespace?
700 var t = $scope.caseType.title ? $scope.caseType.title : '';
701 $scope.caseType.name = t.replace(/ /g, '_').replace(/[^a-zA-Z0-9_]/g, '').toLowerCase();
702 }
703 };
704 $scope.$watch('locks.caseTypeName', updateCaseTypeName);
705 $scope.$watch('caseType.title', updateCaseTypeName);
706
707 if (!$scope.isForkable()) {
708 CRM.alert(ts('The CiviCase XML file for this case-type prohibits editing the definition.'));
709 }
710
711 });
712
713 crmCaseType.controller('CaseTypeListCtrl', function($scope, crmApi, caseTypes) {
714 var ts = $scope.ts = CRM.ts(null);
715
716 $scope.caseTypes = caseTypes.values;
717 $scope.toggleCaseType = function (caseType) {
718 caseType.is_active = (caseType.is_active == '1') ? '0' : '1';
719 crmApi('CaseType', 'create', caseType, true)
720 .catch(function (data) {
721 caseType.is_active = (caseType.is_active == '1') ? '0' : '1'; // revert
722 $scope.$digest();
723 });
724 };
725 $scope.deleteCaseType = function (caseType) {
726 crmApi('CaseType', 'delete', {id: caseType.id}, {
727 error: function (data) {
728 CRM.alert(data.error_message, ts('Error'), 'error');
729 }
730 })
731 .then(function (data) {
732 delete caseTypes.values[caseType.id];
733 });
734 };
735 $scope.revertCaseType = function (caseType) {
736 caseType.definition = 'null';
737 caseType.is_forked = '0';
738 crmApi('CaseType', 'create', caseType, true)
739 .catch(function (data) {
740 caseType.is_forked = '1'; // restore
741 $scope.$digest();
742 });
743 };
744 });
745
746 })(angular, CRM.$, CRM._);