]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/eg2/src/app/staff/admin/server/print-template.component.ts
LP1915464 follow-up: use spaces, not tabs; remove extra comma
[working/Evergreen.git] / Open-ILS / src / eg2 / src / app / staff / admin / server / print-template.component.ts
1 import {Component, OnInit, ViewChild, TemplateRef} from '@angular/core';
2 import {Observable} from 'rxjs';
3 import {map} from 'rxjs/operators';
4 import {ActivatedRoute} from '@angular/router';
5 import {IdlService, IdlObject} from '@eg/core/idl.service';
6 import {PcrudService} from '@eg/core/pcrud.service';
7 import {AuthService} from '@eg/core/auth.service';
8 import {OrgService} from '@eg/core/org.service';
9 import {ServerStoreService} from '@eg/core/server-store.service';
10 import {ComboboxComponent, ComboboxEntry
11 } from '@eg/share/combobox/combobox.component';
12 import {PrintService} from '@eg/share/print/print.service';
13 import {LocaleService} from '@eg/core/locale.service';
14 import {NgbNav, NgbNavChangeEvent} from '@ng-bootstrap/ng-bootstrap';
15 import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component';
16 import {SampleDataService} from '@eg/share/util/sample-data.service';
17 import {OrgFamily} from '@eg/share/org-family-select/org-family-select.component';
18 import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
19
20 /**
21  * Print Template Admin Page
22  */
23
24 @Component({
25     templateUrl: 'print-template.component.html'
26 })
27
28 export class PrintTemplateComponent implements OnInit {
29
30     entries: ComboboxEntry[];
31     template: IdlObject;
32     sampleJson: string;
33     invalidJson = false;
34     localeCode: string;
35     localeEntries: ComboboxEntry[];
36     compiledContent: string;
37     templateCache: {[id: number]: IdlObject} = {};
38     initialOrg: number;
39     selectedOrgs: number[];
40     selectedTab = 'template';
41
42     @ViewChild('templateSelector', { static: true }) templateSelector: ComboboxComponent;
43     @ViewChild('editDialog', { static: true }) editDialog: FmRecordEditorComponent;
44     @ViewChild('confirmDelete', { static: true }) confirmDelete: ConfirmDialogComponent;
45     @ViewChild('printContextCbox', {static: false}) printContextCbox: ComboboxComponent;
46
47     // Define some sample data that can be used for various templates
48     // Data will be filled out via the sample data service.
49     // Keys map to print template names
50     sampleData: any = {
51         patron_address: {},
52         holds_for_bib: {},
53         bills_current: {},
54         bills_payment: {},
55         hold_shelf_slip: {}
56     };
57
58     constructor(
59         private route: ActivatedRoute,
60         private idl: IdlService,
61         private org: OrgService,
62         private pcrud: PcrudService,
63         private auth: AuthService,
64         private store: ServerStoreService,
65         private locale: LocaleService,
66         private printer: PrintService,
67         private samples: SampleDataService
68     ) {
69         this.entries = [];
70         this.localeEntries = [];
71     }
72
73     ngOnInit() {
74         this.initialOrg = this.auth.user().ws_ou();
75         this.selectedOrgs = [this.initialOrg];
76         this.localeCode = this.locale.currentLocaleCode();
77         this.locale.supportedLocales().subscribe(
78             l => this.localeEntries.push({id: l.code(), label: l.name()}));
79         this.setTemplateInfo().subscribe();
80         this.fleshSampleData();
81     }
82
83     fleshSampleData() {
84
85         // NOTE: server templates work fine with IDL objects, but
86         // vanilla hashes are easier to work with in the admin UI.
87
88         // Classes for which sample data exists
89         const classes = ['au', 'ac', 'aua', 'ahr', 'acp', 'mwde', 'mbt', 'mbts'];
90         const samples: any = {};
91         classes.forEach(class_ => samples[class_] =
92             this.idl.toHash(this.samples.listOfThings(class_, 10)));
93
94         // Wide holds are hashes instead of IDL objects.
95         // Add fields as needed.
96         const wide_holds = [{
97             request_time: this.samples.randomDateIso(),
98             ucard_barcode: samples.ac[0].barcode,
99             usr_family_name: samples.au[0].family_name,
100             usr_alias: samples.au[0].alias,
101             cp_barcode: samples.acp[0].barcode
102         }, {
103             request_time: this.samples.randomDateIso(),
104             ucard_barcode: samples.ac[1].barcode,
105             usr_family_name: samples.au[1].family_name,
106             usr_alias: samples.au[1].alias,
107             cp_barcode: samples.acp[1].barcode
108         }];
109
110         this.sampleData.patron_address = {
111             patron:  samples.au[0],
112             address: samples.aua[0]
113         };
114
115         const patron = this.idl.clone(samples.au[0]);
116         patron.addresses = [samples.aua[0]];
117         patron.stat_cat_entries = [{
118             stat_cat: {name: 'A Stat Cat'},
119             stat_cat_entry: 'A Value'
120         }];
121
122         this.sampleData.patron_data = {patron: patron};
123
124         this.sampleData.holds_for_bib = wide_holds;
125
126         // Bills
127         samples.mbt[0].summary = samples.mbts[0];
128         samples.mbt[1].summary = samples.mbts[1];
129         samples.mbt[2].summary = samples.mbts[2];
130
131         this.sampleData.bills_current.xacts = [
132             samples.mbt[0],
133             samples.mbt[1],
134             samples.mbt[2]
135         ];
136
137         // Payments
138         this.sampleData.bills_payment = {
139             previous_balance: 10,
140             payment_type: 'cash_payment',
141             payment_total: 5,
142             payment_applied: 3,
143             amount_voided: 0,
144             change_given: 2,
145             payment_note: 'Test Note',
146             payments: [{
147                 amount: 1,
148                 xact: samples.mbt[0],
149                 title: 'A Title',
150                 copy_barcode: '3423482302393'
151             }, {
152                 amount: 4,
153                 xact: samples.mbt[1],
154                 title: 'Another Title',
155                 copy_barcode: '3423482302394'
156             }]
157         };
158
159         this.sampleData.hold_shelf_slip = {
160             checkin: {
161                 copy: samples.acp[0],
162                 patron: samples.au[0],
163                 hold: samples.ahr[0]
164             }
165         };
166
167         this.sampleData.hold_transit_slip =
168             Object.assign({}, this.sampleData.hold_shelf_slip);
169         this.sampleData.hold_transit_slip.checkin.destOrg =
170             this.org.list()[0];
171     }
172
173     onNavChange(evt: NgbNavChangeEvent) {
174         if (evt.nextId === 'template') {
175             this.refreshPreview();
176         }
177     }
178
179     container(): any {
180         // Only present when its tab is visible
181         return document.getElementById('template-preview-pane');
182     }
183
184     // TODO should the ngModelChange handler fire for org-family-select
185     // even when the values don't change?
186     orgOnChange(family: OrgFamily) {
187         // Avoid reundant server calls.
188         if (!this.sameIds(this.selectedOrgs, family.orgIds)) {
189             this.selectedOrgs = family.orgIds;
190             this.setTemplateInfo().subscribe();
191         }
192     }
193
194     // True if the 2 arrays contain the same contents,
195     // regardless of the order.
196     sameIds(arr1: any[], arr2: any[]): boolean {
197         if (arr1.length !== arr2.length) {
198             return false;
199         }
200         for (let i = 0; i < arr1.length; i++) {
201             if (!arr2.includes(arr1[i])) {
202                 return false;
203             }
204         }
205         return true;
206     }
207
208     localeOnChange(code: string) {
209         if (code) {
210             this.localeCode = code;
211             this.setTemplateInfo().subscribe();
212         }
213     }
214
215     // Fetch name/id for all templates in range.
216     // Avoid fetching the template content until needed.
217     setTemplateInfo(): Observable<IdlObject> {
218         this.entries = [];
219         this.template = null;
220         this.templateSelector.applyEntryId(null);
221         this.compiledContent = '';
222
223         return this.pcrud.search('cpt',
224             {
225                 owner: this.selectedOrgs,
226                 locale: this.localeCode
227             }, {
228                 select: {cpt: ['id', 'label', 'owner']},
229                 order_by: {cpt: 'label'}
230             }
231         ).pipe(map(tmpl => {
232             this.templateCache[tmpl.id()] = tmpl;
233             this.entries.push({id: tmpl.id(), label: tmpl.label()});
234             return tmpl;
235         }));
236     }
237
238     getOwnerName(id: number): string {
239         if (this.templateCache[id]) {
240             return this.org.get(this.templateCache[id].owner()).shortname();
241         }
242         return '';
243     }
244
245     // If the selected template changes through means other than the
246     // template selector, setting updateSelector=true will force the
247     // template to appear in the selector and get selected, regardless
248     // of whether it would have been fetched with current filters.
249     selectTemplate(id: number, updateSelector?: boolean) {
250
251         if (id === null) {
252             this.template = null;
253             this.compiledContent = '';
254             return;
255         }
256
257         // reset things
258         this.selectedTab = 'template';
259         this.compiledContent = '';
260         if (this.container()) {
261             this.container().innerHTML = '';
262         }
263         this.sampleJson = '';
264
265         this.pcrud.retrieve('cpt', id).subscribe(t => {
266             this.template = this.templateCache[id] = t;
267
268             if (updateSelector) {
269                 if (!this.templateSelector.hasEntry(id)) {
270                     this.templateSelector.addEntry({id: id, label: t.label()});
271                 }
272                 this.templateSelector.applyEntryId(id);
273             }
274
275             const data = this.sampleData[t.name()];
276             if (data) {
277                 this.sampleJson = JSON.stringify(data, null, 2);
278                 this.refreshPreview();
279             }
280
281             this.store.getItem('eg.print.template_context.' + this.template.name())
282                 .then(setting => {
283                     this.printContextCbox.applyEntryId(setting || 'unset');
284                 });
285         });
286     }
287
288     // Allow the template editor textarea to expand vertically as
289     // content is added, with a sane minimum row count
290     templateRowCount(): number {
291         const def = 25;
292         if (this.template && this.template.template()) {
293             return Math.max(def,
294                 this.template.template().split(/\n/).length + 2);
295         }
296         return def;
297     }
298
299     refreshPreview() {
300         if (!this.sampleJson) { return; }
301         this.compiledContent = '';
302
303         let data;
304         try {
305             data = JSON.parse(this.sampleJson);
306             this.invalidJson = false;
307         } catch (E) {
308             this.invalidJson = true;
309         }
310
311         this.printer.compileRemoteTemplate({
312             templateId: this.template.id(),
313             contextData: data,
314             printContext: 'default' // required, has no impact here
315
316         }).then(response => {
317
318             this.compiledContent = response.content;
319             if (this.container()) { // null if on alternate tab
320                 if (response.contentType === 'text/html') {
321                     this.container().innerHTML = response.content;
322                 } else {
323                     // Assumes text/plain or similar
324                     this.container().innerHTML = '<pre>' + response.content + '</pre>';
325                 }
326             }
327         });
328     }
329
330     applyChanges() {
331         this.container().innerHTML = '';
332         this.pcrud.update(this.template).toPromise()
333             .then(() => this.refreshPreview());
334     }
335
336     openEditDialog() {
337         this.editDialog.setRecord(this.template);
338         this.editDialog.mode = 'update';
339         this.editDialog.open({size: 'lg'}).toPromise().then(id => {
340             if (id !== undefined) {
341                 const selectedId = this.template.id();
342                 this.setTemplateInfo().toPromise().then(
343                     _ => this.selectTemplate(selectedId)
344                 );
345             }
346         });
347     }
348
349     cloneTemplate() {
350         const tmpl = this.idl.clone(this.template);
351         tmpl.id(null);
352         tmpl.active(false); // Cloning requires manual activation
353         tmpl.owner(null);
354         this.editDialog.setRecord(tmpl);
355         this.editDialog.mode = 'create';
356         this.editDialog.open({size: 'lg'}).toPromise().then(newTmpl => {
357             if (newTmpl !== undefined) {
358                 this.setTemplateInfo().toPromise()
359                     .then(_ => this.selectTemplate(newTmpl.id(), true));
360             }
361         });
362     }
363
364     deleteTemplate() {
365         this.confirmDelete.open().subscribe(confirmed => {
366             if (!confirmed) { return; }
367             this.pcrud.remove(this.template).toPromise().then(_ => {
368                 this.setTemplateInfo().toPromise()
369                     .then(x => this.selectTemplate(null));
370             });
371         });
372     }
373
374     forceContextChange(entry: ComboboxEntry) {
375         if (entry && entry.id !== 'unset') {
376
377             this.store.setItem(
378                 'eg.print.template_context.' + this.template.name(), entry.id);
379
380         } else {
381
382             this.store.removeItem(
383                 'eg.print.template_context.' + this.template.name());
384         }
385     }
386 }
387
388