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