4 dojo.require("fieldmapper.OrgUtils");
5 dojo.require("openils.PermaCrud");
6 dojo.require("openils.widget.OrgUnitFilteringSelect");
7 dojo.require("dojo.data.ItemFileReadStore");
8 dojo.require("dijit.form.DateTextBox");
9 dojo.require("dijit.form.TimeTextBox");
10 dojo.requireLocalization("openils.booking", "reservation");
13 * Globals; prototypes and their instances
15 var localeStrings = dojo.i18n.getLocalization("openils.booking", "reservation");
16 var pcrud = new openils.PermaCrud();
19 var pickup_lib_selected;
23 var just_reserved_now = {};
25 function AttrValueTable() { this.t = {}; }
26 AttrValueTable.prototype.set = function(attr, value) { this.t[attr] = value; };
27 AttrValueTable.prototype.update_from_selector = function(selector) {
28 var attr = selector.name.match(/_(\d+)$/)[1];
29 var value = selector.options[selector.selectedIndex].value;
31 attr_value_table.set(attr, value);
33 AttrValueTable.prototype.get_all_values = function() {
35 for (var k in this.t) {
36 if (this.t[k] != undefined && this.t[k] != "")
37 values.push(this.t[k]);
41 var attr_value_table = new AttrValueTable();
43 function TimestampRange() {
44 this.start = new Date();
45 this.end = new Date();
47 this.validity = {"start": false, "end": false};
49 "start": {"date": undefined, "time": undefined},
50 "end": {"date": undefined, "time": undefined}
52 this.saved_style_properties = {};
53 this.invalid_style_properties = {
54 "backgroundColor": "#ffcccc",
56 "borderColor": "#990000",
60 TimestampRange.prototype.get_timestamp = function(when) {
61 return this.any_widget.serialize(this[when]).
62 replace("T", " ").substr(0, 19);
64 TimestampRange.prototype.get_range = function() {
65 return this.is_backwards() ?
66 [this.get_timestamp("end"), this.get_timestamp("start")] :
67 [this.get_timestamp("start"), this.get_timestamp("end")];
69 TimestampRange.prototype.update_from_widget = function(widget) {
70 var when = widget.id.match(/(start|end)/)[1];
71 var which = widget.id.match(/(date|time)/)[1];
73 if (this.any_widget == undefined)
74 this.any_widget = widget;
75 if (this.nodes[when][which] == undefined)
76 this.nodes[when][which] = widget.domNode; /* We'll need this later */
79 this.update_timestamp(when, which, widget.value);
82 this.compute_validity();
83 this.paint_validity();
85 TimestampRange.prototype.compute_validity = function() {
86 if (Math.abs(this.start - this.end) < 1000) {
87 this.validity.end = false;
89 if (this.start < this.current_minimum())
90 this.validity.start = false;
92 this.validity.start = true;
94 if (this.end < this.current_minimum())
95 this.validity.end = false;
97 this.validity.end = true;
100 /* This method provides the minimum timestamp that is considered valid. For
101 * now it's arbitrarily "now + 15 minutes", meaning that all reservations
102 * must be made at least 15 minutes in the future.
104 * For reasons of keeping the middle layer happy, this should always return
105 * a time that is at least somewhat in the future. The ML isn't able to target
106 * any resources for a reservation with a start date that isn't in the future.
108 TimestampRange.prototype.current_minimum = function() {
109 /* XXX This is going to be a problem with local clocks that are off. */
111 n.setTime(n.getTime() + 1000 * 900); /* XXX 15 minutes; stop hardcoding! */
114 TimestampRange.prototype.update_timestamp = function(when, which, value) {
115 if (which == "date") {
116 this[when].setFullYear(value.getFullYear());
117 this[when].setMonth(value.getMonth());
118 this[when].setDate(value.getDate());
119 } else { /* "time" */
120 this[when].setHours(value.getHours());
121 this[when].setMinutes(value.getMinutes());
122 this[when].setSeconds(0);
125 TimestampRange.prototype.is_backwards = function() {
126 return (this.start > this.end);
128 TimestampRange.prototype.paint_validity = function() {
129 for (var when in this.validity) {
130 if (this.validity[when]) {
131 this.paint_valid_node(this.nodes[when].date);
132 this.paint_valid_node(this.nodes[when].time);
134 this.paint_invalid_node(this.nodes[when].date);
135 this.paint_invalid_node(this.nodes[when].time);
139 TimestampRange.prototype.paint_invalid_node = function(node) {
141 /* Just toggling the class of something would be better than
142 * manually setting style here, but I haven't been able to get that
143 * to play nicely with dojo's styling of the date/time textboxen.
145 if (this.saved_style_properties.backgroundColor == undefined) {
146 for (var k in this.invalid_style_properties) {
147 this.saved_style_properties[k] = node.style[k];
150 for (var k in this.invalid_style_properties) {
151 node.style[k] = this.invalid_style_properties[k];
155 TimestampRange.prototype.paint_valid_node = function(node) {
157 for (var k in this.saved_style_properties) {
158 node.style[k] = this.saved_style_properties[k];
162 TimestampRange.prototype.is_valid = function() {
163 return (this.validity.start && this.validity.end);
165 var reserve_timestamp_range = new TimestampRange();
167 function SelectorMemory(selector) {
168 this.selector = selector;
171 SelectorMemory.prototype.save = function() {
172 for (var i = 0; i < this.selector.options.length; i++) {
173 if (this.selector.options[i].selected) {
174 this.memory[this.selector.options[i].value] = true;
178 SelectorMemory.prototype.restore = function() {
179 for (var i = 0; i < this.selector.options.length; i++) {
180 if (this.memory[this.selector.options[i].value]) {
181 if (!this.selector.options[i].disabled)
182 this.selector.options[i].selected = true;
188 * These functions communicate with the middle layer.
190 function get_all_noncat_brt() {
191 return pcrud.search("brt",
192 {"id": {"!=": null}, "catalog_item": "f"},
193 {"order_by": {"brt":"name"}}
197 function get_brt_by_id(id) {
198 return pcrud.retrieve("brt", id);
201 function get_brsrc_id_list() {
202 var options = {"type": our_brt.id(), "pickup_lib": pickup_lib_selected};
204 /* This mechanism for avoiding the passing of an empty 'attribute_values'
205 * option is essential because if you pass such an option to the
206 * middle layer API at all, it won't return any IDs for brsrcs that
207 * don't have at least one attribute of some kind.
209 var attribute_values = attr_value_table.get_all_values();
210 if (attribute_values.length > 0)
211 options.attribute_values = attribute_values;
213 options.available = reserve_timestamp_range.get_range();
215 return fieldmapper.standardRequest(
216 ["open-ils.booking", "open-ils.booking.resources.filtered_id_list"],
217 [xulG.auth.session.key, options]
221 /* FIXME: We need failure checking after pcrud.retrieve() */
222 function add_brsrc_to_index_if_needed(list, further) {
223 for (var i in list) {
224 if (!brsrc_index[list[i]]) {
225 brsrc_index[list[i]] = pcrud.retrieve("brsrc", list[i]);
228 further(brsrc_index[list[i]]);
232 function sync_brsrc_index_from_ids(available_list, additional_list) {
233 /* Default states for everything in the index. Read the further comments. */
234 for (var i in brsrc_index) {
235 brsrc_index[i].isdeleted(true);
236 brsrc_index[i].ischanged(false);
239 /* Populate the cache with anything that's missing and tag everything
240 * in the "available" list as *not* deleted, and tag everything in the
241 * additional list as "changed." See below. */
242 add_brsrc_to_index_if_needed(
243 available_list, function(o) { o.isdeleted(false); }
245 add_brsrc_to_index_if_needed(
248 if (!(o.id() in just_reserved_now)) o.ischanged(true);
251 /* NOTE: We lightly abuse the isdeleted() and ischanged() magic fieldmapper
252 * attributes of the brsrcs in our cache. Because we're not going to
253 * pass back any brsrcs to the middle layer, it doesn't really matter
254 * what we set this attribute to. What we're using it for is to indicate
255 * in our little brsrc cache how a given brsrc should be displayed in this
256 * UI's current state (based on whether the brsrc matches timestamp range
257 * availability (isdeleted(false)) and whether the brsrc has been forced
258 * into the list because it was selected in a previous interface (like
259 * the catalog) (ischanged(true))).
263 function check_bresv_targeting(results) {
265 for (var i in results) {
266 if (!(results[i].targeting && results[i].targeting.current_resource)) {
269 just_reserved_now[results[i].targeting.current_resource] = true;
275 function create_bresv(resource_list) {
276 var barcode = document.getElementById("patron_barcode").value;
278 alert(localeStrings.WHERES_THE_BARCODE);
280 } else if (!reserve_timestamp_range.is_valid()) {
281 alert(localeStrings.INVALID_TS_RANGE);
286 results = fieldmapper.standardRequest(
287 ["open-ils.booking", "open-ils.booking.reservations.create"],
289 xulG.auth.session.key,
291 reserve_timestamp_range.get_range(),
295 attr_value_table.get_all_values()
299 alert(localeStrings.CREATE_BRESV_LOCAL_ERROR + E);
302 if (is_ils_error(results)) {
303 if (is_ils_actor_card_error(results)) {
304 alert(localeStrings.ACTOR_CARD_NOT_FOUND);
307 localeStrings.CREATE_BRESV_SERVER_ERROR, results
312 alert((missing = check_bresv_targeting(results)) ?
313 localeStrings.CREATE_BRESV_OK_MISSING_TARGET(
314 results.length, missing
316 localeStrings.CREATE_BRESV_OK(results.length)
322 alert(localeStrings.CREATE_BRESV_SERVER_NO_RESPONSE);
326 function flatten_to_dojo_data(obj_list) {
330 "items": obj_list.map(function(o) {
333 "type": o.target_resource_type().name(),
334 "start_time": humanize_timestamp_string(o.start_time()),
335 "end_time": humanize_timestamp_string(o.end_time()),
338 if (o.current_resource())
339 new_obj["resource"] = o.current_resource().barcode();
340 else if (o.target_resource())
341 new_obj["resource"] = "* " + o.target_resource().barcode();
343 new_obj["resource"] = "* " + localeStrings.UNTARGETED + " *";
349 function create_bresv_on_brsrc() {
350 var selector = document.getElementById("brsrc_list");
351 var selected_values = [];
352 for (var i in selector.options) {
353 if (selector.options[i] && selector.options[i].selected)
354 selected_values.push(selector.options[i].value);
356 if (selected_values.length > 0)
357 create_bresv(selected_values);
359 alert(localeStrings.SELECT_A_BRSRC_THEN);
362 function create_bresv_on_brt() {
363 if (any_usable_brsrc())
366 alert(localeStrings.NO_USABLE_BRSRC);
369 function get_actor_by_barcode(barcode) {
370 var usr = fieldmapper.standardRequest(
371 ["open-ils.actor", "open-ils.actor.user.fleshed.retrieve_by_barcode"],
372 [xulG.auth.session.key, barcode]
375 alert(localeStrings.GET_PATRON_NO_RESULT);
376 } else if (is_ils_error(usr)) {
377 return null; /* XXX inelegant: this function is quiet about errors
378 here because to report them would be redundant with
379 another function that gets called right after this one.
386 function init_bresv_grid(barcode) {
387 var result = fieldmapper.standardRequest(
389 "open-ils.booking.reservations.filtered_id_list"
391 [xulG.auth.session.key, {
392 "user_barcode": barcode,
398 }, /* whole_obj */ true]
400 if (result == null) {
401 set_datagrid_empty_store(bresvGrid, flatten_to_dojo_data);
402 alert(localeStrings.GET_BRESV_LIST_NO_RESULT);
403 } else if (is_ils_error(result)) {
404 set_datagrid_empty_store(bresvGrid, flatten_to_dojo_data);
405 if (is_ils_actor_card_error(result)) {
406 alert(localeStrings.ACTOR_CARD_NOT_FOUND);
408 alert(my_ils_error(localeStrings.GET_BRESV_LIST_ERR, result));
411 if (result.length < 1) {
412 document.getElementById("bresv_grid_alt_explanation").innerHTML =
413 localeStrings.NO_EXISTING_BRESV;
414 hide_dom_element(document.getElementById("bresv_grid"));
415 reveal_dom_element(document.getElementById("reserve_under"));
417 document.getElementById("bresv_grid_alt_explanation").innerHTML =
419 reveal_dom_element(document.getElementById("bresv_grid"));
420 reveal_dom_element(document.getElementById("reserve_under"));
422 /* May as well do the following in either case... */
424 new dojo.data.ItemFileReadStore(
425 {"data": flatten_to_dojo_data(result)}
429 for (var i in result) {
430 bresv_index[result[i].id()] = result[i];
435 function cancel_reservations(bresv_id_list) {
436 var result = fieldmapper.standardRequest(
437 ["open-ils.booking", "open-ils.booking.reservations.cancel"],
438 [xulG.auth.session.key, bresv_id_list]
440 setTimeout(update_bresv_grid, 0);
442 alert(localeStrings.CXL_BRESV_FAILURE);
443 } else if (is_ils_error(result)) {
444 alert(my_ils_error(localeStrings.CXL_BRESV_FAILURE2, result));
446 alert(localeStrings.CXL_BRESV_SUCCESS(result.length));
450 function munge_specific_resource(barcode) {
452 var brsrc_list = pcrud.search('brsrc', {'barcode': barcode});
453 if (brsrc_list && brsrc_list.length > 0) {
454 opts.booking_results = {
455 "brt": [[brsrc_list[0].type(), 0, 1]],
456 "brsrc": [[brsrc_list[0].id(), 0, 1]],
458 if (!(our_brt = get_brt_by_id(opts.booking_results.brt[0][0]))) {
459 alert(localeStrings.COULD_NOT_RETRIEVE_BRT_PASSED_IN);
461 init_reservation_interface();
464 alert(localeStrings.BRSRC_NOT_FOUND);
467 alert(localeStrings.BRSRC_RETRIEVE_ERROR + E);
472 * These functions deal with interface tricks (populating widgets,
473 * changing the page, etc.).
475 function init_pickup_lib_selector() {
476 var User = new openils.User();
477 User.buildPermOrgSelector(
478 "ADMIN_BOOKING_RESERVATION", pickup_lib_selector, null,
480 pickup_lib_selected = pickup_lib_selector.getValue();
481 dojo.connect(pickup_lib_selector, "onChange",
483 pickup_lib_selected = this.getValue();
491 function provide_brt_selector(targ_div) {
493 alert(localeStrings.NO_TARG_DIV);
495 brt_list = get_all_noncat_brt();
496 if (!brt_list || brt_list.length < 1) {
497 document.getElementById("select_noncat_brt_block").
498 style.display = "none";
500 var selector = document.createElement("select");
501 selector.setAttribute("id", "brt_selector");
502 selector.setAttribute("name", "brt_selector");
503 /* I'm reluctantly hardcoding this "size" attribute as 8
504 * because you can't accomplish this with CSS anyway.
506 selector.setAttribute("size", 8);
507 for (var i in brt_list) {
508 var option = document.createElement("option");
509 option.setAttribute("value", brt_list[i].id());
510 option.appendChild(document.createTextNode(brt_list[i].name()));
511 selector.appendChild(option);
513 targ_div.innerHTML = "";
514 targ_div.appendChild(selector);
519 function init_resv_iface_arb() {
520 init_reservation_interface(document.getElementById("arbitrary_resource"));
523 function init_resv_iface_sel() {
524 init_reservation_interface(document.getElementById("brt_selector"));
527 function init_reservation_interface(widget) {
528 /* Save a global reference to the brt we're going to reserve */
529 if (widget && (widget.selectedIndex != undefined)) {
530 our_brt = brt_list[widget.selectedIndex];
531 } else if (widget != undefined) {
532 if (!munge_specific_resource(widget.value))
536 /* Hide and reveal relevant divs. */
537 var search_block = document.getElementById("brt_search_block");
538 var reserve_block = document.getElementById("brt_reserve_block");
539 hide_dom_element(search_block);
540 reveal_dom_element(reserve_block);
542 /* Get a list of attributes that can apply to that brt. */
543 var bra_list = pcrud.search("bra", {"resource_type": our_brt.id()});
545 alert(localeString.NO_BRA_LIST);
549 /* Get a table of values that can apply to the above attributes. */
550 var brav_by_bra = {};
551 bra_list.map(function(o) {
552 brav_by_bra[o.id()] = pcrud.search("brav", {"attr": o.id()});
555 /* Hide the label over the attributes widgets if we have nothing to show. */
556 var domf = (bra_list.length < 1) ? hide_dom_element : reveal_dom_element;
557 domf(document.getElementById("bra_and_brav_header"));
559 /* Create DOM widgets to represent each attribute/values set. */
560 for (var i in bra_list) {
561 var bra_div = document.createElement("div");
562 bra_div.setAttribute("class", "nice_vertical_padding");
564 var bra_select = document.createElement("select");
565 bra_select.setAttribute("name", "bra_" + bra_list[i].id());
566 bra_select.setAttribute(
568 "attr_value_table.update_from_selector(this); update_brsrc_list();"
571 var bra_opt_any = document.createElement("option");
572 bra_opt_any.appendChild(document.createTextNode(localeStrings.ANY));
573 bra_opt_any.setAttribute("value", "");
575 bra_select.appendChild(bra_opt_any);
577 var bra_label = document.createElement("label");
578 bra_label.setAttribute("class", "bra");
579 bra_label.appendChild(document.createTextNode(bra_list[i].name()));
581 var j = bra_list[i].id();
582 for (var k in brav_by_bra[j]) {
583 var bra_opt = document.createElement("option");
584 bra_opt.setAttribute("value", brav_by_bra[j][k].id());
586 document.createTextNode(brav_by_bra[j][k].valid_value())
588 bra_select.appendChild(bra_opt);
591 bra_div.appendChild(bra_label);
592 bra_div.appendChild(bra_select);
593 document.getElementById("bra_and_brav").appendChild(bra_div);
595 /* Add a prominent label reminding the user what resource type they're
597 document.getElementById("brsrc_list_header").innerHTML = our_brt.name();
598 init_pickup_lib_selector();
602 function update_brsrc_list() {
603 var brsrc_id_list = get_brsrc_id_list();
604 var force_list = (opts.booking_results && opts.booking_results.brsrc) ?
605 opts.booking_results.brsrc.map(function(o) { return o[0]; }) : [];
607 sync_brsrc_index_from_ids(brsrc_id_list, force_list);
609 var target_selector = document.getElementById("brsrc_list");
610 var selector_memory = new SelectorMemory(target_selector);
611 selector_memory.save();
612 target_selector.innerHTML = "";
614 for (var i in brsrc_index) {
615 if (brsrc_index[i].isdeleted() && (!brsrc_index[i].ischanged()))
618 var opt = document.createElement("option");
619 opt.setAttribute("value", brsrc_index[i].id());
620 opt.appendChild(document.createTextNode(brsrc_index[i].barcode()));
622 if (brsrc_index[i].isdeleted() && (brsrc_index[i].ischanged())) {
623 opt.setAttribute("class", "forced_unavailable");
624 opt.setAttribute("disabled", "disabled");
627 target_selector.appendChild(opt);
630 selector_memory.restore();
633 function any_usable_brsrc() {
634 for (var i in brsrc_index) {
635 if (!brsrc_index[i].isdeleted())
641 function update_bresv_grid() {
642 var widg = document.getElementById("patron_barcode");
643 if (widg.value != "") {
644 setTimeout(function() {
645 var target = document.getElementById(
646 "existing_reservation_patron_line"
648 var patron = get_actor_by_barcode(widg.value);
651 localeStrings.HERE_ARE_EXISTING_BRESV + " " +
652 formal_name(patron) + ": "
655 target.innerHTML = "";
658 setTimeout(function() { init_bresv_grid(widg.value); }, 0);
662 function init_timestamp_widgets() {
663 var when = ["start", "end"];
664 for (var i in when) {
665 reserve_timestamp_range.update_from_widget(
666 new dijit.form.TimeTextBox({
667 name: "reserve_time_" + when[i],
670 timePattern: "HH:mm",
671 clickableIncrement: "T00:15:00",
672 visibleIncrement: "T00:15:00",
673 visibleRange: "T01:30:00",
675 onChange: function() {
676 reserve_timestamp_range.update_from_widget(this);
679 }, "reserve_time_" + when[i])
681 reserve_timestamp_range.update_from_widget(
682 new dijit.form.DateTextBox({
683 name: "reserve_date_" + when[i],
685 onChange: function() {
686 reserve_timestamp_range.update_from_widget(this);
689 }, "reserve_date_" + when[i])
694 function cancel_selected_bresv(bresv_dojo_items) {
695 if (bresv_dojo_items && bresv_dojo_items.length > 0) {
697 bresv_dojo_items.map(function(o) { return o.id[0]; })
699 /* After some delay to allow the cancellations a chance to get
700 * committed, refresh the brsrc list as it might reflect newly
701 * available resources now. */
702 setTimeout(update_brsrc_list, 2000);
704 alert(localeStrings.CXL_BRESV_SELECT_SOMETHING);
708 /* The following function should return true if the reservation interface
709 * should start normally (show a list of brt to choose from) or false if
710 * it should not (because we've "started" it some other way by setting up
711 * and displaying other widgets).
713 function early_action_passthru() {
714 if (opts.booking_results) {
715 if (opts.booking_results.brt.length != 1) {
716 alert(localeStrings.NEED_EXACTLY_ONE_BRT_PASSED_IN);
718 } else if (!(our_brt = get_brt_by_id(opts.booking_results.brt[0][0]))) {
719 alert(localeStrings.COULD_NOT_RETRIEVE_BRT_PASSED_IN);
723 init_reservation_interface();
727 if (opts.patron_barcode) {
728 document.getElementById("contain_patron_barcode").style.display="none";
729 document.getElementById("patron_barcode").value = opts.patron_barcode;
740 hide_dom_element(document.getElementById("brt_reserve_block"));
741 reveal_dom_element(document.getElementById("brt_search_block"));
742 hide_dom_element(document.getElementById("reserve_under"));
743 init_auto_l10n(document.getElementById("auto_l10n_start_here"));
744 init_timestamp_widgets();
746 if (!(opts = xulG.bresv_interface_opts)) opts = {};
747 if (early_action_passthru())
748 provide_brt_selector(document.getElementById("brt_selector_here"));