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 function AttrValueTable() { this.t = {}; }
21 AttrValueTable.prototype.set = function(attr, value) { this.t[attr] = value; };
22 AttrValueTable.prototype.update_from_selector = function(selector) {
23 var attr = selector.name.match(/_(\d+)$/)[1];
24 var value = selector.options[selector.selectedIndex].value;
26 attr_value_table.set(attr, value);
28 AttrValueTable.prototype.get_all_values = function() {
30 for (var k in this.t) {
31 if (this.t[k] != undefined && this.t[k] != "")
32 values.push(this.t[k]);
36 var attr_value_table = new AttrValueTable();
38 function TimestampRange() {
39 this.start = {"date": undefined, "time": undefined};
40 this.end = {"date": undefined, "time": undefined};
42 TimestampRange.prototype.get_timestamp = function(when) {
43 return (this[when].date + " " + this[when].time);
45 TimestampRange.prototype.get_range = function() {
46 return this.is_backwards() ?
47 [this.get_timestamp("end"), this.get_timestamp("start")] :
48 [this.get_timestamp("start"), this.get_timestamp("end")];
50 TimestampRange.prototype.split_time = function(s) {
51 /* We're not interested in seconds for our purposes,
52 * so we floor everything to :00.
54 * Also, notice that following discards all time zone information
55 * from the timestamp string represenation. This should probably
56 * stay the way it is, even when this code is improved to support
57 * selecting time zones (it currently just assumes server's local
58 * time). The easy way to add support will be to add a drop-down
59 * selector from which the user can pick a time zone, then use
60 * that timezone literal in an "AT TIME ZONE" clause in SQL on
63 return s.split("T")[1].replace(/(\d{2}:\d{2}:)(\d{2})(.*)/, "$100");
65 TimestampRange.prototype.split_date = function(s) {
66 return s.split("T")[0];
68 TimestampRange.prototype.update_from_widget = function(widget) {
69 var when = widget.id.match(/(start|end)/)[1];
70 var which = widget.id.match(/(date|time)/)[1];
74 this["split_" + which](widget.serialize(widget.value));
77 TimestampRange.prototype.is_backwards = function() {
78 return (this.get_timestamp("start") > this.get_timestamp("end"));
80 var reserve_timestamp_range = new TimestampRange();
82 function SelectorMemory(selector) {
83 this.selector = selector;
86 SelectorMemory.prototype.save = function() {
87 for (var i = 0; i < this.selector.options.length; i++) {
88 if (this.selector.options[i].selected) {
89 this.memory[this.selector.options[i].value] = true;
93 SelectorMemory.prototype.restore = function() {
94 for (var i = 0; i < this.selector.options.length; i++) {
95 if (this.memory[this.selector.options[i].value]) {
96 this.selector.options[i].selected = true;
102 * Misc helper functions
104 function hide_dom_element(e) { e.style.display = "none"; };
105 function reveal_dom_element(e) { e.style.display = ""; };
106 function get_keys(L) { var K = []; for (var k in L) K.push(k); return K; }
107 function formal_name(u) {
108 var name = u.family_name() + ", " + u.first_given_name();
109 if (u.second_given_name())
110 name += (" " + u.second_given_name());
113 function humanize_timestamp_string(ts) {
114 /* For now, this discards time zones. */
115 var parts = ts.split("T");
116 var timeparts = parts[1].split("-")[0].split(":");
117 return parts[0] + " " + timeparts[0] + ":" + timeparts[1];
119 function set_datagrid_empty_store(grid) {
121 new dojo.data.ItemFileReadStore(
122 {"data": flatten_to_dojo_data([])}
126 function is_ils_error(e) { return (e.ilsevent != undefined); }
127 function is_ils_actor_card_error(e) {
128 return (e.textcode == "ACTOR_CARD_NOT_FOUND");
130 function my_ils_error(header, e) {
131 var s = header + "\n";
133 "ilsevent", "desc", "textcode", "servertime", "pid", "stacktrace"
135 for (var i in keys) {
136 if (e[keys[i]]) s += ("\t" + keys[i] + ": " + e[keys[i]] + "\n");
142 * These functions communicate with the middle layer.
144 function get_all_noncat_brt() {
145 return pcrud.search("brt",
146 {"id": {"!=": null}, "catalog_item": "f"},
147 {"order_by": {"brt":"name"}}
151 function get_brsrc_id_list() {
152 var options = {"type": our_brt.id()};
154 /* This mechanism for avoiding the passing of an empty 'attribute_values'
155 * option is essential because if you pass such an option to the
156 * middle layer API at all, it won't return any IDs for brsrcs that
157 * don't have at least one attribute of some kind.
159 var attribute_values = attr_value_table.get_all_values();
160 if (attribute_values.length > 0)
161 options.attribute_values = attribute_values;
163 options.available = reserve_timestamp_range.get_range();
165 return fieldmapper.standardRequest(
166 ["open-ils.booking", "open-ils.booking.resources.filtered_id_list"],
167 [xulG.auth.session.key, options]
171 // FIXME: We need failure checking after pcrud.retrieve()
172 function sync_brsrc_index_from_ids(id_list) {
173 /* One pass to populate the cache with anything that's missing. */
174 for (var i in id_list) {
175 if (!brsrc_index[id_list[i]]) {
176 brsrc_index[id_list[i]] = pcrud.retrieve("brsrc", id_list[i]);
178 brsrc_index[id_list[i]].isdeleted(false); // See NOTE below.
180 /* A second pass to indicate any entries in the cache to be hidden. */
181 for (var i in brsrc_index) {
182 if (id_list.indexOf(Number(i)) < 0) { // Number() is important.
183 brsrc_index[i].isdeleted(true); // See NOTE below.
186 /* NOTE: We lightly abuse the isdeleted() magic attribute of the brsrcs
187 * in our cache. Because we're not going to pass back any brsrcs to
188 * the middle layer, it doesn't really matter what we set this attribute
189 * to. What we're using it for is to indicate in our little brsrc cache
190 * whether a given brsrc should be displayed in this UI's current state
191 * (based on whether it was returned by the last call to the middle layer,
192 * i.e., whether it matches the currently selected attributes).
196 function check_bresv_targeting(results) {
198 for (var i in results) {
199 if (!(results[i].targeting && results[i].targeting.current_resource))
205 function create_bresv(resource_list) {
206 var barcode = document.getElementById("patron_barcode").value;
208 alert(localeStrings.WHERES_THE_BARCODE);
213 results = fieldmapper.standardRequest(
214 ["open-ils.booking", "open-ils.booking.reservations.create"],
216 xulG.auth.session.key,
218 reserve_timestamp_range.get_range(),
221 attr_value_table.get_all_values()
225 alert(localeStrings.CREATE_BRESV_LOCAL_ERROR + E);
228 if (is_ils_error(results)) {
229 if (is_ils_actor_card_error(results)) {
230 alert(localeStrings.ACTOR_CARD_NOT_FOUND);
233 localeStrings.CREATE_BRESV_SERVER_ERROR, results
238 alert((missing = check_bresv_targeting(results)) ?
239 localeStrings.CREATE_BRESV_OK_MISSING_TARGET(
240 results.length, missing
242 localeStrings.CREATE_BRESV_OK(results.length)
248 alert(localeStrings.CREATE_BRESV_SERVER_NO_RESPONSE);
252 function flatten_to_dojo_data(obj_list) {
256 "items": obj_list.map(function(o) {
259 "type": o.target_resource_type().name(),
260 "start_time": humanize_timestamp_string(o.start_time()),
261 "end_time": humanize_timestamp_string(o.end_time()),
264 if (o.current_resource())
265 new_obj["resource"] = o.current_resource().barcode();
266 else if (o.target_resource())
267 new_obj["resource"] = "* " + o.target_resource().barcode();
269 new_obj["resource"] = "* " + localeStrings.UNTARGETED + " *";
275 function create_bresv_on_brsrc() {
276 var selector = document.getElementById("brsrc_list");
277 var selected_values = [];
278 for (var i in selector.options) {
279 if (selector.options[i].selected)
280 selected_values.push(selector.options[i].value);
282 if (selected_values.length > 0)
283 create_bresv(selected_values);
285 alert(localeStrings.SELECT_A_BRSRC_THEN);
288 function create_bresv_on_brt() { create_bresv(); }
290 function get_actor_by_barcode(barcode) {
291 var usr = fieldmapper.standardRequest(
292 ["open-ils.actor", "open-ils.actor.user.fleshed.retrieve_by_barcode"],
293 [xulG.auth.session.key, barcode]
296 alert(localeStrings.GET_PATRON_NO_RESULT);
297 } else if (is_ils_error(usr)) {
298 return null; /* XXX inelegant: this function is quiet about errors
299 here because to report them would be redundant with
300 another function that gets called right after this one.
307 function init_bresv_grid(barcode) {
308 var result = fieldmapper.standardRequest(
310 "open-ils.booking.reservations.filtered_id_list"
312 [xulG.auth.session.key, {
313 "user_barcode": barcode,
319 }, /* whole_obj */ true]
321 if (result == null) {
322 set_datagrid_empty_store(bresvGrid);
323 alert(localeStrings.GET_BRESV_LIST_NO_RESULT);
324 } else if (is_ils_error(result)) {
325 set_datagrid_empty_store(bresvGrid);
326 if (is_ils_actor_card_error(result)) {
327 alert(localeStrings.ACTOR_CARD_NOT_FOUND);
329 alert(my_ils_error(localeStrings.GET_BRESV_LIST_ERR, result));
333 new dojo.data.ItemFileReadStore(
334 {"data": flatten_to_dojo_data(result)}
337 for (var i in result) {
338 bresv_index[result[i].id()] = result[i];
343 function cancel_reservations(bresv_list) {
344 for (var i in bresv_list) { bresv_list[i].cancel_time("now"); }
347 "oncomplete": function() {
349 alert(localeStrings.CXL_BRESV_SUCCESS(bresv_list.length));
351 "onerror": function(o) {
353 alert(localeStrings.CXL_BRESV_FAILURE + "\n" + o);
360 * These functions deal with interface tricks (populating widgets,
361 * changing the page, etc.).
363 function provide_brt_selector(targ_div) {
365 alert(localeStrings.NO_TARG_DIV);
367 var brt_list = xulG.brt_list = get_all_noncat_brt();
368 if (!brt_list || brt_list.length < 1) {
369 targ_div.appendChild(
370 document.createTextNode(localeStrings.NO_BRT_RESULTS)
373 var selector = document.createElement("select");
374 selector.setAttribute("id", "brt_selector");
375 selector.setAttribute("name", "brt_selector");
376 /* I'm reluctantly hardcoding this "size" attribute as 8
377 * because you can't accomplish this with CSS anyway.
379 selector.setAttribute("size", 8);
380 for (var i in brt_list) {
381 var option = document.createElement("option");
382 option.setAttribute("value", brt_list[i].id());
383 option.appendChild(document.createTextNode(brt_list[i].name()));
384 selector.appendChild(option);
386 targ_div.appendChild(selector);
391 function init_reservation_interface(f) {
392 /* Hide and reveal relevant divs. */
393 var search_block = document.getElementById("brt_search_block");
394 var reserve_block = document.getElementById("brt_reserve_block");
395 hide_dom_element(search_block);
396 reveal_dom_element(reserve_block);
398 /* Save a global reference to the brt we're going to reserve */
399 our_brt = xulG.brt_list[f.brt_selector.selectedIndex];
401 /* Get a list of attributes that can apply to that brt. */
402 var bra_list = pcrud.search("bra", {"resource_type": our_brt.id()});
404 alert(localeString.NO_BRA_LIST);
408 /* Get a table of values that can apply to the above attributes. */
409 var brav_by_bra = {};
410 bra_list.map(function(o) {
411 brav_by_bra[o.id()] = pcrud.search("brav", {"attr": o.id()});
414 /* Create DOM widgets to represent each attribute/values set. */
415 for (var i in bra_list) {
416 var bra_div = document.createElement("div");
417 bra_div.setAttribute("class", "nice_vertical_padding");
419 var bra_select = document.createElement("select");
420 bra_select.setAttribute("name", "bra_" + bra_list[i].id());
421 bra_select.setAttribute(
423 "attr_value_table.update_from_selector(this); update_brsrc_list();"
426 var bra_opt_any = document.createElement("option");
427 bra_opt_any.appendChild(document.createTextNode(localeStrings.ANY));
428 bra_opt_any.setAttribute("value", "");
430 bra_select.appendChild(bra_opt_any);
432 var bra_label = document.createElement("label");
433 bra_label.setAttribute("class", "bra");
434 bra_label.appendChild(document.createTextNode(bra_list[i].name()));
436 var j = bra_list[i].id();
437 for (var k in brav_by_bra[j]) {
438 var bra_opt = document.createElement("option");
439 bra_opt.setAttribute("value", brav_by_bra[j][k].id());
441 document.createTextNode(brav_by_bra[j][k].valid_value())
443 bra_select.appendChild(bra_opt);
446 bra_div.appendChild(bra_label);
447 bra_div.appendChild(bra_select);
448 document.getElementById("bra_and_brav").appendChild(bra_div);
450 /* Add a prominent label reminding the user what resource type they're
452 document.getElementById("brsrc_list_header").innerHTML = our_brt.name();
457 function update_brsrc_list() {
458 var brsrc_id_list = get_brsrc_id_list();
459 sync_brsrc_index_from_ids(brsrc_id_list);
461 var target_selector = document.getElementById("brsrc_list");
462 var selector_memory = new SelectorMemory(target_selector);
463 selector_memory.save();
464 target_selector.innerHTML = "";
466 for (var i in brsrc_index) {
467 if (brsrc_index[i].isdeleted()) {
470 var opt = document.createElement("option");
471 opt.setAttribute("value", brsrc_index[i].id());
472 opt.appendChild(document.createTextNode(brsrc_index[i].barcode()));
473 target_selector.appendChild(opt);
476 selector_memory.restore();
479 function update_bresv_grid() {
480 var widg = document.getElementById("patron_barcode");
481 if (widg.value != "") {
482 setTimeout(function() {
483 var target = document.getElementById(
484 "existing_reservation_patron_line"
486 var patron = get_actor_by_barcode(widg.value);
489 localeStrings.HERE_ARE_EXISTING_BRESV + " " +
490 formal_name(patron) + ": "
493 target.innerHTML = "";
496 setTimeout(function() { init_bresv_grid(widg.value); }, 0);
498 reveal_dom_element(document.getElementById("reserve_under"));
502 function init_timestamp_widgets() {
503 var when = ["start", "end"];
504 for (var i in when) {
505 reserve_timestamp_range.update_from_widget(
506 new dijit.form.TimeTextBox({
507 name: "reserve_time_" + when[i],
510 timePattern: "HH:mm",
511 clickableIncrement: "T00:15:00",
512 visibleIncrement: "T00:15:00",
513 visibleRange: "T01:30:00",
515 onChange: function() {
516 reserve_timestamp_range.update_from_widget(this);
519 }, "reserve_time_" + when[i])
521 reserve_timestamp_range.update_from_widget(
522 new dijit.form.DateTextBox({
523 name: "reserve_date_" + when[i],
525 onChange: function() {
526 reserve_timestamp_range.update_from_widget(this);
529 }, "reserve_date_" + when[i])
534 function cancel_selected_bresv(bresv_dojo_items) {
535 if (bresv_dojo_items && bresv_dojo_items.length > 0) {
537 bresv_dojo_items.map(function(o) { return bresv_index[o.id]; })
540 alert(localeStrings.CXL_BRESV_SELECT_SOMETHING);
544 /* Quick and dirty way to localize some strings; not recommended for reuse.
545 * I'm sure dojo provides a better mechanism for this, but at the moment
546 * this is faster to implement anew than figuring out the Right way to do
547 * the same thing w/ dojo.
549 function init_auto_l10n(el) {
550 function do_it(myel, cls) {
552 var clss = cls.split(" ");
553 for (var k in clss) {
554 var parts = clss[k].match(/^AUTO_ATTR_([A-Z]+)_.+$/);
555 if (parts && localeStrings[clss[k]]) {
557 parts[1].toLowerCase(), localeStrings[clss[k]]
559 } else if (clss[k].match(/^AUTO_/) && localeStrings[clss[k]]) {
560 myel.innerHTML = localeStrings[clss[k]];
566 for (var i in el.attributes) {
567 if (el.attributes[i].nodeName == "class") {
568 do_it(el, el.attributes[i].value);
572 for (var i in el.childNodes) {
573 if (el.childNodes[i].nodeType == 1) { // element node?
574 init_auto_l10n(el.childNodes[i]); // recurse!
583 hide_dom_element(document.getElementById("brt_reserve_block"));
584 reveal_dom_element(document.getElementById("brt_search_block"));
585 hide_dom_element(document.getElementById("reserve_under"));
586 provide_brt_selector(document.getElementById("brt_selector_here"));
587 init_auto_l10n(document.getElementById("auto_l10n_start_here"));
588 init_timestamp_widgets();