1 import {Component, ViewChild, OnInit, AfterViewInit, HostListener} from '@angular/core';
2 import {Router, ActivatedRoute, ParamMap, RoutesRecognized} from '@angular/router';
3 import {NgbNav, NgbNavChangeEvent} from '@ng-bootstrap/ng-bootstrap';
4 import {Observable, throwError, empty} from 'rxjs';
5 import {filter, pairwise, concatMap, tap} from 'rxjs/operators';
6 import {NetService} from '@eg/core/net.service';
7 import {AuthService} from '@eg/core/auth.service';
8 import {PcrudService} from '@eg/core/pcrud.service';
9 import {EventService} from '@eg/core/event.service';
10 import {StoreService} from '@eg/core/store.service';
11 import {ServerStoreService} from '@eg/core/server-store.service';
12 import {PatronService} from '@eg/staff/share/patron/patron.service';
13 import {PatronContextService, BillGridEntry} from './patron.service';
14 import {PatronSearch, PatronSearchComponent
15 } from '@eg/staff/share/patron/search.component';
16 import {EditToolbarComponent} from './edit-toolbar.component';
17 import {EditComponent} from './edit.component';
18 import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
19 import {PromptDialogComponent} from '@eg/share/dialog/prompt.component';
20 import {AlertDialogComponent} from '@eg/share/dialog/alert.component';
23 ['checkout', 'items_out', 'holds', 'bills', 'messages', 'edit', 'search'];
26 templateUrl: 'patron.component.html',
27 styleUrls: ['patron.component.css']
29 export class PatronComponent implements OnInit, AfterViewInit {
34 statementXact: number;
35 billingHistoryTab: string;
38 showRecentPatrons = false;
40 /* eg-patron-edit is unable to find #editorToolbar directly
41 * within the template. Adding a ref here allows it to
42 * successfully transfer to the editor */
43 @ViewChild('editorToolbar') private editorToolbar: EditToolbarComponent;
45 @ViewChild('patronEditor') private patronEditor: EditComponent;
47 @ViewChild('pendingChangesDialog')
48 private pendingChangesDialog: ConfirmDialogComponent;
50 @ViewChild('purgeConfirm1') private purgeConfirm1: ConfirmDialogComponent;
51 @ViewChild('purgeConfirm2') private purgeConfirm2: ConfirmDialogComponent;
52 @ViewChild('purgeConfirmOverride') private purgeConfirmOverride: ConfirmDialogComponent;
53 @ViewChild('purgeStaffDialog') private purgeStaffDialog: PromptDialogComponent;
54 @ViewChild('purgeBadBarcode') private purgeBadBarcode: AlertDialogComponent;
57 private router: Router,
58 private route: ActivatedRoute,
59 private net: NetService,
60 private auth: AuthService,
61 private pcrud: PcrudService,
62 private evt: EventService,
63 private store: StoreService,
64 private serverStore: ServerStoreService,
65 public patronService: PatronService,
66 public context: PatronContextService
70 this.watchForTabChange();
74 @HostListener('window:beforeunload', ['$event'])
75 canDeactivate($event?: Event): Promise<boolean> {
77 if (this.patronEditor && this.patronEditor.changesPending) {
79 // Each warning dialog clears the current "changes are pending"
80 // flag so the user is not presented with the dialog again
81 // unless new changes are made.
82 this.patronEditor.changesPending = false;
84 if ($event) { // window.onbeforeunload
85 $event.preventDefault();
86 $event.returnValue = true;
88 } else { // tab OR route change.
89 return this.pendingChangesDialog.open().toPromise();
93 return Promise.resolve(true);
100 .then(_ => this.loading = false);
103 fetchSettings(): Promise<any> {
105 return this.serverStore.getItemBatch([
106 'eg.circ.patron.summary.collapse'
108 this.showSummary = !prefs['eg.circ.patron.summary.collapse'];
112 recentPatronIds(): number[] {
113 if (this.patronTab === 'search' && this.showRecentPatrons) {
114 return this.store.getLoginSessionItem('eg.circ.recent_patrons') || [];
120 watchForTabChange() {
122 // When routing to a patron UI from a non-patron UI, clear all
123 // patron info that may have persisted via the context service.
125 .pipe(filter((evt: any) => evt instanceof RoutesRecognized), pairwise())
126 .subscribe((events: RoutesRecognized[]) => {
127 const prevUrl = events[0].urlAfterRedirects || '';
128 if (!prevUrl.startsWith('/staff/circ/patron')) {
129 this.context.reset();
133 this.route.data.subscribe(data => {
134 this.showRecentPatrons = (data && data.showRecentPatrons);
137 this.route.paramMap.subscribe((params: ParamMap) => {
138 this.patronTab = params.get('tab') || 'search';
139 this.patronId = +params.get('id');
140 this.statementXact = +params.get('xactId');
141 this.billingHistoryTab = params.get('billingHistoryTab');
143 if (MAIN_TABS.includes(this.patronTab)) {
146 this.altTab = this.patronTab;
147 this.patronTab = 'other';
150 // Clear all previous patron data when returning to the
151 // search from after other patron-level navigation.
152 if (this.patronTab === 'search') {
153 this.context.summary = null;
154 this.patronId = null;
158 this.context.summary ? this.context.summary.id : null;
162 if (this.patronId !== prevId) { // different patron
163 this.changePatron(this.patronId)
164 .then(_ => this.routeToAlertsPane());
167 // Patron already loaded, most likely from the search tab.
168 // See if we still need to show alerts.
169 this.routeToAlertsPane();
172 // Use the ID of the previously loaded patron.
173 this.patronId = prevId;
181 beforeTabChange(evt: NgbNavChangeEvent) {
182 // tab will change with route navigation.
183 evt.preventDefault();
185 // Protect against tab changes with dirty data.
186 this.canDeactivate().then(ok => {
188 this.patronTab = evt.nextId;
194 // The bills tab has various sub-interfaces. If the user is already
195 // on the Bills tab and clicks the tab, return them to the main bills
197 // Avoid the navigate call when not on the bills tab because it
198 // interferes with the pre-tab-change "changes pending" confirm dialog
199 // used by the editor and possibily others.
201 if (this.patronTab === 'bills') {
202 this.router.navigate(['/staff/circ/patron', this.patronId, 'bills']);
207 let url = '/staff/circ/patron/';
209 switch (this.patronTab) {
212 url += this.patronTab;
215 url += `${this.patronId}/${this.altTab}`;
218 url += `${this.patronId}/${this.patronTab}`;
221 this.router.navigate([url]);
224 showSummaryPane(): boolean {
225 return this.showSummary || this.patronTab === 'search';
228 toggleSummaryPane() {
229 this.serverStore.setItem( // collapse is the opposite of show
230 'eg.circ.patron.summary.collapse', this.showSummary);
231 this.showSummary = !this.showSummary;
234 // Patron row single-clicked in the grid. Load the patron without
235 // leaving the search tab.
236 patronSelectionChange(ids: number[]) {
237 if (ids.length !== 1) { return; }
240 if (id !== this.patronId) {
241 this.changePatron(id);
245 changePatron(id: number): Promise<any> {
247 return this.context.loadPatron(id);
250 routeToAlertsPane() {
251 if (this.patronTab !== 'search' &&
252 this.context.summary &&
253 this.context.summary.alerts.hasAlerts() &&
254 !this.context.patronAlertsShown()) {
256 this.router.navigate(['/staff/circ/patron', this.patronId, 'alerts']);
260 // Route to checkout tab for selected patron.
261 patronsActivated(rows: any[]) {
262 if (rows.length !== 1) { return; }
264 const id = rows[0].id();
266 this.patronTab = 'checkout';
270 patronSearchFired(patronSearch: PatronSearch) {
271 this.context.lastPatronSearch = patronSearch;
274 disablePurge(): boolean {
276 !this.context.summary ||
277 this.context.summary.patron.super_user() === 't' ||
278 this.patronId === this.auth.user().id()
284 this.purgeConfirm1.open().toPromise()
287 return this.purgeConfirm2.open().toPromise();
292 return this.net.request(
294 'open-ils.actor.user.has_work_perm_at',
295 this.auth.token(), 'STAFF_LOGIN', this.patronId
301 if (permOrgs.length === 0) { // non-staff
302 return this.doThePurge();
304 return this.handleStaffPurge();
310 handleStaffPurge(): Promise<any> {
312 return this.purgeStaffDialog.open().toPromise()
315 return this.pcrud.search('ac', {barcode: barcode}).toPromise();
320 return this.doThePurge(card.usr());
322 return this.purgeBadBarcode.open();
327 doThePurge(destUserId?: number, override?: boolean): Promise<any> {
328 let method = 'open-ils.actor.user.delete';
329 if (override) { method += '.override'; }
331 return this.net.request('open-ils.actor', method,
332 this.auth.token(), this.patronId, destUserId).toPromise()
335 const evt = this.evt.parse(resp);
337 if (evt.textcode === 'ACTOR_USER_DELETE_OPEN_XACTS') {
338 return this.purgeConfirmOverride.open().toPromise()
341 return this.doThePurge(destUserId, true);
348 this.context.summary = null;
349 this.router.navigate(['/staff/circ/patron/search']);
354 counts(part: string, field: string): number {
355 if (this.context.summary && this.context.summary.stats) {
356 return this.context.summary.stats[part][field];