Patch from Lebbeous Fogle-Weekley to wire up more of the booking functionality and...
authormiker <miker@dcc99617-32d9-48b4-a31d-7c20da2025e4>
Mon, 28 Dec 2009 21:40:02 +0000 (21:40 +0000)
committermiker <miker@dcc99617-32d9-48b4-a31d-7c20da2025e4>
Mon, 28 Dec 2009 21:40:02 +0000 (21:40 +0000)
git-svn-id: svn://svn.open-ils.org/ILS/trunk@15236 dcc99617-32d9-48b4-a31d-7c20da2025e4

19 files changed:
Open-ILS/src/perlmods/OpenILS/Application/Booking.pm
Open-ILS/src/perlmods/OpenILS/Application/Storage/Publisher/action.pm
Open-ILS/web/css/skin/default/booking.css
Open-ILS/web/js/dojo/openils/booking/nls/reservation.js
Open-ILS/web/js/ui/default/booking/reservation.js
Open-ILS/web/opac/locale/en-US/lang.dtd
Open-ILS/web/templates/default/booking/reservation.tt2
Open-ILS/xul/staff_client/chrome/content/util/functional.js
Open-ILS/xul/staff_client/server/cat/copy_browser.js
Open-ILS/xul/staff_client/server/cat/copy_browser.xul
Open-ILS/xul/staff_client/server/cat/util.js
Open-ILS/xul/staff_client/server/circ/copy_status.js
Open-ILS/xul/staff_client/server/circ/copy_status.xul
Open-ILS/xul/staff_client/server/circ/copy_status_overlay.xul
Open-ILS/xul/staff_client/server/patron/display.js
Open-ILS/xul/staff_client/server/patron/display.xul
Open-ILS/xul/staff_client/server/patron/display_horiz.xul
Open-ILS/xul/staff_client/server/patron/display_horiz_overlay.xul
Open-ILS/xul/staff_client/server/patron/display_overlay.xul

index d3b41db..436d51f 100644 (file)
@@ -521,6 +521,66 @@ by the top-level filters ('user', 'type', 'resource').
 NOTES
 );
 
+
+sub get_pull_list {
+    my ($self, $client, $auth, $range, $pickup_lib) = @_;
+
+    my $e = new_editor(xact => 1, authtoken => $auth);
+    return $e->die_event unless $e->checkauth;
+    return $e->die_event unless $e->allowed('RETRIEVE_RESERVATION_PULL_LIST');
+    return $e->die_event unless ref($range) eq 'ARRAY';
+
+    my $query = {
+        "select" => {"bresv" => ["id"]},
+        "from" => "bresv",
+        "where" => {
+            "-and" => [
+                json_query_ranges_overlap(
+                    $range->[0], $range->[1], "start_time", "end_time"
+                ),
+                {"current_resource" => {"!=" => undef}},
+                {"capture_time" => undef},
+                {"cancel_time" => undef},
+                {"return_time" => undef},
+                {"pickup_time" => undef}
+            ],
+        }
+    };
+    if ($pickup_lib) {
+        push @{$query->{"where"}->{"-and"}}, {"pickup_lib" => $pickup_lib};
+    }
+
+    my $ids = [ map { $_->{id} } @{$e->json_query($query)} ];
+    if (@$ids) {
+        my $bresv_list = $e->search_booking_reservation([
+            {"id" => $ids}, {
+                flesh => 1,
+                flesh_fields => {
+                    bresv => [qw/usr target_resource_type current_resource/]
+                }
+            }
+        ]);
+        return $bresv_list ? $bresv_list : [];
+    } else {
+        return $ids;    # empty list
+    }
+}
+__PACKAGE__->register_method(
+    method   => "get_pull_list",
+    api_name => "open-ils.booking.reservations.get_pull_list",
+    argc     => 2,
+    signature=> {
+        params => [
+            {type => 'string', desc => 'Authentication token'},
+            {type => 'array', desc => 'Date/time range for reservations'},
+            {type => 'number', desc => '(Optional) Pickup library'}
+        ],
+        return => { desc => "An array of reservations, fleshed with usr, " .
+            "current_resource, and target_resource_type" }
+    }
+);
+
+
 sub capture_reservation {
     my $self = shift;
     my $client = shift;
@@ -599,7 +659,7 @@ sub capture_reservation {
 }
 __PACKAGE__->register_method(
     method   => "capture_reservation",
-    api_name => "open-ils.booking.reservation.capture",
+    api_name => "open-ils.booking.reservations.capture",
     argc     => 2,
     signature=> {
         params => [
index 1894456..88056c0 100644 (file)
@@ -1411,7 +1411,7 @@ sub reservation_targeter {
                     next;
                 }
 
-                my $copy = asset::copy->search( deleted => f, barcode => $res->barcode )->[0];
+                my $copy = [ asset::copy->search( deleted => f, barcode => $res->barcode )]->[0];
 
                 unless ($copy) {
                     push @good_resources, $res;
index ca59b8e..07a73e2 100644 (file)
@@ -46,3 +46,12 @@ div.nice_vertical_padding {
 span.two_buttons {
     text-align: center;
 }
+option.forced_unavailable {
+    background-color: #ffcccc;
+    color: #990000;
+    font-weight: bold;
+    font-style: italic;
+}
+div#preselected_patron {
+    font-size: 12pt;
+}
index 4f1144b..b2aeae8 100644 (file)
@@ -5,14 +5,16 @@
     'SELECT_A_BRSRC_THEN': "Select a resource from the big list above.",
     'CREATE_BRESV_LOCAL_ERROR': "Exception trying to create reservation: ",
     'CREATE_BRESV_SERVER_ERROR': "Server error trying to create reservation: ",
-    'CREATE_BRESV_SERVER_NO_RESPONSE': "No response from server after trying " +
-        "to create reservation: ",
+    'CREATE_BRESV_SERVER_NO_RESPONSE':
+        "No response from server after trying to create reservation: ",
     /* FIXME: Users aren't likely to be able to do anything with the following
      * message.  Figure out a way to do something more helpful.
      */
     'CREATE_BRESV_OK_MISSING_TARGET': function(n, m) {
         return "Created " + n + " reservation(s), but " + m + " of these " +
-            "couldn't target any resources.";
+            "couldn't target any resources.\n\n" +
+            "This means that it won't be possible to fulfill some of these\n" +
+            "reservations until a suitable resource becomes available.";
     },
     'CREATE_BRESV_OK': function(n) {
         return "Created " + n + " reservation" + (n == 1 ? "" : "s") + ".";
     'WHERES_THE_BARCODE': "Enter a patron's barcode to make a reservation.",
     'ACTOR_CARD_NOT_FOUND': "Patron barcode not found. Please try again.",
     'GET_BRESV_LIST_ERR': "Error while retrieving reservation list: ",
-    'GET_BRESV_LIST_NO_RESULT': "No results from server " +
-        "retrieving reservation list.",
+    'GET_BRESV_LIST_NO_RESULT':
+        "No results from server retrieving reservation list.",
     'OUTSTANDING_BRESV': "Outstanding reservations for patron",
     'UNTARGETED': "None targeted",
-    'GET_PATRON_NO_RESULT': "No server response after attempting to " +
-        "look up patron by barcode.",
+    'GET_PATRON_NO_RESULT':
+        "No server response after attempting to look up patron by barcode.",
     'HERE_ARE_EXISTING_BRESV': "Existing reservations for",
+    'NO_EXISTING_BRESV': "This user has no existing reservations at this time.",
+    'NO_USABLE_BRSRC':
+        "No reservable resources.  Adjust start and end time\n" +
+        "until a resource is available for reservation.",
     'CXL_BRESV_SUCCESS': function(n) {
         return ("Canceled " + n + " reservation" + (n == 1 ? "" : "s") + ".");
     },
     'CXL_BRESV_FAILURE': "Error canceling reservations.",
-    'CXL_BRESV_SELECT_SOMETHING': "You have not selected any reservations to " +
-        "cancel.",
+    'CXL_BRESV_SELECT_SOMETHING':
+        "You have not selected any reservations to cancel.",
+    'NEED_EXACTLY_ONE_BRT_PASSED_IN':
+        "Can't book multiple resource types at once",
+    'COULD_NOT_RETRIEVE_BRT_PASSED_IN':
+        "Error retrieving booking resource type",
+    'INVALID_TS_RANGE':
+        "You must choose a valid start and end time for the reservation.",
     'ANY': "ANY",
 
     'AUTO_choose_a_brt': "Choose a Bookable Resource Type",
     'AUTO_i_need_this_resource': "I need this resource...",
-    'AUTO_starting_at': "Starting at",
-    'AUTO_ending_at': "and ending at",
+    'AUTO_starting_at': "Between",
+    'AUTO_ending_at': "and",
     'AUTO_with_these_attr': "With these attributes:",
     'AUTO_patron_barcode': "Reserve to patron barcode:",
     'AUTO_ATTR_VALUE_next': "Next",
@@ -49,5 +61,6 @@
     'AUTO_bresv_grid_type': "Type",
     'AUTO_bresv_grid_resource': "Resource",
     'AUTO_bresv_grid_start_time': "Start time",
-    'AUTO_bresv_grid_end_time': "End time"
+    'AUTO_bresv_grid_end_time': "End time",
+    'AUTO_brt_noncat_only': "Show only non-cataloged bookable resource types"
 }
index 612a54e..94cdd9c 100644 (file)
@@ -13,9 +13,11 @@ dojo.requireLocalization("openils.booking", "reservation");
  */
 var localeStrings = dojo.i18n.getLocalization("openils.booking", "reservation");
 var pcrud = new openils.PermaCrud();
+var opts;
 var our_brt;
 var brsrc_index = {};
 var bresv_index = {};
+var just_reserved_now = {};
 
 function AttrValueTable() { this.t = {}; }
 AttrValueTable.prototype.set = function(attr, value) { this.t[attr] = value; };
@@ -36,46 +38,126 @@ AttrValueTable.prototype.get_all_values = function() {
 var attr_value_table =  new AttrValueTable();
 
 function TimestampRange() {
-    this.start = {"date": undefined, "time": undefined};
-    this.end = {"date": undefined, "time": undefined};
+    this.start = new Date();
+    this.end = new Date();
+
+    this.validity = {"start": false, "end": false};
+    this.nodes = {
+        "start": {"date": undefined, "time": undefined},
+        "end": {"date": undefined, "time": undefined}
+    };
+    this.saved_style_properties = {};
+    this.invalid_style_properties = {
+        "backgroundColor": "#ffcccc",
+        "color": "#990000",
+        "borderColor": "#990000",
+        "fontWeight": "bold"
+    };
 }
 TimestampRange.prototype.get_timestamp = function(when) {
-    return (this[when].date + " " + this[when].time);
+    return this.any_widget.serialize(this[when]).
+        replace("T", " ").substr(0, 19);
 };
 TimestampRange.prototype.get_range = function() {
     return this.is_backwards() ?
         [this.get_timestamp("end"), this.get_timestamp("start")] :
         [this.get_timestamp("start"), this.get_timestamp("end")];
 };
-TimestampRange.prototype.split_time = function(s) {
-    /* We're not interested in seconds for our purposes,
-     * so we floor everything to :00.
-     *
-     * Also, notice that following discards all time zone information
-     * from the timestamp string represenation.  This should probably
-     * stay the way it is, even when this code is improved to support
-     * selecting time zones (it currently just assumes server's local
-     * time).  The easy way to add support will be to add a drop-down
-     * selector from which the user can pick a time zone, then use
-     * that timezone literal in an "AT TIME ZONE" clause in SQL on
-     * the server side.
-     */
-    return s.split("T")[1].replace(/(\d{2}:\d{2}:)(\d{2})(.*)/, "$100");
-};
-TimestampRange.prototype.split_date = function(s) {
-    return s.split("T")[0];
-};
 TimestampRange.prototype.update_from_widget = function(widget) {
     var when = widget.id.match(/(start|end)/)[1];
     var which = widget.id.match(/(date|time)/)[1];
 
+    if (this.any_widget == undefined)
+        this.any_widget = widget;
+    if (this.nodes[when][which] == undefined)
+        this.nodes[when][which] = widget.domNode; /* We'll need this later */
+
     if (when && which) {
-        this[when][which] =
-            this["split_" + which](widget.serialize(widget.value));
+        this.update_timestamp(when, which, widget.value);
+    }
+
+    this.compute_validity();
+    this.paint_validity();
+};
+TimestampRange.prototype.compute_validity = function() {
+    if (Math.abs(this.start - this.end) < 1000) {
+        this.validity.end = false;
+    } else {
+        if (this.start < this.current_minimum())
+            this.validity.start = false;
+        else
+            this.validity.start = true;
+
+        if (this.end < this.current_minimum())
+            this.validity.end = false;
+        else
+            this.validity.end = true;
+    }
+};
+/* This method provides the minimum timestamp that is considered valid. For
+ * now it's arbitrarily "now + 15 minutes", meaning that all reservations
+ * must be made at least 15 minutes in the future.
+ *
+ * For reasons of keeping the middle layer happy, this should always return
+ * a time that is at least somewhat in the future. The ML isn't able to target
+ * any resources for a reservation with a start date that isn't in the future.
+ */
+TimestampRange.prototype.current_minimum = function() {
+    /* XXX This is going to be a problem with local clocks that are off. */
+    var n = new Date();
+    n.setTime(n.getTime() + 1000 * 900); /* XXX 15 minutes; stop hardcoding! */
+    return n;
+};
+TimestampRange.prototype.update_timestamp = function(when, which, value) {
+    if (which == "date") {
+        this[when].setFullYear(value.getFullYear());
+        this[when].setMonth(value.getMonth());
+        this[when].setDate(value.getDate());
+    } else {    /* "time" */
+        this[when].setHours(value.getHours());
+        this[when].setMinutes(value.getMinutes());
+        this[when].setSeconds(0);
     }
 };
 TimestampRange.prototype.is_backwards = function() {
-    return (this.get_timestamp("start") > this.get_timestamp("end"));
+    return (this.start > this.end);
+};
+TimestampRange.prototype.paint_validity = function()  {
+    for (var when in this.validity) {
+        if (this.validity[when]) {
+            this.paint_valid_node(this.nodes[when].date);
+            this.paint_valid_node(this.nodes[when].time);
+        } else {
+            this.paint_invalid_node(this.nodes[when].date);
+            this.paint_invalid_node(this.nodes[when].time);
+        }
+    }
+};
+TimestampRange.prototype.paint_invalid_node = function(node) {
+    if (node) {
+        /* Just toggling the class of something would be better than
+         * manually setting style here, but I haven't been able to get that
+         * to play nicely with dojo's styling of the date/time textboxen.
+         */
+        if (this.saved_style_properties.backgroundColor == undefined) {
+            for (var k in this.invalid_style_properties) {
+                this.saved_style_properties[k] = node.style[k];
+            }
+        }
+        for (var k in this.invalid_style_properties) {
+            node.style[k] = this.invalid_style_properties[k];
+        }
+    }
+};
+TimestampRange.prototype.paint_valid_node = function(node) {
+    if (node) {
+        for (var k in this.saved_style_properties) {
+            node.style[k] = this.saved_style_properties[k];
+        }
+    }
+};
+TimestampRange.prototype.is_valid = function() {
+    return (this.validity.start && this.validity.end);
 };
 var reserve_timestamp_range = new TimestampRange();
 
@@ -93,7 +175,8 @@ SelectorMemory.prototype.save = function() {
 SelectorMemory.prototype.restore = function() {
     for (var i = 0; i < this.selector.options.length; i++) {
         if (this.memory[this.selector.options[i].value]) {
-            this.selector.options[i].selected = true;
+            if (!this.selector.options[i].disabled)
+                this.selector.options[i].selected = true;
         }
     }
 };
@@ -148,6 +231,10 @@ function get_all_noncat_brt() {
     );
 }
 
+function get_brt_by_id(id) {
+    return pcrud.retrieve("brt", id);
+}
+
 function get_brsrc_id_list() {
     var options = {"type": our_brt.id()};
 
@@ -168,36 +255,56 @@ function get_brsrc_id_list() {
     );
 }
 
-// FIXME: We need failure checking after pcrud.retrieve()
-function sync_brsrc_index_from_ids(id_list) {
-    /* One pass to populate the cache with anything that's missing. */
-    for (var i in id_list) {
-        if (!brsrc_index[id_list[i]]) {
-            brsrc_index[id_list[i]] = pcrud.retrieve("brsrc", id_list[i]);
+/* FIXME: We need failure checking after pcrud.retrieve() */
+function add_brsrc_to_index_if_needed(list, further) {
+    for (var i in list) {
+        if (!brsrc_index[list[i]]) {
+            brsrc_index[list[i]] = pcrud.retrieve("brsrc", list[i]);
         }
-        brsrc_index[id_list[i]].isdeleted(false); // See NOTE below.
+        if (further)
+            further(brsrc_index[list[i]]);
     }
-    /* A second pass to indicate any entries in the cache to be hidden. */
+}
+
+function sync_brsrc_index_from_ids(available_list, additional_list) {
+    /* Default states for everything in the index. Read the further comments. */
     for (var i in brsrc_index) {
-        if (id_list.indexOf(Number(i)) < 0) { // Number() is important.
-            brsrc_index[i].isdeleted(true); // See NOTE below.
-        }
+        brsrc_index[i].isdeleted(true);
+        brsrc_index[i].ischanged(false);
     }
-    /* NOTE: We lightly abuse the isdeleted() magic attribute of the brsrcs
-     * in our cache.  Because we're not going to pass back any brsrcs to
-     * the middle layer, it doesn't really matter what we set this attribute
-     * to. What we're using it for is to indicate in our little brsrc cache
-     * whether a given brsrc should be displayed in this UI's current state
-     * (based on whether it was returned by the last call to the middle layer,
-     * i.e., whether it matches the currently selected attributes).
+
+    /* Populate the cache with anything that's missing and tag everything
+     * in the "available" list as *not* deleted, and tag everything in the
+     * additional list as "changed." See below. */
+    add_brsrc_to_index_if_needed(
+        available_list, function(o) { o.isdeleted(false); }
+    );
+    add_brsrc_to_index_if_needed(
+        additional_list,
+        function(o) {
+            if (!(o.id() in just_reserved_now)) o.ischanged(true);
+        }
+    );
+    /* NOTE: We lightly abuse the isdeleted() and ischanged() magic fieldmapper
+     * attributes of the brsrcs in our cache.  Because we're not going to
+     * pass back any brsrcs to the middle layer, it doesn't really matter
+     * what we set this attribute to. What we're using it for is to indicate
+     * in our little brsrc cache how a given brsrc should be displayed in this
+     * UI's current state (based on whether the brsrc matches timestamp range
+     * availability (isdeleted(false)) and whether the brsrc has been forced
+     * into the list because it was selected in a previous interface (like
+     * the catalog) (ischanged(true))).
      */
 }
 
 function check_bresv_targeting(results) {
     var missing = 0;
     for (var i in results) {
-        if (!(results[i].targeting && results[i].targeting.current_resource))
+        if (!(results[i].targeting && results[i].targeting.current_resource)) {
             missing++;
+        } else {
+            just_reserved_now[results[i].targeting.current_resource] = true;
+        }
     }
     return missing;
 }
@@ -207,6 +314,9 @@ function create_bresv(resource_list) {
     if (barcode == "") {
         alert(localeStrings.WHERES_THE_BARCODE);
         return;
+    } else if (!reserve_timestamp_range.is_valid()) {
+        alert(localeStrings.INVALID_TS_RANGE);
+        return;
     }
     var results;
     try {
@@ -285,7 +395,12 @@ function create_bresv_on_brsrc() {
         alert(localeStrings.SELECT_A_BRSRC_THEN);
 }
 
-function create_bresv_on_brt() { create_bresv(); }
+function create_bresv_on_brt() {
+    if (any_usable_brsrc())
+        create_bresv();
+    else
+        alert(localeStrings.NO_USABLE_BRSRC);
+}
 
 function get_actor_by_barcode(barcode) {
     var usr = fieldmapper.standardRequest(
@@ -329,11 +444,24 @@ function init_bresv_grid(barcode) {
             alert(my_ils_error(localeStrings.GET_BRESV_LIST_ERR, result));
         }
     } else {
+        if (result.length < 1) {
+            document.getElementById("bresv_grid_alt_explanation").innerHTML =
+                localeStrings.NO_EXISTING_BRESV;
+            hide_dom_element(document.getElementById("bresv_grid"));
+            reveal_dom_element(document.getElementById("reserve_under"));
+        } else {
+            document.getElementById("bresv_grid_alt_explanation").innerHTML =
+                "";
+            reveal_dom_element(document.getElementById("bresv_grid"));
+            reveal_dom_element(document.getElementById("reserve_under"));
+        }
+        /* May as well do the following in either case... */
         bresvGrid.setStore(
             new dojo.data.ItemFileReadStore(
                 {"data": flatten_to_dojo_data(result)}
             )
         );
+        bresv_index = {};
         for (var i in result) {
             bresv_index[result[i].id()] = result[i];
         }
@@ -369,6 +497,9 @@ function provide_brt_selector(targ_div) {
             targ_div.appendChild(
                 document.createTextNode(localeStrings.NO_BRT_RESULTS)
             );
+            document.getElementById(
+                "brt_select_other_controls"
+            ).style.display = "none";
         } else {
             var selector = document.createElement("select");
             selector.setAttribute("id", "brt_selector");
@@ -383,6 +514,7 @@ function provide_brt_selector(targ_div) {
                 option.appendChild(document.createTextNode(brt_list[i].name()));
                 selector.appendChild(option);
             }
+            targ_div.innerHTML = "";
             targ_div.appendChild(selector);
         }
     }
@@ -396,7 +528,8 @@ function init_reservation_interface(f) {
     reveal_dom_element(reserve_block);
 
     /* Save a global reference to the brt we're going to reserve */
-    our_brt = xulG.brt_list[f.brt_selector.selectedIndex];
+    if (f)
+        our_brt = xulG.brt_list[f.brt_selector.selectedIndex];
 
     /* Get a list of attributes that can apply to that brt. */
     var bra_list = pcrud.search("bra", {"resource_type": our_brt.id()});
@@ -411,6 +544,10 @@ function init_reservation_interface(f) {
         brav_by_bra[o.id()] = pcrud.search("brav", {"attr": o.id()});
     });
 
+    /* Hide the label over the attributes widgets if we have nothing to show. */
+    var domf = (bra_list.length < 1) ? hide_dom_element : reveal_dom_element;
+    domf(document.getElementById("bra_and_brav_header"));
+
     /* Create DOM widgets to represent each attribute/values set. */
     for (var i in bra_list) {
         var bra_div = document.createElement("div");
@@ -451,12 +588,20 @@ function init_reservation_interface(f) {
      * asking about. */
     document.getElementById("brsrc_list_header").innerHTML = our_brt.name();
 
+    if (opts.patron_barcode) {
+        document.getElementById("holds_patron_barcode").style.display = "none";
+        document.getElementById("patron_barcode").value = opts.patron_barcode;
+        document.getElementById("patron_barcode").onchange();
+    }
     update_brsrc_list();
 }
 
 function update_brsrc_list() {
     var brsrc_id_list = get_brsrc_id_list();
-    sync_brsrc_index_from_ids(brsrc_id_list);
+    var force_list = (opts.booking_results && opts.booking_results.brsrc) ?
+        opts.booking_results.brsrc.map(function(o) { return o[0]; }) : [];
+
+    sync_brsrc_index_from_ids(brsrc_id_list, force_list);
 
     var target_selector = document.getElementById("brsrc_list");
     var selector_memory = new SelectorMemory(target_selector);
@@ -464,18 +609,32 @@ function update_brsrc_list() {
     target_selector.innerHTML = "";
 
     for (var i in brsrc_index) {
-        if (brsrc_index[i].isdeleted()) {
+        if (brsrc_index[i].isdeleted() && (!brsrc_index[i].ischanged()))
             continue;
-        }
+
         var opt = document.createElement("option");
         opt.setAttribute("value", brsrc_index[i].id());
         opt.appendChild(document.createTextNode(brsrc_index[i].barcode()));
+
+        if (brsrc_index[i].isdeleted() && (brsrc_index[i].ischanged())) {
+            opt.setAttribute("class", "forced_unavailable");
+            opt.setAttribute("disabled", "disabled");
+        }
+
         target_selector.appendChild(opt);
     }
 
     selector_memory.restore();
 }
 
+function any_usable_brsrc() {
+    for (var i in brsrc_index) {
+        if (!brsrc_index[i].isdeleted())
+            return true;
+    }
+    return false;
+}
+
 function update_bresv_grid() {
     var widg = document.getElementById("patron_barcode");
     if (widg.value != "") {
@@ -494,8 +653,6 @@ function update_bresv_grid() {
             }
         }, 0);
         setTimeout(function() { init_bresv_grid(widg.value); }, 0);
-
-        reveal_dom_element(document.getElementById("reserve_under"));
     }
 }
 
@@ -536,6 +693,10 @@ function cancel_selected_bresv(bresv_dojo_items) {
         cancel_reservations(
             bresv_dojo_items.map(function(o) { return bresv_index[o.id]; })
         );
+        /* After some delay to allow the cancellations a chance to get
+         * committed, refresh the brsrc list as it might reflect newly
+         * available resources now. */
+        setTimeout(update_brsrc_list, 2000);
     } else {
         alert(localeStrings.CXL_BRESV_SELECT_SOMETHING);
     }
@@ -576,6 +737,41 @@ function init_auto_l10n(el) {
     }
 }
 
+/* The following function should return true if the reservation interface
+ * should start normally (show a list of brt to choose from) or false if
+ * it should not (because we've "started" it some other way by setting up
+ * and displaying other widgets).
+ */
+function early_action_passthru() {
+    if (opts.booking_results) {
+        if (opts.booking_results.brt.length != 1) {
+            alert(localeStrings.NEED_EXACTLY_ONE_BRT_PASSED_IN);
+            return true;
+        } else if (!(our_brt = get_brt_by_id(opts.booking_results.brt[0][0]))) {
+            alert(localeStrings.COULD_NOT_RETRIEVE_BRT_PASSED_IN);
+            return true;
+        }
+
+        init_reservation_interface();
+        return false;
+    }
+
+    if (opts.patron_barcode) {
+        try {
+            var patron = get_actor_by_barcode(opts.patron_barcode);
+            if (patron) {
+                document.getElementById("preselected_patron").innerHTML =
+                    "Patron targeted for reservation: <strong>" +
+                    formal_name(patron) + "</strong>";
+            }
+        } catch (E) {
+            ; /* XXX ignorable? perhaps. */
+        }
+    }
+
+    return true;
+}
+
 /*
  * my_init
  */
@@ -583,7 +779,10 @@ function my_init() {
     hide_dom_element(document.getElementById("brt_reserve_block"));
     reveal_dom_element(document.getElementById("brt_search_block"));
     hide_dom_element(document.getElementById("reserve_under"));
-    provide_brt_selector(document.getElementById("brt_selector_here"));
     init_auto_l10n(document.getElementById("auto_l10n_start_here"));
     init_timestamp_widgets();
+
+    if (!(opts = xulG.bresv_interface_opts)) opts = {};
+    if (early_action_passthru())
+        provide_brt_selector(document.getElementById("brt_selector_here"));
 }
index cacdf24..713d946 100644 (file)
 <!ENTITY staff.patron_navbar.holds.accesskey 'H'>
 <!ENTITY staff.patron_navbar.alert 'Display Alert and Messages'>
 <!ENTITY staff.patron_navbar.alert.accesskey 'A'>
+<!ENTITY staff.patron_navbar.booking 'Booking'>
+<!ENTITY staff.patron_navbar.booking.accesskey 'k'>
 <!ENTITY staff.patron_navbar.other 'Other'>
 <!ENTITY staff.patron_navbar.other.accesskey 'o'>
 <!ENTITY staff.patron_navbar.items 'Items Out'>
 <!ENTITY staff.circ.copy_status_overlay.sel_patron.accesskey "L">
 <!ENTITY staff.circ.copy_status_overlay.cmd_triggered_events.label "Show Triggered Events">
 <!ENTITY staff.circ.copy_status_overlay.cmd_triggered_events.accesskey "T">
+<!ENTITY staff.circ.copy_status_overlay.cmd_book_item_now.label "Book Item Now">
+<!ENTITY staff.circ.copy_status_overlay.cmd_book_item_now.accesskey "N">
+<!ENTITY staff.circ.copy_status_overlay.cmd_create_brt.label "Make Item Bookable">
+<!ENTITY staff.circ.copy_status_overlay.cmd_create_brt.accesskey "K">
 <!ENTITY staff.circ.copy_status_overlay.sel_edit.label "Edit Item Attributes">
 <!ENTITY staff.circ.copy_status_overlay.sel_edit.accesskey "E">
 <!ENTITY staff.circ.copy_status_overlay.sel_mark_items_damaged.label "Mark Item Damaged">
 <!ENTITY staff.cat.copy_browser.actions.cmd_add_items_to_buckets.accesskey "B">
 <!ENTITY staff.cat.copy_browser.actions.sel_copy_details.label "Show Item Details">
 <!ENTITY staff.cat.copy_browser.actions.sel_copy_details.accesskey "I">
-<!ENTITY staff.cat.copy_browser.actions.cmd_book_item_now.label "Book This Item">
-<!ENTITY staff.cat.copy_browser.actions.cmd_book_item_now.accesskey "K">
-<!ENTITY staff.cat.copy_browser.actions.cmd_create_brt.label "Make This Item Bookable">
-<!ENTITY staff.cat.copy_browser.actions.cmd_create_brt.accesskey "Y">
+<!ENTITY staff.cat.copy_browser.actions.cmd_book_item_now.label "Book Item Now">
+<!ENTITY staff.cat.copy_browser.actions.cmd_book_item_now.accesskey "N">
+<!ENTITY staff.cat.copy_browser.actions.cmd_create_brt.label "Make Item Bookable">
+<!ENTITY staff.cat.copy_browser.actions.cmd_create_brt.accesskey "K">
 <!ENTITY staff.cat.copy_browser.actions.sel_patron.label "Show Last Few Circulations">
 <!ENTITY staff.cat.copy_browser.actions.sel_patron.accesskey "L">
 <!ENTITY staff.cat.copy_browser.actions.cmd_edit_items.label "Edit Item Attributes">
index fd337a8..ae80b33 100644 (file)
         <h1 class="booking AUTO_choose_a_brt"></h1>
         <form onsubmit="init_reservation_interface(this); return false;">
             <div id="brt_selector_here" class="nice_vertical_padding"></div>
-            <div class="nice_vertical_padding">
-                <input type="submit" class="AUTO_ATTR_VALUE_next" />
+            <div id="brt_select_other_controls">
+                <!-- <div class="nice_vertical_padding">
+                    <input type="checkbox" name="brt_noncat_only"
+                        id="brt_noncat_only" checked="checked" />
+                    <label for="brt_noncat_only"
+                        class="AUTO_brt_noncat_only"></label>
+                </div> -->
+                <div class="nice_vertical_padding">
+                    <input type="submit"
+                        class="AUTO_ATTR_VALUE_next" />
+                </div>
             </div>
         </form>
+        <div id="preselected_patron"></div>
     </div>
 
     <div id="brt_reserve_block" class="container">
@@ -25,7 +35,7 @@
                     anything in CSS. -->
                 <select id="brsrc_list" name="brsrc_list" multiple="multiple"
                     size="12"></select>
-                <div class="nice_vertical_padding">
+                <div id="holds_patron_barcode" class="nice_vertical_padding">
                     <label class="AUTO_patron_barcode"
                         for="patron_barcode" /></label>
                     <input name="patron_barcode" id="patron_barcode"
                     <input id="reserve_date_end" />
                     <input id="reserve_time_end" />
                 </div>
-                <h2 class="booking AUTO_with_these_attr"></h2>
-                <div id="bra_and_brav">
-                </div>
+                <h2 id="bra_and_brav_header"
+                    class="booking AUTO_with_these_attr"></h2>
+                <div id="bra_and_brav"></div>
             </div>
             <div id="reserve_under">
                 <hr />
                 <h2 class="booking" id="existing_reservation_patron_line"></h2>
+                <div id="bresv_grid_alt_explanation"></div>
                 <table id="bresv_grid" jsId="bresvGrid"
                     dojoType="dojox.grid.DataGrid" query="{id: '*'}"
                     rowSelector="20px" autoHeight="true">
index c9ed325..776a7f4 100644 (file)
@@ -6,7 +6,7 @@ util.functional = {};
 util.functional.EXPORT_OK    = [ 
     'filter_list', 'filter_object', 'find_list', 'find_object', 'map_list', 'map_flat_list', 
     'map_object', 'map_object_to_list', 'convert_object_list_to_hash', 'find_id_object_in_list', 
-    'find_attr_object_in_list', 'walk_tree_preorder',
+    'find_attr_object_in_list', 'walk_tree_preorder', 'unique_list_values',
 ];
 util.functional.EXPORT_TAGS    = { ':all' : util.functional.EXPORT_OK };
 
@@ -134,4 +134,12 @@ util.functional.find_attr_object_in_list = function(list,attr,value) {
     return null;
 }
 
+util.functional.unique_list_values = function(list) {
+    var obj = {};
+    var finished_list = [];
+    for (var i in list) { obj[list[i]] = true; }
+    for (var i in obj) { finished_list.push(i); }
+    return finished_list;
+}
+
 dump('exiting util/functional.js\n');
index 48bfefa..e36e2d3 100644 (file)
@@ -1637,6 +1637,7 @@ cat.copy_browser.prototype = {
         try {
             var found_aou = false; var found_acn = false; var found_acp = false;
             var found_aou_with_can_have_vols = false;
+            var sel_copy_libs = {};
             for (var i = 0; i < obj.sel_list.length; i++) {
                 var type = obj.sel_list[i].split(/_/)[0];
                 switch(type) {
@@ -1646,7 +1647,15 @@ cat.copy_browser.prototype = {
                         if ( get_bool( obj.data.hash.aout[ org.ou_type() ].can_have_vols() ) ) found_aou_with_can_have_vols = true;
                     break;
                     case 'acn' : found_acn = true; break;
-                    case 'acp' : found_acp = true; break;
+                    case 'acp' :
+                        found_acp = true;
+                        sel_copy_libs[
+                            obj.map_acn[
+                                "acn_" +
+                                obj.map_acp[obj.sel_list[i]].call_number()
+                            ].owning_lib()
+                        ] = true;
+                        break;
                 }
             }
             obj.controller.view.cmd_add_items.setAttribute('disabled','true');
@@ -1664,6 +1673,7 @@ cat.copy_browser.prototype = {
             obj.controller.view.cmd_transfer_items.setAttribute('disabled','true');
             obj.controller.view.sel_copy_details.setAttribute('disabled','true');
             obj.controller.view.cmd_create_brt.setAttribute('disabled','true');
+            obj.controller.view.cmd_book_item_now.setAttribute('disabled','true');
             obj.controller.view.sel_patron.setAttribute('disabled','true');
             obj.controller.view.sel_mark_items_damaged.setAttribute('disabled','true');
             obj.controller.view.sel_mark_items_missing.setAttribute('disabled','true');
@@ -1690,6 +1700,11 @@ cat.copy_browser.prototype = {
                 obj.controller.view.sel_copy_details.setAttribute('disabled','false');
                 obj.controller.view.cmd_create_brt.setAttribute('disabled','false');
                 obj.controller.view.sel_patron.setAttribute('disabled','false');
+
+                var L = 0; for (var k in sel_copy_libs) L++;
+                if (L < 2) {
+                    obj.controller.view.cmd_book_item_now.setAttribute('disabled','false');
+                }
             }
         } catch(E) {
             obj.error.standard_unexpected_error_alert(document.getElementById('catStrings').getString('staff.cat.copy_browser.actions.error'),E);
index 09a961d..6c895a5 100644 (file)
@@ -73,7 +73,7 @@ vim:noet:sw=4:ts=4:
         <command id="cmd_broken" />
         <command id="sel_copy_details"/>
         <command id="cmd_create_brt"/>
-        <!-- <command id="cmd_book_item_now"/> -->
+        <command id="cmd_book_item_now"/>
         <command id="sel_patron"/>
         <command id="sel_clip" />
         <command id="cmd_clear" />
@@ -104,8 +104,10 @@ vim:noet:sw=4:ts=4:
             <menuitem command="sel_clip" label="&staff.cat.copy_browser.actions.sel_clip.label;" accesskey="&staff.cat.copy_browser.actions.sel_clip.accesskey;"/>
             <menuitem command="cmd_add_items_to_buckets" label="&staff.cat.copy_browser.actions.cmd_add_items_to_buckets.label;" accesskey="&staff.cat.copy_browser.actions.cmd_add_items_to_buckets.accesskey;"/>
             <menuitem command="sel_copy_details" label="&staff.cat.copy_browser.actions.sel_copy_details.label;" accesskey="&staff.cat.copy_browser.actions.sel_copy_details.label;" />
+            <menuseparator/>
             <menuitem command="cmd_create_brt" label="&staff.cat.copy_browser.actions.cmd_create_brt.label;" accesskey="&staff.cat.copy_browser.actions.cmd_create_brt.accesskey;" />
-            <!-- <menuitem command="cmd_book_item_now" label="&staff.cat.copy_browser.actions.cmd_book_item_now.label;" accesskey="&staff.cat.copy_browser.actions.cmd_book_item_now.accesskey;" /> -->
+            <menuitem command="cmd_book_item_now" label="&staff.cat.copy_browser.actions.cmd_book_item_now.label;" accesskey="&staff.cat.copy_browser.actions.cmd_book_item_now.accesskey;" />
+            <menuseparator/>
             <menuitem command="sel_patron" label="&staff.cat.copy_browser.actions.sel_patron.label;" accesskey="&staff.cat.copy_browser.actions.sel_patron.accesskey;"/>
             <menuseparator/>
             <menuitem command="cmd_edit_items" label="&staff.cat.copy_browser.actions.cmd_edit_items.label;" accesskey="&staff.cat.copy_browser.actions.cmd_edit_items.accesskey;"/>
index c360baf..2632954 100644 (file)
@@ -570,7 +570,7 @@ cat.util.edit_new_bresv = function(booking_results) {
         if (xulG.auth == undefined) {
             xulG.auth = {"session": {"key": ses()}};
         }
-        xulG.booking_results = booking_results;
+        xulG.bresv_interface_opts = {"booking_results": booking_results};
         xulG.new_tab(
             xulG.url_prefix("/eg/booking/reservation"),
             {
index 0f22cbf..e2271d4 100644 (file)
@@ -64,6 +64,8 @@ circ.copy_status.prototype = {
                             obj.controller.view.sel_mark_items_missing.setAttribute('disabled','true');
                             obj.controller.view.sel_patron.setAttribute('disabled','true');
                             obj.controller.view.cmd_triggered_events.setAttribute('disabled','true');
+                            obj.controller.view.cmd_create_brt.setAttribute('disabled','true');
+                            obj.controller.view.cmd_book_item_now.setAttribute('disabled','true');
                             obj.controller.view.sel_spine.setAttribute('disabled','true');
                             obj.controller.view.sel_transit_abort.setAttribute('disabled','true');
                             obj.controller.view.sel_clip.setAttribute('disabled','true');
@@ -88,6 +90,12 @@ circ.copy_status.prototype = {
                             obj.controller.view.sel_copy_details.setAttribute('disabled','false');
                             obj.controller.view.sel_mark_items_damaged.setAttribute('disabled','false');
                             obj.controller.view.sel_mark_items_missing.setAttribute('disabled','false');
+                            if (obj.selected_one_unique_owning_lib()) {
+                                obj.controller.view.cmd_book_item_now.setAttribute('disabled','false');
+                            } else {
+                                obj.controller.view.cmd_book_item_now.setAttribute('disabled','true');
+                            }
+                            obj.controller.view.cmd_create_brt.setAttribute('disabled','false');
                             obj.controller.view.sel_spine.setAttribute('disabled','false');
                             obj.controller.view.sel_transit_abort.setAttribute('disabled','false');
                             obj.controller.view.sel_clip.setAttribute('disabled','false');
@@ -184,6 +192,42 @@ circ.copy_status.prototype = {
                             }
                         }
                     ],
+                    'cmd_create_brt' : [
+                        ['command'],
+                        function() {
+                            JSAN.use("cat.util");
+                            JSAN.use("util.functional");
+
+                            var results = cat.util.make_bookable(
+                                util.functional.map_list(
+                                    obj.selection_list, function (o) {
+                                        return o.copy_id;
+                                    }
+                                )
+                            );
+                            if (results && results["brsrc"]) {
+                                cat.util.edit_new_brsrc(results["brsrc"]);
+                            }
+                        }
+                    ],
+                    'cmd_book_item_now' : [
+                        ['command'],
+                        function() {
+                            JSAN.use("cat.util");
+                            JSAN.use("util.functional");
+
+                            var results = cat.util.make_bookable(
+                                util.functional.map_list(
+                                    obj.selection_list, function (o) {
+                                        return o.copy_id;
+                                    }
+                                )
+                            );
+                            if (results) {
+                                cat.util.edit_new_bresv(results);
+                            }
+                        }
+                    ],
                     'sel_checkin' : [
                         ['command'],
                         function() {
@@ -1008,6 +1052,15 @@ circ.copy_status.prototype = {
 
     },
 
+    'selected_one_unique_owning_lib': function () {
+        JSAN.use('util.functional');
+        var list = util.functional.map_list(
+            this.selection_list,
+            function(o) { return o.owning_lib; }
+        );
+        return util.functional.unique_list_values(list).length == 1;
+    },
+
     'test_barcode' : function(bc) {
         var obj = this;
         var good = util.barcode.check(bc);
@@ -1100,6 +1153,7 @@ circ.copy_status.prototype = {
                                 'renewable' : details.circ ? 't' : 'f', 
                                 'copy_id' : details.copy.id(), 
                                 'acn_id' : details.volume ? details.volume.id() : -1, 
+                                'owning_lib' : details.volume ? details.volume.owning_lib() : -1, 
                                 'barcode' : barcode, 
                                 'doc_id' : details.mvr ? details.mvr.doc_id() : null  
                             } 
index 1c7f65d..fdb1433 100644 (file)
         <command id="cmd_alt_view" />
         <command id="cmd_triggered_events" />
         <command id="save_columns" />
+        <command id="cmd_create_brt" disabled="true"/>
+        <command id="cmd_book_item_now" disabled="true"/>
         <command id="sel_copy_details" disabled="true"/>
         <command id="sel_mark_items_damaged" disabled="true"/>
         <command id="sel_mark_items_missing" disabled="true"/>
index a419f41..5b8586d 100644 (file)
@@ -17,6 +17,9 @@
         <menuitem command="sel_copy_details" label="&staff.circ.copy_status_overlay.sel_copy_details.label;" accesskey="&staff.circ.copy_status_overlay.sel_copy_details.accesskey;" />
         <menuitem command="sel_patron" label="&staff.circ.copy_status_overlay.sel_patron.label;" accesskey="&staff.circ.copy_status_overlay.sel_patron.accesskey;"/>
         <menuseparator/>
+        <menuitem command="cmd_create_brt" label="&staff.circ.copy_status_overlay.cmd_create_brt.label;" accesskey="&staff.circ.copy_status_overlay.cmd_create_brt.accesskey;"/>
+        <menuitem command="cmd_book_item_now" label="&staff.circ.copy_status_overlay.cmd_book_item_now.label;" accesskey="&staff.circ.copy_status_overlay.cmd_book_item_now.accesskey;"/>
+        <menuseparator/>
         <menuitem command="sel_edit" label="&staff.circ.copy_status_overlay.sel_edit.label;" accesskey="&staff.circ.copy_status_overlay.sel_edit.accesskey;" />
         <menuseparator/>
         <menuitem command="sel_mark_items_damaged" label="&staff.circ.copy_status_overlay.sel_mark_items_damaged.label;" accesskey="&staff.circ.copy_status_overlay.sel_mark_items_damaged.accesskey;"/>
             <menuitem command="sel_patron" label="&staff.circ.copy_status_overlay.sel_patron.label;" accesskey="&staff.circ.copy_status_overlay.sel_patron.accesskey;"/>
             <menuitem command="cmd_triggered_events" label="&staff.circ.copy_status_overlay.cmd_triggered_events.label;" accesskey="&staff.circ.copy_status_overlay.cmd_triggered_events.accesskey;"/>
             <menuseparator />
+            <menuitem command="cmd_create_brt" label="&staff.circ.copy_status_overlay.cmd_create_brt.label;" accesskey="&staff.circ.copy_status_overlay.cmd_create_brt.accesskey;"/>
+            <menuitem command="cmd_book_item_now" label="&staff.circ.copy_status_overlay.cmd_book_item_now.label;" accesskey="&staff.circ.copy_status_overlay.cmd_book_item_now.accesskey;"/>
+            <menuseparator />
             <menuitem command="sel_edit" label="&staff.circ.copy_status_overlay.sel_edit.label;" accesskey="&staff.circ.copy_status_overlay.sel_edit.accesskey;" />
             <menuseparator />
             <menuitem command="sel_mark_items_damaged" label="&staff.circ.copy_status_overlay.sel_mark_items_damaged.label;" accesskey="&staff.circ.copy_status_overlay.sel_mark_items_damaged.accesskey;"/>
index ee137af..d6d652f 100644 (file)
@@ -375,6 +375,27 @@ patron.display.prototype = {
                             }
                         }
                     ],
+                    'cmd_patron_reservation' : [
+                        ['command'],
+                        function(ev) {
+                            if (xulG.auth == undefined) {
+                                xulG.auth = {"session": {"key": ses()}};
+                            }
+                            xulG.bresv_interface_opts = {
+                                "patron_barcode": obj.patron.card().barcode()
+                            };
+                            xulG.new_tab(
+                                "/eg/booking/reservation",
+                                {
+                                    "tab_name": offlineStrings.getString(
+                                        "menu.cmd_booking_reservation.tab"
+                                    ),
+                                    "browser": false
+                                },
+                                xulG
+                            );
+                        }
+                    ],
                     'cmd_patron_exit' : [
                         ['command'],
                         function(ev) {
index 7c2931c..441cb06 100644 (file)
         <command id="cmd_patron_info_groups" />
         <command id="cmd_patron_other" />
         <command id="cmd_patron_alert" />
+        <command id="cmd_patron_reservation" />
         <command id="cmd_patron_exit" />
         <command id="cmd_patron_retrieve" />
         <command id="cmd_patron_merge" />
index 97814a7..587c8cc 100644 (file)
         <command id="cmd_patron_info_groups" />
         <command id="cmd_patron_other" />
         <command id="cmd_patron_alert" />
+        <command id="cmd_patron_reservation" />
         <command id="cmd_patron_exit" />
         <command id="cmd_patron_retrieve" />
         <command id="cmd_patron_merge" />
index 55d071f..aff5575 100644 (file)
                             <button id="PatronNavBar_edit" command="cmd_patron_edit" class="nav"
                                 label="&staff.patron_navbar.edit;" accesskey="&staff.patron_navbar.edit.accesskey;"/>
                             <button id="PatronNavBar_messages" label="&staff.patron_navbar.actions.menu.standing_penalties.label;" accesskey="&staff.patron_navbar.actions.menu.standing_penalties.accesskey;" command="cmd_standing_penalties" class="nav"/>
+
+                            <button id="PatronNavBar_booking" command="cmd_patron_booking" class="nav" label="&staff.patron_navbar.booking;" accesskey="&staff.patron_navbar.booking.accesskey;" type="menu">
+                                <menupopup>
+                                    <menuitem label="&staff.main.menu.booking.reservation.label;" accesskey="&staff.main.menu.booking.reservation.accesskey;" command="cmd_patron_reservation" />
+                                </menupopup>
+                            </button>
+
                             <button id="PatronNavBar_other" command="cmd_patron_other" class="nav" label="&staff.patron_navbar.other;" accesskey="&staff.patron_navbar.other.accesskey;" type="menu">
                                 <menupopup>
                                     <menuitem label="&staff.patron_navbar.alert;" accesskey="&staff.patron_navbar.alert.accesskey;" command="cmd_patron_alert"/>
index 7796cb9..afbc23c 100644 (file)
                             <button id="PatronNavBar_edit" command="cmd_patron_edit" class="nav"
                                 label="&staff.patron_navbar.edit;" accesskey="&staff.patron_navbar.edit.accesskey;"/>
                             <button id="PatronNavBar_messages" label="&staff.patron_navbar.actions.menu.standing_penalties.label;" accesskey="&staff.patron_navbar.actions.menu.standing_penalties.accesskey;" command="cmd_standing_penalties" class="nav"/>
+
+                            <button id="PatronNavBar_booking" command="cmd_patron_booking" class="nav" label="&staff.patron_navbar.booking;" accesskey="&staff.patron_navbar.booking.accesskey;" type="menu">
+                                <menupopup>
+                                    <menuitem label="&staff.main.menu.booking.reservation.label;" accesskey="&staff.main.menu.booking.reservation.accesskey;" command="cmd_patron_reservation" />
+                                </menupopup>
+                            </button>
+
                             <button id="PatronNavBar_other" command="cmd_patron_other" class="nav" label="&staff.patron_navbar.other;" accesskey="&staff.patron_navbar.other.accesskey;" type="menu">
                                 <menupopup>
                                     <menuitem label="&staff.patron_navbar.alert;" accesskey="&staff.patron_navbar.alert.accesskey;" command="cmd_patron_alert"/>