d476cbf0a11b80f5cf79b5982e86057eddfef9ae
[evergreen-equinox.git] / Open-ILS / src / eg2 / src / app / staff / circ / patron / patron.component.ts
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';
21
22 const MAIN_TABS =
23     ['checkout', 'items_out', 'holds', 'bills', 'messages', 'edit', 'search'];
24
25 @Component({
26   templateUrl: 'patron.component.html',
27   styleUrls: ['patron.component.css']
28 })
29 export class PatronComponent implements OnInit, AfterViewInit {
30
31     patronId: number;
32     patronTab = 'search';
33     altTab: string;
34     statementXact: number;
35     billingHistoryTab: string;
36     showSummary = true;
37     loading = true;
38     showRecentPatrons = false;
39
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;
44
45     @ViewChild('patronEditor') private patronEditor: EditComponent;
46
47     @ViewChild('pendingChangesDialog')
48         private pendingChangesDialog: ConfirmDialogComponent;
49
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;
55
56     constructor(
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
67     ) {}
68
69     ngOnInit() {
70         this.watchForTabChange();
71         this.load();
72     }
73
74     @HostListener('window:beforeunload', ['$event'])
75     canDeactivate($event?: Event): Promise<boolean> {
76
77         if (this.patronEditor && this.patronEditor.changesPending) {
78
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;
83
84             if ($event) { // window.onbeforeunload
85                 $event.preventDefault();
86                 $event.returnValue = true;
87
88             } else { // tab OR route change.
89                 return this.pendingChangesDialog.open().toPromise();
90             }
91
92         } else {
93             return Promise.resolve(true);
94         }
95     }
96
97     load() {
98         this.loading = true;
99         this.fetchSettings()
100         .then(_ => this.loading = false);
101     }
102
103     fetchSettings(): Promise<any> {
104
105         return this.serverStore.getItemBatch([
106             'eg.circ.patron.summary.collapse'
107         ]).then(prefs => {
108             this.showSummary = !prefs['eg.circ.patron.summary.collapse'];
109         });
110     }
111
112     recentPatronIds(): number[] {
113         if (this.patronTab === 'search' && this.showRecentPatrons) {
114             return this.store.getLoginSessionItem('eg.circ.recent_patrons') || [];
115         } else {
116             return null;
117         }
118     }
119
120     watchForTabChange() {
121
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.
124         this.router.events
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();
130                 }
131             });
132
133         this.route.data.subscribe(data => {
134             this.showRecentPatrons = (data && data.showRecentPatrons);
135         });
136
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');
142
143             if (MAIN_TABS.includes(this.patronTab)) {
144                 this.altTab = null;
145             } else {
146                 this.altTab = this.patronTab;
147                 this.patronTab = 'other';
148             }
149
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;
155             }
156
157             const prevId =
158                 this.context.summary ? this.context.summary.id : null;
159
160             if (this.patronId) {
161
162                 if (this.patronId !== prevId) { // different patron
163                     this.changePatron(this.patronId)
164                     .then(_ => this.routeToAlertsPane());
165
166                 } else {
167                     // Patron already loaded, most likely from the search tab.
168                     // See if we still need to show alerts.
169                     this.routeToAlertsPane();
170                 }
171             } else {
172                 // Use the ID of the previously loaded patron.
173                 this.patronId = prevId;
174             }
175         });
176     }
177
178     ngAfterViewInit() {
179     }
180
181     beforeTabChange(evt: NgbNavChangeEvent) {
182         // tab will change with route navigation.
183         evt.preventDefault();
184
185         // Protect against tab changes with dirty data.
186         this.canDeactivate().then(ok => {
187             if (ok) {
188                 this.patronTab = evt.nextId;
189                 this.routeToTab();
190             }
191         });
192     }
193
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
196     // screen.
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.
200     billsTabClicked() {
201         if (this.patronTab === 'bills') {
202             this.router.navigate(['/staff/circ/patron', this.patronId, 'bills']);
203         }
204     }
205
206     routeToTab() {
207         let url = '/staff/circ/patron/';
208
209         switch (this.patronTab) {
210             case 'search':
211             case 'bcsearch':
212                 url += this.patronTab;
213                 break;
214             case 'other':
215                 url += `${this.patronId}/${this.altTab}`;
216                 break;
217             default:
218                 url += `${this.patronId}/${this.patronTab}`;
219         }
220
221         this.router.navigate([url]);
222     }
223
224     showSummaryPane(): boolean {
225         return this.showSummary || this.patronTab === 'search';
226     }
227
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;
232     }
233
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; }
238
239         const id = ids[0];
240         if (id !== this.patronId) {
241             this.changePatron(id);
242         }
243     }
244
245     changePatron(id: number): Promise<any>  {
246         this.patronId = id;
247         return this.context.loadPatron(id);
248     }
249
250     routeToAlertsPane() {
251         if (this.patronTab !== 'search' &&
252             this.context.summary &&
253             this.context.summary.alerts.hasAlerts() &&
254             !this.context.patronAlertsShown()) {
255
256            this.router.navigate(['/staff/circ/patron', this.patronId, 'alerts']);
257         }
258     }
259
260     // Route to checkout tab for selected patron.
261     patronsActivated(rows: any[]) {
262         if (rows.length !== 1) { return; }
263
264         const id = rows[0].id();
265         this.patronId = id;
266         this.patronTab = 'checkout';
267         this.routeToTab();
268     }
269
270     patronSearchFired(patronSearch: PatronSearch) {
271         this.context.lastPatronSearch = patronSearch;
272     }
273
274     disablePurge(): boolean {
275         return (
276             !this.context.summary ||
277             this.context.summary.patron.super_user() === 't' ||
278             this.patronId === this.auth.user().id()
279         );
280     }
281
282     purgeAccount() {
283
284         this.purgeConfirm1.open().toPromise()
285         .then(confirmed => {
286             if (confirmed) {
287                 return this.purgeConfirm2.open().toPromise();
288             }
289         })
290         .then(confirmed => {
291             if (confirmed) {
292                 return this.net.request(
293                     'open-ils.actor',
294                     'open-ils.actor.user.has_work_perm_at',
295                     this.auth.token(), 'STAFF_LOGIN', this.patronId
296                 ).toPromise();
297             }
298         })
299         .then(permOrgs => {
300             if (permOrgs) {
301                 if (permOrgs.length === 0) { // non-staff
302                     return this.doThePurge();
303                 } else {
304                     return this.handleStaffPurge();
305                 }
306             }
307         });
308     }
309
310     handleStaffPurge(): Promise<any> {
311
312         return this.purgeStaffDialog.open().toPromise()
313         .then(barcode => {
314             if (barcode) {
315                 return this.pcrud.search('ac', {barcode: barcode}).toPromise();
316             }
317         })
318         .then(card => {
319             if (card) {
320                 return this.doThePurge(card.usr());
321             } else {
322                 return this.purgeBadBarcode.open();
323             }
324         });
325     }
326
327     doThePurge(destUserId?: number, override?: boolean): Promise<any> {
328         let method = 'open-ils.actor.user.delete';
329         if (override) { method += '.override'; }
330
331         return this.net.request('open-ils.actor', method,
332             this.auth.token(), this.patronId, destUserId).toPromise()
333         .then(resp => {
334
335             const evt = this.evt.parse(resp);
336             if (evt) {
337                 if (evt.textcode === 'ACTOR_USER_DELETE_OPEN_XACTS') {
338                     return this.purgeConfirmOverride.open().toPromise()
339                     .then(confirmed => {
340                         if (confirmed) {
341                             return this.doThePurge(destUserId, true);
342                         }
343                     });
344                 } else {
345                     alert(evt);
346                 }
347             } else {
348                 this.context.summary = null;
349                 this.router.navigate(['/staff/circ/patron/search']);
350             }
351         });
352     }
353
354     counts(part: string, field: string): number {
355         if (this.context.summary && this.context.summary.stats) {
356             return this.context.summary.stats[part][field];
357         } else {
358             return 0;
359         }
360     }
361 }
362