LP1904036 Patron tabs can be opened in new browser tabs/windows
[evergreen-equinox.git] / Open-ILS / src / eg2 / src / app / staff / circ / patron / patron.service.ts
1 import {Injectable, EventEmitter} from '@angular/core';
2 import {IdlObject} from '@eg/core/idl.service';
3 import {NetService} from '@eg/core/net.service';
4 import {OrgService} from '@eg/core/org.service';
5 import {AuthService} from '@eg/core/auth.service';
6 import {PatronService, PatronSummary, PatronStats, PatronAlerts
7     } from '@eg/staff/share/patron/patron.service';
8 import {PatronSearch} from '@eg/staff/share/patron/search.component';
9 import {StoreService} from '@eg/core/store.service';
10 import {ServerStoreService} from '@eg/core/server-store.service';
11 import {CircService, CircDisplayInfo} from '@eg/staff/share/circ/circ.service';
12
13 export interface BillGridEntry extends CircDisplayInfo {
14     xact: IdlObject; // mbt
15     billingLocation?: string;
16     paymentPending?: number;
17 }
18
19 export interface CircGridEntry {
20     index: number;
21     title?: string;
22     author?: string;
23     isbn?: string;
24     copy?: IdlObject;
25     circ?: IdlObject;
26     volume?: IdlObject;
27     record?: IdlObject;
28     dueDate?: string;
29     copyAlertCount: number;
30     nonCatCount: number;
31     patron: IdlObject;
32 }
33
34 const PATRON_FLESH_FIELDS = [
35     'card',
36     'cards',
37     'settings',
38     'standing_penalties',
39     'addresses',
40     'billing_address',
41     'mailing_address',
42     'waiver_entries',
43     'usr_activity',
44     'notes',
45     'profile',
46     'net_access_level',
47     'ident_type',
48     'ident_type2',
49     'groups'
50 ];
51
52 @Injectable()
53 export class PatronContextService {
54
55     summary: PatronSummary;
56     loaded = false;
57     lastPatronSearch: PatronSearch;
58     searchBarcode: string = null;
59
60     // These should persist tab changes
61     checkouts: CircGridEntry[] = [];
62
63     maxRecentPatrons = 1;
64
65     settingsCache: {[key: string]: any} = {};
66
67     constructor(
68         private store: StoreService,
69         private serverStore: ServerStoreService,
70         private org: OrgService,
71         private circ: CircService,
72         public patrons: PatronService
73     ) {}
74
75     reset() {
76         this.summary = null;
77         this.loaded = false;
78         this.lastPatronSearch = null;
79         this.searchBarcode = null;
80         this.checkouts = [];
81     }
82
83     loadPatron(id: number): Promise<any> {
84         this.loaded = false;
85         this.checkouts = [];
86         return this.refreshPatron(id).then(_ => this.loaded = true);
87     }
88
89     // Update the patron data without resetting all of the context data.
90     refreshPatron(id?: number): Promise<any> {
91
92         if (!id) {
93             if (!this.summary) {
94                 return Promise.resolve();
95             } else {
96                 id = this.summary.id;
97             }
98         }
99
100         return this.patrons.getFleshedById(id, PATRON_FLESH_FIELDS)
101         .then(p => this.summary = new PatronSummary(p))
102         .then(_ => this.getPatronStats(id))
103         .then(_ => this.compileAlerts())
104         .then(_ => this.addRecentPatron());
105     }
106
107     addRecentPatron(patronId?: number): Promise<any> {
108
109         if (!patronId) { patronId = this.summary.id; }
110
111         return this.serverStore.getItem('ui.staff.max_recent_patrons')
112         .then(num => {
113             if (num) { this.maxRecentPatrons = num; }
114
115             const patrons: number[] =
116                 this.store.getLoginSessionItem('eg.circ.recent_patrons') || [];
117
118             patrons.splice(0, 0, patronId);  // put this user at front
119             patrons.splice(this.maxRecentPatrons, 1); // remove excess
120
121             // remove any other occurrences of this user, which may have been
122             // added before the most recent user.
123             const idx = patrons.indexOf(patronId, 1);
124             if (idx > 0) { patrons.splice(idx, 1); }
125
126             this.store.setLoginSessionItem('eg.circ.recent_patrons', patrons);
127         });
128     }
129
130     getPatronStats(id: number): Promise<any> {
131
132         // When quickly navigating patron search results it's possible
133         // for this.patron to be cleared right before this function
134         // is called.  Exit early instead of making an unneeded call.
135         if (!this.summary) { return Promise.resolve(); }
136
137         return this.patrons.getVitalStats(this.summary.patron)
138         .then(stats => this.summary.stats = stats);
139     }
140
141     patronAlertsShown(): boolean {
142         if (!this.summary) { return false; }
143         this.store.addLoginSessionKey('eg.circ.last_alerted_patron');
144         const shown = this.store.getLoginSessionItem('eg.circ.last_alerted_patron');
145         if (shown === this.summary.patron.id()) { return true; }
146         this.store.setLoginSessionItem('eg.circ.last_alerted_patron', this.summary.patron.id());
147         return false;
148     }
149
150     compileAlerts(): Promise<any> {
151
152         // User navigated to a different patron mid-data load.
153         if (!this.summary) { return Promise.resolve(); }
154
155         return this.patrons.compileAlerts(this.summary)
156         .then(alerts => {
157             this.summary.alerts = alerts;
158
159             if (this.searchBarcode) {
160                 const card = this.summary.patron.cards()
161                     .filter(c => c.barcode() === this.searchBarcode)[0];
162                 this.summary.alerts.retrievedWithInactive =
163                     card && card.active() === 'f';
164                 this.searchBarcode = null;
165             }
166         });
167     }
168
169     orgSn(orgId: number): string {
170         const org = this.org.get(orgId);
171         return org ? org.shortname() : '';
172     }
173
174     formatXactForDisplay(xact: IdlObject): BillGridEntry {
175
176         const entry: BillGridEntry = {
177             xact: xact,
178             paymentPending: 0
179         };
180
181         if (xact.summary().xact_type() !== 'circulation') {
182
183             entry.xact.grocery().billing_location(
184                 this.org.get(entry.xact.grocery().billing_location()));
185
186             entry.title = xact.summary().last_billing_type();
187             entry.billingLocation =
188                 xact.grocery().billing_location().shortname();
189             return entry;
190         }
191
192         entry.xact.circulation().circ_lib(
193             this.org.get(entry.xact.circulation().circ_lib()));
194
195         const circDisplay: CircDisplayInfo =
196             this.circ.getDisplayInfo(xact.circulation());
197
198         entry.billingLocation =
199             xact.circulation().circ_lib().shortname();
200
201         return Object.assign(entry, circDisplay);
202     }
203 }
204
205