5c2ab31b9182d55ffde81e1fad5f338bc02bffe7
[working/Evergreen.git] / Open-ILS / web / js / ui / default / vandelay / vandelay.js
1 /* ---------------------------------------------------------------------------
2 # Copyright (C) 2008  Georgia Public Library Service
3 # Bill Erickson <erickson@esilibrary.com>
4
5 # This program is free software; you can redistribute it and/or
6 # modify it under the terms of the GNU General Public License
7 # as published by the Free Software Foundation; either version 2
8 # of the License, or (at your option) any later version.
9
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU General Public License for more details.
14 # --------------------------------------------------------------------------- */
15 dojo.require("dojo.parser");
16 dojo.require("dojo.io.iframe"); 
17 dojo.require("dijit.ProgressBar"); 
18 dojo.require("dijit.form.FilteringSelect"); 
19 dojo.require("dijit.layout.ContentPane");
20 dojo.require("dijit.layout.TabContainer");
21 dojo.require("dijit.layout.LayoutContainer");
22 dojo.require('dijit.form.Button');
23 dojo.require('dijit.form.CheckBox');
24 dojo.require('dijit.Toolbar');
25 dojo.require('dijit.Tooltip');
26 dojo.require('dijit.Menu');
27 dojo.require("dijit.Dialog");
28 dojo.require("dojo.cookie");
29 dojo.require('dojox.grid.DataGrid');
30 dojo.require("dojo.data.ItemFileReadStore");
31 dojo.require('dojo.date.locale');
32 dojo.require('dojo.date.stamp');
33 dojo.require("fieldmapper.Fieldmapper");
34 dojo.require("fieldmapper.dojoData");
35 dojo.require("fieldmapper.OrgUtils");
36 dojo.require('openils.CGI');
37 dojo.require('openils.User');
38 dojo.require('openils.Event');
39 dojo.require('openils.Util');
40 dojo.require('openils.MarcXPathParser');
41 dojo.require('openils.widget.GridColumnPicker');
42 dojo.require('openils.PermaCrud');
43
44
45 var globalDivs = [
46     'vl-generic-progress',
47     'vl-generic-progress-with-total',
48     'vl-marc-upload-div',
49     'vl-queue-div',
50     'vl-match-div',
51     'vl-marc-html-div',
52     'vl-queue-select-div',
53     'vl-marc-upload-status-div',
54     'vl-attr-editor-div',
55     'vl-marc-export-div'
56 ];
57
58 var authtoken;
59 var VANDELAY_URL = '/vandelay-upload';
60 var bibAttrDefs = [];
61 var authAttrDefs = [];
62 var queuedRecords = [];
63 var queuedRecordsMap = {};
64 var bibAttrsFetched = false;
65 var authAttrsFetched = false;
66 var attrDefMap = {}; // maps attr def code names to attr def ids
67 var currentType;
68 var currentQueueId = null;
69 var userCache = {};
70 var currentMatchedRecords; // set of loaded matched bib records
71 var currentOverlayRecordsMap; // map of import record to overlay record
72 var currentOverlayRecordsMapGid; // map of import record to overlay record grid id
73 var currentImportRecId; // when analyzing matches, this is the current import record
74 var userBibQueues = []; // only non-complete queues
75 var userAuthQueues = []; // only non-complete queues
76 var allUserBibQueues;
77 var allUserAuthQueues;
78 var selectableGridRecords;
79 var cgi = new openils.CGI();
80 var vlQueueGridColumePicker = {};
81 var vlBibSources = [];
82 var importItemDefs = [];
83
84 /**
85   * Grab initial data
86   */
87 function vlInit() {
88     authtoken = openils.User.authtoken;
89     var initNeeded = 6; // how many async responses do we need before we're init'd 
90     var initCount = 0; // how many async reponses we've received
91
92     openils.Util.registerEnterHandler(
93         vlQueueDisplayPage.domNode, function(){retrieveQueuedRecords();});
94     openils.Util.addCSSClass(dojo.byId('vl-menu-marc-upload'), 'toolbar_selected');
95
96     function checkInitDone() {
97         initCount++;
98         if(initCount == initNeeded)
99             runStartupCommands();
100     }
101
102     // Fetch the bib and authority attribute definitions 
103     vlFetchBibAttrDefs(function () { checkInitDone(); });
104     vlFetchAuthAttrDefs(function () { checkInitDone(); });
105
106     vlRetrieveQueueList('bib', null, 
107         function(list) {
108             allUserBibQueues = list;
109             for(var i = 0; i < allUserBibQueues.length; i++) {
110                 if(allUserBibQueues[i].complete() == 'f')
111                     userBibQueues.push(allUserBibQueues[i]);
112             }
113             checkInitDone();
114         }
115     );
116
117     vlRetrieveQueueList('auth', null, 
118         function(list) {
119             allUserAuthQueues = list;
120             for(var i = 0; i < allUserAuthQueues.length; i++) {
121                 if(allUserAuthQueues[i].complete() == 'f')
122                     userAuthQueues.push(allUserAuthQueues[i]);
123             }
124             checkInitDone();
125         }
126     );
127
128     fieldmapper.standardRequest(
129         ['open-ils.permacrud', 'open-ils.permacrud.search.cbs.atomic'],
130         {   async: true,
131             params: [authtoken, {id:{"!=":null}}, {order_by:{cbs:'id'}}],
132             oncomplete : function(r) {
133                 vlBibSources = openils.Util.readResponse(r, false, true);
134                 checkInitDone();
135             }
136         }
137     );
138
139     var owner = fieldmapper.aou.orgNodeTrail(fieldmapper.aou.findOrgUnit(new openils.User().user.ws_ou()));
140     new openils.PermaCrud().search('viiad', 
141         {owner: owner.map(function(org) { return org.id(); })},
142         {   async: true,
143             oncomplete: function(r) {
144                 importItemDefs = openils.Util.readResponse(r);
145                 checkInitDone();
146             }
147         }
148     );
149
150     vlAttrEditorInit();
151 }
152
153
154 openils.Util.addOnLoad(vlInit);
155
156
157 // fetch the bib and authority attribute definitions
158
159 function vlFetchBibAttrDefs(postcomplete) {
160     bibAttrDefs = [];
161     fieldmapper.standardRequest(
162         ['open-ils.permacrud', 'open-ils.permacrud.search.vqbrad'],
163         {   async: true,
164             params: [authtoken, {id:{'!=':null}}],
165             onresponse: function(r) {
166                 var def = r.recv().content(); 
167                 if(e = openils.Event.parse(def[0])) 
168                     return alert(e);
169                 bibAttrDefs.push(def);
170             },
171             oncomplete: function() {
172                 bibAttrDefs = bibAttrDefs.sort(
173                     function(a, b) {
174                         if(a.id() > b.id()) return 1;
175                         if(a.id() < b.id()) return -1;
176                         return 0;
177                     }
178                 );
179                 postcomplete();
180             }
181         }
182     );
183 }
184
185 function vlFetchAuthAttrDefs(postcomplete) {
186     authAttrDefs = [];
187     fieldmapper.standardRequest(
188         ['open-ils.permacrud', 'open-ils.permacrud.search.vqarad'],
189         {   async: true,
190             params: [authtoken, {id:{'!=':null}}],
191             onresponse: function(r) {
192                 var def = r.recv().content(); 
193                 if(e = openils.Event.parse(def[0])) 
194                     return alert(e);
195                 authAttrDefs.push(def);
196             },
197             oncomplete: function() {
198                 authAttrDefs = authAttrDefs.sort(
199                     function(a, b) {
200                         if(a.id() > b.id()) return 1;
201                         if(a.id() < b.id()) return -1;
202                         return 0;
203                     }
204                 );
205                 postcomplete();
206             }
207         }
208     );
209 }
210
211 function vlRetrieveQueueList(type, filter, onload) {
212     type = (type == 'bib') ? type : 'authority';
213     fieldmapper.standardRequest(
214         ['open-ils.vandelay', 'open-ils.vandelay.'+type+'_queue.owner.retrieve.atomic'],
215         {   async: true,
216             params: [authtoken, null, filter],
217             oncomplete: function(r) {
218                 var list = r.recv().content();
219                 if(e = openils.Event.parse(list[0]))
220                     return alert(e);
221                 onload(list);
222             }
223         }
224     );
225
226 }
227
228 function displayGlobalDiv(id) {
229     for(var i = 0; i < globalDivs.length; i++) {
230         try {
231             dojo.style(dojo.byId(globalDivs[i]), 'display', 'none');
232         } catch(e) {
233             alert('please define div ' + globalDivs[i]);
234         }
235     }
236     dojo.style(dojo.byId(id),'display','block');
237
238     openils.Util.removeCSSClass(dojo.byId('vl-menu-marc-export'), 'toolbar_selected');
239     openils.Util.removeCSSClass(dojo.byId('vl-menu-marc-upload'), 'toolbar_selected');
240     openils.Util.removeCSSClass(dojo.byId('vl-menu-queue-select'), 'toolbar_selected');
241     openils.Util.removeCSSClass(dojo.byId('vl-menu-attr-editor'), 'toolbar_selected');
242
243     switch(id) {
244         case 'vl-marc-export-div':
245             openils.Util.addCSSClass(dojo.byId('vl-menu-marc-export'), 'toolbar_selected');
246             break;
247         case 'vl-marc-upload-div':
248             openils.Util.addCSSClass(dojo.byId('vl-menu-marc-upload'), 'toolbar_selected');
249             break;
250         case 'vl-queue-select-div':
251             openils.Util.addCSSClass(dojo.byId('vl-menu-queue-select'), 'toolbar_selected');
252             break;
253         case 'vl-attr-editor-div':
254             openils.Util.addCSSClass(dojo.byId('vl-menu-attr-editor'), 'toolbar_selected');
255             break;
256     }
257 }
258
259 function runStartupCommands() {
260     currentQueueId = cgi.param('qid');
261     currentType = cgi.param('qtype');
262     dojo.style('vl-nav-bar', 'visibility', 'visible');
263     if(currentQueueId)
264         return retrieveQueuedRecords(currentType, currentQueueId, handleRetrieveRecords);
265     vlShowUploadForm();
266 }
267
268 /**
269   * asynchronously upload a file of MARC records
270   */
271 function uploadMARC(onload){
272     dojo.byId('vl-upload-status-count').innerHTML = '0';
273     dojo.byId('vl-ses-input').value = authtoken;
274     displayGlobalDiv('vl-marc-upload-status-div');
275     dojo.io.iframe.send({
276         url: VANDELAY_URL,
277         method: "post",
278         handleAs: "html",
279         form: dojo.byId('vl-marc-upload-form'),
280         handle: function(data,ioArgs){
281             var content = data.documentElement.textContent;
282             onload(content);
283         }
284     });
285 }       
286
287 /**
288   * Creates a new vandelay queue
289   */
290 function createQueue(queueName, type, onload, importDefId) {
291     var name = (type=='bib') ? 'bib' : 'authority';
292     var method = 'open-ils.vandelay.'+ name +'_queue.create'
293     fieldmapper.standardRequest(
294         ['open-ils.vandelay', method],
295         {   async: true,
296             params: [authtoken, queueName, null, name, importDefId],
297             oncomplete : function(r) {
298                 var queue = r.recv().content();
299                 if(e = openils.Event.parse(queue)) 
300                     return alert(e);
301                 onload(queue);
302             }
303         }
304     );
305 }
306
307 /**
308   * Tells vandelay to pull a batch of records from the cache and explode them
309   * out into the vandelay tables
310   */
311 function processSpool(key, queueId, type, onload) {
312     fieldmapper.standardRequest(
313         ['open-ils.vandelay', 'open-ils.vandelay.'+type+'.process_spool'],
314         {   async: true,
315             params: [authtoken, key, queueId],
316             onresponse : function(r) {
317                 var resp = r.recv().content();
318                 if(e = openils.Event.parse(resp)) 
319                     return alert(e);
320                 dojo.byId('vl-upload-status-count').innerHTML = resp;
321             },
322             oncomplete : function(r) {onload();}
323         }
324     );
325 }
326
327 function retrieveQueuedRecords(type, queueId, onload) {
328     displayGlobalDiv('vl-generic-progress');
329     queuedRecords = [];
330     queuedRecordsMap = {};
331     currentOverlayRecordsMap = {};
332     currentOverlayRecordsMapGid = {};
333     selectableGridRecords = {};
334     //resetVlQueueGridLayout();
335
336     if(!type) type = currentType;
337     if(!queueId) queueId = currentQueueId;
338     if(!onload) onload = handleRetrieveRecords;
339
340     var method = 'open-ils.vandelay.'+type+'_queue.records.retrieve.atomic';
341     if(vlQueueGridShowMatches.checked)
342         method = method.replace('records', 'records.matches');
343
344     var sel = dojo.byId('vl-queue-display-limit-selector');
345     var limit = parseInt(sel.options[sel.selectedIndex].value);
346     var offset = limit * parseInt(vlQueueDisplayPage.attr('value')-1);
347
348     var params =  [authtoken, queueId, {clear_marc: 1, offset: offset, limit: limit}];
349     if(vlQueueGridShowNonImport.checked)
350         params[2].non_imported = 1;
351
352     fieldmapper.standardRequest(
353         ['open-ils.vandelay', method],
354         {   async: true,
355             params: params,
356             /*
357             onresponse: function(r) {
358                 console.log("ONREPONSE");
359                 var rec = r.recv().content();
360                 if(e = openils.Event.parse(rec))
361                     return alert(e);
362                 console.log("got record " + rec.id());
363                 queuedRecords.push(rec);
364                 queuedRecordsMap[rec.id()] = rec;
365             },
366             */
367             oncomplete: function(r){
368                 var recs = r.recv().content();
369                 if(e = openils.Event.parse(recs[0]))
370                     return alert(e);
371                 for(var i = 0; i < recs.length; i++) {
372                     var rec = recs[i];
373                     queuedRecords.push(rec);
374                     queuedRecordsMap[rec.id()] = rec;
375                 }
376                 onload();
377             }
378         }
379     );
380 }
381
382 function vlLoadMatchUI(recId) {
383     displayGlobalDiv('vl-generic-progress');
384     var matches = queuedRecordsMap[recId].matches();
385     var records = [];
386     currentImportRecId = recId;
387     for(var i = 0; i < matches.length; i++)
388         records.push(matches[i].eg_record());
389
390     var retrieve = ['open-ils.search', 'open-ils.search.biblio.record_entry.slim.retrieve'];
391     var params = [records];
392     if(currentType == 'auth') {
393         retrieve = ['open-ils.cat', 'open-ils.cat.authority.record.retrieve'];
394         parmas = [authtoken, records, {clear_marc:1}];
395     }
396
397     fieldmapper.standardRequest(
398         retrieve,
399         {   async: true,
400             params:params,
401             oncomplete: function(r) {
402                 var recs = r.recv().content();
403                 if(e = openils.Event.parse(recs))
404                     return alert(e);
405
406                 /* ui mangling */
407                 displayGlobalDiv('vl-match-div');
408                 resetVlMatchGridLayout();
409                 currentMatchedRecords = recs;
410                 vlMatchGrid.setStructure(vlMatchGridLayout);
411
412                 // build the data store of records with match information
413                 var dataStore = bre.toStoreData(recs, null, 
414                     {virtualFields:['dest_matchpoint', 'src_matchpoint', '_id']});
415                 dataStore.identifier = '_id';
416
417                 var matchSeenMap = {};
418
419                 for(var i = 0; i < dataStore.items.length; i++) {
420                     var item = dataStore.items[i];
421                     item._id = i; // just need something unique
422                     for(var j = 0; j < matches.length; j++) {
423                         var match = matches[j];
424                         if(match.eg_record() == item.id && !matchSeenMap[match.id()]) {
425                             item.dest_matchpoint = match.field_type();
426                             var attr = getRecAttrFromMatch(queuedRecordsMap[recId], match);
427                             item.src_matchpoint = getRecAttrDefFromAttr(attr, currentType).code();
428                             matchSeenMap[match.id()] = 1;
429                             break;
430                         }
431                     }
432                 }
433
434                 // now populate the grid
435                 vlPopulateMatchGrid(vlMatchGrid, dataStore);
436             }
437         }
438     );
439 }
440
441 function vlPopulateMatchGrid(grid, data) {
442     var store = new dojo.data.ItemFileReadStore({data:data});
443     grid.setStore(store);
444     grid.update();
445 }
446
447 function showMe(id) {
448     dojo.style(dojo.byId(id), 'display', 'block');
449 }
450 function hideMe(id) {
451     dojo.style(dojo.byId(id), 'display', 'none');
452 }
453
454
455 function vlLoadMARCHtml(recId, inCat, oncomplete) {
456     dijit.byId('vl-marc-html-done-button').onClick = oncomplete;
457     displayGlobalDiv('vl-generic-progress');
458     var api;
459     var params = [recId, 1];
460
461     if(inCat) {
462         hideMe('vl-marc-html-edit-button'); // don't show marc editor button
463         dijit.byId('vl-marc-html-edit-button').onClick = function(){}
464         api = ['open-ils.search', 'open-ils.search.biblio.record.html'];
465         if(currentType == 'auth')
466             api = ['open-ils.search', 'open-ils.search.authority.to_html'];
467     } else {
468         showMe('vl-marc-html-edit-button'); // plug in the marc editor button
469         dijit.byId('vl-marc-html-edit-button').onClick = 
470             function() {vlLoadMarcEditor(currentType, recId, oncomplete);};
471         params = [authtoken, recId];
472         api = ['open-ils.vandelay', 'open-ils.vandelay.queued_bib_record.html'];
473         if(currentType == 'auth')
474             api = ['open-ils.vandelay', 'open-ils.vandelay.queued_authority_record.html'];
475     }
476
477     fieldmapper.standardRequest(
478         api, 
479         {   async: true,
480             params: params,
481             oncomplete: function(r) {
482             displayGlobalDiv('vl-marc-html-div');
483                 var html = r.recv().content();
484                 dojo.byId('vl-marc-record-html').innerHTML = html;
485             }
486         }
487     );
488 }
489
490
491 /*
492 function getRecMatchesFromAttrCode(rec, attrCode) {
493     var matches = [];
494     var attr = getRecAttrFromCode(rec, attrCode);
495     for(var j = 0; j < rec.matches().length; j++) {
496         var match = rec.matches()[j];
497         if(match.matched_attr() == attr.id()) 
498             matches.push(match);
499     }
500     return matches;
501 }
502 */
503
504 function getRecAttrFromMatch(rec, match) {
505     for(var i = 0; i < rec.attributes().length; i++) {
506         var attr = rec.attributes()[i];
507         if(attr.id() == match.matched_attr())
508             return attr;
509     }
510 }
511
512 function getRecAttrDefFromAttr(attr, type) {
513     var defs = (type == 'bib') ? bibAttrDefs : authAttrDefs;
514     for(var i = 0; i < defs.length; i++) {
515         var def = defs[i];
516         if(def.id() == attr.field())
517             return def;
518     }
519 }
520
521 function getRecAttrFromCode(rec, attrCode) {
522     var defId = attrDefMap[currentType][attrCode];
523     var attrs = rec.attributes();
524     for(var i = 0; i < attrs.length; i++) {
525         var attr = attrs[i];
526         if(attr.field() == defId) 
527             return attr;
528     }
529     return null;
530 }
531
532 function vlGetViewMatches(rowIdx, item) {
533     if(item) {
534         var id = this.grid.store.getValue(item, 'id');
535         var rec = queuedRecordsMap[id];
536         if(rec.matches().length > 0)
537             return id;
538     }
539     return -1
540 }
541
542 function vlFormatViewMatches(id) {
543     if(id == -1) return '';
544     return '<a href="javascript:void(0);" onclick="vlLoadMatchUI(' + id + ');">' + this.name + '</a>';
545 }
546
547 function vlFormatViewMatchMARC(id) {
548     return '<a href="javascript:void(0);" onclick="vlLoadMARCHtml(' + id + ', false, '+
549         'function(){displayGlobalDiv(\'vl-match-div\');});">' + this.name + '</a>';
550 }
551
552 function getAttrValue(rowIdx, item) {
553     if(!item) return '';
554     var attrCode = this.field.split('.')[1];
555     var rec = queuedRecordsMap[this.grid.store.getValue(item, 'id')];
556     var attr = getRecAttrFromCode(rec, attrCode);
557     return (attr) ? attr.attr_value() : '';
558 }
559
560 function vlGetDateTimeField(rowIdx, item) {
561     if(!item) return '';
562     var value = this.grid.store.getValue(item, this.field);
563     if(!value) return '';
564     var date = dojo.date.stamp.fromISOString(value);
565     return dojo.date.locale.format(date, {selector:'date'});
566 }
567
568 function vlGetCreator(rowIdx, item) {
569     if(!item) return '';
570     var id = this.grid.store.getValue(item, 'creator');
571     if(userCache[id])
572         return userCache[id].usrname();
573     var user = fieldmapper.standardRequest(
574         ['open-ils.actor', 'open-ils.actor.user.retrieve'], [authtoken, id]);
575     if(e = openils.Event.parse(user))
576         return alert(e);
577     userCache[id] = user;
578     return user.usrname();
579 }
580
581 function vlGetViewMARC(rowIdx, item) {
582     return item && this.grid.store.getValue(item, 'id');
583 }
584
585 function vlFormatViewMARC(id) {
586     return '<a href="javascript:void(0);" onclick="vlLoadMARCHtml(' + id + ', false, '+
587         'function(){displayGlobalDiv(\'vl-queue-div\');});">' + this.name + '</a>';
588 }
589
590 function vlGetOverlayTargetSelector(rowIdx, item) {
591     if(!item) return;
592     return this.grid.store.getValue(item, '_id') + ':' + this.grid.store.getValue(item, 'id');
593 }
594
595 function vlFormatOverlayTargetSelector(val) {
596     if(!val) return '';
597     var parts = val.split(':');
598     var _id = parts[0];
599     var id = parts[1];
600     var value = '<input type="checkbox" name="vl-overlay-target-RECID" '+
601         'onclick="vlHandleOverlayTargetSelected(ID, GRIDID);" gridid="GRIDID" match="ID"/>';
602     value = value.replace(/GRIDID/g, _id);
603     value = value.replace(/RECID/g, currentImportRecId);
604     value = value.replace(/ID/g, id);
605     if(_id == currentOverlayRecordsMapGid[currentImportRecId])
606         return value.replace('/>', 'checked="checked"/>');
607     return value;
608 }
609
610
611 /**
612   * see if the user has enabled overlays for the current match set and, 
613   * if so, map the current import record to the overlay target.
614   */
615 function vlHandleOverlayTargetSelected(recId, gridId) {
616     var noneSelected = true;
617     var checkboxes = dojo.query('[name=vl-overlay-target-'+currentImportRecId+']');
618     for(var i = 0; i < checkboxes.length; i++) {
619         var checkbox = checkboxes[i];
620         var matchRecId = checkbox.getAttribute('match');
621         var gid = checkbox.getAttribute('gridid');
622         if(checkbox.checked) {
623             if(matchRecId == recId && gid == gridId) {
624                 noneSelected = false;
625                 currentOverlayRecordsMap[currentImportRecId] = matchRecId;
626                 currentOverlayRecordsMapGid[currentImportRecId] = gid;
627                 dojo.byId('vl-record-list-selected-' + currentImportRecId).checked = true;
628                 dojo.byId('vl-record-list-selected-' + currentImportRecId).parentNode.className = 'overlay_selected';
629             } else {
630                 checkbox.checked = false;
631             }
632         }
633     }
634
635     if(noneSelected) {
636         delete currentOverlayRecordsMap[currentImportRecId];
637         delete currentOverlayRecordsMapGid[currentImportRecId];
638         dojo.byId('vl-record-list-selected-' + currentImportRecId).checked = false;
639         dojo.byId('vl-record-list-selected-' + currentImportRecId).parentNode.className = '';
640     }
641 }
642
643 var valLastQueueType = null;
644 var vlQueueGridLayout = null;
645 function buildRecordGrid(type) {
646     displayGlobalDiv('vl-queue-div');
647
648     if(type == 'bib') {
649         openils.Util.show('vl-bib-queue-grid-wrapper');
650         openils.Util.hide('vl-auth-queue-grid-wrapper');
651         vlQueueGrid = vlBibQueueGrid;
652     } else {
653         openils.Util.show('vl-auth-queue-grid-wrapper');
654         openils.Util.hide('vl-bib-queue-grid-wrapper');
655         vlQueueGrid = vlAuthQueueGrid;
656     }
657
658
659     if(valLastQueueType != type) {
660         valLastQueueType = type;
661         vlQueueGridLayout = vlQueueGrid.attr('structure');
662         var defs = (type == 'bib') ? bibAttrDefs : authAttrDefs;
663         attrDefMap[type] = {};
664         for(var i = 0; i < defs.length; i++) {
665             var def = defs[i]
666             attrDefMap[type][def.code()] = def.id();
667             var col = {
668                 name:def.description(), 
669                 field:'attr.' + def.code(),
670                 get: getAttrValue,
671                 selectableColumn:true
672             };
673             vlQueueGridLayout[0].cells[0].push(col);
674         }
675     }
676
677     dojo.forEach(vlQueueGridLayout[0].cells[0], 
678         function(cell) { 
679             if(cell.field.match(/^\+/)) 
680                 cell.nonSelectable=true;
681         }
682     );
683
684     var storeData;
685     if(type == 'bib')
686         storeData = vqbr.toStoreData(queuedRecords);
687     else
688         storeData = vqar.toStoreData(queuedRecords);
689
690     var store = new dojo.data.ItemFileReadStore({data:storeData});
691     vlQueueGrid.setStore(store);
692
693     if(vlQueueGridColumePicker[type]) {
694         vlQueueGrid.update();
695     } else {
696
697         vlQueueGridColumePicker[type] =
698             new openils.widget.GridColumnPicker(
699                 authtoken, 'vandelay.queue.'+type, vlQueueGrid, vlQueueGridLayout);
700         vlQueueGridColumePicker[type].load();
701     }
702 }
703
704 function vlQueueGridPrevPage() {
705     var page = parseInt(vlQueueDisplayPage.getValue());
706     if(page < 2) return;
707     vlQueueDisplayPage.setValue(page - 1);
708     retrieveQueuedRecords(currentType, currentQueueId, handleRetrieveRecords);
709 }
710
711 function vlQueueGridNextPage() {
712     vlQueueDisplayPage.setValue(parseInt(vlQueueDisplayPage.getValue())+1);
713     retrieveQueuedRecords(currentType, currentQueueId, handleRetrieveRecords);
714 }
715
716 function vlDeleteQueue(type, queueId, onload) {
717     fieldmapper.standardRequest(
718         ['open-ils.vandelay', 'open-ils.vandelay.'+type+'_queue.delete'],
719         {   async: true,
720             params: [authtoken, queueId],
721             oncomplete: function(r) {
722                 var resp = r.recv().content();
723                 if(e = openils.Event.parse(resp))
724                     return alert(e);
725                 onload();
726             }
727         }
728     );
729 }
730
731
732 function vlQueueGridDrawSelectBox(rowIdx, item) {
733     return item &&  this.grid.store.getValue(item, 'id');
734 }
735
736 function vlQueueGridFormatSelectBox(id) {
737     var domId = 'vl-record-list-selected-' + id;
738     if (id) { selectableGridRecords[domId] = id; }
739     return "<div><input type='checkbox' id='"+domId+"'/></div>";
740 }
741
742 function vlSelectAllQueueGridRecords() {
743     for(var id in selectableGridRecords) 
744         dojo.byId(id).checked = true;
745 }
746 function vlSelectNoQueueGridRecords() {
747     for(var id in selectableGridRecords) 
748         dojo.byId(id).checked = false;
749 }
750 function vlToggleQueueGridSelect() {
751     if(dojo.byId('vl-queue-grid-row-selector').checked)
752         vlSelectAllQueueGridRecords();
753     else
754         vlSelectNoQueueGridRecords();
755 }
756
757 var handleRetrieveRecords = function() {
758     buildRecordGrid(currentType);
759     vlFetchQueueSummary(currentQueueId, currentType, 
760         function(summary) {
761             dojo.byId('vl-queue-summary-name').innerHTML = summary.queue.name();
762             dojo.byId('vl-queue-summary-total-count').innerHTML = summary.total +'';
763             dojo.byId('vl-queue-summary-import-count').innerHTML = summary.imported + '';
764         }
765     );
766 }
767
768 function vlFetchQueueSummary(qId, type, onload) {
769     fieldmapper.standardRequest(
770         ['open-ils.vandelay', 'open-ils.vandelay.'+type+'_queue.summary.retrieve'],
771         {   async: true,
772             params: [authtoken, qId],
773             oncomplete : function(r) {
774                 var summary = r.recv().content();
775                 if(e = openils.Event.parse(summary))
776                     return alert(e);
777                 return onload(summary);
778             }
779         }
780     );
781 }
782     
783
784 function vlImportSelectedRecords() {
785     displayGlobalDiv('vl-generic-progress-with-total');
786     var records = [];
787
788     for(var id in selectableGridRecords) {
789         if(dojo.byId(id).checked) {
790             var recId = selectableGridRecords[id];
791             var rec = queuedRecordsMap[recId];
792             if(!rec.import_time()) 
793                 records.push(recId);
794         }
795     }
796
797     fieldmapper.standardRequest(
798         ['open-ils.vandelay', 'open-ils.vandelay.'+currentType+'_record.list.import'],
799         {   async: true,
800             params: [authtoken, records, {overlay_map:currentOverlayRecordsMap}],
801             onresponse: function(r) {
802                 var resp = r.recv().content();
803                 if(e = openils.Event.parse(resp))
804                     return alert(e);
805                 if(resp.complete) {
806                     return retrieveQueuedRecords(currentType, currentQueueId, handleRetrieveRecords);
807                 } else {
808                     vlControlledProgressBar.update({maximum:resp.total, progress:resp.progress});
809                 }
810             }, 
811         }
812     );
813 }
814
815 function vlImportAllRecords() {
816     vlImportRecordQueue(currentType, currentQueueId, false,
817         function(){displayGlobalDiv('vl-queue-div');});
818 }
819
820 function vlImportRecordQueue(type, queueId, noMatchOnly, onload) {
821     displayGlobalDiv('vl-generic-progress-with-total');
822     var method = 'open-ils.vandelay.bib_queue.import';
823     if(noMatchOnly)
824         method = method.replace('import', 'nomatch.import');
825     if(type == 'auth')
826         method = method.replace('bib', 'auth');
827
828     var options = {};
829     if(vlUploadQueueAutoOverlayExact.checked) {
830         options.auto_overlay_exact = true;
831         vlUploadQueueAutoOverlayExact.checked = false;
832     }
833
834     fieldmapper.standardRequest(
835         ['open-ils.vandelay', method],
836         {   async: true,
837             params: [authtoken, queueId, options],
838             onresponse: function(r) {
839                 var resp = r.recv().content();
840                 if(e = openils.Event.parse(resp))
841                     return alert(e);
842                 vlControlledProgressBar.update({maximum:resp.total, progress:resp.progress});
843             },
844             oncomplete: function() {onload();}
845         }
846     );
847 }
848
849
850 function vlImportHoldings(queueId, importProfile, onload) {
851     displayGlobalDiv('vl-generic-progress-with-total');
852     fieldmapper.standardRequest(
853         ['open-ils.vandelay', 'open-ils.vandelay.bib_record.queue.asset.import'],
854         {   async: true,
855             params: [authtoken, importProfile, queueId],
856             onresponse: function(r) {
857                 var resp = openils.Util.readResponse(r);
858                 vlControlledProgressBar.update({maximum:resp.total, progress:resp.progress});
859             },
860             oncomplete: function() {onload();}
861         }
862     );
863 }
864
865 /**
866   * Create queue, upload MARC, process spool, load the newly created queue 
867   */
868 function batchUpload() {
869     var queueName = dijit.byId('vl-queue-name').getValue();
870     currentType = dijit.byId('vl-record-type').getValue();
871
872     var handleProcessSpool = function() {
873         if(vlUploadQueueAutoImport.checked || vlUploadQueueAutoOverlayExact.checked) {
874
875             vlImportRecordQueue(
876                 currentType, 
877                 currentQueueId, 
878                 vlUploadQueueAutoImport.checked,  
879                 function() {
880                     if(vlUploadQueueHoldingsImport.checked) {
881                         vlImportHoldings(
882                             currentQueueId, 
883                             vlUploadQueueHoldingsImportProfile.attr('value'), 
884                             function() { 
885                                 retrieveQueuedRecords(currentType, currentQueueId, handleRetrieveRecords);
886                             }
887                         );
888                     } else {
889                         retrieveQueuedRecords(currentType, currentQueueId, handleRetrieveRecords);
890                     }
891                 }
892             );
893         } else {
894             retrieveQueuedRecords(currentType, currentQueueId, handleRetrieveRecords);
895         }
896     }
897
898     var handleUploadMARC = function(key) {
899         dojo.style(dojo.byId('vl-upload-status-processing'), 'display', 'block');
900         processSpool(key, currentQueueId, currentType, handleProcessSpool);
901     };
902
903     var handleCreateQueue = function(queue) {
904         currentQueueId = queue.id();
905         uploadMARC(handleUploadMARC);
906     };
907     
908     if(vlUploadQueueSelector.getValue() && !queueName) {
909         currentQueueId = vlUploadQueueSelector.getValue();
910         uploadMARC(handleUploadMARC);
911     } else {
912         createQueue(queueName, currentType, handleCreateQueue, vlUploadQueueHoldingsImportProfile.attr('value'));
913     }
914 }
915
916
917 function vlFleshQueueSelect(selector, type) {
918     var data = (type == 'bib') ? vbq.toStoreData(allUserBibQueues) : vaq.toStoreData(allUserAuthQueues);
919     selector.store = new dojo.data.ItemFileReadStore({data:data});
920     selector.setValue(null);
921     selector.setDisplayedValue('');
922     if(data[0])
923         selector.setValue(data[0].id());
924 }
925
926 function vlShowUploadForm() {
927     displayGlobalDiv('vl-marc-upload-div');
928     vlFleshQueueSelect(vlUploadQueueSelector, vlUploadRecordType.getValue());
929     vlUploadSourceSelector.store = 
930         new dojo.data.ItemFileReadStore({data:cbs.toStoreData(vlBibSources, 'source')});
931     vlUploadSourceSelector.setValue(vlBibSources[0].id());
932     vlUploadQueueHoldingsImportProfile.store = 
933         new dojo.data.ItemFileReadStore({data:viiad.toStoreData(importItemDefs)});
934     vlUploadQueueHoldingsImportProfile.attr('disabled', true);
935     dojo.connect(vlUploadQueueHoldingsImport, 'onChange',
936         function(val) {
937             if(val)
938                 vlUploadQueueHoldingsImportProfile.attr('disabled', false);
939             else
940                 vlUploadQueueHoldingsImportProfile.attr('disabled', true);
941         }
942     );
943 }
944
945 function vlShowQueueSelect() {
946     displayGlobalDiv('vl-queue-select-div');
947     vlFleshQueueSelect(vlQueueSelectQueueList, vlQueueSelectType.getValue());
948 }
949
950 function vlFetchQueueFromForm() {
951     currentType = vlQueueSelectType.getValue();
952     currentQueueId = vlQueueSelectQueueList.getValue();
953     retrieveQueuedRecords(currentType, currentQueueId, handleRetrieveRecords);
954 }
955
956 function vlOpenMarcEditWindow(rec, postReloadHTMLHandler) {
957     /*
958         To run in Firefox directly, must set signed.applets.codebase_principal_support
959         to true in about:config
960     */
961     netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
962     win = window.open('/xul/server/cat/marcedit.xul'); // XXX version?
963
964     function onsave(r) {
965         // after the record is saved, reload the HTML display
966         var stat = r.recv().content();
967         if(e = openils.Event.parse(stat))
968             return alert(e);
969         alert(dojo.byId('vl-marc-edit-complete-label').innerHTML);
970         win.close();
971         vlLoadMARCHtml(rec.id(), false, postReloadHTMLHandler);
972     }
973
974     win.xulG = {
975         record : {marc : rec.marc()},
976         save : {
977             label: dojo.byId('vl-marc-edit-save-label').innerHTML,
978             func: function(xmlString) {
979                 var method = 'open-ils.permacrud.update.' + rec.classname;
980                 rec.marc(xmlString);
981                 fieldmapper.standardRequest(
982                     ['open-ils.permacrud', method],
983                     {   async: true,
984                         params: [authtoken, rec],
985                         oncomplete: onsave
986                     }
987                 );
988             },
989         }
990     };
991 }
992
993 function vlLoadMarcEditor(type, recId, postReloadHTMLHandler) {
994     var method = 'open-ils.permacrud.search.vqbr';
995     if(currentType != 'bib')
996         method = method.replace(/vqbr/,'vqar');
997
998     fieldmapper.standardRequest(
999         ['open-ils.permacrud', method],
1000         {   async: true, 
1001             params: [authtoken, {id : recId}],
1002             oncomplete: function(r) {
1003                 var rec = r.recv().content();
1004                 if(e = openils.Event.parse(rec))
1005                     return alert(e);
1006                 vlOpenMarcEditWindow(rec, postReloadHTMLHandler);
1007             }
1008         }
1009     );
1010 }
1011
1012
1013
1014 //------------------------------------------------------------
1015 // attribute editors
1016
1017 // attribute-editor global variables
1018
1019 var ATTR_EDITOR_IN_UPDATE_MODE = false; // true on 'edit', false on 'create'
1020 var ATTR_EDIT_ID = null;                // id of current 'edit' attribute
1021 var ATTR_EDIT_GROUP = 'bib';            // bib-attrs or auth-attrs
1022
1023 function vlAttrEditorInit() {
1024     // set up tooltips on the edit form
1025     connectTooltip('attr-editor-tags'); 
1026     connectTooltip('attr-editor-subfields'); 
1027 }
1028
1029 function vlShowAttrEditor() {
1030     displayGlobalDiv('vl-attr-editor-div');
1031     loadAttrEditorGrid();
1032     idHide('vl-generic-progress');
1033 }
1034
1035 function setAttrEditorGroup(groupName) {
1036     // put us into 'bib'-attr or 'auth'-attr mode.
1037     if (ATTR_EDIT_GROUP != groupName) {
1038         ATTR_EDIT_GROUP = groupName;
1039         loadAttrEditorGrid();
1040     }
1041 }
1042
1043 function onAttrEditorOpen() {
1044     // the "bars" have the create/update/cancel/etc. buttons.
1045     var create_bar = document.getElementById('attr-editor-create-bar');
1046     var update_bar = document.getElementById('attr-editor-update-bar');
1047     if (ATTR_EDITOR_IN_UPDATE_MODE) {
1048         update_bar.style.display='table-row';
1049         create_bar.style.display='none';
1050         // hide the dropdown-button
1051         idStyle('vl-create-attr-editor-button', 'visibility', 'hidden');
1052     } else {
1053         dijit.byId('attr-editor-dialog').reset();
1054         create_bar.style.display='table-row';
1055         update_bar.style.display='none';
1056     }
1057 }
1058
1059 function onAttrEditorClose() {
1060     // reset the form to a "create" form. (We may have borrowed it for editing.)
1061     ATTR_EDITOR_IN_UPDATE_MODE = false;
1062     // show the dropdown-button
1063     idStyle('vl-create-attr-editor-button', 'visibility', 'visible');
1064 }
1065
1066 function loadAttrEditorGrid() {
1067     var _data = (ATTR_EDIT_GROUP == 'auth') ? 
1068         vqarad.toStoreData(authAttrDefs) : vqbrad.toStoreData(bibAttrDefs) ;
1069
1070     var store = new dojo.data.ItemFileReadStore({data:_data});
1071     attrEditorGrid.setStore(store);
1072     dojo.connect(attrEditorGrid, 'onRowDblClick', onAttrEditorClick);
1073     attrEditorGrid.update();
1074 }
1075
1076 function attrGridGetTag(n, item) {
1077     // grid helper: return the tags from the row's xpath column.
1078     return item && xpathParser.parse(this.grid.store.getValue(item, 'xpath')).tags;
1079 }
1080
1081 function attrGridGetSubfield(n, item) {
1082     // grid helper: return the subfields from the row's xpath column.
1083     return item && xpathParser.parse(this.grid.store.getValue(item, 'xpath')).subfields;
1084 }
1085
1086 function onAttrEditorClick() {
1087     var row = this.getItem(this.focus.rowIndex);
1088     ATTR_EDIT_ID = this.store.getValue(row, 'id');
1089     ATTR_EDITOR_IN_UPDATE_MODE = true;
1090
1091     // populate the popup editor.
1092     dijit.byId('attr-editor-code').attr('value', this.store.getValue(row, 'code'));
1093     dijit.byId('attr-editor-description').attr('value', this.store.getValue(row, 'description'));
1094     var parsed_xpath = xpathParser.parse(this.store.getValue(row, 'xpath'));
1095     dijit.byId('attr-editor-tags').attr('value', parsed_xpath.tags);
1096     dijit.byId('attr-editor-subfields').attr('value', parsed_xpath.subfields);
1097     dijit.byId('attr-editor-identifier').attr('value', this.store.getValue(row, 'ident'));
1098     dijit.byId('attr-editor-xpath').attr('value', this.store.getValue(row, 'xpath'));
1099     dijit.byId('attr-editor-remove').attr('value', this.store.getValue(row, 'remove'));
1100
1101     // set up UI for editing
1102     dojo.byId('vl-create-attr-editor-button').click();
1103 }
1104
1105 function vlSaveAttrDefinition(data) {
1106     idHide('vl-attr-editor-div');
1107     idShow('vl-generic-progress');
1108
1109     data.id = ATTR_EDIT_ID;
1110
1111     // this ought to honour custom xpaths, but overwrite xpaths
1112     // derived from tags/subfields.
1113     if (data.xpath == '' || looksLikeDerivedXpath(data.xpath)) {
1114         var _xpath = tagAndSubFieldsToXpath(data.tag, data.subfield);
1115         data.xpath = _xpath;
1116     }
1117
1118     // build up our permacrud params. Key variables here are
1119     // "create or update" and "bib or auth".
1120
1121     var isAuth   = (ATTR_EDIT_GROUP == 'auth');
1122     var isCreate = (ATTR_EDIT_ID == null);
1123     var rad      = isAuth ? new vqarad() : new vqbrad() ;
1124     var method   = 'open-ils.permacrud' + (isCreate ? '.create.' : '.update.') 
1125         + (isAuth ? 'vqarad' : 'vqbrad');
1126     var _data    = rad.fromStoreItem(data);
1127
1128     _data.ischanged(1);
1129
1130     fieldmapper.standardRequest(
1131         ['open-ils.permacrud', method],
1132         {   async: true,
1133             params: [authtoken, _data ],
1134             onresponse: function(r) { },
1135             oncomplete: function(r) {
1136                 attrEditorFetchAttrDefs(vlShowAttrEditor);
1137                 ATTR_EDIT_ID = null;
1138             },
1139             onerror: function(r) {
1140                 alert('vlSaveAttrDefinition comms error: ' + r);
1141             }
1142         }
1143     );
1144 }
1145
1146 function attrEditorFetchAttrDefs(callback) {
1147     var fn = (ATTR_EDIT_GROUP == 'auth') ? vlFetchAuthAttrDefs : vlFetchBibAttrDefs;
1148     return fn(callback);
1149 }
1150
1151 function vlAttrDelete() {
1152     idHide('vl-attr-editor-div');
1153     idShow('vl-generic-progress');
1154
1155     var isAuth = (ATTR_EDIT_GROUP == 'auth');
1156     var method = 'open-ils.permacrud.delete.' + (isAuth ? 'vqarad' : 'vqbrad');
1157     var rad    = isAuth ? new vqarad() : new vqbrad() ;
1158     fieldmapper.standardRequest(
1159         ['open-ils.permacrud', method],
1160         {   async: true,
1161             params: [authtoken, rad.fromHash({ id : ATTR_EDIT_ID }), ],
1162             oncomplete: function() {
1163                 dijit.byId('attr-editor-dialog').onCancel(); // close the dialog
1164                 attrEditorFetchAttrDefs(vlShowAttrEditor);
1165                 ATTR_EDIT_ID = null;
1166             },
1167             onerror: function(r) {
1168                 alert('vlAttrDelete comms error: ' + r);
1169             }
1170         }
1171     );
1172 }
1173
1174 // ------------------------------------------------------------
1175 // utilities for attribute editors
1176
1177 // dom utilities (maybe dojo does these, and these should be replaced)
1178
1179 function idStyle(obId, k, v)    { document.getElementById(obId).style[k] = v;   }
1180 function idShow(obId)           { idStyle(obId, 'display', 'block');            }
1181 function idHide(obId)           { idStyle(obId, 'display' , 'none');            }
1182
1183 function connectTooltip(fieldId) {
1184     // Given an element id, look up a tooltip element in the doc (same
1185     // id with a '-tip' suffix) and associate the two. Maybe dojo has
1186     // a better way to do this?
1187     var fld = dojo.byId(fieldId);
1188     var tip = dojo.byId(fieldId + '-tip');
1189     dojo.connect(fld, 'onfocus', function(evt) {
1190                      dijit.showTooltip(tip.innerHTML, fld, ['below', 'after']); });
1191     dojo.connect(fld, 'onblur', function(evt) { dijit.hideTooltip(fld); });
1192 }
1193
1194 // xpath utilities
1195
1196 var xpathParser = new openils.MarcXPathParser();
1197
1198 function tagAndSubFieldsToXpath(tags, subfields) {
1199     // given tags, and subfields, build up an XPath.
1200     try {
1201         var parts = {
1202             'tags':tags.match(/[\d]+/g), 
1203             'subfields':subfields.match(/[a-zA-z]/g) };
1204         return xpathParser.compile(parts);
1205     } catch (err) {
1206         return {'parts':null, 'tags':null, 'error':err};
1207     }
1208 }
1209
1210 function looksLikeDerivedXpath(path) {
1211     // Does this path look like it was derived from tags and subfields?
1212     var parsed = xpathParser.parse(path);
1213     if (parsed.tags == null) 
1214         return false;
1215     var compiled = xpathParser.compile(parsed);
1216     return (path == compiled);
1217 }
1218
1219 // amazing xpath-util unit-tests
1220 if (!looksLikeDerivedXpath('//*[@tag="901"]/*[@code="c"]'))     alert('vandelay xpath-utility error');
1221 if ( looksLikeDerivedXpath('ba-boo-ba-boo!'))                   alert('vandelay xpath-utility error');