]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/web/js/ui/default/staff/cat/printlabels/app.js
LP#1704873 webstaff: label printing
[working/Evergreen.git] / Open-ILS / web / js / ui / default / staff / cat / printlabels / app.js
1 /**
2  * Vol/Copy Editor
3  */
4
5 angular.module('egPrintLabels',
6     ['ngRoute', 'ui.bootstrap', 'egCoreMod', 'egUiMod', 'egGridMod'])
7
8 .config(function($routeProvider, $locationProvider, $compileProvider) {
9     $locationProvider.html5Mode(true);
10     $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|blob):/); // grid export
11
12     var resolver = {
13         delay : ['egStartup', function(egStartup) { return egStartup.go(); }]
14     };
15
16     $routeProvider.when('/cat/printlabels/:dataKey', {
17         templateUrl: './cat/printlabels/t_view',
18         controller: 'LabelCtrl',
19         resolve : resolver
20     });
21
22 })
23
24 .factory('itemSvc', 
25        ['egCore',
26 function(egCore) {
27
28     var service = {
29         copies : [], // copy barcode search results
30         index : 0 // search grid index
31     };
32
33     service.flesh = {   
34         flesh : 3, 
35         flesh_fields : {
36             acp : ['call_number','location','status','location','floating','circ_modifier','age_protect'],
37             acn : ['record','prefix','suffix'],
38             bre : ['simple_record','creator','editor']
39         },
40         select : { 
41             // avoid fleshing MARC on the bre
42             // note: don't add simple_record.. not sure why
43             bre : ['id','tcn_value','creator','editor'],
44         } 
45     }
46
47     // resolved with the last received copy
48     service.fetch = function(barcode, id, noListDupes) {
49         var promise;
50
51         if (barcode) {
52             promise = egCore.pcrud.search('acp', 
53                 {barcode : barcode, deleted : 'f'}, service.flesh);
54         } else {
55             promise = egCore.pcrud.retrieve('acp', id, service.flesh);
56         }
57
58         var lastRes;
59         return promise.then(
60             function() {return lastRes},
61             null, // error
62
63             // notify reads the stream of copies, one at a time.
64             function(copy) {
65
66                 var flatCopy;
67                 if (noListDupes) {
68                     // use the existing copy if possible
69                     flatCopy = service.copies.filter(
70                         function(c) {return c.id == copy.id()})[0];
71                 }
72
73                 if (!flatCopy) {
74                     flatCopy = egCore.idl.toHash(copy, true);
75                     flatCopy.index = service.index++;
76                     service.copies.unshift(flatCopy);
77                 }
78
79                 return lastRes = {
80                     copy : copy, 
81                     index : flatCopy.index
82                 }
83             }
84         );
85     }
86
87     return service;
88 }])
89
90 /**
91  * Label controller!
92  */
93 .controller('LabelCtrl', 
94        ['$scope','$q','$window','$routeParams','$location','$timeout','egCore','egNet','ngToast','itemSvc',
95 function($scope , $q , $window , $routeParams , $location , $timeout , egCore , egNet , ngToast , itemSvc ) {
96
97     var dataKey = $routeParams.dataKey;
98     console.debug('dataKey: ' + dataKey);
99
100     $scope.print = {
101         template_name : 'item_label',
102         template_output : '',
103         template_context : 'default'
104     };
105
106
107     if (dataKey && dataKey.length > 0) {
108
109         egNet.request(
110             'open-ils.actor',
111             'open-ils.actor.anon_cache.get_value',
112             dataKey, 'print-labels-these-copies'
113         ).then(function (data) {
114
115             if (data) {
116
117                 $scope.preview_scope = {
118                      'copies' : []
119                     ,'settings' : {}
120                     ,'get_cn_for' : function(copy) {
121                         var key = $scope.rendered_cn_key_by_copy_id[copy.id];
122                         if (key) {
123                             var manual_cn = $scope.rendered_call_number_set[key];
124                             if (manual_cn && manual_cn.value) {
125                                 return manual_cn.value;
126                             } else {
127                                 return '..';
128                             }
129                         } else {
130                             return '...';
131                         }
132                     }
133                     ,'get_bib_for' : function(copy) {
134                         return $scope.record_details[copy['call_number.record.id']];
135                     }
136                     ,'get_cn_prefix' : function(copy) {
137                         return copy['call_number.prefix.label'];
138                     }
139                     ,'get_cn_suffix' : function(copy) {
140                         return copy['call_number.suffix.label'];
141                     }
142                     ,'get_location_prefix' : function(copy) {
143                         return copy['location.label_prefix'];
144                     }
145                     ,'get_location_suffix' : function(copy) {
146                         return copy['location.label_suffix'];
147                     }
148                     ,'get_cn_and_location_prefix' : function(copy,separator) {
149                         var acpl_prefix = copy['location.label_prefix'] || '';
150                         var cn_prefix = copy['call_number.prefix.label'] || '';
151                         var prefix = acpl_prefix + ' ' + cn_prefix;
152                         prefix = prefix.trim();
153                         if (separator && prefix != '') { prefix += separator; }
154                         return prefix;
155                     }
156                     ,'get_cn_and_location_suffix' : function(copy,separator) {
157                         var acpl_suffix = copy['location.label_suffix'] || '';
158                         var cn_suffix = copy['call_number.suffix.label'] || '';
159                         var suffix = cn_suffix + ' ' + acpl_suffix;
160                         suffix = suffix.trim();
161                         if (separator && suffix != '') { suffix = separator + suffix; }
162                         return suffix;
163                     }
164                 };
165                 $scope.record_details = {};
166                 $scope.org_unit_settings = {};
167
168                 var promises = [];
169                 $scope.org_unit_setting_list = [
170                      'webstaff.cat.label.font.family'
171                     ,'webstaff.cat.label.font.size'
172                     ,'webstaff.cat.label.font.weight'
173                     ,'webstaff.cat.label.inline_css'
174                     ,'webstaff.cat.label.left_label.height'
175                     ,'webstaff.cat.label.left_label.left_margin'
176                     ,'webstaff.cat.label.left_label.width'
177                     ,'webstaff.cat.label.right_label.height'
178                     ,'webstaff.cat.label.right_label.left_margin'
179                     ,'webstaff.cat.label.right_label.width'
180                     ,'webstaff.cat.label.call_number_wrap_filter_height'
181                     ,'webstaff.cat.label.call_number_wrap_filter_width'
182                 ];
183
184                 promises.push(
185                     egCore.pcrud.search('coust',{name:$scope.org_unit_setting_list}).then(
186                          null
187                         ,null
188                         ,function(yaous) {
189                             $scope.org_unit_settings[yaous.name()] = egCore.idl.toHash(yaous, true);
190                         }
191                     )
192                 );
193
194                 promises.push(
195                     egCore.org.settings($scope.org_unit_setting_list).then(function(res) {
196                         $scope.preview_scope.settings = res;
197                         egCore.hatch.getItem('cat.printlabels.last_settings').then(function(last_settings) {
198                             if (last_settings) {
199                                 for (s in last_settings) {
200                                     $scope.preview_scope.settings[s] = last_settings[s];
201                                 }
202                             }
203                         });
204                     })
205                 );
206
207                 angular.forEach(data.copies, function(copy) {
208                     promises.push(
209                         itemSvc.fetch(null,copy).then(function(res) {
210                             var flat_copy = egCore.idl.toHash(res.copy, true);
211                             $scope.preview_scope.copies.push(flat_copy);
212                             $scope.record_details[ flat_copy['call_number.record.id'] ] = 1;
213                         })
214                     )
215                 });
216
217                 $q.all(promises).then(function() {
218
219                     var promises2 = [];
220                     angular.forEach($scope.record_details, function(el,k,obj) {
221                         promises2.push(
222                             egNet.request(
223                                 'open-ils.search',
224                                 'open-ils.search.biblio.record.mods_slim.retrieve.authoritative',
225                                 k
226                             ).then(function (data) {
227                                 obj[k] = egCore.idl.toHash(data, true);
228                             })
229                         );
230                     });
231
232                     $q.all(promises2).then(function() {
233                         // today, staff, current_location, etc.
234                         egCore.print.fleshPrintScope($scope.preview_scope);
235                         $scope.template_changed(); // load the default
236                         $scope.rebuild_cn_set();
237                     });
238
239                 });
240             } else {
241                 ngToast.danger(egCore.strings.KEY_EXPIRED);
242             }
243
244         });
245
246     }
247
248     $scope.fetchTemplates = function () {
249         egCore.hatch.getItem('cat.printlabels.templates').then(function(t) {
250             if (t) {
251                 $scope.templates = t;
252                 $scope.template_name_list = Object.keys(t);
253             }
254         });
255     }
256     $scope.fetchTemplates();
257
258     $scope.applyTemplate = function (n) {
259         $scope.print.cn_template_content = $scope.templates[n].cn_content;
260         $scope.print.template_content = $scope.templates[n].content;
261         $scope.print.template_context = $scope.templates[n].context;
262         for (var s in $scope.templates[n].settings) {
263             $scope.preview_scope.settings[s] = $scope.templates[n].settings[s];
264         }
265     }
266
267     $scope.deleteTemplate = function (n) {
268         if (n) {
269             delete $scope.templates[n]
270             $scope.template_name_list = Object.keys($scope.templates);
271             $scope.template_name = '';
272             egCore.hatch.setItem('cat.printlabels.templates', $scope.templates);
273             $scope.fetchTemplates();
274             ngToast.create(egCore.strings.PRINT_LABEL_TEMPLATE_SUCCESS_DELETE);
275         }
276     }
277
278     $scope.saveTemplate = function (n) {
279         if (n) {
280
281             $scope.templates[n] = {
282                  content : $scope.print.template_content
283                 ,context : $scope.print.template_context
284                 ,cn_content : $scope.print.cn_template_content
285                 ,settings : $scope.preview_scope.settings
286             };
287             $scope.template_name_list = Object.keys($scope.templates);
288
289             egCore.hatch.setItem('cat.printlabels.templates', $scope.templates);
290             $scope.fetchTemplates();
291
292             $scope.dirty = false;
293         } else {
294             // save all templates, as we might do after an import
295             egCore.hatch.setItem('cat.printlabels.templates', $scope.templates);
296             $scope.fetchTemplates();
297         }
298         ngToast.create(egCore.strings.PRINT_LABEL_TEMPLATE_SUCCESS_SAVE);
299     }
300
301     $scope.templates = {};
302     $scope.imported_templates = { data : '' };
303     $scope.template_name = '';
304     $scope.template_name_list = [];
305
306     $scope.print_labels = function() {
307         $scope.save_locally();
308         return egCore.print.print({
309             context : $scope.print.template_context,
310             template : $scope.print.template_name,
311             scope : $scope.preview_scope,
312         });
313     }
314
315     $scope.template_changed = function() {
316         $scope.print.load_failed = false;
317         egCore.print.getPrintTemplate('item_label')
318         .then(
319             function(html) { 
320                 $scope.print.template_content = html;
321             },
322             function() {
323                 $scope.print.template_content = '';
324                 $scope.print.load_failed = true;
325             }
326         );
327         egCore.print.getPrintTemplateContext('item_label')
328         .then(function(template_context) {
329             $scope.print.template_context = template_context;
330         });
331         egCore.print.getPrintTemplate('item_label_cn')
332         .then(
333             function(html) {
334                 $scope.print.cn_template_content = html;
335             },
336             function() {
337                 $scope.print.cn_template_content = '';
338                 $scope.print.load_failed = true;
339             }
340         );
341         egCore.hatch.getItem('cat.printlabels.last_settings').then(function(s) {
342             if (s) {
343                 $scope.preview_scope.settings = s;
344             }
345         });
346
347     }
348
349     $scope.reset_to_default = function() {
350         egCore.print.removePrintTemplate(
351             'item_label'
352         );
353         egCore.print.removePrintTemplateContext(
354             'item_label'
355         );
356         egCore.print.removePrintTemplate(
357             'item_label_cn'
358         );
359         egCore.hatch.removeItem('cat.printlabels.last_settings');
360         for (s in $scope.preview_scope.settings) {
361             $scope.preview_scope.settings[s] = undefined;
362         }
363         $scope.preview_scope.settings = {};
364         egCore.org.settings($scope.org_unit_setting_list).then(function(res) {
365             $scope.preview_scope.settings = res;
366         });
367
368         $scope.template_changed();
369     }
370
371     $scope.save_locally = function() {
372         egCore.print.storePrintTemplate(
373             'item_label',
374             $scope.print.template_content
375         );
376         egCore.print.storePrintTemplateContext(
377             'item_label',
378             $scope.print.template_context
379         );
380         egCore.print.storePrintTemplate(
381             'item_label_cn',
382             $scope.print.cn_template_content
383         );
384         egCore.hatch.setItem('cat.printlabels.last_settings', $scope.preview_scope.settings);
385     }
386
387     $scope.imported_print_templates = { data : '' };
388     $scope.$watch('imported_templates.data', function(newVal, oldVal) {
389         if (newVal && newVal != oldVal) {
390             try {
391                 var data = JSON.parse(newVal);
392                 angular.forEach(data, function(el,k) {
393                     $scope.templates[k] = {
394                          content : el.content
395                         ,context : el.context
396                         ,cn_content : el.cn_content
397                         ,settings : el.settings
398                     };
399                 });
400                 $scope.saveTemplate();
401                 $scope.template_changed(); // refresh
402                 ngToast.create(egCore.strings.PRINT_TEMPLATES_SUCCESS_IMPORT);
403             } catch (E) {
404                 ngToast.warning(egCore.strings.PRINT_TEMPLATES_FAIL_IMPORT);
405             }
406         }
407     });
408
409     $scope.rendered_call_number_set = {};
410     $scope.rendered_cn_key_by_copy_id = {};
411     $scope.rebuild_cn_set = function() {
412         $timeout(function(){
413             $scope.rendered_call_number_set = {};
414             $scope.rendered_cn_key_by_copy_id = {};
415             for (var i = 0; i < $scope.preview_scope.copies.length; i++) {
416                 var copy = $scope.preview_scope.copies[i];
417                 var rendered_cn = document.getElementById('cn_for_copy_'+copy.id);
418                 if (rendered_cn && rendered_cn.textContent) {
419                     var key = rendered_cn.textContent;
420                     if (typeof $scope.rendered_call_number_set[key] == 'undefined') {
421                         $scope.rendered_call_number_set[key] = {
422                             value : key
423                         };
424                     }
425                     $scope.rendered_cn_key_by_copy_id[copy.id] = key;
426                 }
427             }
428             $scope.preview_scope.tickle = Date() + ' ' + Math.random();
429         });
430     }
431
432     $scope.$watch('print.cn_template_content', function(newVal, oldVal) {
433         if (newVal && newVal != oldVal) {
434             $scope.rebuild_cn_set();
435         }
436     });
437
438     $scope.$watch("preview_scope.settings['webstaff.cat.label.call_number_wrap_filter_height']", function(newVal, oldVal) {
439         if (newVal && newVal != oldVal) {
440             $scope.rebuild_cn_set();
441         }
442     });
443
444     $scope.$watch("preview_scope.settings['webstaff.cat.label.call_number_wrap_filter_width']", function(newVal, oldVal) {
445         if (newVal && newVal != oldVal) {
446             $scope.rebuild_cn_set();
447         }
448     });
449
450     $scope.current_tab = 'call_numbers';
451     $scope.set_tab = function(tab) {
452         $scope.current_tab = tab;
453     }
454
455 }])
456
457 // 
458 .directive('egPrintTemplateOutput', ['$compile',function($compile) {
459     return function(scope, element, attrs) {
460         scope.$watch(
461             function(scope) {
462                 return scope.$eval(attrs.content);
463             },
464             function(value) {
465                 // create an isolate scope and copy the print context
466                 // data into the new scope.
467                 // TODO: see also print security concerns in egHatch
468                 var result = element.html(value);
469                 var context = scope.$eval(attrs.context);
470                 var print_scope = scope.$new(true);
471                 angular.forEach(context, function(val, key) {
472                     print_scope[key] = val;
473                 })
474                 $compile(element.contents())(print_scope);
475             }
476         );
477     };
478 }])
479
480 .filter('cn_wrap', function() {
481     return function(input, w, h, wrap_type) {
482         var names;
483         var prefix = input[0];
484         var callnum = input[1];
485         var suffix = input[2];
486
487         if (!w) { w = 8; }
488         if (!h) { h = 9; }
489
490         /* handle spine labels differently if using LC */
491         if (wrap_type == 'lc' || wrap_type == 3) {
492             /* Establish a pattern where every return value should be isolated on its own line 
493                on the spine label: subclass letters, subclass numbers, cutter numbers, trailing stuff (date) */
494             var patt1 = /^([A-Z]{1,3})\s*(\d+(?:\.\d+)?)\s*(\.[A-Z]\d*)\s*([A-Z]\d*)?\s*(\d\d\d\d(?:-\d\d\d\d)?)?\s*(.*)$/i;
495             var result = callnum.match(patt1);
496             if (result) { 
497                 callnum = result.slice(1).join('\t');  
498             } else {
499                 callnum = callnum.split(/\s+/).join('\t');
500             } 
501
502             /* If result is null, leave callnum alone. Can't parse this malformed call num */
503         } else {
504             callnum = callnum.split(/\s+/).join('\t');
505         }
506
507         if (prefix) {
508             callnum = prefix + '\t' + callnum;
509         }
510         if (suffix) {
511             callnum += '\t' + suffix;
512         }
513
514         /* At this point, the call number pieces are separated by tab characters.  This allows
515         *  some space-containing constructs like "v. 1" to appear on one line
516         */
517         callnum = callnum.replace(/\t\t/g,'\t');  /* Squeeze out empties */ 
518         names = callnum.split('\t');
519         var j = 0; var tb = [];
520         while (j < h) {
521             
522             /* spine */
523             if (j < w) {
524
525                 var name = names.shift();
526                 if (name) {
527                     name = String( name );
528
529                     /* if the name is greater than the label width... */
530                     if (name.length > w) {
531                         /* then try to split it on periods */
532                         var sname = name.split(/\./);
533                         if (sname.length > 1) {
534                             /* if we can, then put the periods back in on each splitted element */
535                             if (name.match(/^\./)) sname[0] = '.' + sname[0];
536                             for (var k = 1; k < sname.length; k++) sname[k] = '.' + sname[k];
537                             /* and put all but the first one back into the names array */
538                             names = sname.slice(1).concat( names );
539                             /* if the name fragment is still greater than the label width... */
540                             if (sname[0].length > w) {
541                                 /* then just truncate and throw the rest back into the names array */
542                                 tb[j] = sname[0].substr(0,w);
543                                 names = [ sname[0].substr(w) ].concat( names );
544                             } else {
545                                 /* otherwise we're set */
546                                 tb[j] = sname[0];
547                             }
548                         } else {
549                             /* if we can't split on periods, then just truncate and throw the rest back into the names array */
550                             tb[j] = name.substr(0,w);
551                             names = [ name.substr(w) ].concat( names );
552                         }
553                     } else {
554                         /* otherwise we're set */
555                         tb[j] = name;
556                     }
557                 }
558             }
559             j++;
560         }
561         return tb.join('\n');
562     }
563 })
564
565 .filter('wrap', function() {
566     return function(input, w, wrap_type, indent) {
567         var output;
568
569         if (!w) return input;
570         if (!indent) indent = '';
571
572         function wrap_on_space(
573                 text,
574                 length,
575                 wrap_just_once,
576                 if_cant_wrap_then_truncate,
577                 idx
578         ) {
579             if (idx>10) {
580                 console.log('possible infinite recursion, aborting');
581                 return '';
582             }
583             if (String(text).length <= length) {
584                 return text;
585             } else {
586                 var truncated_text = String(text).substr(0,length);
587                 var pivot_pos = truncated_text.lastIndexOf(' ');
588                 var left_chunk = text.substr(0,pivot_pos).replace(/\s*$/,'');
589                 var right_chunk = String(text).substr(pivot_pos+1);
590
591                 var wrapped_line;
592                 if (left_chunk.length == 0) {
593                     if (if_cant_wrap_then_truncate) {
594                         wrapped_line = truncated_text;
595                     } else {
596                         wrapped_line = text;
597                     }
598                 } else {
599                     wrapped_line =
600                         left_chunk + '\n'
601                         + indent + (
602                             wrap_just_once
603                             ? right_chunk
604                             : (
605                                 right_chunk.length > length
606                                 ? wrap_on_space(
607                                     right_chunk,
608                                     length,
609                                     false,
610                                     if_cant_wrap_then_truncate,
611                                     idx+1)
612                                 : right_chunk
613                             )
614                         )
615                     ;
616                 }
617                 return wrapped_line;
618             }
619         }
620
621         switch(wrap_type) {
622             case 'once':
623                 output = wrap_on_space(input,w,true,false,0);
624             break;
625             default:
626                 output = wrap_on_space(input,w,false,false,0);
627             break;
628         }
629
630         return output;
631     }
632 })
633