]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/web/js/ui/default/booking/reservation.js
Correct syntax for dojo.string.substitute() and remove offending unpaired parens
[working/Evergreen.git] / Open-ILS / web / js / ui / default / booking / reservation.js
1 /*
2  * Details, details...
3  */
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");
11
12 /*
13  * Globals; prototypes and their instances
14  */
15 var localeStrings = dojo.i18n.getLocalization("openils.booking", "reservation");
16 var pcrud = new openils.PermaCrud();
17 var opts;
18 var our_brt;
19 var pickup_lib_selected;
20 var brt_list = [];
21 var brsrc_index = {};
22 var bresv_index = {};
23 var just_reserved_now = {};
24
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;
30     if (attr)
31         attr_value_table.set(attr, value);
32 };
33 AttrValueTable.prototype.get_all_values = function() {
34     var values = [];
35     for (var k in this.t) {
36         if (this.t[k] != undefined && this.t[k] != "")
37             values.push(this.t[k]);
38     }
39     return values;
40 };
41 var attr_value_table =  new AttrValueTable();
42
43 function TimestampRange() {
44     this.start = new Date();
45     this.end = new Date();
46
47     this.validity = {"start": false, "end": false};
48     this.nodes = {
49         "start": {"date": undefined, "time": undefined},
50         "end": {"date": undefined, "time": undefined}
51     };
52     this.saved_style_properties = {};
53     this.invalid_style_properties = {
54         "backgroundColor": "#ffcccc",
55         "color": "#990000",
56         "borderColor": "#990000",
57         "fontWeight": "bold"
58     };
59 }
60 TimestampRange.prototype.get_timestamp = function(when) {
61     return this.any_widget.serialize(this[when]).
62         replace("T", " ").substr(0, 19);
63 };
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")];
68 };
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];
72
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 */
77
78     if (when && which) {
79         this.update_timestamp(when, which, widget.value);
80     }
81
82     this.compute_validity();
83     this.paint_validity();
84 };
85 TimestampRange.prototype.compute_validity = function() {
86     if (Math.abs(this.start - this.end) < 1000) {
87         this.validity.end = false;
88     } else {
89         if (this.start < this.current_minimum())
90             this.validity.start = false;
91         else
92             this.validity.start = true;
93
94         if (this.end < this.current_minimum())
95             this.validity.end = false;
96         else
97             this.validity.end = true;
98     }
99 };
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.
103  *
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.
107  */
108 TimestampRange.prototype.current_minimum = function() {
109     /* XXX This is going to be a problem with local clocks that are off. */
110     var n = new Date();
111     n.setTime(n.getTime() + 1000 * 900); /* XXX 15 minutes; stop hardcoding! */
112     return n;
113 };
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);
123     }
124 };
125 TimestampRange.prototype.is_backwards = function() {
126     return (this.start > this.end);
127 };
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);
133         } else {
134             this.paint_invalid_node(this.nodes[when].date);
135             this.paint_invalid_node(this.nodes[when].time);
136         }
137     }
138 };
139 TimestampRange.prototype.paint_invalid_node = function(node) {
140     if (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.
144          */
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];
148             }
149         }
150         for (var k in this.invalid_style_properties) {
151             node.style[k] = this.invalid_style_properties[k];
152         }
153     }
154 };
155 TimestampRange.prototype.paint_valid_node = function(node) {
156     if (node) {
157         for (var k in this.saved_style_properties) {
158             node.style[k] = this.saved_style_properties[k];
159         }
160     }
161 };
162 TimestampRange.prototype.is_valid = function() {
163     return (this.validity.start && this.validity.end);
164 };
165 var reserve_timestamp_range = new TimestampRange();
166
167 function SelectorMemory(selector) {
168     this.selector = selector;
169     this.memory = {};
170 }
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;
175         }
176     }
177 };
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;
183         }
184     }
185 };
186
187 /*
188  * These functions communicate with the middle layer.
189  */
190 function get_all_noncat_brt() {
191     return pcrud.search("brt",
192         {"id": {"!=": null}, "catalog_item": "f"},
193         {"order_by": {"brt":"name"}}
194     );
195 }
196
197 function get_brt_by_id(id) {
198     return pcrud.retrieve("brt", id);
199 }
200
201 function get_brsrc_id_list() {
202     var options = {"type": our_brt.id(), "pickup_lib": pickup_lib_selected};
203
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.
208      */
209     var attribute_values = attr_value_table.get_all_values();
210     if (attribute_values.length > 0)
211         options.attribute_values = attribute_values;
212
213     options.available = reserve_timestamp_range.get_range();
214
215     return fieldmapper.standardRequest(
216         ["open-ils.booking", "open-ils.booking.resources.filtered_id_list"],
217         [xulG.auth.session.key, options]
218     );
219 }
220
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]);
226         }
227         if (further)
228             further(brsrc_index[list[i]]);
229     }
230 }
231
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);
237     }
238
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); }
244     );
245     add_brsrc_to_index_if_needed(
246         additional_list,
247         function(o) {
248             if (!(o.id() in just_reserved_now)) o.ischanged(true);
249         }
250     );
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))).
260      */
261 }
262
263 function check_bresv_targeting(results) {
264     var missing = 0;
265     for (var i in results) {
266         if (!(results[i].targeting && results[i].targeting.current_resource)) {
267             missing++;
268         } else {
269             just_reserved_now[results[i].targeting.current_resource] = true;
270         }
271     }
272     return missing;
273 }
274
275 function create_bresv(resource_list) {
276     var barcode = document.getElementById("patron_barcode").value;
277     if (barcode == "") {
278         alert(localeStrings.WHERES_THE_BARCODE);
279         return;
280     } else if (!reserve_timestamp_range.is_valid()) {
281         alert(localeStrings.INVALID_TS_RANGE);
282         return;
283     }
284     var results;
285     try {
286         results = fieldmapper.standardRequest(
287             ["open-ils.booking", "open-ils.booking.reservations.create"],
288             [
289                 xulG.auth.session.key,
290                 barcode,
291                 reserve_timestamp_range.get_range(),
292                 pickup_lib_selected,
293                 our_brt.id(),
294                 resource_list,
295                 attr_value_table.get_all_values()
296             ]
297         );
298     } catch (E) {
299         alert(localeStrings.CREATE_BRESV_LOCAL_ERROR + E);
300     }
301     if (results) {
302         if (is_ils_event(results)) {
303             if (is_ils_actor_card_error(results)) {
304                 alert(localeStrings.ACTOR_CARD_NOT_FOUND);
305             } else {
306                 alert(my_ils_error(
307                     localeStrings.CREATE_BRESV_SERVER_ERROR, results
308                 ));
309             }
310         } else {
311             var missing;
312             if (missing = check_bresv_targeting(results)) {
313                 alert(dojo.string.substitute(
314                     localeStrings.CREATE_BRESV_OK_MISSING_TARGET,
315                     [results.length, missing]
316                 ));
317             } else {
318                 if (results.length == 1) {
319                     alert(dojo.string.substitute(
320                         localeStrings.CREATE_BRESV_OK, [results.length]
321                     ));
322                 } else {
323                     alert(dojo.string.substitute(
324                         localeStrings.CREATE_BRESV_OK_PLURAL, [results.length]
325                     ));
326                 }
327             }
328             update_brsrc_list();
329             update_bresv_grid();
330         }
331     } else {
332         alert(localeStrings.CREATE_BRESV_SERVER_NO_RESPONSE);
333     }
334 }
335
336 function flatten_to_dojo_data(obj_list) {
337     return {
338         "label": "id",
339         "identifier": "id",
340         "items": obj_list.map(function(o) {
341             var new_obj = {
342                 "id": o.id(),
343                 "type": o.target_resource_type().name(),
344                 "start_time": humanize_timestamp_string(o.start_time()),
345                 "end_time": humanize_timestamp_string(o.end_time()),
346             };
347
348             if (o.current_resource())
349                 new_obj["resource"] = o.current_resource().barcode();
350             else if (o.target_resource())
351                 new_obj["resource"] = "* " + o.target_resource().barcode();
352             else
353                 new_obj["resource"] = "* " + localeStrings.UNTARGETED + " *";
354             return new_obj;
355         })
356     };
357 }
358
359 function create_bresv_on_brsrc() {
360     var selector = document.getElementById("brsrc_list");
361     var selected_values = [];
362     for (var i in selector.options) {
363         if (selector.options[i] && selector.options[i].selected)
364             selected_values.push(selector.options[i].value);
365     }
366     if (selected_values.length > 0)
367         create_bresv(selected_values);
368     else
369         alert(localeStrings.SELECT_A_BRSRC_THEN);
370 }
371
372 function create_bresv_on_brt() {
373     if (any_usable_brsrc())
374         create_bresv();
375     else
376         alert(localeStrings.NO_USABLE_BRSRC);
377 }
378
379 function get_actor_by_barcode(barcode) {
380     var usr = fieldmapper.standardRequest(
381         ["open-ils.actor", "open-ils.actor.user.fleshed.retrieve_by_barcode"],
382         [xulG.auth.session.key, barcode]
383     );
384     if (usr == null) {
385         alert(localeStrings.GET_PATRON_NO_RESULT);
386     } else if (is_ils_event(usr)) {
387         return null; /* XXX inelegant: this function is quiet about errors
388                         here because to report them would be redundant with
389                         another function that gets called right after this one.
390                       */
391     } else {
392         return usr;
393     }
394 }
395
396 function init_bresv_grid(barcode) {
397     var result = fieldmapper.standardRequest(
398         ["open-ils.booking",
399             "open-ils.booking.reservations.filtered_id_list"
400         ],
401         [xulG.auth.session.key, {
402             "user_barcode": barcode,
403             "fields": {
404                 "pickup_time": null,
405                 "cancel_time": null,
406                 "return_time": null
407             }
408         }, /* whole_obj */ true]
409     );
410     if (result == null) {
411         set_datagrid_empty_store(bresvGrid, flatten_to_dojo_data);
412         alert(localeStrings.GET_BRESV_LIST_NO_RESULT);
413     } else if (is_ils_event(result)) {
414         set_datagrid_empty_store(bresvGrid, flatten_to_dojo_data);
415         if (is_ils_actor_card_error(result)) {
416             alert(localeStrings.ACTOR_CARD_NOT_FOUND);
417         } else {
418             alert(my_ils_error(localeStrings.GET_BRESV_LIST_ERR, result));
419         }
420     } else {
421         if (result.length < 1) {
422             document.getElementById("bresv_grid_alt_explanation").innerHTML =
423                 localeStrings.NO_EXISTING_BRESV;
424             hide_dom_element(document.getElementById("bresv_grid"));
425             reveal_dom_element(document.getElementById("reserve_under"));
426         } else {
427             document.getElementById("bresv_grid_alt_explanation").innerHTML =
428                 "";
429             reveal_dom_element(document.getElementById("bresv_grid"));
430             reveal_dom_element(document.getElementById("reserve_under"));
431         }
432         /* May as well do the following in either case... */
433         bresvGrid.setStore(
434             new dojo.data.ItemFileReadStore(
435                 {"data": flatten_to_dojo_data(result)}
436             )
437         );
438         bresv_index = {};
439         for (var i in result) {
440             bresv_index[result[i].id()] = result[i];
441         }
442     }
443 }
444
445 function cancel_reservations(bresv_id_list) {
446     try {
447         var result = fieldmapper.standardRequest(
448             ["open-ils.booking", "open-ils.booking.reservations.cancel"],
449             [xulG.auth.session.key, bresv_id_list]
450         );
451     } catch (E) {
452         alert(localeStrings.CXL_BRESV_FAILURE2 + E);
453         return;
454     }
455     setTimeout(update_bresv_grid, 0);
456     if (!result) {
457         alert(localeStrings.CXL_BRESV_FAILURE);
458     } else if (is_ils_event(result)) {
459         alert(my_ils_error(localeStrings.CXL_BRESV_FAILURE2, result));
460     } else {
461         if (result.length == 1) {
462             alert(dojo.string.substitute(
463                 localeStrings.CXL_BRESV_SUCCESS, [result.length]
464             ));
465         } else {
466             alert(dojo.string.substitute(
467                 localeStrings.CXL_BRESV_SUCCESS_PLURAL, [result.length]
468             ));
469         }
470     }
471 }
472
473 function munge_specific_resource(barcode) {
474     try {
475         var copy_list = pcrud.search(
476             "acp", {"barcode": barcode, "deleted": "f"}
477         );
478         if (copy_list && copy_list.length > 0) {
479             var r = fieldmapper.standardRequest(
480                 ["open-ils.booking",
481                     "open-ils.booking.resources.create_from_copies"],
482                 [xulG.auth.session.key,
483                     copy_list.map(function(o) { return o.id(); })]
484             );
485
486             if (!r) {
487                 alert(localeStrings.ON_FLY_NO_RESPONSE);
488             } else if (is_ils_event(r)) {
489                 alert(my_ils_error(localeStrings.ON_FLY_ERROR, r));
490             } else {
491                 if (!(our_brt = get_brt_by_id(r.brt[0][0]))) {
492                     alert(localeStrings.COULD_NOT_RETRIEVE_BRT_PASSED_IN);
493                 } else {
494                     opts.booking_results = r;
495                     init_reservation_interface();
496                 }
497             }
498         } else {
499             alert(localeStrings.BRSRC_NOT_FOUND);
500         }
501     } catch (E) {
502         alert(localeStrings.BRSRC_RETRIEVE_ERROR + E);
503     }
504 }
505
506 /*
507  * These functions deal with interface tricks (populating widgets,
508  * changing the page, etc.).
509  */
510 function init_pickup_lib_selector() {
511     var User = new openils.User();
512     User.buildPermOrgSelector(
513         "ADMIN_BOOKING_RESERVATION", pickup_lib_selector, null,
514         function() {
515             pickup_lib_selected = pickup_lib_selector.getValue();
516             dojo.connect(pickup_lib_selector, "onChange",
517                 function() {
518                     pickup_lib_selected = this.getValue();
519                     update_brsrc_list();
520                 }
521             )
522         }
523     );
524 }
525
526 function provide_brt_selector(targ_div) {
527     if (!targ_div) {
528         alert(localeStrings.NO_TARG_DIV);
529     } else {
530         brt_list = get_all_noncat_brt();
531         if (!brt_list || brt_list.length < 1) {
532             document.getElementById("select_noncat_brt_block").
533                 style.display = "none";
534         } else {
535             var selector = document.createElement("select");
536             selector.setAttribute("id", "brt_selector");
537             selector.setAttribute("name", "brt_selector");
538             /* I'm reluctantly hardcoding this "size" attribute as 8
539              * because you can't accomplish this with CSS anyway.
540              */
541             selector.setAttribute("size", 8);
542             for (var i in brt_list) {
543                 var option = document.createElement("option");
544                 option.setAttribute("value", brt_list[i].id());
545                 option.appendChild(document.createTextNode(brt_list[i].name()));
546                 selector.appendChild(option);
547             }
548             targ_div.innerHTML = "";
549             targ_div.appendChild(selector);
550         }
551     }
552 }
553
554 function init_resv_iface_arb() {
555     init_reservation_interface(document.getElementById("arbitrary_resource"));
556 }
557
558 function init_resv_iface_sel() {
559     init_reservation_interface(document.getElementById("brt_selector"));
560 }
561
562 function init_reservation_interface(widget) {
563     /* Save a global reference to the brt we're going to reserve */
564     if (widget && (widget.selectedIndex != undefined)) {
565         our_brt = brt_list[widget.selectedIndex];
566     } else if (widget != undefined) {
567         if (!munge_specific_resource(widget.value))
568             return;
569     }
570
571     /* Hide and reveal relevant divs. */
572     var search_block = document.getElementById("brt_search_block");
573     var reserve_block = document.getElementById("brt_reserve_block");
574     hide_dom_element(search_block);
575     reveal_dom_element(reserve_block);
576
577     /* Get a list of attributes that can apply to that brt. */
578     var bra_list = pcrud.search("bra", {"resource_type": our_brt.id()});
579     if (!bra_list) {
580         alert(localeString.NO_BRA_LIST);
581         return;
582     }
583
584     /* Get a table of values that can apply to the above attributes. */
585     var brav_by_bra = {};
586     bra_list.map(function(o) {
587         brav_by_bra[o.id()] = pcrud.search("brav", {"attr": o.id()});
588     });
589
590     /* Hide the label over the attributes widgets if we have nothing to show. */
591     var domf = (bra_list.length < 1) ? hide_dom_element : reveal_dom_element;
592     domf(document.getElementById("bra_and_brav_header"));
593
594     /* Create DOM widgets to represent each attribute/values set. */
595     for (var i in bra_list) {
596         var bra_div = document.createElement("div");
597         bra_div.setAttribute("class", "nice_vertical_padding");
598
599         var bra_select = document.createElement("select");
600         bra_select.setAttribute("name", "bra_" + bra_list[i].id());
601         bra_select.setAttribute(
602             "onchange",
603             "attr_value_table.update_from_selector(this); update_brsrc_list();"
604         );
605
606         var bra_opt_any = document.createElement("option");
607         bra_opt_any.appendChild(document.createTextNode(localeStrings.ANY));
608         bra_opt_any.setAttribute("value", "");
609
610         bra_select.appendChild(bra_opt_any);
611
612         var bra_label = document.createElement("label");
613         bra_label.setAttribute("class", "bra");
614         bra_label.appendChild(document.createTextNode(bra_list[i].name()));
615
616         var j = bra_list[i].id();
617         for (var k in brav_by_bra[j]) {
618             var bra_opt = document.createElement("option");
619             bra_opt.setAttribute("value", brav_by_bra[j][k].id());
620             bra_opt.appendChild(
621                 document.createTextNode(brav_by_bra[j][k].valid_value())
622             );
623             bra_select.appendChild(bra_opt);
624         }
625
626         bra_div.appendChild(bra_label);
627         bra_div.appendChild(bra_select);
628         document.getElementById("bra_and_brav").appendChild(bra_div);
629     }
630     /* Add a prominent label reminding the user what resource type they're
631      * asking about. */
632     document.getElementById("brsrc_list_header").innerHTML = our_brt.name();
633     init_pickup_lib_selector();
634     update_brsrc_list();
635 }
636
637 function update_brsrc_list() {
638     var brsrc_id_list = get_brsrc_id_list();
639     var force_list = (opts.booking_results && opts.booking_results.brsrc) ?
640         opts.booking_results.brsrc.map(function(o) { return o[0]; }) : [];
641
642     sync_brsrc_index_from_ids(brsrc_id_list, force_list);
643
644     var target_selector = document.getElementById("brsrc_list");
645     var selector_memory = new SelectorMemory(target_selector);
646     selector_memory.save();
647     target_selector.innerHTML = "";
648
649     for (var i in brsrc_index) {
650         if (brsrc_index[i].isdeleted() && (!brsrc_index[i].ischanged()))
651             continue;
652
653         var opt = document.createElement("option");
654         opt.setAttribute("value", brsrc_index[i].id());
655         opt.appendChild(document.createTextNode(brsrc_index[i].barcode()));
656
657         if (brsrc_index[i].isdeleted() && (brsrc_index[i].ischanged())) {
658             opt.setAttribute("class", "forced_unavailable");
659             opt.setAttribute("disabled", "disabled");
660         }
661
662         target_selector.appendChild(opt);
663     }
664
665     selector_memory.restore();
666 }
667
668 function any_usable_brsrc() {
669     for (var i in brsrc_index) {
670         if (!brsrc_index[i].isdeleted())
671             return true;
672     }
673     return false;
674 }
675
676 function update_bresv_grid() {
677     var widg = document.getElementById("patron_barcode");
678     if (widg.value != "") {
679         setTimeout(function() {
680             var target = document.getElementById(
681                 "existing_reservation_patron_line"
682             );
683             var patron = get_actor_by_barcode(widg.value);
684             if (patron) {
685                 target.innerHTML = (
686                     localeStrings.HERE_ARE_EXISTING_BRESV + " " +
687                     formal_name(patron) + ": "
688                 );
689             } else {
690                 target.innerHTML = "";
691             }
692         }, 0);
693         setTimeout(function() { init_bresv_grid(widg.value); }, 0);
694     }
695 }
696
697 function init_timestamp_widgets() {
698     var when = ["start", "end"];
699     for (var i in when) {
700         reserve_timestamp_range.update_from_widget(
701             new dijit.form.TimeTextBox({
702                 name: "reserve_time_" + when[i],
703                 value: new Date(),
704                 constraints: {
705                     timePattern: "HH:mm",
706                     clickableIncrement: "T00:15:00",
707                     visibleIncrement: "T00:15:00",
708                     visibleRange: "T01:30:00",
709                 },
710                 onChange: function() {
711                     reserve_timestamp_range.update_from_widget(this);
712                     update_brsrc_list();
713                 }
714             }, "reserve_time_" + when[i])
715         );
716         reserve_timestamp_range.update_from_widget(
717             new dijit.form.DateTextBox({
718                 name: "reserve_date_" + when[i],
719                 value: new Date(),
720                 onChange: function() {
721                     reserve_timestamp_range.update_from_widget(this);
722                     update_brsrc_list();
723                 }
724             }, "reserve_date_" + when[i])
725         );
726     }
727 }
728
729 function cancel_selected_bresv(bresv_dojo_items) {
730     if (bresv_dojo_items && bresv_dojo_items.length > 0 &&
731         (bresv_dojo_items[0].length == undefined ||
732             bresv_dojo_items[0].length > 0)) {
733         cancel_reservations(
734             bresv_dojo_items.map(function(o) { return o.id[0]; })
735         );
736         /* After some delay to allow the cancellations a chance to get
737          * committed, refresh the brsrc list as it might reflect newly
738          * available resources now. */
739         setTimeout(update_brsrc_list, 2000);
740     } else {
741         alert(localeStrings.CXL_BRESV_SELECT_SOMETHING);
742     }
743 }
744
745 /* The following function should return true if the reservation interface
746  * should start normally (show a list of brt to choose from) or false if
747  * it should not (because we've "started" it some other way by setting up
748  * and displaying other widgets).
749  */
750 function early_action_passthru() {
751     if (opts.booking_results) {
752         if (opts.booking_results.brt.length != 1) {
753             alert(localeStrings.NEED_EXACTLY_ONE_BRT_PASSED_IN);
754             return true;
755         } else if (!(our_brt = get_brt_by_id(opts.booking_results.brt[0][0]))) {
756             alert(localeStrings.COULD_NOT_RETRIEVE_BRT_PASSED_IN);
757             return true;
758         }
759
760         init_reservation_interface();
761         return false;
762     }
763
764     if (opts.patron_barcode) {
765         document.getElementById("contain_patron_barcode").style.display="none";
766         document.getElementById("patron_barcode").value = opts.patron_barcode;
767         update_bresv_grid();
768     }
769
770     return true;
771 }
772
773 /*
774  * my_init
775  */
776 function my_init() {
777     hide_dom_element(document.getElementById("brt_reserve_block"));
778     reveal_dom_element(document.getElementById("brt_search_block"));
779     hide_dom_element(document.getElementById("reserve_under"));
780     init_auto_l10n(document.getElementById("auto_l10n_start_here"));
781     init_timestamp_widgets();
782
783     if (!(opts = xulG.bresv_interface_opts)) opts = {};
784     if (early_action_passthru())
785         provide_brt_selector(document.getElementById("brt_selector_here"));
786 }