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 | ||
d9f57ab1 | 242 | crmCaseType.controller('CaseTypeCtrl', function($scope, 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(); | |
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; | |
2058bf54 D |
266 | // for dropdown lists, only include enabled choices |
267 | $scope.relationshipTypeOptions = getRelationshipTypeOptions(true); | |
268 | // for comparisons, include disabled | |
269 | $scope.relationshipTypeOptionsAll = getRelationshipTypeOptions(false); | |
ad8d1ce3 RO |
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 | ||
bb8b702c AF |
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 | // | |
2058bf54 D |
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). | |
bb8b702c AF |
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 | |
2058bf54 | 286 | // names must follow the convention in the XML of describing case roles |
bb8b702c | 287 | // from the perspective of the non-client. |
2058bf54 D |
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) { | |
bb8b702c | 308 | var isBidirectionalRelationship = relType.label_a_b === relType.label_b_a; |
bb8b702c | 309 | |
2058bf54 D |
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) { | |
bb8b702c | 335 | result.push({ |
2058bf54 D |
336 | xmlName: relType.name_b_a, |
337 | text: relType.label_a_b, | |
338 | id: relType.id + '_b_a' | |
bb8b702c | 339 | }); |
bb8b702c AF |
340 | } |
341 | }, []); | |
342 | } | |
343 | ||
ad8d1ce3 RO |
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 | } | |
76e4acb8 | 354 | |
ad8d1ce3 RO |
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 || []; | |
06f21064 TO |
363 | $scope.caseType.definition.restrictActivityAsgmtToCmsUser = $scope.caseType.definition.restrictActivityAsgmtToCmsUser || 0; |
364 | $scope.caseType.definition.activityAsgmtGrps = $scope.caseType.definition.activityAsgmtGrps || []; | |
ad8d1ce3 RO |
365 | |
366 | _.each($scope.caseType.definition.activitySets, function (set) { | |
367 | _.each(set.activityTypes, function (type, name) { | |
368 | var isDefaultAssigneeTypeUndefined = _.isUndefined(type.default_assignee_type); | |
1e921db0 RO |
369 | var typeDefinition = $scope.activityTypes[type.name]; |
370 | type.label = (typeDefinition && typeDefinition.label) || type.name; | |
ad8d1ce3 RO |
371 | |
372 | if (isDefaultAssigneeTypeUndefined) { | |
373 | type.default_assignee_type = defaultAssigneeDefaultValue.value; | |
374 | } | |
375 | }); | |
be5aae33 | 376 | }); |
bb8b702c | 377 | |
2058bf54 D |
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. | |
bb8b702c | 381 | _.each($scope.caseType.definition.caseRoles, function (set) { |
2058bf54 D |
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; | |
bb8b702c AF |
391 | } |
392 | }); | |
393 | }); | |
ad8d1ce3 | 394 | } |
7c2b40d1 | 395 | |
ad8d1ce3 RO |
396 | /// initializes the selected statuses |
397 | function initSelectedStatuses() { | |
398 | $scope.selectedStatuses = {}; | |
093f1cfd | 399 | |
ad8d1ce3 RO |
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 | } | |
4c58e251 | 404 | |
76e4acb8 TO |
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 | ||
4324b8d7 CW |
422 | function formatActivityTypeOption(type) { |
423 | return {id: type.name, text: type.label, icon: type.icon}; | |
424 | } | |
425 | ||
426 | function addActivityToSet(activitySet, activityTypeName) { | |
0f25eb9c SA |
427 | activitySet.activityTypes = activitySet.activityTypes || []; |
428 | var activity = { | |
093f1cfd AP |
429 | name: activityTypeName, |
430 | label: $scope.activityTypes[activityTypeName].label, | |
431 | status: 'Scheduled', | |
432 | reference_activity: 'Open Case', | |
433 | reference_offset: '1', | |
ad8d1ce3 RO |
434 | reference_select: 'newest', |
435 | default_assignee_type: $scope.defaultAssigneeTypeValues.NONE | |
093f1cfd AP |
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 | }); | |
4324b8d7 CW |
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 | }); | |
60dd172b | 470 | } |
d7c25f6c TO |
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)) { | |
4324b8d7 CW |
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 | } | |
d7c25f6c TO |
485 | } |
486 | }; | |
487 | ||
ad8d1ce3 RO |
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 | ||
2058bf54 D |
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. | |
bafce1db | 519 | var names = _.pluck($scope.caseType.definition.caseRoles, 'name'); |
2058bf54 D |
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}); | |
4324b8d7 | 525 | } |
2058bf54 D |
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 | }); | |
60dd172b | 536 | } |
8c7e0ae8 TO |
537 | }; |
538 | ||
2058bf54 D |
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. | |
cdd01a31 | 547 | $scope.addRoleOnTheFly = function(roles, newType) { |
2058bf54 D |
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. | |
cdd01a31 | 575 | if (newType.label_a_b != newType.label_b_a) { |
2058bf54 D |
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); | |
cdd01a31 D |
583 | } |
584 | }; | |
585 | ||
4c58e251 TO |
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); | |
093f1cfd | 598 | resetTimelineActivityTypes(); |
4c58e251 TO |
599 | } |
600 | }; | |
601 | ||
b40b4114 | 602 | $scope.isForkable = function() { |
f2bad133 | 603 | return !$scope.caseType.id || $scope.caseType.is_forkable; |
b40b4114 TO |
604 | }; |
605 | ||
7c2b40d1 CW |
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 | ||
5d973e24 TO |
615 | $scope.isNewActivitySetAllowed = function(workflow) { |
616 | switch (workflow) { | |
617 | case 'timeline': | |
618 | return true; | |
b387506c | 619 | case 'sequence': |
b04e5ffb | 620 | return 0 === _.where($scope.caseType.definition.activitySets, {sequence: '1'}).length; |
5d973e24 | 621 | default: |
bba9b4f0 | 622 | CRM.console('warn', 'Denied access to unrecognized workflow: (' + workflow + ')'); |
5d973e24 TO |
623 | return false; |
624 | } | |
625 | }; | |
626 | ||
259a7652 | 627 | $scope.isActivityRemovable = function(activitySet, activity) { |
12b84ade | 628 | return true; |
259a7652 TO |
629 | }; |
630 | ||
f42b448f TO |
631 | $scope.isValidName = function(name) { |
632 | return !name || name.match(/^[a-zA-Z0-9_]+$/); | |
633 | }; | |
634 | ||
4c58e251 | 635 | $scope.getWorkflowName = function(activitySet) { |
76e4acb8 TO |
636 | var result = 'Unknown'; |
637 | _.each($scope.workflows, function(value, key) { | |
638 | if (activitySet[key]) result = value; | |
639 | }); | |
640 | return result; | |
4c58e251 TO |
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) { | |
ef5d18a1 | 650 | return '~/crmCaseType/timelineTable.html'; |
b387506c | 651 | } else if (activitySet.sequence) { |
ef5d18a1 | 652 | return '~/crmCaseType/sequenceTable.html'; |
4c58e251 TO |
653 | } else { |
654 | return ''; | |
655 | } | |
656 | }; | |
657 | ||
658 | $scope.dump = function() { | |
659 | console.log($scope.caseType); | |
76e4acb8 TO |
660 | }; |
661 | ||
aa1a7c2e | 662 | $scope.save = function() { |
7c2b40d1 CW |
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; | |
06f21064 TO |
670 | |
671 | if ($scope.caseType.definition.activityAsgmtGrps) { | |
672 | $scope.caseType.definition.activityAsgmtGrps = $scope.caseType.definition.activityAsgmtGrps.toString().split(","); | |
673 | } | |
674 | ||
bb8b702c | 675 | function dropDisplaylabel (v) { |
671a5fef | 676 | delete v.displayLabel; |
bb8b702c AF |
677 | } |
678 | ||
679 | // strip out labels from $scope.caseType.definition.caseRoles | |
680 | _.map($scope.caseType.definition.caseRoles, dropDisplaylabel); | |
681 | ||
c7bccb5f | 682 | var result = crmApi('CaseType', 'create', $scope.caseType, true); |
3140a415 | 683 | result.then(function(data) { |
b04e5ffb | 684 | if (data.is_error === 0 || data.is_error == '0') { |
c7bccb5f | 685 | $scope.caseType.id = data.id; |
1ab5b88e | 686 | window.location.href = '#/caseType'; |
c7bccb5f | 687 | } |
688 | }); | |
aa1a7c2e TO |
689 | }; |
690 | ||
76e4acb8 TO |
691 | $scope.$watchCollection('caseType.definition.activitySets', function() { |
692 | _.defer(function() { | |
8fc6fba7 | 693 | $('.crmCaseType-acttab').tabs('refresh'); |
76e4acb8 TO |
694 | }); |
695 | }); | |
685acae4 | 696 | |
697 | var updateCaseTypeName = function () { | |
698 | if (!$scope.caseType.id && $scope.locks.caseTypeName) { | |
699 | // Should we do some filtering? Lowercase? Strip whitespace? | |
a5ca1f48 | 700 | var t = $scope.caseType.title ? $scope.caseType.title : ''; |
701 | $scope.caseType.name = t.replace(/ /g, '_').replace(/[^a-zA-Z0-9_]/g, '').toLowerCase(); | |
685acae4 | 702 | } |
703 | }; | |
704 | $scope.$watch('locks.caseTypeName', updateCaseTypeName); | |
705 | $scope.$watch('caseType.title', updateCaseTypeName); | |
b40b4114 TO |
706 | |
707 | if (!$scope.isForkable()) { | |
708 | CRM.alert(ts('The CiviCase XML file for this case-type prohibits editing the definition.')); | |
709 | } | |
093f1cfd | 710 | |
4c58e251 TO |
711 | }); |
712 | ||
b75c2546 | 713 | crmCaseType.controller('CaseTypeListCtrl', function($scope, crmApi, caseTypes) { |
7abbf317 CW |
714 | var ts = $scope.ts = CRM.ts(null); |
715 | ||
b75c2546 | 716 | $scope.caseTypes = caseTypes.values; |
4b8c8b42 TO |
717 | $scope.toggleCaseType = function (caseType) { |
718 | caseType.is_active = (caseType.is_active == '1') ? '0' : '1'; | |
719 | crmApi('CaseType', 'create', caseType, true) | |
c99f1a0a TO |
720 | .catch(function (data) { |
721 | caseType.is_active = (caseType.is_active == '1') ? '0' : '1'; // revert | |
722 | $scope.$digest(); | |
4b8c8b42 TO |
723 | }); |
724 | }; | |
725 | $scope.deleteCaseType = function (caseType) { | |
eb8e4c2d TO |
726 | crmApi('CaseType', 'delete', {id: caseType.id}, { |
727 | error: function (data) { | |
7abbf317 | 728 | CRM.alert(data.error_message, ts('Error'), 'error'); |
eb8e4c2d TO |
729 | } |
730 | }) | |
4b8c8b42 | 731 | .then(function (data) { |
c99f1a0a | 732 | delete caseTypes.values[caseType.id]; |
4b8c8b42 TO |
733 | }); |
734 | }; | |
470a458e TO |
735 | $scope.revertCaseType = function (caseType) { |
736 | caseType.definition = 'null'; | |
737 | caseType.is_forked = '0'; | |
738 | crmApi('CaseType', 'create', caseType, true) | |
c99f1a0a TO |
739 | .catch(function (data) { |
740 | caseType.is_forked = '1'; // restore | |
741 | $scope.$digest(); | |
470a458e TO |
742 | }); |
743 | }; | |
b75c2546 TO |
744 | }); |
745 | ||
bba9b4f0 | 746 | })(angular, CRM.$, CRM._); |