update record links; initial holds placement
authorBill Erickson <berick@esilibrary.com>
Thu, 17 Jul 2014 14:46:34 +0000 (10:46 -0400)
committerBill Erickson <berick@esilibrary.com>
Thu, 17 Jul 2014 14:46:34 +0000 (10:46 -0400)
Signed-off-by: Bill Erickson <berick@esilibrary.com>
14 files changed:
Open-ILS/src/templates/staff/admin/workstation/t_print_templates.tt2
Open-ILS/src/templates/staff/cat/bucket/record/t_view.tt2
Open-ILS/src/templates/staff/cat/catalog/t_catalog.tt2
Open-ILS/src/templates/staff/cat/catalog/t_holds.tt2
Open-ILS/src/templates/staff/cat/item/t_list.tt2
Open-ILS/src/templates/staff/cat/share/t_record_summary.tt2
Open-ILS/src/templates/staff/circ/checkin/t_checkin_table.tt2
Open-ILS/src/templates/staff/circ/holds/t_pull_list.tt2
Open-ILS/src/templates/staff/circ/holds/t_shelf_list.tt2
Open-ILS/src/templates/staff/circ/patron/t_bill_history_payments.tt2
Open-ILS/src/templates/staff/circ/patron/t_bill_history_xacts.tt2
Open-ILS/src/templates/staff/circ/patron/t_bills_list.tt2
Open-ILS/web/js/ui/default/opac/staff.js
Open-ILS/web/js/ui/default/staff/cat/catalog/app.js

index 0502538..cbc79f8 100644 (file)
@@ -18,6 +18,7 @@
       <option value="checkout">[% l('Checkout') %]</option>
       <option value="hold_transit_slip">[% l('Hold Transit Slip') %]</option>
       <option value="hold_shelf_slip">[% l('Hold Shelf Slip') %]</option>
+      <option value="holds_for_bibs">[% l('Holds for Bib Record') %]</option>
       <option value="holds_for_patron">[% l('Holds for Patron') %]</option>
       <option value="patron_address">[% l('Patron Address') %]</option>
       <option value="patron_note">[% l('Patron Note') %]</option>
index 6943d59..39c866f 100644 (file)
@@ -19,7 +19,7 @@
   <eg-grid-field path="id" required hidden></eg-grid-field>
 
   <eg-grid-field label="[% l('Title') %]" path="title">
-    <a target="_self" href="[% ctx.base_path %]/opac/record/{{item.id}}">
+    <a target="_self" href="[% ctx.base_path %]/staff/cat/catalog/record/{{item.id}}">
       {{item.title}}
     </a>
   </eg-grid-field>
index 398335e..47f1c6f 100644 (file)
@@ -2,10 +2,13 @@
 <div class="row pad-vert">
   <div class="col-md-9">
     <div class="alert alert-info alert-less-pad strong-text-2">
-      <span>[% l('Catalog') %]</span>
+      <span ng-if="record_tab == 'catalog'">[% l('Catalog') %]</span>
+      <span ng-if="record_tab == 'marc_html'">[% l('MARC HTML') %]</span>
+      <span ng-if="record_tab == 'holds'">[% l('Holds for Record') %]</span>
     </div>
   </div>
   <div class="col-md-3">
+    <!-- actions for this record menu -->
     <div class="btn-group pull-right" dropdown>
       <button type="button" 
           class="btn btn-default dropdown-toggle" ng-disabled="!record_id">
         <span class="caret"></span>
       </button>
       <ul class="dropdown-menu dropdown-menu-right" role="menu">
-        <li><a href dropdown-toggle ng-click="set_record_tab('catalog')">[% l('OPAC View') %]</a></li>
-        <li><a href dropdown-toggle ng-click="set_record_tab('record_html')">[% l('MARC View') %]</a></li>
+        <li><a href dropdown-toggle ng-click="set_record_tab('catalog')">
+            [% l('OPAC View') %]</a></li>
+        <li><a href dropdown-toggle ng-click="set_record_tab('marc_html')">
+            [% l('MARC View') %]</a></li>
         <li class="divider"></li>
-        <li><a href dropdown-toggle ng-click="set_record_tab('holds')">[% l('View Holds') %]</a></li>
-        <li class="disabled"><a href dropdown-toggle>[% l('Mark as Title Hold Transfer Destination') %]</a></li>
-        <li class="disabled"><a href dropdown-toggle>[% l('Transfer All Title Holds') %]</a></li>
+        <li><a href dropdown-toggle ng-click="set_record_tab('holds')">
+            [% l('View Holds') %]</a></li>
+        <li><a href dropdown-toggle ng-click="mark_hold_transfer_dest()">
+            [% l('Mark as Title Hold Transfer Destination') %]</a></li>
+        <li><a href dropdown-toggle ng-click="transfer_holds_to_marked()">
+            [% l('Transfer All Title Holds') %]</a></li>
       </ul>
     </div>
   </div>
@@ -30,7 +38,7 @@
     <eg-embed-frame url="catalog_url" handlers="handlers" onchange="handle_page"></eg-embed-frame>
   </div>
   <!-- ng-if the remaining tabs so they can be instantiated on demand -->
-  <div ng-if="record_tab == 'record_html'">
+  <div ng-if="record_tab == 'marc_html'">
     <eg-record-html record-id="record_id"></eg-record-html>
   </div>
   <div ng-if="record_tab == 'holds'">
index a420265..62af918 100644 (file)
@@ -66,7 +66,7 @@
     <eg-grid-field label="[% l('Pickup Library') %]" path='hold.pickup_lib.shortname'></eg-grid-field>
 
     <eg-grid-field label="[% l('Title') %]" path='mvr.title'>
-      <a href="[% ctx.base_path %]/opac/record/{{item.mvr.doc_id()}}">
+      <a target="_self" href="[% ctx.base_path %]/staff/cat/catalog/record/{{item.mvr.doc_id()}}">
         {{item.mvr.title()}}
       </a>
     </eg-grid-field>
index 8c296a8..cbd2e76 100644 (file)
@@ -13,7 +13,7 @@
 
   <eg-grid-field label="[% l('Title') %]"       
     path="call_number.record.simple_record.title" visible>
-    <a href="[% ctx.base_path %]/opac/record/{{item['call_number.record.id']}}">
+    <a target="_self" href="[% ctx.base_path %]/staff/cat/catalog/record/{{item['call_number.record.id']}}">
       {{item['call_number.record.simple_record.title']}}
     </a>
   </eg-grid-field>
index e9d4fc3..3de97e5 100644 (file)
@@ -4,8 +4,8 @@
   <div class="flex-row">
     <div class="flex-cell strong-text">[% l('Title:') %]</div>
     <div class="flex-cell flex-2">
-      <a target="_blank
-        href="[% ctx.base_path %]/opac/record/{{record.id()}}">
+      <a target="_self
+        href="[% ctx.base_path %]/staff/cat/catalog/record/{{record.id()}}">
         {{record.simple_record().title()}}
       </a>
     </div>
index 9156842..1ee0c09 100644 (file)
@@ -63,7 +63,7 @@
     path='circ.xact_start'></eg-grid-field>
 
   <eg-grid-field label="[% l('Title') %]" path="title">
-    <a target="_self" href="[% ctx.base_path %]/opac/record/{{record.doc_id()}}">
+    <a target="_self" href="[% ctx.base_path %]/staff/cat/catalog/record/{{record.doc_id()}}">
       {{item.title}}
     </a>
   </eg-grid-field>
index 312ad68..8cea1d1 100644 (file)
@@ -70,7 +70,7 @@
   <eg-grid-field label="[% l('Call Number') %]" path='volume.label'></eg-grid-field>
 
   <eg-grid-field label="[% l('Title') %]" path='mvr.title'>
-    <a href="[% ctx.base_path %]/opac/record/{{item.mvr.doc_id()}}">
+    <a target="_self" href="[% ctx.base_path %]/staff/cat/catalog/record/{{item.mvr.doc_id()}}">
       {{item.mvr.title()}}
     </a>
   </eg-grid-field>
index 33e5320..3700df9 100644 (file)
@@ -67,7 +67,7 @@
   <eg-grid-field label="[% l('Post-Clear') %]" path='post_clear'></eg-grid-field>
 
   <eg-grid-field label="[% l('Title') %]" path='mvr.title'>
-    <a href="[% ctx.base_path %]/opac/record/{{item.mvr.doc_id()}}">
+    <a target="_self" href="[% ctx.base_path %]/staff/cat/catalog/record/{{item.mvr.doc_id()}}">
       {{item.mvr.title()}}
     </a>
   </eg-grid-field>
index 4c4a020..c4fa9d3 100644 (file)
@@ -19,7 +19,7 @@
 
     <eg-grid-field label="[% l('Title') %]" name="title" 
       path="xact.circulation.target_copy.call_number.record.simple_record.title">
-      <a href="[% ctx.base_path %]/opac/record/{{item.record_id}}">{{item.title}}</a>
+      <a target="_self" href="[% ctx.base_path %]/staff/cat/catalog/record/{{item.record_id}}">{{item.title}}</a>
     </eg-grid-field>
 
     <!-- needed for bib link -->
index 7c77e5c..70f48b6 100644 (file)
@@ -21,7 +21,7 @@
 
     <eg-grid-field label="[% l('Title') %]" name="title" 
       path="circulation.target_copy.call_number.record.simple_record.title">
-      <a href="[% ctx.base_path %]/opac/record/{{item.record_id}}">{{item.title}}</a>
+      <a target="_self" href="[% ctx.base_path %]/staff/cat/catalog/record/{{item.record_id}}">{{item.title}}</a>
     </eg-grid-field>
 
     <!-- needed for bib link -->
index e1058f6..c98d041 100644 (file)
@@ -56,7 +56,7 @@
 
   <eg-grid-field label="[% l('Title') %]" name="title"
     path='circulation.target_copy.call_number.record.simple_record.title'>
-    <a href="[% ctx.base_path %]/opac/record/{{item.record_id}}">{{item.title}}</a>
+    <a target="_self" href="[% ctx.base_path %]/staff/cat/catalog/record/{{item.record_id}}">{{item.title}}</a>
   </eg-grid-field>
   <!-- fetch the record ID so we can link to it.  hide it by default -->
   <eg-grid-field path="circulation.target_copy.call_number.record.id" 
index adbfd97..d281ca2 100644 (file)
@@ -41,89 +41,116 @@ function staff_hold_usr_barcode_changed(isload) {
         return;
     }
 
-    if(typeof xulG != 'undefined' && xulG.get_barcode_and_settings) {
-        var cur_hold_barcode = undefined;
-        var barcode = isload;
-        if(!barcode || barcode === true) barcode = document.getElementById('staff_barcode').value;
-        var only_settings = true;
-        if(!document.getElementById('hold_usr_is_requestor').checked) {
-            if(!isload) {
-                barcode = document.getElementById('hold_usr_input').value;
-                only_settings = false;
-            }
-            if(barcode && barcode != '' && !document.getElementById('hold_usr_is_requestor_not').checked)
-                document.getElementById('hold_usr_is_requestor_not').checked = 'checked';
-        }
-        if(barcode == undefined || barcode == '') {
-            document.getElementById('patron_name').innerHTML = '';
-            // No submitting on empty barcode, but empty barcode doesn't really count as "not found" either
-            document.getElementById('place_hold_submit').disabled = true;
-            document.getElementById("patron_usr_barcode_not_found").style.display = 'none';
-            cur_hold_barcode = null;
-            return;
+    if (!window.xulG) return;
+
+    var cur_hold_barcode = undefined;
+    var barcode = isload;
+    if(!barcode || barcode === true) barcode = document.getElementById('staff_barcode').value;
+    var only_settings = true;
+    if(!document.getElementById('hold_usr_is_requestor').checked) {
+        if(!isload) {
+            barcode = document.getElementById('hold_usr_input').value;
+            only_settings = false;
         }
-        if(barcode == cur_hold_barcode)
-            return;
-        // No submitting until we think the barcode is valid
+        if(barcode && barcode != '' && !document.getElementById('hold_usr_is_requestor_not').checked)
+            document.getElementById('hold_usr_is_requestor_not').checked = 'checked';
+    }
+    if(barcode == undefined || barcode == '') {
+        document.getElementById('patron_name').innerHTML = '';
+        // No submitting on empty barcode, but empty barcode doesn't really count as "not found" either
         document.getElementById('place_hold_submit').disabled = true;
+        document.getElementById("patron_usr_barcode_not_found").style.display = 'none';
+        cur_hold_barcode = null;
+        return;
+    }
+    if(barcode == cur_hold_barcode)
+        return;
+    // No submitting until we think the barcode is valid
+    document.getElementById('place_hold_submit').disabled = true;
+
+    if (window.IAMBROWSER) {
+        // Browser client operates asynchronously
+        if (!xulG.get_barcode_and_settings_async) return;
+        xulG.get_barcode_and_settings_async(barcode, only_settings)
+        .then(
+            function(load_info) { // load succeeded
+                staff_hold_usr_barcode_changed2(
+                    isload, only_settings, barcode, cur_hold_barcode, load_info);
+            },
+            function() { 
+                // load failed (rejected).  Call staff_hold_usr_barcode_changed2
+                // anyway, since it handles clearing the form
+                staff_hold_usr_barcode_changed2(
+                    isload, only_settings, barcode, cur_hold_barcode, false);
+            }
+        )
+    } else {
+        // XUL version is synchronous
+        if (!xulG.get_barcode_and_settings) return;
         var load_info = xulG.get_barcode_and_settings(window, barcode, only_settings);
-        if(load_info == false || load_info == undefined) {
-            document.getElementById('patron_name').innerHTML = '';
-            document.getElementById("patron_usr_barcode_not_found").style.display = '';
-            cur_hold_barcode = null;
-            return;
-        }
-        cur_hold_barcode = load_info.barcode;
-        if(!only_settings || (isload && isload !== true)) document.getElementById('hold_usr_input').value = load_info.barcode; // Safe at this point as we already set cur_hold_barcode
-        if(load_info.settings['opac.default_pickup_location'])
-            document.getElementById('pickup_lib').value = load_info.settings['opac.default_pickup_location'];
-        if(!load_info.settings['opac.default_phone']) load_info.settings['opac.default_phone'] = '';
-        if(!load_info.settings['opac.default_sms_notify']) load_info.settings['opac.default_sms_notify'] = '';
-        if(!load_info.settings['opac.default_sms_carrier']) load_info.settings['opac.default_sms_carrier'] = '';
-        if(load_info.settings['opac.hold_notify'] || load_info.settings['opac.hold_notify'] === '') {
-            var email = load_info.settings['opac.hold_notify'].indexOf('email') > -1;
-            var phone = load_info.settings['opac.hold_notify'].indexOf('phone') > -1;
-            var sms = load_info.settings['opac.hold_notify'].indexOf('sms') > -1;
-            var update_elements = document.getElementsByName('email_notify');
-            for(var i in update_elements) update_elements[i].checked = (email ? 'checked' : '');
-            update_elements = document.getElementsByName('phone_notify_checkbox');
-            for(var i in update_elements) update_elements[i].checked = (phone ? 'checked' : '');
-            update_elements = document.getElementsByName('sms_notify_checkbox');
-            for(var i in update_elements) update_elements[i].checked = (sms ? 'checked' : '');
-        }
-        update_elements = document.getElementsByName('phone_notify');
-        for(var i in update_elements) update_elements[i].value = load_info.settings['opac.default_phone'];
-        update_elements = document.getElementsByName('sms_notify');
-        for(var i in update_elements) update_elements[i].value = load_info.settings['opac.default_sms_notify'];
-        update_elements = document.getElementsByName('sms_carrier');
-        for(var i in update_elements) update_elements[i].value = load_info.settings['opac.default_sms_carrier'];
-        update_elements = document.getElementsByName('email_notify');
-        for(var i in update_elements) {
-            update_elements[i].disabled = (load_info.user_email ? false : true);
-            if(update_elements[i].disabled) update_elements[i].checked = false;
-        }
-        update_elements = document.getElementsByName('email_address');
-        for(var i in update_elements) update_elements[i].textContent = load_info.user_email;
-        if(!document.getElementById('hold_usr_is_requestor').checked && document.getElementById('hold_usr_input').value) {
-            document.getElementById('patron_name').innerHTML = load_info.patron_name;
-            document.getElementById("patron_usr_barcode_not_found").style.display = 'none';
-        }
-        // Ok, now we can allow submitting again, unless this is a "true" load, in which case we likely have a blank barcode box active
-
-        // update the advanced hold options link to propagate the patron
-        // barcode if clicked.  This is needed when the patron barcode
-        // is manually entered (i.e. the staff client does not provide one).
-        var adv_link = document.getElementById('advanced_hold_link');
-        if (adv_link) { // not present on MR hold pages
-            var href = adv_link.getAttribute('href').replace(
-                /;usr_barcode=[^;\&]+|$/, 
-                ';usr_barcode=' + encodeURIComponent(cur_hold_barcode));
-            adv_link.setAttribute('href', href);
-        }
+        staff_hold_usr_barcode_changed2(isload, only_settings, barcode, cur_hold_barcode, load_info);
+    }
+}
+
+function staff_hold_usr_barcode_changed2(
+    isload, only_settings, barcode, cur_hold_barcode, load_info) {
+
+    if(load_info == false || load_info == undefined) {
+        document.getElementById('patron_name').innerHTML = '';
+        document.getElementById("patron_usr_barcode_not_found").style.display = '';
+        cur_hold_barcode = null;
+        return;
+    }
+    cur_hold_barcode = load_info.barcode;
+    if(!only_settings || (isload && isload !== true)) document.getElementById('hold_usr_input').value = load_info.barcode; // Safe at this point as we already set cur_hold_barcode
+    if(load_info.settings['opac.default_pickup_location'])
+        document.getElementById('pickup_lib').value = load_info.settings['opac.default_pickup_location'];
+    if(!load_info.settings['opac.default_phone']) load_info.settings['opac.default_phone'] = '';
+    if(!load_info.settings['opac.default_sms_notify']) load_info.settings['opac.default_sms_notify'] = '';
+    if(!load_info.settings['opac.default_sms_carrier']) load_info.settings['opac.default_sms_carrier'] = '';
+    if(load_info.settings['opac.hold_notify'] || load_info.settings['opac.hold_notify'] === '') {
+        var email = load_info.settings['opac.hold_notify'].indexOf('email') > -1;
+        var phone = load_info.settings['opac.hold_notify'].indexOf('phone') > -1;
+        var sms = load_info.settings['opac.hold_notify'].indexOf('sms') > -1;
+        var update_elements = document.getElementsByName('email_notify');
+        for(var i in update_elements) update_elements[i].checked = (email ? 'checked' : '');
+        update_elements = document.getElementsByName('phone_notify_checkbox');
+        for(var i in update_elements) update_elements[i].checked = (phone ? 'checked' : '');
+        update_elements = document.getElementsByName('sms_notify_checkbox');
+        for(var i in update_elements) update_elements[i].checked = (sms ? 'checked' : '');
+    }
+    update_elements = document.getElementsByName('phone_notify');
+    for(var i in update_elements) update_elements[i].value = load_info.settings['opac.default_phone'];
+    update_elements = document.getElementsByName('sms_notify');
+    for(var i in update_elements) update_elements[i].value = load_info.settings['opac.default_sms_notify'];
+    update_elements = document.getElementsByName('sms_carrier');
+    for(var i in update_elements) update_elements[i].value = load_info.settings['opac.default_sms_carrier'];
+    update_elements = document.getElementsByName('email_notify');
+    for(var i in update_elements) {
+        update_elements[i].disabled = (load_info.user_email ? false : true);
+        if(update_elements[i].disabled) update_elements[i].checked = false;
+    }
+    update_elements = document.getElementsByName('email_address');
+    for(var i in update_elements) update_elements[i].textContent = load_info.user_email;
+    if(!document.getElementById('hold_usr_is_requestor').checked && document.getElementById('hold_usr_input').value) {
+        document.getElementById('patron_name').innerHTML = load_info.patron_name;
+        document.getElementById("patron_usr_barcode_not_found").style.display = 'none';
+    }
+    // Ok, now we can allow submitting again, unless this is a "true" load, in which case we likely have a blank barcode box active
 
-        if (isload !== true)
-            document.getElementById('place_hold_submit').disabled = false;
+    // update the advanced hold options link to propagate the patron
+    // barcode if clicked.  This is needed when the patron barcode
+    // is manually entered (i.e. the staff client does not provide one).
+    var adv_link = document.getElementById('advanced_hold_link');
+    if (adv_link) { // not present on MR hold pages
+        var href = adv_link.getAttribute('href').replace(
+            /;usr_barcode=[^;\&]+|$/, 
+            ';usr_barcode=' + encodeURIComponent(cur_hold_barcode));
+        adv_link.setAttribute('href', href);
     }
+
+    if (isload !== true)
+        document.getElementById('place_hold_submit').disabled = false;
 }
 window.onload = function() {
     // record details page events
index b50d4f9..4a58a23 100644 (file)
@@ -29,6 +29,13 @@ angular.module('egCatalogApp', ['ui.bootstrap','ngRoute','egCoreMod','egGridMod'
         resolve : resolver
     });
 
+    // create some catalog page-specific mappings
+    $routeProvider.when('/cat/catalog/record/:record_id/:record_tab', {
+        templateUrl: './cat/catalog/t_catalog',
+        controller: 'CatalogCtrl',
+        resolve : resolver
+    });
+
     $routeProvider.otherwise({redirectTo : '/cat/catalog/index'});
 })
 
@@ -41,20 +48,17 @@ angular.module('egCatalogApp', ['ui.bootstrap','ngRoute','egCoreMod','egGridMod'
 function($scope , $routeParams , $location , $q , egCore , egHolds, 
          egGridDataProvider , egHoldGridActions) {
 
-    // TODO: is start path configurable in the xul client?
-    var url = $location.absUrl().replace(/\/staff.*/, '/opac/advanced');
+    // set record ID on page load if available...
+    $scope.record_id = $routeParams.record_id;
 
-    if ($routeParams.record_id) {
-        $scope.record_id = $routeParams.record_id;
-        url = url.replace(/advanced/, '/record/' + $scope.record_id);
-    }
+    // also set it when the iframe changes to a new record
+    $scope.handle_page = function(url) {
 
-    // pass the reg URL into the scope, thus into the 
-    $scope.catalog_url = url;
-    // default to catalog view of the record page
-    $scope.record_tab = 'catalog';
+        if (!url || url == 'about:blank') {
+            // nothing loaded.  If we already have a record ID, leave it.
+            return;
+        }
 
-    $scope.handle_page = function(url) {
         var match = url.match(/\/+opac\/+record\/+(\d+)/);
         if (match) {
             $scope.record_id = match[1];
@@ -67,10 +71,86 @@ function($scope , $routeParams , $location , $q , egCore , egHolds,
         }
     }
 
-    // xulG handlers
-    $scope.handlers = {};
+    // xulG catalog handlers
+    $scope.handlers = {
+        get_barcode_and_settings_async : function(barcode, only_settings) {
+            if (!barcode) return $q.reject();
+            var deferred = $q.defer();
+
+            var barcode_promise = $q.when(barcode);
+            if (!only_settings) {
+
+                // first verify / locate the barcode
+                barcode_promise = egCore.net.request(
+                    'open-ils.actor',
+                    'open-ils.actor.get_barcodes',
+                    egCore.auth.token(), 
+                    egCore.auth.user().ws_ou(), 'actor', barcode
+                ).then(function(resp) {
+
+                    if (!resp || egCore.evt.parse(resp) || !resp.length) {
+                        console.error('user not found: ' + barcode);
+                        deferred.reject();
+                        return null;
+                    } 
+
+                    resp = resp[0];
+                    return barcode = resp.barcode;
+                });
+            }
+
+            barcode_promise.then(function(barcode) {
+                if (!barcode) return;
+
+                return egCore.net.request(
+                    'open-ils.actor',
+                    'open-ils.actor.user.fleshed.retrieve_by_barcode',
+                    egCore.auth.token(), barcode);
+
+            }).then(function(user) {
+                if (!user) return null;
+
+                if (e = egCore.evt.parse(user)) {
+                    console.error('user fetch failed : ' + e.toString());
+                    deferred.reject();
+                    return null;
+                }
+
+                // copied more or less directly from XUL menu.js
+                var settings = {};
+                for(var i = 0; i < user.settings().length; i++) {
+                    settings[user.settings()[i].name()] = 
+                        JSON2js(user.settings()[i].value());
+                }
 
-    // Holds bits -------------------------------
+                if(!settings['opac.default_phone'] && user.day_phone()) 
+                    settings['opac.default_phone'] = user.day_phone();
+                if(!settings['opac.hold_notify'] && settings['opac.hold_notify'] !== '') 
+                    settings['opac.hold_notify'] = 'email:phone';
+
+                // Taken from patron/util.js format_name
+                // FIXME: I18n
+                var patron_name = 
+                    ( user.prefix() ? user.prefix() + ' ' : '') +
+                    user.family_name() + ', ' +
+                    user.first_given_name() + ' ' +
+                    ( user.second_given_name() ? user.second_given_name() + ' ' : '' ) +
+                    ( user.suffix() ? user.suffix() : '');
+
+                deferred.resolve({
+                    "barcode": barcode, 
+                    "settings" : settings, 
+                    "user_email" : user.email(), 
+                    "patron_name" : patron_name
+                });
+            });
+
+            return deferred.promise;
+        }
+    }
+
+    // ------------------------------------------------------------------
+    // Holds 
     var provider = egGridDataProvider.instance({});
     $scope.hold_grid_data_provider = provider;
     $scope.grid_actions = egHoldGridActions;
@@ -81,8 +161,6 @@ function($scope , $routeParams , $location , $q , egCore , egHolds,
         var ids = hold_ids.slice(offset, offset + count);
         return egHolds.fetch_holds(ids).then(null, null,
             function(hold_data) { 
-                //patronSvc.holds.push(hold_data);
-                console.log('returning hold ' + hold_data.mvr.title());
                 return hold_data;
             }
         );
@@ -129,17 +207,6 @@ function($scope , $routeParams , $location , $q , egCore , egHolds,
         provider.refresh();
     }
 
-    $scope.set_record_tab = function(tab) {
-        $scope.record_tab = tab;
-
-        if (tab == 'holds') {
-            $scope.detail_hold_record_id = $scope.record_id; 
-
-            // refresh the holds grid
-            provider.refresh();
-        }
-    }
-
     $scope.print_holds = function() {
         var holds = [];
         angular.forEach($scope.hold_grid_controls.allItems(), function(item) {
@@ -162,6 +229,56 @@ function($scope , $routeParams , $location , $q , egCore , egHolds,
         });
     }
 
+    $scope.mark_hold_transfer_dest = function() {
+        egCore.hatch.setLocalItem(
+            'eg.circ.hold.title_transfer_target', $scope.record_id);
+    }
+
+    // UI presents this option as "all holds"
+    $scope.transfer_holds_to_marked = function() {
+        var hold_ids = $scope.hold_grid_controls.allItems().map(
+            function(hold_data) {return hold_data.hold.id()});
+        egHolds.transfer_to_marked_title(hold_ids);
+    }
+
+    // ------------------------------------------------------------------
+    // Initialize the selected tab
+
+    function init_cat_url() {
+        // Set the initial catalog URL.  This only happens once.
+        // The URL is otherwise generated through user navigation.
+        if ($scope.catalog_url) return; 
+
+        var url = $location.absUrl().replace(/\/staff.*/, '/opac/advanced');
+
+        // A record ID in the path indicates a request for the record-
+        // specific page.
+        if ($routeParams.record_id) {
+            url = url.replace(/advanced/, '/record/' + $scope.record_id);
+        }
+
+        $scope.catalog_url = url;
+    }
+
+    $scope.set_record_tab = function(tab) {
+        $scope.record_tab = tab;
+
+        switch(tab) {
+
+            case 'catalog':
+                init_cat_url();
+                break;
+
+            case 'holds':
+                $scope.detail_hold_record_id = $scope.record_id; 
+                // refresh the holds grid
+                provider.refresh();
+                break;
+        }
+    }
+
+    var tab = $routeParams.record_tab || 'catalog';
+    $scope.set_record_tab(tab);
 
 }])