LP1936233 Item status actions menu
authorBill Erickson <berickxx@gmail.com>
Wed, 21 Jul 2021 15:51:25 +0000 (11:51 -0400)
committerBill Erickson <berickxx@gmail.com>
Wed, 21 Jul 2021 15:51:25 +0000 (11:51 -0400)
Signed-off-by: Bill Erickson <berickxx@gmail.com>
Open-ILS/src/eg2/src/app/share/grouped-menu/grouped-menu.component.html
Open-ILS/src/eg2/src/app/share/grouped-menu/grouped-menu.component.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
Open-ILS/src/eg2/src/app/staff/cat/item/summary.component.html
Open-ILS/src/eg2/src/app/staff/catalog/record/holdings.component.ts
Open-ILS/src/eg2/src/app/staff/share/holdings/mark-items-dialog.component.ts
Open-ILS/src/eg2/src/app/staff/share/holdings/mark-missing-dialog.component.html
Open-ILS/src/eg2/src/app/staff/share/holdings/mark-missing-dialog.component.ts

index bbfdb98..863863c 100644 (file)
@@ -1,8 +1,8 @@
 
-<div ngbDropdown>
+<div ngbDropdown [placement]="placement">
   <button class="btn btn-outline-dark" id="{{domId}}" 
     ngbDropdownToggle i18n>{{label}}</button>                                
-  <div ngbDropdownMenu>
+  <div ngbDropdownMenu class="scrollable-menu">
     <button class="dropdown-item" *ngFor="let entry of menuEntries"
       [disabled]="entry.disabled"
       (click)="performAction(entry)" tabindex="0">
index 9e04f9f..6961c82 100644 (file)
@@ -28,6 +28,9 @@ export class GroupedMenuComponent implements OnInit, AfterViewInit {
 
     @Input() domId = 'grouped-menu-' + GroupedMenuComponent.autoId++;
 
+    // https://ng-bootstrap.github.io/#/components/dropdown/api
+    @Input() placement = 'bottom-right';
+
     menuEntries: GroupedMenuEntry[] = [];
 
     constructor(
index a888a8f..ddcab8a 100644 (file)
       <eg-grouped-menu-entry i18n-label label="Add Record To Bucket" 
         i18n-group group="Add" (entryClicked)="addRecordToBucket([item])">
       </eg-grouped-menu-entry>
+      <eg-grouped-menu-entry
+        i18n-group group="Add" i18n-label label="Add Items"
+        (entryClicked)="addItems([item])">
+      </eg-grouped-menu-entry>
+      <eg-grouped-menu-entry
+        i18n-group group="Add" i18n-label label="Add Call Numbers and Items"
+        (entryClicked)="addVols([item])">
+      </eg-grouped-menu-entry>
+      <eg-grouped-menu-entry
+        i18n-group group="Add" i18n-label label="Add Item Alerts"
+        (entryClicked)="itemAlerts([item], 'create')">
+      </eg-grouped-menu-entry>
 
       <!-- Edit -->
       <eg-grouped-menu-entry i18n-label label="Delete Items"
         i18n-group group="Edit" (entryClicked)="deleteItems([item])">
       </eg-grouped-menu-entry>
+      <eg-grouped-menu-entry i18n-label label="Edit Call Numbers"
+        i18n-group group="Edit" (entryClicked)="editVols([item])">
+      </eg-grouped-menu-entry>
+      <eg-grouped-menu-entry i18n-label label="Edit Items"
+        i18n-group group="Edit" (entryClicked)="editItems([item])">
+      </eg-grouped-menu-entry>
+      <eg-grouped-menu-entry i18n-label label="Edit Call Numbers and Items"
+        i18n-group group="Edit" (entryClicked)="editVolsAndItems([item])">
+      </eg-grouped-menu-entry>
+      <eg-grouped-menu-entry i18n-label label="Replace Barcodes"
+        i18n-group group="Edit" (entryClicked)="replaceBarcodes([item])">
+      </eg-grouped-menu-entry>
+      <eg-grouped-menu-entry
+        i18n-group group="Edit" i18n-label label="Manage Item Alerts"
+        (entryClicked)="itemAlerts([item], 'manage')">
+      </eg-grouped-menu-entry>
 
       <!-- Mark -->
       <eg-grouped-menu-entry i18n-label label="Mark Item as Damaged"
       <eg-grouped-menu-entry i18n-label label="Mark Item as Discard/Weed"
         i18n-group group="Mark" (entryClicked)="discardWeed([item])">
       </eg-grouped-menu-entry>
-
+      <eg-grouped-menu-entry i18n-label label="Mark Item as Missing"
+        i18n-group group="Mark" (entryClicked)="markMissing([item])">
+      </eg-grouped-menu-entry>
 
       <!-- Show -->
       <eg-grouped-menu-entry i18n-label label="Show Record Holds"
         i18n-group group="Show" (entryClicked)="showAcq([item])">
       </eg-grouped-menu-entry>
 
-
       <!-- Booking -->
       <eg-grouped-menu-entry i18n-label label="Make Items Bookable"
         i18n-group group="Booking" (entryClicked)="makeItemsBookable([item])">
         i18n-group group="Circulation" (entryClicked)="cancelTransits([item])">
       </eg-grouped-menu-entry>
 
+      <!-- Transfer -->
+      <eg-grouped-menu-entry i18n-label 
+        label="Transfer Items to Previously Marked Library"
+        i18n-group group="Transfer" (entryClicked)="transferItemsToLib([item])">
+      </eg-grouped-menu-entry>
+
+      <eg-grouped-menu-entry i18n-label 
+        label="Transfer Items to Previously Marked Call Number"
+        i18n-group group="Transfer" (entryClicked)="transferItemsToCn([item])">
+      </eg-grouped-menu-entry>
+
     </eg-grouped-menu>
 
     <!-- grouped menu todo -->
index 541a8f8..1d0e51f 100644 (file)
@@ -7,6 +7,7 @@ import {PcrudService} from '@eg/core/pcrud.service';
 import {AuthService} from '@eg/core/auth.service';
 import {NetService} from '@eg/core/net.service';
 import {PrintService} from '@eg/share/print/print.service';
+import {StoreService} from '@eg/core/store.service';
 import {HoldingsService} from '@eg/staff/share/holdings/holdings.service';
 import {EventService} from '@eg/core/event.service';
 import {PatronPenaltyDialogComponent} from '@eg/staff/share/patron/penalty-dialog.component';
@@ -96,6 +97,7 @@ export class ItemStatusComponent implements OnInit, AfterViewInit {
         private net: NetService,
         private idl: IdlService,
         private printer: PrintService,
+        private store: StoreService,
         private pcrud: PcrudService,
         private auth: AuthService,
         private circ: CircService,
@@ -215,10 +217,22 @@ export class ItemStatusComponent implements OnInit, AfterViewInit {
         .toPromise().then(item => {
             this.item = item;
             this.itemId = item.id();
+            this.mungeIsbns();
             this.selectInput();
         });
     }
 
+    // A bit of cleanup to make the ISBN's look friendlier
+    mungeIsbns() {
+        const item = this.item;
+        const isbn = item.call_number().record().simple_record().isbn();
+        if (isbn) {
+            item._isbns = isbn.match(/"(.*?)"/g).map(i => i.replace(/"/g, ''));
+        } else {
+            item._isbns = [item.dummy_isbn()];
+        }
+    }
+
     openProgressDialog(copies: IdlObject[]): ProgressDialogComponent {
         this.progressDialog.update({value: 0, max: copies.length});
         this.progressDialog.open();
@@ -427,21 +441,132 @@ export class ItemStatusComponent implements OnInit, AfterViewInit {
 
 
     discardWeed(copies: IdlObject[]) {
+        if (copies.length === 0) { return; }
         let modified = false;
 
         this.markItemsDialog.markAs = 'discard';
         this.markItemsDialog.copies = copies;
         this.markItemsDialog.open()
         .subscribe(
-            copyId => {
-                if (copyId !== null) { modified = true; }
-            },
+            copyId => modified = true,
             null,
             () => {
                 if (modified) { this.load(); }
             }
         );
     }
+
+    markMissing(copies: IdlObject[]) {
+        copies = copies.filter(c => c.status() !== 4 /* Missing */);
+        if (copies.length === 0) { return; }
+
+        let modified = false;
+
+        this.markMissingDialog.copies = copies;
+        this.markMissingDialog.open()
+        .subscribe(
+            copyId => modified = true,
+            null,
+            () => {
+                if (modified) { this.load(); }
+            }
+        );
+    }
+
+    addItems(copies: IdlObject[]) {
+        if (copies.length === 0) { return; }
+        const callNumIds = copies.map(c => c.call_number().id());
+        this.holdings.spawnAddHoldingsUi(null, callNumIds);
+    }
+
+    editItems(copies: IdlObject[]) {
+        if (copies.length === 0) { return; }
+        const copyIds = copies.map(c => c.id());
+        this.holdings.spawnAddHoldingsUi(null, null, null, copyIds);
+    }
+
+    editVols(copies: IdlObject[]) {
+        if (copies.length === 0) { return; }
+        const callNumIds = copies.map(c => c.call_number().id());
+        this.holdings.spawnAddHoldingsUi(null, callNumIds, null, null, true);
+    }
+
+    editVolsAndItems(copies: IdlObject[]) {
+        if (copies.length === 0) { return; }
+        const callNumIds = copies.map(c => c.call_number().id());
+        const copyIds = copies.map(c => c.id());
+        this.holdings.spawnAddHoldingsUi(null, callNumIds, null, copyIds);
+    }
+
+
+    // Only the first item is used as the basis for new
+    // call numbers.
+    addVols(copies: IdlObject[]) {
+        if (copies.length === 0) { return; }
+        const copy = copies[0];
+        const cnData = [{owner: copy.call_number().owning_lib()}];
+        this.holdings.spawnAddHoldingsUi(
+            copy.call_number().record().id(), null, cnData);
+    }
+
+    itemAlerts(copies: IdlObject[], mode: 'create' | 'manage') {
+        if (copies.length === 0) { return; }
+
+        this.copyAlertsDialog.copyIds = copies.map(c => c.id());
+        this.copyAlertsDialog.mode = mode;
+        this.copyAlertsDialog.open({size: 'lg'}).subscribe(
+            modified => {
+                if (modified) {
+                    this.load();
+                }
+            }
+        );
+    }
+
+    replaceBarcodes(copies: IdlObject[]) {
+        if (copies.length === 0) { return; }
+        this.replaceBarcode.copyIds = copies.map(c => c.id());
+        this.replaceBarcode.open({}).subscribe(
+            modified => {
+                if (modified) {
+                    this.load();
+                }
+            }
+        );
+    }
+
+    transferItemsToLib(copies: IdlObject[]) {
+        if (copies.length === 0) { return; }
+
+        const orgId = this.store.getLocalItem('eg.cat.transfer_target_lib');
+
+        const recId =
+            this.store.getLocalItem('eg.cat.transfer_target_record')
+            || copies[0].call_number().record().id();
+
+        if (!orgId) {
+            return this.transferAlert.open().toPromise();
+        }
+
+        copies = this.idl.clone(copies); // avoid tweaking active data
+        this.transferItems.autoTransferItems(copies, recId, orgId)
+        .then(success => success ? this.load() : null);
+    }
+
+
+    transferItemsToCn(copies: IdlObject[]) {
+        if (copies.length === 0) { return; }
+
+        const cnId =
+            this.store.getLocalItem('eg.cat.transfer_target_vol');
+
+        if (!cnId) {
+            return this.transferAlert.open().toPromise();
+        }
+
+        this.transferItems.transferItems(copies.map(c => c.id()), cnId)
+        .then(success => success ? this.load() : null);
+    }
 }
 
 
index 674ea0e..c9a5548 100644 (file)
@@ -76,7 +76,7 @@
   <div class="well-row">
     <div class="well-label" i18n>ISBN</div>
     <div class="well-value">
-      {{item.call_number().record().simple_record().isbn() || item.dummy_isbn()}}
+      <ng-container *ngFor="let i of item._isbns">{{i}}<br/></ng-container>
     </div>
 
     <div class="well-label" i18n>Loan Duration</div>
index 2a58ef6..d2d9068 100644 (file)
@@ -1011,7 +1011,6 @@ export class HoldingsMaintenanceComponent implements OnInit {
             items.forEach(i => i.call_number(
                 this.treeNodeCache.callNum[i.call_number()].target));
 
-            console.log(items);
             promise = this.transferItems.autoTransferItems(items, recId, orgId);
 
         } else {
index 42a666d..2536d37 100644 (file)
@@ -140,8 +140,7 @@ export class MarkItemsDialogComponent
         }));
     }
 
-    // Returns a stream of copy IDs.  Any non-null ID is a copy that
-    // was successfully modified.
+    // Returns a stream of copy IDs for successfully modified copies.
     markItems() {
         if (!this.copies || this.copies.length === 0) {
             this.close();
@@ -152,7 +151,14 @@ export class MarkItemsDialogComponent
 
         from(this.copies).pipe(concatMap(copy => {
             return this.markOneItem(copy)
-            .pipe(map(modified => this.respond(modified ? copy.id() : null)));
+            .pipe(map(modified => {
+                if (modified) {
+                    this.numSucceeded++;
+                    this.respond(copy.id());
+                } else {
+                    this.numFailed++;
+                }
+            }));
         })).toPromise().then(_ => this.close());
     }
 }
index ea6b7d7..2bd93a2 100644 (file)
@@ -1,44 +1,3 @@
 
+<eg-mark-items-dialog #markItemsDialog></eg-mark-items-dialog>
 
-<eg-string #successMsg
-    text="Successfully Marked Item Missing" i18n-text></eg-string>
-<eg-string #errorMsg 
-    text="Failed To Mark Item Missing" i18n-text></eg-string>
-
-<ng-template #dialogContent>
-    <div class="modal-header bg-info">
-      <h4 class="modal-title">
-        <span i18n>Mark Item Missing</span>
-      </h4>
-      <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">
-      <div class="row d-flex justify-content-center">
-          <h5>Mark {{copyIds.length}} Item(s) Missing?</h5>
-      </div>
-      <div class="row" *ngIf="numSucceeded > 0">
-        <div class="col-lg-12" i18n>
-          {{numSucceeded}} Items(s) Successfully Marked Missing
-        </div>
-      </div>
-      <div class="row" *ngIf="numFailed > 0">
-        <div class="col-lg-12">
-          <div class="alert alert-warning">
-            {{numFailed}} Items(s) Failed to be Marked Missing
-          </div>
-        </div>
-      </div>
-    </div>
-    <div class="modal-footer">
-      <ng-container *ngIf="!chargeResponse">
-        <button type="button" class="btn btn-warning" 
-          (click)="close()" i18n>Cancel</button>
-        <button type="button" class="btn btn-success" 
-          (click)="markItemsMissing()" i18n>Mark Missing</button>
-      </ng-container>
-    </div>
-  </ng-template>
-  
index 079298e..13517cc 100644 (file)
@@ -1,16 +1,23 @@
 import {Component, OnInit, Input, ViewChild} from '@angular/core';
-import {Observable, throwError} from 'rxjs';
+import {of, Observable, throwError} from 'rxjs';
+import {concatMap} from 'rxjs/operators';
 import {NetService} from '@eg/core/net.service';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {PcrudService} from '@eg/core/pcrud.service';
 import {EventService} from '@eg/core/event.service';
 import {ToastService} from '@eg/share/toast/toast.service';
 import {AuthService} from '@eg/core/auth.service';
 import {DialogComponent} from '@eg/share/dialog/dialog.component';
 import {NgbModal, NgbModalOptions} from '@ng-bootstrap/ng-bootstrap';
 import {StringComponent} from '@eg/share/string/string.component';
+import {MarkItemsDialogComponent} from './mark-items-dialog.component';
+
 
 
 /**
  * Dialog for marking items missing.
+ *
+ * This now simply invokes the generic Mark Items Dialog.
  */
 
 @Component({
@@ -22,65 +29,34 @@ export class MarkMissingDialogComponent
     extends DialogComponent implements OnInit {
 
     @Input() copyIds: number[];
+    @Input() copies: IdlObject[];
 
-    numSucceeded: number;
-    numFailed: number;
-
-    @ViewChild('successMsg', { static: true })
-        private successMsg: StringComponent;
-
-    @ViewChild('errorMsg', { static: true })
-        private errorMsg: StringComponent;
+    @ViewChild('markItemsDialog')
+        private markItemsDialog: MarkItemsDialogComponent;
 
     constructor(
-        private modal: NgbModal, // required for passing to parent
-        private toast: ToastService,
-        private net: NetService,
-        private evt: EventService,
-        private auth: AuthService) {
-        super(modal); // required for subclassing
-    }
+        private pcrud: PcrudService,
+        private modal: NgbModal
+    ) { super(modal); }
 
     ngOnInit() {}
 
-    open(args: NgbModalOptions): Observable<boolean> {
-        this.numSucceeded = 0;
-        this.numFailed = 0;
-        return super.open(args);
-    }
+    open(args?: NgbModalOptions): Observable<boolean> {
+        let obs: Observable<IdlObject[]>;
 
-    async markOneItemMissing(ids: number[]): Promise<any> {
-        if (ids.length === 0) {
-            return Promise.resolve();
+        if (this.copies) {
+            obs = of(this.copies);
+        } else {
+            obs = this.pcrud.search(
+                'acp', {id: this.copyIds}, {}, {atomic: true});
         }
 
-        const id = ids.pop();
+        return obs.pipe(concatMap(copies => {
+            this.markItemsDialog.markAs = 'missing';
+            this.markItemsDialog.copies = copies;
+            return this.markItemsDialog.open(args);
+        }));
 
-        return this.net.request(
-            'open-ils.circ',
-            'open-ils.circ.mark_item_missing',
-            this.auth.token(), id
-        ).toPromise().then(async(result) => {
-            if (Number(result) === 1) {
-                this.numSucceeded++;
-                this.toast.success(await this.successMsg.current());
-            } else {
-                this.numFailed++;
-                console.error('Mark missing failed ', this.evt.parse(result));
-                this.toast.warning(await this.errorMsg.current());
-            }
-            return this.markOneItemMissing(ids);
-        });
-    }
-
-    async markItemsMissing(): Promise<any> {
-        this.numSucceeded = 0;
-        this.numFailed = 0;
-        const ids = [].concat(this.copyIds);
-        await this.markOneItemMissing(ids);
-        this.close(this.numSucceeded > 0);
     }
 }
 
-
-