diff --git a/include/ajax.admin.php b/include/ajax.admin.php
index 0e2f18f67bb430792874aa22a798dab5e7fad7fe..c9f82a5c40e1dd8ed6dd19df1f052144952e3cf2 100644
--- a/include/ajax.admin.php
+++ b/include/ajax.admin.php
@@ -1,6 +1,7 @@
 <?php
 
 require_once(INCLUDE_DIR . 'class.dept.php');
+require_once(INCLUDE_DIR . 'class.role.php');
 require_once(INCLUDE_DIR . 'class.team.php');
 
 class AdminAjaxAPI extends AjaxController {
@@ -98,4 +99,61 @@ class AdminAjaxAPI extends AjaxController {
 
         include STAFFINC_DIR . 'templates/quick-add.tmpl.php';
     }
+
+    /**
+     * Ajax: GET /admin/add/role
+     *
+     * Uses a dialog to add a new role
+     *
+     * Returns:
+     * 200 - HTML form for addition
+     * 201 - {id: <id>, name: <name>}
+     *
+     * Throws:
+     * 403 - Not logged in
+     * 403 - Not an adminitrator
+     */
+    function addRole() {
+        global $ost, $thisstaff;
+
+        if (!$thisstaff)
+            Http::response(403, 'Agent login required');
+        if (!$thisstaff->isAdmin())
+            Http::response(403, 'Access denied');
+
+        $form = new RoleQuickAddForm($_POST);
+
+        if ($_POST && $form->isValid()) {
+            $role = Role::create();
+            $errors = array();
+            $vars = $form->getClean();
+            if ($role->update($vars, $errors)) {
+                Http::response(201, $this->encode(array(
+                    'id' => $role->getId(),
+                    'name' => $role->name,
+                ), 'application/json'));
+            }
+            foreach ($errors as $name=>$desc)
+                if ($F = $form->getField($name))
+                    $F->addError($desc);
+        }
+
+        $title = __("Add New Role");
+        $path = ltrim($ost->get_path_info(), '/');
+
+        include STAFFINC_DIR . 'templates/quick-add-role.tmpl.php';
+    }
+
+    function getRolePerms($id) {
+        global $ost, $thisstaff;
+
+        if (!$thisstaff)
+            Http::response(403, 'Agent login required');
+        if (!$thisstaff->isAdmin())
+            Http::response(403, 'Access denied');
+        if (!($role = Role::lookup($id)))
+            Http::response(404, 'No such role');
+
+        return $this->encode($role->getPermissionInfo());
+    }
 }
diff --git a/include/class.forms.php b/include/class.forms.php
index c0fbd9040f3a296af2d3c0e3341e2e810f887543..20fed8cd6dcb56fe5e0027dd5e546d5d9eb21192 100644
--- a/include/class.forms.php
+++ b/include/class.forms.php
@@ -2932,9 +2932,10 @@ class ChoicesWidget extends Widget {
         <select name="<?php echo $this->name; ?>[]"
             <?php echo implode(' ', array_filter(array($classes))); ?>
             id="<?php echo $this->id; ?>"
-            <?php foreach ($config['data'] as $D=>$V) {
-              echo ' data-'.$D.'="'.Format::htmlchars($V).'"';
-            } ?>
+            <?php if (isset($config['data']))
+              foreach ($config['data'] as $D=>$V)
+                echo ' data-'.$D.'="'.Format::htmlchars($V).'"';
+            ?>
             data-placeholder="<?php echo $prompt; ?>"
             <?php if ($config['multiselect'])
                 echo ' multiple="multiple"'; ?>>
@@ -3026,6 +3027,115 @@ class ChoicesWidget extends Widget {
     }
 }
 
+/**
+ * A widget for the ChoiceField which will render a list of radio boxes or
+ * checkboxes depending on the value of $config['multiple']. Complex choices
+ * are also supported and will be rendered as divs.
+ */
+class BoxChoicesWidget extends Widget {
+    function render($options=array()) {
+        $this->emitChoices($this->field->getChoices());
+    }
+
+    function emitChoices($choices) {
+      if (!isset($this->value))
+          $this->value = $this->field->get('default');
+      $config = $this->field->getConfiguration();
+      $type = $config['multiple'] ? 'checkbox' : 'radio';
+      if (isset($config['classes']))
+          $classes = 'class="'.$config['classes'].'"';
+
+      $i=0;
+      foreach ($choices as $k => $v) {
+          if (is_array($v)) {
+              $this->renderSectionBreak($k);
+              $this->emitChoices($v);
+              continue;
+          }
+          $id = sprintf("%s-%s", $this->id, $i++);
+?>
+        <label <?php echo $classes; ?>
+          for="<?php echo $id; ?>" style="display:block;">
+        <input id="<?php echo $id; ?>" style="vertical-align:top;"
+            type="<?php echo $type; ?>" name="<?php echo $this->name; ?>[]" <?php
+            if ($this->value[$k]) echo 'checked="checked"'; ?> value="<?php
+            echo Format::htmlchars($k); ?>"/>
+        <?php
+        if ($v) { ?>
+            <em style="display:inline-block;vertical-align:middle;width:90%;width:calc(100% - 25px);"><?php
+            echo Format::viewableImages($v); ?></em>
+<?php     } ?>
+        </label>
+<?php   }
+    }
+
+    function renderSectionBreak($label) { ?>
+        <div><?php echo Format::htmlchars($label); ?></div>
+<?php
+    }
+
+    function getValue() {
+        $data = $this->field->getSource();
+        if (count($data)) {
+            if (!isset($data[$this->name]))
+                return array();
+            return $this->collectValues($data[$this->name], $this->field->getChoices());
+        }
+        return parent::getValue();
+    }
+
+    function collectValues($data, $choices) {
+        $value = array();
+        foreach ($choices as $k => $v) {
+            if (is_array($v))
+                $value = array_merge($value, $this->collectValues($data, $v));
+            elseif (@in_array($k, $data))
+                $value[$k] = $v;
+        }
+        return $value;
+    }
+}
+
+/**
+ * An extension to the BoxChoicesWidget which will render complex choices in
+ * tabs.
+ */
+class TabbedBoxChoicesWidget extends BoxChoicesWidget {
+    function render($options=array()) {
+        $tabs = array();
+        foreach ($this->field->getChoices() as $label=>$group) {
+            if (is_array($group)) {
+                $tabs[$label] = $group;
+            }
+            else {
+                $this->emitChoices(array($label=>$group));
+            }
+        }
+        if ($tabs) {
+            ?>
+            <div>
+            <ul class="alt tabs">
+<?php       $i = 0;
+            foreach ($tabs as $label => $group) {
+                $active = $i++ == 0; ?>
+                <li <?php if ($active) echo 'class="active"';
+                  ?>><a href="#<?php echo sprintf('%s-%s', $this->name, Format::slugify($label));
+                  ?>"><?php echo Format::htmlchars($label); ?></a></li>
+<?php       } ?>
+            </ul>
+<?php       $i = 0;
+            foreach ($tabs as $label => $group) {
+                $first = $i++ == 0; ?>
+                <div class="tab_content <?php if (!$first) echo 'hidden'; ?>" id="<?php
+                  echo sprintf('%s-%s', $this->name, Format::slugify($label));?>">
+<?php           $this->emitChoices($group); ?>
+                </div>
+<?php       } ?>
+            </div>
+<?php   }
+    }
+}
+
 class CheckboxWidget extends Widget {
     function __construct($field) {
         parent::__construct($field);
diff --git a/include/class.orm.php b/include/class.orm.php
index 6fbdf0ae45b12fdc5b7bd4dd0d787f71b3f58893..5fc2362077d0dc6163ccf9275f6beaf1ba2baf47 100644
--- a/include/class.orm.php
+++ b/include/class.orm.php
@@ -2167,14 +2167,14 @@ class MySqlCompiler extends SqlCompiler {
      * (string) token to be placed into the compiled SQL statement. This
      * is a colon followed by a number
      */
-    function input($what, $slot=false) {
+    function input($what, $slot=false, $model=false) {
         if ($what instanceof QuerySet) {
             $q = $what->getQuery(array('nosort'=>true));
             $this->params = array_merge($this->params, $q->params);
             return '('.$q->sql.')';
         }
         elseif ($what instanceof SqlFunction) {
-            return $what->toSql($this);
+            return $what->toSql($this, $model);
         }
         elseif (!isset($what)) {
             return 'NULL';
@@ -2462,7 +2462,7 @@ class MySqlCompiler extends SqlCompiler {
         $table = $model::getMeta('table');
         $set = array();
         foreach ($what as $field=>$value)
-            $set[] = sprintf('%s = %s', $this->quote($field), $this->input($value));
+            $set[] = sprintf('%s = %s', $this->quote($field), $this->input($value, false, $model));
         $set = implode(', ', $set);
         list($where, $having) = $this->getWhereHavingClause($queryset);
         $joins = $this->getJoins($queryset);
diff --git a/include/class.role.php b/include/class.role.php
index 5f73d8d5684b23584d7d2a949424a3cc049b1191..6e9c42a01870f832f4390c9b67b0dbd5e036a3fd 100644
--- a/include/class.role.php
+++ b/include/class.role.php
@@ -185,7 +185,7 @@ class Role extends RoleModel {
             return false;
 
         // Remove dept access entries
-        GroupDeptAccess::objects()
+        StaffDeptAccess::objects()
             ->filter(array('role_id'=>$this->getId()))
             ->update(array('role_id' => 0));
 
@@ -321,4 +321,57 @@ class RolePermission {
         }
     }
 }
-?>
+
+class RoleQuickAddForm
+extends AbstractForm {
+    function buildFields() {
+        $permissions = array();
+        foreach (RolePermission::allPermissions() as $g => $perms) {
+            foreach ($perms as $k => $v) {
+                if ($v['primary'])
+                    continue;
+                $permissions[$g][$k] = "{$v['title']} — {$v['desc']}";
+            }
+        }
+        return array(
+            'name' => new TextboxField(array(
+                'required' => true,
+                'configuration' => array(
+                    'placeholder' => __('Name'),
+                    'classes' => 'span12',
+                    'autofocus' => true,
+                    'length' => 128,
+                ),
+            )),
+            'clone' => new ChoiceField(array(
+                'default' => 0,
+                'choices' => array_merge(
+                    array(0 => '— '.__('Clone an existing role').' —'),
+                    Role::getRoles()
+                ),
+                'configuration' => array(
+                    'classes' => 'span12',
+                ),
+            )),
+            'perms' => new ChoiceField(array(
+                'choices' => $permissions,
+                'widget' => 'TabbedBoxChoicesWidget',
+                'configuration' => array(
+                    'multiple' => true,
+                    'classes' => 'vertical-pad',
+                ),
+            )),
+        );
+    }
+
+    function getClean() {
+        $clean = parent::getClean();
+        // Index permissions as ['ticket.edit' => 1]
+        $clean['perms'] = array_keys($clean['perms']);
+        return $clean;
+    }
+
+    function render($staff=true) {
+        return parent::render($staff, false, array('template' => 'dynamic-form-simple.tmpl.php'));
+    }
+}
diff --git a/include/staff/staff.inc.php b/include/staff/staff.inc.php
index 533eb4f935102ee2072b1d999a4f7b1f94b75fac..7ff2ac4e05bbae231670872c51a022ee1d4a6516 100644
--- a/include/staff/staff.inc.php
+++ b/include/staff/staff.inc.php
@@ -217,7 +217,7 @@ if (count($bks) > 1) {
             <div class="error"><?php echo $errors['role_id']; ?></div>
           </td>
           <td>
-            <select name="role_id">
+            <select name="role_id" data-quick-add="role">
               <option value="0">&mdash; <?php echo __('Select Role');?> &mdash;</option>
               <?php
               foreach (Role::getRoles() as $id=>$name) {
@@ -225,6 +225,7 @@ if (count($bks) > 1) {
                 echo sprintf('<option value="%d" %s>%s</option>',$id,$sel,$name);
               }
               ?>
+              <option value="0" data-quick-add>&mdash; <?php echo __('Add New');?> &mdash;</option>
             </select>
             <i class="offset help-tip icon-question-sign" href="#primary_role"></i>
           </td>
@@ -236,13 +237,14 @@ if (count($bks) > 1) {
             <input type="hidden" data-name="dept_access[]" value="" />
           </td>
           <td>
-            <select data-name="dept_access_role">
+            <select data-name="dept_access_role" data-quick-add="role">
               <option value="0">&mdash; <?php echo __('Select Role');?> &mdash;</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>&mdash; <?php echo __('Add New');?> &mdash;</option>
             </select>
             <span style="display:inline-block;width:20px"> </span>
             <input type="checkbox" data-name="dept_access_alerts" value="1" />
diff --git a/include/staff/templates/quick-add-role.tmpl.php b/include/staff/templates/quick-add-role.tmpl.php
new file mode 100644
index 0000000000000000000000000000000000000000..929107b716468dcc302faa6e7705c8215f149d2f
--- /dev/null
+++ b/include/staff/templates/quick-add-role.tmpl.php
@@ -0,0 +1,26 @@
+<?php
+include 'quick-add.tmpl.php';
+$clone = $form->getField('clone')->getWidget()->name;
+$permissions = $form->getField('perms')->getWidget()->name;
+$name = $form->getField('name')->getWidget()->name;
+?>
+<script type="text/javascript">
+  $('#_<?php echo $clone; ?>').change(function() {
+    var $this = $(this),
+        id = $this.val(),
+        form = $this.closest('form'),
+        name = $('[name="<?php echo $name; ?>"]:first', form);
+    $.ajax({
+      url: 'ajax.php/admin/role/'+id+'/perms',
+      dataType: 'json',
+      success: function(json) {
+        $('[name="<?php echo $permissions; ?>[]"]', form).prop('checked', false);
+        $.each(json, function(k, v) {
+          form.find('[value="'+k+'"]', form).prop('checked', !!v);
+        });
+        if (!name.val())
+          name.val(__('Copy of {0}').replace('{0}', $this.find(':selected').text()));
+      }
+    });
+  });
+</script>
diff --git a/include/staff/templates/quick-add.tmpl.php b/include/staff/templates/quick-add.tmpl.php
index fc5c01c66a6081c5c5cbd333194f305715f48de0..b8efa8dd8283dadefc58ede8be02c1f0cc79f134 100644
--- a/include/staff/templates/quick-add.tmpl.php
+++ b/include/staff/templates/quick-add.tmpl.php
@@ -2,6 +2,9 @@
 <b><a class="close" href="#"><i class="icon-remove-circle"></i></a></b>
 <div class="clear"></div>
 <hr/>
+<?php if (isset($errors['err'])) { ?>
+    <div id="msg_error" class="error-banner"><?php echo Format::htmlchars($errors['err']); ?></div>
+<?php } ?>
 <form method="post" action="#<?php echo $path; ?>">
   <div class="inset quick-add">
     <?php $form->render(); ?>
@@ -15,7 +18,7 @@
     </span>
     <span class="buttons pull-right">
       <input type="submit" value="<?php
-        echo $verb ?: __('Submit'); ?>" />
+        echo $verb ?: __('Create'); ?>" />
     </span>
   </p>
   <div class="clear"></div>
diff --git a/scp/ajax.php b/scp/ajax.php
index 58e153a6a921e8d74fcbc818b6dc25895e192007..72e68d429ae8cd633761be9e484fd4f1f84fc9e0 100644
--- a/scp/ajax.php
+++ b/scp/ajax.php
@@ -225,11 +225,13 @@ $dispatcher = patterns('',
         url_post('^translate/(?P<tag>\w+)$', 'updateTranslations'),
         url_get('^(?P<lang>[\w_]+)/(?P<tag>\w+)$', 'getLanguageFile')
     )),
-    url('^/admin', patterns('',
+    url('^/admin', patterns('ajax.admin.php:AdminAjaxAPI',
         url('^/quick-add', patterns('ajax.admin.php:AdminAjaxAPI',
             url('^/department$', 'addDepartment'),
-            url('^/team', 'addTeam')
-        ))
+            url('^/team$', 'addTeam'),
+            url('^/role$', 'addRole')
+        )),
+        url_get('^/role/(?P<id>\d+)/perms', 'getRolePerms')
     )),
     url('^/staff/(?P<id>\d+)', patterns('ajax.staff.php:StaffAjaxAPI',
         url('^/set-password$', 'setPassword')
diff --git a/scp/css/scp.css b/scp/css/scp.css
index 65c4b41f4fc467f5c3e782185f8c4bd4794c39ba..76f5468e4751863add285ee0267f3ce65a899930 100644
--- a/scp/css/scp.css
+++ b/scp/css/scp.css
@@ -2524,6 +2524,7 @@ form .inset {
 .form.footer {
     margin-top: 50px;
 }
+<<<<<<< HEAD
 .clearfix:after {
   visibility: hidden;
   display: block;
@@ -2532,3 +2533,6 @@ form .inset {
   clear: both;
   height: 0;
 }
+.vertical-pad {
+  margin-top: 3px;
+}
diff --git a/scp/js/scp.js b/scp/js/scp.js
index cbba7c8e21ff7777f96f5ac5877eb7a4bfce14b1..257e8f9330fe2c18409449bd768ad473a5a8859a 100644
--- a/scp/js/scp.js
+++ b/scp/js/scp.js
@@ -980,7 +980,7 @@ $(document).on('change', 'select[data-quick-add]', function() {
           $('<option>')
             .attr('value', id)
             .text(data.name)
-            .insertBefore(selected);
+            .insertBefore($('select[data-quick-add="'+type+'"] option[data-quick-add]'));
           $select.val(id);
         }
     });