LP1908722 Staff catalog Show More Details
authorBill Erickson <berickxx@gmail.com>
Fri, 2 Jul 2021 20:57:40 +0000 (16:57 -0400)
committerGalen Charlton <gmc@equinoxOLI.org>
Mon, 12 Jul 2021 21:00:26 +0000 (17:00 -0400)
Adds a "Show More Details" (and "Show Fewer Details") buttons to the
Angualr staff catalog.  Similar to the TPAC, activating the button means
more holdings details are displayed in the search results page.

Adds a new workstation setting type called
'eg.staff.catalog.results.show_more'

Signed-off-by: Bill Erickson <berickxx@gmail.com>
Signed-off-by: Galen Charlton <gmc@equinoxOLI.org>
Signed-off-by: Michele Morgan <mmorgan@noblenet.org>

Open-ILS/src/eg2/src/app/share/catalog/bib-record.service.ts
Open-ILS/src/eg2/src/app/share/catalog/catalog.service.ts
Open-ILS/src/eg2/src/app/share/catalog/search-context.ts
Open-ILS/src/eg2/src/app/staff/catalog/catalog.service.ts
Open-ILS/src/eg2/src/app/staff/catalog/resolver.service.ts
Open-ILS/src/eg2/src/app/staff/catalog/result/record.component.html
Open-ILS/src/eg2/src/app/staff/catalog/result/results.component.html
Open-ILS/src/eg2/src/app/staff/catalog/result/results.component.ts
Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Biblio.pm
Open-ILS/src/sql/Pg/950.data.seed-values.sql
Open-ILS/src/sql/Pg/upgrade/XXXX.data.angstcat-show-more-details.sql [new file with mode: 0644]

index ce352c7..999ce37 100644 (file)
@@ -38,6 +38,7 @@ export class BibRecordSummary {
     net: NetService;
     displayHighlights: {[name: string]: string | string[]} = {};
     eResourceUrls: EResourceUrl[] = [];
+    copies: any[];
 
     constructor(record: IdlObject, orgId: number, orgDepth?: number) {
         this.id = Number(record.id());
@@ -95,8 +96,8 @@ export class BibRecordService {
         return this.getBibSummaries([id], orgId, isStaff);
     }
 
-    getBibSummaries(bibIds: number[],
-        orgId?: number, isStaff?: boolean): Observable<BibRecordSummary> {
+    getBibSummaries(bibIds: number[], orgId?: number,
+        isStaff?: boolean, options?: any): Observable<BibRecordSummary> {
 
         if (bibIds.length === 0) { return from([]); }
         if (!orgId) { orgId = this.org.root().id(); }
@@ -104,7 +105,7 @@ export class BibRecordService {
         let method = 'open-ils.search.biblio.record.catalog_summary';
         if (isStaff) { method += '.staff'; }
 
-        return this.net.request('open-ils.search', method, orgId, bibIds)
+        return this.net.request('open-ils.search', method, orgId, bibIds, options)
         .pipe(map(bibSummary => {
             const summary = new BibRecordSummary(bibSummary.record, orgId);
             summary.net = this.net; // inject
@@ -113,12 +114,14 @@ export class BibRecordService {
             summary.holdCount = bibSummary.hold_count;
             summary.holdingsSummary = bibSummary.copy_counts;
             summary.eResourceUrls = bibSummary.urls;
+            summary.copies = bibSummary.copies;
+
             return summary;
         }));
     }
 
     getMetabibSummaries(metabibIds: number[],
-        orgId?: number, isStaff?: boolean): Observable<BibRecordSummary> {
+        orgId?: number, isStaff?: boolean, options?: any): Observable<BibRecordSummary> {
 
         if (metabibIds.length === 0) { return from([]); }
         if (!orgId) { orgId = this.org.root().id(); }
@@ -126,7 +129,7 @@ export class BibRecordService {
         let method = 'open-ils.search.biblio.metabib.catalog_summary';
         if (isStaff) { method += '.staff'; }
 
-        return this.net.request('open-ils.search', method, orgId, metabibIds)
+        return this.net.request('open-ils.search', method, orgId, metabibIds, options)
         .pipe(map(metabibSummary => {
             const summary = new BibRecordSummary(metabibSummary.record, orgId);
             summary.net = this.net; // inject
@@ -136,6 +139,8 @@ export class BibRecordService {
             summary.attributes = metabibSummary.attributes;
             summary.holdCount = metabibSummary.hold_count;
             summary.holdingsSummary = metabibSummary.copy_counts;
+            summary.copies = metabibSummary.copies;
+
             return summary;
         }));
     }
index 136e27a..d2e3044 100644 (file)
@@ -205,20 +205,27 @@ export class CatalogService {
     // Returns a void promise once all records have been retrieved
     fetchBibSummaries(ctx: CatalogSearchContext): Promise<void> {
 
-        const depth = ctx.global ?
-            ctx.org.root().ou_type().depth() :
-            ctx.searchOrg.ou_type().depth();
+        const org = ctx.global ? ctx.org.root() : ctx.searchOrg;
+        const depth = org.ou_type().depth();
 
         const isMeta = ctx.termSearch.isMetarecordSearch();
 
         let observable: Observable<BibRecordSummary>;
 
+        const options: any = {};
+        if (ctx.showResultExtras) {
+            options.flesh_copies = true;
+            options.copy_depth = depth;
+            options.copy_limit = 5;
+            options.pref_ou = ctx.prefOu;
+        }
+
         if (isMeta) {
             observable = this.bibService.getMetabibSummaries(
-                ctx.currentResultIds(), ctx.searchOrg.id(), ctx.isStaff);
+                ctx.currentResultIds(), ctx.searchOrg.id(), ctx.isStaff, options);
         } else {
             observable = this.bibService.getBibSummaries(
-                ctx.currentResultIds(), ctx.searchOrg.id(), ctx.isStaff);
+                ctx.currentResultIds(), ctx.searchOrg.id(), ctx.isStaff, options);
         }
 
         return observable.pipe(map(summary => {
index 4552d5e..7ee7ab1 100644 (file)
@@ -341,6 +341,7 @@ export class CatalogSearchContext {
     showBasket: boolean;
     searchOrg: IdlObject;
     global: boolean;
+    prefOu: number;
 
     termSearch: CatalogTermContext;
     marcSearch: CatalogMarcContext;
@@ -352,6 +353,9 @@ export class CatalogSearchContext {
     result: CatalogSearchResults;
     searchState: CatalogSearchState = CatalogSearchState.PENDING;
 
+    // fetch and show extra holdings data, etc.
+    showResultExtras = false;
+
     // List of IDs in page/offset context.
     resultIds: number[];
 
index 081135c..e763b35 100644 (file)
@@ -25,7 +25,6 @@ export class StaffCatalogService {
     // Display the Exclude Electronic checkbox
     showExcludeElectronic = false;
 
-    // TODO: does unapi support pref-lib for result-page copy counts?
     prefOrg: IdlObject;
 
     // Default search tab
index 5ec91d5..f7e9562 100644 (file)
@@ -57,6 +57,7 @@ export class CatalogResolver implements Resolve<Promise<any[]>> {
             'opac.search.enable_bookplate_search',
             'eg.staffcat.exclude_electronic',
             'eg.catalog.search.form.open',
+            'eg.staff.catalog.results.show_more',
             'circ.staff_placed_holds_fallback_to_ws_ou'
         ]).then(settings => {
             this.staffCat.defaultSearchOrg =
index 3f05d42..a8cb874 100644 (file)
         </div>
       </div><!-- col -->
     </div><!-- row -->
+    <div class="row" *ngIf="summary.copies">
+      <div class="col-lg-12 mt-2">
+        <div class="w-auto ml-2 mr-2">
+          <ng-container *ngIf="summary.copies.length">
+            <div class="row p-1 font-weight-bold border-top">
+              <div class="col-lg-2" i18n>Library</div>
+              <div class="col-lg-3" i18n>Shelving location</div>
+              <div class="col-lg-4" i18n>Call number</div>
+              <div class="col-lg-3" i18n>Status</div>
+            </div>
+            <div class="row p-1 mt-1 mb-1 border-top" *ngFor="let copy of summary.copies">
+              <div class="col-lg-2" i18n>{{copy.circ_lib_sn}}</div>
+              <div class="col-lg-3" i18n>{{copy.copy_location}}</div>
+              <div class="col-lg-4" i18n>
+                {{copy.call_number_prefix_label}} 
+                {{copy.call_number_label}}
+                {{copy.call_number_suffix_label}}
+              </div>
+              <div class="col-lg-3" i18n>{{copy.copy_status}}</div>
+            </div>
+          </ng-container>
+          <ng-container *ngIf="!summary.copies.length">
+            <span class="font-italic" i18n>No Items To Display</span>
+          </ng-container>
+        </div>
+      </div>
+    </div>
   </div><!-- card-body -->
 </div><!-- card -->
 
index 4346fc0..96953ee 100644 (file)
@@ -43,7 +43,7 @@
     <div class="col-lg-2" *ngIf="searchContext.basket">
       <h3 i18n>Basket View</h3>
     </div>
-    <div class="col-lg-2">
+    <div class="col-lg-3">
       <label class="checkbox" *ngIf="!searchContext.basket">
         <input type='checkbox' [(ngModel)]="allRecsSelected" 
             (change)="toggleAllRecsSelected()"/>
           {{searchContext.pager.rowNumber(searchContext.currentResultIds().length - 1)}}
         </span>
       </label>
+      <button class="btn btn-outline-dark ml-2" (click)="toggleShowMore()">
+        <ng-container *ngIf="showMoreDetails" i18n>Show Fewer Details</ng-container>
+        <ng-container *ngIf="!showMoreDetails" i18n>Show More Details</ng-container>
+      </button>
     </div>
-    <div class="col-lg-8">
+    <div class="col-lg-7">
       <div class="float-right">
         <eg-catalog-result-pagination></eg-catalog-result-pagination>
       </div>
index edcb381..34706f1 100644 (file)
@@ -10,6 +10,7 @@ import {PcrudService} from '@eg/core/pcrud.service';
 import {StaffCatalogService} from '../catalog.service';
 import {IdlObject} from '@eg/core/idl.service';
 import {BasketService} from '@eg/share/catalog/basket.service';
+import {ServerStoreService} from '@eg/core/server-store.service';
 
 @Component({
   selector: 'eg-catalog-results',
@@ -28,6 +29,7 @@ export class ResultsComponent implements OnInit, OnDestroy {
     searchSub: Subscription;
     routeSub: Subscription;
     basketSub: Subscription;
+    showMoreDetails = false;
 
     constructor(
         private route: ActivatedRoute,
@@ -36,6 +38,7 @@ export class ResultsComponent implements OnInit, OnDestroy {
         private bib: BibRecordService,
         private catUrl: CatalogUrlService,
         private staffCat: StaffCatalogService,
+        private serverStore: ServerStoreService,
         private basket: BasketService
     ) {}
 
@@ -95,16 +98,44 @@ export class ResultsComponent implements OnInit, OnDestroy {
     searchByUrl(params: ParamMap): void {
         this.catUrl.applyUrlParams(this.searchContext, params);
 
+
         if (this.searchContext.isSearchable()) {
 
-            this.cat.search(this.searchContext)
-            .then(ok => {
-                this.cat.fetchFacets(this.searchContext);
-                this.cat.fetchBibSummaries(this.searchContext);
+            this.serverStore.getItem('eg.staff.catalog.results.show_more')
+            .then(showMore => {
+
+                this.showMoreDetails =
+                    this.searchContext.showResultExtras = showMore;
+
+                if (this.staffCat.prefOrg) {
+                    this.searchContext.prefOu = this.staffCat.prefOrg.id();
+                }
+
+                this.cat.search(this.searchContext)
+                .then(ok => {
+                    this.cat.fetchFacets(this.searchContext);
+                    this.cat.fetchBibSummaries(this.searchContext);
+                });
             });
         }
     }
 
+    toggleShowMore() {
+        this.showMoreDetails = !this.showMoreDetails;
+
+        this.serverStore.setItem(
+            'eg.staff.catalog.results.show_more', this.showMoreDetails)
+        .then(_ => {
+
+            if (this.showMoreDetails) {
+                this.staffCat.search();
+            } else {
+                // Clear the collected copies.  No need for another search.
+                this.searchContext.result.records.forEach(rec => rec.copies = undefined);
+            }
+        });
+    }
+
     searchIsDone(): boolean {
         return this.searchContext.searchState === CatalogSearchState.COMPLETE;
     }
index 95720a8..1bdb62f 100644 (file)
@@ -2887,8 +2887,9 @@ sub mk_copy_query {
     my $copy_offset = shift;
     my $pref_ou = shift;
     my $is_staff = shift;
+    my $base_query = shift;
 
-    my $query = $U->basic_opac_copy_query(
+    my $query = $base_query || $U->basic_opac_copy_query(
         $rec_id, undef, undef, $copy_limit, $copy_offset, $is_staff
     );
 
@@ -2912,6 +2913,16 @@ sub mk_copy_query {
                 }
             }
         }};
+
+        if ($pref_ou) {
+            # Make sure the pref OU is included in the results
+            my $in = $query->{from}->{acp}->[1]->{aou}->{filter}->{id}->{in};
+            delete $query->{from}->{acp}->[1]->{aou}->{filter}->{id};
+            $query->{from}->{acp}->[1]->{aou}->{filter}->{'-or'} = [
+                {id => {in => $in}},
+                {id => $pref_ou}
+            ];
+        }
     };
 
     # Unsure if we want these in the shared function, leaving here for now
@@ -3057,8 +3068,9 @@ __PACKAGE__->register_method(
 
 
 sub catalog_record_summary {
-    my ($self, $client, $org_id, $record_ids) = @_;
+    my ($self, $client, $org_id, $record_ids, $options) = @_;
     my $e = new_editor();
+    $options ||= {};
 
     my $is_meta = ($self->api_name =~ /metabib/);
     my $is_staff = ($self->api_name =~ /staff/);
@@ -3086,12 +3098,83 @@ sub catalog_record_summary {
         $response->{hold_count} = 
             $U->simplereq('open-ils.circ', $holds_method, $rec_id);
 
+        if ($options->{flesh_copies}) {
+            $response->{copies} = get_representative_copies(
+                $e, $rec_id, $org_id, $is_staff, $is_meta, $options);
+        }
+
         $client->respond($response);
     }
 
     return undef;
 }
 
+# Returns a snapshot of copy information for a given record or metarecord,
+# sorted by pref org and search org.
+sub get_representative_copies {
+    my ($e, $rec_id, $org_id, $is_staff, $is_meta, $options) = @_;
+
+    my @rec_ids;
+    my $limit = $options->{copy_limit};
+    my $copy_depth = $options->{copy_depth};
+    my $copy_offset = $options->{copy_offset};
+    my $pref_ou = $options->{pref_ou};
+
+    my $org_tree = $U->get_org_tree;
+    if (!$org_id) { $org_id = $org_tree->id; }
+    my $org = $U->find_org($org_tree, $org_id);
+
+    return [] unless $org;
+
+    my $func = 'unapi.biblio_record_entry_feed';
+    my $includes = '{holdings_xml,acp,acnp,acns}';
+    my $limits = "acn=>$limit,acp=>$limit";
+
+    if ($is_meta) {
+        $func = 'unapi.metabib_virtual_record_feed';
+        $includes = '{holdings_xml,acp,acnp,acns,mmr.unapi}';
+        $limits .= ",bre=>$limit";
+    }
+
+    my $xml_query = $e->json_query({from => [
+        $func, '{'.$rec_id.'}', 'marcxml', 
+        $includes, $org->shortname, $copy_depth, $limits,
+        undef, undef,undef, undef, undef, 
+        undef, undef, undef, $pref_ou
+    ]})->[0];
+
+    my $xml = $xml_query->{$func};
+
+    my $doc = XML::LibXML->new->parse_string($xml);
+
+    my $copies = [];
+    for my $volume ($doc->documentElement->findnodes('//*[local-name()="volume"]')) {
+        my $label = $volume->getAttribute('label');
+        my $prefix = $volume->getElementsByTagName('call_number_prefix')->[0]->getAttribute('label');
+        my $suffix = $volume->getElementsByTagName('call_number_suffix')->[0]->getAttribute('label');
+
+        my $copies_node = $volume->findnodes('./*[local-name()="copies"]')->[0];
+
+        for my $copy ($copies_node->findnodes('./*[local-name()="copy"]')) {
+
+            my $status = $copy->getElementsByTagName('status')->[0]->textContent;
+            my $location = $copy->getElementsByTagName('location')->[0]->textContent;
+            my $circ_lib_sn = $copy->getElementsByTagName('circ_lib')->[0]->getAttribute('shortname');
+
+            push(@$copies, {
+                call_number_label => $label,
+                call_number_prefix_label => $prefix,
+                call_number_suffix_label => $suffix,
+                circ_lib_sn => $circ_lib_sn,
+                copy_status => $status,
+                copy_location => $location
+            });
+        }
+    }
+
+    return $copies;
+}
+
 sub get_one_rec_urls {
     my ($self, $e, $org_id, $bib_id) = @_;
 
index 3e2a6b5..612fcc7 100644 (file)
@@ -21644,3 +21644,14 @@ VALUES
      'coust', 'description'),
    'integer' );
 
+
+INSERT INTO config.workstation_setting_type (name, grp, datatype, label)
+VALUES (
+    'eg.staff.catalog.results.show_more', 'gui', 'bool',
+    oils_i18n_gettext(
+        'eg.staff.catalog.results.show_more',
+        'Show more details in Angular staff catalog',
+        'cwst', 'label'
+    )
+);
+
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.data.angstcat-show-more-details.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.data.angstcat-show-more-details.sql
new file mode 100644 (file)
index 0000000..31b2e4c
--- /dev/null
@@ -0,0 +1,15 @@
+BEGIN;
+
+-- SELECT evergreen.upgrade_deps_block_check('TODO', :eg_version);
+
+INSERT INTO config.workstation_setting_type (name, grp, datatype, label)
+VALUES (
+    'eg.staff.catalog.results.show_more', 'gui', 'bool',
+    oils_i18n_gettext(
+        'eg.staff.catalog.results.show_more',
+        'Show more details in Angular staff catalog',
+        'cwst', 'label'
+    )
+);
+
+COMMIT;