Merge pull request #12153 from JMAConsulting/dev_core_116
[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: {limit: 0}
61 }];
62 reqs.actTypes = ['OptionValue', 'get', {
63 option_group_id: 'activity_type',
64 sequential: 1,
65 options: {
66 sort: 'name',
67 limit: 0
68 }
69 }];
70 reqs.relTypes = ['RelationshipType', 'get', {
71 sequential: 1,
72 options: {
73 sort: CRM.crmCaseType.REL_TYPE_CNAME,
74 limit: 0
75 }
76 }];
77 if ($route.current.params.id !== 'new') {
78 reqs.caseType = ['CaseType', 'getsingle', {
79 id: $route.current.params.id
80 }];
81 }
82 return crmApi(reqs);
83 }
84 }
85 });
86 }
87 ]);
88
89 // Add a new record by name.
90 // Ex: <crmAddName crm-options="['Alpha','Beta','Gamma']" crm-var="newItem" crm-on-add="callMyCreateFunction(newItem)" />
91 crmCaseType.directive('crmAddName', function() {
92 return {
93 restrict: 'AE',
94 template: '<input class="add-activity crm-action-menu fa-plus" type="hidden" />',
95 link: function(scope, element, attrs) {
96
97 var input = $('input', element);
98
99 scope._resetSelection = function() {
100 $(input).select2('close');
101 $(input).select2('val', '');
102 scope[attrs.crmVar] = '';
103 };
104
105 $(input).crmSelect2({
106 data: function () {
107 return { results: scope[attrs.crmOptions] };
108 },
109 createSearchChoice: function(term) {
110 return {id: term, text: term + ' (' + ts('new') + ')'};
111 },
112 createSearchChoicePosition: 'bottom',
113 placeholder: attrs.placeholder
114 });
115 $(input).on('select2-selecting', function(e) {
116 scope[attrs.crmVar] = e.val;
117 scope.$evalAsync(attrs.crmOnAdd);
118 scope.$evalAsync('_resetSelection()');
119 e.preventDefault();
120 });
121 }
122 };
123 });
124
125 crmCaseType.directive('crmEditableTabTitle', function($timeout) {
126 return {
127 restrict: 'AE',
128 link: function(scope, element, attrs) {
129 element.addClass('crm-editable crm-editable-enabled');
130 var titleLabel = $(element).find('span');
131 var penIcon = $('<i class="crm-i fa-pencil crm-editable-placeholder"></i>').prependTo(element);
132 var saveButton = $('<button type="button"><i class="crm-i fa-check"></i></button>').appendTo(element);
133 var cancelButton = $('<button type="cancel"><i class="crm-i fa-times"></i></button>').appendTo(element);
134 $('button', element).wrapAll('<div class="crm-editable-form" style="display:none" />');
135 var buttons = $('.crm-editable-form', element);
136 titleLabel.on('click', startEditMode);
137 penIcon.on('click', startEditMode);
138
139 function detectEscapeKeyPress (event) {
140 var isEscape = false;
141
142 if ("key" in event) {
143 isEscape = (event.key == "Escape" || event.key == "Esc");
144 } else {
145 isEscape = (event.keyCode == 27);
146 }
147
148 return isEscape;
149 }
150
151 function detectEnterKeyPress (event) {
152 var isEnter = false;
153
154 if ("key" in event) {
155 isEnter = (event.key == "Enter");
156 } else {
157 isEnter = (event.keyCode == 13);
158 }
159
160 return isEnter;
161 }
162
163 function startEditMode () {
164 if (titleLabel.is(":focus")) {
165 return;
166 }
167
168 penIcon.hide();
169 buttons.show();
170
171 saveButton.click(function () {
172 updateTextValue();
173 stopEditMode();
174 });
175
176 cancelButton.click(function () {
177 revertTextValue();
178 stopEditMode();
179 });
180
181 $(element).addClass('crm-editable-editing');
182
183 titleLabel
184 .attr("contenteditable", "true")
185 .focus()
186 .focusout(function (event) {
187 $timeout(function () {
188 revertTextValue();
189 stopEditMode();
190 }, 500);
191 })
192 .keydown(function(event) {
193 event.stopImmediatePropagation();
194
195 if(detectEscapeKeyPress(event)) {
196 revertTextValue();
197 stopEditMode();
198 } else if(detectEnterKeyPress(event)) {
199 event.preventDefault();
200 updateTextValue();
201 stopEditMode();
202 }
203 });
204 }
205
206 function stopEditMode () {
207 titleLabel.removeAttr("contenteditable").off("focusout");
208 titleLabel.off("keydown");
209 saveButton.off("click");
210 cancelButton.off("click");
211 $(element).removeClass('crm-editable-editing');
212
213 penIcon.show();
214 buttons.hide();
215 }
216
217 function revertTextValue () {
218 titleLabel.text(scope.activitySet.label);
219 }
220
221 function updateTextValue () {
222 var updatedTitle = titleLabel.text();
223
224 scope.$evalAsync(function () {
225 scope.activitySet.label = updatedTitle;
226 });
227 }
228 }
229 };
230 });
231
232 crmCaseType.controller('CaseTypeCtrl', function($scope, crmApi, apiCalls) {
233 // CRM_Case_XMLProcessor::REL_TYPE_CNAME
234 var REL_TYPE_CNAME = CRM.crmCaseType.REL_TYPE_CNAME,
235
236 ts = $scope.ts = CRM.ts(null);
237
238 $scope.activityStatuses = apiCalls.actStatuses.values;
239 $scope.caseStatuses = _.indexBy(apiCalls.caseStatuses.values, 'name');
240 $scope.activityTypes = _.indexBy(apiCalls.actTypes.values, 'name');
241 $scope.activityTypeOptions = _.map(apiCalls.actTypes.values, formatActivityTypeOption);
242 $scope.relationshipTypeOptions = _.map(apiCalls.relTypes.values, function(type) {
243 return {id: type[REL_TYPE_CNAME], text: type.label_b_a};
244 });
245 $scope.locks = {caseTypeName: true, activitySetName: true};
246
247 $scope.workflows = {
248 'timeline': 'Timeline',
249 'sequence': 'Sequence'
250 };
251
252 $scope.caseType = apiCalls.caseType ? apiCalls.caseType : _.cloneDeep(newCaseTypeTemplate);
253 $scope.caseType.definition = $scope.caseType.definition || [];
254 $scope.caseType.definition.activityTypes = $scope.caseType.definition.activityTypes || [];
255 $scope.caseType.definition.activitySets = $scope.caseType.definition.activitySets || [];
256 _.each($scope.caseType.definition.activitySets, function (set) {
257 _.each(set.activityTypes, function (type, name) {
258 type.label = $scope.activityTypes[type.name].label;
259 });
260 });
261 $scope.caseType.definition.caseRoles = $scope.caseType.definition.caseRoles || [];
262 $scope.caseType.definition.statuses = $scope.caseType.definition.statuses || [];
263
264 $scope.selectedStatuses = {};
265 _.each(apiCalls.caseStatuses.values, function (status) {
266 $scope.selectedStatuses[status.name] = !$scope.caseType.definition.statuses.length || $scope.caseType.definition.statuses.indexOf(status.name) > -1;
267 });
268
269 $scope.addActivitySet = function(workflow) {
270 var activitySet = {};
271 activitySet[workflow] = '1';
272 activitySet.activityTypes = [];
273
274 var offset = 1;
275 var names = _.pluck($scope.caseType.definition.activitySets, 'name');
276 while (_.contains(names, workflow + '_' + offset)) offset++;
277 activitySet.name = workflow + '_' + offset;
278 activitySet.label = (offset == 1 ) ? $scope.workflows[workflow] : ($scope.workflows[workflow] + ' #' + offset);
279
280 $scope.caseType.definition.activitySets.push(activitySet);
281 _.defer(function() {
282 $('.crmCaseType-acttab').tabs('refresh').tabs({active: -1});
283 });
284 };
285
286 function formatActivityTypeOption(type) {
287 return {id: type.name, text: type.label, icon: type.icon};
288 }
289
290 function addActivityToSet(activitySet, activityTypeName) {
291 activitySet.activityTypes.push({
292 name: activityTypeName,
293 label: $scope.activityTypes[activityTypeName].label,
294 status: 'Scheduled',
295 reference_activity: 'Open Case',
296 reference_offset: '1',
297 reference_select: 'newest'
298 });
299 }
300
301 function createActivity(name, callback) {
302 CRM.loadForm(CRM.url('civicrm/admin/options/activity_type', {action: 'add', reset: 1, label: name, component_id: 7}))
303 .on('crmFormSuccess', function(e, data) {
304 $scope.activityTypes[data.optionValue.name] = data.optionValue;
305 $scope.activityTypeOptions.push(formatActivityTypeOption(data.optionValue));
306 callback(data.optionValue);
307 $scope.$digest();
308 });
309 }
310
311 // Add a new activity entry to an activity-set
312 $scope.addActivity = function(activitySet, activityType) {
313 if ($scope.activityTypes[activityType]) {
314 addActivityToSet(activitySet, activityType);
315 } else {
316 createActivity(activityType, function(newActivity) {
317 addActivityToSet(activitySet, newActivity.name);
318 });
319 }
320 };
321
322 /// Add a new top-level activity-type entry
323 $scope.addActivityType = function(activityType) {
324 var names = _.pluck($scope.caseType.definition.activityTypes, 'name');
325 if (!_.contains(names, activityType)) {
326 // Add an activity type that exists
327 if ($scope.activityTypes[activityType]) {
328 $scope.caseType.definition.activityTypes.push({name: activityType});
329 } else {
330 createActivity(activityType, function(newActivity) {
331 $scope.caseType.definition.activityTypes.push({name: newActivity.name});
332 });
333 }
334 }
335 };
336
337 /// Add a new role
338 $scope.addRole = function(roles, roleName) {
339 var names = _.pluck($scope.caseType.definition.caseRoles, 'name');
340 if (!_.contains(names, roleName)) {
341 if (_.where($scope.relationshipTypeOptions, {id: roleName}).length) {
342 roles.push({name: roleName});
343 } else {
344 CRM.loadForm(CRM.url('civicrm/admin/reltype', {action: 'add', reset: 1, label_a_b: roleName, label_b_a: roleName}))
345 .on('crmFormSuccess', function(e, data) {
346 roles.push({name: data.relationshipType[REL_TYPE_CNAME]});
347 $scope.relationshipTypeOptions.push({id: data.relationshipType[REL_TYPE_CNAME], text: data.relationshipType.label_b_a});
348 $scope.$digest();
349 });
350 }
351 }
352 };
353
354 $scope.onManagerChange = function(managerRole) {
355 angular.forEach($scope.caseType.definition.caseRoles, function(caseRole) {
356 if (caseRole != managerRole) {
357 caseRole.manager = '0';
358 }
359 });
360 };
361
362 $scope.removeItem = function(array, item) {
363 var idx = _.indexOf(array, item);
364 if (idx != -1) {
365 array.splice(idx, 1);
366 }
367 };
368
369 $scope.isForkable = function() {
370 return !$scope.caseType.id || $scope.caseType.is_forkable;
371 };
372
373 $scope.newStatus = function() {
374 CRM.loadForm(CRM.url('civicrm/admin/options/case_status', {action: 'add', reset: 1}))
375 .on('crmFormSuccess', function(e, data) {
376 $scope.caseStatuses[data.optionValue.name] = data.optionValue;
377 $scope.selectedStatuses[data.optionValue.name] = true;
378 $scope.$digest();
379 });
380 };
381
382 $scope.isNewActivitySetAllowed = function(workflow) {
383 switch (workflow) {
384 case 'timeline':
385 return true;
386 case 'sequence':
387 return 0 === _.where($scope.caseType.definition.activitySets, {sequence: '1'}).length;
388 default:
389 CRM.console('warn', 'Denied access to unrecognized workflow: (' + workflow + ')');
390 return false;
391 }
392 };
393
394 $scope.isActivityRemovable = function(activitySet, activity) {
395 return true;
396 };
397
398 $scope.isValidName = function(name) {
399 return !name || name.match(/^[a-zA-Z0-9_]+$/);
400 };
401
402 $scope.getWorkflowName = function(activitySet) {
403 var result = 'Unknown';
404 _.each($scope.workflows, function(value, key) {
405 if (activitySet[key]) result = value;
406 });
407 return result;
408 };
409
410 /**
411 * Determine which HTML partial to use for a particular
412 *
413 * @return string URL of the HTML partial
414 */
415 $scope.activityTableTemplate = function(activitySet) {
416 if (activitySet.timeline) {
417 return '~/crmCaseType/timelineTable.html';
418 } else if (activitySet.sequence) {
419 return '~/crmCaseType/sequenceTable.html';
420 } else {
421 return '';
422 }
423 };
424
425 $scope.dump = function() {
426 console.log($scope.caseType);
427 };
428
429 $scope.save = function() {
430 // Add selected statuses
431 var selectedStatuses = [];
432 _.each($scope.selectedStatuses, function(v, k) {
433 if (v) selectedStatuses.push(k);
434 });
435 // Ignore if ALL or NONE selected
436 $scope.caseType.definition.statuses = selectedStatuses.length == _.size($scope.selectedStatuses) ? [] : selectedStatuses;
437 var result = crmApi('CaseType', 'create', $scope.caseType, true);
438 result.then(function(data) {
439 if (data.is_error === 0 || data.is_error == '0') {
440 $scope.caseType.id = data.id;
441 window.location.href = '#/caseType';
442 }
443 });
444 };
445
446 $scope.$watchCollection('caseType.definition.activitySets', function() {
447 _.defer(function() {
448 $('.crmCaseType-acttab').tabs('refresh');
449 });
450 });
451
452 var updateCaseTypeName = function () {
453 if (!$scope.caseType.id && $scope.locks.caseTypeName) {
454 // Should we do some filtering? Lowercase? Strip whitespace?
455 var t = $scope.caseType.title ? $scope.caseType.title : '';
456 $scope.caseType.name = t.replace(/ /g, '_').replace(/[^a-zA-Z0-9_]/g, '').toLowerCase();
457 }
458 };
459 $scope.$watch('locks.caseTypeName', updateCaseTypeName);
460 $scope.$watch('caseType.title', updateCaseTypeName);
461
462 if (!$scope.isForkable()) {
463 CRM.alert(ts('The CiviCase XML file for this case-type prohibits editing the definition.'));
464 }
465 });
466
467 crmCaseType.controller('CaseTypeListCtrl', function($scope, crmApi, caseTypes) {
468 var ts = $scope.ts = CRM.ts(null);
469
470 $scope.caseTypes = caseTypes.values;
471 $scope.toggleCaseType = function (caseType) {
472 caseType.is_active = (caseType.is_active == '1') ? '0' : '1';
473 crmApi('CaseType', 'create', caseType, true)
474 .catch(function (data) {
475 caseType.is_active = (caseType.is_active == '1') ? '0' : '1'; // revert
476 $scope.$digest();
477 });
478 };
479 $scope.deleteCaseType = function (caseType) {
480 crmApi('CaseType', 'delete', {id: caseType.id}, {
481 error: function (data) {
482 CRM.alert(data.error_message, ts('Error'), 'error');
483 }
484 })
485 .then(function (data) {
486 delete caseTypes.values[caseType.id];
487 });
488 };
489 $scope.revertCaseType = function (caseType) {
490 caseType.definition = 'null';
491 caseType.is_forked = '0';
492 crmApi('CaseType', 'create', caseType, true)
493 .catch(function (data) {
494 caseType.is_forked = '1'; // restore
495 $scope.$digest();
496 });
497 };
498 });
499
500 })(angular, CRM.$, CRM._);