Angular Acquistions Search
authorGalen Charlton <gmc@equinoxinitiative.org>
Tue, 22 Oct 2019 19:29:40 +0000 (15:29 -0400)
committerGalen Charlton <gmc@equinoxinitiative.org>
Mon, 3 Feb 2020 17:15:26 +0000 (12:15 -0500)
Angular app + linking AngularJS navigation elements to it

Signed-off-by: Galen Charlton <gmc@equinoxinitiative.org>
31 files changed:
Open-ILS/src/eg2/src/app/staff/acq/routing.module.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/acq/search/acq-search-form.component.css [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/acq/search/acq-search-form.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/acq/search/acq-search-form.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/acq/search/acq-search.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/acq/search/acq-search.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/acq/search/acq-search.module.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/acq/search/acq-search.service.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/acq/search/invoice-results.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/acq/search/invoice-results.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/acq/search/lineitem-results.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/acq/search/lineitem-results.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/acq/search/picklist-clone-dialog.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/acq/search/picklist-clone-dialog.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/acq/search/picklist-create-dialog.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/acq/search/picklist-create-dialog.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/acq/search/picklist-delete-dialog.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/acq/search/picklist-delete-dialog.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/acq/search/picklist-merge-dialog.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/acq/search/picklist-merge-dialog.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/acq/search/picklist-results.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/acq/search/picklist-results.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/acq/search/purchase-order-results.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/acq/search/purchase-order-results.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/acq/search/routing.module.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/nav.component.html
Open-ILS/src/eg2/src/app/staff/nav.component.ts
Open-ILS/src/eg2/src/app/staff/routing.module.ts
Open-ILS/src/eg2/src/styles.css
Open-ILS/src/templates/staff/navbar.tt2
Open-ILS/web/js/ui/default/staff/services/navbar.js

diff --git a/Open-ILS/src/eg2/src/app/staff/acq/routing.module.ts b/Open-ILS/src/eg2/src/app/staff/acq/routing.module.ts
new file mode 100644 (file)
index 0000000..1305bd0
--- /dev/null
@@ -0,0 +1,15 @@
+import {NgModule} from '@angular/core';
+import {RouterModule, Routes} from '@angular/router';
+
+const routes: Routes = [
+  { path: 'search',
+    loadChildren: './search/acq-search.module#AcqSearchModule'
+  }
+];
+
+@NgModule({
+  imports: [RouterModule.forChild(routes)],
+  exports: [RouterModule]
+})
+
+export class AcqRoutingModule {}
diff --git a/Open-ILS/src/eg2/src/app/staff/acq/search/acq-search-form.component.css b/Open-ILS/src/eg2/src/app/staff/acq/search/acq-search-form.component.css
new file mode 100644 (file)
index 0000000..8842d11
--- /dev/null
@@ -0,0 +1,5 @@
+#acq-search-form {
+  border-radius: 0px 0px 7px 7px;
+  background-color: rgb(247, 247, 247);
+  box-shadow: 1px 2px 3px -1px rgba(0, 0, 0, .2);
+}
diff --git a/Open-ILS/src/eg2/src/app/staff/acq/search/acq-search-form.component.html b/Open-ILS/src/eg2/src/app/staff/acq/search/acq-search-form.component.html
new file mode 100644 (file)
index 0000000..53a6152
--- /dev/null
@@ -0,0 +1,142 @@
+<div id="acq-search-form" class="pl-3 pr-3 pt-3 pb-3">
+<form>
+  <div class="row mb-1">
+    <div class="col-lg-5 form-group form-inline">
+      <label i18n>Search for records matching
+      <select class="form-inline ml-1 mr-1" id="acq-search-conjunction" [ngModelOptions]="{standalone: true}" [(ngModel)]="searchConjunction">
+        <option i18n select value="all">all</option>
+        <option i18n select value="any">any</option>
+      </select>
+      of the following terms:</label>
+    </div>
+    <div class="col-lg-6"></div>
+    <div class="col-lg-1">
+      <a class="with-material-icon no-href text-primary"
+        title="Show Form" i18n-title
+        *ngIf="!showForm" (click)="showForm=true">
+        <span class="material-icons">expand_more</span>
+      </a>
+      <a class="with-material-icon no-href text-primary"
+        title="Hide Form" i18n-title
+        *ngIf="showForm" (click)="showForm=false">
+        <span class="material-icons">expand_less</span>
+      </a>
+    </div>
+  </div>
+  <div class="row mb-1" *ngFor="let t of searchTerms; let idx=index" [hidden]="!showForm">
+    <div class="col-lg-3">
+      <select class="form-control" id="selected-search-term" [ngModelOptions]="{standalone: true}" [(ngModel)]="t.field"
+        (change)="clearSearchTerm(t)">
+        <option disabled="disabled" i18n>Select Search Field</option>
+        <optgroup *ngFor="let g of hints" label="{{availableSearchFields[g]['__label']}}">
+          <option *ngFor="let o of availableSearchFields[g]['__fields']" value="{{g}}:{{o}}">
+            {{availableSearchFields[g]['__label']}} - {{availableSearchFields[g][o].label}}
+          </option>
+        </optgroup>
+      </select>
+    </div>
+    <div class="col-lg-2">
+      <select class="form-control" id="selected-search-op" [ngModelOptions]="{standalone: true}" [(ngModel)]="t.op"
+        (change)="clearSearchTermValueAfterOpChange(t)">
+        <option i18n value="">is</option>
+        <option i18n value="__not">is NOT</option>
+        <option i18n value="__fuzzy" [disabled]="searchTermDatatypes[t.field] != 'text' && searchFieldLinkedClasses[t.field] !== 'acqpro'">contains</option>
+        <option i18n value="__not,__fuzzy" [disabled]="searchTermDatatypes[t.field] != 'text'">does NOT contain</option>
+        <option i18n value="__starts" [disabled]="searchTermDatatypes[t.field] != 'text'">STARTS with</option>
+        <option i18n value="__ends" [disabled]="searchTermDatatypes[t.field] != 'text'">ENDS with</option>
+        <option i18n value="__lte" [disabled]="searchTermDatatypes[t.field] != 'timestamp'">is on or BEFORE</option>
+        <option i18n value="__gte" [disabled]="searchTermDatatypes[t.field] != 'timestamp'">is on or AFTER</option>
+        <option i18n value="__between" [disabled]="searchTermDatatypes[t.field] != 'timestamp'">is BETWEEN</option>
+        <option i18n value="__age" [disabled]="searchTermDatatypes[t.field] != 'timestamp'">age (relative date)</option>
+        <option i18n value="__in">matches a term from a file</option>
+      </select>
+    </div>
+    <div class="col-lg-3">
+      <ng-container *ngIf="t.op == '__in'">
+        <eg-file-reader [(ngModel)]="t.value1" [ngModelOptions]="{standalone: true}"></eg-file-reader>
+      </ng-container>
+      <ng-container *ngIf="t.op !== '__in'">
+       <div *ngIf="t.field.endsWith(':state') && (t.op === '' || t.op === '__not'); else notStateField">
+         <select class="form-control" [ngModelOptions]="{standalone: true}" [(ngModel)]="t.value1">
+            <option i18n value="new">New</option>
+            <option i18n *ngIf="!t.field.startsWith('acqpo')" value="selector-ready">Selector-Ready</option>
+            <option i18n *ngIf="!t.field.startsWith('acqpo')" value="order-ready">Order-Ready</option>
+            <option i18n *ngIf="!t.field.startsWith('acqpo')" value="approved">Approved</option>
+            <option i18n *ngIf="t.field.startsWith('acqpo')" value="pending">Pending</option>
+            <option i18n *ngIf="!t.field.startsWith('acqpo')" value="pending-order">Pending-Order</option>
+            <option i18n value="on-order">On-Order</option>
+            <option i18n value="received">Received</option>
+            <option i18n value="cancelled">Cancelled</option>
+         </select>
+       </div>
+       <ng-template #notStateField>
+        <input [ngModelOptions]="{standalone: true}" [(ngModel)]="t.value1" type="text" *ngIf="searchTermDatatypes[t.field] == 'id'" class="form-control" />
+        <input [ngModelOptions]="{standalone: true}" [(ngModel)]="t.value1" type="text" *ngIf="searchTermDatatypes[t.field] == 'text'" class="form-control" />
+        <input [ngModelOptions]="{standalone: true}" [(ngModel)]="t.value1" type="number" *ngIf="searchTermDatatypes[t.field] == 'int'" class="form-control" />
+        <input [ngModelOptions]="{standalone: true}" [(ngModel)]="t.value1" type="number" *ngIf="searchTermDatatypes[t.field] == 'money'" class="form-control" />
+        <eg-org-select *ngIf="searchTermDatatypes[t.field] == 'org_unit'"
+          [initialOrgId]="t.value1"
+          (onChange)="setOrgUnitSearchValue($event, t)">
+        </eg-org-select>
+        <ng-container *ngIf="searchTermDatatypes[t.field] == 'link'">
+          <ng-container *ngIf="searchFieldLinkedClasses[t.field] === 'acqpro'">
+            <eg-combobox *ngIf="t.op != '__fuzzy'"
+              [idlClass]="searchFieldLinkedClasses[t.field]"
+              [selectedId]="t.value1"
+              (onChange)="t.value1 = $event ? $event.id : ''">
+            </eg-combobox>
+            <input [ngModelOptions]="{standalone: true}" [(ngModel)]="t.value1" type="text" *ngIf="t.op == '__fuzzy'" class="form-control" />
+          </ng-container>
+          <ng-container *ngIf="searchFieldLinkedClasses[t.field] !== 'acqpro'">
+            <eg-combobox
+              [idlClass]="searchFieldLinkedClasses[t.field]"
+              [selectedId]="t.value1"
+              (onChange)="t.value1 = $event ? $event.id : ''">
+            </eg-combobox>
+          </ng-container>
+        </ng-container>
+        <eg-date-select *ngIf="searchTermDatatypes[t.field] == 'timestamp' && t.op != '__age'"
+          (onChangeAsIso)="t.value1 = $event ? $event : ''; t.is_date = true">
+        </eg-date-select>
+        <ng-container *ngIf="searchTermDatatypes[t.field] == 'timestamp' && t.op == '__between'">
+          <span i18n>and</span>
+          <eg-date-select
+            (onChangeAsIso)="t.value2 = $event ? $event : ''; t.is_date = true">
+          </eg-date-select>
+        </ng-container>
+        <eg-interval-input *ngIf="searchTermDatatypes[t.field] == 'timestamp' && t.op == '__age'"
+          [ngModelOptions]="{standalone: true}" [(ngModel)]="t.value1">
+        </eg-interval-input>
+       </ng-template>
+      </ng-container>
+    </div>
+    <div class="col-lg-2 pl-0 pr-1">
+      <button class="btn btn-sm material-icon-button" type="button"
+        (click)="addSearchTerm()"
+        i18n-title title="Add Search Row">
+        <span class="material-icons">add_circle_outline</span>
+      </button>
+      <button class="btn btn-sm material-icon-button" type="button"
+        (click)="delSearchTerm(idx)"
+        i18n-title title="Remove Search Row">
+        <span class="material-icons">remove_circle_outline</span>
+      </button>
+    </div>
+  </div>
+  <div class="row" [hidden]="!showForm">
+    <div class="col-lg-2">
+      <button class="form-control btn btn-success" (click)="submitSearch()" type="submit" i18n>Search</button>
+    </div>
+    <div class="col-lg-5"></div>
+    <div class="col-lg-2">
+      <input class="form-check-input" type="checkbox" id="retrieve-immediately"
+             (change)="saveRunImmediately()"
+             [ngModelOptions]="{standalone: true}" [(ngModel)]="runImmediately"/>
+      <label for="retrieve-immediately" class="form-check-label" i18n>Retrieve Results Immediately</label>
+    </div>
+    <div class="col-lg-3">
+      <button class="form-control btn btn-primary" (click)="saveSearchAsDefault()" type="button" i18n>Set As Default {{searchTypeLabel}} Search</button>
+    </div>
+  </div>
+</form>
+</div>
diff --git a/Open-ILS/src/eg2/src/app/staff/acq/search/acq-search-form.component.ts b/Open-ILS/src/eg2/src/app/staff/acq/search/acq-search-form.component.ts
new file mode 100644 (file)
index 0000000..785e0a6
--- /dev/null
@@ -0,0 +1,173 @@
+import {Component, OnInit, AfterViewInit, Input, Output, EventEmitter} from '@angular/core';
+import {NgbTabset, NgbTabChangeEvent} from '@ng-bootstrap/ng-bootstrap';
+import {Router, ActivatedRoute} from '@angular/router';
+import {StaffCommonModule} from '@eg/staff/common.module';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {AcqSearchTerm, AcqSearch} from './acq-search.service';
+import {ServerStoreService} from '@eg/core/server-store.service';
+
+@Component({
+  selector: 'eg-acq-search-form',
+  styleUrls: ['acq-search-form.component.css'],
+  templateUrl: './acq-search-form.component.html'
+})
+
+export class AcqSearchFormComponent implements OnInit, AfterViewInit {
+
+    @Input() initialSearchTerms: AcqSearchTerm[] = [];
+    @Input() defaultSearchSetting = '';
+    @Input() runImmediatelySetting = '';
+    @Input() searchTypeLabel = '';
+
+    @Output() searchSubmitted = new EventEmitter<AcqSearch>();
+
+    showForm = true;
+
+    hints = ['jub', 'acqpl', 'acqpo', 'acqinv', 'acqlid'];
+    availableSearchFields = {};
+    searchTermDatatypes = {};
+    searchFieldLinkedClasses = {};
+    validSearchTypes = ['lineitems', 'purchaseorders', 'invoices', 'selectionlists'];
+    defaultSearchType = 'lineitems';
+    searchConjunction = 'all';
+    runImmediately = false;
+
+    searchTerms: AcqSearchTerm[] = [];
+
+    constructor(
+        private router: Router,
+        private route: ActivatedRoute,
+        private pcrud: PcrudService,
+        private store: ServerStoreService,
+        private idl: IdlService,
+    ) {}
+
+    ngOnInit() {
+        const self = this;
+
+        this.store.getItem(this.runImmediatelySetting).then(val => {
+            this.runImmediately = val;
+
+            this.hints.forEach(
+                function(hint) {
+                    const o = {};
+                    o['__label'] = self.idl.classes[hint].label;
+                    o['__fields'] = [];
+                    self.idl.classes[hint].fields.forEach(
+                        function(field) {
+                            if (!field.virtual) {
+                                o['__fields'].push(field.name);
+                                o[field.name] = {
+                                    label: field.label,
+                                    datatype: field.datatype
+                                };
+                                self.searchTermDatatypes[hint + ':' + field.name] = field.datatype;
+                                if (field.datatype === 'link') {
+                                    self.searchFieldLinkedClasses[hint + ':' + field.name] = field.class;
+                                }
+                            }
+                        }
+                    );
+                    self.availableSearchFields[hint] = o;
+                }
+            );
+
+            this.hints.push('acqlia');
+            this.availableSearchFields['acqlia'] = {'__label': this.idl.classes.acqlia.label, '__fields': []};
+            this.pcrud.retrieveAll('acqliad', {'order_by': {'acqliad': 'id'}})
+            .subscribe(liad => {
+                this.availableSearchFields['acqlia']['__fields'].push('' + liad.id());
+                this.availableSearchFields['acqlia'][liad.id()] = {
+                    label: liad.description(),
+                    datatype: 'text'
+                };
+                this.searchTermDatatypes['acqlia:' + liad.id()] = 'text';
+            });
+
+            if (this.initialSearchTerms.length > 0) {
+                this.searchTerms = JSON.parse(JSON.stringify(this.initialSearchTerms)); // deep copy
+                this.submitSearch(); // if we've been passed an initial search, e.g., via a URL, assume
+                                     // we want the results immediately regardless of the workstation
+                                     // setting
+            } else {
+                this.store.getItem(this.defaultSearchSetting).then(
+                    defaultSearch => {
+                        if (defaultSearch) {
+                            this.searchTerms = JSON.parse(JSON.stringify(defaultSearch.terms));
+                            this.searchConjunction = defaultSearch.conjunction;
+                        } else {
+                            this.addSearchTerm();
+                        }
+                        if (this.runImmediately) {
+                            this.submitSearch();
+                        }
+                    }
+                );
+            }
+        });
+    }
+
+    ngAfterViewInit() {}
+
+    addSearchTerm() {
+        this.searchTerms.push({ field: '', op: '', value1: '', value2: '' });
+    }
+    delSearchTerm(index: number) {
+        if (this.searchTerms.length < 2) {
+            this.clearSearchTerm(this.searchTerms[0]);
+        } else {
+            this.searchTerms.splice(index, 1);
+        }
+    }
+    clearSearchTerm(term: AcqSearchTerm) {
+        term.value1 = '';
+        term.value2 = '';
+        term.is_date = false;
+
+        if (term.field.startsWith('acqlia:') && term.op === '') {
+            // default operator for line item attributes should be "contains"
+            term.op = '__fuzzy';
+        } else if (this.searchTermDatatypes[term.field] !== 'text' && term.op.endsWith('__fuzzy')) {
+            // avoid trying to use the "contains" operator for non-text fields
+            term.op = '';
+        }
+    }
+    // conditionally clear the search term after changing
+    // to selected search operators
+    clearSearchTermValueAfterOpChange(term: AcqSearchTerm) {
+        if (term.op === '__age') {
+            term.value1 = '';
+            term.value2 = '';
+        }
+    }
+
+    setOrgUnitSearchValue(org: IdlObject, term: AcqSearchTerm) {
+        if (org == null) {
+            term.value1 = '';
+        } else {
+            term.value1 = org.id();
+        }
+    }
+
+    submitSearch() {
+        // tossing setTimeout here to ensure that the
+        // grid data source is fully initialized
+        setTimeout(() => {
+            this.searchSubmitted.emit({
+                terms: this.searchTerms,
+                conjunction: this.searchConjunction
+            });
+        });
+    }
+
+    saveSearchAsDefault() {
+        return this.store.setItem(this.defaultSearchSetting, {
+            terms: this.searchTerms,
+            conjunction: this.searchConjunction
+        });
+    }
+    saveRunImmediately() {
+        return this.store.setItem(this.runImmediatelySetting, this.runImmediately);
+    }
+}
diff --git a/Open-ILS/src/eg2/src/app/staff/acq/search/acq-search.component.html b/Open-ILS/src/eg2/src/app/staff/acq/search/acq-search.component.html
new file mode 100644 (file)
index 0000000..d5d5a4d
--- /dev/null
@@ -0,0 +1,24 @@
+<eg-staff-banner bannerText="Acquisitions Search" i18n-bannerText>
+</eg-staff-banner>
+
+<div class="row">
+  <div class="ml-auto mr-3"><a i18n href="/eg/staff/acq/legacy/search/unified">Legacy Search Interface</a></div>
+</div>
+<div class="row" id="acq-search-page">
+  <div class="col-lg-12">
+    <ngb-tabset #acqSearchTabs [activeId]="searchType" (tabChange)="onTabChange($event)">
+      <ngb-tab title="Line Items Search" i18n-title id="lineitems">
+        <ng-template ngbTabContent><eg-lineitem-results [initialSearchTerms]="urlSearchTerms"></eg-lineitem-results></ng-template>
+      </ngb-tab>
+      <ngb-tab title="Purchase Orders Search" i18n-title id="purchaseorders">
+        <ng-template ngbTabContent><eg-purchase-order-results [initialSearchTerms]="urlSearchTerms"></eg-purchase-order-results></ng-template>
+      </ngb-tab>
+      <ngb-tab title="Invoices Search" i18n-title id="invoices">
+        <ng-template ngbTabContent><eg-invoice-results [initialSearchTerms]="urlSearchTerms"></eg-invoice-results></ng-template>
+      </ngb-tab>
+      <ngb-tab title="Selection Lists Search" i18n-title id="selectionlists">
+        <ng-template ngbTabContent><eg-picklist-results [initialSearchTerms]="urlSearchTerms"></eg-picklist-results></ng-template>
+      </ngb-tab>
+    </ngb-tabset>
+  </div>
+</div>
diff --git a/Open-ILS/src/eg2/src/app/staff/acq/search/acq-search.component.ts b/Open-ILS/src/eg2/src/app/staff/acq/search/acq-search.component.ts
new file mode 100644 (file)
index 0000000..7eb21d8
--- /dev/null
@@ -0,0 +1,102 @@
+import {Component, OnInit, AfterViewInit, ViewChild, ViewChildren, QueryList} from '@angular/core';
+import {NgbTabset, NgbTabChangeEvent} from '@ng-bootstrap/ng-bootstrap';
+import {Router, ActivatedRoute, ParamMap, NavigationEnd} from '@angular/router';
+import {StaffCommonModule} from '@eg/staff/common.module';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {AcqSearchTerm} from './acq-search.service';
+import {LineitemResultsComponent} from './lineitem-results.component';
+import {PurchaseOrderResultsComponent} from './purchase-order-results.component';
+import {InvoiceResultsComponent} from './invoice-results.component';
+import {PicklistResultsComponent} from './picklist-results.component';
+
+@Component({
+  templateUrl: './acq-search.component.html'
+})
+
+export class AcqSearchComponent implements OnInit, AfterViewInit {
+
+    searchType = '';
+    validSearchTypes = ['lineitems', 'purchaseorders', 'invoices', 'selectionlists'];
+    defaultSearchType = 'lineitems';
+
+    urlSearchTerms: AcqSearchTerm[] = [];
+
+    onTabChange: ($event: NgbTabChangeEvent) => void;
+    @ViewChild('acqSearchTabs', { static: true }) tabs: NgbTabset;
+    @ViewChildren(LineitemResultsComponent) liResults: QueryList<PurchaseOrderResultsComponent>;
+    @ViewChildren(PurchaseOrderResultsComponent) poResults: QueryList<PurchaseOrderResultsComponent>;
+    @ViewChildren(InvoiceResultsComponent) invResults: QueryList<PurchaseOrderResultsComponent>;
+    @ViewChildren(PicklistResultsComponent) plResults: QueryList<PurchaseOrderResultsComponent>;
+
+    constructor(
+        private router: Router,
+        private route: ActivatedRoute,
+        private pcrud: PcrudService,
+        private idl: IdlService,
+    ) {
+        this.route.queryParamMap.subscribe((params: ParamMap) => {
+            this.urlSearchTerms = [];
+            const fields = params.getAll('f');
+            const ops = params.getAll('op');
+            const values1 = params.getAll('val1');
+            const values2 = params.getAll('val2');
+            fields.forEach((f, idx) => {
+                const term: AcqSearchTerm = {
+                    field:  f,
+                    op:     '',
+                    value1: '',
+                    value2: ''
+                };
+                if (idx < ops.length) {
+                    term.op = ops[idx];
+                }
+                if (idx < values1.length) {
+                    term.value1 = values1[idx];
+                    if (term.value1 === 'null') {
+                        // convert the string 'null' to a true
+                        // null value, mostly for the benefit of the
+                        // open invoices navigation link
+                        term.value1 = null;
+                    }
+                }
+                if (idx < values2.length) {
+                    term.value2 = values2[idx];
+                }
+                this.urlSearchTerms.push(term);
+                this.ngOnInit(); // TODO: probably overkill
+            });
+        });
+        this.router.events.subscribe(routeEvent => {
+            if (routeEvent instanceof NavigationEnd) {
+                this.ngOnInit(); // TODO: probably overkill
+            }
+        });
+    }
+
+    ngOnInit() {
+        const self = this;
+
+        const searchTypeParam = this.route.snapshot.paramMap.get('searchtype');
+
+        if (searchTypeParam) {
+            if (this.validSearchTypes.includes(searchTypeParam)) {
+                this.searchType = searchTypeParam;
+            } else {
+                this.searchType = this.defaultSearchType;
+                this.router.navigate(['/staff', 'acq', 'search', this.searchType]);
+            }
+        }
+
+        this.onTabChange = ($event) => {
+            if (this.validSearchTypes.includes($event.nextId)) {
+                this.searchType = $event.nextId;
+                this.urlSearchTerms = [];
+                this.router.navigate(['/staff', 'acq', 'search', $event.nextId]);
+            }
+        };
+    }
+
+    ngAfterViewInit() {}
+
+}
diff --git a/Open-ILS/src/eg2/src/app/staff/acq/search/acq-search.module.ts b/Open-ILS/src/eg2/src/app/staff/acq/search/acq-search.module.ts
new file mode 100644 (file)
index 0000000..4dc2251
--- /dev/null
@@ -0,0 +1,35 @@
+import {NgModule} from '@angular/core';
+import {StaffCommonModule} from '@eg/staff/common.module';
+import {AcqSearchRoutingModule} from './routing.module';
+import {AcqSearchComponent} from './acq-search.component';
+import {AcqSearchFormComponent} from './acq-search-form.component';
+import {LineitemResultsComponent} from './lineitem-results.component';
+import {PurchaseOrderResultsComponent} from './purchase-order-results.component';
+import {InvoiceResultsComponent} from './invoice-results.component';
+import {PicklistResultsComponent} from './picklist-results.component';
+import {PicklistCreateDialogComponent} from './picklist-create-dialog.component';
+import {PicklistCloneDialogComponent} from './picklist-clone-dialog.component';
+import {PicklistDeleteDialogComponent} from './picklist-delete-dialog.component';
+import {PicklistMergeDialogComponent} from './picklist-merge-dialog.component';
+
+@NgModule({
+  declarations: [
+    AcqSearchComponent,
+    AcqSearchFormComponent,
+    LineitemResultsComponent,
+    PurchaseOrderResultsComponent,
+    InvoiceResultsComponent,
+    PicklistResultsComponent,
+    PicklistCreateDialogComponent,
+    PicklistCloneDialogComponent,
+    PicklistDeleteDialogComponent,
+    PicklistMergeDialogComponent
+  ],
+  imports: [
+    StaffCommonModule,
+    AcqSearchRoutingModule
+  ]
+})
+
+export class AcqSearchModule {
+}
diff --git a/Open-ILS/src/eg2/src/app/staff/acq/search/acq-search.service.ts b/Open-ILS/src/eg2/src/app/staff/acq/search/acq-search.service.ts
new file mode 100644 (file)
index 0000000..d32c121
--- /dev/null
@@ -0,0 +1,259 @@
+import {Injectable} from '@angular/core';
+import {empty, throwError} from 'rxjs';
+import {map} from 'rxjs/operators';
+import {NetService} from '@eg/core/net.service';
+import {AuthService} from '@eg/core/auth.service';
+import {GridDataSource} from '@eg/share/grid/grid';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {Pager} from '@eg/share/util/pager';
+import {IdlObject} from '@eg/core/idl.service';
+import {EventService} from '@eg/core/event.service';
+
+const defaultSearch = {
+    lineitem: {
+        jub: [{
+            id: '0',
+            __gte: true
+        }]
+    },
+    purchase_order: {
+        acqpo: [{
+            id: '0',
+            __gte: true
+        }]
+    },
+    picklist: {
+        acqpl: [{
+            id: '0',
+            __gte: true
+        }]
+    },
+    invoice: {
+        acqinv: [{
+            id: '0',
+            __gte: true
+        }]
+    },
+};
+
+const searchOptions = {
+    lineitem: {
+        flesh_attrs: true,
+        flesh_cancel_reason: true,
+        flesh_notes: true,
+        flesh_provider: true,
+        flesh_claim_policy: true,
+        flesh_queued_record: true,
+    },
+    purchase_order: {
+        no_flesh_cancel_reason: true,
+        flesh_provider: true,
+        flesh_owner: false,
+        flesh_creator: false,
+        flesh_editor: false
+    },
+    picklist: {
+        flesh_lineitem_count: true,
+        flesh_owner: true,
+        flesh_creator: false,
+        flesh_editor: false
+    },
+    invoice: {
+        no_flesh_misc: true,
+        flesh_provider: true // and shipper, which is also a provider
+    }
+};
+
+const operatorMap = {
+    '!=': '__not',
+    '>': '__gte',
+    '>=': '__gte',
+    '<=': '__lte',
+    '<': '__lte',
+    'startswith': '__starts',
+    'endswith': '__ends',
+    'like': '__fuzzy',
+};
+
+export interface AcqSearchTerm {
+    field: string;
+    op: string;
+    value1: string;
+    value2: string;
+    is_date?: boolean;
+}
+
+export interface AcqSearch {
+    terms: AcqSearchTerm[];
+    conjunction: string;
+}
+
+@Injectable()
+export class AcqSearchService {
+
+    _terms: AcqSearchTerm[] = [];
+    _conjunction = 'all';
+    attrDefs: {[code: string]: IdlObject};
+    firstRun = true;
+
+    constructor(
+        private net: NetService,
+        private evt: EventService,
+        private auth: AuthService,
+        private pcrud: PcrudService
+    ) {
+        this.attrDefs = {};
+        this.firstRun = true;
+    }
+
+    fetchAttrDefs(): Promise<void> {
+        if (Object.keys(this.attrDefs).length) {
+            return Promise.resolve();
+        }
+        return new Promise((resolve, reject) => {
+            this.pcrud.retrieveAll('acqliad', {},
+                {atomic: true}
+            ).subscribe(list => {
+                list.forEach(acqliad => {
+                    this.attrDefs[acqliad.code()] = acqliad;
+                });
+                resolve();
+            });
+        });
+    }
+
+    setSearch(search: AcqSearch) {
+        this._terms = search.terms;
+        this._conjunction = search.conjunction;
+        this.firstRun = false;
+    }
+
+    generateAcqSearch(searchType, filters): any {
+        const andTerms = JSON.parse(JSON.stringify(defaultSearch[searchType])); // deep copy
+        const orTerms = {};
+        const coreRecType = Object.keys(defaultSearch[searchType])[0];
+
+        // handle supplied search terms
+        this._terms.forEach(term => {
+            if (term.value1 === '') {
+                return;
+            }
+            const searchTerm: Object = {};
+            const recType = term.field.split(':')[0];
+            const searchField = term.field.split(':')[1];
+            if (term.op === '__between') {
+                searchTerm[searchField] = [term.value1, term.value2];
+            } else {
+                searchTerm[searchField] = term.value1;
+            }
+            if (term.op !== '') {
+                searchTerm[term.op] = true;
+            }
+            if (term.is_date) {
+                searchTerm['__castdate'] = true;
+            }
+            if (this._conjunction === 'any') {
+                if (!(recType in orTerms)) {
+                    orTerms[recType] = [];
+                }
+                orTerms[recType].push(searchTerm);
+            } else {
+                if (!(recType in andTerms)) {
+                    andTerms[recType] = [];
+                }
+                andTerms[recType].push(searchTerm);
+            }
+        });
+
+        // handle grid filters
+        // note that date filters coming from the grid do not need
+        // to worry about __castdate because the grid filter supplies
+        // both the start and end times
+        const observables = [];
+        Object.keys(filters).forEach(filterField => {
+            filters[filterField].forEach(condition => {
+                const searchTerm: Object = {};
+                let filterOp = '=';
+                let filterVal = '';
+                if (Object.keys(condition).some(x => x === '-not')) {
+                    filterOp = Object.keys(condition['-not'][filterField])[0];
+                    filterVal = condition['-not'][filterField][filterOp];
+                    searchTerm['__not'] = true;
+                } else {
+                    filterOp = Object.keys(condition[filterField])[0];
+                    filterVal = condition[filterField][filterOp];
+                    if (filterOp === 'like' && filterVal.length > 1) {
+                        if (filterVal[0] === '%' && filterVal[filterVal.length - 1] === '%') {
+                            filterVal = filterVal.slice(1, filterVal.length - 1);
+                        } else if (filterVal[filterVal.length - 1] === '%') {
+                            filterVal = filterVal.slice(0, filterVal.length - 1);
+                            filterOp = 'startswith';
+                        } else if (filterVal[0] === '%') {
+                            filterVal = filterVal.slice(1);
+                            filterOp = 'endswith';
+                        }
+                    }
+                }
+
+                if (filterOp in operatorMap) {
+                    searchTerm[operatorMap[filterOp]] = true;
+                }
+                if ((['title', 'author'].indexOf(filterField) > -1) &&
+                     (filterField in this.attrDefs)) {
+                        if (!('acqlia' in andTerms)) {
+                            andTerms['acqlia'] = [];
+                        }
+                        searchTerm[this.attrDefs[filterField].id()] = filterVal;
+                        andTerms['acqlia'].push(searchTerm);
+                } else {
+                    searchTerm[filterField] = filterVal;
+                    andTerms[coreRecType].push(searchTerm);
+                }
+            });
+        });
+        return { andTerms: andTerms, orTerms: orTerms };
+    }
+
+    getAcqSearchDataSource(searchType: string): GridDataSource {
+        const gridSource = new GridDataSource();
+
+        this.fetchAttrDefs().then(() => {
+            gridSource.getRows = (pager: Pager) => {
+
+                // don't do a search the very first time we
+                // get invoked, which is during initialization; we'll
+                // let components higher up the change decide whether
+                // to submit a search
+                if (this.firstRun) {
+                    this.firstRun = false;
+                    return empty();
+                }
+
+                const currentSearch = this.generateAcqSearch(searchType, gridSource.filters);
+
+                const opts = { ...searchOptions[searchType] };
+                opts['offset'] = pager.offset;
+                opts['limit'] = pager.limit;
+                opts['au_by_id'] = true;
+                return this.net.request(
+                    'open-ils.acq',
+                    'open-ils.acq.' + searchType + '.unified_search',
+                        this.auth.token(),
+                        currentSearch.andTerms,
+                        currentSearch.orTerms,
+                        null,
+                        opts
+                ).pipe(
+                    map(res => {
+                        if (this.evt.parse(res)) {
+                            throw throwError(res);
+                        } else {
+                            return res;
+                        }
+                    }),
+                );
+            };
+        });
+        return gridSource;
+    }
+}
diff --git a/Open-ILS/src/eg2/src/app/staff/acq/search/invoice-results.component.html b/Open-ILS/src/eg2/src/app/staff/acq/search/invoice-results.component.html
new file mode 100644 (file)
index 0000000..e5823a4
--- /dev/null
@@ -0,0 +1,45 @@
+<eg-acq-search-form (searchSubmitted)="doSearch($event)" [initialSearchTerms]="initialSearchTerms"
+  i18n-searchTypeLabel searchTypeLabel="Invoice" runImmediatelySetting="eg.acq.search.invoices.run_immediately"
+  defaultSearchSetting="eg.acq.search.default.invoices"></eg-acq-search-form>
+
+<ng-template #inv_identTmpl let-invoice="row">
+  <a href="/eg/staff/acq/legacy/invoice/view/{{invoice.id()}}"
+     target="_blank">
+    {{invoice.inv_ident()}}
+  </a>
+</ng-template>
+<ng-template #providerTmpl let-invoice="row">
+  <a href="/eg/staff/admin/acq/conify/provider/{{invoice.provider().id()}}"
+     target="_blank">
+    {{invoice.provider().code()}}
+  </a>
+</ng-template>
+<ng-template #shipperTmpl let-invoice="row">
+  <a href="/eg/staff/admin/acq/conify/provider/{{invoice.shipper().id()}}"
+     target="_blank">
+    {{invoice.shipper().code()}}
+  </a>
+</ng-template>
+
+<eg-grid #acqSearchInvoicesGrid
+  persistKey="acq.search.invoices"
+  [stickyHeader]="true"
+  [filterable]="true"
+  idlClass="acqinv" [dataSource]="gridSource">
+
+  <eg-grid-toolbar-action label="Print Selected Invoices" i18n-label
+    (onClick)="printSelectedInvoices($event)" [disableOnRows]="noSelectedRows">
+  </eg-grid-toolbar-action>
+
+  <eg-grid-column path="inv_ident" [cellTemplate]="inv_identTmpl"></eg-grid-column>
+  <eg-grid-column path="provider" [cellTemplate]="providerTmpl"></eg-grid-column>
+  <eg-grid-column path="shipper" [cellTemplate]="shipperTmpl"></eg-grid-column>
+
+  <eg-grid-column path="id" [hidden]="true"></eg-grid-column>
+  <eg-grid-column path="recv_date" [hidden]="true"></eg-grid-column>
+
+</eg-grid>
+
+<eg-alert-dialog #printfail i18n-dialogBody
+  dialogBody="Could not print the selected invoices.">
+</eg-alert-dialog>
diff --git a/Open-ILS/src/eg2/src/app/staff/acq/search/invoice-results.component.ts b/Open-ILS/src/eg2/src/app/staff/acq/search/invoice-results.component.ts
new file mode 100644 (file)
index 0000000..04f2d6a
--- /dev/null
@@ -0,0 +1,82 @@
+import {Component, OnInit, Input, ViewChild} from '@angular/core';
+import {Observable} from 'rxjs';
+import {map} from 'rxjs/operators';
+import {Router, ActivatedRoute, ParamMap} from '@angular/router';
+import {Pager} from '@eg/share/util/pager';
+import {IdlObject} from '@eg/core/idl.service';
+import {EventService} from '@eg/core/event.service';
+import {AlertDialogComponent} from '@eg/share/dialog/alert.component';
+import {PrintService} from '@eg/share/print/print.service';
+import {NetService} from '@eg/core/net.service';
+import {AuthService} from '@eg/core/auth.service';
+import {GridComponent} from '@eg/share/grid/grid.component';
+import {GridDataSource} from '@eg/share/grid/grid';
+import {AcqSearchService, AcqSearchTerm, AcqSearch} from './acq-search.service';
+import {AcqSearchFormComponent} from './acq-search-form.component';
+
+@Component({
+  selector: 'eg-invoice-results',
+  templateUrl: 'invoice-results.component.html',
+  providers: [AcqSearchService]
+})
+export class InvoiceResultsComponent implements OnInit {
+
+    @Input() initialSearchTerms: AcqSearchTerm[] = [];
+
+    gridSource: GridDataSource;
+    @ViewChild('acqSearchInvoicesGrid', { static: true }) invoiceResultsGrid: GridComponent;
+    @ViewChild('printfail', { static: true }) private printfail: AlertDialogComponent;
+
+    noSelectedRows: (rows: IdlObject[]) => boolean;
+
+    constructor(
+        private router: Router,
+        private route: ActivatedRoute,
+        private printer: PrintService,
+        private evt: EventService,
+        private net: NetService,
+        private auth: AuthService,
+        private acqSearch: AcqSearchService) {
+    }
+
+    ngOnInit() {
+        this.gridSource = this.acqSearch.getAcqSearchDataSource('invoice');
+        this.noSelectedRows = (rows: IdlObject[]) => (rows.length === 0);
+    }
+
+    printSelectedInvoices(rows: IdlObject[]) {
+      const that = this;
+      let html = '<style type="text/css">.acq-invoice-' +
+        'voucher {page-break-after:always;}' +
+        '</style>\n';
+      this.net.request(
+        'open-ils.acq',
+        'open-ils.acq.invoice.print.html',
+        this.auth.token(), rows.map( invoice => invoice.id() )
+      ).subscribe(
+        (res) => {
+          if (this.evt.parse(res)) {
+            console.error(res);
+            this.printfail.open();
+          } else {
+            html +=  res.template_output().data();
+          }
+        },
+        (err) => {
+          console.error(err);
+          this.printfail.open();
+        },
+        () => this.printer.print({
+          text: html,
+          printContext: 'default'
+        })
+      );
+    }
+
+    doSearch(search: AcqSearch) {
+        setTimeout(() => {
+            this.acqSearch.setSearch(search);
+            this.invoiceResultsGrid.reload();
+        });
+    }
+}
diff --git a/Open-ILS/src/eg2/src/app/staff/acq/search/lineitem-results.component.html b/Open-ILS/src/eg2/src/app/staff/acq/search/lineitem-results.component.html
new file mode 100644 (file)
index 0000000..d1ef61d
--- /dev/null
@@ -0,0 +1,71 @@
+<eg-acq-search-form (searchSubmitted)="doSearch($event)" [initialSearchTerms]="initialSearchTerms"
+  i18n-searchTypeLabel searchTypeLabel="Line Item" runImmediatelySetting="eg.acq.search.lineitems.run_immediately"
+  defaultSearchSetting="eg.acq.search.default.lineitems"></eg-acq-search-form>
+
+<ng-template #idTmpl let-lineitem="row">
+  <a *ngIf="lineitem.purchase_order()" href="/eg/staff/acq/legacy/po/view/{{lineitem.purchase_order()}}?focus_li={{lineitem.id()}}"
+     target="_blank">
+    {{lineitem.id()}}
+  </a>
+  <a *ngIf="lineitem.picklist() && !lineitem.purchase_order()" href="/eg/staff/acq/legacy/picklist/view/{{lineitem.picklist()}}?focus_li={{lineitem.id()}}"
+     target="_blank">
+    {{lineitem.id()}}
+  </a>
+</ng-template>
+
+<ng-template #liAttrTmpl let-lineitem="row" let-col="col">
+  <ng-container *ngFor="let lia of lineitem.attributes()">
+    <ng-container *ngIf="lia.attr_name() === col.path">
+      {{lia.attr_value()}}
+    </ng-container>
+  </ng-container>
+</ng-template>
+
+<ng-template #providerTmpl let-lineitem="row">
+  <a *ngIf="lineitem.provider()" href="/eg/staff/admin/acq/conify/provider/{{lineitem.provider().id()}}"
+     target="_blank">
+    {{lineitem.provider().name()}}
+  </a>
+</ng-template>
+
+<ng-template #liLinksTmpl let-lineitem="row">
+  <ul>
+    <li *ngIf="lineitem.eg_bib_id()">
+      <a href="/eg/staff/cat/catalog/record/{{lineitem.eg_bib_id()}}"
+         target="_blank" i18n>Catalog</a></li>
+    <li><a href="/eg/staff/acq/legacy/lineitem/worksheet/{{lineitem.id()}}"
+           target="_blank" i18n>Worksheet</a></li>
+    <li *ngIf="lineitem.purchase_order()">
+      <a href="/eg/staff/acq/legacy/po/view/{{lineitem.purchase_order()}}"
+          target="_blank" i18n>Purchase Order</a></li>
+    <li><a href="/eg/staff/acq/requests/lineitem/{{lineitem.id()}}"
+           target="_blank" i18n>Requests</a></li>
+    <li>
+      <a routerLink="/staff/acq/search/invoices" [queryParams]="{f: 'jub:id', val1: lineitem.id()}"
+        target="_blank" i18n>Invoices</a></li>
+    <li *ngIf="lineitem.queued_record()">
+      <a routerLink="/staff/cat/vandelay/queue/bib/{{lineitem.queued_record().queue()}}"
+        target="_blank" i18n>Queue</a></li>
+    <li *ngIf="lineitem.picklist()">
+      <a href="/eg/staff/acq/legacy/picklist/view/{{lineitem.picklist()}}"
+        target="_blank" i18n>Selection List</a></li>
+  </ul>
+</ng-template>
+
+<eg-grid #acqSearchLineitemsGrid
+  persistKey="acq.search.lineitems"
+  idlClass="jub" [dataSource]="gridSource"
+  [stickyHeader]="true"
+  [filterable]="true"
+  [showDeclaredFieldsOnly]="true">
+
+  <eg-grid-column path="id" [cellTemplate]="idTmpl" [disableTooltip]="true"></eg-grid-column>
+  <!-- TODO: Title and Author filters will require special work as they're acqlia values -->
+  <eg-grid-column i18n-label label="Title" path="title" [cellTemplate]="liAttrTmpl"></eg-grid-column>
+  <eg-grid-column i18n-label label="Author" path="author" [cellTemplate]="liAttrTmpl"></eg-grid-column>
+  <eg-grid-column path="provider" [cellTemplate]="providerTmpl"></eg-grid-column>
+  <eg-grid-column i18n-label label="Links" path="_links" [cellTemplate]="liLinksTmpl" [disableTooltip]="true" [filterable]="false"></eg-grid-column>
+  <eg-grid-column path="claim_policy.name"></eg-grid-column>
+  <eg-grid-column i18n-label label="Status" path="state" [disableTooltip]="true"></eg-grid-column>
+  <eg-grid-column path="estimated_unit_price" [disableTooltip]="true"></eg-grid-column>
+</eg-grid>
diff --git a/Open-ILS/src/eg2/src/app/staff/acq/search/lineitem-results.component.ts b/Open-ILS/src/eg2/src/app/staff/acq/search/lineitem-results.component.ts
new file mode 100644 (file)
index 0000000..061eddb
--- /dev/null
@@ -0,0 +1,44 @@
+import {Component, OnInit, Input, ViewChild} from '@angular/core';
+import {Observable} from 'rxjs';
+import {map} from 'rxjs/operators';
+import {Router, ActivatedRoute, ParamMap} from '@angular/router';
+import {Pager} from '@eg/share/util/pager';
+import {IdlObject} from '@eg/core/idl.service';
+import {NetService} from '@eg/core/net.service';
+import {AuthService} from '@eg/core/auth.service';
+import {GridComponent} from '@eg/share/grid/grid.component';
+import {GridDataSource} from '@eg/share/grid/grid';
+import {AcqSearchService, AcqSearchTerm, AcqSearch} from './acq-search.service';
+import {AcqSearchFormComponent} from './acq-search-form.component';
+
+@Component({
+  selector: 'eg-lineitem-results',
+  templateUrl: 'lineitem-results.component.html',
+  providers: [AcqSearchService]
+})
+export class LineitemResultsComponent implements OnInit {
+
+    @Input() initialSearchTerms: AcqSearchTerm[] = [];
+
+    gridSource: GridDataSource;
+    @ViewChild('acqSearchLineitemsGrid', { static: true }) lineitemResultsGrid: GridComponent;
+
+    constructor(
+        private router: Router,
+        private route: ActivatedRoute,
+        private net: NetService,
+        private auth: AuthService,
+        private acqSearch: AcqSearchService) {
+    }
+
+    ngOnInit() {
+        this.gridSource = this.acqSearch.getAcqSearchDataSource('lineitem');
+    }
+
+    doSearch(search: AcqSearch) {
+        setTimeout(() => {
+            this.acqSearch.setSearch(search);
+            this.lineitemResultsGrid.reload();
+        });
+    }
+}
diff --git a/Open-ILS/src/eg2/src/app/staff/acq/search/picklist-clone-dialog.component.html b/Open-ILS/src/eg2/src/app/staff/acq/search/picklist-clone-dialog.component.html
new file mode 100644 (file)
index 0000000..662aaca
--- /dev/null
@@ -0,0 +1,27 @@
+<ng-template #dialogContent>
+<form class="form-validated">
+  <div class="modal-header bg-info">
+    <h3 class="modal-title" i18n>Clone Selection List: {{leadListName}}</h3>
+    <button type="button" class="close"
+      i18n-aria-label aria-label="Close" (click)="close()">
+      <span aria-hidden="true">&times;</span>
+    </button>
+  </div>
+  <div class="modal-body">
+    <h4 i18n>Selection list name:</h4>
+    <input type="text" id="create-picklist-name" required
+      [ngModelOptions]="{standalone: true}"
+      class="form-control" [(ngModel)]="selectionListName">
+  </div>
+  <div class="modal-footer">
+    <button type="button" class="btn btn-success"
+      [disabled]="!selectionListName"
+      (click)="cloneList()" i18n>Clone</button>
+    <button type="button" class="btn btn-warning"
+      (click)="close()" i18n>Cancel</button>
+  </div>
+</form>
+</ng-template>
+<eg-alert-dialog #fail i18n-dialogBody
+  dialogBody="Could not create this selection list.">
+</eg-alert-dialog>
diff --git a/Open-ILS/src/eg2/src/app/staff/acq/search/picklist-clone-dialog.component.ts b/Open-ILS/src/eg2/src/app/staff/acq/search/picklist-clone-dialog.component.ts
new file mode 100644 (file)
index 0000000..e2b23d3
--- /dev/null
@@ -0,0 +1,75 @@
+import {Component, Input, ViewChild, TemplateRef, OnInit, Renderer2} from '@angular/core';
+import {Observable, from, empty, throwError} from 'rxjs';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+import {AlertDialogComponent} from '@eg/share/dialog/alert.component';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {EventService} from '@eg/core/event.service';
+import {NetService} from '@eg/core/net.service';
+import {AuthService} from '@eg/core/auth.service';
+import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
+import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
+
+@Component({
+  selector: 'eg-picklist-clone-dialog',
+  templateUrl: './picklist-clone-dialog.component.html'
+})
+
+export class PicklistCloneDialogComponent
+  extends DialogComponent implements OnInit {
+
+  @Input() grid: any;
+  selectionListName: String;
+  leadListName: String;
+  selections: IdlObject[];
+
+  @ViewChild('fail', { static: true }) private fail: AlertDialogComponent;
+
+  constructor(
+    private renderer: Renderer2,
+    private idl: IdlService,
+    private evt: EventService,
+    private net: NetService,
+    private auth: AuthService,
+    private modal: NgbModal
+  ) {
+    super(modal);
+  }
+
+  ngOnInit() {
+  }
+
+  update() {
+    this.leadListName = this.grid.context.getSelectedRows()[0].name();
+    this.renderer.selectRootElement('#create-picklist-name').focus();
+    this.selectionListName = 'Copy of ' + this.leadListName;
+  }
+
+  cloneList() {
+    const picklist = this.idl.create('acqpl');
+    picklist.owner(this.auth.user().id());
+    picklist.name(this.selectionListName);
+    this.net.request(
+      'open-ils.acq',
+      'open-ils.acq.picklist.clone',
+      this.auth.token(),
+      this.grid.context.getSelectedRows()[0].id(),
+      this.selectionListName
+    ).subscribe(
+      (res) => {
+        if (this.evt.parse(res)) {
+          console.error(res);
+          this.fail.open();
+        } else {
+          console.log(res);
+        }
+      },
+      (err) => {
+        console.error(err);
+        this.fail.open();
+      },
+      () => this.close(true)
+    );
+  }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/acq/search/picklist-create-dialog.component.html b/Open-ILS/src/eg2/src/app/staff/acq/search/picklist-create-dialog.component.html
new file mode 100644 (file)
index 0000000..b4ac8ad
--- /dev/null
@@ -0,0 +1,27 @@
+<ng-template #dialogContent>
+<form class="form-validated">
+  <div class="modal-header bg-info">
+    <h3 class="modal-title" i18n>Create New Selection List</h3>
+    <button type="button" class="close"
+      i18n-aria-label aria-label="Close" (click)="close()">
+      <span aria-hidden="true">&times;</span>
+    </button>
+  </div>
+  <div class="modal-body">
+    <h4 i18n>Selection list name:</h4>
+    <input type="text" id="create-picklist-name" required
+      [ngModelOptions]="{standalone: true}" required
+      class="form-control col-lg-7" [(ngModel)]="selectionListName">
+  </div>
+  <div class="modal-footer">
+    <button type="button" class="btn btn-success"
+      [disabled]="!selectionListName"
+      (click)="createList()" i18n>Create</button>
+    <button type="button" class="btn btn-warning"
+      (click)="close()" i18n>Cancel</button>
+  </div>
+</form>
+</ng-template>
+<eg-alert-dialog #fail i18n-dialogBody
+  dialogBody="Could not create this selection list.">
+</eg-alert-dialog>
diff --git a/Open-ILS/src/eg2/src/app/staff/acq/search/picklist-create-dialog.component.ts b/Open-ILS/src/eg2/src/app/staff/acq/search/picklist-create-dialog.component.ts
new file mode 100644 (file)
index 0000000..9a5a8da
--- /dev/null
@@ -0,0 +1,70 @@
+import {Component, Input, ViewChild, TemplateRef, OnInit, Renderer2} from '@angular/core';
+import {Observable, from, empty, throwError} from 'rxjs';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+import {AlertDialogComponent} from '@eg/share/dialog/alert.component';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {EventService} from '@eg/core/event.service';
+import {NetService} from '@eg/core/net.service';
+import {AuthService} from '@eg/core/auth.service';
+import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
+import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
+
+@Component({
+  selector: 'eg-picklist-create-dialog',
+  templateUrl: './picklist-create-dialog.component.html'
+})
+
+export class PicklistCreateDialogComponent
+  extends DialogComponent implements OnInit {
+
+  selectionListName: String;
+
+  @ViewChild('fail', { static: true }) private fail: AlertDialogComponent;
+
+  constructor(
+    private renderer: Renderer2,
+    private idl: IdlService,
+    private evt: EventService,
+    private net: NetService,
+    private auth: AuthService,
+    private modal: NgbModal
+  ) {
+    super(modal);
+  }
+
+  ngOnInit() {
+    this.selectionListName = '';
+  }
+
+  update() {
+    this.selectionListName = '';
+    this.renderer.selectRootElement('#create-picklist-name').focus();
+  }
+
+  createList() {
+    const picklist = this.idl.create('acqpl');
+    picklist.owner(this.auth.user().id());
+    picklist.name(this.selectionListName);
+    this.net.request(
+      'open-ils.acq',
+      'open-ils.acq.picklist.create',
+      this.auth.token(), picklist
+    ).subscribe(
+      (res) => {
+        if (this.evt.parse(res)) {
+          console.error(res);
+          this.fail.open();
+        } else {
+          console.log(res);
+        }
+      },
+      (err) => {
+        console.error(err);
+        this.fail.open();
+      },
+      () => this.close(true)
+    );
+  }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/acq/search/picklist-delete-dialog.component.html b/Open-ILS/src/eg2/src/app/staff/acq/search/picklist-delete-dialog.component.html
new file mode 100644 (file)
index 0000000..bd30a9d
--- /dev/null
@@ -0,0 +1,24 @@
+<ng-template #dialogContent>
+  <div class="modal-header bg-info">
+    <h3 class="modal-title" i18n>Confirm Delete</h3>
+    <button type="button" class="close"
+      i18n-aria-label aria-label="Close" (click)="close()">
+      <span aria-hidden="true">&times;</span>
+    </button>
+  </div>
+  <div class="modal-body">
+    <h4 i18n>Delete the following selection lists?</h4>
+    <ul>
+      <li *ngFor="let listName of listNames">{{listName}}</li>
+    </ul>
+  </div>
+  <div class="modal-footer">
+    <button type="button" class="btn btn-success"
+      (click)="deleteLists()" i18n>Delete</button>
+    <button type="button" class="btn btn-warning"
+      (click)="close()" i18n>Cancel</button>
+  </div>
+</ng-template>
+<eg-alert-dialog #fail i18n-dialogBody
+  dialogBody="Could not delete the selection list(s).">
+</eg-alert-dialog>
diff --git a/Open-ILS/src/eg2/src/app/staff/acq/search/picklist-delete-dialog.component.ts b/Open-ILS/src/eg2/src/app/staff/acq/search/picklist-delete-dialog.component.ts
new file mode 100644 (file)
index 0000000..401937e
--- /dev/null
@@ -0,0 +1,75 @@
+import {Component, Input, ViewChild, TemplateRef, OnInit} from '@angular/core';
+import {Observable, forkJoin, from, empty, throwError} from 'rxjs';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+import {AlertDialogComponent} from '@eg/share/dialog/alert.component';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {EventService} from '@eg/core/event.service';
+import {NetService} from '@eg/core/net.service';
+import {AuthService} from '@eg/core/auth.service';
+import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
+import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
+
+@Component({
+  selector: 'eg-picklist-delete-dialog',
+  templateUrl: './picklist-delete-dialog.component.html'
+})
+
+export class PicklistDeleteDialogComponent
+  extends DialogComponent implements OnInit {
+
+  @Input() grid: any;
+  listNames: string[];
+
+  @ViewChild('fail', { static: true }) private fail: AlertDialogComponent;
+
+  constructor(
+    private idl: IdlService,
+    private evt: EventService,
+    private net: NetService,
+    private auth: AuthService,
+    private modal: NgbModal
+  ) {
+    super(modal);
+  }
+
+  ngOnInit() {
+  }
+
+  update() {
+    this.listNames = this.grid.context.getSelectedRows().map( r => r.name() );
+  }
+
+  deleteList(list) {
+    return this.net.request(
+      'open-ils.acq',
+      'open-ils.acq.picklist.delete',
+      this.auth.token(),
+      list.id()
+    );
+  }
+
+  deleteLists() {
+    const that = this;
+    const observables = [];
+    this.grid.context.getSelectedRows().forEach(function(r) {
+      observables.push( that.deleteList(r) );
+    });
+    forkJoin(observables).subscribe(
+      (res) => {
+        if (this.evt.parse(res)) {
+          console.error(res);
+          this.fail.open();
+        } else {
+          console.log(res);
+        }
+      },
+      (err) => {
+        console.error(err);
+        this.fail.open();
+      },
+      () => this.close(true)
+    );
+  }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/acq/search/picklist-merge-dialog.component.html b/Open-ILS/src/eg2/src/app/staff/acq/search/picklist-merge-dialog.component.html
new file mode 100644 (file)
index 0000000..6cdf54e
--- /dev/null
@@ -0,0 +1,32 @@
+<ng-template #dialogContent>
+<form class="form-validated">
+  <div class="modal-header bg-info">
+    <h3 class="modal-title" i18n>Merge Selection Lists</h3>
+    <button type="button" class="close"
+      i18n-aria-label aria-label="Close" (click)="close()">
+      <span aria-hidden="true">&times;</span>
+    </button>
+  </div>
+  <div class="modal-body">
+    <h4 i18n>Lead Selection List:</h4>
+      <select [(ngModel)]="leadList" [ngModelOptions]="{standalone: true}" required>
+        <option *ngFor="let list of selectedLists"
+          value="{{list.id()}}">{{list.name()}}</option>
+      </select>
+    <h4 i18n>Merge the following selection lists?</h4>
+    <ul>
+      <li *ngFor="let listName of listNames">{{listName}}</li>
+    </ul>
+  </div>
+  <div class="modal-footer">
+    <button type="button" class="btn btn-success"
+      [disabled]="!leadList"
+      (click)="mergeLists()" i18n>Merge</button>
+    <button type="button" class="btn btn-warning"
+      (click)="close()" i18n>Cancel</button>
+  </div>
+</form>
+</ng-template>
+<eg-alert-dialog #fail i18n-dialogBody
+  dialogBody="Could not merge the selection lists.">
+</eg-alert-dialog>
diff --git a/Open-ILS/src/eg2/src/app/staff/acq/search/picklist-merge-dialog.component.ts b/Open-ILS/src/eg2/src/app/staff/acq/search/picklist-merge-dialog.component.ts
new file mode 100644 (file)
index 0000000..a7d7643
--- /dev/null
@@ -0,0 +1,71 @@
+import {Component, Input, ViewChild, TemplateRef, OnInit} from '@angular/core';
+import {Observable, forkJoin, from, empty, throwError} from 'rxjs';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+import {AlertDialogComponent} from '@eg/share/dialog/alert.component';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {EventService} from '@eg/core/event.service';
+import {NetService} from '@eg/core/net.service';
+import {AuthService} from '@eg/core/auth.service';
+import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
+import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
+
+@Component({
+  selector: 'eg-picklist-merge-dialog',
+  templateUrl: './picklist-merge-dialog.component.html'
+})
+
+export class PicklistMergeDialogComponent
+  extends DialogComponent implements OnInit {
+
+  @Input() grid: any;
+  listNames: string[];
+  leadList: number;
+  selectedLists: IdlObject[];
+
+  @ViewChild('fail', { static: true }) private fail: AlertDialogComponent;
+
+  constructor(
+    private idl: IdlService,
+    private evt: EventService,
+    private net: NetService,
+    private auth: AuthService,
+    private modal: NgbModal
+  ) {
+    super(modal);
+  }
+
+  ngOnInit() {
+  }
+
+  update() {
+    this.selectedLists = this.grid.context.getSelectedRows();
+    this.listNames = this.selectedLists.map( r => r.name() );
+  }
+
+  mergeLists() {
+    const that = this;
+    this.net.request(
+      'open-ils.acq',
+      'open-ils.acq.picklist.merge',
+      this.auth.token(), this.leadList,
+      this.selectedLists.map( list => list.id() ).filter(function(p) { return p !== that.leadList; })
+    ).subscribe(
+      (res) => {
+        if (this.evt.parse(res)) {
+          console.error(res);
+          this.fail.open();
+        } else {
+          console.log(res);
+        }
+      },
+      (err) => {
+        console.error(err);
+        this.fail.open();
+      },
+      () => this.close(true)
+    );
+  }
+
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/acq/search/picklist-results.component.html b/Open-ILS/src/eg2/src/app/staff/acq/search/picklist-results.component.html
new file mode 100644 (file)
index 0000000..9994ec2
--- /dev/null
@@ -0,0 +1,59 @@
+<eg-acq-search-form (searchSubmitted)="doSearch($event)" [initialSearchTerms]="initialSearchTerms"
+  i18n-searchTypeLabel searchTypeLabel="Selection List" runImmediatelySetting="eg.acq.search.selectionlists.run_immediately"
+  defaultSearchSetting="eg.acq.search.default.selectionlists"></eg-acq-search-form>
+
+<eg-string #createSelectionListString i18n-text text="Selection List Created">
+</eg-string>
+<eg-string #cloneSelectionListString i18n-text text="Selection List Cloned">
+</eg-string>
+<eg-string #deleteSelectionListString i18n-text text="Selection List(s) Deleted">
+</eg-string>
+<eg-string #mergeSelectionListString i18n-text text="Selection Lists Merged">
+</eg-string>
+
+<ng-template #nameTmpl let-selectionlist="row">
+  <a href="/eg/staff/acq/legacy/picklist/view/{{selectionlist.id()}}"
+     target="_blank">
+    {{selectionlist.name()}}
+  </a>
+</ng-template>
+
+<eg-picklist-create-dialog #picklistCreateDialog>
+</eg-picklist-create-dialog>
+
+<eg-picklist-clone-dialog #picklistCloneDialog [grid]="picklistResultsGrid">
+</eg-picklist-clone-dialog>
+
+<eg-picklist-delete-dialog #picklistDeleteDialog [grid]="picklistResultsGrid">
+</eg-picklist-delete-dialog>
+
+<eg-picklist-merge-dialog #picklistMergeDialog [grid]="picklistResultsGrid">
+</eg-picklist-merge-dialog>
+
+<eg-grid #acqSearchPicklistsGrid
+  persistKey="acq.search.selectionlists"
+  [stickyHeader]="true"
+  [filterable]="true"
+  idlClass="acqpl" [dataSource]="gridSource">
+
+  <eg-grid-toolbar-action label="New Selection List" i18n-label
+    (onClick)="openCreateDialog()" [disableOnRows]="createNotAppropriate">
+  </eg-grid-toolbar-action>
+  <eg-grid-toolbar-action label="Clone Selected" i18n-label
+    (onClick)="openCloneDialog($event)" [disableOnRows]="cloneNotAppropriate">
+  </eg-grid-toolbar-action>
+  <eg-grid-toolbar-action label="Merge Selected" i18n-label
+    (onClick)="openMergeDialog($event)" [disableOnRows]="mergeNotAppropriate">
+  </eg-grid-toolbar-action>
+  <eg-grid-toolbar-action label="Delete Selected" i18n-label
+    (onClick)="openDeleteDialog($event)" [disableOnRows]="deleteNotAppropriate">
+  </eg-grid-toolbar-action>
+
+  <eg-grid-column path="name" [cellTemplate]="nameTmpl"></eg-grid-column>
+  <eg-grid-column path="entry_count" [filterable]="false"></eg-grid-column>
+
+  <eg-grid-column path="id" [hidden]="true"></eg-grid-column>
+  <eg-grid-column path="creator" [hidden]="true"></eg-grid-column>
+  <eg-grid-column path="editor" [hidden]="true"></eg-grid-column>
+
+</eg-grid>
diff --git a/Open-ILS/src/eg2/src/app/staff/acq/search/picklist-results.component.ts b/Open-ILS/src/eg2/src/app/staff/acq/search/picklist-results.component.ts
new file mode 100644 (file)
index 0000000..3705ad1
--- /dev/null
@@ -0,0 +1,121 @@
+import {Component, OnInit, Input, ViewChild} from '@angular/core';
+import {Observable} from 'rxjs';
+import {map} from 'rxjs/operators';
+import {Router, ActivatedRoute, ParamMap} from '@angular/router';
+import {Pager} from '@eg/share/util/pager';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {StringComponent} from '@eg/share/string/string.component';
+import {IdlObject} from '@eg/core/idl.service';
+import {NetService} from '@eg/core/net.service';
+import {AuthService} from '@eg/core/auth.service';
+import {PermService} from '@eg/core/perm.service';
+import {GridComponent} from '@eg/share/grid/grid.component';
+import {GridDataSource} from '@eg/share/grid/grid';
+import {AcqSearchService, AcqSearchTerm, AcqSearch} from './acq-search.service';
+import {PicklistCreateDialogComponent} from './picklist-create-dialog.component';
+import {PicklistCloneDialogComponent} from './picklist-clone-dialog.component';
+import {PicklistDeleteDialogComponent} from './picklist-delete-dialog.component';
+import {PicklistMergeDialogComponent} from './picklist-merge-dialog.component';
+import {AcqSearchFormComponent} from './acq-search-form.component';
+
+@Component({
+  selector: 'eg-picklist-results',
+  templateUrl: 'picklist-results.component.html',
+  providers: [AcqSearchService]
+})
+export class PicklistResultsComponent implements OnInit {
+
+    @Input() initialSearchTerms: AcqSearchTerm[] = [];
+
+    gridSource: GridDataSource;
+    @ViewChild('acqSearchPicklistsGrid', { static: true }) picklistResultsGrid: GridComponent;
+    @ViewChild('picklistCreateDialog', { static: true }) picklistCreateDialog: PicklistCreateDialogComponent;
+    @ViewChild('picklistCloneDialog', { static: true }) picklistCloneDialog: PicklistCloneDialogComponent;
+    @ViewChild('picklistDeleteDialog', { static: true }) picklistDeleteDialog: PicklistDeleteDialogComponent;
+    @ViewChild('picklistMergeDialog', { static: true }) picklistMergeDialog: PicklistMergeDialogComponent;
+    @ViewChild('createSelectionListString', { static: true }) createSelectionListString: StringComponent;
+    @ViewChild('cloneSelectionListString', { static: true }) cloneSelectionListString: StringComponent;
+    @ViewChild('deleteSelectionListString', { static: true }) deleteSelectionListString: StringComponent;
+    @ViewChild('mergeSelectionListString', { static: true }) mergeSelectionListString: StringComponent;
+
+    permissions: {[name: string]: boolean};
+    noSelectedRows: (rows: IdlObject[]) => boolean;
+    oneSelectedRows: (rows: IdlObject[]) => boolean;
+    createNotAppropriate: (rows: IdlObject[]) => boolean;
+    cloneNotAppropriate: (rows: IdlObject[]) => boolean;
+    mergeNotAppropriate: (rows: IdlObject[]) => boolean;
+    deleteNotAppropriate: (rows: IdlObject[]) => boolean;
+
+    constructor(
+        private router: Router,
+        private route: ActivatedRoute,
+        private toast: ToastService,
+        private net: NetService,
+        private auth: AuthService,
+        private acqSearch: AcqSearchService,
+        private perm: PermService
+    ) {
+      this.permissions = {};
+    }
+
+    ngOnInit() {
+        this.gridSource = this.acqSearch.getAcqSearchDataSource('picklist');
+
+        this.perm.hasWorkPermHere(['CREATE_PICKLIST', 'UPDATE_PICKLIST', 'VIEW_PICKLIST']).
+          then(perms => this.permissions = perms);
+
+        this.noSelectedRows = (rows: IdlObject[]) => (rows.length === 0);
+        this.oneSelectedRows = (rows: IdlObject[]) => (rows.length === 1);
+        this.createNotAppropriate = (rows: IdlObject[]) => (!this.permissions.CREATE_PICKLIST);
+        this.cloneNotAppropriate = (rows: IdlObject[]) => (!this.permissions.CREATE_PICKLIST || !this.oneSelectedRows(rows));
+        this.mergeNotAppropriate = (rows: IdlObject[]) => (!this.permissions.UPDATE_PICKLIST || this.noSelectedRows(rows));
+        this.deleteNotAppropriate = (rows: IdlObject[]) => (!this.permissions.UPDATE_PICKLIST || this.noSelectedRows(rows));
+    }
+
+    openCreateDialog() {
+        this.picklistCreateDialog.open().subscribe(
+            modified => {
+                this.createSelectionListString.current().then(msg => this.toast.success(msg));
+                this.picklistResultsGrid.reload(); // FIXME - spec calls for inserted grid row and not refresh
+            }
+        );
+        this.picklistCreateDialog.update(); // clear and focus the textbox
+    }
+
+    openCloneDialog(rows: IdlObject[]) {
+        this.picklistCloneDialog.open().subscribe(
+            modified => {
+                this.cloneSelectionListString.current().then(msg => this.toast.success(msg));
+                this.picklistResultsGrid.reload(); // FIXME - spec calls for inserted grid row and not refresh
+            }
+        );
+        this.picklistCloneDialog.update(); // update the dialog UI with selections
+    }
+
+    openDeleteDialog(rows: IdlObject[]) {
+        this.picklistDeleteDialog.open().subscribe(
+            modified => {
+                this.deleteSelectionListString.current().then(msg => this.toast.success(msg));
+                this.picklistResultsGrid.reload(); // FIXME - spec calls for removed grid rows and not refresh
+            }
+        );
+        this.picklistDeleteDialog.update(); // update the dialog UI with selections
+    }
+
+    openMergeDialog(rows: IdlObject[]) {
+        this.picklistMergeDialog.open().subscribe(
+            modified => {
+                this.mergeSelectionListString.current().then(msg => this.toast.success(msg));
+                this.picklistResultsGrid.reload(); // FIXME - spec calls for removed grid rows and not refresh
+            }
+        );
+        this.picklistMergeDialog.update(); // update the dialog UI with selections
+    }
+
+    doSearch(search: AcqSearch) {
+        setTimeout(() => {
+            this.acqSearch.setSearch(search);
+            this.picklistResultsGrid.reload();
+        });
+    }
+}
diff --git a/Open-ILS/src/eg2/src/app/staff/acq/search/purchase-order-results.component.html b/Open-ILS/src/eg2/src/app/staff/acq/search/purchase-order-results.component.html
new file mode 100644 (file)
index 0000000..14c6d96
--- /dev/null
@@ -0,0 +1,32 @@
+<eg-acq-search-form (searchSubmitted)="doSearch($event)" [initialSearchTerms]="initialSearchTerms"
+  i18n-searchTypeLabel searchTypeLabel="Purchase Order" runImmediatelySetting="eg.acq.search.purchaseorders.run_immediately"
+   defaultSearchSetting="eg.acq.search.default.purchaseorders"></eg-acq-search-form>
+
+<ng-template #nameTmpl let-purchaseorder="row">
+  <a href="/eg/staff/acq/legacy/po/view/{{purchaseorder.id()}}"
+     target="_blank">
+    {{purchaseorder.name()}}
+  </a>
+</ng-template>
+
+<ng-template #providerTmpl let-purchaseorder="row">
+  <a href="/eg/staff/admin/acq/conify/provider/{{purchaseorder.provider().id()}}"
+     target="_blank">
+    {{purchaseorder.provider().code()}}
+  </a>
+</ng-template>
+
+<eg-grid #acqSearchPurchaseOrdersGrid
+  persistKey="acq.search.purchaseorders"
+  [stickyHeader]="true"
+  [filterable]="true"
+  idlClass="acqpo" [dataSource]="gridSource">
+
+  <eg-grid-column path="name" [cellTemplate]="nameTmpl"></eg-grid-column>
+  <eg-grid-column path="provider" [cellTemplate]="providerTmpl"></eg-grid-column>
+
+  <eg-grid-column path="creator" [hidden]="true"></eg-grid-column>
+  <eg-grid-column path="editor" [hidden]="true"></eg-grid-column>
+  <eg-grid-column path="owner" [hidden]="true"></eg-grid-column>
+
+</eg-grid>
diff --git a/Open-ILS/src/eg2/src/app/staff/acq/search/purchase-order-results.component.ts b/Open-ILS/src/eg2/src/app/staff/acq/search/purchase-order-results.component.ts
new file mode 100644 (file)
index 0000000..75888b9
--- /dev/null
@@ -0,0 +1,44 @@
+import {Component, OnInit, Input, ViewChild} from '@angular/core';
+import {Observable} from 'rxjs';
+import {map} from 'rxjs/operators';
+import {Router, ActivatedRoute, ParamMap} from '@angular/router';
+import {Pager} from '@eg/share/util/pager';
+import {IdlObject} from '@eg/core/idl.service';
+import {NetService} from '@eg/core/net.service';
+import {AuthService} from '@eg/core/auth.service';
+import {GridComponent} from '@eg/share/grid/grid.component';
+import {GridDataSource} from '@eg/share/grid/grid';
+import {AcqSearchService, AcqSearchTerm, AcqSearch} from './acq-search.service';
+import {AcqSearchFormComponent} from './acq-search-form.component';
+
+@Component({
+  selector: 'eg-purchase-order-results',
+  templateUrl: 'purchase-order-results.component.html',
+  providers: [AcqSearchService]
+})
+export class PurchaseOrderResultsComponent implements OnInit {
+
+    @Input() initialSearchTerms: AcqSearchTerm[] = [];
+
+    gridSource: GridDataSource;
+    @ViewChild('acqSearchPurchaseOrdersGrid', { static: true }) purchaseOrderResultsGrid: GridComponent;
+
+    constructor(
+        private router: Router,
+        private route: ActivatedRoute,
+        private net: NetService,
+        private auth: AuthService,
+        private acqSearch: AcqSearchService) {
+    }
+
+    ngOnInit() {
+        this.gridSource = this.acqSearch.getAcqSearchDataSource('purchase_order');
+    }
+
+    doSearch(search: AcqSearch) {
+        setTimeout(() => {
+            this.acqSearch.setSearch(search);
+            this.purchaseOrderResultsGrid.reload();
+        });
+    }
+}
diff --git a/Open-ILS/src/eg2/src/app/staff/acq/search/routing.module.ts b/Open-ILS/src/eg2/src/app/staff/acq/search/routing.module.ts
new file mode 100644 (file)
index 0000000..c4a4f68
--- /dev/null
@@ -0,0 +1,20 @@
+import {NgModule} from '@angular/core';
+import {RouterModule, Routes} from '@angular/router';
+import {AcqSearchComponent} from './acq-search.component';
+
+const routes: Routes = [
+  { path: '',
+    component: AcqSearchComponent
+  },
+  { path: ':searchtype',
+    component: AcqSearchComponent
+  }
+];
+
+@NgModule({
+  imports: [RouterModule.forChild(routes)],
+  exports: [RouterModule],
+  providers: []
+})
+
+export class AcqSearchRoutingModule {}
index 265368a..120cae1 100644 (file)
         </a>
         <div class="dropdown-menu" ngbDropdownMenu>
           <a class="dropdown-item" 
-            href="/eg/staff/acq/legacy/search/unified">
+            routerLink="/staff/acq/search">
             <span class="material-icons">search</span>
             <span i18n>General Search</span>
           </a>
           <div class="dropdown-divider"></div>
           <a class="dropdown-item" 
-            href="/eg/staff/acq/legacy/search/unified?ca=pl">
+            routerLink="/staff/acq/search/selectionlists"
+            [queryParams]="{f: 'acqpl:owner', val1: user_id()}">
             <span class="material-icons">view_list</span>
             <span i18n>My Selection Lists</span>
           </a>
             <span i18n>Load MARC Order Records</span>
           </a>
           <a class="dropdown-item" 
-            href="/eg/staff/acq/legacy/search/unified?ca=po">
+            routerLink="/staff/acq/search/purchaseorders"
+            [queryParams]="{f: ['acqpo:ordering_agency','acqpo:state'], val1: [ws_ou(), 'on-order']}">
             <span class="material-icons">shopping_cart</span>
             <span i18n>Purchase Orders</span>
           </a>
             <span i18n>Claim-Ready Items</span>
           </a>
           <a class="dropdown-item" 
-            href="/eg/staff/acq/legacy/search/unified?ca=inv">
+            routerLink="/staff/acq/search/invoices"
+            [queryParams]="{f: ['acqinv:receiver', 'acqinv:close_date'], val1: [ws_ou(), null]}">
             <span class="material-icons">attach_money</span>
             <span i18n>Open Invoices</span>
           </a>
index f143727..1497870 100644 (file)
@@ -60,10 +60,18 @@ export class StaffNavComponent implements OnInit {
         return this.auth.user() ? this.auth.user().usrname() : '';
     }
 
+    user_id() {
+        return this.auth.user() ? this.auth.user().id() : '';
+    }
+
     workstation() {
         return this.auth.user() ? this.auth.workstation() : '';
     }
 
+    ws_ou() {
+        return this.auth.user() ? this.auth.user().ws_ou() : '';
+    }
+
     setLocale(locale: any) {
         this.locale.setLocale(locale.code());
     }
index e390a3d..59aab99 100644 (file)
@@ -19,6 +19,9 @@ const routes: Routes = [{
     redirectTo: 'splash',
     pathMatch: 'full',
   }, {
+    path: 'acq',
+    loadChildren : '@eg/staff/acq/routing.module#AcqRoutingModule'
+  }, {
     path: 'booking',
     loadChildren : '@eg/staff/booking/booking.module#BookingModule'
   }, {
index ef97e2a..fb43897 100644 (file)
@@ -219,3 +219,22 @@ body>.dropdown-menu {z-index: 2100;}
   background-color: #c9efe4;
   color: black;
 }
+
+/**
+ * Make the acquisitions search form's navigation tabs match
+ * those of the staff interface. This is a global rule because
+ * various approaches to doing it local to the acq search component
+ * don't work:
+ *
+ * 1. A rule bound to the container of the acq search tabset
+ *    would make the entire background be grey.
+ * 2. ":host ::ng-deep" for a local rule works, but depends on a
+ *    mechanism that is deprecated.
+ * 4. ng-tabset provides no hooks for custom styles for the nav-tab
+ *    background.
+ * 5. Turning off view encapsulation for the acq search component
+ *    breaks a lot of styles.
+ */
+#acq-search-page ngb-tabset .nav.nav-tabs {
+  background-color: rgb(247, 247, 247);
+}
index 1028f42..731926f 100644 (file)
         </a>
         <ul uib-dropdown-menu>
           <li>
-            <a href="./acq/legacy/search/unified" target="_self">
+            <a href="/eg2/staff/acq/search" target="_self">
               <span class="glyphicon glyphicon-search"></span>
               [% l('General Search') %]
             </a>
           </li>
           <li class="divider"></li>
           <li>
-            <a href="./acq/legacy/search/unified?ca=pl" target="_self">
+            <a href="/eg2/staff/acq/search/selectionlists?f=acqpl:owner&val1={{user_id}}" target="_self">
               <span class="glyphicon glyphicon-list"></span>
               [% l('My Selection Lists') %]
             </a>
             </a>
           </li>
           <li>
-            <a href="./acq/legacy/search/unified?ca=po" target="_self">
+            <a href="/eg2/staff/acq/search/purchaseorders?f=acqpo:ordering_agency&f=acqpo:state&val1={{ws_ou}}&val1=on-order" target="_self">
               <span class="glyphicon glyphicon-shopping-cart"></span>
               [% l('Purchase Orders') %]
             </a>
             </a>
           </li>
           <li>
-            <a href="./acq/legacy/search/unified?ca=inv" target="_self">
+            <a href="/eg2/staff/acq/search/invoices?f=acqinv:receiver&f=acqinv:close_date&val1={{ws_ou}}&val1=null" target="_self">
               <span class="glyphicon glyphicon-usd"></span>
               [% l('Open Invoices') %]
             </a>
index b6ff1d3..9c01748 100644 (file)
@@ -108,6 +108,8 @@ angular.module('egCoreMod')
                         if (egCore.auth.user()) {
                             $scope.op_changed = egCore.auth.OCtoken() ? true : false;
                             $scope.username = egCore.auth.user().usrname();
+                            $scope.user_id = egCore.auth.user().id();
+                            $scope.ws_ou = egCore.auth.user().ws_ou();
                             $scope.workstation = egCore.auth.workstation();
 
                             egCore.org.settings([