4 dojo.require("fieldmapper.OrgUtils");
5 dojo.require("openils.PermaCrud");
6 dojo.require("dojo.data.ItemFileReadStore");
7 dojo.require("dijit.form.DateTextBox");
8 dojo.require("dijit.form.TimeTextBox");
9 dojo.requireLocalization("openils.booking", "reservation");
12 * Globals; prototypes and their instances
14 var localeStrings = dojo.i18n.getLocalization("openils.booking", "reservation");
15 var pcrud = new openils.PermaCrud();
21 var just_reserved_now = {};
23 function AttrValueTable() { this.t = {}; }
24 AttrValueTable.prototype.set = function(attr, value) { this.t[attr] = value; };
25 AttrValueTable.prototype.update_from_selector = function(selector) {
26 var attr = selector.name.match(/_(\d+)$/)[1];
27 var value = selector.options[selector.selectedIndex].value;
29 attr_value_table.set(attr, value);
31 AttrValueTable.prototype.get_all_values = function() {
33 for (var k in this.t) {
34 if (this.t[k] != undefined && this.t[k] != "")
35 values.push(this.t[k]);
39 var attr_value_table = new AttrValueTable();
41 function TimestampRange() {
42 this.start = new Date();
43 this.end = new Date();
45 this.validity = {"start": false, "end": false};
47 "start": {"date": undefined, "time": undefined},
48 "end": {"date": undefined, "time": undefined}
50 this.saved_style_properties = {};
51 this.invalid_style_properties = {
52 "backgroundColor": "#ffcccc",
54 "borderColor": "#990000",
58 TimestampRange.prototype.get_timestamp = function(when) {
59 return this.any_widget.serialize(this[when]).
60 replace("T", " ").substr(0, 19);
62 TimestampRange.prototype.get_range = function() {
63 return this.is_backwards() ?
64 [this.get_timestamp("end"), this.get_timestamp("start")] :
65 [this.get_timestamp("start"), this.get_timestamp("end")];
67 TimestampRange.prototype.update_from_widget = function(widget) {
68 var when = widget.id.match(/(start|end)/)[1];
69 var which = widget.id.match(/(date|time)/)[1];
71 if (this.any_widget == undefined)
72 this.any_widget = widget;
73 if (this.nodes[when][which] == undefined)
74 this.nodes[when][which] = widget.domNode; /* We'll need this later */
77 this.update_timestamp(when, which, widget.value);
80 this.compute_validity();
81 this.paint_validity();
83 TimestampRange.prototype.compute_validity = function() {
84 if (Math.abs(this.start - this.end) < 1000) {
85 this.validity.end = false;
87 if (this.start < this.current_minimum())
88 this.validity.start = false;
90 this.validity.start = true;
92 if (this.end < this.current_minimum())
93 this.validity.end = false;
95 this.validity.end = true;
98 /* This method provides the minimum timestamp that is considered valid. For
99 * now it's arbitrarily "now + 15 minutes", meaning that all reservations
100 * must be made at least 15 minutes in the future.
102 * For reasons of keeping the middle layer happy, this should always return
103 * a time that is at least somewhat in the future. The ML isn't able to target
104 * any resources for a reservation with a start date that isn't in the future.
106 TimestampRange.prototype.current_minimum = function() {
107 /* XXX This is going to be a problem with local clocks that are off. */
109 n.setTime(n.getTime() + 1000 * 900); /* XXX 15 minutes; stop hardcoding! */
112 TimestampRange.prototype.update_timestamp = function(when, which, value) {
113 if (which == "date") {
114 this[when].setFullYear(value.getFullYear());
115 this[when].setMonth(value.getMonth());
116 this[when].setDate(value.getDate());
117 } else { /* "time" */
118 this[when].setHours(value.getHours());
119 this[when].setMinutes(value.getMinutes());
120 this[when].setSeconds(0);
123 TimestampRange.prototype.is_backwards = function() {
124 return (this.start > this.end);
126 TimestampRange.prototype.paint_validity = function() {
127 for (var when in this.validity) {
128 if (this.validity[when]) {
129 this.paint_valid_node(this.nodes[when].date);
130 this.paint_valid_node(this.nodes[when].time);
132 this.paint_invalid_node(this.nodes[when].date);
133 this.paint_invalid_node(this.nodes[when].time);
137 TimestampRange.prototype.paint_invalid_node = function(node) {
139 /* Just toggling the class of something would be better than
140 * manually setting style here, but I haven't been able to get that
141 * to play nicely with dojo's styling of the date/time textboxen.
143 if (this.saved_style_properties.backgroundColor == undefined) {
144 for (var k in this.invalid_style_properties) {
145 this.saved_style_properties[k] = node.style[k];
148 for (var k in this.invalid_style_properties) {
149 node.style[k] = this.invalid_style_properties[k];
153 TimestampRange.prototype.paint_valid_node = function(node) {
155 for (var k in this.saved_style_properties) {
156 node.style[k] = this.saved_style_properties[k];
160 TimestampRange.prototype.is_valid = function() {
161 return (this.validity.start && this.validity.end);
163 var reserve_timestamp_range = new TimestampRange();
165 function SelectorMemory(selector) {
166 this.selector = selector;
169 SelectorMemory.prototype.save = function() {
170 for (var i = 0; i < this.selector.options.length; i++) {
171 if (this.selector.options[i].selected) {
172 this.memory[this.selector.options[i].value] = true;
176 SelectorMemory.prototype.restore = function() {
177 for (var i = 0; i < this.selector.options.length; i++) {
178 if (this.memory[this.selector.options[i].value]) {
179 if (!this.selector.options[i].disabled)
180 this.selector.options[i].selected = true;
186 * Misc helper functions
188 function set_datagrid_empty_store(grid) {
190 new dojo.data.ItemFileReadStore(
191 {"data": flatten_to_dojo_data([])}
197 * These functions communicate with the middle layer.
199 function get_all_noncat_brt() {
200 return pcrud.search("brt",
201 {"id": {"!=": null}, "catalog_item": "f"},
202 {"order_by": {"brt":"name"}}
206 function get_brt_by_id(id) {
207 return pcrud.retrieve("brt", id);
210 function get_brsrc_id_list() {
211 var options = {"type": our_brt.id()};
213 /* This mechanism for avoiding the passing of an empty 'attribute_values'
214 * option is essential because if you pass such an option to the
215 * middle layer API at all, it won't return any IDs for brsrcs that
216 * don't have at least one attribute of some kind.
218 var attribute_values = attr_value_table.get_all_values();
219 if (attribute_values.length > 0)
220 options.attribute_values = attribute_values;
222 options.available = reserve_timestamp_range.get_range();
224 return fieldmapper.standardRequest(
225 ["open-ils.booking", "open-ils.booking.resources.filtered_id_list"],
226 [xulG.auth.session.key, options]
230 /* FIXME: We need failure checking after pcrud.retrieve() */
231 function add_brsrc_to_index_if_needed(list, further) {
232 for (var i in list) {
233 if (!brsrc_index[list[i]]) {
234 brsrc_index[list[i]] = pcrud.retrieve("brsrc", list[i]);
237 further(brsrc_index[list[i]]);
241 function sync_brsrc_index_from_ids(available_list, additional_list) {
242 /* Default states for everything in the index. Read the further comments. */
243 for (var i in brsrc_index) {
244 brsrc_index[i].isdeleted(true);
245 brsrc_index[i].ischanged(false);
248 /* Populate the cache with anything that's missing and tag everything
249 * in the "available" list as *not* deleted, and tag everything in the
250 * additional list as "changed." See below. */
251 add_brsrc_to_index_if_needed(
252 available_list, function(o) { o.isdeleted(false); }
254 add_brsrc_to_index_if_needed(
257 if (!(o.id() in just_reserved_now)) o.ischanged(true);
260 /* NOTE: We lightly abuse the isdeleted() and ischanged() magic fieldmapper
261 * attributes of the brsrcs in our cache. Because we're not going to
262 * pass back any brsrcs to the middle layer, it doesn't really matter
263 * what we set this attribute to. What we're using it for is to indicate
264 * in our little brsrc cache how a given brsrc should be displayed in this
265 * UI's current state (based on whether the brsrc matches timestamp range
266 * availability (isdeleted(false)) and whether the brsrc has been forced
267 * into the list because it was selected in a previous interface (like
268 * the catalog) (ischanged(true))).
272 function check_bresv_targeting(results) {
274 for (var i in results) {
275 if (!(results[i].targeting && results[i].targeting.current_resource)) {
278 just_reserved_now[results[i].targeting.current_resource] = true;
284 function create_bresv(resource_list) {
285 var barcode = document.getElementById("patron_barcode").value;
287 alert(localeStrings.WHERES_THE_BARCODE);
289 } else if (!reserve_timestamp_range.is_valid()) {
290 alert(localeStrings.INVALID_TS_RANGE);
295 results = fieldmapper.standardRequest(
296 ["open-ils.booking", "open-ils.booking.reservations.create"],
298 xulG.auth.session.key,
300 reserve_timestamp_range.get_range(),
303 attr_value_table.get_all_values()
307 alert(localeStrings.CREATE_BRESV_LOCAL_ERROR + E);
310 if (is_ils_error(results)) {
311 if (is_ils_actor_card_error(results)) {
312 alert(localeStrings.ACTOR_CARD_NOT_FOUND);
315 localeStrings.CREATE_BRESV_SERVER_ERROR, results
320 alert((missing = check_bresv_targeting(results)) ?
321 localeStrings.CREATE_BRESV_OK_MISSING_TARGET(
322 results.length, missing
324 localeStrings.CREATE_BRESV_OK(results.length)
330 alert(localeStrings.CREATE_BRESV_SERVER_NO_RESPONSE);
334 function flatten_to_dojo_data(obj_list) {
338 "items": obj_list.map(function(o) {
341 "type": o.target_resource_type().name(),
342 "start_time": humanize_timestamp_string(o.start_time()),
343 "end_time": humanize_timestamp_string(o.end_time()),
346 if (o.current_resource())
347 new_obj["resource"] = o.current_resource().barcode();
348 else if (o.target_resource())
349 new_obj["resource"] = "* " + o.target_resource().barcode();
351 new_obj["resource"] = "* " + localeStrings.UNTARGETED + " *";
357 function create_bresv_on_brsrc() {
358 var selector = document.getElementById("brsrc_list");
359 var selected_values = [];
360 for (var i in selector.options) {
361 if (selector.options[i].selected)
362 selected_values.push(selector.options[i].value);
364 if (selected_values.length > 0)
365 create_bresv(selected_values);
367 alert(localeStrings.SELECT_A_BRSRC_THEN);
370 function create_bresv_on_brt() {
371 if (any_usable_brsrc())
374 alert(localeStrings.NO_USABLE_BRSRC);
377 function get_actor_by_barcode(barcode) {
378 var usr = fieldmapper.standardRequest(
379 ["open-ils.actor", "open-ils.actor.user.fleshed.retrieve_by_barcode"],
380 [xulG.auth.session.key, barcode]
383 alert(localeStrings.GET_PATRON_NO_RESULT);
384 } else if (is_ils_error(usr)) {
385 return null; /* XXX inelegant: this function is quiet about errors
386 here because to report them would be redundant with
387 another function that gets called right after this one.
394 function init_bresv_grid(barcode) {
395 var result = fieldmapper.standardRequest(
397 "open-ils.booking.reservations.filtered_id_list"
399 [xulG.auth.session.key, {
400 "user_barcode": barcode,
406 }, /* whole_obj */ true]
408 if (result == null) {
409 set_datagrid_empty_store(bresvGrid);
410 alert(localeStrings.GET_BRESV_LIST_NO_RESULT);
411 } else if (is_ils_error(result)) {
412 set_datagrid_empty_store(bresvGrid);
413 if (is_ils_actor_card_error(result)) {
414 alert(localeStrings.ACTOR_CARD_NOT_FOUND);
416 alert(my_ils_error(localeStrings.GET_BRESV_LIST_ERR, result));
419 if (result.length < 1) {
420 document.getElementById("bresv_grid_alt_explanation").innerHTML =
421 localeStrings.NO_EXISTING_BRESV;
422 hide_dom_element(document.getElementById("bresv_grid"));
423 reveal_dom_element(document.getElementById("reserve_under"));
425 document.getElementById("bresv_grid_alt_explanation").innerHTML =
427 reveal_dom_element(document.getElementById("bresv_grid"));
428 reveal_dom_element(document.getElementById("reserve_under"));
430 /* May as well do the following in either case... */
432 new dojo.data.ItemFileReadStore(
433 {"data": flatten_to_dojo_data(result)}
437 for (var i in result) {
438 bresv_index[result[i].id()] = result[i];
443 function cancel_reservations(bresv_list) {
444 for (var i in bresv_list) { bresv_list[i].cancel_time("now"); }
447 "oncomplete": function() {
449 alert(localeStrings.CXL_BRESV_SUCCESS(bresv_list.length));
451 "onerror": function(o) {
453 alert(localeStrings.CXL_BRESV_FAILURE + "\n" + o);
459 function munge_specific_resource(barcode) {
461 var brsrc_list = pcrud.search('brsrc', {'barcode': barcode});
462 if (brsrc_list && brsrc_list.length > 0) {
463 opts.booking_results = {
464 "brt": [[brsrc_list[0].type(), 0, 1]],
465 "brsrc": [[brsrc_list[0].id(), 0, 1]],
467 if (!(our_brt = get_brt_by_id(opts.booking_results.brt[0][0]))) {
468 alert(localeStrings.COULD_NOT_RETRIEVE_BRT_PASSED_IN);
470 init_reservation_interface();
473 alert(localeStrings.BRSRC_NOT_FOUND);
476 alert(localeStrings.BRSRC_RETRIEVE_ERROR + E);
481 * These functions deal with interface tricks (populating widgets,
482 * changing the page, etc.).
484 function provide_brt_selector(targ_div) {
486 alert(localeStrings.NO_TARG_DIV);
488 brt_list = get_all_noncat_brt();
489 if (!brt_list || brt_list.length < 1) {
490 document.getElementById("select_noncat_brt_block").
491 style.display = "none";
493 var selector = document.createElement("select");
494 selector.setAttribute("id", "brt_selector");
495 selector.setAttribute("name", "brt_selector");
496 /* I'm reluctantly hardcoding this "size" attribute as 8
497 * because you can't accomplish this with CSS anyway.
499 selector.setAttribute("size", 8);
500 for (var i in brt_list) {
501 var option = document.createElement("option");
502 option.setAttribute("value", brt_list[i].id());
503 option.appendChild(document.createTextNode(brt_list[i].name()));
504 selector.appendChild(option);
506 targ_div.innerHTML = "";
507 targ_div.appendChild(selector);
512 function init_resv_iface_arb() {
513 init_reservation_interface(document.getElementById("arbitrary_resource"));
516 function init_resv_iface_sel() {
517 init_reservation_interface(document.getElementById("brt_selector"));
520 function init_reservation_interface(widget) {
521 /* Save a global reference to the brt we're going to reserve */
522 if (widget && (widget.selectedIndex != undefined)) {
523 our_brt = brt_list[widget.selectedIndex];
524 } else if (widget != undefined) {
525 if (!munge_specific_resource(widget.value))
529 /* Hide and reveal relevant divs. */
530 var search_block = document.getElementById("brt_search_block");
531 var reserve_block = document.getElementById("brt_reserve_block");
532 hide_dom_element(search_block);
533 reveal_dom_element(reserve_block);
535 /* Get a list of attributes that can apply to that brt. */
536 var bra_list = pcrud.search("bra", {"resource_type": our_brt.id()});
538 alert(localeString.NO_BRA_LIST);
542 /* Get a table of values that can apply to the above attributes. */
543 var brav_by_bra = {};
544 bra_list.map(function(o) {
545 brav_by_bra[o.id()] = pcrud.search("brav", {"attr": o.id()});
548 /* Hide the label over the attributes widgets if we have nothing to show. */
549 var domf = (bra_list.length < 1) ? hide_dom_element : reveal_dom_element;
550 domf(document.getElementById("bra_and_brav_header"));
552 /* Create DOM widgets to represent each attribute/values set. */
553 for (var i in bra_list) {
554 var bra_div = document.createElement("div");
555 bra_div.setAttribute("class", "nice_vertical_padding");
557 var bra_select = document.createElement("select");
558 bra_select.setAttribute("name", "bra_" + bra_list[i].id());
559 bra_select.setAttribute(
561 "attr_value_table.update_from_selector(this); update_brsrc_list();"
564 var bra_opt_any = document.createElement("option");
565 bra_opt_any.appendChild(document.createTextNode(localeStrings.ANY));
566 bra_opt_any.setAttribute("value", "");
568 bra_select.appendChild(bra_opt_any);
570 var bra_label = document.createElement("label");
571 bra_label.setAttribute("class", "bra");
572 bra_label.appendChild(document.createTextNode(bra_list[i].name()));
574 var j = bra_list[i].id();
575 for (var k in brav_by_bra[j]) {
576 var bra_opt = document.createElement("option");
577 bra_opt.setAttribute("value", brav_by_bra[j][k].id());
579 document.createTextNode(brav_by_bra[j][k].valid_value())
581 bra_select.appendChild(bra_opt);
584 bra_div.appendChild(bra_label);
585 bra_div.appendChild(bra_select);
586 document.getElementById("bra_and_brav").appendChild(bra_div);
588 /* Add a prominent label reminding the user what resource type they're
590 document.getElementById("brsrc_list_header").innerHTML = our_brt.name();
594 function update_brsrc_list() {
595 var brsrc_id_list = get_brsrc_id_list();
596 var force_list = (opts.booking_results && opts.booking_results.brsrc) ?
597 opts.booking_results.brsrc.map(function(o) { return o[0]; }) : [];
599 sync_brsrc_index_from_ids(brsrc_id_list, force_list);
601 var target_selector = document.getElementById("brsrc_list");
602 var selector_memory = new SelectorMemory(target_selector);
603 selector_memory.save();
604 target_selector.innerHTML = "";
606 for (var i in brsrc_index) {
607 if (brsrc_index[i].isdeleted() && (!brsrc_index[i].ischanged()))
610 var opt = document.createElement("option");
611 opt.setAttribute("value", brsrc_index[i].id());
612 opt.appendChild(document.createTextNode(brsrc_index[i].barcode()));
614 if (brsrc_index[i].isdeleted() && (brsrc_index[i].ischanged())) {
615 opt.setAttribute("class", "forced_unavailable");
616 opt.setAttribute("disabled", "disabled");
619 target_selector.appendChild(opt);
622 selector_memory.restore();
625 function any_usable_brsrc() {
626 for (var i in brsrc_index) {
627 if (!brsrc_index[i].isdeleted())
633 function update_bresv_grid() {
634 var widg = document.getElementById("patron_barcode");
635 if (widg.value != "") {
636 setTimeout(function() {
637 var target = document.getElementById(
638 "existing_reservation_patron_line"
640 var patron = get_actor_by_barcode(widg.value);
643 localeStrings.HERE_ARE_EXISTING_BRESV + " " +
644 formal_name(patron) + ": "
647 target.innerHTML = "";
650 setTimeout(function() { init_bresv_grid(widg.value); }, 0);
654 function init_timestamp_widgets() {
655 var when = ["start", "end"];
656 for (var i in when) {
657 reserve_timestamp_range.update_from_widget(
658 new dijit.form.TimeTextBox({
659 name: "reserve_time_" + when[i],
662 timePattern: "HH:mm",
663 clickableIncrement: "T00:15:00",
664 visibleIncrement: "T00:15:00",
665 visibleRange: "T01:30:00",
667 onChange: function() {
668 reserve_timestamp_range.update_from_widget(this);
671 }, "reserve_time_" + when[i])
673 reserve_timestamp_range.update_from_widget(
674 new dijit.form.DateTextBox({
675 name: "reserve_date_" + when[i],
677 onChange: function() {
678 reserve_timestamp_range.update_from_widget(this);
681 }, "reserve_date_" + when[i])
686 function cancel_selected_bresv(bresv_dojo_items) {
687 if (bresv_dojo_items && bresv_dojo_items.length > 0) {
689 bresv_dojo_items.map(function(o) { return bresv_index[o.id]; })
691 /* After some delay to allow the cancellations a chance to get
692 * committed, refresh the brsrc list as it might reflect newly
693 * available resources now. */
694 setTimeout(update_brsrc_list, 2000);
696 alert(localeStrings.CXL_BRESV_SELECT_SOMETHING);
700 /* The following function should return true if the reservation interface
701 * should start normally (show a list of brt to choose from) or false if
702 * it should not (because we've "started" it some other way by setting up
703 * and displaying other widgets).
705 function early_action_passthru() {
706 if (opts.booking_results) {
707 if (opts.booking_results.brt.length != 1) {
708 alert(localeStrings.NEED_EXACTLY_ONE_BRT_PASSED_IN);
710 } else if (!(our_brt = get_brt_by_id(opts.booking_results.brt[0][0]))) {
711 alert(localeStrings.COULD_NOT_RETRIEVE_BRT_PASSED_IN);
715 init_reservation_interface();
719 if (opts.patron_barcode) {
720 document.getElementById("contain_patron_barcode").style.display="none";
721 document.getElementById("patron_barcode").value = opts.patron_barcode;
732 hide_dom_element(document.getElementById("brt_reserve_block"));
733 reveal_dom_element(document.getElementById("brt_search_block"));
734 hide_dom_element(document.getElementById("reserve_under"));
735 init_auto_l10n(document.getElementById("auto_l10n_start_here"));
736 init_timestamp_widgets();
738 if (!(opts = xulG.bresv_interface_opts)) opts = {};
739 if (early_action_passthru())
740 provide_brt_selector(document.getElementById("brt_selector_here"));