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