LP#1942220: start adding filtering to the LI table
authorGalen Charlton <gmc@equinoxOLI.org>
Mon, 29 Nov 2021 23:18:21 +0000 (18:18 -0500)
committerBill Erickson <berickxx@gmail.com>
Thu, 12 May 2022 14:27:06 +0000 (10:27 -0400)
TODO:

- publication date age filter
- filereader for 'terms from file'
- claim count and item count filters

Note that this is following the acquisitions search form, not the grid
filters

Signed-off-by: Galen Charlton <gmc@equinoxOLI.org>
Open-ILS/src/eg2/src/app/staff/acq/lineitem/lineitem-list.component.html
Open-ILS/src/eg2/src/app/staff/acq/lineitem/lineitem-list.component.ts

index 504f1e9..afca18d 100644 (file)
     <div class="col-lg-12 d-flex">
       <div class="d-flex justify-content-center flex-column h-100">
         <div class="form-group form-inline">
+          <label for="filter-field-select" class="form-check-label mr-1">Filter by:</label>
+          <select name="filter-field-select" id="filter-field-select"
+            [ngModel]="filterField" (ngModelChange)="filterFieldChange($event)"
+            class="form-control">
+            <option value="" i18n></option>
+            <option value="id" i18n>Lineitem ID</option>
+            <option value="state" i18n>Status</option>
+            <option value="acqlia:title" i18n>Title</option>
+            <option value="acqlia:author" i18n>Author</option>
+            <option value="acqlia:publisher" i18n>Publisher</option>
+            <option value="acqlia:pubdate" i18n>Publication date</option>
+            <option value="acqlia:isbn" i18n>ISBN</option>
+            <option value="acqlia:issn" i18n>ISSN</option>
+            <option value="acqlia:upc" i18n>UPC</option>
+<!-- TODO
+            <option value="claim_count" i18n>Claim count</option>
+            <option value="item_count" i18n>Item count</option>
+-->
+            <option value="estimated_unit_price" i18n>Estimated unit price</option>
+          </select> 
+          <label for="filter-operator-select" class="form-check-label mr-1 ml-1">using operator:</label>
+          <select name="filter-operator-select" id="filter-operator-select"
+            [(ngModel)]="filterOperator" (ngModelChange)="filterOperatorChange($event)"
+            class="form-control">
+            <option i18n value="">is</option>
+            <option i18n value="__not">is NOT</option>
+            <option i18n value="__fuzzy" [hidden]="searchTermDatatypes[filterField] != 'text'">contains</option>
+            <option i18n value="__not,__fuzzy" [hidden]="searchTermDatatypes[filterField]">does NOT contain</option>
+            <option i18n value="__starts" [hidden]="searchTermDatatypes[filterField] != 'text'">STARTS with</option>
+            <option i18n value="__ends" [hidden]="searchTermDatatypes[filterField] != 'text'">ENDS with</option>
+            <option i18n value="__lte" [hidden]="searchTermDatatypes[filterField] != 'timestamp' && !dateLikeSearchFields[filterField]">is on or BEFORE</option>
+            <option i18n value="__gte" [hidden]="searchTermDatatypes[filterField] != 'timestamp' && !dateLikeSearchFields[filterField]">is on or AFTER</option>
+            <option i18n value="__between" [hidden]="searchTermDatatypes[filterField] != 'timestamp'">is BETWEEN</option>
+            <option i18n value="__age" [hidden]="searchTermDatatypes[filterField] != 'timestamp'">age (relative date)</option>
+<!-- TODO
+            <option i18n value="__isnotnull" [hidden]="searchTermDatatypes[filterField] == 'id'">exists</option>
+            <option i18n value="__isnull" [hidden]="searchTermDatatypes[filterField] == 'id'">does NOT exist</option>
+            <option i18n value="__in">matches a term from a file</option>
+-->
+          </select> 
+          <label for="filter-value-input" class="form-check-label mr-1 ml-1">with value:</label>
+          <input *ngIf="searchTermDatatypes[filterField] != 'state'" type="text" name="filter-value-input" id="filter-value-input" [(ngModel)]="filterValue">
+          <eg-combobox *ngIf="searchTermDatatypes[filterField] == 'state'"
+            [asyncSupportsEmptyTermClick]="true"
+            idlClass="jubstlbl"
+            [selectedId]="filterValue"
+            (onChange)="filterValue = $event ? $event.id : ''">
+          </eg-combobox>
+          <button type="button" (click)="applyFilter()"
+            class="btn btn-sm btn-outline-dark mr-1 ml-1" [disabled]="!canApplyFilter()" i18n>Apply Filter</button>
+          <button type="button" (click)="resetFilter()"
+            class="btn btn-sm btn-outline-dark mr-1" i18n>Reset Filter</button>
+          </div>
+      </div>
+    </div>
+    <div class="col-lg-12 d-flex">
+      <div class="d-flex justify-content-center flex-column h-100">
+        <div class="form-group form-inline">
           <label for="sort-order-select" class="form-check-label mr-1">Sort by:</label>
           <select name="sort-order-select" id="sort-order-select"
             [ngModel]="sortOrder" (ngModelChange)="sortOrderChange($event)"
index 80fb5ea..592c07c 100644 (file)
@@ -66,6 +66,28 @@ export class LineitemListComponent implements OnInit {
     // sorting and filtering
     sortOrder = DEFAULT_SORT_ORDER;
     showFilterSort = false;
+    filterField = '';
+    filterOperator = '';
+    filterValue = '';
+    filterApplied = false;
+
+    searchTermDatatypes = {
+        'id': 'id',
+        'state': 'state',
+        'acqlia:title': 'text',
+        'acqlia:author': 'text',
+        'acqlia:publisher': 'text',
+        'acqlia:pubdate': 'text',
+        'acqlia:isbn': 'text',
+        'acqlia:issn': 'text',
+        'acqlia:upc': 'text',
+        'claim_count': 'text',
+        'item_count': 'text',
+        'estimated_unit_price': 'money',
+    };
+    dateLikeSearchFields = {
+        'acqlia:pubdate': true,
+    };
 
     batchNote: string;
     noteIsPublic = false;
@@ -97,6 +119,8 @@ export class LineitemListComponent implements OnInit {
 
     ngOnInit() {
 
+        this.liService.getLiAttrDefs();
+
         this.route.queryParamMap.subscribe((params: ParamMap) => {
             this.pager.offset = +params.get('offset');
             this.pager.limit = +params.get('limit');
@@ -154,6 +178,52 @@ export class LineitemListComponent implements OnInit {
         });
     }
 
+    filterFieldChange(event) {
+        this.filterOperator = '';
+        if (this.filterField === 'state') {
+            this.filterValue = '';
+        }
+        this.filterField = event;
+    }
+
+    filterOperatorChange() {
+        // empty for now
+    }
+
+    canApplyFilter(): boolean {
+        if (this.filterField !== '' &&
+            this.filterValue !== '') {
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    applyFilter() {
+        this.filterApplied = true;
+        if (this.pager.isFirstPage()) {
+            this.load();
+        } else {
+            this.pager.toFirst();
+            this.goToPage();
+        }
+    }
+
+    resetFilter() {
+        this.filterField = '';
+        this.filterOperator = '';
+        this.filterValue = '';
+        if (this.filterApplied) {
+            this.filterApplied = false;
+            if (this.pager.isFirstPage()) {
+                this.load();
+            } else {
+                this.pager.toFirst();
+                this.goToPage();
+            }
+        }
+    }
+
     // Focus the selected lineitem, which may not yet exist in the
     // DOM for focusing.
     focusLineitem(id?: number) {
@@ -197,6 +267,10 @@ export class LineitemListComponent implements OnInit {
             Object.assign(searchTerms, { jub: [ { purchase_order: this.poId } ] });
         }
 
+        if (this.filterApplied) {
+            this._handleFiltering(searchTerms);
+        }
+
         if (!(this.sortOrder in SORT_ORDER_MAP)) {
             this.sortOrder = DEFAULT_SORT_ORDER;
         }
@@ -216,6 +290,42 @@ export class LineitemListComponent implements OnInit {
         });
     }
 
+    _handleFiltering(searchTerms: any) {
+        const searchTerm: Object = {};
+        const filterField = this.filterField;
+        let filterOp = this.filterOperator;
+        let filterVal = this.filterValue;
+
+        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 !== '') {
+            searchTerm[filterOp] = true;
+        }
+
+        if (filterField.match(/^acqlia:/)) {
+            const attrName = (filterField.split(':'))[1];
+            const def = this.liService.liAttrDefs.filter(
+                d => d.code() === attrName)[0];
+            if (def) {
+                searchTerm[def.id()] = filterVal;
+                searchTerms['acqlia'] = [ searchTerm ];
+            }
+        } else {
+            searchTerm[filterField] = filterVal;
+            searchTerms['jub'].push(searchTerm);
+        }
+    }
+
     goToPage() {
         this.focusLi = null;
         this.router.navigate([], {