diff --git a/css/jquery.multiselect.css b/css/jquery.multiselect.css new file mode 100644 index 0000000000000000000000000000000000000000..8a08e22b75ba758290971dbadb1a903bddca534e --- /dev/null +++ b/css/jquery.multiselect.css @@ -0,0 +1,23 @@ +.ui-multiselect { padding:2px 0 2px 4px; text-align:left } +.ui-multiselect span.ui-icon { float:right } +.ui-multiselect-single .ui-multiselect-checkboxes input { position:absolute !important; top: auto !important; left:-9999px; } +.ui-multiselect-single .ui-multiselect-checkboxes label { padding:5px !important } + +.ui-multiselect-header { margin-bottom:3px; padding:3px 0 3px 4px } +.ui-multiselect-header ul { font-size:0.9em } +.ui-multiselect-header ul li { float:left; padding:0 10px 0 0 } +.ui-multiselect-header a { text-decoration:none } +.ui-multiselect-header a:hover { text-decoration:underline } +.ui-multiselect-header span.ui-icon { float:left } +.ui-multiselect-header li.ui-multiselect-close { float:right; text-align:right; padding-right:0 } + +.ui-multiselect-menu { display:none; padding:3px; position:absolute; z-index:10000; text-align: left } +.ui-multiselect-checkboxes { position:relative /* fixes bug in IE6/7 */; overflow-y:auto } +.ui-multiselect-checkboxes label { cursor:default; display:block; border:1px solid transparent; padding:3px 1px } +.ui-multiselect-checkboxes label input { position:relative; top:1px } +.ui-multiselect-checkboxes li { clear:both; font-size:0.9em; padding-right:3px } +.ui-multiselect-checkboxes li.ui-multiselect-optgroup-label { text-align:center; font-weight:bold; border-bottom:1px solid } +.ui-multiselect-checkboxes li.ui-multiselect-optgroup-label a { display:block; padding:3px; margin:1px 0; text-decoration:none } + +/* remove label borders in IE6 because IE6 does not support transparency */ +* html .ui-multiselect-checkboxes label { border:none } diff --git a/include/ajax.orgs.php b/include/ajax.orgs.php index eeaa23c7eb4a9a1405c624d731dac599ad49a946..9a72c20449470d3c8ce8fa916132e063374fd862 100644 --- a/include/ajax.orgs.php +++ b/include/ajax.orgs.php @@ -61,11 +61,12 @@ class OrgsAjaxAPI extends AjaxController { ); $forms = $org->getForms(); + $action = "#orgs/{$org->id}/profile"; - include(STAFFINC_DIR . 'templates/org.tmpl.php'); + include(STAFFINC_DIR . 'templates/org-profile.tmpl.php'); } - function updateOrg($id) { + function updateOrg($id, $profile=false) { global $thisstaff; if(!$thisstaff) @@ -78,7 +79,15 @@ class OrgsAjaxAPI extends AjaxController { Http::response(201, $org->to_json()); $forms = $org->getForms(); - include(STAFFINC_DIR . 'templates/org.tmpl.php'); + + if ($profile) { + $action = "#orgs/{$org->id}/profile"; + include(STAFFINC_DIR . 'templates/org-profile.tmpl.php'); + } + else { + $action = "#orgs/{$org->id}"; + include(STAFFINC_DIR . 'templates/org.tmpl.php'); + } } diff --git a/include/class.config.php b/include/class.config.php index 03fe8891c0acfbabe2ea41c750f926a606225277..aae5a2a7e8eb8d98bc0a234fbe5b1e4d84d94995 100644 --- a/include/class.config.php +++ b/include/class.config.php @@ -610,6 +610,10 @@ class OsticketConfig extends Config { return ($this->get('message_alert_dept_manager')); } + function alertAcctManagerONNewMessage() { + return ($this->get('message_alert_acct_manager')); + } + function alertONNewNote() { return ($this->get('note_alert_active')); } @@ -642,6 +646,10 @@ class OsticketConfig extends Config { return ($this->get('ticket_alert_dept_members')); } + function alertAcctManagerONNewTicket() { + return ($this->get('ticket_alert_acct_manager')); + } + function alertONTransfer() { return ($this->get('transfer_alert_active')); } @@ -1061,13 +1069,15 @@ class OsticketConfig extends Config { if($vars['ticket_alert_active'] && (!isset($vars['ticket_alert_admin']) && !isset($vars['ticket_alert_dept_manager']) - && !isset($vars['ticket_alert_dept_members']))) { + && !isset($vars['ticket_alert_dept_members']) + && !isset($vars['ticket_alert_acct_manager']))) { $errors['ticket_alert_active']='Select recipient(s)'; } if($vars['message_alert_active'] && (!isset($vars['message_alert_laststaff']) && !isset($vars['message_alert_assigned']) - && !isset($vars['message_alert_dept_manager']))) { + && !isset($vars['message_alert_dept_manager']) + && !isset($vars['message_alert_acct_manager']))) { $errors['message_alert_active']='Select recipient(s)'; } @@ -1106,10 +1116,12 @@ class OsticketConfig extends Config { 'ticket_alert_admin'=>isset($vars['ticket_alert_admin'])?1:0, 'ticket_alert_dept_manager'=>isset($vars['ticket_alert_dept_manager'])?1:0, 'ticket_alert_dept_members'=>isset($vars['ticket_alert_dept_members'])?1:0, + 'ticket_alert_acct_manager'=>isset($vars['ticket_alert_acct_manager'])?1:0, 'message_alert_active'=>$vars['message_alert_active'], 'message_alert_laststaff'=>isset($vars['message_alert_laststaff'])?1:0, 'message_alert_assigned'=>isset($vars['message_alert_assigned'])?1:0, 'message_alert_dept_manager'=>isset($vars['message_alert_dept_manager'])?1:0, + 'message_alert_acct_manager'=>isset($vars['message_alert_acct_manager'])?1:0, 'note_alert_active'=>$vars['note_alert_active'], 'note_alert_laststaff'=>isset($vars['note_alert_laststaff'])?1:0, 'note_alert_assigned'=>isset($vars['note_alert_assigned'])?1:0, diff --git a/include/class.organization.php b/include/class.organization.php index a837505fa4b61c1f40311e2d562f8a9081caa827..4f55f290b3d08d038c07c2d97552095d80932633 100644 --- a/include/class.organization.php +++ b/include/class.organization.php @@ -28,6 +28,12 @@ class OrganizationModel extends VerySimpleModel { ) ); + const COLLAB_ALL_MEMBERS = 0x0001; + const COLLAB_PRIMARY_CONTACT = 0x0002; + const ASSIGN_AGENT_MANAGER = 0x0004; + + var $_manager; + function getId() { return $this->id; } @@ -36,6 +42,32 @@ class OrganizationModel extends VerySimpleModel { return $this->name; } + function getAccountManager() { + if (!isset($this->_manager)) { + if ($this->manager[0] == 't') + $this->_manager = Team::lookup(substr($this->manager, 1)); + if ($this->manager[0] == 's') + $this->_manager = Staff::lookup(substr($this->manager, 1)); + } + return $this->_manager; + } + + function getAccountManagerId() { + return $this->manager; + } + + function autoAddCollabs() { + return $this->check(self::COLLAB_ALL_MEMBERS | self::COLLAB_PRIMARY_CONTACT); + } + + function autoAddPrimaryContactsAsCollabs() { + return $this->check(self::COLLAB_PRIMARY_CONTACT); + } + + function autoAddMembersAsCollabs() { + return $this->check(self::COLLAB_ALL_MEMBERS); + } + function getUpdateDate() { return $this->updated; } @@ -43,6 +75,22 @@ class OrganizationModel extends VerySimpleModel { function getCreateDate() { return $this->created; } + + function check($flag) { + return 0 !== ($this->status & $flag); + } + + protected function clearStatus($flag) { + return $this->set('status', $this->get('status') & ~$flag); + } + + protected function setStatus($flag) { + return $this->set('status', $this->get('status') | $flag); + } + + function allMembers() { + return $this->users; + } } class Organization extends OrganizationModel { @@ -96,6 +144,28 @@ class Organization extends OrganizationModel { return $this->_forms; } + function getInfo() { + $base = $this->ht; + foreach (array( + 'collab-all-flag' => Organization::COLLAB_ALL_MEMBERS, + 'collab-pc-flag' => Organization::COLLAB_PRIMARY_CONTACT, + 'assign-am-flag' => Organization::ASSIGN_AGENT_MANAGER, + ) as $ck=>$flag) { + if ($this->check($flag)) + $base[$ck] = true; + } + return $base; + } + + function isMappedToDomain($domain) { + foreach (explode(',', $this->domain) as $d) { + if (strcasecmp($domain, trim($d)) === 0) { + return true; + } + } + return false; + } + function to_json() { $info = array( @@ -129,7 +199,29 @@ class Organization extends OrganizationModel { } } - if (!$valid) + if ($vars['domain']) { + foreach (explode(',', $vars['domain']) as $d) { + if (!Validator::is_email('t@' . trim($d))) { + $errors['domain'] = 'Enter a valid email domain, like domain.com'; + } + } + } + + if ($vars['manager']) { + switch ($vars['manager'][0]) { + case 's': + if ($staff = Staff::lookup(substr($vars['manager'], 1))) + break; + case 't': + if ($vars['manager'][0] == 't' + && $team = Team::lookup(substr($vars['manager'], 1))) + break; + default: + $errors['manager'] = 'Select a staff member or team from the list'; + } + } + + if (!$valid || $errors) return false; foreach ($this->getDynamicData() as $cd) { @@ -142,7 +234,29 @@ class Organization extends OrganizationModel { $cd->save(); } - return true; + // Set flags + foreach (array( + 'collab-all-flag' => Organization::COLLAB_ALL_MEMBERS, + 'collab-pc-flag' => Organization::COLLAB_PRIMARY_CONTACT, + 'assign-am-flag' => Organization::ASSIGN_AGENT_MANAGER, + ) as $ck=>$flag) { + if ($vars[$ck]) + $this->setStatus($flag); + else + $this->clearStatus($flag); + } + + // Set staff and primary contacts + $this->set('domain', $vars['domain']); + $this->set('manager', $vars['manager'] ?: ''); + if ($vars['contacts'] && is_array($vars['contacts'])) { + foreach ($this->allMembers() as $u) { + $u->setPrimaryContact(array_search($u->id, $vars['contacts']) !== false); + $u->save(); + } + } + + return $this->save(); } static function fromVars($vars) { diff --git a/include/class.ticket.php b/include/class.ticket.php index a61717b811dcbd94460edd0167e135262dbbef67..0bf1e57d7c80e088db109e2e989914d464dd55eb 100644 --- a/include/class.ticket.php +++ b/include/class.ticket.php @@ -937,6 +937,16 @@ class Ticket { if($cfg->alertDeptManagerONNewTicket() && $dept && ($manager=$dept->getManager())) $recipients[]= $manager; + // Account manager + if ($cfg->alertAcctManagerONNewMessage() + && ($org = $this->getOwner()->getOrganization()) + && ($acct_manager = $org->getAcctManager())) { + if ($acct_manager instanceof Team) + $recipients = array_merge($recipients, $acct_manager->getMembers()); + else + $recipients[] = $acct_manager; + } + foreach( $recipients as $k=>$staff) { if(!is_object($staff) || !$staff->isAvailable() || in_array($staff->getEmail(), $sentlist)) continue; $alert = $this->replaceVars($msg, array('recipient' => $staff)); @@ -1132,7 +1142,7 @@ class Ticket { $recipients[] = $assignee; } elseif(!strcasecmp(get_class($assignee), 'Team')) { if($cfg->alertTeamMembersONAssignment() && ($members=$assignee->getMembers())) - $recipients+=$members; + $recipients = array_merge($recipients, $members); elseif($cfg->alertTeamLeadONAssignment() && ($lead=$assignee->getTeamLead())) $recipients[] = $lead; } @@ -1388,11 +1398,11 @@ class Ticket { if($this->getStaffId()) $recipients[]=$this->getStaff(); elseif($this->getTeamId() && ($team=$this->getTeam()) && ($members=$team->getMembers())) - $recipients+=$members; + $recipients = array_merge($recipients, $members); } elseif($cfg->alertDeptMembersONTransfer() && !$this->isAssigned()) { //Only alerts dept members if the ticket is NOT assigned. if(($members=$dept->getMembers())) - $recipients+=$members; + $recipients = array_merge($recipients, $members); } //Always alert dept manager?? @@ -1620,6 +1630,16 @@ class Ticket { if($cfg->alertDeptManagerONNewMessage() && $dept && ($manager=$dept->getManager())) $recipients[]=$manager; + // Account manager + if ($cfg->alertAcctManagerONNewMessage() + && ($org = $this->getOwner()->getOrganization()) + && ($acct_manager = $org->getAcctManager())) { + if ($acct_manager instanceof Team) + $recipients = array_merge($recipients, $acct_manager->getMembers()); + else + $recipients[] = $acct_manager; + } + $sentlist=array(); //I know it sucks...but..it works. foreach( $recipients as $k=>$staff) { if(!$staff || !$staff->getEmail() || !$staff->isAvailable() || in_array($staff->getEmail(), $sentlist)) continue; @@ -2333,6 +2353,15 @@ class Ticket { $source = 'Email'; } + // Auto assignment to organization account manager + if (($org = $user->getOrganization()) + && ($code = $org->getAccountManagerId())) { + if (!isset($vars['staffId']) && $code[0] == 's') + $vars['staffId'] = substr($code, 1); + elseif (!isset($vars['teamId']) && $code[0] == 't') + $vars['teamId'] = substr($code, 1); + } + // Intenal mapping magic...see if we need to override anything if (isset($topic)) { $deptId = $deptId ?: $topic->getDeptId(); @@ -2409,6 +2438,28 @@ class Ticket { $dept = $ticket->getDept(); + // Add organizational collaborators + if ($org && $org->autoAddCollabs()) { + $pris = $org->autoAddPrimaryContactsAsCollabs(); + $members = $org->autoAddMembersAsCollabs(); + $settings = array('isactive' => true); + $collabs = array(); + foreach ($org->allMembers() as $u) { + if ($members || ($pris && $u->isPrimaryContact())) { + if ($c = $this->addCollaborator($u, $settings, $errors)) { + $collabs[] = (string) $c; + } + } + } + //TODO: Can collaborators add others? + if ($collabs) { + //TODO: Change EndUser to name of user. + $this->logNote(sprintf('Collaborators for %s organization added', + $org->getName()), + implode("<br>", $collabs), $org->getName(), false); + } + } + //post the message. unset($vars['cannedattachments']); //Ticket::open() might have it set as part of open & respond. $vars['title'] = $vars['subject']; //Use the initial subject as title of the post. @@ -2418,11 +2469,18 @@ class Ticket { // Configure service-level-agreement for this ticket $ticket->selectSLAId($vars['slaId']); - //Auto assign staff or team - auto assignment based on filter rules. - if($vars['staffId'] && !$vars['assignId']) - $ticket->assignToStaff($vars['staffId'], 'Auto Assignment'); - if($vars['teamId'] && !$vars['assignId']) - $ticket->assignToTeam($vars['teamId'], 'Auto Assignment'); + // Assign ticket to staff or team (new ticket by staff) + if($vars['assignId']) { + $ticket->assign($vars['assignId'], $vars['note']); + } + else { + // Auto assign staff or team - auto assignment based on filter + // rules. Both team and staff can be assigned + if ($vars['staffId']) + $ticket->assignToStaff($vars['staffId'], 'Auto Assignment'); + if ($vars['teamId']) + $ticket->assignToTeam($vars['teamId'], 'Auto Assignment'); + } /********** double check auto-response ************/ //Override auto responder if the FROM email is one of the internal emails...loop control. @@ -2489,7 +2547,10 @@ class Ticket { $errors['name'] = 'Name required'; } - if(!($ticket=Ticket::create($vars, $errors, 'staff', false, (!$vars['assignId'])))) + if (!$thisstaff->canAssignTickets()) + unset($vars['assignId']); + + if(!($ticket=Ticket::create($vars, $errors, 'staff', false))) return false; $vars['msgId']=$ticket->getLastMsgId(); @@ -2510,13 +2571,14 @@ class Ticket { } } - //Post Internal note - if($vars['assignId'] && $thisstaff->canAssignTickets()) { //Assign ticket to staff or team. - $ticket->assign($vars['assignId'], $vars['note']); - } elseif($vars['note']) { //Not assigned...save optional note if any + // Not assigned...save optional note if any + if (!$vars['assignId'] && $vars['note']) { $ticket->logNote('New Ticket', $vars['note'], $thisstaff, false); - } else { //Not assignment and no internal note - log activity - $ticket->logActivity('New Ticket by Staff','Ticket created by staff -'.$thisstaff->getName()); + } + else { + // Not assignment and no internal note - log activity + $ticket->logActivity('New Ticket by Staff', + 'Ticket created by staff -'.$thisstaff->getName()); } $ticket->reload(); diff --git a/include/class.user.php b/include/class.user.php index 65f40b103f2b213350bbb6f97ce29419eb0b571d..8adb24ea07d5294cdcd2ad4d99331dd937c9789b 100644 --- a/include/class.user.php +++ b/include/class.user.php @@ -77,6 +77,8 @@ class UserModel extends VerySimpleModel { ) ); + const PRIMARY_ORG_CONTACT = 0x0001; + function getId() { return $this->id; } @@ -110,6 +112,29 @@ class UserModel extends VerySimpleModel { return true; } + + protected function hasStatus($flag) { + return $this->get('status') & $flag !== 0; + } + + protected function clearStatus($flag) { + return $this->set('status', $this->get('status') & ~$flag); + } + + protected function setStatus($flag) { + return $this->set('status', $this->get('status') | $flag); + } + + function isPrimaryContact() { + return $this->hasStatus(User::PRIMARY_ORG_CONTACT); + } + + function setPrimaryContact($flag) { + if ($flag) + $this->setStatus(User::PRIMARY_ORG_CONTACT); + else + $this->clearStatus(User::PRIMARY_ORG_CONTACT); + } } class User extends UserModel { @@ -129,6 +154,16 @@ class User extends UserModel { // of the detached emails is fixed. 'default_email' => UserEmail::ensure($vars['email']) )); + // Is there an organization registered for this domain + list($mailbox, $domain) = explode('@', $vars['email'], 2); + foreach (Organization::objects() + ->filter(array('domain__contains'=>$domain)) as $org) { + if ($org->isMappedToDomain($domain)) { + $user->setOrganization($org); + break; + } + } + $user->save(true); $user->emails->add($user->default_email); // Attach initial custom fields diff --git a/include/class.validator.php b/include/class.validator.php index c5cd2283eb362a58325e6291a3d2c5f245816116..28bf61c2b90f917c97b31a2b6bf049b2b7668299 100644 --- a/include/class.validator.php +++ b/include/class.validator.php @@ -140,13 +140,23 @@ class Validator { /*** Functions below can be called directly without class instance. Validator::func(var..); (nolint) ***/ - function is_email($email) { - if (strpos($email, '@') === false) - return false; - + function is_email($email, $list=false) { require_once 'Mail/RFC822.php'; require_once 'PEAR.php'; - return !PEAR::isError(Mail_RFC822::parseAddressList($email)); + if (!($mails = Mail_RFC822::parseAddressList($email)) || PEAR::isError($mails)) + return false; + + if (!$list && count($mails) > 1) + return false; + + foreach ($mails as $m) { + if (!$m->mailbox) + return false; + if ($m->host == 'localhost') + return false; + } + + return true; } function is_phone($phone) { /* We're not really validating the phone number but just making sure it doesn't contain illegal chars and of acceptable len */ diff --git a/include/i18n/en_US/help/tips/settings.alerts.yaml b/include/i18n/en_US/help/tips/settings.alerts.yaml index 19fb979c9b472557ec09a9a3b51920afe422be4b..c0532f1ac482f33efabdc2d0acb02b0284c68c56 100644 --- a/include/i18n/en_US/help/tips/settings.alerts.yaml +++ b/include/i18n/en_US/help/tips/settings.alerts.yaml @@ -16,8 +16,14 @@ new_ticket: title: New Ticket Alert content: > + <p> Alert sent out to staff members when new tickets are created in the system. + </p><p class="info-banner"> + <i class="icon-info-sign"></i> + Staff members (department members) are not + notified with this alert if the ticket is automatically assigned. + </p> links: - title: Default New Ticket Alert Template href: /scp/templates.php?default_for=ticket.alert diff --git a/include/staff/settings-alerts.inc.php b/include/staff/settings-alerts.inc.php index 0ab5cb689367ca713400a67af17613d261eec4b8..c0dbc628e38bae07c38eac2da5a83258229e56c3 100644 --- a/include/staff/settings-alerts.inc.php +++ b/include/staff/settings-alerts.inc.php @@ -36,6 +36,11 @@ <input type="checkbox" name="ticket_alert_dept_members" <?php echo $config['ticket_alert_dept_members']?'checked':''; ?>> Department Members <em>(spammy)</em> </td> </tr> + <tr> + <td> + <input type="checkbox" name="ticket_alert_acct_manager" <?php echo $config['ticket_alert_acct_manager']?'checked':''; ?>> Organization Account Manager + </td> + </tr> <tr><th><em><b>New Message Alert</b>: <i class="help-tip icon-question-sign" href="#new_message"></i> </em></th></tr> @@ -61,6 +66,11 @@ <input type="checkbox" name="message_alert_dept_manager" <?php echo $config['message_alert_dept_manager']?'checked':''; ?>> Department Manager <em>(spammy)</em> </td> </tr> + <tr> + <td> + <input type="checkbox" name="message_alert_acct_manager" <?php echo $config['message_alert_acct_manager']?'checked':''; ?>> Organization Account Manager + </td> + </tr> <tr><th><em><b>New Internal Note Alert</b>: <i class="help-tip icon-question-sign" href="#new_activity"></i> </em></th></tr> @@ -72,11 +82,6 @@ <font class="error"> <?php echo $errors['note_alert_active']; ?></font> </td> </tr> - <tr> - <td> - <input type="checkbox" name="note_alert_laststaff" <?php echo $config['note_alert_laststaff']?'checked':''; ?>> Last Respondent - </td> - </tr> <tr> <td> <input type="checkbox" name="note_alert_assigned" <?php echo $config['note_alert_assigned']?'checked':''; ?>> Assigned Staff diff --git a/include/staff/templates/org-profile.tmpl.php b/include/staff/templates/org-profile.tmpl.php new file mode 100644 index 0000000000000000000000000000000000000000..3dda76484b0a7e0432957b2045eff10b83580f9f --- /dev/null +++ b/include/staff/templates/org-profile.tmpl.php @@ -0,0 +1,171 @@ +<?php +$info=($_POST && $errors)?Format::input($_POST):@Format::htmlchars($org->getInfo()); + +if (!$info['title']) + $info['title'] = Format::htmlchars($org->getName()); +?> +<script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/jquery.multiselect.min.js"></script> +<link rel="stylesheet" href="<?php echo ROOT_PATH; ?>css/jquery.multiselect.css"/> +<h3><?php echo $info['title']; ?></h3> +<b><a class="close" href="#"><i class="icon-remove-circle"></i></a></b> +<hr/> +<?php +if ($info['error']) { + echo sprintf('<p id="msg_error">%s</p>', $info['error']); +} elseif ($info['msg']) { + echo sprintf('<p id="msg_notice">%s</p>', $info['msg']); +} ?> +<ul class="tabs"> + <li><a href="#tab-profile" class="active" + ><i class="icon-edit"></i> Fields</a></li> + <li><a href="#contact-settings" + ><i class="icon-fixed-width icon-cogs faded"></i> Settings</a></li> +</ul> +<form method="post" class="org" action="<?php echo $action; ?>"> + +<div class="tab_content" id="tab-profile" style="margin:5px;"> +<?php +$action = $info['action'] ? $info['action'] : ('#orgs/'.$org->getId()); +if ($ticket && $ticket->getOwnerId() == $user->getId()) + $action = '#tickets/'.$ticket->getId().'/user'; +?> + <input type="hidden" name="id" value="<?php echo $org->getId(); ?>" /> + <table width="100%"> + <?php + if (!$forms) $forms = $org->getForms(); + foreach ($forms as $form) + $form->render(); + ?> + </table> +</div> + +<div class="tab_content" id="contact-settings" style="display:none;margin:5px;"> + <table style="width:100%"> + <tbody> + <tr> + <td width="180"> + Account Manager: + </td> + <td> + <select name="manager"> + <option value="0" selected="selected">— None —</option><?php + if ($users=Staff::getAvailableStaffMembers()) { ?> + <optgroup label="Staff Members (<?php echo count($users); ?>)"> +<?php foreach($users as $id => $name) { + $k = "s$id"; + echo sprintf('<option value="%s" %s>%s</option>', + $k,(($info['manager']==$k)?'selected="selected"':''),$name); + } + echo '</optgroup>'; + } + + if ($teams=Team::getActiveTeams()) { ?> + <optgroup label="Teams (<?php echo count($teams); ?>)"> +<?php foreach($teams as $id => $name) { + $k="t$id"; + echo sprintf('<option value="%s" %s>%s</option>', + $k,(($info['manager']==$k)?'selected="selected"':''),$name); + } + echo '</optgroup>'; + } ?> + </select> + <br/><span class="error"><?php echo $errors['manager']; ?></span> + </td> + </tr> + <tr> + <td width="180"> + Auto-Assignment: + </td> + <td> + <input type="checkbox" name="assign-am-flag" value="1" <?php echo $info['assign-am-flag']?'checked="checked"':''; ?>> + Assign tickets from this organization to the <em>Account Manager</em> + </tr> + <tr> + <td width="180"> + Primary Contacts: + </td> + <td> + <select name="contacts[]" id="primary_contacts" multiple="multiple"> +<?php foreach ($org->allMembers() as $u) { ?> + <option value="<?php echo $u->id; ?>" <?php + if ($u->isPrimaryContact()) + echo 'selected="selected"'; ?>><?php echo $u->getName(); ?></option> +<?php } ?> + </select> + <br/><span class="error"><?php echo $errors['contacts']; ?></span> + </td> + <tr> + <th colspan="2"> + Automated Collaboration: + </th> + </tr> + <tr> + <td width="180"> + Primary Contacts: + </td> + <td> + <input type="checkbox" name="collab-pc-flag" value="1" <?php echo $info['collab-pc-flag']?'checked="checked"':''; ?>> + Add to all tickets from this organization + </td> + </tr> + <tr> + <td width="180"> + Organization Members: + </td> + <td> + <input type="checkbox" name="collab-all-flag" value="1" <?php echo $info['collab-all-flag']?'checked="checked"':''; ?>> + Add to all tickets from this organization + </td> + </tr> + <tr> + <th colspan="2"> + Main Domain + </th> + </tr> + <tr> + <td style="width:180px"> + Auto Add Members From: + </td> + <td> + <input type="text" size="40" maxlength="60" name="domain" + value="<?php echo $info['domain']; ?>" /> + <br/><span class="error"><?php echo $errors['domain']; ?></span> + </td> + </tr> + </tbody> + </table> +</div> + +<div class="clear"></div> + +<hr> +<p class="full-width"> + <span class="buttons" style="float:left"> + <input type="reset" value="Reset"> + <input type="button" name="cancel" class="<?php +echo $account ? 'cancel' : 'close'; ?>" value="Cancel"> + </span> + <span class="buttons" style="float:right"> + <input type="submit" value="Update Organization"> + </span> +</p> +</form> + +<script type="text/javascript"> +$(function() { + $('a#editorg').click( function(e) { + e.preventDefault(); + $('div#org-profile').hide(); + $('div#org-form').fadeIn(); + return false; + }); + + $(document).on('click', 'form.org input.cancel', function (e) { + e.preventDefault(); + $('div#org-form').hide(); + $('div#org-profile').fadeIn(); + return false; + }); + $("#primary_contacts").multiselect({'noneSelectedText':'Select Contacts'}); +}); +</script> diff --git a/js/jquery.multiselect.min.js b/js/jquery.multiselect.min.js new file mode 100644 index 0000000000000000000000000000000000000000..e9243506c0555f0d53b58b40d5d185ace8035b7c --- /dev/null +++ b/js/jquery.multiselect.min.js @@ -0,0 +1,20 @@ +/* + * jQuery MultiSelect UI Widget 1.13 + * Copyright (c) 2012 Eric Hynds + * + * http://www.erichynds.com/jquery/jquery-ui-multiselect-widget/ + * + * Depends: + * - jQuery 1.4.2+ + * - jQuery UI 1.8 widget factory + * + * Optional: + * - jQuery UI effects + * - jQuery UI position utility + * + * Dual licensed under the MIT and GPL licenses: + * http://www.opensource.org/licenses/mit-license.php + * http://www.gnu.org/licenses/gpl.html + * + */ +(function(d){var k=0;d.widget("ech.multiselect",{options:{header:!0,height:175,minWidth:225,classes:"",checkAllText:"Check all",uncheckAllText:"Uncheck all",noneSelectedText:"Select options",selectedText:"# selected",selectedList:0,show:null,hide:null,autoOpen:!1,multiple:!0,position:{}},_create:function(){var a=this.element.hide(),b=this.options;this.speed=d.fx.speeds._default;this._isOpen=!1;a=(this.button=d('<button type="button"><span class="ui-icon ui-icon-triangle-2-n-s"></span></button>')).addClass("ui-multiselect ui-widget ui-state-default ui-corner-all").addClass(b.classes).attr({title:a.attr("title"),"aria-haspopup":!0,tabIndex:a.attr("tabIndex")}).insertAfter(a);(this.buttonlabel=d("<span />")).html(b.noneSelectedText).appendTo(a);var a=(this.menu=d("<div />")).addClass("ui-multiselect-menu ui-widget ui-widget-content ui-corner-all").addClass(b.classes).appendTo(document.body),c=(this.header=d("<div />")).addClass("ui-widget-header ui-corner-all ui-multiselect-header ui-helper-clearfix").appendTo(a);(this.headerLinkContainer=d("<ul />")).addClass("ui-helper-reset").html(function(){return!0===b.header?'<li><a class="ui-multiselect-all" href="#"><span class="ui-icon ui-icon-check"></span><span>'+b.checkAllText+'</span></a></li><li><a class="ui-multiselect-none" href="#"><span class="ui-icon ui-icon-closethick"></span><span>'+b.uncheckAllText+"</span></a></li>":"string"===typeof b.header?"<li>"+b.header+"</li>":""}).append('<li class="ui-multiselect-close"><a href="#" class="ui-multiselect-close"><span class="ui-icon ui-icon-circle-close"></span></a></li>').appendTo(c);(this.checkboxContainer=d("<ul />")).addClass("ui-multiselect-checkboxes ui-helper-reset").appendTo(a);this._bindEvents();this.refresh(!0);b.multiple||a.addClass("ui-multiselect-single")},_init:function(){!1===this.options.header&&this.header.hide();this.options.multiple||this.headerLinkContainer.find(".ui-multiselect-all, .ui-multiselect-none").hide();this.options.autoOpen&&this.open();this.element.is(":disabled")&&this.disable()},refresh:function(a){var b=this.element,c=this.options,f=this.menu,h=this.checkboxContainer,g=[],e="",i=b.attr("id")||k++;b.find("option").each(function(b){d(this);var a=this.parentNode,f=this.innerHTML,h=this.title,k=this.value,b="ui-multiselect-"+(this.id||i+"-option-"+b),l=this.disabled,n=this.selected,m=["ui-corner-all"],o=(l?"ui-multiselect-disabled ":" ")+this.className,j;"OPTGROUP"===a.tagName&&(j=a.getAttribute("label"),-1===d.inArray(j,g)&&(e+='<li class="ui-multiselect-optgroup-label '+a.className+'"><a href="#">'+j+"</a></li>",g.push(j)));l&&m.push("ui-state-disabled");n&&!c.multiple&&m.push("ui-state-active");e+='<li class="'+o+'">';e+='<label for="'+b+'" title="'+h+'" class="'+m.join(" ")+'">';e+='<input id="'+b+'" name="multiselect_'+i+'" type="'+(c.multiple?"checkbox":"radio")+'" value="'+k+'" title="'+f+'"';n&&(e+=' checked="checked"',e+=' aria-selected="true"');l&&(e+=' disabled="disabled"',e+=' aria-disabled="true"');e+=" /><span>"+f+"</span></label></li>"});h.html(e);this.labels=f.find("label");this.inputs=this.labels.children("input");this._setButtonWidth();this._setMenuWidth();this.button[0].defaultValue=this.update();a||this._trigger("refresh")},update:function(){var a=this.options,b=this.inputs,c=b.filter(":checked"),f=c.length,a=0===f?a.noneSelectedText:d.isFunction(a.selectedText)?a.selectedText.call(this,f,b.length,c.get()):/\d/.test(a.selectedList)&&0<a.selectedList&&f<=a.selectedList?c.map(function(){return d(this).next().html()}).get().join(", "):a.selectedText.replace("#",f).replace("#",b.length);this.buttonlabel.html(a);return a},_bindEvents:function(){function a(){b[b._isOpen? "close":"open"]();return!1}var b=this,c=this.button;c.find("span").bind("click.multiselect",a);c.bind({click:a,keypress:function(a){switch(a.which){case 27:case 38:case 37:b.close();break;case 39:case 40:b.open()}},mouseenter:function(){c.hasClass("ui-state-disabled")||d(this).addClass("ui-state-hover")},mouseleave:function(){d(this).removeClass("ui-state-hover")},focus:function(){c.hasClass("ui-state-disabled")||d(this).addClass("ui-state-focus")},blur:function(){d(this).removeClass("ui-state-focus")}});this.header.delegate("a","click.multiselect",function(a){if(d(this).hasClass("ui-multiselect-close"))b.close();else b[d(this).hasClass("ui-multiselect-all")?"checkAll":"uncheckAll"]();a.preventDefault()});this.menu.delegate("li.ui-multiselect-optgroup-label a","click.multiselect",function(a){a.preventDefault();var c=d(this),g=c.parent().nextUntil("li.ui-multiselect-optgroup-label").find("input:visible:not(:disabled)"),e=g.get(),c=c.parent().text();!1!==b._trigger("beforeoptgrouptoggle",a,{inputs:e,label:c})&&(b._toggleChecked(g.filter(":checked").length!==g.length,g),b._trigger("optgrouptoggle",a,{inputs:e,label:c,checked:e[0].checked}))}).delegate("label","mouseenter.multiselect",function(){d(this).hasClass("ui-state-disabled")||(b.labels.removeClass("ui-state-hover"),d(this).addClass("ui-state-hover").find("input").focus())}).delegate("label","keydown.multiselect",function(a){a.preventDefault();switch(a.which){case 9:case 27:b.close();break;case 38:case 40:case 37:case 39:b._traverse(a.which,this);break;case 13:d(this).find("input")[0].click()}}).delegate('input[type="checkbox"], input[type="radio"]',"click.multiselect",function(a){var c=d(this),g=this.value,e=this.checked,i=b.element.find("option");this.disabled||!1===b._trigger("click",a,{value:g,text:this.title,checked:e})?a.preventDefault():(c.focus(),c.attr("aria-selected",e),i.each(function(){this.value===g?this.selected=e:b.options.multiple||(this.selected=!1)}),b.options.multiple||(b.labels.removeClass("ui-state-active"),c.closest("label").toggleClass("ui-state-active",e),b.close()),b.element.trigger("change"),setTimeout(d.proxy(b.update,b),10))});d(document).bind("mousedown.multiselect",function(a){b._isOpen&&(!d.contains(b.menu[0],a.target)&&!d.contains(b.button[0],a.target)&&a.target!==b.button[0])&&b.close()});d(this.element[0].form).bind("reset.multiselect",function(){setTimeout(d.proxy(b.refresh,b),10)})},_setButtonWidth:function(){var a=this.element.outerWidth(),b=this.options;/\d/.test(b.minWidth)&&a<b.minWidth&&(a=b.minWidth);this.button.width(a)},_setMenuWidth:function(){var a=this.menu,b=this.button.outerWidth()-parseInt(a.css("padding-left"),10)-parseInt(a.css("padding-right"),10)-parseInt(a.css("border-right-width"),10)-parseInt(a.css("border-left-width"),10);a.width(b||this.button.outerWidth())},_traverse:function(a,b){var c=d(b),f=38===a||37===a,c=c.parent()[f?"prevAll":"nextAll"]("li:not(.ui-multiselect-disabled, .ui-multiselect-optgroup-label)")[f?"last":"first"]();c.length?c.find("label").trigger("mouseover"):(c=this.menu.find("ul").last(),this.menu.find("label")[f? "last":"first"]().trigger("mouseover"),c.scrollTop(f?c.height():0))},_toggleState:function(a,b){return function(){this.disabled||(this[a]=b);b?this.setAttribute("aria-selected",!0):this.removeAttribute("aria-selected")}},_toggleChecked:function(a,b){var c=b&&b.length?b:this.inputs,f=this;c.each(this._toggleState("checked",a));c.eq(0).focus();this.update();var h=c.map(function(){return this.value}).get();this.element.find("option").each(function(){!this.disabled&&-1<d.inArray(this.value,h)&&f._toggleState("selected",a).call(this)});c.length&&this.element.trigger("change")},_toggleDisabled:function(a){this.button.attr({disabled:a,"aria-disabled":a})[a?"addClass":"removeClass"]("ui-state-disabled");var b=this.menu.find("input"),b=a?b.filter(":enabled").data("ech-multiselect-disabled",!0):b.filter(function(){return!0===d.data(this,"ech-multiselect-disabled")}).removeData("ech-multiselect-disabled");b.attr({disabled:a,"arial-disabled":a}).parent()[a?"addClass":"removeClass"]("ui-state-disabled");this.element.attr({disabled:a,"aria-disabled":a})},open:function(){var a=this.button,b=this.menu,c=this.speed,f=this.options,h=[];if(!(!1===this._trigger("beforeopen")||a.hasClass("ui-state-disabled")||this._isOpen)){var g=b.find("ul").last(),e=f.show,i=a.offset();d.isArray(f.show)&&(e=f.show[0],c=f.show[1]||this.speed);e&&(h=[e,c]);g.scrollTop(0).height(f.height);d.ui.position&&!d.isEmptyObject(f.position)?(f.position.of=f.position.of||a,b.show().position(f.position).hide()):b.css({top:i.top+a.outerHeight(),left:i.left});d.fn.show.apply(b,h);this.labels.eq(0).trigger("mouseover").trigger("mouseenter").find("input").trigger("focus");a.addClass("ui-state-active");this._isOpen=!0;this._trigger("open")}},close:function(){if(!1!==this._trigger("beforeclose")){var a=this.options,b=a.hide,c=this.speed,f=[];d.isArray(a.hide)&&(b=a.hide[0],c=a.hide[1]||this.speed);b&&(f=[b,c]);d.fn.hide.apply(this.menu,f);this.button.removeClass("ui-state-active").trigger("blur").trigger("mouseleave");this._isOpen=!1;this._trigger("close")}},enable:function(){this._toggleDisabled(!1)},disable:function(){this._toggleDisabled(!0)},checkAll:function(){this._toggleChecked(!0);this._trigger("checkAll")},uncheckAll:function(){this._toggleChecked(!1);this._trigger("uncheckAll")},getChecked:function(){return this.menu.find("input").filter(":checked")},destroy:function(){d.Widget.prototype.destroy.call(this);this.button.remove();this.menu.remove();this.element.show();return this},isOpen:function(){return this._isOpen},widget:function(){return this.menu},getButton:function(){return this.button},_setOption:function(a,b){var c=this.menu;switch(a){case "header":c.find("div.ui-multiselect-header")[b?"show":"hide"]();break;case "checkAllText":c.find("a.ui-multiselect-all span").eq(-1).text(b);break;case "uncheckAllText":c.find("a.ui-multiselect-none span").eq(-1).text(b);break;case "height":c.find("ul").last().height(parseInt(b,10));break;case "minWidth":this.options[a]=parseInt(b,10);this._setButtonWidth();this._setMenuWidth();break;case "selectedText":case "selectedList":case "noneSelectedText":this.options[a]=b;this.update();break;case "classes":c.add(this.button).removeClass(this.options.classes).addClass(b);break;case "multiple":c.toggleClass("ui-multiselect-single",!b),this.options.multiple=b,this.element[0].multiple=b,this.refresh()}d.Widget.prototype._setOption.apply(this,arguments)}})})(jQuery); diff --git a/scp/ajax.php b/scp/ajax.php index 27fb8022a9d143a2856bc3ad95b686dfef8ffb06..d0830bdf84cf81116154d9caf9f29fd0b1272d28 100644 --- a/scp/ajax.php +++ b/scp/ajax.php @@ -96,6 +96,7 @@ $dispatcher = patterns('', url_get('^/search$', 'search'), url_get('^/(?P<id>\d+)$', 'getOrg'), url_post('^/(?P<id>\d+)$', 'updateOrg'), + url_post('^/(?P<id>\d+)/profile$', 'updateOrg', array(true)), url_get('^/(?P<id>\d+)/edit$', 'editOrg'), url_get('^/lookup/form$', 'lookup'), url_post('^/lookup/form$', 'addOrg'), diff --git a/scp/css/scp.css b/scp/css/scp.css index c73e107cf7fa7b7d8c54417541f7490909acc36a..ce9023b6cbe3e735d8e36bb326eda625154770ed 100644 --- a/scp/css/scp.css +++ b/scp/css/scp.css @@ -51,13 +51,17 @@ a { color: #555; } -#msg_info { margin: 0; padding: 5px; margin-bottom: 10px; color: #3a87ad; border: 1px solid #bce8f1; background-color: #d9edf7; } +#msg_info, +.info-banner { margin: 0; padding: 5px; margin-bottom: 10px; color: #3a87ad; border: 1px solid #bce8f1; background-color: #d9edf7; } -#msg_notice { margin: 0; padding: 5px 10px 5px 36px; margin-bottom: 10px; border: 1px solid #0a0; background: url('../images/icons/ok.png') 10px 50% no-repeat #e0ffe0; } +#msg_notice, +.notice-banner { margin: 0; padding: 5px 10px 5px 36px; margin-bottom: 10px; border: 1px solid #0a0; background: url('../images/icons/ok.png') 10px 50% no-repeat #e0ffe0; } -#msg_warning { margin: 0; padding: 5px 10px 5px 36px; margin-bottom: 10px; border: 1px solid #f26522; background: url('../images/icons/alert.png') 10px 50% no-repeat #ffffdd; } +#msg_warning, +.warning-banner { margin: 0; padding: 5px 10px 5px 36px; margin-bottom: 10px; border: 1px solid #f26522; background: url('../images/icons/alert.png') 10px 50% no-repeat #ffffdd; } -#msg_error { margin: 0; padding: 5px 10px 5px 36px; margin-bottom: 10px; border: 1px solid #a00; background: url('../images/icons/error.png') 10px 50% no-repeat #fff0f0; } +#msg_error, +.error-banner { margin: 0; padding: 5px 10px 5px 36px; margin-bottom: 10px; border: 1px solid #a00; background: url('../images/icons/error.png') 10px 50% no-repeat #fff0f0; } #notice_bar { margin: 0; padding: 5px 10px 5px 36px; height: 16px; line-height: 16px; border: 1px solid #0a0; background: url('../images/icons/ok.png') 10px 50% no-repeat #e0ffe0; } diff --git a/scp/js/scp.js b/scp/js/scp.js index 79f5804f2c5727ecf13694b4f4f5112020aa2987..e2bb171c5a8a7a0e1beef6ce8a7bc1b068304d3f 100644 --- a/scp/js/scp.js +++ b/scp/js/scp.js @@ -589,9 +589,10 @@ $(document).ready(function(){ $(document).on('click.tab', 'ul.tabs li a', function(e) { e.preventDefault(); if ($('.tab_content'+$(this).attr('href')).length) { - $('ul.tabs li a', $(this).closest('ul').parent()).removeClass('active'); + var ul = $(this).closest('ul'); + $('ul.tabs li a', ul.parent()).removeClass('active'); $(this).addClass('active'); - $('.tab_content').hide(); + $('.tab_content', ul.parent()).hide(); $('.tab_content'+$(this).attr('href')).show(); } }); diff --git a/setup/test/tests/test.validation.php b/setup/test/tests/test.validation.php index 27e61af8438a4fc8a75bcb3003013af6ab56801d..bce9fe85505d08e41251fc127e230bba9f775394 100644 --- a/setup/test/tests/test.validation.php +++ b/setup/test/tests/test.validation.php @@ -37,6 +37,9 @@ class TestValidation extends Test { // Illegal or unsupported $this->assert(!Validator::is_email('jared r@domain.tld')); $this->assert(!Validator::is_email('jared')); + $this->assert(!Validator::is_email('jared@')); + $this->assert(!Validator::is_email('@domain.tld')); + $this->assert(!Validator::is_email('@domain.tld, @domain2.tld')); // Odd cases, but legal $this->assert(Validator::is_email('jared@host'));