Commit | Line | Data |
---|---|---|
4c58e251 TO |
1 | (function(angular, $, _) { |
2 | ||
0b199194 | 3 | var crmCaseType = angular.module('crmCaseType', CRM.angRequires('crmCaseType')); |
4c58e251 | 4 | |
506cd414 | 5 | // Note: This template will be passed to cloneDeep(), so don't put any funny stuff in here! |
9be5fc34 TO |
6 | var newCaseTypeTemplate = { |
7 | title: "", | |
8 | name: "", | |
9 | is_active: "1", | |
10 | weight: "1", | |
11 | definition: { | |
12 | activityTypes: [ | |
4d8bbcf6 TO |
13 | {name: 'Open Case', max_instances: 1}, |
14 | {name: 'Email'}, | |
15 | {name: 'Follow up'}, | |
16 | {name: 'Meeting'}, | |
17 | {name: 'Phone Call'} | |
9be5fc34 TO |
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 | } | |
4d74de55 TO |
33 | }; |
34 | ||
4c58e251 TO |
35 | crmCaseType.config(['$routeProvider', |
36 | function($routeProvider) { | |
b75c2546 | 37 | $routeProvider.when('/caseType', { |
ef5d18a1 | 38 | templateUrl: '~/crmCaseType/list.html', |
b75c2546 TO |
39 | controller: 'CaseTypeListCtrl', |
40 | resolve: { | |
41 | caseTypes: function($route, crmApi) { | |
a214ce43 | 42 | return crmApi('CaseType', 'get', {options: {limit: 0}}); |
b75c2546 TO |
43 | } |
44 | } | |
45 | }); | |
4c58e251 | 46 | $routeProvider.when('/caseType/:id', { |
ef5d18a1 | 47 | templateUrl: '~/crmCaseType/edit.html', |
4d74de55 TO |
48 | controller: 'CaseTypeCtrl', |
49 | resolve: { | |
9625aad1 TO |
50 | apiCalls: function($route, crmApi) { |
51 | var reqs = {}; | |
52 | reqs.actStatuses = ['OptionValue', 'get', { | |
7c2b40d1 CW |
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, | |
6a55cb01 | 60 | options: { |
61 | sort: 'weight', | |
62 | limit: 0 | |
63 | } | |
9625aad1 TO |
64 | }]; |
65 | reqs.actTypes = ['OptionValue', 'get', { | |
66 | option_group_id: 'activity_type', | |
4324b8d7 | 67 | sequential: 1, |
9625aad1 TO |
68 | options: { |
69 | sort: 'name', | |
70 | limit: 0 | |
71 | } | |
72 | }]; | |
ad8d1ce3 RO |
73 | reqs.defaultAssigneeTypes = ['OptionValue', 'get', { |
74 | option_group_id: 'activity_default_assignee', | |
75 | sequential: 1, | |
76 | options: { | |
77 | limit: 0 | |
78 | } | |
79 | }]; | |
bb8b702c | 80 | reqs.relTypes = ['RelationshipType', 'get', { |
4324b8d7 | 81 | sequential: 1, |
9625aad1 | 82 | options: { |
bb8b702c | 83 | sort: 'label_a_b', |
9625aad1 TO |
84 | limit: 0 |
85 | } | |
86 | }]; | |
87 | if ($route.current.params.id !== 'new') { | |
88 | reqs.caseType = ['CaseType', 'getsingle', { | |
89 | id: $route.current.params.id | |
90 | }]; | |
87dcd909 | 91 | } |
9625aad1 | 92 | return crmApi(reqs); |
4d74de55 TO |
93 | } |
94 | } | |
4c58e251 TO |
95 | }); |
96 | } | |
97 | ]); | |
98 | ||
95fd24c0 TO |
99 | // Add a new record by name. |
100 | // Ex: <crmAddName crm-options="['Alpha','Beta','Gamma']" crm-var="newItem" crm-on-add="callMyCreateFunction(newItem)" /> | |
8fc6fba7 | 101 | crmCaseType.directive('crmAddName', function() { |
95fd24c0 TO |
102 | return { |
103 | restrict: 'AE', | |
9597c394 | 104 | template: '<input class="add-activity crm-action-menu fa-plus" type="hidden" />', |
bafce1db | 105 | link: function(scope, element, attrs) { |
bafce1db TO |
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 | ||
4324b8d7 | 115 | $(input).crmSelect2({ |
c0bb8bd4 DB |
116 | data: function () { |
117 | return { results: scope[attrs.crmOptions] }; | |
118 | }, | |
bafce1db | 119 | createSearchChoice: function(term) { |
4324b8d7 | 120 | return {id: term, text: term + ' (' + ts('new') + ')'}; |
00eee619 | 121 | }, |
4324b8d7 | 122 | createSearchChoicePosition: 'bottom', |
00eee619 | 123 | placeholder: attrs.placeholder |
bafce1db TO |
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 | } | |
95fd24c0 TO |
132 | }; |
133 | }); | |
134 | ||
d7a470db DB |
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'); | |
13a3d214 AH |
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); | |
d7a470db DB |
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 | ||
fef937a8 | 242 | crmCaseType.controller('CaseTypeCtrl', function($scope, $timeout, crmApi, apiCalls, crmUiHelp) { |
41cf58d3 | 243 | var defaultAssigneeDefaultValue, ts; |
ad8d1ce3 RO |
244 | |
245 | (function init () { | |
ad8d1ce3 RO |
246 | |
247 | ts = $scope.ts = CRM.ts(null); | |
d9f57ab1 | 248 | $scope.hs = crmUiHelp({file: 'CRM/Case/CaseType'}); |
ad8d1ce3 RO |
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(); | |
fef937a8 CW |
257 | |
258 | $timeout(function() { | |
259 | $('form[name=editCaseTypeForm] .crmCaseType-acttab').tabs({show: true, hide: true}); | |
260 | }); | |
ad8d1ce3 RO |
261 | })(); |
262 | ||
263 | /// Stores the api calls results in the $scope object | |
264 | function storeApiCallsResults() { | |
265 | $scope.activityStatuses = apiCalls.actStatuses.values; | |
266 | $scope.caseStatuses = _.indexBy(apiCalls.caseStatuses.values, 'name'); | |
267 | $scope.activityTypes = _.indexBy(apiCalls.actTypes.values, 'name'); | |
268 | $scope.activityTypeOptions = _.map(apiCalls.actTypes.values, formatActivityTypeOption); | |
269 | $scope.defaultAssigneeTypes = apiCalls.defaultAssigneeTypes.values; | |
2058bf54 D |
270 | // for dropdown lists, only include enabled choices |
271 | $scope.relationshipTypeOptions = getRelationshipTypeOptions(true); | |
272 | // for comparisons, include disabled | |
273 | $scope.relationshipTypeOptionsAll = getRelationshipTypeOptions(false); | |
ad8d1ce3 RO |
274 | // stores the default assignee values indexed by their option name: |
275 | $scope.defaultAssigneeTypeValues = _.chain($scope.defaultAssigneeTypes) | |
276 | .indexBy('name').mapValues('value').value(); | |
277 | } | |
278 | ||
bb8b702c AF |
279 | // Returns the relationship type options. If the relationship is |
280 | // bidirectional (Ex: Spouse of) it adds a single option otherwise it adds | |
281 | // two options representing the relationship type directions (Ex: Employee | |
282 | // of, Employer of). | |
283 | // | |
2058bf54 D |
284 | // The relationship dropdown needs to be given IDs with direction, |
285 | // while the role name in the xml needs values that are names (with | |
286 | // implicit direction). | |
bb8b702c AF |
287 | // |
288 | // At any rate, the labels should follow the convention in the UI of | |
289 | // describing case roles from the perspective of the client, while the | |
2058bf54 | 290 | // names must follow the convention in the XML of describing case roles |
bb8b702c | 291 | // from the perspective of the non-client. |
2058bf54 D |
292 | // |
293 | // @param onlyActive bool | |
294 | // If true, only include enabled relationship types. | |
295 | // @return array[object] | |
296 | // object: { | |
297 | // xmlName: The name corresponding to what's stored in xml/caseRoles. | |
298 | // text: The text in dropdowns, i.e. <option value="id">text</option> | |
299 | // It's called text because that is what select2 is expecting. | |
300 | // id: The id value in dropdowns, i.e. <option value="id">text</option> | |
301 | // Is the concatenation of id+direction, e.g. 2_a_b. | |
302 | // It's called id because that is what select2 is expecting. | |
303 | // } | |
304 | function getRelationshipTypeOptions(onlyActive) { | |
305 | var relationshipTypesToUse; | |
306 | if (onlyActive) { | |
307 | relationshipTypesToUse = _.filter(apiCalls.relTypes.values, {is_active: "1"}); | |
308 | } else { | |
309 | relationshipTypesToUse = apiCalls.relTypes.values; | |
310 | } | |
311 | return _.transform(relationshipTypesToUse, function(result, relType) { | |
bb8b702c | 312 | var isBidirectionalRelationship = relType.label_a_b === relType.label_b_a; |
bb8b702c | 313 | |
2058bf54 D |
314 | // The order here of a's and b's here is important regarding |
315 | // unidirectional and bidirectional. Because the xml spec DOES support | |
316 | // direction for activity auto-assignees, if this is changed you might | |
317 | // end up with activity assignees in existing xml that then come | |
318 | // through as blank in the dropdown on the timelines tab if it's a | |
319 | // bidirectional relationship. E.g. if spouse is stored in an existing | |
320 | // xml definition as 2_a_b, but we only push 2_b_a, then it won't match | |
321 | // in the dropdown. | |
322 | // | |
323 | // This has some implications for when we add a new type on the fly | |
324 | // later, but it works out ok as long as we also do it in the same | |
325 | // direction there. See notes in addRoleOnTheFly(). | |
326 | result.push({ | |
327 | // This is what we want to store in the caseRoles.name field, | |
328 | // which corresponds to the xml file, when we send it back to | |
329 | // the server. | |
330 | xmlName: relType.name_a_b, | |
331 | // This has to be called text because select2 is expecting it. | |
332 | // And yes it's the opposite direction from name. | |
333 | text: relType.label_b_a, | |
334 | // This has to be called id because select2 is expecting it. | |
335 | id: relType.id + '_a_b' | |
336 | }); | |
337 | ||
338 | if (!isBidirectionalRelationship) { | |
bb8b702c | 339 | result.push({ |
2058bf54 D |
340 | xmlName: relType.name_b_a, |
341 | text: relType.label_a_b, | |
342 | id: relType.id + '_b_a' | |
bb8b702c | 343 | }); |
bb8b702c AF |
344 | } |
345 | }, []); | |
346 | } | |
347 | ||
ad8d1ce3 RO |
348 | /// initializes the case type object |
349 | function initCaseType() { | |
350 | var isNewCaseType = !apiCalls.caseType; | |
351 | ||
352 | if (isNewCaseType) { | |
353 | $scope.caseType = _.cloneDeep(newCaseTypeTemplate); | |
354 | } else { | |
355 | $scope.caseType = apiCalls.caseType; | |
356 | } | |
357 | } | |
76e4acb8 | 358 | |
ad8d1ce3 RO |
359 | /// initializes the case type definition object |
360 | function initCaseTypeDefinition() { | |
361 | $scope.caseType.definition = $scope.caseType.definition || []; | |
362 | $scope.caseType.definition.activityTypes = $scope.caseType.definition.activityTypes || []; | |
363 | $scope.caseType.definition.activitySets = $scope.caseType.definition.activitySets || []; | |
364 | $scope.caseType.definition.caseRoles = $scope.caseType.definition.caseRoles || []; | |
365 | $scope.caseType.definition.statuses = $scope.caseType.definition.statuses || []; | |
366 | $scope.caseType.definition.timelineActivityTypes = $scope.caseType.definition.timelineActivityTypes || []; | |
06f21064 TO |
367 | $scope.caseType.definition.restrictActivityAsgmtToCmsUser = $scope.caseType.definition.restrictActivityAsgmtToCmsUser || 0; |
368 | $scope.caseType.definition.activityAsgmtGrps = $scope.caseType.definition.activityAsgmtGrps || []; | |
ad8d1ce3 RO |
369 | |
370 | _.each($scope.caseType.definition.activitySets, function (set) { | |
371 | _.each(set.activityTypes, function (type, name) { | |
372 | var isDefaultAssigneeTypeUndefined = _.isUndefined(type.default_assignee_type); | |
1e921db0 RO |
373 | var typeDefinition = $scope.activityTypes[type.name]; |
374 | type.label = (typeDefinition && typeDefinition.label) || type.name; | |
ad8d1ce3 RO |
375 | |
376 | if (isDefaultAssigneeTypeUndefined) { | |
377 | type.default_assignee_type = defaultAssigneeDefaultValue.value; | |
378 | } | |
379 | }); | |
be5aae33 | 380 | }); |
bb8b702c | 381 | |
2058bf54 D |
382 | // Go lookup and add client-perspective labels for |
383 | // $scope.caseType.definition.caseRoles, since the xml doesn't have them | |
384 | // and we need to display them in the roles table. | |
bb8b702c | 385 | _.each($scope.caseType.definition.caseRoles, function (set) { |
2058bf54 D |
386 | _.each($scope.relationshipTypeOptionsAll, function (relationshipTypeOption) { |
387 | if (relationshipTypeOption.xmlName == set.name) { | |
388 | // relationshipTypeOption.text here corresponds to one of the | |
389 | // apiCalls.relTypes label fields (i.e. civicrm_relationship_type | |
390 | // label database fields). It has to be called text because | |
391 | // it's used in select2 which expects it to be called text. | |
392 | set.displayLabel = relationshipTypeOption.text; | |
393 | // break out of inner `each` loop | |
394 | return false; | |
bb8b702c AF |
395 | } |
396 | }); | |
397 | }); | |
ad8d1ce3 | 398 | } |
7c2b40d1 | 399 | |
ad8d1ce3 RO |
400 | /// initializes the selected statuses |
401 | function initSelectedStatuses() { | |
402 | $scope.selectedStatuses = {}; | |
093f1cfd | 403 | |
ad8d1ce3 RO |
404 | _.each(apiCalls.caseStatuses.values, function (status) { |
405 | $scope.selectedStatuses[status.name] = !$scope.caseType.definition.statuses.length || $scope.caseType.definition.statuses.indexOf(status.name) > -1; | |
406 | }); | |
407 | } | |
4c58e251 | 408 | |
76e4acb8 TO |
409 | $scope.addActivitySet = function(workflow) { |
410 | var activitySet = {}; | |
411 | activitySet[workflow] = '1'; | |
412 | activitySet.activityTypes = []; | |
413 | ||
414 | var offset = 1; | |
415 | var names = _.pluck($scope.caseType.definition.activitySets, 'name'); | |
416 | while (_.contains(names, workflow + '_' + offset)) offset++; | |
417 | activitySet.name = workflow + '_' + offset; | |
418 | activitySet.label = (offset == 1 ) ? $scope.workflows[workflow] : ($scope.workflows[workflow] + ' #' + offset); | |
419 | ||
420 | $scope.caseType.definition.activitySets.push(activitySet); | |
421 | _.defer(function() { | |
422 | $('.crmCaseType-acttab').tabs('refresh').tabs({active: -1}); | |
423 | }); | |
424 | }; | |
425 | ||
4324b8d7 CW |
426 | function formatActivityTypeOption(type) { |
427 | return {id: type.name, text: type.label, icon: type.icon}; | |
428 | } | |
429 | ||
430 | function addActivityToSet(activitySet, activityTypeName) { | |
0f25eb9c SA |
431 | activitySet.activityTypes = activitySet.activityTypes || []; |
432 | var activity = { | |
093f1cfd AP |
433 | name: activityTypeName, |
434 | label: $scope.activityTypes[activityTypeName].label, | |
435 | status: 'Scheduled', | |
436 | reference_activity: 'Open Case', | |
437 | reference_offset: '1', | |
ad8d1ce3 RO |
438 | reference_select: 'newest', |
439 | default_assignee_type: $scope.defaultAssigneeTypeValues.NONE | |
093f1cfd AP |
440 | }; |
441 | activitySet.activityTypes.push(activity); | |
442 | if(typeof activitySet.timeline !== "undefined" && activitySet.timeline == "1") { | |
443 | $scope.caseType.definition.timelineActivityTypes.push(activity); | |
444 | } | |
445 | } | |
446 | ||
447 | function resetTimelineActivityTypes() { | |
448 | $scope.caseType.definition.timelineActivityTypes = []; | |
449 | angular.forEach($scope.caseType.definition.activitySets, function(activitySet) { | |
450 | angular.forEach(activitySet.activityTypes, function(activityType) { | |
451 | $scope.caseType.definition.timelineActivityTypes.push(activityType); | |
452 | }); | |
453 | }); | |
4324b8d7 CW |
454 | } |
455 | ||
456 | function createActivity(name, callback) { | |
457 | CRM.loadForm(CRM.url('civicrm/admin/options/activity_type', {action: 'add', reset: 1, label: name, component_id: 7})) | |
458 | .on('crmFormSuccess', function(e, data) { | |
459 | $scope.activityTypes[data.optionValue.name] = data.optionValue; | |
460 | $scope.activityTypeOptions.push(formatActivityTypeOption(data.optionValue)); | |
461 | callback(data.optionValue); | |
462 | $scope.$digest(); | |
463 | }); | |
464 | } | |
465 | ||
466 | // Add a new activity entry to an activity-set | |
467 | $scope.addActivity = function(activitySet, activityType) { | |
468 | if ($scope.activityTypes[activityType]) { | |
469 | addActivityToSet(activitySet, activityType); | |
470 | } else { | |
471 | createActivity(activityType, function(newActivity) { | |
472 | addActivityToSet(activitySet, newActivity.name); | |
473 | }); | |
60dd172b | 474 | } |
d7c25f6c TO |
475 | }; |
476 | ||
477 | /// Add a new top-level activity-type entry | |
478 | $scope.addActivityType = function(activityType) { | |
479 | var names = _.pluck($scope.caseType.definition.activityTypes, 'name'); | |
480 | if (!_.contains(names, activityType)) { | |
4324b8d7 CW |
481 | // Add an activity type that exists |
482 | if ($scope.activityTypes[activityType]) { | |
483 | $scope.caseType.definition.activityTypes.push({name: activityType}); | |
484 | } else { | |
485 | createActivity(activityType, function(newActivity) { | |
486 | $scope.caseType.definition.activityTypes.push({name: newActivity.name}); | |
487 | }); | |
488 | } | |
d7c25f6c TO |
489 | } |
490 | }; | |
491 | ||
ad8d1ce3 RO |
492 | /// Clears the activity's default assignee values for relationship and contact |
493 | $scope.clearActivityDefaultAssigneeValues = function(activity) { | |
494 | activity.default_assignee_relationship = null; | |
495 | activity.default_assignee_contact = null; | |
496 | }; | |
497 | ||
2058bf54 D |
498 | // Add a new role. |
499 | // Called from the select2 dropdown when a selection is made. | |
500 | // | |
501 | // @param roles array | |
502 | // The roles currently in the table. | |
503 | // @param roleIdOrLabel string | |
504 | // The trick here is that since you can add roles on the fly, the | |
505 | // roleIdOrLabel parameter can be two different types of things. It can be | |
506 | // the id, like '2_a_b' if it's an existing choice that was selected, or | |
507 | // it can be a LABEL if they typed something that isn't in the list. If | |
508 | // the latter the select2 has no choice but to give us a label because | |
509 | // there is no id yet. | |
510 | $scope.addRole = function(roles, roleIdOrLabel) { | |
511 | var matchingRole; | |
512 | // First check does what we've been given match up to any relationship | |
513 | // type, based on id, which is the id from the select2 (i.e. html | |
514 | // <option value="id">) | |
515 | var matchingRoles = _.filter($scope.relationshipTypeOptions, {id: roleIdOrLabel}); | |
516 | if (matchingRoles.length) { | |
517 | matchingRole = matchingRoles.shift(); | |
518 | } | |
519 | // If found, is the corresponding machine name in the list of existing | |
520 | // roles for the case type. Unfortunately, caseRoles only stores name, | |
521 | // which doesn't indicate the id or direction, because the xml spec | |
522 | // doesn't support those. | |
bafce1db | 523 | var names = _.pluck($scope.caseType.definition.caseRoles, 'name'); |
2058bf54 D |
524 | if (matchingRole) { |
525 | // If it's not in the table already, add it, otherwise do nothing since | |
526 | // don't want to add it twice. | |
527 | if (!_.contains(names, matchingRole.xmlName)) { | |
528 | roles.push({name: matchingRole.xmlName, displayLabel: matchingRole.text}); | |
4324b8d7 | 529 | } |
2058bf54 D |
530 | } else { |
531 | // Not a known relationship type, so create on-the-fly. | |
532 | // At this point roleIdOrLabel must be the new label they just typed. | |
533 | CRM.loadForm(CRM.url('civicrm/admin/reltype', {action: 'add', reset: 1, label_a_b: roleIdOrLabel})) | |
534 | .on('crmFormSuccess', function(e, data) { | |
535 | var newType = _.values(data.relationshipType)[0]; | |
536 | $scope.$apply(function() { | |
537 | $scope.addRoleOnTheFly(roles, newType); | |
538 | }); | |
539 | }); | |
60dd172b | 540 | } |
8c7e0ae8 TO |
541 | }; |
542 | ||
2058bf54 D |
543 | // Add a newly created relationship type as a role to the table and |
544 | // update the list of options. | |
545 | // | |
546 | // @param roles array | |
547 | // The roles currently in the table. | |
548 | // @param newType array | |
549 | // The array returned from the api call that created the new type | |
550 | // earlier. | |
cdd01a31 | 551 | $scope.addRoleOnTheFly = function(roles, newType) { |
2058bf54 D |
552 | // Add it to the roles table. Assume they want the A-B direction since |
553 | // that's what they would have typed. | |
554 | // Name and label are opposites here because name represents the value | |
555 | // in the xml here which historically is the opposite. | |
556 | roles.push({name: newType.name_b_a, displayLabel: newType.label_a_b}); | |
557 | ||
558 | // But now add both directions as option choices for future dropdown | |
559 | // selections. | |
560 | // Note that to keep in line with the original population on init, | |
561 | // we're pushing a different direction here than we just added to the | |
562 | // table, but there's only two possibilities: | |
563 | // 1. Labels are the same, and since it's a new type name_a_b and | |
564 | // name_b_a will therefore be the same, and so it doesn't matter which | |
565 | // name is stored in caseRoles. | |
566 | // 2. Labels are different, in which case we're also going to push the | |
567 | // other direction below. | |
568 | // So either way we're covered. | |
569 | // See also note in getRelationshipTypeOptions(). | |
570 | var newRelTypeOption = { | |
571 | xmlName: newType.name_a_b, | |
572 | // Yes text is the opposite direction from name here. | |
573 | text: newType.label_b_a, | |
574 | id: newType.id + '_a_b' | |
575 | }; | |
576 | $scope.relationshipTypeOptions.push(newRelTypeOption); | |
577 | $scope.relationshipTypeOptionsAll.push(newRelTypeOption); | |
578 | // Add the other direction if different. | |
cdd01a31 | 579 | if (newType.label_a_b != newType.label_b_a) { |
2058bf54 D |
580 | newRelTypeOption = { |
581 | xmlName: newType.name_b_a, | |
582 | text: newType.label_a_b, | |
583 | id: newType.id + '_b_a' | |
584 | }; | |
585 | $scope.relationshipTypeOptions.push(newRelTypeOption); | |
586 | $scope.relationshipTypeOptionsAll.push(newRelTypeOption); | |
cdd01a31 D |
587 | } |
588 | }; | |
589 | ||
4c58e251 TO |
590 | $scope.onManagerChange = function(managerRole) { |
591 | angular.forEach($scope.caseType.definition.caseRoles, function(caseRole) { | |
592 | if (caseRole != managerRole) { | |
593 | caseRole.manager = '0'; | |
594 | } | |
595 | }); | |
596 | }; | |
597 | ||
598 | $scope.removeItem = function(array, item) { | |
599 | var idx = _.indexOf(array, item); | |
600 | if (idx != -1) { | |
601 | array.splice(idx, 1); | |
093f1cfd | 602 | resetTimelineActivityTypes(); |
4c58e251 TO |
603 | } |
604 | }; | |
605 | ||
b40b4114 | 606 | $scope.isForkable = function() { |
f2bad133 | 607 | return !$scope.caseType.id || $scope.caseType.is_forkable; |
b40b4114 TO |
608 | }; |
609 | ||
7c2b40d1 CW |
610 | $scope.newStatus = function() { |
611 | CRM.loadForm(CRM.url('civicrm/admin/options/case_status', {action: 'add', reset: 1})) | |
612 | .on('crmFormSuccess', function(e, data) { | |
613 | $scope.caseStatuses[data.optionValue.name] = data.optionValue; | |
614 | $scope.selectedStatuses[data.optionValue.name] = true; | |
615 | $scope.$digest(); | |
616 | }); | |
617 | }; | |
618 | ||
5d973e24 TO |
619 | $scope.isNewActivitySetAllowed = function(workflow) { |
620 | switch (workflow) { | |
621 | case 'timeline': | |
622 | return true; | |
b387506c | 623 | case 'sequence': |
b04e5ffb | 624 | return 0 === _.where($scope.caseType.definition.activitySets, {sequence: '1'}).length; |
5d973e24 | 625 | default: |
bba9b4f0 | 626 | CRM.console('warn', 'Denied access to unrecognized workflow: (' + workflow + ')'); |
5d973e24 TO |
627 | return false; |
628 | } | |
629 | }; | |
630 | ||
259a7652 | 631 | $scope.isActivityRemovable = function(activitySet, activity) { |
12b84ade | 632 | return true; |
259a7652 TO |
633 | }; |
634 | ||
f42b448f TO |
635 | $scope.isValidName = function(name) { |
636 | return !name || name.match(/^[a-zA-Z0-9_]+$/); | |
637 | }; | |
638 | ||
4c58e251 | 639 | $scope.getWorkflowName = function(activitySet) { |
76e4acb8 TO |
640 | var result = 'Unknown'; |
641 | _.each($scope.workflows, function(value, key) { | |
642 | if (activitySet[key]) result = value; | |
643 | }); | |
644 | return result; | |
4c58e251 TO |
645 | }; |
646 | ||
647 | /** | |
648 | * Determine which HTML partial to use for a particular | |
649 | * | |
650 | * @return string URL of the HTML partial | |
651 | */ | |
652 | $scope.activityTableTemplate = function(activitySet) { | |
653 | if (activitySet.timeline) { | |
ef5d18a1 | 654 | return '~/crmCaseType/timelineTable.html'; |
b387506c | 655 | } else if (activitySet.sequence) { |
ef5d18a1 | 656 | return '~/crmCaseType/sequenceTable.html'; |
4c58e251 TO |
657 | } else { |
658 | return ''; | |
659 | } | |
660 | }; | |
661 | ||
662 | $scope.dump = function() { | |
663 | console.log($scope.caseType); | |
76e4acb8 TO |
664 | }; |
665 | ||
aa1a7c2e | 666 | $scope.save = function() { |
7c2b40d1 CW |
667 | // Add selected statuses |
668 | var selectedStatuses = []; | |
669 | _.each($scope.selectedStatuses, function(v, k) { | |
670 | if (v) selectedStatuses.push(k); | |
671 | }); | |
672 | // Ignore if ALL or NONE selected | |
673 | $scope.caseType.definition.statuses = selectedStatuses.length == _.size($scope.selectedStatuses) ? [] : selectedStatuses; | |
06f21064 TO |
674 | |
675 | if ($scope.caseType.definition.activityAsgmtGrps) { | |
676 | $scope.caseType.definition.activityAsgmtGrps = $scope.caseType.definition.activityAsgmtGrps.toString().split(","); | |
677 | } | |
678 | ||
bb8b702c | 679 | function dropDisplaylabel (v) { |
671a5fef | 680 | delete v.displayLabel; |
bb8b702c AF |
681 | } |
682 | ||
683 | // strip out labels from $scope.caseType.definition.caseRoles | |
684 | _.map($scope.caseType.definition.caseRoles, dropDisplaylabel); | |
685 | ||
c7bccb5f | 686 | var result = crmApi('CaseType', 'create', $scope.caseType, true); |
3140a415 | 687 | result.then(function(data) { |
b04e5ffb | 688 | if (data.is_error === 0 || data.is_error == '0') { |
c7bccb5f | 689 | $scope.caseType.id = data.id; |
1ab5b88e | 690 | window.location.href = '#/caseType'; |
c7bccb5f | 691 | } |
692 | }); | |
aa1a7c2e TO |
693 | }; |
694 | ||
76e4acb8 TO |
695 | $scope.$watchCollection('caseType.definition.activitySets', function() { |
696 | _.defer(function() { | |
8fc6fba7 | 697 | $('.crmCaseType-acttab').tabs('refresh'); |
76e4acb8 TO |
698 | }); |
699 | }); | |
685acae4 | 700 | |
701 | var updateCaseTypeName = function () { | |
702 | if (!$scope.caseType.id && $scope.locks.caseTypeName) { | |
703 | // Should we do some filtering? Lowercase? Strip whitespace? | |
a5ca1f48 | 704 | var t = $scope.caseType.title ? $scope.caseType.title : ''; |
705 | $scope.caseType.name = t.replace(/ /g, '_').replace(/[^a-zA-Z0-9_]/g, '').toLowerCase(); | |
685acae4 | 706 | } |
707 | }; | |
708 | $scope.$watch('locks.caseTypeName', updateCaseTypeName); | |
709 | $scope.$watch('caseType.title', updateCaseTypeName); | |
b40b4114 TO |
710 | |
711 | if (!$scope.isForkable()) { | |
712 | CRM.alert(ts('The CiviCase XML file for this case-type prohibits editing the definition.')); | |
713 | } | |
093f1cfd | 714 | |
4c58e251 TO |
715 | }); |
716 | ||
b75c2546 | 717 | crmCaseType.controller('CaseTypeListCtrl', function($scope, crmApi, caseTypes) { |
7abbf317 CW |
718 | var ts = $scope.ts = CRM.ts(null); |
719 | ||
b75c2546 | 720 | $scope.caseTypes = caseTypes.values; |
4b8c8b42 TO |
721 | $scope.toggleCaseType = function (caseType) { |
722 | caseType.is_active = (caseType.is_active == '1') ? '0' : '1'; | |
723 | crmApi('CaseType', 'create', caseType, true) | |
c99f1a0a TO |
724 | .catch(function (data) { |
725 | caseType.is_active = (caseType.is_active == '1') ? '0' : '1'; // revert | |
726 | $scope.$digest(); | |
4b8c8b42 TO |
727 | }); |
728 | }; | |
729 | $scope.deleteCaseType = function (caseType) { | |
eb8e4c2d TO |
730 | crmApi('CaseType', 'delete', {id: caseType.id}, { |
731 | error: function (data) { | |
7abbf317 | 732 | CRM.alert(data.error_message, ts('Error'), 'error'); |
eb8e4c2d TO |
733 | } |
734 | }) | |
4b8c8b42 | 735 | .then(function (data) { |
c99f1a0a | 736 | delete caseTypes.values[caseType.id]; |
4b8c8b42 TO |
737 | }); |
738 | }; | |
470a458e TO |
739 | $scope.revertCaseType = function (caseType) { |
740 | caseType.definition = 'null'; | |
741 | caseType.is_forked = '0'; | |
742 | crmApi('CaseType', 'create', caseType, true) | |
c99f1a0a TO |
743 | .catch(function (data) { |
744 | caseType.is_forked = '1'; // restore | |
745 | $scope.$digest(); | |
470a458e TO |
746 | }); |
747 | }; | |
b75c2546 TO |
748 | }); |
749 | ||
bba9b4f0 | 750 | })(angular, CRM.$, CRM._); |