]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/eg2/src/app/staff/catalog/catalog.service.ts
LP#2019207: (follow-up) fix lint
[Evergreen.git] / Open-ILS / src / eg2 / src / app / staff / catalog / catalog.service.ts
1 import {Injectable, EventEmitter, NgZone} from '@angular/core';
2 import {Router, ActivatedRoute} from '@angular/router';
3 import {IdlObject} from '@eg/core/idl.service';
4 import {OrgService} from '@eg/core/org.service';
5 import {CatalogService} from '@eg/share/catalog/catalog.service';
6 import {CatalogUrlService} from '@eg/share/catalog/catalog-url.service';
7 import {CatalogSearchContext} from '@eg/share/catalog/search-context';
8 import {BibRecordSummary} from '@eg/share/catalog/bib-record.service';
9 import {PatronService} from '@eg/staff/share/patron/patron.service';
10 import {StoreService} from '@eg/core/store.service';
11 import {BroadcastService} from '@eg/share/util/broadcast.service';
12 import {Observable} from 'rxjs';
13 import {tap} from 'rxjs/operators';
14
15 const HOLD_FOR_PATRON_KEY = 'eg.circ.patron_hold_target';
16
17 /**
18  * Shared bits needed by the staff version of the catalog.
19  */
20
21 @Injectable()
22 export class StaffCatalogService {
23
24     searchContext: CatalogSearchContext;
25     routeIndex = 0;
26     defaultSearchOrg: IdlObject;
27     defaultSearchLimit: number;
28     // Track the current template through route changes.
29     selectedTemplate: string;
30
31     // Display the Exclude Electronic checkbox
32     showExcludeElectronic = false;
33
34     // Advanced search filters to display
35     searchFilters: string[];
36
37     // TODO: does unapi support pref-lib for result-page copy counts?
38     prefOrg: IdlObject;
39
40     // Default search tab
41     defaultTab: string;
42
43     // Patron barcode we hope to place a hold for.
44     holdForBarcode: string;
45     // User object for above barcode.
46     holdForUser: IdlObject;
47
48     // Emit that the value has changed so components can detect
49     // the change even when the component is not itself digesting
50     // new values.
51     holdForChange: EventEmitter<void> = new EventEmitter<void>();
52
53     // Cache the currently selected detail record (i.g. catalog/record/123)
54     // summary so the record detail component can avoid duplicate fetches
55     // during record tab navigation.
56     currentDetailRecordSummary: any;
57
58     // Add digital bookplate to search options.
59     enableBookplates = false;
60
61     // Cache of browse results so the browse pager is not forced to
62     // re-run the browse search on each navigation.
63     browsePagerData: any[];
64
65     // whether to redirect to record page upon a single search
66     // result
67     jumpOnSingleHit = false;
68
69     // discovery layer URL to display an item in "patron view"
70     patronViewUrl = '';
71
72     constructor(
73         private router: Router,
74         private route: ActivatedRoute,
75         private store: StoreService,
76         private org: OrgService,
77         private cat: CatalogService,
78         private patron: PatronService,
79         private catUrl: CatalogUrlService,
80         private broadcaster: BroadcastService,
81         private zone: NgZone
82     ) { }
83
84     createContext(): void {
85         // Initialize the search context from the load-time URL params.
86         // Do this here so the search form and other context data are
87         // applied on every page, not just the search results page.  The
88         // search results pages will handle running the actual search.
89         this.searchContext =
90             this.catUrl.fromUrlParams(this.route.snapshot.queryParamMap);
91
92         this.holdForBarcode = this.store.getLoginSessionItem(HOLD_FOR_PATRON_KEY);
93
94         if (this.holdForBarcode) {
95             this.patron.getByBarcode(this.holdForBarcode)
96                 .then(user => {
97                     this.holdForUser = user;
98                     this.holdForChange.emit();
99                 });
100         } else {
101             // In case the session item was cleared from another component.
102             this.clearHoldPatron();
103         }
104
105         this.searchContext.org = this.org; // service, not searchOrg
106         this.searchContext.isStaff = true;
107         this.applySearchDefaults();
108     }
109
110     clearHoldPatron(broadcast = true) {
111         const removedTarget = this.holdForBarcode;
112
113         this.holdForUser = null;
114         this.holdForBarcode = null;
115         this.store.removeLoginSessionItem(HOLD_FOR_PATRON_KEY);
116         this.holdForChange.emit();
117         if (!broadcast) {return;}
118
119         // clear hold patron on other tabs
120         this.broadcaster.broadcast(
121             HOLD_FOR_PATRON_KEY, { removedTarget }
122         );
123     }
124
125     onBeforeUnload(): void {
126         const closedTarget = this.holdForBarcode;
127         if (closedTarget) {
128             this.clearHoldPatron(false);
129             this.broadcaster.broadcast(HOLD_FOR_PATRON_KEY,
130                 { closedTarget }
131             );
132         }
133     }
134
135     onChangeHoldPatron(): Observable<any> {
136         return this.broadcaster.listen(HOLD_FOR_PATRON_KEY).pipe(
137             tap(({ removedTarget, closedTarget }) => {
138                 if (removedTarget && this.holdForBarcode) {
139                     // broadcaster doesn't trigger change detection,
140                     // so trigger it manually
141                     this.zone.run(() => this.clearHoldPatron(false));
142
143                 } else if (closedTarget) {
144                     // if hold target was unset by another tab,
145                     // restore the hold target
146                     if (closedTarget === this.holdForBarcode) {
147                         this.store.setLoginSessionItem(
148                             HOLD_FOR_PATRON_KEY, closedTarget
149                         );
150                     }
151                 }
152             })
153         );
154     }
155
156     cloneContext(context: CatalogSearchContext): CatalogSearchContext {
157         const params: any = this.catUrl.toUrlParams(context);
158         const ctx = this.catUrl.fromUrlHash(params);
159         ctx.isStaff = true; // not carried in the URL
160         return ctx;
161     }
162
163     applySearchDefaults(): void {
164         if (!this.searchContext.searchOrg) {
165             this.searchContext.searchOrg =
166                 this.defaultSearchOrg || this.org.root();
167         }
168
169         if (!this.searchContext.pager.limit) {
170             this.searchContext.pager.limit = this.defaultSearchLimit || 10;
171         }
172     }
173
174     /**
175      * Redirect to the search results page while propagating the current
176      * search paramters into the URL.  Let the search results component
177      * execute the actual search.
178      */
179     search(): void {
180         if (!this.searchContext.isSearchable()) { return; }
181
182         // Clear cached detail summary for new searches.
183         this.currentDetailRecordSummary = null;
184
185         const params = this.catUrl.toUrlParams(this.searchContext);
186
187         // Force a new search every time this method is called, even if
188         // it's the same as the active search.  Since router navigation
189         // exits early when the route + params is identical, add a
190         // random token to the route params to force a full navigation.
191         // This also resolves a problem where only removing secondary+
192         // versions of a query param fail to cause a route navigation.
193         // (E.g. going from two query= params to one).  Investigation
194         // pending.
195         params.ridx = '' + this.routeIndex++;
196
197         this.router.navigate(
198             ['/staff/catalog/search'], {queryParams: params});
199     }
200
201     /**
202      * Redirect to the browse results page while propagating the current
203      * browse paramters into the URL.  Let the browse results component
204      * execute the actual browse.
205      */
206     browse(): void {
207         if (!this.searchContext.browseSearch.isSearchable()) { return; }
208         const params = this.catUrl.toUrlParams(this.searchContext);
209
210         // Force a new browse every time this method is called, even if
211         // it's the same as the active browse.  Since router navigation
212         // exits early when the route + params is identical, add a
213         // random token to the route params to force a full navigation.
214         // This also resolves a problem where only removing secondary+
215         // versions of a query param fail to cause a route navigation.
216         // (E.g. going from two query= params to one).
217         params.ridx = '' + this.routeIndex++;
218
219         this.router.navigate(
220             ['/staff/catalog/browse'], {queryParams: params});
221     }
222
223     // Call number browse.
224     // Redirect to cn browse page and let its component perform the search
225     cnBrowse(): void {
226         if (!this.searchContext.cnBrowseSearch.isSearchable()) { return; }
227         const params = this.catUrl.toUrlParams(this.searchContext);
228         params.ridx = '' + this.routeIndex++; // see comments above
229         this.router.navigate(['/staff/catalog/cnbrowse'], {queryParams: params});
230     }
231
232     // Params to genreate a new author search based on a reset
233     // clone of the current page params.
234     getAuthorSearchParams(summary: BibRecordSummary): any {
235         const tmpContext = this.cloneContext(this.searchContext);
236         tmpContext.reset();
237         tmpContext.termSearch.fieldClass = ['author'];
238         tmpContext.termSearch.query = [summary.display.author];
239         return this.catUrl.toUrlParams(tmpContext);
240     }
241 }
242
243