Skip to content

Commit

Permalink
dev/core#107 Automatically add default assignees when creating new cases
Browse files Browse the repository at this point in the history
This feature allows users to define default assignees for each activity in a case type.
When creating a new case contacts are assigned to activities automatically by following
one of these rules:

* By relationship to case client
* The user creating the case
* A specific contact
* None (default)
  • Loading branch information
reneolivo committed May 17, 2018
1 parent f735b1a commit 994084b
Show file tree
Hide file tree
Showing 8 changed files with 601 additions and 43 deletions.
121 changes: 121 additions & 0 deletions CRM/Case/XMLProcessor/Process.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
* @copyright CiviCRM LLC (c) 2004-2018
*/
class CRM_Case_XMLProcessor_Process extends CRM_Case_XMLProcessor {
protected $defaultAssigneeOptionsValues = [];

/**
* Run.
*
Expand Down Expand Up @@ -314,6 +316,7 @@ public function activityTypes($activityTypesXML, $maxInst = FALSE, $isLabel = FA

/**
* @param SimpleXMLElement $caseTypeXML
*
* @return array<string> symbolic activity-type names
*/
public function getDeclaredActivityTypes($caseTypeXML) {
Expand Down Expand Up @@ -342,6 +345,7 @@ public function getDeclaredActivityTypes($caseTypeXML) {

/**
* @param SimpleXMLElement $caseTypeXML
*
* @return array<string> symbolic relationship-type names
*/
public function getDeclaredRelationshipTypes($caseTypeXML) {
Expand Down Expand Up @@ -474,6 +478,8 @@ public function createActivity($activityTypeXML, &$params) {
);
}

$activityParams['assignee_contact_id'] = $this->getDefaultAssigneeForActivity($activityParams, $activityTypeXML);

//parsing date to default preference format
$params['activity_date_time'] = CRM_Utils_Date::processDate($params['activity_date_time']);

Expand Down Expand Up @@ -568,6 +574,119 @@ public function createActivity($activityTypeXML, &$params) {
return TRUE;
}

/**
* Return the default assignee contact for the activity.
*
* @param array $activityParams
* @param object $activityTypeXML
*
* @return int|null the ID of the default assignee contact or null if none.
*/
protected function getDefaultAssigneeForActivity($activityParams, $activityTypeXML) {
if (!isset($activityTypeXML->default_assignee_type)) {
return NULL;
}

$defaultAssigneeOptionsValues = $this->getDefaultAssigneeOptionValues();

switch ($activityTypeXML->default_assignee_type) {
case $defaultAssigneeOptionsValues['BY_RELATIONSHIP']:
return $this->getDefaultAssigneeByRelationship($activityParams, $activityTypeXML);

break;
case $defaultAssigneeOptionsValues['SPECIFIC_CONTACT']:
return $this->getDefaultAssigneeBySpecificContact($activityTypeXML);

break;
case $defaultAssigneeOptionsValues['USER_CREATING_THE_CASE']:
return $activityParams['source_contact_id'];

break;
case $defaultAssigneeOptionsValues['NONE']:
default:
return NULL;
}
}

/**
* Fetches and caches the activity's default assignee options.
*
* @return array
*/
protected function getDefaultAssigneeOptionValues() {
if (!empty($this->defaultAssigneeOptionsValues)) {
return $this->defaultAssigneeOptionsValues;
}

$defaultAssigneeOptions = civicrm_api3('OptionValue', 'get', [
'option_group_id' => 'activity_default_assignee',
'options' => [ 'limit' => 0 ]
]);

foreach ($defaultAssigneeOptions['values'] as $option) {
$this->defaultAssigneeOptionsValues[$option['name']] = $option['value'];
}

return $this->defaultAssigneeOptionsValues;
}

/**
* Returns the default assignee for the activity by searching for the target's
* contact relationship type defined in the activity's details.
*
* @param array $activityParams
* @param object $activityTypeXML
*
* @return int|null the ID of the default assignee contact or null if none.
*/
protected function getDefaultAssigneeByRelationship($activityParams, $activityTypeXML) {
if (!isset($activityTypeXML->default_assignee_relationship)) {
return NULL;
}

$targetContactId = is_array($activityParams['target_contact_id'])
? CRM_Utils_Array::first($activityParams['target_contact_id'])
: $activityParams['target_contact_id'];

$relationships = civicrm_api3('Relationship', 'get', [
'contact_id_b' => $targetContactId,
'relationship_type_id.name_b_a' => (string) $activityTypeXML->default_assignee_relationship,
'is_active' => 1,
'sequential' => 1,
]);

if ($relationships['count']) {
return $relationships['values'][0]['contact_id_a'];
}
else {
return NULL;
}
}

/**
* Returns the activity's default assignee for a specific contact if the contact exists,
* otherwise returns null.
*
* @param object $activityTypeXML
*
* @return int|null
*/
protected function getDefaultAssigneeBySpecificContact($activityTypeXML) {
if (!$activityTypeXML->default_assignee_contact) {
return NULL;
}

$contact = civicrm_api3('Contact', 'get', [
'id' => $activityTypeXML->default_assignee_contact
]);

if ($contact['count'] == 1) {
return $activityTypeXML->default_assignee_contact;
}

return NULL;
}

/**
* @param $activitySetsXML
*
Expand Down Expand Up @@ -617,6 +736,7 @@ public function getCaseManagerRoleId($caseType) {

/**
* @param string $caseType
*
* @return array<\Civi\CCase\CaseChangeListener>
*/
public function getListeners($caseType) {
Expand Down Expand Up @@ -662,6 +782,7 @@ public function getNaturalActivityTypeSort() {
* @param string $settingKey
* @param string $xmlTag
* @param mixed $default
*
* @return int
*/
private function getBoolSetting($settingKey, $xmlTag, $default = 0) {
Expand Down
15 changes: 11 additions & 4 deletions CRM/Core/BAO/OptionValue.php
Original file line number Diff line number Diff line change
Expand Up @@ -547,16 +547,23 @@ public static function getOptionValuesAssocArrayFromName($optionGroupName) {
* that an option value exists, without hitting an error if it already exists.
*
* This is sympathetic to sites who might pre-add it.
*
* @param array $params the option value attributes.
* @return array the option value attributes.
*/
public static function ensureOptionValueExists($params) {
$existingValues = civicrm_api3('OptionValue', 'get', array(
$result = civicrm_api3('OptionValue', 'get', array(
'option_group_id' => $params['option_group_id'],
'name' => $params['name'],
'return' => 'id',
'return' => ['id', 'value'],
'sequential' => 1,
));
if (!$existingValues['count']) {
civicrm_api3('OptionValue', 'create', $params);

if (!$result['count']) {
$result = civicrm_api3('OptionValue', 'create', $params);
}

return CRM_Utils_Array::first($result['values']);
}

}
36 changes: 36 additions & 0 deletions CRM/Upgrade/Incremental/php/FiveThree.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,42 @@ public function setPostUpgradeMessage(&$postUpgradeMessage, $rev) {
// }
}

/**
* Upgrade function for version 5.3.alpha1. This upgrader adds the default assignee
* option values that can be selected when creating or editing a new
* workflow's activity.
*
* @param string $rev
*/
public function upgrade_5_3_alpha1($rev) {
$this->addTask(ts('Upgrade DB to %1: SQL', array(1 => $rev)), 'runSql', $rev);

// Add option group for activity default assignees:
CRM_Core_BAO_OptionGroup::ensureOptionGroupExists(array(
'name' => 'activity_default_assignee',
'title' => ts('Activity default assignee'),
'is_reserved' => 1,
));

// Add option values for activity default assignees:
$options = array(
array('name' => 'NONE', 'label' => ts('None'), 'is_default' => 1),
array('name' => 'BY_RELATIONSHIP', 'label' => ts('By relationship to case client')),
array('name' => 'SPECIFIC_CONTACT', 'label' => ts('Specific contact')),
array('name' => 'USER_CREATING_THE_CASE', 'label' => ts('User creating the case')),
);

foreach ($options as $option) {
CRM_Core_BAO_OptionValue::ensureOptionValueExists(array(
'option_group_id' => 'activity_default_assignee',
'name' => $option['name'],
'label' => $option['label'],
'is_default' => CRM_Utils_Array::value('is_default', $option, 0),
'is_active' => TRUE,
));
}
}

/*
* Important! All upgrade functions MUST add a 'runSql' task.
* Uncomment and use the following template for a new upgrade version
Expand Down
112 changes: 81 additions & 31 deletions ang/crmCaseType.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,13 @@
limit: 0
}
}];
reqs.defaultAssigneeTypes = ['OptionValue', 'get', {
option_group_id: 'activity_default_assignee',
sequential: 1,
options: {
limit: 0
}
}];
reqs.relTypes = ['RelationshipType', 'get', {
sequential: 1,
options: {
Expand Down Expand Up @@ -123,41 +130,77 @@
});

crmCaseType.controller('CaseTypeCtrl', function($scope, crmApi, apiCalls) {
// CRM_Case_XMLProcessor::REL_TYPE_CNAME
var REL_TYPE_CNAME = CRM.crmCaseType.REL_TYPE_CNAME,

ts = $scope.ts = CRM.ts(null);
var REL_TYPE_CNAME, defaultAssigneeDefaultValue, ts;

(function init () {
// CRM_Case_XMLProcessor::REL_TYPE_CNAME
REL_TYPE_CNAME = CRM.crmCaseType.REL_TYPE_CNAME;

ts = $scope.ts = CRM.ts(null);
$scope.locks = { caseTypeName: true, activitySetName: true };
$scope.workflows = { timeline: 'Timeline', sequence: 'Sequence' };
defaultAssigneeDefaultValue = _.find(apiCalls.defaultAssigneeTypes.values, { is_default: '1' }) || {};

storeApiCallsResults();
initCaseType();
initCaseTypeDefinition();
initSelectedStatuses();
})();

/// Stores the api calls results in the $scope object
function storeApiCallsResults() {
$scope.activityStatuses = apiCalls.actStatuses.values;
$scope.caseStatuses = _.indexBy(apiCalls.caseStatuses.values, 'name');
$scope.activityTypes = _.indexBy(apiCalls.actTypes.values, 'name');
$scope.activityTypeOptions = _.map(apiCalls.actTypes.values, formatActivityTypeOption);
$scope.defaultAssigneeTypes = apiCalls.defaultAssigneeTypes.values;
$scope.relationshipTypeOptions = _.map(apiCalls.relTypes.values, function(type) {
return {id: type[REL_TYPE_CNAME], text: type.label_b_a};
});
// stores the default assignee values indexed by their option name:
$scope.defaultAssigneeTypeValues = _.chain($scope.defaultAssigneeTypes)
.indexBy('name').mapValues('value').value();
}

$scope.activityStatuses = apiCalls.actStatuses.values;
$scope.caseStatuses = _.indexBy(apiCalls.caseStatuses.values, 'name');
$scope.activityTypes = _.indexBy(apiCalls.actTypes.values, 'name');
$scope.activityTypeOptions = _.map(apiCalls.actTypes.values, formatActivityTypeOption);
$scope.relationshipTypeOptions = _.map(apiCalls.relTypes.values, function(type) {
return {id: type[REL_TYPE_CNAME], text: type.label_b_a};
});
$scope.locks = {caseTypeName: true, activitySetName: true};
/// initializes the case type object
function initCaseType() {
var isNewCaseType = !apiCalls.caseType;

$scope.workflows = {
'timeline': 'Timeline',
'sequence': 'Sequence'
};
if (isNewCaseType) {
$scope.caseType = _.cloneDeep(newCaseTypeTemplate);
} else {
$scope.caseType = apiCalls.caseType;
}
}

$scope.caseType = apiCalls.caseType ? apiCalls.caseType : _.cloneDeep(newCaseTypeTemplate);
$scope.caseType.definition = $scope.caseType.definition || [];
$scope.caseType.definition.activityTypes = $scope.caseType.definition.activityTypes || [];
$scope.caseType.definition.activitySets = $scope.caseType.definition.activitySets || [];
_.each($scope.caseType.definition.activitySets, function (set) {
_.each(set.activityTypes, function (type, name) {
type.label = $scope.activityTypes[type.name].label;
/// initializes the case type definition object
function initCaseTypeDefinition() {
$scope.caseType.definition = $scope.caseType.definition || [];
$scope.caseType.definition.activityTypes = $scope.caseType.definition.activityTypes || [];
$scope.caseType.definition.activitySets = $scope.caseType.definition.activitySets || [];
$scope.caseType.definition.caseRoles = $scope.caseType.definition.caseRoles || [];
$scope.caseType.definition.statuses = $scope.caseType.definition.statuses || [];

_.each($scope.caseType.definition.activitySets, function (set) {
_.each(set.activityTypes, function (type, name) {
var isDefaultAssigneeTypeUndefined = _.isUndefined(type.default_assignee_type);
type.label = $scope.activityTypes[type.name].label;

if (isDefaultAssigneeTypeUndefined) {
type.default_assignee_type = defaultAssigneeDefaultValue.value;
}
});
});
});
$scope.caseType.definition.caseRoles = $scope.caseType.definition.caseRoles || [];
$scope.caseType.definition.statuses = $scope.caseType.definition.statuses || [];
}

$scope.selectedStatuses = {};
_.each(apiCalls.caseStatuses.values, function (status) {
$scope.selectedStatuses[status.name] = !$scope.caseType.definition.statuses.length || $scope.caseType.definition.statuses.indexOf(status.name) > -1;
});
/// initializes the selected statuses
function initSelectedStatuses() {
$scope.selectedStatuses = {};

_.each(apiCalls.caseStatuses.values, function (status) {
$scope.selectedStatuses[status.name] = !$scope.caseType.definition.statuses.length || $scope.caseType.definition.statuses.indexOf(status.name) > -1;
});
}

$scope.addActivitySet = function(workflow) {
var activitySet = {};
Expand Down Expand Up @@ -187,7 +230,8 @@
status: 'Scheduled',
reference_activity: 'Open Case',
reference_offset: '1',
reference_select: 'newest'
reference_select: 'newest',
default_assignee_type: $scope.defaultAssigneeTypeValues.NONE
});
}

Expand Down Expand Up @@ -227,6 +271,12 @@
}
};

/// Clears the activity's default assignee values for relationship and contact
$scope.clearActivityDefaultAssigneeValues = function(activity) {
activity.default_assignee_relationship = null;
activity.default_assignee_contact = null;
};

/// Add a new role
$scope.addRole = function(roles, roleName) {
var names = _.pluck($scope.caseType.definition.caseRoles, 'name');
Expand Down
Loading

0 comments on commit 994084b

Please sign in to comment.