]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/web/js/ui/default/booking/reservation.js
Patch from Lebbeous Fogle-Weekley to add booking reservation interfaces, supporting...
[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 our_brt;
17 var brsrc_index = {};
18 var bresv_index = {};
19
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;
25     if (attr)
26         attr_value_table.set(attr, value);
27 };
28 AttrValueTable.prototype.get_all_values = function() {
29     var values = [];
30     for (var k in this.t) {
31         if (this.t[k] != undefined && this.t[k] != "")
32             values.push(this.t[k]);
33     }
34     return values;
35 };
36 var attr_value_table =  new AttrValueTable();
37
38 function TimestampRange() {
39     this.start = {"date": undefined, "time": undefined};
40     this.end = {"date": undefined, "time": undefined};
41 }
42 TimestampRange.prototype.get_timestamp = function(when) {
43     return (this[when].date + " " + this[when].time);
44 };
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")];
49 };
50 TimestampRange.prototype.split_time = function(s) {
51     /* We're not interested in seconds for our purposes,
52      * so we floor everything to :00.
53      *
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
61      * the server side.
62      */
63     return s.split("T")[1].replace(/(\d{2}:\d{2}:)(\d{2})(.*)/, "$100");
64 };
65 TimestampRange.prototype.split_date = function(s) {
66     return s.split("T")[0];
67 };
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];
71
72     if (when && which) {
73         this[when][which] =
74             this["split_" + which](widget.serialize(widget.value));
75     }
76 };
77 TimestampRange.prototype.is_backwards = function() {
78     return (this.get_timestamp("start") > this.get_timestamp("end"));
79 };
80 var reserve_timestamp_range = new TimestampRange();
81
82 function SelectorMemory(selector) {
83     this.selector = selector;
84     this.memory = {};
85 }
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;
90         }
91     }
92 };
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;
97         }
98     }
99 };
100
101 /*
102  * Misc helper functions
103  */
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());
111     return name;
112 }
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];
118 }
119 function set_datagrid_empty_store(grid) {
120     grid.setStore(
121         new dojo.data.ItemFileReadStore(
122             {"data": flatten_to_dojo_data([])}
123         )
124     );
125 }
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");
129 }
130 function my_ils_error(header, e) {
131     var s = header + "\n";
132     var keys = [
133         "ilsevent", "desc", "textcode", "servertime", "pid", "stacktrace"
134     ];
135     for (var i in keys) {
136         if (e[keys[i]]) s += ("\t" + keys[i] + ": " + e[keys[i]] + "\n");
137     }
138     return s;
139 }
140
141 /*
142  * These functions communicate with the middle layer.
143  */
144 function get_all_noncat_brt() {
145     return pcrud.search("brt",
146         {"id": {"!=": null}, "catalog_item": "f"},
147         {"order_by": {"brt":"name"}}
148     );
149 }
150
151 function get_brsrc_id_list() {
152     var options = {"type": our_brt.id()};
153
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.
158      */
159     var attribute_values = attr_value_table.get_all_values();
160     if (attribute_values.length > 0)
161         options.attribute_values = attribute_values;
162
163     options.available = reserve_timestamp_range.get_range();
164
165     return fieldmapper.standardRequest(
166         ["open-ils.booking", "open-ils.booking.resources.filtered_id_list"],
167         [xulG.auth.session.key, options]
168     );
169 }
170
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]);
177         }
178         brsrc_index[id_list[i]].isdeleted(false); // See NOTE below.
179     }
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.
184         }
185     }
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).
193      */
194 }
195
196 function check_bresv_targeting(results) {
197     var missing = 0;
198     for (var i in results) {
199         if (!(results[i].targeting && results[i].targeting.current_resource))
200             missing++;
201     }
202     return missing;
203 }
204
205 function create_bresv(resource_list) {
206     var barcode = document.getElementById("patron_barcode").value;
207     if (barcode == "") {
208         alert(localeStrings.WHERES_THE_BARCODE);
209         return;
210     }
211     var results;
212     try {
213         results = fieldmapper.standardRequest(
214             ["open-ils.booking", "open-ils.booking.reservations.create"],
215             [
216                 xulG.auth.session.key,
217                 barcode,
218                 reserve_timestamp_range.get_range(),
219                 our_brt.id(),
220                 resource_list,
221                 attr_value_table.get_all_values()
222             ]
223         );
224     } catch (E) {
225         alert(localeStrings.CREATE_BRESV_LOCAL_ERROR + E);
226     }
227     if (results) {
228         if (is_ils_error(results)) {
229             if (is_ils_actor_card_error(results)) {
230                 alert(localeStrings.ACTOR_CARD_NOT_FOUND);
231             } else {
232                 alert(my_ils_error(
233                     localeStrings.CREATE_BRESV_SERVER_ERROR, results
234                 ));
235             }
236         } else {
237             var missing;
238             alert((missing = check_bresv_targeting(results)) ?
239                 localeStrings.CREATE_BRESV_OK_MISSING_TARGET(
240                     results.length, missing
241                 ) :
242                 localeStrings.CREATE_BRESV_OK(results.length)
243             );
244             update_brsrc_list();
245             update_bresv_grid();
246         }
247     } else {
248         alert(localeStrings.CREATE_BRESV_SERVER_NO_RESPONSE);
249     }
250 }
251
252 function flatten_to_dojo_data(obj_list) {
253     return {
254         "label": "id",
255         "identifier": "id",
256         "items": obj_list.map(function(o) {
257             var new_obj = {
258                 "id": o.id(),
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()),
262             };
263
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();
268             else
269                 new_obj["resource"] = "* " + localeStrings.UNTARGETED + " *";
270             return new_obj;
271         })
272     };
273 }
274
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);
281     }
282     if (selected_values.length > 0)
283         create_bresv(selected_values);
284     else
285         alert(localeStrings.SELECT_A_BRSRC_THEN);
286 }
287
288 function create_bresv_on_brt() { create_bresv(); }
289
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]
294     );
295     if (usr == null) {
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.
301                       */
302     } else {
303         return usr;
304     }
305 }
306
307 function init_bresv_grid(barcode) {
308     var result = fieldmapper.standardRequest(
309         ["open-ils.booking",
310             "open-ils.booking.reservations.filtered_id_list"
311         ],
312         [xulG.auth.session.key, {
313             "user_barcode": barcode,
314             "fields": {
315                 "pickup_time": null,
316                 "cancel_time": null,
317                 "return_time": null
318             }
319         }, /* whole_obj */ true]
320     );
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);
328         } else {
329             alert(my_ils_error(localeStrings.GET_BRESV_LIST_ERR, result));
330         }
331     } else {
332         bresvGrid.setStore(
333             new dojo.data.ItemFileReadStore(
334                 {"data": flatten_to_dojo_data(result)}
335             )
336         );
337         for (var i in result) {
338             bresv_index[result[i].id()] = result[i];
339         }
340     }
341 }
342
343 function cancel_reservations(bresv_list) {
344     for (var i in bresv_list) { bresv_list[i].cancel_time("now"); }
345     pcrud.update(
346         bresv_list, {
347             "oncomplete": function() {
348                 update_bresv_grid();
349                 alert(localeStrings.CXL_BRESV_SUCCESS(bresv_list.length));
350             },
351             "onerror": function(o) {
352                 update_bresv_grid();
353                 alert(localeStrings.CXL_BRESV_FAILURE + "\n" + o);
354             }
355         }
356     );
357 }
358
359 /*
360  * These functions deal with interface tricks (populating widgets,
361  * changing the page, etc.).
362  */
363 function provide_brt_selector(targ_div) {
364     if (!targ_div) {
365         alert(localeStrings.NO_TARG_DIV);
366     } else {
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)
371             );
372         } else {
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.
378              */
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);
385             }
386             targ_div.appendChild(selector);
387         }
388     }
389 }
390
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);
397
398     /* Save a global reference to the brt we're going to reserve */
399     our_brt = xulG.brt_list[f.brt_selector.selectedIndex];
400
401     /* Get a list of attributes that can apply to that brt. */
402     var bra_list = pcrud.search("bra", {"resource_type": our_brt.id()});
403     if (!bra_list) {
404         alert(localeString.NO_BRA_LIST);
405         return;
406     }
407
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()});
412     });
413
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");
418
419         var bra_select = document.createElement("select");
420         bra_select.setAttribute("name", "bra_" + bra_list[i].id());
421         bra_select.setAttribute(
422             "onchange",
423             "attr_value_table.update_from_selector(this); update_brsrc_list();"
424         );
425
426         var bra_opt_any = document.createElement("option");
427         bra_opt_any.appendChild(document.createTextNode(localeStrings.ANY));
428         bra_opt_any.setAttribute("value", "");
429
430         bra_select.appendChild(bra_opt_any);
431
432         var bra_label = document.createElement("label");
433         bra_label.setAttribute("class", "bra");
434         bra_label.appendChild(document.createTextNode(bra_list[i].name()));
435
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());
440             bra_opt.appendChild(
441                 document.createTextNode(brav_by_bra[j][k].valid_value())
442             );
443             bra_select.appendChild(bra_opt);
444         }
445
446         bra_div.appendChild(bra_label);
447         bra_div.appendChild(bra_select);
448         document.getElementById("bra_and_brav").appendChild(bra_div);
449     }
450     /* Add a prominent label reminding the user what resource type they're
451      * asking about. */
452     document.getElementById("brsrc_list_header").innerHTML = our_brt.name();
453
454     update_brsrc_list();
455 }
456
457 function update_brsrc_list() {
458     var brsrc_id_list = get_brsrc_id_list();
459     sync_brsrc_index_from_ids(brsrc_id_list);
460
461     var target_selector = document.getElementById("brsrc_list");
462     var selector_memory = new SelectorMemory(target_selector);
463     selector_memory.save();
464     target_selector.innerHTML = "";
465
466     for (var i in brsrc_index) {
467         if (brsrc_index[i].isdeleted()) {
468             continue;
469         }
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);
474     }
475
476     selector_memory.restore();
477 }
478
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"
485             );
486             var patron = get_actor_by_barcode(widg.value);
487             if (patron) {
488                 target.innerHTML = (
489                     localeStrings.HERE_ARE_EXISTING_BRESV + " " +
490                     formal_name(patron) + ": "
491                 );
492             } else {
493                 target.innerHTML = "";
494             }
495         }, 0);
496         setTimeout(function() { init_bresv_grid(widg.value); }, 0);
497
498         reveal_dom_element(document.getElementById("reserve_under"));
499     }
500 }
501
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],
508                 value: new Date(),
509                 constraints: {
510                     timePattern: "HH:mm",
511                     clickableIncrement: "T00:15:00",
512                     visibleIncrement: "T00:15:00",
513                     visibleRange: "T01:30:00",
514                 },
515                 onChange: function() {
516                     reserve_timestamp_range.update_from_widget(this);
517                     update_brsrc_list();
518                 }
519             }, "reserve_time_" + when[i])
520         );
521         reserve_timestamp_range.update_from_widget(
522             new dijit.form.DateTextBox({
523                 name: "reserve_date_" + when[i],
524                 value: new Date(),
525                 onChange: function() {
526                     reserve_timestamp_range.update_from_widget(this);
527                     update_brsrc_list();
528                 }
529             }, "reserve_date_" + when[i])
530         );
531     }
532 }
533
534 function cancel_selected_bresv(bresv_dojo_items) {
535     if (bresv_dojo_items && bresv_dojo_items.length > 0) {
536         cancel_reservations(
537             bresv_dojo_items.map(function(o) { return bresv_index[o.id]; })
538         );
539     } else {
540         alert(localeStrings.CXL_BRESV_SELECT_SOMETHING);
541     }
542 }
543
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.
548  */
549 function init_auto_l10n(el) {
550     function do_it(myel, cls) {
551         if (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]]) {
556                     myel.setAttribute(
557                         parts[1].toLowerCase(), localeStrings[clss[k]]
558                     );
559                 } else if (clss[k].match(/^AUTO_/) && localeStrings[clss[k]]) {
560                     myel.innerHTML = localeStrings[clss[k]];
561                 }
562             }
563         }
564     }
565
566     for (var i in el.attributes) {
567         if (el.attributes[i].nodeName == "class") {
568             do_it(el, el.attributes[i].value);
569             break;
570         }
571     }
572     for (var i in el.childNodes) {
573         if (el.childNodes[i].nodeType == 1) { // element node?
574             init_auto_l10n(el.childNodes[i]); // recurse!
575         }
576     }
577 }
578
579 /*
580  * my_init
581  */
582 function my_init() {
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();
589 }