f57df1ec91f2da596fe6fdfe767801b9b12e365c
[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 {ComboboxComponent, ComboboxEntry
10     } from '@eg/share/combobox/combobox.component';
11 import {PrintService} from '@eg/share/print/print.service';
12 import {LocaleService} from '@eg/core/locale.service';
13 import {NgbTabset, NgbTabChangeEvent} from '@ng-bootstrap/ng-bootstrap';
14 import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component';
15 import {SampleDataService} from '@eg/share/util/sample-data.service';
16 import {OrgFamily} from '@eg/share/org-family-select/org-family-select.component';
17 import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
18
19 /**
20  * Print Template Admin Page
21  */
22
23 @Component({
24     templateUrl: 'print-template.component.html'
25 })
26
27 export class PrintTemplateComponent implements OnInit {
28
29     entries: ComboboxEntry[];
30     template: IdlObject;
31     sampleJson: string;
32     invalidJson = false;
33     localeCode: string;
34     localeEntries: ComboboxEntry[];
35     compiledContent: string;
36     templateCache: {[id: number]: IdlObject} = {};
37     initialOrg: number;
38     selectedOrgs: number[];
39
40     @ViewChild('templateSelector') templateSelector: ComboboxComponent;
41     @ViewChild('tabs') tabs: NgbTabset;
42     @ViewChild('editDialog') editDialog: FmRecordEditorComponent;
43     @ViewChild('confirmDelete') confirmDelete: ConfirmDialogComponent;
44
45     // Define some sample data that can be used for various templates
46     // Data will be filled out via the sample data service.
47     // Keys map to print template names
48     sampleData: any = {
49         patron_address: {},
50         holds_for_bib: {}
51     };
52
53     constructor(
54         private route: ActivatedRoute,
55         private idl: IdlService,
56         private org: OrgService,
57         private pcrud: PcrudService,
58         private auth: AuthService,
59         private locale: LocaleService,
60         private printer: PrintService,
61         private samples: SampleDataService
62     ) {
63         this.entries = [];
64         this.localeEntries = [];
65     }
66
67     ngOnInit() {
68         this.initialOrg = this.auth.user().ws_ou();
69         this.selectedOrgs = [this.initialOrg];
70         this.localeCode = this.locale.currentLocaleCode();
71         this.locale.supportedLocales().subscribe(
72             l => this.localeEntries.push({id: l.code(), label: l.name()}));
73         this.setTemplateInfo().subscribe();
74         this.fleshSampleData();
75     }
76
77     fleshSampleData() {
78
79         // NOTE: server templates work fine with IDL objects, but
80         // vanilla hashes are easier to work with in the admin UI.
81
82         // Classes for which sample data exists
83         const classes = ['au', 'ac', 'aua', 'ahr', 'acp', 'mwde'];
84         const samples: any = {};
85         classes.forEach(class_ => samples[class_] =
86             this.idl.toHash(this.samples.listOfThings(class_, 10)));
87
88         // Wide holds are hashes instead of IDL objects.
89         // Add fields as needed.
90         const wide_holds = [{
91             request_time: this.samples.randomDate().toISOString(),
92             ucard_barcode: samples.ac[0].barcode,
93             usr_family_name: samples.au[0].family_name,
94             usr_alias: samples.au[0].alias,
95             cp_barcode: samples.acp[0].barcode
96         }, {
97             request_time: this.samples.randomDate().toISOString(),
98             ucard_barcode: samples.ac[1].barcode,
99             usr_family_name: samples.au[1].family_name,
100             usr_alias: samples.au[1].alias,
101             cp_barcode: samples.acp[1].barcode
102         }];
103
104         this.sampleData.patron_address = {
105             patron:  samples.au[0],
106             address: samples.aua[0]
107         };
108
109         this.sampleData.holds_for_bib = wide_holds;
110     }
111
112     onTabChange(evt: NgbTabChangeEvent) {
113         if (evt.nextId === 'template') {
114             this.refreshPreview();
115         }
116     }
117
118     container(): any {
119         // Only present when its tab is visible
120         return document.getElementById('template-preview-pane');
121     }
122
123     // TODO should the ngModelChange handler fire for org-family-select
124     // even when the values don't change?
125     orgOnChange(family: OrgFamily) {
126         // Avoid reundant server calls.
127         if (!this.sameIds(this.selectedOrgs, family.orgIds)) {
128             this.selectedOrgs = family.orgIds;
129             this.setTemplateInfo().subscribe();
130         }
131     }
132
133     // True if the 2 arrays contain the same contents,
134     // regardless of the order.
135     sameIds(arr1: any[], arr2: any[]): boolean {
136         if (arr1.length !== arr2.length) {
137             return false;
138         }
139         for (let i = 0; i < arr1.length; i++) {
140             if (!arr2.includes(arr1[i])) {
141                 return false;
142             }
143         }
144         return true;
145     }
146
147     localeOnChange(code: string) {
148         if (code) {
149             this.localeCode = code;
150             this.setTemplateInfo().subscribe();
151         }
152     }
153
154     // Fetch name/id for all templates in range.
155     // Avoid fetching the template content until needed.
156     setTemplateInfo(): Observable<IdlObject> {
157         this.entries = [];
158         this.template = null;
159         this.templateSelector.applyEntryId(null);
160         this.compiledContent = '';
161
162         return this.pcrud.search('cpt',
163             {
164                 owner: this.selectedOrgs,
165                 locale: this.localeCode
166             }, {
167                 select: {cpt: ['id', 'label', 'owner']},
168                 order_by: {cpt: 'label'}
169             }
170         ).pipe(map(tmpl => {
171             this.templateCache[tmpl.id()] = tmpl;
172             this.entries.push({id: tmpl.id(), label: tmpl.label()});
173             return tmpl;
174         }));
175     }
176
177     getOwnerName(id: number): string {
178         return this.org.get(this.templateCache[id].owner()).shortname();
179     }
180
181     selectTemplate(id: number) {
182
183         if (id === null) {
184             this.template = null;
185             this.compiledContent = '';
186             return;
187         }
188
189         this.pcrud.retrieve('cpt', id).subscribe(t => {
190             this.template = t;
191             const data = this.sampleData[t.name()];
192             if (data) {
193                 this.sampleJson = JSON.stringify(data, null, 2);
194                 this.refreshPreview();
195             }
196         });
197     }
198
199     refreshPreview() {
200         if (!this.sampleJson) { return; }
201         this.compiledContent = '';
202
203         let data;
204         try {
205             data = JSON.parse(this.sampleJson);
206             this.invalidJson = false;
207         } catch (E) {
208             this.invalidJson = true;
209         }
210
211         this.printer.compileRemoteTemplate({
212             templateId: this.template.id(),
213             contextData: data,
214             printContext: 'default' // required, has no impact here
215
216         }).then(response => {
217
218             this.compiledContent = response.content;
219             if (response.contentType === 'text/html') {
220                 this.container().innerHTML = response.content;
221             } else {
222                 // Assumes text/plain or similar
223                 this.container().innerHTML = '<pre>' + response.content + '</pre>';
224             }
225         });
226     }
227
228     applyChanges() {
229         this.container().innerHTML = '';
230         this.pcrud.update(this.template).toPromise()
231             .then(() => this.refreshPreview());
232     }
233
234     openEditDialog() {
235         this.editDialog.setRecord(this.template);
236         this.editDialog.mode = 'update';
237         this.editDialog.open({size: 'lg'}).toPromise().then(id => {
238             if (id !== undefined) {
239                 const selectedId = this.template.id();
240                 this.setTemplateInfo().toPromise().then(
241                     _ => this.selectTemplate(selectedId)
242                 );
243             }
244         });
245     }
246
247     cloneTemplate() {
248         const tmpl = this.idl.clone(this.template);
249         tmpl.id(null);
250         this.editDialog.setRecord(tmpl);
251         this.editDialog.mode = 'create';
252         this.editDialog.open({size: 'lg'}).toPromise().then(newTmpl => {
253             if (newTmpl !== undefined) {
254                 this.setTemplateInfo().toPromise()
255                     .then(_ => this.selectTemplate(newTmpl.id()));
256             }
257         });
258     }
259
260     deleteTemplate() {
261         this.confirmDelete.open().subscribe(confirmed => {
262             if (!confirmed) { return; }
263             this.pcrud.remove(this.template).toPromise().then(_ => {
264                 this.setTemplateInfo().toPromise()
265                     .then(x => this.selectTemplate(null));
266             });
267         });
268     }
269 }
270
271