LP#1726568: expose circ lib and owning lib fields to spine/pocket labels
[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?|mailto|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','circ_lib'],
37             acn : ['record','prefix','suffix','owning_lib'],
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 (set_default) {
249         return 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                 if (set_default) {
254                     egCore.hatch.getItem('cat.printlabels.default_template').then(function(d) {
255                         if ($scope.template_name_list.indexOf(d,0) > -1) {
256                             $scope.template_name = d;
257                         }
258                     });
259                 }
260             }
261         });
262     }
263     $scope.fetchTemplates(true);
264
265     $scope.applyTemplate = function (n) {
266         $scope.print.cn_template_content = $scope.templates[n].cn_content;
267         $scope.print.template_content = $scope.templates[n].content;
268         $scope.print.template_context = $scope.templates[n].context;
269         for (var s in $scope.templates[n].settings) {
270             $scope.preview_scope.settings[s] = $scope.templates[n].settings[s];
271         }
272         egCore.hatch.setItem('cat.printlabels.default_template', n);
273         $scope.save_locally();
274     }
275
276     $scope.deleteTemplate = function (n) {
277         if (n) {
278             delete $scope.templates[n]
279             $scope.template_name_list = Object.keys($scope.templates);
280             $scope.template_name = '';
281             egCore.hatch.setItem('cat.printlabels.templates', $scope.templates);
282             $scope.fetchTemplates();
283             ngToast.create(egCore.strings.PRINT_LABEL_TEMPLATE_SUCCESS_DELETE);
284             egCore.hatch.getItem('cat.printlabels.default_template').then(function(d) {
285                 if (d && d == n) {
286                     egCore.hatch.removeItem('cat.printlabels.default_template');
287                 }
288             });
289         }
290     }
291
292     $scope.saveTemplate = function (n) {
293         if (n) {
294
295             $scope.templates[n] = {
296                  content : $scope.print.template_content
297                 ,context : $scope.print.template_context
298                 ,cn_content : $scope.print.cn_template_content
299                 ,settings : $scope.preview_scope.settings
300             };
301             $scope.template_name_list = Object.keys($scope.templates);
302
303             egCore.hatch.setItem('cat.printlabels.templates', $scope.templates);
304             $scope.fetchTemplates();
305
306             $scope.dirty = false;
307         } else {
308             // save all templates, as we might do after an import
309             egCore.hatch.setItem('cat.printlabels.templates', $scope.templates);
310             $scope.fetchTemplates();
311         }
312         ngToast.create(egCore.strings.PRINT_LABEL_TEMPLATE_SUCCESS_SAVE);
313     }
314
315     $scope.templates = {};
316     $scope.imported_templates = { data : '' };
317     $scope.template_name = '';
318     $scope.template_name_list = [];
319
320     $scope.print_labels = function() {
321         return egCore.print.print({
322             context : $scope.print.template_context,
323             template : $scope.print.template_name,
324             scope : $scope.preview_scope,
325         });
326     }
327
328     $scope.template_changed = function() {
329         $scope.print.load_failed = false;
330         egCore.print.getPrintTemplate('item_label')
331         .then(
332             function(html) { 
333                 $scope.print.template_content = html;
334             },
335             function() {
336                 $scope.print.template_content = '';
337                 $scope.print.load_failed = true;
338             }
339         );
340         egCore.print.getPrintTemplateContext('item_label')
341         .then(function(template_context) {
342             $scope.print.template_context = template_context;
343         });
344         egCore.print.getPrintTemplate('item_label_cn')
345         .then(
346             function(html) {
347                 $scope.print.cn_template_content = html;
348             },
349             function() {
350                 $scope.print.cn_template_content = '';
351                 $scope.print.load_failed = true;
352             }
353         );
354         egCore.hatch.getItem('cat.printlabels.last_settings').then(function(s) {
355             if (s) {
356                 $scope.preview_scope.settings = s;
357             }
358         });
359
360     }
361
362     $scope.reset_to_default = function() {
363         egCore.print.removePrintTemplate(
364             'item_label'
365         );
366         egCore.print.removePrintTemplateContext(
367             'item_label'
368         );
369         egCore.print.removePrintTemplate(
370             'item_label_cn'
371         );
372         egCore.hatch.removeItem('cat.printlabels.last_settings');
373         for (s in $scope.preview_scope.settings) {
374             $scope.preview_scope.settings[s] = undefined;
375         }
376         $scope.preview_scope.settings = {};
377         egCore.org.settings($scope.org_unit_setting_list).then(function(res) {
378             $scope.preview_scope.settings = res;
379         });
380
381         $scope.template_changed();
382     }
383
384     $scope.save_locally = function() {
385         egCore.print.storePrintTemplate(
386             'item_label',
387             $scope.print.template_content
388         );
389         egCore.print.storePrintTemplateContext(
390             'item_label',
391             $scope.print.template_context
392         );
393         egCore.print.storePrintTemplate(
394             'item_label_cn',
395             $scope.print.cn_template_content
396         );
397         egCore.hatch.setItem('cat.printlabels.last_settings', $scope.preview_scope.settings);
398     }
399
400     $scope.imported_print_templates = { data : '' };
401     $scope.$watch('imported_templates.data', function(newVal, oldVal) {
402         if (newVal && newVal != oldVal) {
403             try {
404                 var data = JSON.parse(newVal);
405                 angular.forEach(data, function(el,k) {
406                     $scope.templates[k] = {
407                          content : el.content
408                         ,context : el.context
409                         ,cn_content : el.cn_content
410                         ,settings : el.settings
411                     };
412                 });
413                 $scope.saveTemplate();
414                 $scope.template_changed(); // refresh
415                 ngToast.create(egCore.strings.PRINT_TEMPLATES_SUCCESS_IMPORT);
416             } catch (E) {
417                 ngToast.warning(egCore.strings.PRINT_TEMPLATES_FAIL_IMPORT);
418             }
419         }
420     });
421
422     $scope.rendered_call_number_set = {};
423     $scope.rendered_cn_key_by_copy_id = {};
424     $scope.rebuild_cn_set = function() {
425         $timeout(function(){
426             $scope.rendered_call_number_set = {};
427             $scope.rendered_cn_key_by_copy_id = {};
428             for (var i = 0; i < $scope.preview_scope.copies.length; i++) {
429                 var copy = $scope.preview_scope.copies[i];
430                 var rendered_cn = document.getElementById('cn_for_copy_'+copy.id);
431                 if (rendered_cn && rendered_cn.textContent) {
432                     var key = rendered_cn.textContent;
433                     if (typeof $scope.rendered_call_number_set[key] == 'undefined') {
434                         $scope.rendered_call_number_set[key] = {
435                             value : key
436                         };
437                     }
438                     $scope.rendered_cn_key_by_copy_id[copy.id] = key;
439                 }
440             }
441             $scope.preview_scope.tickle = Date() + ' ' + Math.random();
442         });
443     }
444
445     $scope.$watch('print.cn_template_content', function(newVal, oldVal) {
446         if (newVal && newVal != oldVal) {
447             $scope.rebuild_cn_set();
448         }
449     });
450
451     $scope.$watch("preview_scope.settings['webstaff.cat.label.call_number_wrap_filter_height']", function(newVal, oldVal) {
452         if (newVal && newVal != oldVal) {
453             $scope.rebuild_cn_set();
454         }
455     });
456
457     $scope.$watch("preview_scope.settings['webstaff.cat.label.call_number_wrap_filter_width']", function(newVal, oldVal) {
458         if (newVal && newVal != oldVal) {
459             $scope.rebuild_cn_set();
460         }
461     });
462
463     $scope.current_tab = 'call_numbers';
464     $scope.set_tab = function(tab) {
465         $scope.current_tab = tab;
466     }
467
468 }])
469
470 // 
471 .directive('egPrintTemplateOutput', ['$compile',function($compile) {
472     return function(scope, element, attrs) {
473         scope.$watch(
474             function(scope) {
475                 return scope.$eval(attrs.content);
476             },
477             function(value) {
478                 // create an isolate scope and copy the print context
479                 // data into the new scope.
480                 // TODO: see also print security concerns in egHatch
481                 var result = element.html(value);
482                 var context = scope.$eval(attrs.context);
483                 var print_scope = scope.$new(true);
484                 angular.forEach(context, function(val, key) {
485                     print_scope[key] = val;
486                 })
487                 $compile(element.contents())(print_scope);
488             }
489         );
490     };
491 }])
492
493 .filter('cn_wrap', function() {
494     return function(input, w, h, wrap_type) {
495         var names;
496         var prefix = input[0];
497         var callnum = input[1];
498         var suffix = input[2];
499
500         if (!w) { w = 8; }
501         if (!h) { h = 9; }
502
503         /* handle spine labels differently if using LC */
504         if (wrap_type == 'lc' || wrap_type == 3) {
505             /* Establish a pattern where every return value should be isolated on its own line 
506                on the spine label: subclass letters, subclass numbers, cutter numbers, trailing stuff (date) */
507             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;
508             var result = callnum.match(patt1);
509             if (result) { 
510                 callnum = result.slice(1).join('\t');  
511             } else {
512                 callnum = callnum.split(/\s+/).join('\t');
513             } 
514
515             /* If result is null, leave callnum alone. Can't parse this malformed call num */
516         } else {
517             callnum = callnum.split(/\s+/).join('\t');
518         }
519
520         if (prefix) {
521             callnum = prefix + '\t' + callnum;
522         }
523         if (suffix) {
524             callnum += '\t' + suffix;
525         }
526
527         /* At this point, the call number pieces are separated by tab characters.  This allows
528         *  some space-containing constructs like "v. 1" to appear on one line
529         */
530         callnum = callnum.replace(/\t\t/g,'\t');  /* Squeeze out empties */ 
531         names = callnum.split('\t');
532         var j = 0; var tb = [];
533         while (j < h) {
534             
535             /* spine */
536             if (j < w) {
537
538                 var name = names.shift();
539                 if (name) {
540                     name = String( name );
541
542                     /* if the name is greater than the label width... */
543                     if (name.length > w) {
544                         /* then try to split it on periods */
545                         var sname = name.split(/\./);
546                         if (sname.length > 1) {
547                             /* if we can, then put the periods back in on each splitted element */
548                             if (name.match(/^\./)) sname[0] = '.' + sname[0];
549                             for (var k = 1; k < sname.length; k++) sname[k] = '.' + sname[k];
550                             /* and put all but the first one back into the names array */
551                             names = sname.slice(1).concat( names );
552                             /* if the name fragment is still greater than the label width... */
553                             if (sname[0].length > w) {
554                                 /* then just truncate and throw the rest back into the names array */
555                                 tb[j] = sname[0].substr(0,w);
556                                 names = [ sname[0].substr(w) ].concat( names );
557                             } else {
558                                 /* otherwise we're set */
559                                 tb[j] = sname[0];
560                             }
561                         } else {
562                             /* if we can't split on periods, then just truncate and throw the rest back into the names array */
563                             tb[j] = name.substr(0,w);
564                             names = [ name.substr(w) ].concat( names );
565                         }
566                     } else {
567                         /* otherwise we're set */
568                         tb[j] = name;
569                     }
570                 }
571             }
572             j++;
573         }
574         return tb.join('\n');
575     }
576 })
577
578 .filter('wrap', function() {
579     return function(input, w, wrap_type, indent) {
580         var output;
581
582         if (!w) return input;
583         if (!indent) indent = '';
584
585         function wrap_on_space(
586                 text,
587                 length,
588                 wrap_just_once,
589                 if_cant_wrap_then_truncate,
590                 idx
591         ) {
592             if (idx>10) {
593                 console.log('possible infinite recursion, aborting');
594                 return '';
595             }
596             if (String(text).length <= length) {
597                 return text;
598             } else {
599                 var truncated_text = String(text).substr(0,length);
600                 var pivot_pos = truncated_text.lastIndexOf(' ');
601                 var left_chunk = text.substr(0,pivot_pos).replace(/\s*$/,'');
602                 var right_chunk = String(text).substr(pivot_pos+1);
603
604                 var wrapped_line;
605                 if (left_chunk.length == 0) {
606                     if (if_cant_wrap_then_truncate) {
607                         wrapped_line = truncated_text;
608                     } else {
609                         wrapped_line = text;
610                     }
611                 } else {
612                     wrapped_line =
613                         left_chunk + '\n'
614                         + indent + (
615                             wrap_just_once
616                             ? right_chunk
617                             : (
618                                 right_chunk.length > length
619                                 ? wrap_on_space(
620                                     right_chunk,
621                                     length,
622                                     false,
623                                     if_cant_wrap_then_truncate,
624                                     idx+1)
625                                 : right_chunk
626                             )
627                         )
628                     ;
629                 }
630                 return wrapped_line;
631             }
632         }
633
634         switch(wrap_type) {
635             case 'once':
636                 output = wrap_on_space(input,w,true,false,0);
637             break;
638             default:
639                 output = wrap_on_space(input,w,false,false,0);
640             break;
641         }
642
643         return output;
644     }
645 })
646