1: <?php
2: /*****************************************************************************************
3: * X2Engine Open Source Edition is a customer relationship management program developed by
4: * X2Engine, Inc. Copyright (C) 2011-2016 X2Engine Inc.
5: *
6: * This program is free software; you can redistribute it and/or modify it under
7: * the terms of the GNU Affero General Public License version 3 as published by the
8: * Free Software Foundation with the addition of the following permission added
9: * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK
10: * IN WHICH THE COPYRIGHT IS OWNED BY X2ENGINE, X2ENGINE DISCLAIMS THE WARRANTY
11: * OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
12: *
13: * This program is distributed in the hope that it will be useful, but WITHOUT
14: * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
15: * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
16: * details.
17: *
18: * You should have received a copy of the GNU Affero General Public License along with
19: * this program; if not, see http://www.gnu.org/licenses or write to the Free
20: * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
21: * 02110-1301 USA.
22: *
23: * You can contact X2Engine, Inc. P.O. Box 66752, Scotts Valley,
24: * California 95067, USA. or at email address contact@x2engine.com.
25: *
26: * The interactive user interfaces in modified source and object code versions
27: * of this program must display Appropriate Legal Notices, as required under
28: * Section 5 of the GNU Affero General Public License version 3.
29: *
30: * In accordance with Section 7(b) of the GNU Affero General Public License version 3,
31: * these Appropriate Legal Notices must retain the display of the "Powered by
32: * X2Engine" logo. If the display of the logo is not reasonably feasible for
33: * technical reasons, the Appropriate Legal Notices must display the words
34: * "Powered by X2Engine".
35: *****************************************************************************************/
36:
37: /**
38: * Logic attributes/methods for lead distribution.
39: *
40: * LeadRouting is a CBehavior that provides logic for simple or complex
41: * distribution of leads to users
42: * @package application.components
43: */
44: class LeadRoutingBehavior extends CBehavior {
45:
46: /**
47: * Picks the next asignee based on the routing type
48: *
49: * @return string Username that should be assigned the next lead
50: */
51: public function getNextAssignee($contact = null) {
52: $admin = &Yii::app()->settings;
53: $type = $admin->leadDistribution;
54: if ($type == "") {
55: return "Anyone";
56: } elseif ($type == "evenDistro") { // legacy lead routing option
57: return $this->evenDistro();
58: } elseif ($type == "trueRoundRobin") {
59: return $this->roundRobin();
60: } elseif ($type == "customRoundRobin") {
61: return $this->customRoundRobin($contact);
62: } elseif ($type == 'singleUser') {
63: return $this->singleUser();
64: }
65: }
66:
67: public function singleUser() {
68: $admin = &Yii::app()->settings;
69: $user = User::model()->findByPk($admin->rrId);
70: if (isset($user)) {
71: $username = $user->username;
72:
73: if (($admin->onlineOnly && !in_array($username,
74: Session::getOnlineUsers())) ||
75: !in_array($username,
76: Profile::model()->getUsernamesOfAvailableUsers())) {
77:
78: return 'Anyone';
79: } else {
80: return $username;
81: }
82: } else {
83: return "Anyone";
84: }
85: }
86:
87: /**
88: * Picks the next asignee for custom round robin lead routing rule.
89: * @param mixed $contact null or Contacts model. If this is set, it will be used in place of
90: * POST data for the purposes of testing routing rules.
91: * @return mixed
92: */
93: public function customRoundRobin($contact = null) {
94: if ($contact) {
95: $arr = $contact->getAttributes();
96: } else {
97: $arr = $_POST;
98: /* for new lead capture form:
99: "Contacts" maps to an array of fields, check if this array exists and has fields,
100: if so, set arr */
101: if (isset($arr['Contacts']) && is_array($arr['Contacts']) &&
102: count($arr['Contacts']) > 0) {
103: $arr = $arr['Contacts'];
104: }
105: }
106: $users = $this->getRoutingRules($arr);
107: if (!empty($users) && is_array($users) && count($users) > 1) {
108: $rrId = $users[count($users) - 1];
109: unset($users[count($users) - 1]);
110: $i = $rrId % count($users);
111: return $users[$i];
112: } else {
113: return "Anyone";
114: }
115: }
116:
117: /**
118: * Legacy lead routing option. This can no longer be selected option from the lead routing
119: * admin page.
120: *
121: * Picks the next asignee such that the resulting routing distribution
122: * would be even.
123: *
124: * @return mixed
125: */
126: public function evenDistro() {
127: $admin = &Yii::app()->settings;
128: $online = $admin->onlineOnly;
129: Session::cleanUpSessions();
130: $usernames = array();
131: $sessions = Session::getOnlineUsers();
132: $users = X2Model::model('User')->findAll();
133: foreach ($users as $user) {
134: $usernames[] = $user->username;
135: }
136:
137: if ($online == 1) {
138: foreach ($usernames as $user) {
139: if (in_array($user, $sessions)) $users[] = $user;
140: }
141: }else {
142: $users = $usernames;
143: }
144:
145: $users = array_values(array_intersect(Profile::model()->getUsernamesOfAvailableUsers(),
146: $users));
147:
148: $numbers = array();
149: foreach ($users as $user) {
150: if ($user != 'admin' && $user != 'api') {
151: $actions = X2Model::model('Actions')->findAllByAttributes(array(
152: 'assignedTo' => $user, 'complete' => 'No'));
153: if (isset($actions)) $numbers[$user] = count($actions);
154: else $numbers[$user] = 0;
155: }
156: }
157: asort($numbers);
158: reset($numbers);
159: return key($numbers);
160: }
161:
162: /**
163: * Picks the next assignee in a round-robin manner.
164: *
165: * Users get a chance to be picked in this manner only if online. In the
166: * round-robin distribution of leads, the last person who was picked for
167: * a lead assignment is stored using {@link updateRoundRobin()}. If no
168: * one is online, the lead will be assigned to "Anyone".
169: * @return mixed
170: */
171: public function roundRobin() {
172: $admin = &Yii::app()->settings;
173: $online = $admin->onlineOnly;
174: Session::cleanUpSessions();
175: $usernames = array();
176: $sessions = Session::getOnlineUsers();
177: $users = X2Model::model('User')->findAll();
178: foreach ($users as $userRecord) {
179: //exclude admin from candidates
180: if ($userRecord->username != 'admin' && $userRecord->username != 'api')
181: $usernames[] = $userRecord->username;
182: }
183: if ($online == 1) {
184: $userList = array();
185: foreach ($usernames as $user) {
186: if (in_array($user, $sessions)) $userList[] = $user;
187: }
188: }else {
189: $userList = $usernames;
190: }
191:
192: $userList = array_values(
193: array_intersect(Profile::model()->getUsernamesOfAvailableUsers(),
194: $userList));
195:
196: $rrId = $this->getRoundRobin();
197: if (count($userList) > 0) {
198: $i = $rrId % count($userList);
199: $this->updateRoundRobin();
200: return $userList[$i];
201: } else {
202: return "Anyone";
203: }
204: }
205:
206: /**
207: * Returns the round-robin state
208: * @return integer
209: */
210: public function getRoundRobin() {
211: $admin = &Yii::app()->settings;
212: $rrId = $admin->rrId;
213: return $rrId;
214: }
215:
216: /**
217: * Stores the round-robin state.
218: */
219: public function updateRoundRobin() {
220: $admin = &Yii::app()->settings;
221: $admin->rrId = $admin->rrId + 1;
222: $admin->save();
223: }
224:
225: const WITHIN_GROUPS = 0;
226: const BETWEEN_GROUPS = 1;
227:
228: /**
229: * Obtains lead routing rules.
230: * @param type $data
231: * @return type
232: */
233: public function getRoutingRules($data) {
234: $admin = &Yii::app()->settings;
235: $online = $admin->onlineOnly;
236: Session::cleanUpSessions();
237: $sessions = Session::getOnlineUsers();
238: $criteria = new CDbCriteria;
239: $criteria->order = "priority ASC";
240: $rules = X2Model::model('LeadRouting')->findAll($criteria);
241: foreach ($rules as $rule) {
242: $arr = LeadRouting::parseCriteria($rule->criteria);
243: $flagArr = array();
244: foreach ($arr as $criteria) {
245: if (isset($data[$criteria['field']])) {
246: $val = $data[$criteria['field']];
247: $operator = $criteria['comparison'];
248: $target = $criteria['value'];
249: if ($operator != 'contains') {
250: switch ($operator) {
251: case '>':
252: $flag = ($val >= $target);
253: break;
254: case '<':
255: $flag = ($val <= $target);
256: break;
257: case '=':
258: $flag = ($val == $target);
259: break;
260: case '!=':
261: $flag = ($val != $target);
262: break;
263: default:
264: $flag = false;
265: }
266: } else {
267: $flag = preg_match("/$target/i", $val) != 0;
268: }
269: $flagArr[] = $flag;
270: }
271: }
272: if (!in_array(false, $flagArr) && count($flagArr) > 0) {
273: $users = $rule->users;
274: $users = explode(", ", $users);
275: if (is_null($rule->groupType)) {
276: if ($online == 1)
277: $users = array_intersect($users, $sessions);
278: }else {
279: $groups = $rule->users;
280: $groups = explode(", ", $groups);
281: $users = array();
282: foreach ($groups as $group) {
283: if ($rule->groupType == self::WITHIN_GROUPS) {
284: $links = GroupToUser::model()->findAllByAttributes(
285: array('groupId' => $group));
286: foreach ($links as $link) {
287: $usernames[] = User::model()->findByPk($link->userId)->username;
288: }
289: } else { // $rule->groupType == self::BETWEEN_GROUPS
290: $users[] = $group;
291: }
292: }
293: if ($online == 1 && $rule->groupType == self::WITHIN_GROUPS) {
294: foreach ($usernames as $user) {
295: if (in_array($user, $sessions)) $users[] = $user;
296: }
297: }elseif ($rule->groupType == self::WITHIN_GROUPS) {
298: $users = $usernames;
299: }
300: }
301:
302: if ($rule->groupType == self::WITHIN_GROUPS) {
303: $users = array_values(
304: array_intersect(
305: Profile::model()->getUsernamesOfAvailableUsers(),
306: $users));
307: }
308:
309: $users[] = $rule->rrId;
310: $rule->rrId++;
311: $rule->save();
312: return $users;
313: }
314: }
315: }
316:
317: }
318: