LP1936233 Item status list view
authorBill Erickson <berickxx@gmail.com>
Wed, 21 Jul 2021 21:39:59 +0000 (17:39 -0400)
committerBill Erickson <berickxx@gmail.com>
Wed, 21 Jul 2021 21:39:59 +0000 (17:39 -0400)
Signed-off-by: Bill Erickson <berickxx@gmail.com>
Open-ILS/src/eg2/src/app/staff/cat/item/item.service.ts
Open-ILS/src/eg2/src/app/staff/cat/item/status.component.html
Open-ILS/src/eg2/src/app/staff/cat/item/status.component.ts

index 1b1700b..ee879f1 100644 (file)
@@ -3,7 +3,12 @@ import {IdlObject} from '@eg/core/idl.service';
 
 @Injectable()
 export class ItemStatusService {
+
     // These must persist route changes.
     scannedItems: IdlObject[] = [];
+
+    // If true, we've loaded the search/list UI's copies from its ID list
+    // Avoid re-loading them.
+    preloadItemsLoaded = false;
 }
 
index 463604e..0b184e5 100644 (file)
       </div>
       <input type="text" class="form-control" id="item-barcode-input" 
         (keydown)="noSuchItem=false; true;"
-        (keyup.enter)="getItemByBarcode()" [(ngModel)]="itemBarcode" 
+        (keyup.enter)="getItemFromBarcodeInput()" [(ngModel)]="itemBarcode" 
         aria-describedby="barcode-label"/>
     </div>
     <button class="btn btn-outline-dark mr-1" 
-      (click)="getItemByBarcode()" i18n>Submit</button>
+      (click)="getItemFromBarcodeInput()" i18n>Submit</button>
     <eg-help-popover placement="top" i18n-helpText 
       helpText="Single barcode or list of barcodes separated with commas.">
     </eg-help-popover>
 
+    <eg-help-popover placement="top" i18n-helpText 
+      helpText="File Format: One barcode per line. 
+        All whitespace and commas will be removed before processing.">
+    </eg-help-popover>
+
     <div class="flex-1"></div>
 
     <button *ngIf="tab == 'list'" 
   {{r.call_number().suffix().label()}}
 </ng-template>
 
-<eg-grid *ngIf="tab == 'list'" #grid [dataSource]="dataSource"
+<eg-grid *ngIf="tab == 'list'" #grid [dataSource]="dataSource" idlClass="acp"
+  (onRowActivate)="showDetails($event)" [cellTextGenerator]="cellTextGenerator"
   [useLocalSort]="true" [sortable]="true" [showDeclaredFieldsOnly]="true">
 
-  <eg-grid-column i18n-label label="Item ID" path="id" [index]="true">
-  </eg-grid-column>
+  <eg-grid-column path="id" [hidden]="true"></eg-grid-column>
+  <eg-grid-column path="alert_message"></eg-grid-column>
+  <eg-grid-column path="barcode"></eg-grid-column>
 
-  <eg-grid-column i18n-label label="Alert Message" path="alert_message">
+  <eg-grid-column i18n-label label="Location" path="location.name">
   </eg-grid-column>
 
-  <eg-grid-column i18n-label label="Barcode" path="barcode">
+  <eg-grid-column i18n-label label="Item Status" path="status.name">
   </eg-grid-column>
 
-  <eg-grid-column i18n-label label="Location" path="location.name">
-  </eg-grid-column>
 
   <eg-grid-column i18n-label label="Due Date" path="_circ.due_date"
     timezoneContextOrg="_circ.circ_lib" dateOnlyIntervalField="_circ.duration"
   <eg-grid-column i18n-label label="Title"
     path="call_number.record.simple_record.title"></eg-grid-column>
 
-  <eg-grid-column i18n-label label="Author"
-    path="call_number.record.simple_record.author"></eg-grid-column>
-
   <eg-grid-column path="call_number.*" [hidden]="true"></eg-grid-column>
+  <eg-grid-column path="call_number.record.*" [hidden]="true"></eg-grid-column>
   <eg-grid-column path="call_number.record.simple_record.*" [hidden]="true">
   </eg-grid-column>
 
index f2537b2..7af2f61 100644 (file)
@@ -57,13 +57,14 @@ import {ItemStatusService} from './item.service';
 
 export class ItemStatusComponent implements OnInit, AfterViewInit {
 
-    itemId: number;
+    currentItemId: number;
     itemBarcode: string;
     noSuchItem = false;
     item: IdlObject;
     tab: string;
     preloadCopyIds: number[];
 
+    cellTextGenerator: GridCellTextGenerator;
     dataSource: GridDataSource = new GridDataSource();
     @ViewChild('grid') private grid: GridComponent;
 
@@ -120,27 +121,37 @@ export class ItemStatusComponent implements OnInit, AfterViewInit {
 
     ngOnInit() {
 
-        this.itemId = +this.route.snapshot.paramMap.get('id');
+        this.currentItemId = +this.route.snapshot.paramMap.get('id');
         this.tab = this.route.snapshot.paramMap.get('tab');
 
         const copyIdList = this.route.snapshot.paramMap.get('copyIdList');
-        if (copyIdList) {
+
+        if (copyIdList && !this.itemService.preloadItemsLoaded) {
+            this.itemService.preloadItemsLoaded = true;
             this.preloadCopyIds = copyIdList.split(',').map(id => Number(id));
         }
 
         if (!this.tab) {
-            if (this.itemId) {
+            if (this.currentItemId) {
                 this.tab = 'summary';
             } else {
                 this.tab = 'list';
             }
         }
 
-        this.worklog.loadSettings().then(_ => this.load(true));
-
         this.dataSource.getRows = (pager: Pager, sort: any[]) => {
             return from(this.itemService.scannedItems);
         };
+
+        this.cellTextGenerator = {
+            call_number_label: row => {
+                return  row.call_number().prefix().label() + ' ' +
+                        row.call_number().label() + ' ' +
+                        row.call_number().suffix().label();
+            }
+        }
+
+        this.worklog.loadSettings().then(_ => this.load(true));
     }
 
     load(first?: boolean) {
@@ -148,8 +159,8 @@ export class ItemStatusComponent implements OnInit, AfterViewInit {
         this.cat.fetchCcvms()
         .then(_ => this.cat.fetchCmfs())
         .then(_ => {
-            if (this.itemId) {
-                return this.getItemById(this.itemId);
+            if (this.currentItemId) {
+                return this.getItemById(this.currentItemId);
             } else if (this.preloadCopyIds) {
                 return from(this.preloadCopyIds).pipe(concatMap(id => {
                     return of(this.getItemById(id));
@@ -167,8 +178,8 @@ export class ItemStatusComponent implements OnInit, AfterViewInit {
                 this.tab = params.get('tab') || 'list';
                 const id = +params.get('id');
 
-                if (id && id !== this.itemId) {
-                    this.itemId = id;
+                if (id && id !== this.currentItemId) {
+                    this.currentItemId = id;
                     this.getItemById(id);
                 }
             });
@@ -180,27 +191,52 @@ export class ItemStatusComponent implements OnInit, AfterViewInit {
     }
 
     tabChange(evt: NgbNavChangeEvent) {
-        this.router.navigate([`/staff/cat/item/${this.itemId}/${evt.nextId}`]);
+        this.router.navigate([`/staff/cat/item/${this.currentItemId}/${evt.nextId}`]);
     }
 
-    getItemByBarcode(): Promise<any> {
-        this.itemId = null;
+    getItemFromBarcodeInput(): Promise<any> {
+        this.currentItemId = null;
         this.item = null;
 
         if (!this.itemBarcode) { return Promise.resolve(); }
 
-        return this.barcodeSelect.getBarcode('asset', this.itemBarcode)
+        // The barcode may be a comma-separated list of values.
+        const barcodes = [];
+        this.itemBarcode.split(/,/).forEach(bc => {
+            bc = bc.replace(/[\s,]+/g,'');
+            if (bc) { barcodes.push(bc); }
+        });
+
+        let index = 0;
+        return from(barcodes).pipe(concatMap(bc => {
+            return of(
+                this.getOneItemFromBarcode(bc)
+                .then(_ => {
+                    if (++index < barcodes.length) { return; }
+                    if (this.tab === 'list') { return; }
+
+                    // When entering multiple items via input or file
+                    // on a non-list page, show the detail view of the
+                    // last item loaded.
+                    if (this.itemService.scannedItems.length > 0) {
+                        const id = this.itemService.scannedItems[0].id();
+                        this.router.navigate([`/staff/cat/item/${id}/${this.tab}`]);
+                    }
+                })
+            );
+        })).toPromise();
+    }
+
+    getOneItemFromBarcode(barcode: string): Promise<any> {
+        return this.barcodeSelect.getBarcode('asset', barcode)
         .then(res => {
             if (!res.id) {
                 this.noSuchItem = true;
             } else {
                 this.itemBarcode = null;
-
                 if (this.tab === 'list') {
                     this.selectInput();
                     return this.getItemById(res.id);
-                } else {
-                    this.router.navigate([`/staff/cat/item/${res.id}/${this.tab}`]);
                 }
             }
         });
@@ -242,11 +278,37 @@ export class ItemStatusComponent implements OnInit, AfterViewInit {
             this.item = item;
             this.mungeIsbns();
             this.selectInput();
-            this.itemService.scannedItems.unshift(item);
-            this.grid.reload();
+
+            if (this.tab === 'list') {
+                this.itemService.scannedItems.unshift(item);
+            } else {
+                // Only add the copy to the scanned items list on a non-list
+                // page when the item does not already exist in the list.
+                const existing = this.itemService.scannedItems
+                    .filter(c => c.id() === item.id())[0];
+                if (!existing) {
+                    this.itemService.scannedItems.unshift(item);
+                }
+            }
+
+            return this.getItemCirc(item).then(_ => {
+                if (this.grid) {
+                    this.grid.reload();
+                }
+            });
         });
     }
 
+    getItemCirc(item: IdlObject): Promise<any> {
+        if (item.status().id() !== 1 /* Checked Out */) {
+            return Promise.resolve();
+        }
+
+        return this.pcrud.search('circ', {target_copy: item.id()},
+            {order_by: {circ: 'xact_start DESC'}, limit: 1})
+        .toPromise().then(circ => item._circ = circ);
+    }
+
     // A bit of cleanup to make the ISBN's look friendlier
     mungeIsbns() {
         const item = this.item;
@@ -594,10 +656,27 @@ export class ItemStatusComponent implements OnInit, AfterViewInit {
         .then(success => success ? this.load() : null);
     }
 
-    showDetails() {
+    showDetails(copy?: IdlObject) {
+        let copyId;
+
+        if (copy) {
+            // Row doubleclick
+            copyId = copy.id();
+        } else if (this.grid) {
+            // Row select + clicking the Show Details button
+            copyId = this.grid.context.rowSelector.selected()[0];
+        }
+
+        // No item selected.  Use the first one in the list if we have one.
+        if (!copyId && this.itemService.scannedItems.length > 0) {
+            copyId = this.itemService.scannedItems[0].id();
+        }
+
+        this.router.navigate([`/staff/cat/item/${copyId}/summary`]);
     }
 
     showList() {
+        this.currentItemId = null;
         this.router.navigate(['/staff/cat/item/list']);
     }
 }