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