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();
20 var just_reserved_now = {};
22 function AttrValueTable() { this.t = {}; }
23 AttrValueTable.prototype.set = function(attr, value) { this.t[attr] = value; };
24 AttrValueTable.prototype.update_from_selector = function(selector) {
25 var attr = selector.name.match(/_(\d+)$/)[1];
26 var value = selector.options[selector.selectedIndex].value;
28 attr_value_table.set(attr, value);
30 AttrValueTable.prototype.get_all_values = function() {
32 for (var k in this.t) {
33 if (this.t[k] != undefined && this.t[k] != "")
34 values.push(this.t[k]);
38 var attr_value_table = new AttrValueTable();
40 function TimestampRange() {
41 this.start = new Date();
42 this.end = new Date();
44 this.validity = {"start": false, "end": false};
46 "start": {"date": undefined, "time": undefined},
47 "end": {"date": undefined, "time": undefined}
49 this.saved_style_properties = {};
50 this.invalid_style_properties = {
51 "backgroundColor": "#ffcccc",
53 "borderColor": "#990000",
57 TimestampRange.prototype.get_timestamp = function(when) {
58 return this.any_widget.serialize(this[when]).
59 replace("T", " ").substr(0, 19);
61 TimestampRange.prototype.get_range = function() {
62 return this.is_backwards() ?
63 [this.get_timestamp("end"), this.get_timestamp("start")] :
64 [this.get_timestamp("start"), this.get_timestamp("end")];
66 TimestampRange.prototype.update_from_widget = function(widget) {
67 var when = widget.id.match(/(start|end)/)[1];
68 var which = widget.id.match(/(date|time)/)[1];
70 if (this.any_widget == undefined)
71 this.any_widget = widget;
72 if (this.nodes[when][which] == undefined)
73 this.nodes[when][which] = widget.domNode; /* We'll need this later */
76 this.update_timestamp(when, which, widget.value);
79 this.compute_validity();
80 this.paint_validity();
82 TimestampRange.prototype.compute_validity = function() {
83 if (Math.abs(this.start - this.end) < 1000) {
84 this.validity.end = false;
86 if (this.start < this.current_minimum())
87 this.validity.start = false;
89 this.validity.start = true;
91 if (this.end < this.current_minimum())
92 this.validity.end = false;
94 this.validity.end = true;
97 /* This method provides the minimum timestamp that is considered valid. For
98 * now it's arbitrarily "now + 15 minutes", meaning that all reservations
99 * must be made at least 15 minutes in the future.
101 * For reasons of keeping the middle layer happy, this should always return
102 * a time that is at least somewhat in the future. The ML isn't able to target
103 * any resources for a reservation with a start date that isn't in the future.
105 TimestampRange.prototype.current_minimum = function() {
106 /* XXX This is going to be a problem with local clocks that are off. */
108 n.setTime(n.getTime() + 1000 * 900); /* XXX 15 minutes; stop hardcoding! */
111 TimestampRange.prototype.update_timestamp = function(when, which, value) {
112 if (which == "date") {
113 this[when].setFullYear(value.getFullYear());
114 this[when].setMonth(value.getMonth());
115 this[when].setDate(value.getDate());
116 } else { /* "time" */
117 this[when].setHours(value.getHours());
118 this[when].setMinutes(value.getMinutes());
119 this[when].setSeconds(0);
122 TimestampRange.prototype.is_backwards = function() {
123 return (this.start > this.end);
125 TimestampRange.prototype.paint_validity = function() {
126 for (var when in this.validity) {
127 if (this.validity[when]) {
128 this.paint_valid_node(this.nodes[when].date);
129 this.paint_valid_node(this.nodes[when].time);
131 this.paint_invalid_node(this.nodes[when].date);
132 this.paint_invalid_node(this.nodes[when].time);
136 TimestampRange.prototype.paint_invalid_node = function(node) {
138 /* Just toggling the class of something would be better than
139 * manually setting style here, but I haven't been able to get that
140 * to play nicely with dojo's styling of the date/time textboxen.
142 if (this.saved_style_properties.backgroundColor == undefined) {
143 for (var k in this.invalid_style_properties) {
144 this.saved_style_properties[k] = node.style[k];
147 for (var k in this.invalid_style_properties) {
148 node.style[k] = this.invalid_style_properties[k];
152 TimestampRange.prototype.paint_valid_node = function(node) {
154 for (var k in this.saved_style_properties) {
155 node.style[k] = this.saved_style_properties[k];
159 TimestampRange.prototype.is_valid = function() {
160 return (this.validity.start && this.validity.end);
162 var reserve_timestamp_range = new TimestampRange();
164 function SelectorMemory(selector) {
165 this.selector = selector;
168 SelectorMemory.prototype.save = function() {
169 for (var i = 0; i < this.selector.options.length; i++) {
170 if (this.selector.options[i].selected) {
171 this.memory[this.selector.options[i].value] = true;
175 SelectorMemory.prototype.restore = function() {
176 for (var i = 0; i < this.selector.options.length; i++) {
177 if (this.memory[this.selector.options[i].value]) {
178 if (!this.selector.options[i].disabled)
179 this.selector.options[i].selected = true;
185 * Misc helper functions
187 function hide_dom_element(e) { e.style.display = "none"; };
188 function reveal_dom_element(e) { e.style.display = ""; };
189 function get_keys(L) { var K = []; for (var k in L) K.push(k); return K; }
190 function formal_name(u) {
191 var name = u.family_name() + ", " + u.first_given_name();
192 if (u.second_given_name())
193 name += (" " + u.second_given_name());
196 function humanize_timestamp_string(ts) {
197 /* For now, this discards time zones. */
198 var parts = ts.split("T");
199 var timeparts = parts[1].split("-")[0].split(":");
200 return parts[0] + " " + timeparts[0] + ":" + timeparts[1];
202 function set_datagrid_empty_store(grid) {
204 new dojo.data.ItemFileReadStore(
205 {"data": flatten_to_dojo_data([])}
209 function is_ils_error(e) { return (e.ilsevent != undefined); }
210 function is_ils_actor_card_error(e) {
211 return (e.textcode == "ACTOR_CARD_NOT_FOUND");
213 function my_ils_error(header, e) {
214 var s = header + "\n";
216 "ilsevent", "desc", "textcode", "servertime", "pid", "stacktrace"
218 for (var i in keys) {
219 if (e[keys[i]]) s += ("\t" + keys[i] + ": " + e[keys[i]] + "\n");
225 * These functions communicate with the middle layer.
227 function get_all_noncat_brt() {
228 return pcrud.search("brt",
229 {"id": {"!=": null}, "catalog_item": "f"},
230 {"order_by": {"brt":"name"}}
234 function get_brt_by_id(id) {
235 return pcrud.retrieve("brt", id);
238 function get_brsrc_id_list() {
239 var options = {"type": our_brt.id()};
241 /* This mechanism for avoiding the passing of an empty 'attribute_values'
242 * option is essential because if you pass such an option to the
243 * middle layer API at all, it won't return any IDs for brsrcs that
244 * don't have at least one attribute of some kind.
246 var attribute_values = attr_value_table.get_all_values();
247 if (attribute_values.length > 0)
248 options.attribute_values = attribute_values;
250 options.available = reserve_timestamp_range.get_range();
252 return fieldmapper.standardRequest(
253 ["open-ils.booking", "open-ils.booking.resources.filtered_id_list"],
254 [xulG.auth.session.key, options]
258 /* FIXME: We need failure checking after pcrud.retrieve() */
259 function add_brsrc_to_index_if_needed(list, further) {
260 for (var i in list) {
261 if (!brsrc_index[list[i]]) {
262 brsrc_index[list[i]] = pcrud.retrieve("brsrc", list[i]);
265 further(brsrc_index[list[i]]);
269 function sync_brsrc_index_from_ids(available_list, additional_list) {
270 /* Default states for everything in the index. Read the further comments. */
271 for (var i in brsrc_index) {
272 brsrc_index[i].isdeleted(true);
273 brsrc_index[i].ischanged(false);
276 /* Populate the cache with anything that's missing and tag everything
277 * in the "available" list as *not* deleted, and tag everything in the
278 * additional list as "changed." See below. */
279 add_brsrc_to_index_if_needed(
280 available_list, function(o) { o.isdeleted(false); }
282 add_brsrc_to_index_if_needed(
285 if (!(o.id() in just_reserved_now)) o.ischanged(true);
288 /* NOTE: We lightly abuse the isdeleted() and ischanged() magic fieldmapper
289 * attributes of the brsrcs in our cache. Because we're not going to
290 * pass back any brsrcs to the middle layer, it doesn't really matter
291 * what we set this attribute to. What we're using it for is to indicate
292 * in our little brsrc cache how a given brsrc should be displayed in this
293 * UI's current state (based on whether the brsrc matches timestamp range
294 * availability (isdeleted(false)) and whether the brsrc has been forced
295 * into the list because it was selected in a previous interface (like
296 * the catalog) (ischanged(true))).
300 function check_bresv_targeting(results) {
302 for (var i in results) {
303 if (!(results[i].targeting && results[i].targeting.current_resource)) {
306 just_reserved_now[results[i].targeting.current_resource] = true;
312 function create_bresv(resource_list) {
313 var barcode = document.getElementById("patron_barcode").value;
315 alert(localeStrings.WHERES_THE_BARCODE);
317 } else if (!reserve_timestamp_range.is_valid()) {
318 alert(localeStrings.INVALID_TS_RANGE);
323 results = fieldmapper.standardRequest(
324 ["open-ils.booking", "open-ils.booking.reservations.create"],
326 xulG.auth.session.key,
328 reserve_timestamp_range.get_range(),
331 attr_value_table.get_all_values()
335 alert(localeStrings.CREATE_BRESV_LOCAL_ERROR + E);
338 if (is_ils_error(results)) {
339 if (is_ils_actor_card_error(results)) {
340 alert(localeStrings.ACTOR_CARD_NOT_FOUND);
343 localeStrings.CREATE_BRESV_SERVER_ERROR, results
348 alert((missing = check_bresv_targeting(results)) ?
349 localeStrings.CREATE_BRESV_OK_MISSING_TARGET(
350 results.length, missing
352 localeStrings.CREATE_BRESV_OK(results.length)
358 alert(localeStrings.CREATE_BRESV_SERVER_NO_RESPONSE);
362 function flatten_to_dojo_data(obj_list) {
366 "items": obj_list.map(function(o) {
369 "type": o.target_resource_type().name(),
370 "start_time": humanize_timestamp_string(o.start_time()),
371 "end_time": humanize_timestamp_string(o.end_time()),
374 if (o.current_resource())
375 new_obj["resource"] = o.current_resource().barcode();
376 else if (o.target_resource())
377 new_obj["resource"] = "* " + o.target_resource().barcode();
379 new_obj["resource"] = "* " + localeStrings.UNTARGETED + " *";
385 function create_bresv_on_brsrc() {
386 var selector = document.getElementById("brsrc_list");
387 var selected_values = [];
388 for (var i in selector.options) {
389 if (selector.options[i].selected)
390 selected_values.push(selector.options[i].value);
392 if (selected_values.length > 0)
393 create_bresv(selected_values);
395 alert(localeStrings.SELECT_A_BRSRC_THEN);
398 function create_bresv_on_brt() {
399 if (any_usable_brsrc())
402 alert(localeStrings.NO_USABLE_BRSRC);
405 function get_actor_by_barcode(barcode) {
406 var usr = fieldmapper.standardRequest(
407 ["open-ils.actor", "open-ils.actor.user.fleshed.retrieve_by_barcode"],
408 [xulG.auth.session.key, barcode]
411 alert(localeStrings.GET_PATRON_NO_RESULT);
412 } else if (is_ils_error(usr)) {
413 return null; /* XXX inelegant: this function is quiet about errors
414 here because to report them would be redundant with
415 another function that gets called right after this one.
422 function init_bresv_grid(barcode) {
423 var result = fieldmapper.standardRequest(
425 "open-ils.booking.reservations.filtered_id_list"
427 [xulG.auth.session.key, {
428 "user_barcode": barcode,
434 }, /* whole_obj */ true]
436 if (result == null) {
437 set_datagrid_empty_store(bresvGrid);
438 alert(localeStrings.GET_BRESV_LIST_NO_RESULT);
439 } else if (is_ils_error(result)) {
440 set_datagrid_empty_store(bresvGrid);
441 if (is_ils_actor_card_error(result)) {
442 alert(localeStrings.ACTOR_CARD_NOT_FOUND);
444 alert(my_ils_error(localeStrings.GET_BRESV_LIST_ERR, result));
447 if (result.length < 1) {
448 document.getElementById("bresv_grid_alt_explanation").innerHTML =
449 localeStrings.NO_EXISTING_BRESV;
450 hide_dom_element(document.getElementById("bresv_grid"));
451 reveal_dom_element(document.getElementById("reserve_under"));
453 document.getElementById("bresv_grid_alt_explanation").innerHTML =
455 reveal_dom_element(document.getElementById("bresv_grid"));
456 reveal_dom_element(document.getElementById("reserve_under"));
458 /* May as well do the following in either case... */
460 new dojo.data.ItemFileReadStore(
461 {"data": flatten_to_dojo_data(result)}
465 for (var i in result) {
466 bresv_index[result[i].id()] = result[i];
471 function cancel_reservations(bresv_list) {
472 for (var i in bresv_list) { bresv_list[i].cancel_time("now"); }
475 "oncomplete": function() {
477 alert(localeStrings.CXL_BRESV_SUCCESS(bresv_list.length));
479 "onerror": function(o) {
481 alert(localeStrings.CXL_BRESV_FAILURE + "\n" + o);
488 * These functions deal with interface tricks (populating widgets,
489 * changing the page, etc.).
491 function provide_brt_selector(targ_div) {
493 alert(localeStrings.NO_TARG_DIV);
495 var brt_list = xulG.brt_list = get_all_noncat_brt();
496 if (!brt_list || brt_list.length < 1) {
497 targ_div.appendChild(
498 document.createTextNode(localeStrings.NO_BRT_RESULTS)
500 document.getElementById(
501 "brt_select_other_controls"
502 ).style.display = "none";
504 var selector = document.createElement("select");
505 selector.setAttribute("id", "brt_selector");
506 selector.setAttribute("name", "brt_selector");
507 /* I'm reluctantly hardcoding this "size" attribute as 8
508 * because you can't accomplish this with CSS anyway.
510 selector.setAttribute("size", 8);
511 for (var i in brt_list) {
512 var option = document.createElement("option");
513 option.setAttribute("value", brt_list[i].id());
514 option.appendChild(document.createTextNode(brt_list[i].name()));
515 selector.appendChild(option);
517 targ_div.innerHTML = "";
518 targ_div.appendChild(selector);
523 function init_reservation_interface(f) {
524 /* Hide and reveal relevant divs. */
525 var search_block = document.getElementById("brt_search_block");
526 var reserve_block = document.getElementById("brt_reserve_block");
527 hide_dom_element(search_block);
528 reveal_dom_element(reserve_block);
530 /* Save a global reference to the brt we're going to reserve */
532 our_brt = xulG.brt_list[f.brt_selector.selectedIndex];
534 /* Get a list of attributes that can apply to that brt. */
535 var bra_list = pcrud.search("bra", {"resource_type": our_brt.id()});
537 alert(localeString.NO_BRA_LIST);
541 /* Get a table of values that can apply to the above attributes. */
542 var brav_by_bra = {};
543 bra_list.map(function(o) {
544 brav_by_bra[o.id()] = pcrud.search("brav", {"attr": o.id()});
547 /* Hide the label over the attributes widgets if we have nothing to show. */
548 var domf = (bra_list.length < 1) ? hide_dom_element : reveal_dom_element;
549 domf(document.getElementById("bra_and_brav_header"));
551 /* Create DOM widgets to represent each attribute/values set. */
552 for (var i in bra_list) {
553 var bra_div = document.createElement("div");
554 bra_div.setAttribute("class", "nice_vertical_padding");
556 var bra_select = document.createElement("select");
557 bra_select.setAttribute("name", "bra_" + bra_list[i].id());
558 bra_select.setAttribute(
560 "attr_value_table.update_from_selector(this); update_brsrc_list();"
563 var bra_opt_any = document.createElement("option");
564 bra_opt_any.appendChild(document.createTextNode(localeStrings.ANY));
565 bra_opt_any.setAttribute("value", "");
567 bra_select.appendChild(bra_opt_any);
569 var bra_label = document.createElement("label");
570 bra_label.setAttribute("class", "bra");
571 bra_label.appendChild(document.createTextNode(bra_list[i].name()));
573 var j = bra_list[i].id();
574 for (var k in brav_by_bra[j]) {
575 var bra_opt = document.createElement("option");
576 bra_opt.setAttribute("value", brav_by_bra[j][k].id());
578 document.createTextNode(brav_by_bra[j][k].valid_value())
580 bra_select.appendChild(bra_opt);
583 bra_div.appendChild(bra_label);
584 bra_div.appendChild(bra_select);
585 document.getElementById("bra_and_brav").appendChild(bra_div);
587 /* Add a prominent label reminding the user what resource type they're
589 document.getElementById("brsrc_list_header").innerHTML = our_brt.name();
591 if (opts.patron_barcode) {
592 document.getElementById("holds_patron_barcode").style.display = "none";
593 document.getElementById("patron_barcode").value = opts.patron_barcode;
594 document.getElementById("patron_barcode").onchange();
599 function update_brsrc_list() {
600 var brsrc_id_list = get_brsrc_id_list();
601 var force_list = (opts.booking_results && opts.booking_results.brsrc) ?
602 opts.booking_results.brsrc.map(function(o) { return o[0]; }) : [];
604 sync_brsrc_index_from_ids(brsrc_id_list, force_list);
606 var target_selector = document.getElementById("brsrc_list");
607 var selector_memory = new SelectorMemory(target_selector);
608 selector_memory.save();
609 target_selector.innerHTML = "";
611 for (var i in brsrc_index) {
612 if (brsrc_index[i].isdeleted() && (!brsrc_index[i].ischanged()))
615 var opt = document.createElement("option");
616 opt.setAttribute("value", brsrc_index[i].id());
617 opt.appendChild(document.createTextNode(brsrc_index[i].barcode()));
619 if (brsrc_index[i].isdeleted() && (brsrc_index[i].ischanged())) {
620 opt.setAttribute("class", "forced_unavailable");
621 opt.setAttribute("disabled", "disabled");
624 target_selector.appendChild(opt);
627 selector_memory.restore();
630 function any_usable_brsrc() {
631 for (var i in brsrc_index) {
632 if (!brsrc_index[i].isdeleted())
638 function update_bresv_grid() {
639 var widg = document.getElementById("patron_barcode");
640 if (widg.value != "") {
641 setTimeout(function() {
642 var target = document.getElementById(
643 "existing_reservation_patron_line"
645 var patron = get_actor_by_barcode(widg.value);
648 localeStrings.HERE_ARE_EXISTING_BRESV + " " +
649 formal_name(patron) + ": "
652 target.innerHTML = "";
655 setTimeout(function() { init_bresv_grid(widg.value); }, 0);
659 function init_timestamp_widgets() {
660 var when = ["start", "end"];
661 for (var i in when) {
662 reserve_timestamp_range.update_from_widget(
663 new dijit.form.TimeTextBox({
664 name: "reserve_time_" + when[i],
667 timePattern: "HH:mm",
668 clickableIncrement: "T00:15:00",
669 visibleIncrement: "T00:15:00",
670 visibleRange: "T01:30:00",
672 onChange: function() {
673 reserve_timestamp_range.update_from_widget(this);
676 }, "reserve_time_" + when[i])
678 reserve_timestamp_range.update_from_widget(
679 new dijit.form.DateTextBox({
680 name: "reserve_date_" + when[i],
682 onChange: function() {
683 reserve_timestamp_range.update_from_widget(this);
686 }, "reserve_date_" + when[i])
691 function cancel_selected_bresv(bresv_dojo_items) {
692 if (bresv_dojo_items && bresv_dojo_items.length > 0) {
694 bresv_dojo_items.map(function(o) { return bresv_index[o.id]; })
696 /* After some delay to allow the cancellations a chance to get
697 * committed, refresh the brsrc list as it might reflect newly
698 * available resources now. */
699 setTimeout(update_brsrc_list, 2000);
701 alert(localeStrings.CXL_BRESV_SELECT_SOMETHING);
705 /* Quick and dirty way to localize some strings; not recommended for reuse.
706 * I'm sure dojo provides a better mechanism for this, but at the moment
707 * this is faster to implement anew than figuring out the Right way to do
708 * the same thing w/ dojo.
710 function init_auto_l10n(el) {
711 function do_it(myel, cls) {
713 var clss = cls.split(" ");
714 for (var k in clss) {
715 var parts = clss[k].match(/^AUTO_ATTR_([A-Z]+)_.+$/);
716 if (parts && localeStrings[clss[k]]) {
718 parts[1].toLowerCase(), localeStrings[clss[k]]
720 } else if (clss[k].match(/^AUTO_/) && localeStrings[clss[k]]) {
721 myel.innerHTML = localeStrings[clss[k]];
727 for (var i in el.attributes) {
728 if (el.attributes[i].nodeName == "class") {
729 do_it(el, el.attributes[i].value);
733 for (var i in el.childNodes) {
734 if (el.childNodes[i].nodeType == 1) { // element node?
735 init_auto_l10n(el.childNodes[i]); // recurse!
740 /* The following function should return true if the reservation interface
741 * should start normally (show a list of brt to choose from) or false if
742 * it should not (because we've "started" it some other way by setting up
743 * and displaying other widgets).
745 function early_action_passthru() {
746 if (opts.booking_results) {
747 if (opts.booking_results.brt.length != 1) {
748 alert(localeStrings.NEED_EXACTLY_ONE_BRT_PASSED_IN);
750 } else if (!(our_brt = get_brt_by_id(opts.booking_results.brt[0][0]))) {
751 alert(localeStrings.COULD_NOT_RETRIEVE_BRT_PASSED_IN);
755 init_reservation_interface();
759 if (opts.patron_barcode) {
761 var patron = get_actor_by_barcode(opts.patron_barcode);
763 document.getElementById("preselected_patron").innerHTML =
764 "Patron targeted for reservation: <strong>" +
765 formal_name(patron) + "</strong>";
768 ; /* XXX ignorable? perhaps. */
779 hide_dom_element(document.getElementById("brt_reserve_block"));
780 reveal_dom_element(document.getElementById("brt_search_block"));
781 hide_dom_element(document.getElementById("reserve_under"));
782 init_auto_l10n(document.getElementById("auto_l10n_start_here"));
783 init_timestamp_widgets();
785 if (!(opts = xulG.bresv_interface_opts)) opts = {};
786 if (early_action_passthru())
787 provide_brt_selector(document.getElementById("brt_selector_here"));