Acq: unified search interface for LI, PO, and PL. Usable but not 100% finished
authorsenator <senator@dcc99617-32d9-48b4-a31d-7c20da2025e4>
Tue, 6 Apr 2010 16:40:33 +0000 (16:40 +0000)
committersenator <senator@dcc99617-32d9-48b4-a31d-7c20da2025e4>
Tue, 6 Apr 2010 16:40:33 +0000 (16:40 +0000)
Still to come:
    Paging results (very important with large result sets)
    Searchable timestamp fields (those don't work yet)
    Search terms interpreted from URI (to enable returning to search later)
    Misc PO and PL controls to enable acting on search results
    ** Bib record searching

git-svn-id: svn://svn.open-ils.org/ILS/trunk@16140 dcc99617-32d9-48b4-a31d-7c20da2025e4

12 files changed:
Open-ILS/examples/fm_IDL.xml
Open-ILS/src/perlmods/OpenILS/Application/Acq/Financials.pm
Open-ILS/src/perlmods/OpenILS/Application/Acq/Search.pm
Open-ILS/web/css/skin/default/acq.css
Open-ILS/web/js/dojo/openils/acq/nls/acq.js
Open-ILS/web/js/dojo/openils/widget/AutoFieldWidget.js
Open-ILS/web/js/ui/default/acq/search/unified.js [new file with mode: 0644]
Open-ILS/web/opac/locale/en-US/lang.dtd
Open-ILS/web/templates/default/acq/search/unified.tt2 [new file with mode: 0644]
Open-ILS/xul/staff_client/chrome/content/main/menu.js
Open-ILS/xul/staff_client/chrome/content/main/menu_frame_menus.xul
Open-ILS/xul/staff_client/chrome/locale/en-US/offline.properties

index 12ee483..bf822c3 100644 (file)
@@ -5238,7 +5238,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
                <fields oils_persist:primary="id" oils_persist:sequence="acq.picklist_id_seq">
                        <field reporter:label="Picklist ID" name="id" reporter:datatype="id" />
                        <field reporter:label="Owner" name="owner" reporter:datatype="link" />
-                       <field reporter:label="Org Unit" name="org_unit" reporter:datatype="link" />
+                       <field reporter:label="Org Unit" name="org_unit" reporter:datatype="org_unit" />
                        <field reporter:label="Name" name="name" reporter:datatype="text" oils_persist:i18n="true" />
                        <field reporter:label="Creation Time" name="create_time" reporter:datatype="timestamp" />
                        <field reporter:label="Edit Time" name="edit_time" reporter:datatype="timestamp" />
index e669e11..1dcb678 100644 (file)
@@ -899,19 +899,23 @@ sub build_price_summary {
 sub retrieve_purchase_order_impl {
     my($e, $po_id, $options) = @_;
 
-    # let's just always flesh this if it's there. what the hey.
-    my $flesh = {
-        "flesh" => 1, "flesh_fields" => {"acqpo" => ["cancel_reason"]}
-    };
+    my $flesh = {"flesh" => 1, "flesh_fields" => {"acqpo" => []}};
 
     $options ||= {};
+    unless ($options->{"no_flesh_cancel_reason"}) {
+        push @{$flesh->{"flesh_fields"}->{"acqpo"}}, "cancel_reason";
+    }
     if ($options->{"flesh_notes"}) {
         push @{$flesh->{"flesh_fields"}->{"acqpo"}}, "notes";
     }
     if ($options->{"flesh_provider"}) {
         push @{$flesh->{"flesh_fields"}->{"acqpo"}}, "provider";
     }
-    my $po = $e->retrieve_acq_purchase_order([$po_id, $flesh])
+
+    my $args = (@{$flesh->{"flesh_fields"}->{"acqpo"}}) ?
+        [$po_id, $flesh] : $po_id;
+
+    my $po = $e->retrieve_acq_purchase_order($args)
         or return $e->event;
 
     if($$options{flesh_lineitems}) {
index 16728b5..24e1b92 100644 (file)
@@ -156,37 +156,39 @@ sub gen_au_term {
 # alias, given names and family name.
 sub prepare_au_terms {
     my ($terms, $join_num) = @_;
+
     my @joins = ();
+    my $nots = 0;
     $join_num ||= 0;
 
     foreach my $conj (qw/-and -or/) {
         next unless exists $terms->{$conj};
 
         my @new_outer_terms = ();
-        foreach my $hint_unit (@{$terms->{$conj}}) {
+        HINT_UNIT: foreach my $hint_unit (@{$terms->{$conj}}) {
             my $hint = (keys %$hint_unit)[0];
             (my $plain_hint = $hint) =~ y/+//d;
+            if ($hint eq "-not") {
+                $hint_unit = $hint_unit->{$hint};
+                $nots++;
+                redo HINT_UNIT;
+            }
 
             if (my $links = get_fm_links_by_hint($plain_hint) and
                 $plain_hint ne "acqlia") {
                 my @new_terms = ();
-                foreach my $pair (@{$hint_unit->{$hint}}) {
-                    my ($attr, $value) = breakdown_term($pair);
-                    if ($links->{$attr} and
-                        $links->{$attr}->{"class"} eq "au") {
-                        push @joins, [$plain_hint, $attr, $join_num];
-                        push @new_outer_terms, gen_au_term($value, $join_num);
-                        $join_num++;
-                    } else {
-                        push @new_terms, $pair;
-                    }
-                }
-                if (@new_terms) {
-                    $hint_unit->{$hint} = [ @new_terms ];
-                } else {
+                my ($attr, $value) = breakdown_term($hint_unit->{$hint});
+                if ($links->{$attr} and
+                    $links->{$attr}->{"class"} eq "au") {
+                    push @joins, [$plain_hint, $attr, $join_num];
+                    my $au_term = gen_au_term($value, $join_num);
+                    $au_term = {"-not" => $au_term} if $nots--;
+                    push @new_outer_terms, $au_term;
+                    $join_num++;
                     delete $hint_unit->{$hint};
                 }
             }
+            $hint_unit = {"-not" => $hint_unit} if $nots--;
             push @new_outer_terms, $hint_unit if scalar keys %$hint_unit;
         }
         $terms->{$conj} = [ @new_outer_terms ];
@@ -203,7 +205,6 @@ sub prepare_terms {
     foreach my $class (qw/acqpo acqpl jub/) {
         next if not exists $terms->{$class};
 
-        my $clause = [];
         $outer_clause->{$conj} = [] unless $outer_clause->{$conj};
         foreach my $unit (@{$terms->{$class}}) {
             my ($k, $v, $fuzzy, $between, $not) = breakdown_term($unit);
@@ -218,9 +219,10 @@ sub prepare_terms {
                 next;
             }
 
-            push @$clause, $not ? {"-not" => $term_clause} : $term_clause;
+            my $clause = {"+" . $class => $term_clause};
+            $clause = {"-not" => $clause} if $not;
+            push @{$outer_clause->{$conj}}, $clause;
         }
-        push @{$outer_clause->{$conj}}, {"+" . $class => $clause};
     }
 
     if ($terms->{"acqlia"}) {
@@ -328,12 +330,12 @@ sub unified_search {
         "from" => {
             "jub" => {
                 "acqpo" => {
-                    "type" => "left",
+                    "type" => "full",
                     "field" => "id",
                     "fkey" => "purchase_order"
                 },
                 "acqpl" => {
-                    "type" => "left",
+                    "type" => "full",
                     "field" => "id",
                     "fkey" => "picklist"
                 }
@@ -352,6 +354,9 @@ sub unified_search {
         };
     };
 
+    # TODO find instances of fields of type "timestamp" and massage the
+    # comparison to match search input (which is only at date precision,
+    # not timestamp).
     my $offset = add_au_joins($query->{"from"}, prepare_au_terms($and_terms));
     add_au_joins($query->{"from"}, prepare_au_terms($or_terms, $offset));
 
@@ -369,7 +374,16 @@ sub unified_search {
     }
 
     my $results = $e->json_query($query) or return $e->die_event;
-    $conn->respond($retriever->($e, $_->{"id"}, $options)) foreach (@$results);
+    if ($options->{"id_list"}) {
+        foreach (@$results) {
+            $conn->respond($_->{"id"}) if $_->{"id"};
+        }
+    } else {
+        foreach (@$results) {
+            $conn->respond($retriever->($e, $_->{"id"}, $options))
+                if $_->{"id"};
+        }
+    }
     $e->disconnect;
     undef;
 }
index 1af2109..024f32d 100644 (file)
@@ -195,9 +195,24 @@ span[name="notes_alert_flag"] {color: #c00;font-weight: bold;font-size: 110%;mar
     border:1px solid #888;
 }
 
-
 /* INVOICING */
 #oils-acq-invoice-table td { padding: 5px; }
 #acq-invoice-new-msg { font-weight: bold; margin: 10px;}
 #acq-invoice-li-details { padding: 10px; font-weight: bold; border: 1px solid #888; margin: 10px; }
 #acq-invoice-create { margin: 10px; }
+#acq-unified-heading { margin-bottom: 10px; }
+#acq-unified-heading-actual { float: left; width: 50%; font-size: 120%; font-weight: bold; }
+#acq-unified-heading-controls { float: right; width: 50%; text-align: right; }
+#acq-unified-form { margin-bottom: 10px; border-bottom: 1px dashed #666; padding-bottom: 10px; }
+#acq-unified-form > div { margin: 6px 0; }
+option[disabled="disabled"] { font-style: italic; }
+#acq-unified-terms-table { width: 100%; }
+#acq-unified-terms-table td { padding: 4px 0; }
+#acq-unified-add-term { padding-bottom: 20px; }
+.acq-unified-option-header { padding-left: 12px; }
+.acq-unified-option-regular { padding-left: 24px; }
+.acq-unified-terms-selector { width: 20%; }
+.acq-unified-terms-widget { width: 60%; }
+.acq-unified-terms-match { width: 15%; }
+.acq-unified-terms-remove { width: 5%; text-align: right; }
+.acq-unified-remover { color: #c00; }
index 0ae9ffc..7b7b7a0 100644 (file)
@@ -57,4 +57,5 @@
     'CONFIRM_FUNDS_AT_STOP': "One or more of the selected funds has a balance below its stop level.\nYou may not be able to activate purchase orders incorporating these copies.\nContinue?",
     'CONFIRM_FUNDS_AT_WARNING': "One or more of the selected funds has a balance below its warning level.\nContinue?",
     'INVOICE_ITEM_DETAILS' : "${0} <br/> ${1} <br/> ${2}. <br/> Estimated Price: $${3}. <br/> Lineitem ID: ${4} <br/> PO: ${5} <br/> Order Date: ${6}",
+    'UNNAMED': "Unnamed"
 }
index 25c9919..838872b 100644 (file)
@@ -494,7 +494,7 @@ if(!dojo._hasResource['openils.widget.AutoFieldWidget']) {
             } else {
 
                 this.baseWidgetValue(this.widgetValue);
-                if(this.idlField.name == this.fmIDL.pkey && this.fmIDL.pkey_sequence && !this.selfReference)
+                if(this.idlField.name == this.fmIDL.pkey && this.fmIDL.pkey_sequence && (!this.selfReference && !this.noDisablePkey))
                     this.widget.attr('disabled', true); 
                 if(this.disableWidgetTest && this.disableWidgetTest(this.idlField.name, this.fmObject))
                     this.widget.attr('disabled', true); 
diff --git a/Open-ILS/web/js/ui/default/acq/search/unified.js b/Open-ILS/web/js/ui/default/acq/search/unified.js
new file mode 100644 (file)
index 0000000..4f19542
--- /dev/null
@@ -0,0 +1,445 @@
+dojo.require("openils.widget.AutoGrid");
+dojo.require("openils.widget.AutoWidget");
+dojo.require("openils.PermaCrud");
+dojo.require("openils.Util");
+
+var termSelectorFactory;
+var termManager;
+var resultManager;
+var pcrud = new openils.PermaCrud();
+
+/* typing save: add getValue() to all HTML <select> elements */
+HTMLSelectElement.prototype.getValue = function() {
+    return this.options[this.selectedIndex].value;
+}
+
+/* quickly find elements by the value of a "name" attribute */
+function nodeByName(name, root) {
+    return dojo.query("[name='" + name + "']", root)[0];
+}
+
+function hideForm() {
+    openils.Util.hide("acq-unified-hide-form");
+    openils.Util.show("acq-unified-reveal-form", "inline");
+    openils.Util.hide("acq-unified-form");
+}
+
+function revealForm() {
+    openils.Util.hide("acq-unified-reveal-form");
+    openils.Util.show("acq-unified-hide-form", "inline");
+    openils.Util.show("acq-unified-form");
+}
+
+/* The TermSelectorFactory will be instantiated by the TermManager. It
+ * provides HTML select controls whose options are all the searchable
+ * fields.  Selecting a field from one of these controls will create the
+ * appopriate type of corresponding widget for the user to enter a search
+ * term against the selected field.
+ */
+function TermSelectorFactory(terms) {
+    var self = this;
+    this.terms = terms;
+
+    this.template = dojo.create("select");
+    this.template.appendChild(
+        dojo.create("option", {
+            "disabled": "disabled",
+            "selected": "selected",
+            "value": "",
+            "innerHTML": "Select Search Field" // XXX i18n
+        })
+    );
+
+    /* Create abbreviations for class names to make field categories
+     * more readable in field selector control. */
+    this._abbreviate = function(s) {
+        var last, result;
+        for (var i = 0; i < s.length; i++) {
+            if (s[i] != " ") {
+                if (!i) result = s[i];
+                else if (last == " ") result += s[i];
+            }
+            last = s[i];
+        }
+        return result;
+    };
+
+    var selectorMethods = {
+        /* Important: within the following functions, "this" refers to one
+         * HTMLSelect object, and "self" refers to the TermSelectorFactory. */
+        "getTerm": function() {
+            var parts = this.getValue().split(":");
+            return {
+                "hint": parts[0],
+                "field": parts[1],
+                "datatype": self.terms[parts[0]][parts[1]].datatype
+            };
+        },
+        "makeWidget": function(parentNode, wStore, callback) {
+            var term = this.getTerm();
+            var widgetKey = this.uniq;
+            if (term.hint == "acqlia") {
+                wStore[widgetKey] = dojo.create(
+                    "input", {"type": "text"}, parentNode, "only"
+                );
+                wStore[widgetKey].focus();
+                if (typeof(callback) == "function")
+                    callback(term, widgetKey);
+            } else {
+                var widget = new openils.widget.AutoFieldWidget({
+                    "fmClass": term.hint,
+                    "fmField": term.field,
+                    "noDisablePkey": true,
+                    "parentNode": dojo.create("span", null, parentNode, "only")
+                });
+                widget.build(
+                    function(w) {
+                        wStore[widgetKey] = w;
+                        w.focus();
+                        if (typeof(callback) == "function")
+                            callback(term, widgetKey);
+                    }
+                );
+            }
+        }
+    }
+
+    for (var hint in this.terms) {
+        var optgroup = dojo.create(
+            "optgroup", {"value": "", "label": this.terms[hint].__label}
+        );
+        var prefix = this._abbreviate(this.terms[hint].__label);
+
+        for (var field in this.terms[hint]) {
+            if (!/^__/.test(field)) {
+                optgroup.appendChild(
+                    dojo.create("option", {
+                        "class": "acq-unified-option-regular",
+                        "value": hint + ":" + field,
+                        "innerHTML": prefix + " - " +
+                            this.terms[hint][field].label
+                    })
+                );
+            }
+        }
+
+        this.template.appendChild(optgroup);
+    }
+
+    this.make = function(n) {
+        var node = dojo.clone(this.template);
+        node.uniq = n;
+        dojo.attr(node, "id", "term-" + n);
+        for (var name in selectorMethods)
+            node[name] = selectorMethods[name];
+        return node;
+    };
+}
+
+/* The term manager retrieves information from the IDL about all the fields
+ * in the classes that we consider searchable for our purpose.  It maintains
+ * a dynamic HTML table of search terms, using the TermSelectorFactory
+ * to generate search field selectors, which in turn provide appropriate
+ * widgets for entering search terms.  The TermManager provides search term
+ * modifiers (fuzzy searching, not searching). The TermManager also handles
+ * adding and removing rows of search terms, as well as building the search
+ * query to pass to the middle layer from the search term widgets.
+ */
+function TermManager() {
+    var self = this;
+
+    this.terms = {};
+    ["jub", "acqpl", "acqpo"].forEach(
+        function(hint) {
+            var o = {};
+            o.__label = fieldmapper.IDL.fmclasses[hint].label;
+            fieldmapper.IDL.fmclasses[hint].fields.forEach(
+                function(field) {
+                    if (!field.virtual) {
+                        o[field.name] = {
+                            "label": field.label, "datatype": field.datatype
+                        };
+                    }
+                }
+            );
+            self.terms[hint] = o;
+        }
+    );
+
+    this.terms.acqlia = {"__label": fieldmapper.IDL.fmclasses.acqlia.label};
+    pcrud.retrieveAll("acqliad", {"order_by": {"acqliad": "id"}}).forEach(
+        function(def) {
+            self.terms.acqlia[def.id()] =
+                {"label": def.description(), "datatype": "text"}
+        }
+    );
+
+    this.selectorFactory = new TermSelectorFactory(this.terms);
+    this.template = dojo.byId("acq-unified-terms-tbody").
+        removeChild(dojo.byId("acq-unified-terms-row-tmpl"));
+    dojo.attr(this.template, "id");
+
+    this.rowId = 0;
+    this.widgets = {};
+
+    this._row = function(id) { return dojo.byId("term-row-" + id); };
+    this._selector = function(id) { return dojo.byId("term-" + id); };
+    this._match_how = function(id) { return dojo.byId("term-match-" + id); };
+
+    this.removerButton = function(n) {
+        return dojo.create("button", {
+            "innerHTML": "X",
+            "class": "acq-unified-remover",
+            "onclick": function() { self.removeRow(n); }
+        });
+    };
+
+    this.matchHowAllow = function(id, what, which) {
+        dojo.query(
+            "option[value*='" + what + "']", this._match_how(id)
+        ).forEach(function(o) { o.disabled = !which; });
+    };
+
+    this.getLinkTarget = function(term) {
+        return fieldmapper.IDL.fmclasses[term.hint].
+            field_map[term.field]["class"];
+    };
+
+    this.updateRowWidget = function(id) {
+        var where = nodeByName("widget", this._row(id));
+
+        delete this.widgets[id];
+        dojo.empty(where);
+
+        this._selector(id).makeWidget(
+            where, this.widgets,
+            function(term, key) {
+                var w = self.widgets[key];
+                var can_do_fuzzy;
+                if (term.datatype == "id") {
+                    can_do_fuzzy = false;
+                } else if (term.datatype == "link") {
+                    can_do_fuzzy = (self.getLinkTarget(term) == "au");
+                } else if (typeof(w.declaredClass) != "undefined") {
+                    can_do_fuzzy = Boolean(w.declaredClass.match(/form\.Text/));
+                } else {
+                    var type = dojo.attr(w, "type");
+                    if (type)
+                        can_do_fuzzy = (type == "text");
+                    else
+                        can_do_fuzzy = false;
+                }
+                self.matchHowAllow(id, "__fuzzy", can_do_fuzzy);
+            }
+        );
+    };
+
+    this.addRow = function() {
+        var uniq = (this.rowId)++;
+
+        var row = dojo.clone(this.template);
+        dojo.attr(row, "id", "term-row-" + uniq);
+
+        var selector = this.selectorFactory.make(uniq);
+        dojo.attr(
+            selector,
+            "onchange",
+            function() { self.updateRowWidget(uniq); }
+        );
+
+        var match_how = dojo.query("select", nodeByName("match", row))[0];
+        dojo.attr(match_how, "id", "term-match-" + uniq);
+        dojo.attr(match_how, "selectedIndex", 0);
+
+        nodeByName("selector", row).appendChild(selector);
+        nodeByName("remove", row).appendChild(this.removerButton(uniq));
+
+        dojo.place(row, "acq-unified-terms-tbody", "last");
+    }
+
+    this.removeRow = function(id) {
+        delete this.widgets[id];
+        dojo.destroy(this._row(id));
+    };
+
+    this.buildSearchObject = function() {
+        var so = {};
+
+        for (var id in this.widgets) {
+            var attr_parts = this._selector(id).getValue().split(":");
+            if (attr_parts.length != 2)
+                continue;
+
+            var hint = attr_parts[0];
+            var attr = attr_parts[1];
+            var match_how =
+                this._match_how(id).getValue().split(",").filter(Boolean);
+
+            var value;
+            try {
+                value = this.widgets[id].attr("value");
+            } catch (E) {
+                value = this.widgets[id].value;
+            }
+
+            if (!so[hint])
+                so[hint] = [];
+            var unit = {};
+            match_how.forEach(function(key) { unit[key] = true; });
+            unit[attr] = value;
+
+            so[hint].push(unit);
+        }
+        return so;
+    };
+}
+
+/* The result manager is used primarily when the users submits a search.  It
+ * consults the termManager to get the search query to send to the middl
+ * layer, and it chooses which ML method to call as well as what widgets to use
+ * to display the results.
+ */
+function ResultManager(liTable, poGrid, plGrid) {
+    var self = this;
+
+    this.liTable = liTable;
+    this.poGrid = poGrid;
+    this.plGrid = plGrid;
+    this.poCache = {};
+    this.plCache = {};
+
+    this.result_types = {
+        "lineitem": {
+            "search_options": {
+                "flesh_attrs": true,
+                "flesh_cancel_reason": true,
+                "flesh_notes": true
+            },
+            "revealer": function() {
+                self.liTable.reset();
+                self.liTable.show("list");
+            }
+        },
+        "purchase_order": {
+            "search_options": {
+                "no_flesh_cancel_reason": true
+            },
+            "revealer": function() {
+                self.poGrid.resetStore();
+                self.poCache = {};
+            }
+        },
+        "picklist": {
+            "search_options": {
+                "flesh_lineitem_count": true,
+                "flesh_owner": true
+            },
+            "revealer": function() {
+                self.plGrid.resetStore();
+                self.plCache = {};
+            }
+        },
+        "no_results": {
+            "revealer": function() { alert(localeStrings.NO_RESULTS); }
+        }
+    };
+
+    this._add_lineitem = function(li) {
+        this.liTable.addLineitem(li);
+    };
+
+    this._add_purchase_order = function(po) {
+        this.poCache[po.id()] = po;
+        this.poGrid.store.newItem(acqpo.toStoreItem(po));
+    };
+
+    this._add_picklist = function(pl) {
+        this.plCache[pl.id()] = pl;
+        this.plGrid.store.newItem(acqpl.toStoreItem(pl));
+    };
+
+    this._finish_purchase_order = function() {
+        this.poGrid.hideLoadProgressIndicator();
+    };
+
+    this._finish_picklist = function() {
+        this.plGrid.hideLoadProgressIndicator();
+    };
+
+    this.add = function(which, what) {
+        var name = "_add_" + which;
+        if (this[name]) this[name](what);
+    };
+
+    this.finish = function(which) {
+        var name = "_finish_" + which;
+        if (this[name]) this[name]();
+    };
+
+    this.show = function(which) {
+        openils.Util.objectProperties(this.result_types).forEach(
+            function(rt) {
+                openils.Util[rt == which ? "show" : "hide"](
+                    "acq-unified-results-" + rt
+                );
+            }
+        );
+        this.result_types[which].revealer();
+    };
+
+    this.search = function(search_obj) {
+        var count_results = 0;
+        var result_type = dojo.byId("acq-unified-result-type").getValue();
+        var conjunction = dojo.byId("acq-unified-conjunction").getValue();
+
+        /* XXX TODO when result_type can be "lineitem_and_bib" there may be a
+         * totally different ML method to call; not sure how that will work
+         * yet. */
+        var method_name = "open-ils.acq." + result_type + ".unified_search";
+        var params = [
+            openils.User.authtoken,
+            null, null, null,
+            this.result_types[result_type].search_options
+        ];
+
+        params[conjunction == "and" ? 1 : 2] = search_obj;
+
+        fieldmapper.standardRequest(
+            ["open-ils.acq", method_name], {
+                "params": params,
+                "async": true,
+                "onresponse": function(r) {
+                    if (r = openils.Util.readResponse(r)) {
+                        if (!count_results++)
+                            self.show(result_type);
+                        self.add(result_type, r);
+                    }
+                },
+                "oncomplete": function() {
+                    if (!count_results)
+                        self.show("no_results");
+                    else
+                        self.finish(result_type);
+                }
+            }
+        );
+    }
+}
+
+/* The rest of the functions below handle the relatively unorganized
+ * miscellany of the search interface.
+ */
+
+/* onload */
+openils.Util.addOnLoad(
+    function() {
+        termManager = new TermManager();
+        termManager.addRow();
+        resultManager = new ResultManager(
+            new AcqLiTable(),
+            dijit.byId("acq-unified-po-grid"),
+            dijit.byId("acq-unified-pl-grid")
+        );
+        openils.Util.show("acq-unified-body");
+    }
+);
index 4dccd85..4667a73 100644 (file)
 <!ENTITY staff.main.menu.acq.bib_search.accesskey "T">
 <!ENTITY staff.main.menu.acq.li_search.label "Lineitem Search">
 <!ENTITY staff.main.menu.acq.li_search.accesskey "I">
+<!ENTITY staff.main.menu.acq.unified_search.label "Acquisitions Search">
+<!ENTITY staff.main.menu.acq.unified_search.accesskey "A">
 <!ENTITY staff.main.menu.acq.brief_record.label "New Brief Record">
 <!ENTITY staff.main.menu.acq.brief_record.accesskey "B">
 <!ENTITY staff.main.menu.acq.upload.label "Load Order Record">
diff --git a/Open-ILS/web/templates/default/acq/search/unified.tt2 b/Open-ILS/web/templates/default/acq/search/unified.tt2
new file mode 100644 (file)
index 0000000..b400d4e
--- /dev/null
@@ -0,0 +1,167 @@
+[% WRAPPER "default/base.tt2" %]
+[% ctx.page_title = "Acquisitions Search" %]
+<script src="[% ctx.media_prefix %]/js/ui/default/acq/search/unified.js">
+</script>
+<script>
+    /* The only functions in this <script> element are for
+     * formatting/getting fields for autogrids. */
+    function getName(rowIndex, item) {
+        if (item) {
+            return {
+                "name": this.grid.store.getValue(item, "name") ||
+                    localeStrings.UNNAMED,
+                "id": this.grid.store.getValue(item, "id")
+            };
+        }
+    }
+
+    function getPlOwnerName(rowIndex, item) {
+        try {
+            return resultManager.plCache[this.grid.store.getValue(item, "id")].
+                owner().usrname();
+        } catch (E) {
+            return "";
+        }
+    }
+
+    function formatPoName(po) {
+        if (po) {
+            return "<a href='" + oilsBasePath + "/acq/po/view/" + po.id +
+                "'>" + po.name + "</a>";
+        }
+    }
+
+    function formatPlName(pl) {
+        if (pl) {
+            return "<a href='" + oilsBasePath + "/acq/picklist/view/" +
+                pl.id + "'>" + pl.name + "</a>";
+        }
+    }
+</script>
+<!-- later: "[% ctx.page_args.0 %]" -->
+<div id="acq-unified-body" class="hidden">
+    <div id="acq-unified-heading">
+        <span id="acq-unified-heading-actual">Acquisitions Search</span>
+        <span id="acq-unified-heading-controls">
+            <button id="acq-unified-hide-form" onclick="hideForm();">
+                Hide Search Form
+            </button>
+            <button id="acq-unified-reveal-form" onclick="revealForm();"
+                class="hidden">
+                Reveal Search Form
+            </button>
+        </span>
+        <div style="clear: both;"><!-- layout; don't remove --></div>
+    </div>
+    <div id="acq-unified-form">
+        <div>
+            <label for="acq-unified-result-type">Search for</label>
+            <select id="acq-unified-result-type">
+                <option value="lineitem">line items</option>
+                <option value="lineitem_and_bib" disabled="disabled">
+                    <!-- XXX not yet implemented -->
+                    line items &amp; catalog records
+                </option>
+                <option value="picklist">pick lists</option>
+                <option value="purchase_order">purchase orders</option>
+            </select>
+            <label for="acq-unified-conjunction">matching</label>
+            <select id="acq-unified-conjunction">
+                <option value="or">any</option>
+                <option value="and">all</option>
+            </select>
+            <label for="acq-unified-conjunction">
+                of the following terms:
+            </label>
+        </div>
+        <div id="acq-unified-terms">
+            <table id="acq-unified-terms-table">
+                <tbody id="acq-unified-terms-tbody">
+                    <tr id="acq-unified-terms-row-tmpl"
+                        class="acq-unified-terms-row">
+                        <td name="selector"
+                            class="acq-unified-terms-selector"></td>
+                        <td name="match"
+                            class="acq-unified-terms-match">
+                            <select>
+                                <option value="">matches exactly</option>
+                                <option value="__not">
+                                    does NOT match exactly
+                                </option>
+                                <option value="__fuzzy">contains</option>
+                                <option value="__not,__fuzzy">
+                                    does NOT contain
+                                </option>
+                            </select>
+                        </td>
+                        <td name="widget"
+                            class="acq-unified-terms-widget"></td>
+                        <td name="remove"
+                            class="acq-unified-terms-remove"></td>
+                    </tr>
+                </tbody>
+            </table>
+        </div>
+        <div id="acq-unified-add-term">
+            <button onclick="termManager.addRow()">Add Search Term</button>
+        </div>
+        <div>
+            <button
+                onclick="resultManager.search(termManager.buildSearchObject())">
+                Search
+            </button>
+        </div>
+    </div>
+    <div id="acq-unified-results-purchase_order" class="hidden">
+        <table
+            id="acq-unified-po-grid"
+            autoHeight="true"
+            dojoType="openils.widget.AutoGrid"
+            query="{id: '*'}"
+            fieldOrder="['name', 'owner', 'ordering_agency', 'provider',
+                'create_time', 'edit_time', 'state']"
+            suppressFields="['owner', 'editor', 'creator']"
+            defaultCellWidth="'auto'"
+            showPaginator="true"
+            fmClass="acqpo">
+            <thead>
+                <tr>
+                    <th field="name" get="getName" formatter="formatPoName">
+                        Name
+                    </th>
+                </tr>
+            </thead>
+        </table>
+    </div>
+    <div id="acq-unified-results-picklist" class="hidden">
+        <table
+            id="acq-unified-pl-grid"
+            autoHeight="true"
+            dojoType="openils.widget.AutoGrid"
+            query="{id: '*'}"
+            fieldOrder="['name', 'owner', 'entry_count',
+                'create_time', 'edit_time']"
+            suppressFields="['editor', 'creator']"
+            defaultCellWidth="'auto'"
+            showPaginator="true"
+            fmClass="acqpl">
+            <thead>
+                <tr>
+                    <th field="name" get="getName" formatter="formatPlName">
+                        Name
+                    </th>
+                    <th field="owner" get="getPlOwnerName">Owner</th>
+                    <th field="entry_count">Entry Count</th>
+                </tr>
+            </thead>
+        </table>
+    </div>
+    <div id="acq-unified-results-no_results" class="hidden">
+        There are no results from your search.
+    </div>
+    <div id="acq-unified-results-lineitem" class="hidden">
+        [% INCLUDE "default/acq/common/li_table.tt2" %]
+    </div>
+</div>
+
+[% END %]
index eca544c..02f5ae4 100644 (file)
@@ -691,6 +691,10 @@ main.menu.prototype = {
                 ['oncommand'],
                 function() { open_eg_web_page('acq/picklist/bib_search', 'menu.cmd_acq_bib_search.tab'); }
             ],
+            'cmd_acq_unified_search' : [
+                ['oncommand'],
+                function() { open_eg_web_page('acq/search/unified', 'menu.cmd_acq_unified_search.tab'); }
+            ],
             'cmd_acq_li_search' : [
                 ['oncommand'],
                 function() { open_eg_web_page('acq/lineitem/search', 'menu.cmd_acq_li_search.tab'); }
index bfbc461..2caaa9f 100644 (file)
@@ -83,6 +83,7 @@
     <command id="cmd_acq_user_requests" />
     <command id="cmd_acq_bib_search" />
     <command id="cmd_acq_li_search" />
+    <command id="cmd_acq_unified_search" />
     <command id="cmd_acq_new_brief_record" />
     <command id="cmd_acq_view_fund" />
     <command id="cmd_acq_view_funding_source" />
 <!-- The Acquisitions menu on the main menu -->
 <menu id="main.menu.acq" label="&staff.main.menu.acq.label;" accesskey="&staff.main.menu.acq.accesskey;">
     <menupopup id="main.menu.acq.popup">
-        <menuitem label="&staff.main.menu.acq.picklist.label;" accesskey="&staff.main.menu.acq.picklist.accesskey;" command="cmd_acq_view_picklist"/>
+        <menuitem label="&staff.main.menu.acq.unified_search.label;" accesskey="&staff.main.menu.acq.unified_search.accesskey;" command="cmd_acq_unified_search"/>
         <menuitem label="&staff.main.menu.acq.bib_search.label;" accesskey="&staff.main.menu.acq.bib_search.accesskey;" command="cmd_acq_bib_search"/>
         <menuitem label="&staff.main.menu.acq.li_search.label;" accesskey="&staff.main.menu.acq.li_search.accesskey;" command="cmd_acq_li_search"/>
+        <menuitem label="&staff.main.menu.acq.po.label;" accesskey="&staff.main.menu.acq.po.accesskey;" command="cmd_acq_view_po" />
+        <menuitem label="&staff.main.menu.acq.picklist.label;" accesskey="&staff.main.menu.acq.picklist.accesskey;" command="cmd_acq_view_picklist"/>
+        <menuseparator />
         <menuitem label="&staff.main.menu.acq.upload.label;" accesskey="&staff.main.menu.acq.upload.accesskey;" command="cmd_acq_upload"/>
         <menuitem label="&staff.main.menu.acq.brief_record.label;" accesskey="&staff.main.menu.acq.brief_record.accesskey;" command="cmd_acq_new_brief_record"/>
-        <menuseparator />
-        <menuitem label="&staff.main.menu.acq.po.label;" accesskey="&staff.main.menu.acq.po.accesskey;" command="cmd_acq_view_po" />
         <menuitem label="&staff.main.menu.acq.po_events.label;" accesskey="&staff.main.menu.acq.po_events.accesskey;" command="cmd_acq_view_po_events" />
         <menuitem label="&staff.main.menu.acq.user_requests.label;" accesskey="&staff.main.menu.acq.user_requests.accesskey;" command="cmd_acq_user_requests" />
         <menuseparator />
index 2c41c0a..916e158 100644 (file)
@@ -222,6 +222,7 @@ menu.cmd_local_admin_cash_reports.tab=Cash Reports
 menu.cmd_local_admin_transit_list.tab=Transits
 menu.cmd_acq_view_picklist.tab=Selection Lists
 menu.cmd_acq_bib_search.tab=Title Search
+menu.cmd_acq_unified_search.tab=Acquisitions Search
 menu.cmd_acq_li_search.tab=Lineitem Search
 menu.cmd_acq_upload.tab=Load Order Record
 menu.cmd_acq_new_brief_record.tab=New Brief Record