]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/web/js/ui/default/booking/reservation.js
Patch from Lebbeous Fogle-Weekley to wire up more of the booking functionality and...
[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("dojo.data.ItemFileReadStore");
7 dojo.require("dijit.form.DateTextBox");
8 dojo.require("dijit.form.TimeTextBox");
9 dojo.requireLocalization("openils.booking", "reservation");
10
11 /*
12  * Globals; prototypes and their instances
13  */
14 var localeStrings = dojo.i18n.getLocalization("openils.booking", "reservation");
15 var pcrud = new openils.PermaCrud();
16 var opts;
17 var our_brt;
18 var brsrc_index = {};
19 var bresv_index = {};
20 var just_reserved_now = {};
21
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;
27     if (attr)
28         attr_value_table.set(attr, value);
29 };
30 AttrValueTable.prototype.get_all_values = function() {
31     var values = [];
32     for (var k in this.t) {
33         if (this.t[k] != undefined && this.t[k] != "")
34             values.push(this.t[k]);
35     }
36     return values;
37 };
38 var attr_value_table =  new AttrValueTable();
39
40 function TimestampRange() {
41     this.start = new Date();
42     this.end = new Date();
43
44     this.validity = {"start": false, "end": false};
45     this.nodes = {
46         "start": {"date": undefined, "time": undefined},
47         "end": {"date": undefined, "time": undefined}
48     };
49     this.saved_style_properties = {};
50     this.invalid_style_properties = {
51         "backgroundColor": "#ffcccc",
52         "color": "#990000",
53         "borderColor": "#990000",
54         "fontWeight": "bold"
55     };
56 }
57 TimestampRange.prototype.get_timestamp = function(when) {
58     return this.any_widget.serialize(this[when]).
59         replace("T", " ").substr(0, 19);
60 };
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")];
65 };
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];
69
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 */
74
75     if (when && which) {
76         this.update_timestamp(when, which, widget.value);
77     }
78
79     this.compute_validity();
80     this.paint_validity();
81 };
82 TimestampRange.prototype.compute_validity = function() {
83     if (Math.abs(this.start - this.end) < 1000) {
84         this.validity.end = false;
85     } else {
86         if (this.start < this.current_minimum())
87             this.validity.start = false;
88         else
89             this.validity.start = true;
90
91         if (this.end < this.current_minimum())
92             this.validity.end = false;
93         else
94             this.validity.end = true;
95     }
96 };
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.
100  *
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.
104  */
105 TimestampRange.prototype.current_minimum = function() {
106     /* XXX This is going to be a problem with local clocks that are off. */
107     var n = new Date();
108     n.setTime(n.getTime() + 1000 * 900); /* XXX 15 minutes; stop hardcoding! */
109     return n;
110 };
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);
120     }
121 };
122 TimestampRange.prototype.is_backwards = function() {
123     return (this.start > this.end);
124 };
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);
130         } else {
131             this.paint_invalid_node(this.nodes[when].date);
132             this.paint_invalid_node(this.nodes[when].time);
133         }
134     }
135 };
136 TimestampRange.prototype.paint_invalid_node = function(node) {
137     if (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.
141          */
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];
145             }
146         }
147         for (var k in this.invalid_style_properties) {
148             node.style[k] = this.invalid_style_properties[k];
149         }
150     }
151 };
152 TimestampRange.prototype.paint_valid_node = function(node) {
153     if (node) {
154         for (var k in this.saved_style_properties) {
155             node.style[k] = this.saved_style_properties[k];
156         }
157     }
158 };
159 TimestampRange.prototype.is_valid = function() {
160     return (this.validity.start && this.validity.end);
161 };
162 var reserve_timestamp_range = new TimestampRange();
163
164 function SelectorMemory(selector) {
165     this.selector = selector;
166     this.memory = {};
167 }
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;
172         }
173     }
174 };
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;
180         }
181     }
182 };
183
184 /*
185  * Misc helper functions
186  */
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());
194     return name;
195 }
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];
201 }
202 function set_datagrid_empty_store(grid) {
203     grid.setStore(
204         new dojo.data.ItemFileReadStore(
205             {"data": flatten_to_dojo_data([])}
206         )
207     );
208 }
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");
212 }
213 function my_ils_error(header, e) {
214     var s = header + "\n";
215     var keys = [
216         "ilsevent", "desc", "textcode", "servertime", "pid", "stacktrace"
217     ];
218     for (var i in keys) {
219         if (e[keys[i]]) s += ("\t" + keys[i] + ": " + e[keys[i]] + "\n");
220     }
221     return s;
222 }
223
224 /*
225  * These functions communicate with the middle layer.
226  */
227 function get_all_noncat_brt() {
228     return pcrud.search("brt",
229         {"id": {"!=": null}, "catalog_item": "f"},
230         {"order_by": {"brt":"name"}}
231     );
232 }
233
234 function get_brt_by_id(id) {
235     return pcrud.retrieve("brt", id);
236 }
237
238 function get_brsrc_id_list() {
239     var options = {"type": our_brt.id()};
240
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.
245      */
246     var attribute_values = attr_value_table.get_all_values();
247     if (attribute_values.length > 0)
248         options.attribute_values = attribute_values;
249
250     options.available = reserve_timestamp_range.get_range();
251
252     return fieldmapper.standardRequest(
253         ["open-ils.booking", "open-ils.booking.resources.filtered_id_list"],
254         [xulG.auth.session.key, options]
255     );
256 }
257
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]);
263         }
264         if (further)
265             further(brsrc_index[list[i]]);
266     }
267 }
268
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);
274     }
275
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); }
281     );
282     add_brsrc_to_index_if_needed(
283         additional_list,
284         function(o) {
285             if (!(o.id() in just_reserved_now)) o.ischanged(true);
286         }
287     );
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))).
297      */
298 }
299
300 function check_bresv_targeting(results) {
301     var missing = 0;
302     for (var i in results) {
303         if (!(results[i].targeting && results[i].targeting.current_resource)) {
304             missing++;
305         } else {
306             just_reserved_now[results[i].targeting.current_resource] = true;
307         }
308     }
309     return missing;
310 }
311
312 function create_bresv(resource_list) {
313     var barcode = document.getElementById("patron_barcode").value;
314     if (barcode == "") {
315         alert(localeStrings.WHERES_THE_BARCODE);
316         return;
317     } else if (!reserve_timestamp_range.is_valid()) {
318         alert(localeStrings.INVALID_TS_RANGE);
319         return;
320     }
321     var results;
322     try {
323         results = fieldmapper.standardRequest(
324             ["open-ils.booking", "open-ils.booking.reservations.create"],
325             [
326                 xulG.auth.session.key,
327                 barcode,
328                 reserve_timestamp_range.get_range(),
329                 our_brt.id(),
330                 resource_list,
331                 attr_value_table.get_all_values()
332             ]
333         );
334     } catch (E) {
335         alert(localeStrings.CREATE_BRESV_LOCAL_ERROR + E);
336     }
337     if (results) {
338         if (is_ils_error(results)) {
339             if (is_ils_actor_card_error(results)) {
340                 alert(localeStrings.ACTOR_CARD_NOT_FOUND);
341             } else {
342                 alert(my_ils_error(
343                     localeStrings.CREATE_BRESV_SERVER_ERROR, results
344                 ));
345             }
346         } else {
347             var missing;
348             alert((missing = check_bresv_targeting(results)) ?
349                 localeStrings.CREATE_BRESV_OK_MISSING_TARGET(
350                     results.length, missing
351                 ) :
352                 localeStrings.CREATE_BRESV_OK(results.length)
353             );
354             update_brsrc_list();
355             update_bresv_grid();
356         }
357     } else {
358         alert(localeStrings.CREATE_BRESV_SERVER_NO_RESPONSE);
359     }
360 }
361
362 function flatten_to_dojo_data(obj_list) {
363     return {
364         "label": "id",
365         "identifier": "id",
366         "items": obj_list.map(function(o) {
367             var new_obj = {
368                 "id": o.id(),
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()),
372             };
373
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();
378             else
379                 new_obj["resource"] = "* " + localeStrings.UNTARGETED + " *";
380             return new_obj;
381         })
382     };
383 }
384
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);
391     }
392     if (selected_values.length > 0)
393         create_bresv(selected_values);
394     else
395         alert(localeStrings.SELECT_A_BRSRC_THEN);
396 }
397
398 function create_bresv_on_brt() {
399     if (any_usable_brsrc())
400         create_bresv();
401     else
402         alert(localeStrings.NO_USABLE_BRSRC);
403 }
404
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]
409     );
410     if (usr == null) {
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.
416                       */
417     } else {
418         return usr;
419     }
420 }
421
422 function init_bresv_grid(barcode) {
423     var result = fieldmapper.standardRequest(
424         ["open-ils.booking",
425             "open-ils.booking.reservations.filtered_id_list"
426         ],
427         [xulG.auth.session.key, {
428             "user_barcode": barcode,
429             "fields": {
430                 "pickup_time": null,
431                 "cancel_time": null,
432                 "return_time": null
433             }
434         }, /* whole_obj */ true]
435     );
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);
443         } else {
444             alert(my_ils_error(localeStrings.GET_BRESV_LIST_ERR, result));
445         }
446     } else {
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"));
452         } else {
453             document.getElementById("bresv_grid_alt_explanation").innerHTML =
454                 "";
455             reveal_dom_element(document.getElementById("bresv_grid"));
456             reveal_dom_element(document.getElementById("reserve_under"));
457         }
458         /* May as well do the following in either case... */
459         bresvGrid.setStore(
460             new dojo.data.ItemFileReadStore(
461                 {"data": flatten_to_dojo_data(result)}
462             )
463         );
464         bresv_index = {};
465         for (var i in result) {
466             bresv_index[result[i].id()] = result[i];
467         }
468     }
469 }
470
471 function cancel_reservations(bresv_list) {
472     for (var i in bresv_list) { bresv_list[i].cancel_time("now"); }
473     pcrud.update(
474         bresv_list, {
475             "oncomplete": function() {
476                 update_bresv_grid();
477                 alert(localeStrings.CXL_BRESV_SUCCESS(bresv_list.length));
478             },
479             "onerror": function(o) {
480                 update_bresv_grid();
481                 alert(localeStrings.CXL_BRESV_FAILURE + "\n" + o);
482             }
483         }
484     );
485 }
486
487 /*
488  * These functions deal with interface tricks (populating widgets,
489  * changing the page, etc.).
490  */
491 function provide_brt_selector(targ_div) {
492     if (!targ_div) {
493         alert(localeStrings.NO_TARG_DIV);
494     } else {
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)
499             );
500             document.getElementById(
501                 "brt_select_other_controls"
502             ).style.display = "none";
503         } else {
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.
509              */
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);
516             }
517             targ_div.innerHTML = "";
518             targ_div.appendChild(selector);
519         }
520     }
521 }
522
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);
529
530     /* Save a global reference to the brt we're going to reserve */
531     if (f)
532         our_brt = xulG.brt_list[f.brt_selector.selectedIndex];
533
534     /* Get a list of attributes that can apply to that brt. */
535     var bra_list = pcrud.search("bra", {"resource_type": our_brt.id()});
536     if (!bra_list) {
537         alert(localeString.NO_BRA_LIST);
538         return;
539     }
540
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()});
545     });
546
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"));
550
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");
555
556         var bra_select = document.createElement("select");
557         bra_select.setAttribute("name", "bra_" + bra_list[i].id());
558         bra_select.setAttribute(
559             "onchange",
560             "attr_value_table.update_from_selector(this); update_brsrc_list();"
561         );
562
563         var bra_opt_any = document.createElement("option");
564         bra_opt_any.appendChild(document.createTextNode(localeStrings.ANY));
565         bra_opt_any.setAttribute("value", "");
566
567         bra_select.appendChild(bra_opt_any);
568
569         var bra_label = document.createElement("label");
570         bra_label.setAttribute("class", "bra");
571         bra_label.appendChild(document.createTextNode(bra_list[i].name()));
572
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());
577             bra_opt.appendChild(
578                 document.createTextNode(brav_by_bra[j][k].valid_value())
579             );
580             bra_select.appendChild(bra_opt);
581         }
582
583         bra_div.appendChild(bra_label);
584         bra_div.appendChild(bra_select);
585         document.getElementById("bra_and_brav").appendChild(bra_div);
586     }
587     /* Add a prominent label reminding the user what resource type they're
588      * asking about. */
589     document.getElementById("brsrc_list_header").innerHTML = our_brt.name();
590
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();
595     }
596     update_brsrc_list();
597 }
598
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]; }) : [];
603
604     sync_brsrc_index_from_ids(brsrc_id_list, force_list);
605
606     var target_selector = document.getElementById("brsrc_list");
607     var selector_memory = new SelectorMemory(target_selector);
608     selector_memory.save();
609     target_selector.innerHTML = "";
610
611     for (var i in brsrc_index) {
612         if (brsrc_index[i].isdeleted() && (!brsrc_index[i].ischanged()))
613             continue;
614
615         var opt = document.createElement("option");
616         opt.setAttribute("value", brsrc_index[i].id());
617         opt.appendChild(document.createTextNode(brsrc_index[i].barcode()));
618
619         if (brsrc_index[i].isdeleted() && (brsrc_index[i].ischanged())) {
620             opt.setAttribute("class", "forced_unavailable");
621             opt.setAttribute("disabled", "disabled");
622         }
623
624         target_selector.appendChild(opt);
625     }
626
627     selector_memory.restore();
628 }
629
630 function any_usable_brsrc() {
631     for (var i in brsrc_index) {
632         if (!brsrc_index[i].isdeleted())
633             return true;
634     }
635     return false;
636 }
637
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"
644             );
645             var patron = get_actor_by_barcode(widg.value);
646             if (patron) {
647                 target.innerHTML = (
648                     localeStrings.HERE_ARE_EXISTING_BRESV + " " +
649                     formal_name(patron) + ": "
650                 );
651             } else {
652                 target.innerHTML = "";
653             }
654         }, 0);
655         setTimeout(function() { init_bresv_grid(widg.value); }, 0);
656     }
657 }
658
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],
665                 value: new Date(),
666                 constraints: {
667                     timePattern: "HH:mm",
668                     clickableIncrement: "T00:15:00",
669                     visibleIncrement: "T00:15:00",
670                     visibleRange: "T01:30:00",
671                 },
672                 onChange: function() {
673                     reserve_timestamp_range.update_from_widget(this);
674                     update_brsrc_list();
675                 }
676             }, "reserve_time_" + when[i])
677         );
678         reserve_timestamp_range.update_from_widget(
679             new dijit.form.DateTextBox({
680                 name: "reserve_date_" + when[i],
681                 value: new Date(),
682                 onChange: function() {
683                     reserve_timestamp_range.update_from_widget(this);
684                     update_brsrc_list();
685                 }
686             }, "reserve_date_" + when[i])
687         );
688     }
689 }
690
691 function cancel_selected_bresv(bresv_dojo_items) {
692     if (bresv_dojo_items && bresv_dojo_items.length > 0) {
693         cancel_reservations(
694             bresv_dojo_items.map(function(o) { return bresv_index[o.id]; })
695         );
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);
700     } else {
701         alert(localeStrings.CXL_BRESV_SELECT_SOMETHING);
702     }
703 }
704
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.
709  */
710 function init_auto_l10n(el) {
711     function do_it(myel, cls) {
712         if (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]]) {
717                     myel.setAttribute(
718                         parts[1].toLowerCase(), localeStrings[clss[k]]
719                     );
720                 } else if (clss[k].match(/^AUTO_/) && localeStrings[clss[k]]) {
721                     myel.innerHTML = localeStrings[clss[k]];
722                 }
723             }
724         }
725     }
726
727     for (var i in el.attributes) {
728         if (el.attributes[i].nodeName == "class") {
729             do_it(el, el.attributes[i].value);
730             break;
731         }
732     }
733     for (var i in el.childNodes) {
734         if (el.childNodes[i].nodeType == 1) { // element node?
735             init_auto_l10n(el.childNodes[i]); // recurse!
736         }
737     }
738 }
739
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).
744  */
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);
749             return true;
750         } else if (!(our_brt = get_brt_by_id(opts.booking_results.brt[0][0]))) {
751             alert(localeStrings.COULD_NOT_RETRIEVE_BRT_PASSED_IN);
752             return true;
753         }
754
755         init_reservation_interface();
756         return false;
757     }
758
759     if (opts.patron_barcode) {
760         try {
761             var patron = get_actor_by_barcode(opts.patron_barcode);
762             if (patron) {
763                 document.getElementById("preselected_patron").innerHTML =
764                     "Patron targeted for reservation: <strong>" +
765                     formal_name(patron) + "</strong>";
766             }
767         } catch (E) {
768             ; /* XXX ignorable? perhaps. */
769         }
770     }
771
772     return true;
773 }
774
775 /*
776  * my_init
777  */
778 function my_init() {
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();
784
785     if (!(opts = xulG.bresv_interface_opts)) opts = {};
786     if (early_action_passthru())
787         provide_brt_selector(document.getElementById("brt_selector_here"));
788 }