Initial dev repository
[kcls-web.git] / js / ui / default / acq / search / unified.js
1 dojo.require("dojo.date.stamp");
2 dojo.require("dojox.encoding.base64");
3 dojo.require("openils.widget.AutoGrid");
4 dojo.require("openils.widget.AutoWidget");
5 dojo.require("openils.widget.XULTermLoader");
6 dojo.require("openils.PermaCrud");
7
8 var termSelectorFactory;
9 var termManager;
10 var resultManager;
11 var uriManager;
12 var pcrud = new openils.PermaCrud();
13 var cgi = new openils.CGI();
14
15 /* typing save: add {get,set}Value() to all HTML <select> elements */
16 HTMLSelectElement.prototype.getValue = function() {
17     return this.options[this.selectedIndex].value;
18 }
19
20 /* only sets the selected value if such an option is actually available */
21 HTMLSelectElement.prototype.setValue = function(s) {
22     for (var i = 0; i < this.options.length; i++) {
23         if (s == this.options[i].value) {
24             this.selectedIndex = i;
25             break;
26         }
27     }
28 }
29
30 /* minor formatting function used by autogrids in unified.tt2 */
31 function getName(rowIndex, item) {
32     if (item) {
33         return {
34             "name": this.grid.store.getValue(item, "name") ||
35                 localeStrings.UNNAMED,
36             "id": this.grid.store.getValue(item, "id")
37         };
38     }
39 }
40
41 /* quickly find elements by the value of a "name" attribute */
42 function nodeByName(name, root) {
43     return dojo.query("[name='" + name + "']", root)[0];
44 }
45
46 function hideForm() {
47     openils.Util.hide("acq-unified-hide-form");
48     openils.Util.show("acq-unified-reveal-form", "inline");
49     openils.Util.hide("acq-unified-form");
50 }
51
52 function revealForm() {
53     openils.Util.hide("acq-unified-reveal-form");
54     openils.Util.show("acq-unified-hide-form", "inline");
55     openils.Util.show("acq-unified-form");
56 }
57
58 /* The TermSelectorFactory will be instantiated by the TermManager. It
59  * provides HTML select controls whose options are all the searchable
60  * fields.  Selecting a field from one of these controls will create the
61  * appopriate type of corresponding widget for the user to enter a search
62  * term against the selected field.
63  */
64 function TermSelectorFactory(terms) {
65     var self = this;
66     this.terms = terms;
67     this.onlyBibFriendly = false;
68
69     this.template = dojo.create("select");
70     this.template.appendChild(
71         dojo.create("option", {
72             "disabled": "disabled",
73             "selected": "selected",
74             "value": "",
75             "innerHTML": localeStrings.SELECT_SEARCH_FIELD
76         })
77     );
78
79     /* Create abbreviations for class names to make field categories
80      * more readable in field selector control. */
81     this._abbreviate = function(s) {
82         var last, result;
83         for (var i = 0; i < s.length; i++) {
84             if (s[i] != " ") {
85                 if (!i) result = s[i];
86                 else if (last == " ") result += s[i];
87             }
88             last = s[i];
89         }
90         return result;
91     };
92
93     var selectorMethods = {
94         /* Important: within the following functions, "this" refers to one
95          * HTMLSelect object, and "self" refers to the TermSelectorFactory. */
96         "getTerm": function() { return this.valueToTerm(this.getValue()); },
97         "valueToTerm": function(value) {
98             var parts = value.split(":");
99             if (!parts || parts.length != 2) return null;
100             return dojo.mixin(
101                 self.terms[parts[0]][parts[1]],
102                 {"hint": parts[0], "field": parts[1]}
103             );
104         },
105         "onlyBibFriendly": function(yes) {
106             if (yes) {
107                 for (var i = 0; i < this.options.length; i++) {
108                     if (this.options[i].value) {
109                         var term = this.valueToTerm(this.options[i].value);
110                         this.options[i].disabled = !term.bib_friendly;
111                     }
112                 }
113             } else {
114                 for (var i = 0; i < this.options.length; i++) {
115                     if (this.options[i].value)
116                         this.options[i].disabled = false;
117                 }
118             }
119         },
120         "makeWidget": function(
121             parentNode, wStore, matchHow, value, noFocus, callback
122         ) {
123             var term = this.getTerm();
124             var widgetKey = this.uniq;
125             if (matchHow.getValue() == "__in") {
126                 new openils.widget.XULTermLoader({
127                     "parentNode": parentNode
128                 }).build(
129                     function(w) {
130                         wStore[widgetKey] = w;
131                         if (typeof(callback) == "function")
132                             callback(term, widgetKey);
133                         if (typeof(value) != "undefined")
134                             w.attr("value", value);
135                         /* I would love for the following call not to be
136                          * necessary, so that updating the value of the dijit
137                          * would lead to this automatically, but I can't yet
138                          * figure out the correct way to do this in Dojo.
139                          */
140                         w.updateCount();
141                     }
142                 );
143             } else if (term.hint == "acqlia" ||
144                 (term.hint == "jub" && term.field == "eg_bib_id")) {
145                 /* The test for jub.eg_bib_id is a special case to prevent
146                  * AutoFieldWidget from trying to render a ridiculous dropdown
147                  * of every bib record ID in the system. */
148                 wStore[widgetKey] = dojo.create(
149                     "input", {"type": "text"}, parentNode, "only"
150                 );
151                 if (typeof(value) != "undefined")
152                     wStore[widgetKey].value = value;
153                 if (!noFocus)
154                     wStore[widgetKey].focus();
155                 if (typeof(callback) == "function")
156                     callback(term, widgetKey);
157             } else {
158                 new openils.widget.AutoFieldWidget({
159                     "fmClass": term.hint,
160                     "fmField": term.field,
161                     "noDisablePkey": true,
162                     "parentNode": dojo.create("span", null, parentNode, "only")
163                 }).build(
164                     function(w) {
165                         wStore[widgetKey] = w;
166                         if (typeof(value) != "undefined") {
167                             if (w.declaredClass.match(/Check/))
168                                 w.attr("checked", value == "t");
169                             else
170                                 w.attr("value", value);
171                         }
172                         if (!noFocus)
173                             w.focus();
174                         if (typeof(callback) == "function")
175                             callback(term, widgetKey);
176
177                         // submit on enter
178                         openils.Util.registerEnterHandler(w.domNode,
179                             function() { 
180                                 resultManager.go(termManager.buildSearchObject());
181                             }
182                         );
183                     }
184                 );
185             }
186         }
187     }
188
189     for (var hint in this.terms) {
190         var optgroup = dojo.create(
191             "optgroup", {"value": "", "label": this.terms[hint].__label}
192         );
193         var prefix = this._abbreviate(this.terms[hint].__label);
194
195         for (var field in this.terms[hint]) {
196             if (!/^__/.test(field)) {
197                 optgroup.appendChild(
198                     dojo.create("option", {
199                         "class": "acq-unified-option-regular",
200                         "value": hint + ":" + field,
201                         "innerHTML": prefix + " - " +
202                             this.terms[hint][field].label
203                     })
204                 );
205             }
206         }
207
208         this.template.appendChild(optgroup);
209     }
210
211     this.make = function(n) {
212         var node = dojo.clone(this.template);
213         node.uniq = n;
214         dojo.attr(node, "id", "term-" + n);
215         for (var name in selectorMethods)
216             node[name] = selectorMethods[name];
217         if (this.onlyBibFriendly)
218             node.onlyBibFriendly(true);
219         return node;
220     };
221 }
222
223 /* The term manager retrieves information from the IDL about all the fields
224  * in the classes that we consider searchable for our purpose.  It maintains
225  * a dynamic HTML table of search terms, using the TermSelectorFactory
226  * to generate search field selectors, which in turn provide appropriate
227  * widgets for entering search terms.  The TermManager provides search term
228  * modifiers (fuzzy searching, not searching). The TermManager also handles
229  * adding and removing rows of search terms, as well as building the search
230  * query to pass to the middle layer from the search term widgets.
231  */
232 function TermManager() {
233     var self = this;
234
235     /* All the keys in this object are bib-search-friendly attributes, but the
236      * boolean values indicate whether they should be searched by their
237      * field name as such, or simply mapped to "keyword". */
238     this.bibFriendlyAttrNames = {
239         "author": true, "title": true,
240         "isbn": false, "issn": false, "upc": false
241     };
242
243     this.terms = {};
244     ["jub", "acqpl", "acqpo", "acqinv"].forEach(
245         function(hint) {
246             var o = {};
247             o.__label = fieldmapper.IDL.fmclasses[hint].label;
248             fieldmapper.IDL.fmclasses[hint].fields.forEach(
249                 function(field) {
250                     if (!field.virtual) {
251                         o[field.name] = {
252                             "label": field.label, "datatype": field.datatype
253                         };
254                     }
255                 }
256             );
257             self.terms[hint] = o;
258         }
259     );
260
261     this.terms.acqlia = {"__label": fieldmapper.IDL.fmclasses.acqlia.label};
262     pcrud.retrieveAll("acqliad", {"order_by": {"acqliad": "id"}}).forEach(
263         function(def) {
264             self.terms.acqlia[def.id()] = {
265                 "label": def.description(),
266                 "datatype": "text",
267                 "bib_friendly":
268                     (typeof(self.bibFriendlyAttrNames[def.code()]) !=
269                         "undefined"),
270                 "bib_attr_name":
271                     self.bibFriendlyAttrNames[def.code()] ?
272                         def.code() : "keyword"
273             };
274         }
275     );
276
277     this.selectorFactory = new TermSelectorFactory(this.terms);
278     this.template = dojo.byId("acq-unified-terms-tbody").
279         removeChild(dojo.byId("acq-unified-terms-row-tmpl"));
280     dojo.attr(this.template, "id");
281
282     this.lastResultType = null;
283
284     this.rowId = 0;
285     this.widgets = {};
286
287     dojo.byId("acq-unified-result-type").onchange = function() {
288         self.resultTypeChange(this.getValue());
289     };
290
291     this.allRowIds = function() {
292         return dojo.query("tr[id^='term-row-']", "acq-unified-terms-tbody").
293             map(function(o) { return o.id.match(/^term-row-(\d+)$/)[1]; });
294     };
295
296     this._row = function(id) { return dojo.byId("term-row-" + id); };
297     this._selector = function(id) { return dojo.byId("term-" + id); };
298     this._match_how = function(id) { return dojo.byId("term-match-" + id); };
299
300     this._updateMatchHowForField = function(term, key) {
301         /* NOTE important to use self, not this, in this function.
302          *
303          * Based on the selected field (its datatype and the kind of widget
304          * that AutoFieldWidget provides for it) we update the possible
305          * choices in the mach_how selector.
306          */
307         var w = self.widgets[key];
308         var can_do_fuzzy, can_do_in;
309         if (term.datatype == "id") {
310             can_do_fuzzy = false;
311             can_do_in = true;
312         } else if (term.datatype == "link") {
313             var target = self.getLinkTarget(term);
314             can_do_fuzzy = (target == "au");
315             can_do_in = (target == "bre"); /* XXX might revise later */
316         } else if (typeof(w.declaredClass) != "undefined") {
317             can_do_fuzzy = can_do_in =
318                 Boolean(w.declaredClass.match(/form\.Text|XULT/));
319         } else {
320             var type = dojo.attr(w, "type");
321             if (type)
322                 can_do_fuzzy = can_do_in = (type == "text");
323             else
324                 can_do_fuzzy = can_do_in = false;
325         }
326
327         self.matchHowAllow(key, "__fuzzy", can_do_fuzzy);
328         self.matchHowAllow(key, "__in", can_do_in);
329
330         var inequalities = (term.datatype == "timestamp");
331         self.matchHowAllow(key, "__gte", inequalities);
332         self.matchHowAllow(key, "__lte", inequalities);
333     };
334
335     this.removerButton = function(n) {
336         return dojo.create("button", {
337             "innerHTML": "X",
338             "class": "acq-unified-remover",
339             "onclick": function() { self.removeRow(n); }
340         });
341     };
342
343     this.matchHowAllow = function(where, what, which, exact) {
344         dojo.query(
345             "option[value" + (exact ? "" : "*") + "='" + what + "']",
346             typeof(where) == "object" ? where : this._match_how(where)
347         ).forEach(function(o) { o.disabled = !which; });
348     };
349
350     this.getLinkTarget = function(term) {
351         return fieldmapper.IDL.fmclasses[term.hint].
352             field_map[term.field]["class"];
353     };
354
355     this.updateRowWidget = function(id, value, noFocus) {
356         var where = nodeByName("widget", this._row(id));
357
358         delete this.widgets[id];
359         dojo.empty(where);
360
361         this._selector(id).makeWidget(
362             where, this.widgets, this._match_how(id), value, noFocus,
363             this._updateMatchHowForField
364         );
365     };
366
367     this.resultTypeChange = function(resultType) {
368         if (
369             this.lastResultType == "lineitem_and_bib" &&
370             resultType != "lineitem_and_bib"
371         ) {
372             /* Re-enable all non-bib-friendly fields in all search term
373              * field selectors. */
374             this.allRowIds().forEach(
375                 function(id) {
376                     self._selector(id).onlyBibFriendly(false);
377                     self.matchHowAllow(id, "", true, /* exact */ true);
378                     self.matchHowAllow(id, "__not", true, /* exact */ true);
379                 }
380             );
381             /* Tell the selector factory to create new search term field
382              * selectors with all fields, not just bib-friendly ones. */
383             this.selectorFactory.onlyBibFriendly = false;
384         } else if (
385             this.lastResultType != "lineitem_and_bib" &&
386             resultType == "lineitem_and_bib"
387         ) {
388             /* Remove all search term rows set to non-bib-friendly fields. */
389             this.allRowIds().forEach(
390                 function(id) {
391                     var term = self._selector(id).getTerm();
392                     if (term &&
393                         !self.terms[term.hint][term.field].bib_friendly) {
394                         self.removeRow(id);
395                     }
396                 }
397             );
398             /* Disable all non-bib-friendly fields in all remaining search term
399              * field selectors. */
400             this.allRowIds().forEach(
401                 function(id) {
402                     self._selector(id).onlyBibFriendly(true);
403                     self.matchHowAllow(id, "", false, /* exact */ true);
404                     self.matchHowAllow(id, "__not", false, /* exact */ true);
405                 }
406             );
407             /* Tell the selector factory to create new search term field
408              * selectors with only bib friendly options. */
409             this.selectorFactory.onlyBibFriendly = true;
410         }
411         this.lastResultType = resultType;
412     };
413
414     /* this method is particularly kludgy... puts back together a string
415      * based on object properties that might arrive in indeterminate order. */
416     this._term_reverse_match_how = function(term) {
417         /* only two-key combination we use */
418         if (term.__not && term.__fuzzy)
419             return "__not,__fuzzy";
420
421         /* only other possibilities are single-key or no key */
422         for (var key in term) {
423             if (/^__/.test(key))
424                 return key;
425         }
426
427         return null;
428     };
429
430
431     this._term_reverse_selector_field = function(term) {
432         for (var key in term) {
433             if (!/^__/.test(key))
434                 return key;
435         }
436         return null;
437     };
438
439     this._term_reverse_selector_value = function(term) {
440         for (var key in term) {
441             if (!/^__/.test(key))
442                 return term[key];
443         }
444         return null;
445     };
446
447     this.addRow = function(term, hint) {
448         var uniq = (this.rowId)++;
449
450         var row = dojo.clone(this.template);
451         dojo.attr(row, "id", "term-row-" + uniq);
452
453         var selector = this.selectorFactory.make(uniq);
454         dojo.attr(
455             selector, "onchange", function() { self.updateRowWidget(uniq); }
456         );
457
458         var match_how = dojo.query("select", nodeByName("match", row))[0];
459         dojo.attr(match_how, "id", "term-match-" + uniq);
460         dojo.attr(match_how, "selectedIndex", 0);
461         dojo.attr(
462             match_how, "onchange",
463             function() {
464                 if (this.getValue() == "__in") {
465                     self.updateRowWidget(uniq);
466                     this.was_in = true;
467                 } else if (this.was_in) {
468                     self.updateRowWidget(uniq);
469                     this.was_in = false;
470                 }
471                 if (self.widgets[uniq]) self.widgets[uniq].focus();
472             }
473         );
474
475         /* Kind of inelegant; could be improved: this section turns off
476          * match-type options that don't apply to bib searching. */
477         this.matchHowAllow(
478             match_how, "",
479             !this.selectorFactory.onlyBibFriendly, /* exact */ true
480         );
481         this.matchHowAllow(
482             match_how, "__not",
483             !this.selectorFactory.onlyBibFriendly, /* exact */ true
484         );
485         if (this.selectorFactory.onlyBibFriendly)
486             match_how.setValue("__fuzzy");
487
488         nodeByName("selector", row).appendChild(selector);
489         nodeByName("remove", row).appendChild(this.removerButton(uniq));
490
491         dojo.place(row, "acq-unified-terms-tbody", "last");
492
493         if (term && hint) {
494             var attr = this._term_reverse_selector_field(term);
495             var field = hint + ":" + attr;
496             selector.setValue(field);
497
498             var match_how_value = this._term_reverse_match_how(term);
499             if (match_how_value)
500                 match_how.setValue(match_how_value);
501
502             var value = this._term_reverse_selector_value(term);
503             if (this.terms[hint][attr].datatype == "timestamp")
504                 value = dojo.date.stamp.fromISOString(value);
505             this.updateRowWidget(uniq, value, /* noFocus */ true);
506
507         }
508     }
509
510     this.removeRow = function(id) {
511         delete this.widgets[id];
512         dojo.destroy(this._row(id));
513     };
514
515     this.reflect = function(search_object) {
516         for (var hint in search_object) {
517             search_object[hint].forEach(
518                 function(term) { self.addRow(term, hint); }
519             );
520         }
521     };
522
523     this.buildSearchObject = function() {
524         var so = {};
525
526         for (var id in this.widgets) {
527             var kvlist = this._selector(id).getValue().split(":");
528             var hint = kvlist[0];
529             var attr = kvlist[1];
530             if (!(hint && attr)) continue;
531
532             var match_how =
533                 this._match_how(id).getValue().split(",").filter(Boolean);
534
535             var value;
536             if (typeof(this.widgets[id].declaredClass) != "undefined") {
537                 if (this.widgets[id].declaredClass.match(/Date/)) {
538                     value =
539                         dojo.date.stamp.toISOString(this.widgets[id].value).
540                             split("T")[0];
541                 } else {
542                     value = this.widgets[id].attr("value");
543                     if (this.widgets[id].declaredClass.match(/Check/))
544                         value = (value == "on") ? "t" : "f";
545                 }
546             } else {
547                 value = this.widgets[id].value;
548             }
549
550             if (!so[hint])
551                 so[hint] = [];
552
553             var unit = {};
554             unit[attr] = value;
555             match_how.forEach(function(key) { unit[key] = true; });
556             if (this.terms[hint][attr].datatype == "timestamp")
557                 unit.__castdate = true;
558
559             so[hint].push(unit);
560         }
561         return so;
562     };
563
564     this.buildBibSearchString = function() {
565         var conj = {"and": " ", "or": " || "}[
566             dojo.byId("acq-unified-conjunction").getValue()
567         ];
568
569         var sso = {};
570         /* Notice that below we use conj in two places and a constant " || "
571          * in one. That constant " || " is applied for the "file of terms"
572          * search term type, which is in itself always an or search. */
573         for (var id in this.widgets) {
574             var term = this._selector(id).getTerm();
575             var attr = term.bib_attr_name;
576             var match_how = this._match_how(id).getValue();
577             var widget = this.widgets[id];
578
579             if (!sso[attr]) sso[attr] = [];
580             var  value = (
581                 typeof(widget.attr) == "function" ?
582                     widget.attr("value") : widget.value
583             );
584             if (typeof(value) != "string")
585                 value = value.join(" || ");
586             sso[attr].push(
587                 (match_how.indexOf("__not") == -1 ? "" : "-") + value
588             );
589         }
590         var ssa = [];
591         for (var attr in sso)
592             ssa.push(attr + ": " + sso[attr].join(conj));
593         return "(" + ssa.join(conj) + ")";
594     };
595 }
596
597 /* The result manager is used primarily when the users submits a search.  It
598  * consults the termManager to get the search query to send to the middl
599  * layer, and it chooses which ML method to call as well as what widgets to use
600  * to display the results.
601  */
602 function ResultManager(liPager, poGrid, plGrid, invGrid) {
603     var self = this;
604
605     this.liPager = liPager;
606     this.liPager.liTable.isUni = true;
607
608     this.poGrid = poGrid;
609     this.plGrid = plGrid;
610     this.invGrid = invGrid;
611     this.poCache = {};
612     this.plCache = {};
613     this.invCache = {};
614
615     this.result_types = {
616         "lineitem": {
617             "search_options": {
618                 "flesh_attrs": true,
619                 "flesh_cancel_reason": true,
620                 "flesh_notes": true
621             },
622             "revealer": function() {
623                 self.liPager.show();
624                 progressDialog.show(true);
625             },
626             "finisher": function() {
627                 self.liPager.batch_length = self.count_results;
628                 self.liPager.relabelControls();
629                 self.liPager.enableControls(true);
630                 progressDialog.hide();
631             },
632             "adder": function(li) {
633                 self.liPager.liTable.addLineitem(li);
634             },
635             "interface": self.liPager
636         },
637         "purchase_order": {
638             "search_options": {
639                 "no_flesh_cancel_reason": true
640             },
641             "revealer": function() {
642                 self.poGrid.resetStore();
643                 self.poGrid.showLoadProgressIndicator();
644                 self.poCache = {};
645             },
646             "finisher": function() {
647                 self.poGrid.hideLoadProgressIndicator();
648             },
649             "adder": function(po) {
650                 self.poCache[po.id()] = po;
651                 self.poGrid.store.newItem(acqpo.toStoreItem(po));
652             },
653             "interface": self.poGrid
654         },
655         "picklist": {
656             "search_options": {
657                 "flesh_lineitem_count": true,
658                 "flesh_owner": true
659             },
660             "revealer": function() {
661                 self.plGrid.resetStore();
662                 self.plGrid.showLoadProgressIndicator();
663                 self.plCache = {};
664             },
665             "finisher": function() {
666                 self.plGrid.hideLoadProgressIndicator();
667             },
668             "adder": function(pl) {
669                 self.plCache[pl.id()] = pl;
670                 self.plGrid.store.newItem(acqpl.toStoreItem(pl));
671             },
672             "interface": self.plGrid
673         },
674         "invoice": {
675             "search_options": {
676                 "no_flesh_misc": true
677             },
678             "finisher": function() {
679                 self.invGrid.hideLoadProgressIndicator();
680             },
681             "revealer": function() {
682                 self.invGrid.resetStore();
683                 self.invCache = {};
684             },
685             "adder": function(inv) {
686                 self.invCache[inv.id()] = inv;
687                 self.invGrid.store.newItem(acqinv.toStoreItem(inv));
688             },
689             "interface": self.invGrid
690         },
691         "no_results": {
692             "revealer": function() { alert(localeStrings.NO_RESULTS); }
693         }
694     };
695
696     this._dataLoader = function() {
697         /* This function must contain references to "self" only, not "this." */
698         var grid = self.result_types[self.result_type].interface;
699         self.count_results = 0;
700         self.params[4].offset = grid.displayOffset;
701         self.params[4].limit = grid.displayLimit;
702
703         fieldmapper.standardRequest(
704             ["open-ils.acq", self.method_name], {
705                 "params": self.params,
706                 "async": true,
707                 "onresponse": function(r) {
708                     if (r = openils.Util.readResponse(r)) {
709                         if (!self.count_results++)
710                             self.show(self.result_type);
711                         self.add(self.result_type, r);
712                     }
713                 },
714                 "oncomplete": function() { self.resultsComplete(); }
715             }
716         );
717     };
718
719     this.add = function(which, what) {
720         var f = this.result_types[which].adder;
721         if (f) f(what);
722     };
723
724     this.finish = function(which) {
725         var f = this.result_types[which].finisher;
726         if (f) f();
727     };
728
729     this.show = function(which) {
730         openils.Util.objectProperties(this.result_types).forEach(
731             function(rt) {
732                 openils.Util[rt == which ? "show" : "hide"](
733                     "acq-unified-results-" + rt
734                 );
735             }
736         );
737         this.result_types[which].revealer();
738     };
739
740     this.resultsComplete = function() {
741         if (!this.count_results)
742             this.show("no_results");
743         else this.finish(this.result_type);
744     };
745
746     this.go = function(search_object) {
747         location.href = oilsBasePath + "/acq/search/unified?" +
748             "so=" + base64Encode(search_object) +
749             "&rt=" + dojo.byId("acq-unified-result-type").getValue() +
750             "&c=" + dojo.byId("acq-unified-conjunction").getValue();
751     };
752
753     this.search = function(uriManager, termManager) {
754         var bib_search_string = null;
755         this.count_results = 0;
756         this.result_type = dojo.byId("acq-unified-result-type").getValue();
757
758         /* lineitem_and_bib: a special case */
759         if (this.result_type == "lineitem_and_bib") {
760             this.result_type = "lineitem";
761             bib_search_string = termManager.buildBibSearchString();
762         }
763
764         this.method_name = "open-ils.acq." + this.result_type +
765             ".unified_search";
766         /* Except for building the API method name that we want to call,
767          * we want to treat lineitem_and_bib the same way as lineitem from
768          * here forward. */
769
770         this.params = [
771             openils.User.authtoken,
772             null, null, null,
773             this.result_types[this.result_type].search_options
774         ];
775
776         this.params[
777             dojo.byId("acq-unified-conjunction").getValue() == "and" ? 1 : 2
778         ] = uriManager.search_object;
779         if (uriManager.order_by)
780             this.params[4].order_by = uriManager.order_by;
781
782         var interface = this.result_types[this.result_type].interface;
783         interface.dataLoader = this._dataLoader;
784
785         if (bib_search_string) {
786             /* Have the ML do the bib search first, which incidentally has the
787              * side effect of creating line items that will show up when
788              * we do the LI part of the search (so we don't actually want
789              * to display these results directly). */
790             fieldmapper.standardRequest(
791                 ["open-ils.acq", "open-ils.acq.biblio.wrapped_search.atomic"], {
792                     "params": [
793                         openils.User.authtoken, bib_search_string, {
794                             "clear_marc": true
795                         }
796                     ],
797                     "onresponse": function(r) {
798                         r = openils.Util.readResponse(r, false, true);
799                     }
800                 }
801             );
802         }
803
804         interface.dataLoader();
805     };
806 }
807
808 function URIManager() {
809     var self = this;
810     this.cannedSearches = {
811         "po": {
812             "search_object": {
813                 "acqpo": [
814                     {"ordering_agency": openils.User.user.ws_ou()},
815                     {"state": "on-order"}
816                 ]
817             },
818             "half_search": true,
819             "result_type": "purchase_order",
820             "conjunction": "and",
821             "order_by": [
822                 {"class": "acqpo", "field": "edit_time", "direction": "desc"}
823             ]
824         },
825         "pl": {
826             "search_object": {
827                 "acqpl": [
828                     {"owner": openils.User.user.usrname()}
829                 ]
830             },
831             "result_type": "picklist",
832             "conjunction": "and",
833             "order_by": [
834                 {"class": "acqpl", "field": "edit_time", "direction": "desc"}
835             ]
836         },
837         "inv": {
838             "search_object": {
839                 "acqinv": [
840                     {"complete": "f"},
841                     {"receiver": openils.User.user.ws_ou()}
842                 ]
843             },
844             "half_search": true,
845             "result_type": "invoice",
846             "conjunction": "and",
847             "order_by": [
848                 {"class": "acqinv", "field": "recv_date", "direction": "desc"}
849             ]
850         }
851     };
852
853     if (this.canned = cgi.param("ca")) { /* assignment */
854         dojo.mixin(this, this.cannedSearches[this.canned]);
855         dojo.byId("acq-unified-result-type").setValue(this.result_type);
856         dojo.byId("acq-unified-result-type").onchange();
857         dojo.byId("acq-unified-conjunction").setValue(this.conjunction);
858     } else {
859         this.search_object = cgi.param("so");
860         if (this.search_object)
861             this.search_object = base64Decode(this.search_object);
862
863         this.result_type = cgi.param("rt");
864         if (this.result_type) {
865             dojo.byId("acq-unified-result-type").setValue(this.result_type);
866             dojo.byId("acq-unified-result-type").onchange();
867         }
868
869         this.conjunction = cgi.param("c");
870         if (this.conjunction)
871             dojo.byId("acq-unified-conjunction").setValue(this.conjunction);
872     }
873 }
874
875 /* onload */
876 openils.Util.addOnLoad(
877     function() {
878         termManager = new TermManager();
879         resultManager = new ResultManager(
880             new LiTablePager(null, new AcqLiTable()),
881             dijit.byId("acq-unified-po-grid"),
882             dijit.byId("acq-unified-pl-grid"),
883             dijit.byId("acq-unified-inv-grid")
884         );
885
886         uriManager = new URIManager();
887         if (uriManager.search_object) {
888             if (!uriManager.half_search)
889                 hideForm();
890             openils.Util.show("acq-unified-body");
891             termManager.reflect(uriManager.search_object);
892
893             if (!uriManager.half_search)
894                 resultManager.search(uriManager, termManager);
895         } else {
896             termManager.addRow();
897             openils.Util.show("acq-unified-body");
898         }
899     }
900 );