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