Acq: in acquistions unified search, make timestamp fields searchable
[working/Evergreen.git] / Open-ILS / web / js / ui / default / acq / search / unified.js
1 dojo.require("dojo.date.stamp");
2 dojo.require("openils.widget.AutoGrid");
3 dojo.require("openils.widget.AutoWidget");
4 dojo.require("openils.PermaCrud");
5 dojo.require("openils.Util");
6
7 var termSelectorFactory;
8 var termManager;
9 var resultManager;
10 var pcrud = new openils.PermaCrud();
11
12 /* typing save: add getValue() to all HTML <select> elements */
13 HTMLSelectElement.prototype.getValue = function() {
14     return this.options[this.selectedIndex].value;
15 }
16
17 /* quickly find elements by the value of a "name" attribute */
18 function nodeByName(name, root) {
19     return dojo.query("[name='" + name + "']", root)[0];
20 }
21
22 function hideForm() {
23     openils.Util.hide("acq-unified-hide-form");
24     openils.Util.show("acq-unified-reveal-form", "inline");
25     openils.Util.hide("acq-unified-form");
26 }
27
28 function revealForm() {
29     openils.Util.hide("acq-unified-reveal-form");
30     openils.Util.show("acq-unified-hide-form", "inline");
31     openils.Util.show("acq-unified-form");
32 }
33
34 /* The TermSelectorFactory will be instantiated by the TermManager. It
35  * provides HTML select controls whose options are all the searchable
36  * fields.  Selecting a field from one of these controls will create the
37  * appopriate type of corresponding widget for the user to enter a search
38  * term against the selected field.
39  */
40 function TermSelectorFactory(terms) {
41     var self = this;
42     this.terms = terms;
43
44     this.template = dojo.create("select");
45     this.template.appendChild(
46         dojo.create("option", {
47             "disabled": "disabled",
48             "selected": "selected",
49             "value": "",
50             "innerHTML": "Select Search Field" // XXX i18n
51         })
52     );
53
54     /* Create abbreviations for class names to make field categories
55      * more readable in field selector control. */
56     this._abbreviate = function(s) {
57         var last, result;
58         for (var i = 0; i < s.length; i++) {
59             if (s[i] != " ") {
60                 if (!i) result = s[i];
61                 else if (last == " ") result += s[i];
62             }
63             last = s[i];
64         }
65         return result;
66     };
67
68     var selectorMethods = {
69         /* Important: within the following functions, "this" refers to one
70          * HTMLSelect object, and "self" refers to the TermSelectorFactory. */
71         "getTerm": function() {
72             var parts = this.getValue().split(":");
73             return {
74                 "hint": parts[0],
75                 "field": parts[1],
76                 "datatype": self.terms[parts[0]][parts[1]].datatype
77             };
78         },
79         "makeWidget": function(parentNode, wStore, callback) {
80             var term = this.getTerm();
81             var widgetKey = this.uniq;
82             if (term.hint == "acqlia") {
83                 wStore[widgetKey] = dojo.create(
84                     "input", {"type": "text"}, parentNode, "only"
85                 );
86                 wStore[widgetKey].focus();
87                 if (typeof(callback) == "function")
88                     callback(term, widgetKey);
89             } else {
90                 var widget = new openils.widget.AutoFieldWidget({
91                     "fmClass": term.hint,
92                     "fmField": term.field,
93                     "noDisablePkey": true,
94                     "parentNode": dojo.create("span", null, parentNode, "only")
95                 });
96                 widget.build(
97                     function(w) {
98                         wStore[widgetKey] = w;
99                         w.focus();
100                         if (typeof(callback) == "function")
101                             callback(term, widgetKey);
102                     }
103                 );
104             }
105         }
106     }
107
108     for (var hint in this.terms) {
109         var optgroup = dojo.create(
110             "optgroup", {"value": "", "label": this.terms[hint].__label}
111         );
112         var prefix = this._abbreviate(this.terms[hint].__label);
113
114         for (var field in this.terms[hint]) {
115             if (!/^__/.test(field)) {
116                 optgroup.appendChild(
117                     dojo.create("option", {
118                         "class": "acq-unified-option-regular",
119                         "value": hint + ":" + field,
120                         "innerHTML": prefix + " - " +
121                             this.terms[hint][field].label
122                     })
123                 );
124             }
125         }
126
127         this.template.appendChild(optgroup);
128     }
129
130     this.make = function(n) {
131         var node = dojo.clone(this.template);
132         node.uniq = n;
133         dojo.attr(node, "id", "term-" + n);
134         for (var name in selectorMethods)
135             node[name] = selectorMethods[name];
136         return node;
137     };
138 }
139
140 /* The term manager retrieves information from the IDL about all the fields
141  * in the classes that we consider searchable for our purpose.  It maintains
142  * a dynamic HTML table of search terms, using the TermSelectorFactory
143  * to generate search field selectors, which in turn provide appropriate
144  * widgets for entering search terms.  The TermManager provides search term
145  * modifiers (fuzzy searching, not searching). The TermManager also handles
146  * adding and removing rows of search terms, as well as building the search
147  * query to pass to the middle layer from the search term widgets.
148  */
149 function TermManager() {
150     var self = this;
151
152     this.terms = {};
153     ["jub", "acqpl", "acqpo"].forEach(
154         function(hint) {
155             var o = {};
156             o.__label = fieldmapper.IDL.fmclasses[hint].label;
157             fieldmapper.IDL.fmclasses[hint].fields.forEach(
158                 function(field) {
159                     if (!field.virtual) {
160                         o[field.name] = {
161                             "label": field.label, "datatype": field.datatype
162                         };
163                     }
164                 }
165             );
166             self.terms[hint] = o;
167         }
168     );
169
170     this.terms.acqlia = {"__label": fieldmapper.IDL.fmclasses.acqlia.label};
171     pcrud.retrieveAll("acqliad", {"order_by": {"acqliad": "id"}}).forEach(
172         function(def) {
173             self.terms.acqlia[def.id()] =
174                 {"label": def.description(), "datatype": "text"}
175         }
176     );
177
178     this.selectorFactory = new TermSelectorFactory(this.terms);
179     this.template = dojo.byId("acq-unified-terms-tbody").
180         removeChild(dojo.byId("acq-unified-terms-row-tmpl"));
181     dojo.attr(this.template, "id");
182
183     this.rowId = 0;
184     this.widgets = {};
185
186     this._row = function(id) { return dojo.byId("term-row-" + id); };
187     this._selector = function(id) { return dojo.byId("term-" + id); };
188     this._match_how = function(id) { return dojo.byId("term-match-" + id); };
189
190     this.removerButton = function(n) {
191         return dojo.create("button", {
192             "innerHTML": "X",
193             "class": "acq-unified-remover",
194             "onclick": function() { self.removeRow(n); }
195         });
196     };
197
198     this.matchHowAllow = function(id, what, which) {
199         dojo.query(
200             "option[value*='" + what + "']", this._match_how(id)
201         ).forEach(function(o) { o.disabled = !which; });
202     };
203
204     this.getLinkTarget = function(term) {
205         return fieldmapper.IDL.fmclasses[term.hint].
206             field_map[term.field]["class"];
207     };
208
209     this.updateRowWidget = function(id) {
210         var where = nodeByName("widget", this._row(id));
211
212         delete this.widgets[id];
213         dojo.empty(where);
214
215         this._selector(id).makeWidget(
216             where, this.widgets,
217             function(term, key) {
218                 var w = self.widgets[key];
219                 var can_do_fuzzy;
220                 if (term.datatype == "id") {
221                     can_do_fuzzy = false;
222                 } else if (term.datatype == "link") {
223                     can_do_fuzzy = (self.getLinkTarget(term) == "au");
224                 } else if (typeof(w.declaredClass) != "undefined") {
225                     can_do_fuzzy = Boolean(w.declaredClass.match(/form\.Text/));
226                 } else {
227                     var type = dojo.attr(w, "type");
228                     if (type)
229                         can_do_fuzzy = (type == "text");
230                     else
231                         can_do_fuzzy = false;
232                 }
233                 self.matchHowAllow(id, "__fuzzy", can_do_fuzzy);
234             }
235         );
236     };
237
238     this.addRow = function() {
239         var uniq = (this.rowId)++;
240
241         var row = dojo.clone(this.template);
242         dojo.attr(row, "id", "term-row-" + uniq);
243
244         var selector = this.selectorFactory.make(uniq);
245         dojo.attr(
246             selector,
247             "onchange",
248             function() { self.updateRowWidget(uniq); }
249         );
250
251         var match_how = dojo.query("select", nodeByName("match", row))[0];
252         dojo.attr(match_how, "id", "term-match-" + uniq);
253         dojo.attr(match_how, "selectedIndex", 0);
254
255         nodeByName("selector", row).appendChild(selector);
256         nodeByName("remove", row).appendChild(this.removerButton(uniq));
257
258         dojo.place(row, "acq-unified-terms-tbody", "last");
259     }
260
261     this.removeRow = function(id) {
262         delete this.widgets[id];
263         dojo.destroy(this._row(id));
264     };
265
266     this.buildSearchObject = function() {
267         var so = {};
268
269         for (var id in this.widgets) {
270             var attr_parts = this._selector(id).getValue().split(":");
271             if (attr_parts.length != 2)
272                 continue;
273
274             var hint = attr_parts[0];
275             var attr = attr_parts[1];
276             var match_how =
277                 this._match_how(id).getValue().split(",").filter(Boolean);
278
279             var value;
280             if (typeof(this.widgets[id].declaredClass) != "undefined") {
281                 if (this.widgets[id].declaredClass.match(/Date/)) {
282                     value =
283                         dojo.date.stamp.toISOString(this.widgets[id].value).
284                             split("T")[0];
285                 } else {
286                     value = this.widgets[id].attr("value");
287                 }
288             } else {
289                 value = this.widgets[id].value;
290             }
291
292             if (!so[hint])
293                 so[hint] = [];
294
295             var unit = {};
296             unit[attr] = value;
297             match_how.forEach(function(key) { unit[key] = true; });
298             if (this.terms[hint][attr].datatype == "timestamp")
299                 unit.__castdate = true;
300
301             so[hint].push(unit);
302         }
303         return so;
304     };
305 }
306
307 /* The result manager is used primarily when the users submits a search.  It
308  * consults the termManager to get the search query to send to the middl
309  * layer, and it chooses which ML method to call as well as what widgets to use
310  * to display the results.
311  */
312 function ResultManager(liTable, poGrid, plGrid) {
313     var self = this;
314
315     this.liTable = liTable;
316     this.poGrid = poGrid;
317     this.plGrid = plGrid;
318     this.poCache = {};
319     this.plCache = {};
320
321     this.result_types = {
322         "lineitem": {
323             "search_options": {
324                 "flesh_attrs": true,
325                 "flesh_cancel_reason": true,
326                 "flesh_notes": true
327             },
328             "revealer": function() {
329                 self.liTable.reset();
330                 self.liTable.show("list");
331             }
332         },
333         "purchase_order": {
334             "search_options": {
335                 "no_flesh_cancel_reason": true
336             },
337             "revealer": function() {
338                 self.poGrid.resetStore();
339                 self.poCache = {};
340             }
341         },
342         "picklist": {
343             "search_options": {
344                 "flesh_lineitem_count": true,
345                 "flesh_owner": true
346             },
347             "revealer": function() {
348                 self.plGrid.resetStore();
349                 self.plCache = {};
350             }
351         },
352         "no_results": {
353             "revealer": function() { alert(localeStrings.NO_RESULTS); }
354         }
355     };
356
357     this._add_lineitem = function(li) {
358         this.liTable.addLineitem(li);
359     };
360
361     this._add_purchase_order = function(po) {
362         this.poCache[po.id()] = po;
363         this.poGrid.store.newItem(acqpo.toStoreItem(po));
364     };
365
366     this._add_picklist = function(pl) {
367         this.plCache[pl.id()] = pl;
368         this.plGrid.store.newItem(acqpl.toStoreItem(pl));
369     };
370
371     this._finish_purchase_order = function() {
372         this.poGrid.hideLoadProgressIndicator();
373     };
374
375     this._finish_picklist = function() {
376         this.plGrid.hideLoadProgressIndicator();
377     };
378
379     this.add = function(which, what) {
380         var name = "_add_" + which;
381         if (this[name]) this[name](what);
382     };
383
384     this.finish = function(which) {
385         var name = "_finish_" + which;
386         if (this[name]) this[name]();
387     };
388
389     this.show = function(which) {
390         openils.Util.objectProperties(this.result_types).forEach(
391             function(rt) {
392                 openils.Util[rt == which ? "show" : "hide"](
393                     "acq-unified-results-" + rt
394                 );
395             }
396         );
397         this.result_types[which].revealer();
398     };
399
400     this.search = function(search_obj) {
401         var count_results = 0;
402         var result_type = dojo.byId("acq-unified-result-type").getValue();
403         var conjunction = dojo.byId("acq-unified-conjunction").getValue();
404
405         /* XXX TODO when result_type can be "lineitem_and_bib" there may be a
406          * totally different ML method to call; not sure how that will work
407          * yet. */
408         var method_name = "open-ils.acq." + result_type + ".unified_search";
409         var params = [
410             openils.User.authtoken,
411             null, null, null,
412             this.result_types[result_type].search_options
413         ];
414
415         params[conjunction == "and" ? 1 : 2] = search_obj;
416
417         fieldmapper.standardRequest(
418             ["open-ils.acq", method_name], {
419                 "params": params,
420                 "async": true,
421                 "onresponse": function(r) {
422                     if (r = openils.Util.readResponse(r)) {
423                         if (!count_results++)
424                             self.show(result_type);
425                         self.add(result_type, r);
426                     }
427                 },
428                 "oncomplete": function() {
429                     if (!count_results)
430                         self.show("no_results");
431                     else
432                         self.finish(result_type);
433                 }
434             }
435         );
436     }
437 }
438
439 /* The rest of the functions below handle the relatively unorganized
440  * miscellany of the search interface.
441  */
442
443 /* onload */
444 openils.Util.addOnLoad(
445     function() {
446         termManager = new TermManager();
447         termManager.addRow();
448         resultManager = new ResultManager(
449             new AcqLiTable(),
450             dijit.byId("acq-unified-po-grid"),
451             dijit.byId("acq-unified-pl-grid")
452         );
453         openils.Util.show("acq-unified-body");
454     }
455 );