LP1851882 Angular catalog recall/force/part holds
authorBill Erickson <berickxx@gmail.com>
Fri, 8 Nov 2019 22:02:48 +0000 (17:02 -0500)
committerChris Sharp <csharp@georgialibraries.org>
Fri, 25 Sep 2020 17:28:04 +0000 (13:28 -0400)
Adds entry points for placing Recall, Force, and Part-level holds.

For any item-level hold type, the user now has the option to cycle
between Item, Recall, and Force hold types.  The selected type affects
the full batch of holds.

For title-level holds, the user now has the option to select a part as
the hold target for each hold in the list.  Part selection is optional.

Signed-off-by: Bill Erickson <berickxx@gmail.com>
Signed-off-by: Jennifer Weston <jennifer.weston@equinoxinitiative.org>
Signed-off-by: Chris Sharp <csharp@georgialibraries.org>

Open-ILS/src/eg2/src/app/staff/catalog/hold/hold.component.html
Open-ILS/src/eg2/src/app/staff/catalog/hold/hold.component.ts
Open-ILS/src/eg2/src/app/staff/share/holds/holds.service.ts
Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Holds.pm

index dca200d..6645443 100644 (file)
 
 <div class="row"><div class="col-lg-12"><hr/></div></div>
 
-<div class="row font-weight-bold pt-3 ml-1 mr-1">
-  <div class="col-lg-12" i18n>Placing 
-    <ng-container *ngIf="holdType == 'M'">METARECORD</ng-container> 
-    <ng-container *ngIf="holdType == 'T'">TITLE</ng-container> 
-    <ng-container *ngIf="holdType == 'V'">CALL NUMBER</ng-container>
-    <ng-container *ngIf="holdType == 'F'">FORCE COPY</ng-container> 
-    <ng-container *ngIf="holdType == 'C'">COPY</ng-container> 
-    <ng-container *ngIf="holdType == 'R'">RECALL</ng-container> 
-    <ng-container *ngIf="holdType == 'I'">ISSUANCE</ng-container> 
-    <ng-container *ngIf="holdType == 'P'">PARTS</ng-container> 
-    hold on record(s)</div>
+<div class="row pt-3 ml-1 mr-1 d-flex">
+  <div class="">
+    <span class="font-weight-bold" i18n>Placing 
+      <ng-container *ngIf="holdType == 'M'">METARECORD</ng-container> 
+      <ng-container *ngIf="holdType == 'T'">TITLE</ng-container> 
+      <ng-container *ngIf="holdType == 'V'">CALL NUMBER</ng-container>
+      <ng-container *ngIf="holdType == 'F'">FORCE ITEM</ng-container> 
+      <ng-container *ngIf="holdType == 'C'">ITEM</ng-container> 
+      <ng-container *ngIf="holdType == 'R'">RECALL</ng-container> 
+      <ng-container *ngIf="holdType == 'I'">ISSUANCE</ng-container> 
+      <ng-container *ngIf="holdType == 'P'">PARTS</ng-container> 
+      hold on record(s)
+    </span>
+  </div>
+  <div class="flex-1"> </div>
+  <div>
+    <span class="pl-3" *ngIf="isItemHold()">
+      <span i18n>Item-Level Hold Options:</span>
+      <span class="pl-2">
+        <a routerLink="/staff/catalog/hold/C" queryParamsHandling="merge">
+          <button [disabled]="holdType === 'C'" class="btn btn-outline-primary" 
+            i18n>Item Hold</button>
+        </a>
+      </span>
+      <span class="pl-2">
+        <a routerLink="/staff/catalog/hold/R" queryParamsHandling="merge">
+          <button [disabled]="holdType === 'R'" class="btn btn-outline-primary" 
+            i18n>Recall Hold</button>
+        </a>
+      </span>
+      <span class="pl-2">
+        <a routerLink="/staff/catalog/hold/F" queryParamsHandling="merge">
+          <button [disabled]="holdType === 'F'" class="btn btn-outline-primary"
+            i18n>Force Item Hold</button>
+        </a>
+      </span>
+    </span>
+  </div>
 </div>
 
 <ng-template #anyValue>
 
   <div class="row mt-2 ml-1 mr-1 font-weight-bold">
     <div class="col-lg-1" i18n>Format</div>
-    <div class="col-lg-3" i18n>Title</div>
-    <div class="col-lg-2" i18n>Author</div>
+    <div class="col-lg-2" i18n>Title</div>
+    <div class="col-lg-1" i18n>Author</div>
+    <div class="col-lg-2" i18n>Part</div>
     <div class="col-lg-2" i18n>Call Number</div>
     <div class="col-lg-1" i18n>Barcode</div>
     <div class="col-lg-2" i18n>Holds Status</div>
         </div>
         <!-- TODO: link for a metarecord should 
             jump to constituent bib list search page? -->
-        <div class="col-lg-3">
+        <div class="col-lg-2">
           <a routerLink="/staff/catalog/record/{{ctx.holdMeta.bibId}}">
             {{ctx.holdMeta.bibSummary.display.title}}
           </a>
         </div>
-        <div class="col-lg-2">{{ctx.holdMeta.bibSummary.display.author}}</div>
+        <div class="col-lg-1">{{ctx.holdMeta.bibSummary.display.author}}</div>
+        <div class="col-lg-2">
+          <ng-container *ngIf="ctx.holdMeta.parts.length">
+            <select class="form-control" (change)="setPart(ctx, $event)">
+              <option value="" i18n>Any Part</option>
+              <option *ngFor="let part of ctx.holdMeta.parts" 
+                value="{{part.id()}}">{{part.label()}}</option>
+            </select>
+          </ng-container>
+          <ng-container *ngIf="ctx.holdMeta.parts.length == 0">
+            <ng-container *ngIf="ctx.holdMeta.part">
+              <span>{{ctx.holdMeta.part.label()}}</span>
+            </ng-container>
+            <ng-container *ngIf="!ctx.holdMeta.part">
+              <span i18n>N/A</span>
+            </ng-container>
+          </ng-container>
+        </div>
         <div class="col-lg-2">
           <ng-container *ngIf="ctx.holdMeta.callNum; else anyValue">
             {{ctx.holdMeta.callNum.label()}}
         </div>
         <div class="col-lg-2">
           <ng-container *ngIf="!ctx.lastRequest && !ctx.processing">
-            <div class="alert alert-info" i18n>Hold Pending</div>
+            <div class="alert alert-info p-1 ml-2" i18n>Hold Pending</div>
           </ng-container>
           <ng-container *ngIf="ctx.processing">
-            <div class="alert alert-primary" i18n>Hold Processing...</div>
+            <div class="alert alert-primary p-1 ml-2" i18n>Hold Processing...</div>
           </ng-container>
           <ng-container *ngIf="ctx.lastRequest">
             <ng-container *ngIf="ctx.lastRequest.result.success">
-              <div class="alert alert-success" i18n>Hold Succeeded</div>
+              <div class="alert alert-success p-1 ml-2" i18n>Hold Succeeded</div>
             </ng-container>
             <ng-container *ngIf="!ctx.lastRequest.result.success">
-              <div class="alert alert-danger">
+              <div class="alert alert-danger p-1 ml-2" 
+                title="{{ctx.lastRequest.result.evt.textcode}}">
                 {{ctx.lastRequest.result.evt.textcode}}
               </div>
             </ng-container>
index 687783e..133480f 100644 (file)
@@ -91,6 +91,13 @@ export class HoldComponent implements OnInit {
 
     ngOnInit() {
 
+        // Respond to changes in hold type.  This currently assumes hold
+        // types only toggle post-init between copy-level types (C,R,F)
+        // and no other params (e.g. target) change with it.  If other
+        // types require tracking, additional data collection may be needed.
+        this.route.paramMap.subscribe(
+            (params: ParamMap) => this.holdType = params.get('type'));
+
         this.holdType = this.route.snapshot.params['type'];
         this.holdTargets = this.route.snapshot.queryParams['target'];
         this.holdFor = this.route.snapshot.queryParams['holdFor'] || 'patron';
@@ -316,7 +323,10 @@ export class HoldComponent implements OnInit {
     // Attempt hold placement on all targets
     placeHolds(idx?: number) {
         if (!idx) { idx = 0; }
-        if (!this.holdTargets[idx]) { return; }
+        if (!this.holdTargets[idx]) {
+            this.placeHoldsClicked = false;
+            return;
+        }
         this.placeHoldsClicked = true;
 
         const target = this.holdTargets[idx];
@@ -331,9 +341,21 @@ export class HoldComponent implements OnInit {
         ctx.processing = true;
         const selectedFormats = this.mrSelectorsToFilters(ctx);
 
+        let hType = this.holdType;
+        let hTarget = ctx.holdTarget;
+        if (hType === 'T' && ctx.holdMeta.part) {
+            // A Title hold morphs into a Part hold at hold placement time
+            // if a part is selected.  This can happen on a per-hold basis
+            // when placing T-level holds.
+            hType = 'P';
+            hTarget = ctx.holdMeta.part.id();
+        }
+
+        console.debug(`Placing ${hType}-type hold on ${hTarget}`);
+
         return this.holds.placeHold({
-            holdTarget: ctx.holdTarget,
-            holdType: this.holdType,
+            holdTarget: hTarget,
+            holdType: hType,
             recipient: this.user.id(),
             requestor: this.requestor.id(),
             pickupLib: this.pickupLib,
@@ -348,20 +370,22 @@ export class HoldComponent implements OnInit {
 
         }).toPromise().then(
             request => {
-                console.log('hold returned: ', request);
                 ctx.lastRequest = request;
                 ctx.processing = false;
 
-                // If this request failed and was not already an override,
-                // see of this user has permission to override.
-                if (!request.override &&
-                    !request.result.success && request.result.evt) {
+                if (!request.result.success) {
+                    console.debug('hold failed with: ', request);
+
+                    // If this request failed and was not already an override,
+                    // see of this user has permission to override.
+                    if (!request.override && request.result.evt) {
 
-                    const txtcode = request.result.evt.textcode;
-                    const perm = txtcode + '.override';
+                        const txtcode = request.result.evt.textcode;
+                        const perm = txtcode + '.override';
 
-                    return this.perm.hasWorkPermHere(perm).then(
-                        permResult => ctx.canOverride = permResult[perm]);
+                        return this.perm.hasWorkPermHere(perm).then(
+                            permResult => ctx.canOverride = permResult[perm]);
+                    }
                 }
             },
             error => {
@@ -411,6 +435,22 @@ export class HoldComponent implements OnInit {
             }
         );
     }
+
+    isItemHold(): boolean {
+        return this.holdType === 'C'
+            || this.holdType === 'R'
+            || this.holdType === 'F';
+    }
+
+    setPart(ctx: HoldContext, $event) {
+        const partId = $event.target.value;
+        if (partId) {
+            ctx.holdMeta.part =
+                ctx.holdMeta.parts.filter(p => +p.id() === +partId)[0];
+        } else {
+            ctx.holdMeta.part = null;
+        }
+    }
 }
 
 
index a6ddb7e..f1c6d0b 100644 (file)
@@ -50,6 +50,7 @@ export interface HoldRequestTarget {
     bibId?: number;
     bibSummary?: BibRecordSummary;
     part?: IdlObject;
+    parts?: IdlObject[];
     callNum?: IdlObject;
     copy?: IdlObject;
     issuance?: IdlObject;
index 11c29f8..25f2633 100644 (file)
@@ -4874,6 +4874,7 @@ sub hold_metadata {
             volume => $volume,
             issuance => $issuance,
             part => $part,
+            parts => [],
             bibrecord => $bre,
             metarecord => $metarecord,
             metarecord_filters => {}
@@ -4891,6 +4892,10 @@ sub hold_metadata {
                 $meta->{metarecord} = 
                     $e->retrieve_metabib_metarecord($map->metarecord);
             }
+
+            # Also fetch the available parts for bib-level holds.
+            $meta->{parts} = $e->search_biblio_monograph_part(
+                {record => $bre->id, deleted => 'f'});
         }
 
         if ($meta->{metarecord}) {