LP1936233 Item status actions menu
authorBill Erickson <berickxx@gmail.com>
Thu, 15 Jul 2021 20:36:15 +0000 (16:36 -0400)
committerBill Erickson <berickxx@gmail.com>
Mon, 24 Oct 2022 15:05:00 +0000 (11:05 -0400)
Signed-off-by: Bill Erickson <berickxx@gmail.com>
Open-ILS/src/eg2/src/app/staff/cat/item/item.module.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

index fcb84ea..8f90048 100644 (file)
@@ -15,6 +15,7 @@ import {ItemRecentHistoryComponent} from './recent-history.component';
 import {ItemCircHistoryComponent} from './circ-history.component';
 import {ItemHoldsTransitsComponent} from './holds.component';
 import {GroupedMenuModule} from '@eg/share/grouped-menu/grouped-menu.module';
+import {WorkLogModule} from '@eg/staff/share/worklog/worklog.module';
 
 @NgModule({
   declarations: [
@@ -35,6 +36,7 @@ import {GroupedMenuModule} from '@eg/share/grouped-menu/grouped-menu.module';
     BookingModule,
     PatronModule,
     BillingModule,
+    WorkLogModule,
     GroupedMenuModule
   ],
   providers: [
index 1efc194..b5f917a 100644 (file)
@@ -1,8 +1,10 @@
 <eg-staff-banner i18n-bannerText bannerText="Item Status">
 </eg-staff-banner>
 
-<eg-barcode-select #barcodeSelect></eg-barcode-select>
 <eg-circ-components></eg-circ-components>
+<eg-worklog-strings-components></eg-worklog-strings-components> 
+<eg-progress-dialog #progressDialog></eg-progress-dialog>
+<eg-barcode-select #barcodeSelect></eg-barcode-select>
 <eg-mark-damaged-dialog #markDamagedDialog></eg-mark-damaged-dialog>
 <eg-mark-missing-dialog #markMissingDialog></eg-mark-missing-dialog>
 <eg-copy-alerts-dialog #copyAlertsDialog></eg-copy-alerts-dialog>
 <eg-make-bookable-dialog #makeBookableDialog></eg-make-bookable-dialog>
 <eg-transfer-items #transferItems></eg-transfer-items>
 <eg-transfer-holdings #transferHoldings></eg-transfer-holdings>
+<eg-cancel-transit-dialog #cancelTransitDialog></eg-cancel-transit-dialog>
 <eg-alert-dialog #transferAlert
   i18n-dialogTitle i18n-dialogBody
   dialogTitle="No Target Selected"
   dialogBody="Please select a suitable transfer target"></eg-alert-dialog>
 
-
 <div class="row">
   <div class="col-lg-12 form-inline d-flex">
     <div class="input-group">
 
     <div class="flex-1"></div>
 
+    <!-- ACTIONS MENU -->
     <eg-grouped-menu i18n-label label="Actions" *ngIf="item && tab != 'list'">
 
+      <!-- Un-grouped -->
+      <eg-grouped-menu-entry i18n-label label="Request Items"
+        (entryClicked)="requestItems([item])">
+      </eg-grouped-menu-entry>
+      <eg-grouped-menu-entry i18n-label 
+        label="Link as Conjoined to Marked Bib Record"
+        (entryClicked)="openConjoinedDialog([item])">
+      </eg-grouped-menu-entry>
+      <eg-grouped-menu-entry i18n-label label="Delete Items"
+        (entryClicked)="deleteItems([item])">
+      </eg-grouped-menu-entry>
+
+      <!-- Add -->
       <eg-grouped-menu-entry i18n-label label="Add Item To Bucket" 
-        (entryClicked)="addToBucket()"></eg-grouped-menu-entry>
+        i18n-group group="Add" (entryClicked)="addItemToBucket([item])">
+      </eg-grouped-menu-entry>
+      <eg-grouped-menu-entry i18n-label label="Add Record To Bucket" 
+        i18n-group group="Add" (entryClicked)="addRecordToBucket([item])">
+      </eg-grouped-menu-entry>
 
+      <!-- Show -->
       <eg-grouped-menu-entry i18n-label label="Show Record Holds"
         i18n-group group="Show" [newTab]="true"
         routerLink="/staff/catalog/record/{{item.call_number().record().id()}}/holds">
       </eg-grouped-menu-entry>
+      <eg-grouped-menu-entry i18n-label label="Show In Catalog"
+        i18n-group group="Show" [newTab]="true"
+        routerLink="/staff/catalog/record/{{item.call_number().record().id()}}">
+      </eg-grouped-menu-entry>
+
+      <!-- Booking -->
+      <eg-grouped-menu-entry i18n-label label="Make Items Bookable"
+        i18n-group group="Booking" (entryClicked)="makeItemsBookable([item])">
+      </eg-grouped-menu-entry>
+      <eg-grouped-menu-entry i18n-label label="Book Item Now"
+        i18n-group group="Booking" (entryClicked)="bookItems([item])">
+      </eg-grouped-menu-entry>
+      <eg-grouped-menu-entry i18n-label label="Manage Reservations"
+        i18n-group group="Booking" (entryClicked)="manageReservations([item])">
+      </eg-grouped-menu-entry>
+
+      <eg-grouped-menu-entry i18n-label label="Check In Items"
+        i18n-group group="Circulation" (entryClicked)="checkinItems([item])">
+      </eg-grouped-menu-entry>
+      <eg-grouped-menu-entry i18n-label label="Renew Items"
+        i18n-group group="Circulation" (entryClicked)="renewItems([item])">
+      </eg-grouped-menu-entry>
+      <eg-grouped-menu-entry i18n-label label="Cancel Transit"
+        i18n-group group="Circulation" (entryClicked)="cancelTransits([item])">
+      </eg-grouped-menu-entry>
 
     </eg-grouped-menu>
 
 
 <div *ngIf="tab != 'list' && item">
 
+  <div class="row" *ngIf="item.deleted() == 't'">
+    <div class="col-lg-4 offset-lg-4 alert alert-danger" i18n>
+      This item has been marked as Deleted.
+    </div>
+  </div>
+
   <div class="mt-2 mb-4">
     <eg-bib-summary [recordId]="item.call_number().record().id()">
     </eg-bib-summary>
index bb84666..8d204ef 100644 (file)
@@ -1,6 +1,8 @@
 import {Component, Input, OnInit, AfterViewInit, ViewChild} from '@angular/core';
 import {Router, ActivatedRoute, ParamMap} from '@angular/router';
-import {IdlObject} from '@eg/core/idl.service';
+import {from, empty} from 'rxjs';
+import {concatMap, tap} from 'rxjs/operators';
+import {IdlObject, IdlService} from '@eg/core/idl.service';
 import {PcrudService} from '@eg/core/pcrud.service';
 import {AuthService} from '@eg/core/auth.service';
 import {NetService} from '@eg/core/net.service';
@@ -10,7 +12,12 @@ import {EventService} from '@eg/core/event.service';
 import {PatronPenaltyDialogComponent} from '@eg/staff/share/patron/penalty-dialog.component';
 import {BarcodeSelectComponent} from '@eg/staff/share/barcodes/barcode-select.component';
 import {CatalogService} from '@eg/share/catalog/catalog.service';
+import {CircService} from '@eg/staff/share/circ/circ.service';
 import {NgbNav, NgbNavChangeEvent} from '@ng-bootstrap/ng-bootstrap';
+import {ProgressDialogComponent} from '@eg/share/dialog/progress.component';
+import {WorkLogService, WorkLogEntry} from '@eg/staff/share/worklog/worklog.service';
+import {CancelTransitDialogComponent
+    } from '@eg/staff/share/circ/cancel-transit-dialog.component';
 import {CopyAlertsDialogComponent
     } from '@eg/staff/share/holdings/copy-alerts-dialog.component';
 import {ReplaceBarcodeDialogComponent
@@ -69,16 +76,23 @@ export class ItemStatusComponent implements OnInit, AfterViewInit {
         private transferHoldings: TransferHoldingsComponent;
     @ViewChild('transferAlert')
         private transferAlert: AlertDialogComponent;
+    @ViewChild('progressDialog')
+        private progressDialog: ProgressDialogComponent;
+    @ViewChild('cancelTransitDialog')
+        private cancelTransitDialog: CancelTransitDialogComponent;
 
     constructor(
         private router: Router,
         private route: ActivatedRoute,
         private net: NetService,
+        private idl: IdlService,
         private printer: PrintService,
         private pcrud: PcrudService,
         private auth: AuthService,
+        private circ: CircService,
         private evt: EventService,
         private cat: CatalogService,
+        private worklog: WorkLogService,
         private holdings: HoldingsService
     ) {}
 
@@ -89,15 +103,13 @@ export class ItemStatusComponent implements OnInit, AfterViewInit {
 
         if (!this.tab) {
             if (this.itemId) {
-                this.router.navigate([`/staff/cat/item/${this.itemId}/summary`])
-                .then(ok => {if (ok) { this.load(); }});
-                return;
+                this.tab = 'summary';
             } else {
                 this.tab = 'list';
             }
         }
 
-        this.load();
+        this.worklog.loadSettings().then(_ => this.load());
     }
 
     load() {
@@ -195,11 +207,160 @@ export class ItemStatusComponent implements OnInit, AfterViewInit {
         });
     }
 
-    addToBucket() {
+    openProgressDialog(copies: IdlObject[]): ProgressDialogComponent {
+        this.progressDialog.update({value: 0, max: copies.length});
+        this.progressDialog.open();
+        return this.progressDialog;
+    }
+
+    addItemToBucket(copies: IdlObject[]) {
+        if (copies.length === 0) { return; }
         this.bucketDialog.bucketClass = 'copy';
-        this.bucketDialog.itemIds = [this.item.id()];
+        this.bucketDialog.itemIds = copies.map(c => c.id());
+        this.bucketDialog.open({size: 'lg'});
+    }
+
+    addRecordToBucket(copies: IdlObject[]) {
+        if (copies.length === 0) { return; }
+        const recId = copies[0].call_number().record().id();
+        this.bucketDialog.bucketClass = 'biblio';
+        this.bucketDialog.itemIds = [recId];
         this.bucketDialog.open({size: 'lg'});
     }
+
+    makeItemsBookable(copies: IdlObject[]) {
+        if (copies.length === 0) { return; }
+        this.makeBookableDialog.copyIds = copies.map(c => c.id());
+        this.makeBookableDialog.open({});
+    }
+
+    bookItems(copies: IdlObject[]) {
+        if (copies.length === 0) { return; }
+        const barcode = copies[0].barcode();
+        this.router.navigate(
+            ['/staff/booking/create_reservation/for_resource', barcode]);
+    }
+
+    manageReservations(copies: IdlObject[]) {
+        if (copies.length === 0) { return; }
+        const barcode = copies[0].barcode();
+        this.router.navigate(
+            ['/staff/booking/manage_reservations/by_resource', barcode]);
+    }
+
+    requestItems(copies: IdlObject[]) {
+        if (copies.length === 0) { return; }
+        const params = {target: copies.map(c => c.id()), holdFor: 'staff'};
+        this.router.navigate(['/staff/catalog/hold/C'], {queryParams: params});
+    }
+
+    openConjoinedDialog(copies: IdlObject[]) {
+        if (copies.length === 0) { return; }
+        this.conjoinedDialog.copyIds = copies.map(c => c.id());
+        this.conjoinedDialog.open({size: 'sm'});
+    }
+
+    deleteItems(copies: IdlObject[]) {
+        if (copies.length === 0) { return; }
+        const callNumHash: any = {};
+
+        // Collect the copies to be deleted, including their call numbers
+        // since the API expects fleshed call number objects.
+        copies.forEach(copy => {
+            const callNum = copy.call_number();
+            if (!callNumHash[callNum.id()]) {
+                callNumHash[callNum.id()] = this.idl.clone(callNum);
+                callNumHash[callNum.id()].copies([]);
+            }
+            const delCopy = this.idl.clone(copy);
+            delCopy.isdeleted(true);
+            callNumHash[callNum.id()].copies().push(delCopy);
+        });
+
+        if (Object.keys(callNumHash).length === 0) {
+            // No data to process.
+            return;
+        }
+
+        this.deleteHolding.callNums = Object.values(callNumHash);
+        this.deleteHolding.open({size: 'sm'}).subscribe(modified => this.load());
+    }
+
+    checkinItems(copies: IdlObject[]) {
+        if (copies.length === 0) { return; }
+
+        const dialog = this.openProgressDialog(copies);
+
+        let changesApplied = false;
+
+        this.circ.checkinBatch(copies.map(c => c.id()))
+        .subscribe(
+            result => {
+                if (result) { changesApplied = true; }
+                dialog.increment();
+            },
+            err => {
+                console.error('' + err);
+                dialog.close();
+            },
+            () => {
+                dialog.close();
+                this.load();
+            }
+        );
+    }
+
+    renewItems(copies: IdlObject[]) {
+        if (copies.length === 0) { return; }
+
+        const dialog = this.openProgressDialog(copies);
+
+        let changesApplied = false;
+
+        this.circ.renewBatch(copies.map(c => c.id()))
+        .subscribe(
+            result => {
+                if (result) { changesApplied = true; }
+                dialog.increment();
+            },
+            err => {
+                console.error('' + err);
+                dialog.close();
+            },
+            () => {
+                dialog.close();
+                this.load();
+            }
+        );
+    }
+
+    cancelTransits(copies: IdlObject[]) {
+        if (copies.length === 0) { return; }
+
+        // Copies in transit are not always accompanied by their transit.
+        const transitIds = [];
+        from(copies).pipe(concatMap(c => {
+            return from(
+                this.circ.findCopyTransitById(c.id())
+                .then(
+                    transit => transitIds.push(transit.id()),
+                    err => {}
+                )
+            );
+        }))
+
+        .pipe(concatMap(_ => {
+
+            if (transitIds.length > 0) {
+                this.cancelTransitDialog.transitIds = transitIds;
+                return this.cancelTransitDialog.open();
+            } else {
+                return empty();
+            }
+
+        })).subscribe();
+    }
 }
 
 
+
index 86d90e5..674ea0e 100644 (file)
@@ -1,10 +1,6 @@
 
 <eg-copy-alerts-dialog #copyAlertsDialog></eg-copy-alerts-dialog>
 
-<div class="alert alert-danger" *ngIf="item.deleted() == 't'">
-  This item has been marked as Deleted.
-</div>
-
 <div class="well-table" *ngIf="item && !loading">
 
   <div class="well-row" *ngIf="item.dummy_title() || item.dummy_author()">