net: NetService;
displayHighlights: {[name: string]: string | string[]} = {};
eResourceUrls: EResourceUrl[] = [];
+ copies: any[];
constructor(record: IdlObject, orgId: number, orgDepth?: number) {
this.id = Number(record.id());
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(); }
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
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(); }
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
summary.attributes = metabibSummary.attributes;
summary.holdCount = metabibSummary.hold_count;
summary.holdingsSummary = metabibSummary.copy_counts;
+ summary.copies = metabibSummary.copies;
+
return summary;
}));
}
// 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 => {
showBasket: boolean;
searchOrg: IdlObject;
global: boolean;
+ prefOu: number;
termSearch: CatalogTermContext;
marcSearch: CatalogMarcContext;
result: CatalogSearchResults;
searchState: CatalogSearchState = CatalogSearchState.PENDING;
+ // fetch and show extra holdings data, etc.
+ showResultExtras = false;
+
// List of IDs in page/offset context.
resultIds: number[];
// Display the Exclude Electronic checkbox
showExcludeElectronic = false;
- // TODO: does unapi support pref-lib for result-page copy counts?
prefOrg: IdlObject;
// Default search tab
'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 =
</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 -->
<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>
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',
searchSub: Subscription;
routeSub: Subscription;
basketSub: Subscription;
+ showMoreDetails = false;
constructor(
private route: ActivatedRoute,
private bib: BibRecordService,
private catUrl: CatalogUrlService,
private staffCat: StaffCatalogService,
+ private serverStore: ServerStoreService,
private basket: BasketService
) {}
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;
}
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
);
}
}
}};
+
+ 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
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/);
$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) = @_;
'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'
+ )
+);
+
--- /dev/null
+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;