diff --git a/include/class.dept.php b/include/class.dept.php index f09ad0587370b9adfac33298c2f9ed2a1b0c7df2..8131be44fda8db7147d65c5c22ee41fade6396fc 100644 --- a/include/class.dept.php +++ b/include/class.dept.php @@ -152,22 +152,25 @@ implements TemplateVariable { if (!$this->_members || $criteria) { $members = Staff::objects() ->distinct('staff_id') + ->constrain(array( + // Ensure that joining through dept_access is only relevant + // for this department, so that the `alerts` annotation + // can work properly + 'dept_access' => new Q(array('dept_access__dept_id' => $this->getId())) + )) ->filter(Q::any(array( 'dept_id' => $this->getId(), - new Q(array( - 'dept_access__dept_id' => $this->getId(), - 'dept_access__dept__group_membership' => self::ALERTS_DEPT_AND_GROUPS, - )), - 'staff_id' => $this->manager_id + 'staff_id' => $this->manager_id, + 'dept_access__dept_id' => $this->getId(), ))); - if ($criteria && $criteria['available']) + // TODO: Consider moving this into ::getAvailableMembers + if ($criteria && $criteria['available']) { $members->filter(array( 'isactive' => 1, 'onvacation' => 0, )); - - $members->distinct('staff_id'); + } switch ($cfg->getAgentNameFormat()) { case 'last': case 'lastfirst': @@ -180,11 +183,11 @@ implements TemplateVariable { } if ($criteria) - return $members->all(); + return $members; - $this->_members = $members->all(); + $this->_members = $members; } - return new UserList($this->_members); + return new UserList($this->_members->all()); } function getAvailableMembers() { @@ -197,7 +200,13 @@ implements TemplateVariable { $rv = array(); } else { - $rv = $this->getAvailableMembers(); + $rv = clone $this->getAvailableMembers(); + $rv->filter(Q::any(array( + // Ensure "Alerts" is enabled — must be a primary member or + // have alerts enabled on your membership. + 'dept_id' => $this->getId(), + 'dept_access__flags__hasbit' => StaffDeptAccess::FLAG_ALERTS, + ))); } return $rv; } @@ -313,12 +322,6 @@ implements TemplateVariable { return $this->getHashtable(); } - function updateSettings($vars) { - $this->path = $this->getFullPath(); - $this->save(); - return true; - } - function delete() { global $cfg; @@ -335,16 +338,26 @@ implements TemplateVariable { if (parent::delete()) { // DO SOME HOUSE CLEANING //Move tickets to default Dept. TODO: Move one ticket at a time and send alerts + log notes. - db_query('UPDATE '.TICKET_TABLE.' SET dept_id='.db_input($cfg->getDefaultDeptId()).' WHERE dept_id='.db_input($id)); + Ticket::objects() + ->filter(array('dept_id' => $id)) + ->update(array('dept_id' => $cfg->getDefaultDeptId())); + //Move Dept members: This should never happen..since delete should be issued only to empty Depts...but check it anyways - db_query('UPDATE '.STAFF_TABLE.' SET dept_id='.db_input($cfg->getDefaultDeptId()).' WHERE dept_id='.db_input($id)); + Staff::objects() + ->filter(array('dept_id' => $id)) + ->update(array('dept_id' => $cfg->getDefaultDeptId())); // Clear any settings using dept to default back to system default - db_query('UPDATE '.TOPIC_TABLE.' SET dept_id=0 WHERE dept_id='.db_input($id)); - db_query('UPDATE '.EMAIL_TABLE.' SET dept_id=0 WHERE dept_id='.db_input($id)); + Topic::objects() + ->filter(array('dept_id' => $id)) + ->delete(); + Email::objects() + ->filter(array('dept_id' => $id)) + ->delete(); + db_query('UPDATE '.FILTER_TABLE.' SET dept_id=0 WHERE dept_id='.db_input($id)); - // Delete extended access + // Delete extended access entries StaffDeptAccess::objects() ->filter(array('dept_id' => $id)) ->delete(); @@ -528,11 +541,20 @@ implements TemplateVariable { if ($vars['pid'] && !($p = static::lookup($vars['pid']))) $errors['pid'] = __('Department selection is required'); + // Format access update as [array(dept_id, alerts?)] + $access = array(); + if (isset($vars['members'])) { + foreach (@$vars['members'] as $staff_id) { + $access[] = array($staff_id, $vars['member_role'][$staff_id], + @$vars['member_alerts'][$staff_id]); + } + } + $this->updateAccess($access, $errors); + if ($errors) return false; $this->pid = $vars['pid'] ?: 0; - $this->updated = SqlFunction::NOW(); $this->ispublic = isset($vars['ispublic'])?$vars['ispublic']:0; $this->email_id = isset($vars['email_id'])?$vars['email_id']:0; $this->tpl_id = isset($vars['tpl_id'])?$vars['tpl_id']:0; @@ -545,9 +567,10 @@ implements TemplateVariable { $this->ticket_auto_response = isset($vars['ticket_auto_response'])?$vars['ticket_auto_response']:1; $this->message_auto_response = isset($vars['message_auto_response'])?$vars['message_auto_response']:1; $this->flags = isset($vars['assign_members_only']) ? self::FLAG_ASSIGN_MEMBERS_ONLY : 0; + $this->path = $this->getFullPath(); - if ($this->save()) - return $this->updateSettings($vars); + if ($rv = $this->save()) + return $rv; if (isset($this->id)) $errors['err']=sprintf(__('Unable to update %s.'), __('this department')) @@ -559,6 +582,39 @@ implements TemplateVariable { return false; } + function updateAccess($access, &$errors) { + reset($access); + $dropped = array(); + foreach ($this->extended as $DA) + $dropped[$DA->staff_id] = 1; + while (list(, list($staff_id, $role_id, $alerts)) = each($access)) { + unset($dropped[$staff_id]); + if (!$role_id || !Role::lookup($role_id)) + $errors['members'][$staff_id] = __('Select a valid role'); + if (!$staff_id || !Staff::lookup($staff_id)) + $errors['members'][$staff_id] = __('No such agent'); + $da = $this->extended->findFirst(array('staff_id' => $staff_id)); + if (!isset($da)) { + $da = StaffDeptAccess::create(array( + 'staff_id' => $staff_id, 'role_id' => $role_id + )); + $this->extended->add($da); + } + else { + $da->role_id = $role_id; + } + $da->setAlerts($alerts); + if (!$errors) + $da->save(); + } + if (!$errors && $dropped) { + $this->extended + ->filter(array('staff_id__in' => array_keys($dropped))) + ->delete(); + $this->extended->reset(); + } + return !$errors; + } } class DepartmentQuickAddForm diff --git a/include/class.orm.php b/include/class.orm.php index 5fc2362077d0dc6163ccf9275f6beaf1ba2baf47..8e1a35537ca6ab7803dcce3496bef1bf05113a20 100644 --- a/include/class.orm.php +++ b/include/class.orm.php @@ -901,6 +901,7 @@ class QuerySet implements IteratorAggregate, ArrayAccess, Serializable, Countabl var $model; var $constraints = array(); + var $path_constraints = array(); var $ordering = array(); var $limit = false; var $offset = 0; @@ -942,6 +943,15 @@ class QuerySet implements IteratorAggregate, ArrayAccess, Serializable, Countabl return $this; } + function constrain() { + foreach (func_get_args() as $I) { + foreach ($I as $path => $Q) { + $this->path_constraints[$path][] = $Q; + } + } + return $this; + } + function defer() { foreach (func_get_args() as $f) $this->defer[$f] = true; @@ -1367,7 +1377,7 @@ class ModelInstanceManager extends ResultSet { // using an AnnotatedModel instance. if ($annotations && $modelClass == $this->model) { foreach ($annotations as $name=>$A) { - if (isset($fields[$name])) { + if (array_key_exists($name, $fields)) { $extras[$name] = $fields[$name]; unset($fields[$name]); } @@ -1954,8 +1964,20 @@ class SqlCompiler { function getJoins($queryset) { $sql = ''; - foreach ($this->joins as $j) - $sql .= $j['sql']; + foreach ($this->joins as $path => $j) { + if (!$j['sql']) + continue; + list($base, $constraints) = $j['sql']; + // Add in path-specific constraints, if any + if (isset($queryset->path_constraints[$path])) { + foreach ($queryset->path_constraints[$path] as $Q) { + $constraints[] = $this->compileQ($Q, $queryset->model); + } + } + $sql .= $base; + if ($constraints) + $sql .= ' ON ('.implode(' AND ', $constraints).')'; + } // Add extra items from QuerySet if (isset($queryset->extra['tables'])) { foreach ($queryset->extra['tables'] as $S) { @@ -2149,9 +2171,7 @@ class MySqlCompiler extends SqlCompiler { ? $rmodel::getQuery($this) : $this->quote($rmodel::getMeta('table')); $base = "{$join}{$table} {$alias}"; - if ($constraints) - $base .= ' ON ('.implode(' AND ', $constraints).')'; - return $base; + return array($base, $constraints); } /** diff --git a/include/class.staff.php b/include/class.staff.php index 8b3a087ca60a97b930f2b3995716989412716d27..d830515b84eb937fb87e24783e0b6924c1e964ee 100644 --- a/include/class.staff.php +++ b/include/class.staff.php @@ -655,6 +655,7 @@ implements AuthenticatedUser, EmailContact, TemplateVariable { $member = $this->teams ->filter(array('team_id__in' => array_keys($dropped))) ->delete(); + $this->teams->reset(); } return true; } @@ -1009,17 +1010,22 @@ implements AuthenticatedUser, EmailContact, TemplateVariable { if (!$errors) $da->save(); } - if (!$errors && $dropped) + if (!$errors && $dropped) { $this->dept_access ->filter(array('dept_id__in' => array_keys($dropped))) ->delete(); + $this->dept_access->reset(); + } return !$errors; } private function updatePerms($vars, &$errors) { - $permissions = $this->getPermission(); + if (!$vars) { + $this->permissions = ''; + return; + } foreach (RolePermission::allPermissions() as $g => $perms) { - foreach($perms as $k => $v) { + foreach ($perms as $k => $v) { $permissions->set($k, in_array($k, $vars) ? 1 : 0); } } diff --git a/include/staff/department.inc.php b/include/staff/department.inc.php index c5ea4cda2e884ce36a971fa43e414f2fb558c6e8..e3be963f66c13d92a2cae69031887744c5a1cbde 100644 --- a/include/staff/department.inc.php +++ b/include/staff/department.inc.php @@ -33,6 +33,8 @@ $info = Format::htmlchars(($errors && $_POST) ? $_POST : $info); <ul class="clean tabs"> <li class="active"><a href="#settings"> <i class="icon-file"></i> <?php echo __('Settings'); ?></a></li> + <li><a href="#access"> + <i class="icon-user"></i> <?php echo __('Access'); ?></a></li> </ul> <div id="settings" class="tab_content"> <table class="form_table" width="940" border="0" cellspacing="0" cellpadding="2"> @@ -287,6 +289,59 @@ $info = Format::htmlchars(($errors && $_POST) ? $_POST : $info); </tbody> </table> </div> + +<div id="access" class="hidden tab_content"> + <table class="two-column table" width="100%"> + <tbody> +<?php +$agents = Staff::getStaffMembers(); +foreach ($dept->getMembers() as $member) { + unset($agents[$member->getId()]); +} ?> + <tr id="add_extended_access"> + <td colspan="2"> + <i class="icon-plus-sign"></i> + <select id="add_access" data-quick-add="staff"> + <option value="0">— <?php echo __('Select Agent');?> —</option> + <?php + foreach ($agents as $id=>$name) { + echo sprintf('<option value="%d">%s</option>',$id,$name); + } + ?> + <option value="0" data-quick-add>— <?php echo __('Add New');?> —</option> + </select> + <button type="button" class="action-button"> + <?php echo __('Add'); ?> + </button> + </td> + </tr> + </tbody> + <tbody> + <tr id="member_template" class="hidden"> + <td> + <input type="hidden" data-name="members[]" value="" /> + </td> + <td> + <select data-name="member_role" data-quick-add="role"> + <option value="0">— <?php echo __('Select Role');?> —</option> + <?php + foreach (Role::getRoles() as $id=>$name) { + echo sprintf('<option value="%d" %s>%s</option>',$id,$sel,$name); + } + ?> + <option value="0" data-quick-add>— <?php echo __('Add New');?> —</option> + </select> + <span style="display:inline-block;width:60px"> </span> + <input type="checkbox" data-name="member_alerts" value="1" /> + <?php echo __('Alerts'); ?> + <a href="#" class="pull-right drop-membership" title="<?php echo __('Delete'); + ?>"><i class="icon-trash"></i></a> + </td> + </tr> + </tbody> + </table> +</div> + <p style="text-align:center"> <input type="submit" name="submit" value="<?php echo $submit_text; ?>"> <input type="reset" name="reset" value="<?php echo __('Reset');?>"> @@ -294,3 +349,73 @@ $info = Format::htmlchars(($errors && $_POST) ? $_POST : $info); onclick='window.location.href="?"'> </p> </form> + +<script type="text/javascript"> +var addAccess = function(staffid, name, role, alerts, primary, error) { + var copy = $('#member_template').clone(); + + copy.find('td:first').append(document.createTextNode(name)); + if (primary) { + copy.find('td:first').append($('<span class="faded">').text(primary)); + copy.find('td:last').empty(); + } + else { + copy.find('[data-name^=member_alerts]') + .attr('name', 'member_alerts['+staffid+']') + .prop('checked', alerts); + copy.find('[data-name^=member_role]') + .attr('name', 'member_role['+staffid+']') + .val(role || 0); + copy.find('[data-name=members\\[\\]]') + .attr('name', 'members[]') + .val(staffid); + } + copy.attr('id', '').show().insertBefore($('#add_extended_access')); + copy.removeClass('hidden') + if (error) + $('<div class="error">').text(error).appendTo(copy.find('td:last')); +}; + +$('#add_extended_access').find('button').on('click', function() { + var selected = $('#add_access').find(':selected'); + addAccess(selected.val(), selected.text(), 0, true); + selected.remove(); + return false; +}); + +$(document).on('click', 'a.drop-membership', function() { + var tr = $(this).closest('tr'); + $('#add_access').append( + $('<option>') + .attr('value', tr.find('input[name^=members][type=hidden]').val()) + .text(tr.find('td:first').text()) + ); + tr.fadeOut(function() { $(this).remove(); }); + return false; +}); + +<?php +if ($dept) { + $members = $dept->members->all(); + foreach ($dept->extended as $x) { + $members[] = new AnnotatedModel($x->staff, array( + 'alerts' => $x->isAlertsEnabled(), + 'role_id' => $x->role_id, + )); + } + usort($members, function($a, $b) { return strcmp($a->getName(), $b->getName()); }); + + foreach ($members as $member) { + $primary = $member->dept_id == $info['id']; + echo sprintf('addAccess(%d, %s, %d, %d, %s, %s);', + $member->getId(), + JsonDataEncoder::encode((string) $member->getName()), + $member->role_id, + $member->get('alerts', 0), + JsonDataEncoder::encode($primary ? ' — '.__('Primary') : ''), + JsonDataEncoder::encode($errors['members'][$member->staff_id]) + ); + } +} +?> +</script> diff --git a/include/staff/staff.inc.php b/include/staff/staff.inc.php index 7ff2ac4e05bbae231670872c51a022ee1d4a6516..9bc201df682559cf191bc51b6ec11ddf63600b81 100644 --- a/include/staff/staff.inc.php +++ b/include/staff/staff.inc.php @@ -2,18 +2,24 @@ if(!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isAdmin()) die('Access Denied'); $info = $qs = array(); -if ($staff && $_REQUEST['a']!='add'){ + +if ($_REQUEST['a']=='add'){ + if (!$staff) + $staff = Staff::create(array( + 'isactive' => true, + )); + $title=__('Add New Agent'); + $action='create'; + $submit_text=__('Create'); +} +else { //Editing Department. $title=__('Manage Agent'); $action='update'; $submit_text=__('Save Changes'); - $info = $staff->getInfo(); $info['id'] = $staff->getId(); - $info['teams'] = $staff->getTeams(); - $info['signature'] = Format::viewableImages($info['signature']); $qs += array('id' => $staff->getId()); } -$info = Format::htmlchars($info); ?> <form action="staff.php?<?php echo Http::build_query($qs); ?>" method="post" id="save" autocomplete="off"> @@ -24,7 +30,7 @@ $info = Format::htmlchars($info); <h2><?php echo $title; ?> <div> - <small><?php echo $info['firstname'].' '.$info['lastname'];?></small> + <small><?php echo $staff->getName(); ?></small> </div> </h2> @@ -42,10 +48,10 @@ $info = Format::htmlchars($info); <td class="required"><?php echo __('Name'); ?>:</td> <td> <input type="text" size="20" maxlength="64" style="width: 145px" name="firstname" - autofocus value="<?php echo $info['firstname']; ?>" + autofocus value="<?php echo Format::htmlchars($staff->firstname); ?>" placeholder="<?php echo __("First Name"); ?>" /> <input type="text" size="20" maxlength="64" style="width: 145px" name="lastname" - value="<?php echo $info['lastname']; ?>" + value="<?php echo Format::htmlchars($staff->lastname); ?>" placeholder="<?php echo __("Last Name"); ?>" /> <div class="error"><?php echo $errors['firstname']; ?></div> <div class="error"><?php echo $errors['lastname']; ?></div> @@ -55,7 +61,7 @@ $info = Format::htmlchars($info); <td class="required"><?php echo __('Email Address'); ?>:</td> <td> <input type="email" size="40" maxlength="64" style="width: 300px" name="email" - value="<?php echo $info['email']; ?>" + value="<?php echo Format::htmlchars($staff->email); ?>" placeholder="<?php echo __('e.g. me@mycompany.com'); ?>" /> <div class="error"><?php echo $errors['email']; ?></div> </td> @@ -64,10 +70,10 @@ $info = Format::htmlchars($info); <td><?php echo __('Phone Number');?>:</td> <td> <input type="tel" size="18" name="phone" class="auto phone" - value="<?php echo $info['phone']; ?>" /> + value="<?php echo Format::htmlchars($staff->phone); ?>" /> <?php echo __('Ext');?> <input type="text" size="5" name="phone_ext" - value="<?php echo $info['phone_ext']; ?>"> + value="<?php echo Format::htmlchars($staff->phone_ext); ?>"> <div class="error"><?php echo $errors['phone']; ?></div> <div class="error"><?php echo $errors['phone_ext']; ?></div> </td> @@ -76,7 +82,7 @@ $info = Format::htmlchars($info); <td><?php echo __('Mobile Number');?>:</td> <td> <input type="tel" size="18" name="mobile" class="auto phone" - value="<?php echo $info['mobile']; ?>" /> + value="<?php echo Format::htmlchars($staff->mobile); ?>" /> <div class="error"><?php echo $errors['mobile']; ?></div> </td> </tr> @@ -94,7 +100,7 @@ $info = Format::htmlchars($info); <td> <input type="text" size="40" style="width:300px" class="staff-username typeahead" - name="username" value="<?php echo $info['username']; ?>" /> + name="username" value="<?php echo Format::htmlchars($staff->username); ?>" /> <?php if (!($bk = $staff->getAuthBackend()) || $bk->supportsPasswordChange()) { ?> <button type="button" class="action-button" onclick="javascript: $.dialog('ajax.php/staff/'+<?php echo $info['id']; ?>+'/set-password', 201);"> @@ -126,7 +132,7 @@ if (count($bks) > 1) { <option value="">— <?php echo __('Use any available backend'); ?> —</option> <?php foreach ($bks as $ab) { ?> <option value="<?php echo $ab::$id; ?>" <?php - if ($info['backend'] == $ab::$id) + if ($staff->backend == $ab::$id) echo 'selected="selected"'; ?>><?php echo $ab->getName(); ?></option> <?php } ?> @@ -155,19 +161,19 @@ if (count($bks) > 1) { <br/> <label> <input type="checkbox" name="isadmin" value="1" - <?php echo ($info['isadmin']) ? 'checked="checked"' : ''; ?> /> + <?php echo ($staff->isadmin) ? 'checked="checked"' : ''; ?> /> <?php echo __('Administrator'); ?> </label> <br/> <label> <input type="checkbox" name="assigned_only" - <?php echo ($info['assigned_only']) ? 'checked="checked"' : ''; ?> /> + <?php echo ($staff->assigned_only) ? 'checked="checked"' : ''; ?> /> <?php echo __('Limit ticket access to ONLY assigned tickets'); ?> </label> <br/> <label> <input type="checkbox" name="onvacation" - <?php echo ($info['onvacation']) ? 'checked="checked"' : ''; ?> /> + <?php echo ($staff->onvacation) ? 'checked="checked"' : ''; ?> /> <?php echo __('Vacation Mode'); ?> </label> <br/> @@ -181,7 +187,7 @@ if (count($bks) > 1) { </div> <textarea name="notes" class="richtext"> - <?php echo $info['notes']; ?> + <?php echo Format::viewableImages($staff->notes); ?> </textarea> </div> @@ -206,7 +212,7 @@ if (count($bks) > 1) { <option value="0">— <?php echo __('Select Department');?> —</option> <?php foreach (Dept::getDepartments() as $id=>$name) { - $sel=($info['dept_id']==$id)?'selected="selected"':''; + $sel=($staff->dept_id==$id)?'selected="selected"':''; echo sprintf('<option value="%d" %s>%s</option>',$id,$sel,$name); } ?> @@ -221,7 +227,7 @@ if (count($bks) > 1) { <option value="0">— <?php echo __('Select Role');?> —</option> <?php foreach (Role::getRoles() as $id=>$name) { - $sel=($info['role_id']==$id)?'selected="selected"':''; + $sel=($staff->role_id==$id)?'selected="selected"':''; echo sprintf('<option value="%d" %s>%s</option>',$id,$sel,$name); } ?> diff --git a/scp/css/scp.css b/scp/css/scp.css index 76f5468e4751863add285ee0267f3ce65a899930..aed30308b6c8706f526736f91e799777aa92ba9a 100644 --- a/scp/css/scp.css +++ b/scp/css/scp.css @@ -667,6 +667,9 @@ input[type=search] { .table > tbody > tr.header + tr td { padding-top: 10px; } +.table td .pull-right { + margin-right: 15px; +} .form_table { margin-top:3px;