


// override browser error reporting
window.onerror = function(error, file, line){
  if (window.Acat && Acat.jq){
    if (typeof error == "object"){
      error = Acat.array.var_dump(error, 'html', 2, null);
    }
    Acat.jq.logStringArr.push([Acat.date.timestamp(), ': --- ERROR: ', error, ' (', file, ': ', line, ')'].join(''));
    Acat.jq.errorHandler();
  }
  
  return false;
};


if (!window.Acat){
  Acat = {};
}


// if (!Acat.jq){
  // Acat.jq = {};
// }



Acat.jq = function () { //}   <-- this commented closing brace is necessary for bug in Notepad++ FunctionList plugin


  var
    aa = Acat.array,
    am = Acat.math,
    as = Acat.string,
    ad = Acat.date,
    dp = Date.parse,
    dpExact = Date.parseExact,
    
    logCount = 1,
    
    logStringArr = [],
  
    forms = [],
    permissions = [],
    user = {},
    
    deploy, //true,
    
    mouseDown,
    
    // regex strings for validation
    validationDefault = {
      standard: '^[\\s\\S]{0,}$', // zero or more characters of any type, including newlines (. doesn't matches newlines)
      required: '^[\\s\\S]{1,}$' // one or more characters of any type, including newlines (. doesn't matches newlines)
    },
    
    validating = false,
    
    debugLog = '',
    
    shared = {
      systemStart: '2008-07-01',
      nullElement: $([]), //$('#NULL'),
      metaLoaded: false,
      serverUTCOffset: 0,
      clientUTCOffset: 0, //clientUTCOffset(),
      inputFocused: '',
      //physicistList: [],
      physicistsById: [],
      periods: [],
      lastJSON: {},
      //vars: {},
      geo: {
        locations: [
          {
            lat: 40.512662,
            lng: -74.858854,
            name: 'Bio-Med Associates',
            address: '4 Main Street, Flemington, NJ 08822'
          }
        ],
        loaded: false
      },
      timers: {
        active: [],
        timeoutIdle: 300,
        start: function(timer){
          if (timer && shared.timers[timer]){
            $('body').everyTime([shared.timers[timer].timeout, 's'].join(''), timer, shared.timers[timer].callback); 
          }
          else {
            fblog('bad timer: ' + timer);
          }
        },
        
        billingProgressPaperless: {
          callback: function(){
            billingGetProgressPaperless();
          },
          timeout: 5 // short timeout, but only causes significant overhead when there are items in progress, which should be rarely
        },
        
        itemSaveDirty: {
          callback: function(){
            itemSaveDirty();
          },
          timeout: 60
        },
        
        scheduleGetUpdated: {
          callback: function(){
            scheduleGetUpdated();
          },
          timeout: 120
        },
          
        scheduleSaveDirty: {
          callback: function(){
            scheduleSaveDirty();
          },
          timeout: 60
        },
        
        staffGetUpdated: {
          callback: function(){
            staffGetUpdated();
          },
          timeout: 120
        }
        
      },
      performance: {
        timeoutPing: 300
      },
      items: {
        // active: {},
        templates: {
        
          address: [
            {
              name: 'address1',
              label: 'Street address',
              required: true,
              capitalize: true,
              // prefillVal: clients.editing.data.address1 || '',
              width: 'full'
            },
            {
              type: 'eol'
            },
            {
              name: 'address2',
              label: 'Box / suite / floor',
              capitalize: true,
              // prefillVal: clients.editing.data.address2 || '',
              width: 'full'
            },
            {
              type: 'eol'
            },
            {
              name: 'address3',
              label: 'Other address information',
              capitalize: true,
              // prefillVal: clients.editing.data.address3 || '',
              width: 'full'
            },
            {
              type: 'eol'
            },
            {
              name: 'city',
              label: 'City',
              required: true,
              capitalize: true,
              // prefillVal: clients.editing.data.city || '',
              width: 'half'
            },
            {
              name: 'state',
              label: 'State',
              required: true,
              capitalize: 'upper',
              // prefillVal: clients.editing.data.state || '',
              width: 'sixth'
            },
            {
              name: 'zip',
              label: 'Zip code',
              // prefillVal: clients.editing.data.zip || '',
              width: 'third'
            }
          ],
          
          contact: [
            {
              name: 'name',
              label: 'Name',
              required: true,
              capitalize: true,
              width: 'full',
              get: function(data){
                if (!data){
                  return false;
                }
                
                // combine name parts 
                var newVal = [];
                
                as.pushIf(newVal, [
                  data.name_prefix,
                  data.name_first,
                  data.name_middle,
                  data.name_last
                ]);
                
                newVal = newVal.join(' ');
                
                if (data.name_suffix){
                  newVal = [newVal, ', ', data.name_suffix].join('');
                }
                
                return newVal;
              },
              set: function(data, value){
                // parse passed value as name, e.g. "Dr. John Q. Smith-Jones, MD"
                // var parts = /(?:([\w\-]+\.?)\s+)?(?:([\w\-]+\.?)\s+)?(?:([\w\-]+\.?)\s+)?([\w\-]+\.?)(?:,\s+?([\w\s\-\.,]+))/.exec(value);
                // var parts = /(?:([\w\-']{2,}\.)\s+)?(?:([\w\-]{1}\.|[\w\-']+)\s+)?(?:([\w\-]{1}\.|[\w\-']+)\s+)?([\w\-']+\.?)(?:,\s*([\w\s\-\.,]+))?/.exec(value);
                var parts = /(?:([\w\-']{2,}\.)\s+)?([\w\-]{1}\.|[\w\-']+)(?:\s+)?(?:([\w\-]{1}\.|[\w\-']+)\s+)?(?:([\w\-']+\.?))?(?:,\s*([\w\s\-\.,]+))?/.exec(value);
                
                if (!data){
                  data = {};
                }
                
                if (parts && parts.length){
                  // if only prefix and last name passed
                  // regex (incorrectly) recognizes this as prefix and first name, so move parts[2] to parts[4]
                  if (parts[1] && parts[2] && !parts[3] && !parts[4]){
                    parts[4] = parts[2];
                    parts[2] = '';
                  }
                  
                  data.name_prefix = parts[1] || '';
                  data.name_first = parts[2] || '';
                  data.name_middle = parts[3] || '';
                  data.name_last = parts[4] || '';
                  data.name_suffix = parts[5] || '';
                }
                else {
                  fblog('unable to match name entry: ' + value);
                  return false;
                }
                
                return data;
              }
            },
            {
              name: 'vip',
              label: 'VIP',
              type: 'dropdown',
              dataType: 'numeric',
              //editPermissions: ['accounting', 'schedule-admin'],
              //displayCondition: scheduleEditNotInternal,
              required: true,
              width: 'third',
              style: 'itemRight',
              helpIcon: 'VIPs are highlighted in the contact list, and will receive priority treatment by office staff',
              values: [
                {id: 0, name: 'No'},
                {id: 1, name: 'Yes'}
              ],
              defaultValue: 0,
              // get: function(data){
                // return Number(data.vip);
              // },
              prefillVal: function(){
                return shared.items.active.data.vip || 0;
              },
              selectExclusive: true, //[0, 1],
              selectCallback: itemEditOnChange
            },
            // {
              // type: 'eol'
            // },
            {
              name: 'title',
              label: 'Title',
              required: true,
              // prefillVal: clients.editing.data.title || '',
              capitalize: true
            },
            {
              name: 'department',
              label: 'Department',
              required: true,
              // prefillVal: clients.editing.data.department || '',
              capitalize: true
            },
            {
              name: 'account_contact',
              label: 'Account contact',
              type: 'dropdown',
              dataType: 'numeric',
              //editPermissions: ['accounting', 'schedule-admin'],
              displayCondition: function(){
                return shared.items.active.store.num_locations > 1;
              },
              required: true,
              width: 'third',
              style: 'itemRight',
              helpIcon: 'Account contacts are shown under every location on this account',
              values: [
                {id: 0, name: 'No'},
                {id: 1, name: 'Yes'}
              ],
              defaultValue: 0,
              // get: function(data){
                // return Number(data.vip);
              // },
              prefillVal: function(){
                return shared.items.active.data.account_contact || 0;
              },
              selectExclusive: true, //[0, 1],
              selectCallback: itemEditOnChange
            },
            {
              type: 'eol',
              divider: true
            },
            {
              name: 'phone',
              // prefillVal: clients.editing.data.phone || '',
              label: 'Phone'
            },
            {
              name: 'phone2',
              // prefillVal: clients.editing.data.phone2 || '',
              label: 'Alternate phone'
            },
            {
              name: 'pager',
              // prefillVal: clients.editing.data.pager || '',
              label: 'Pager'
            },
            {
              name: 'phone_cell',
              // prefillVal: clients.editing.data.phone_cell || '',
              label: 'Cell phone'
            },
            {
              name: 'fax',
              // prefillVal: clients.editing.data.fax || '',
              label: 'Fax'
            },
            {
              name: 'fax2',
              // prefillVal: clients.editing.data.fax2 || '',
              label: 'Alternate fax'
            },
            {
              type: 'eol',
              divider: true
            },
            {
              name: 'email',
              // prefillVal: clients.editing.data.email || '',
              label: 'Email'
            },
            {
              name: 'email2',
              // prefillVal: clients.editing.data.email2 || '',
              label: 'Alternate email'
            }
          ],
          
          taskItem: [
            // {
              // name: 'local_file',
              // type: 'static',
              // label: 'File'
            // },
            // {
              // type: 'eol'
            // },
            {
              name: 'process_send',
              label: 'Instructions',
              type: 'dropdown',
              width: 'full',
              required: true,
              values: [
                { id: 0, name: 'Send to client' },
                { id: 1, name: 'Send to physicist with stamped mailer' },
                { id: 2, name: 'Send to physicist for hand delivery' },
                { id: 3, name: 'Archive only' },
                { id: 4, name: 'Special (please describe)' }
              ],
              selectExclusive: true,
              prefillVal: function(){
                return shared.items.active.data.process_send || 0;
              }
              // selectCallback: scheduleOnTaskEditChange,
            },
            {
              type: 'eol'
            },
            {
              name: 'recurrence',
              label: 'Recurrence',
              type: 'dropdown',
              required: true,
              values: function(){
                return shared.taskrecurrences;
              },
              selectExclusive: true,
              //selectCallback: scheduleOnTaskEditChange,
              prefillVal: function(){
                return shared.items.active.data.recurrence || 2;  // default to "annual"
              },
              help: 'If and when a followup visit is required, you will receive a reminder on the Staff Home page and via email.'
            },
            {
              name: 'recurrenceNotes',
              label: 'Description of recurrence',
              required: true,
              displayCondition: function(){
                return (shared.items.active.data.recurrence == 6);
              },
              capitalize: 'sentence',
              width: 'full',
              //whenValidCallback: scheduleOnTaskEditChange,
              prefillVal: function(){
                return shared.items.active.data.recurrenceNotes || 'test';
              },
              help: 'NOTE: This is only for reference - you won\'t receive an automated reminder.'
            },
            {
              type: 'eol'
            },
            {
              name: 'server_file',
              type: 'static',
              displayCondition: function(){ // closure to evaluate on each call
                  return (userHasPermission('office') && (shared.items.active.data.server_file != shared.items.active.data.local_file));
              },
              label: 'File name saved on server (renamed to avoid conflict with existing file)'
            },
            {
              type: 'eol'
            },
            {
              name: 'server_folder',
              type: 'static',
              displayCondition: function(){
                return userHasPermission('office');
              },
              label: 'Incoming folder name on server'
            }
          ]
          
        }
      },
      activeInterface: 'test',
      domWip: []
    },
    
    staff = {
      gettingUpdates: false,
      initIdle: true,
      performance: {
        timeoutGetUpdated: 120,
        timeoutSavePreferences: 2,
        timeoutIdle: 300
      },
      preferences: {},
      vacationWarningThreshold: {
        low: -1,
        out: 0
      }
      
    },
    
    schedule = {
      activeEdit: {},
      start: {},
      end: {},
      today: '',
      config: {},
      precache: {
        inProgress: false,
        cancel: false,
        complete: false,
        offset: 0,
        maxOffset: 0, //5
        start: '',
        end: ''
      },
      
      autoUserName: 'Automated Scheduler',
      
      initIdle: true,
      
      // settings that affect schedule rendering speed and responsiveness
      performance: {
      
        // seconds
        timeoutSaveDirty: 60,
        timeoutSavePreferences: 2,
        timeoutGetUpdated: 120,
        timeoutPrecache: 15,
        timeoutIdle: 300,
        
        // milliseconds
        renderDelay: 1,
        renderDelayPrecache: 50,
        timeoutNavScroll: 50,
        
        // items processed per pass
        renderBulk: 20,
        renderBulkPrecache: 5
      },
      
      buttonNext: null,
      buttonPrevious: null,
      blockPrecache: false,
      blockInput: false,
      gettingUpdates: false,
      pages: [],
      currentPage: null,
      currentPageElement: [],
      maxPage: 0,
      retrieving: false,
      rendering: false,
      renderWait: false,
      cancelRendering: false,
      selected: null,
      selectedRow: null,
      selectAfterLoad: false,
      selectLastTimeblock: 'morning',
      inputInProgress: false,
      weekStartsOn: 'sunday',
      headerInterval: 5, // number of data rows between header rows
      highlightRecentRange: 3,
      highlightTasks: [],
      // highlightRecentOptions: [
        // {
          // range: false,
          // option: 'disabled',
          // description: ''
        // },
        // {
          // range: 'session',
          // option: 'since previous session',
          // description: 'Changes made <b>since your previous session</b> are <span class="backgroundAttention highlightAttention">highlighted</span>',
        // },
        // {
          // range: 1,
          // option: 'from the past day',
          // description: 'Changes made <b>in the past day</b> are <span class="backgroundAttention highlightAttention">highlighted</span>'
        // },
        // {
          // text: 'Changes made <b>in the past two days</b> are <span class="backgroundAttention highlightAttention">highlighted</span>'
        // }
      // ],
      multiselect: {
        options: {}
      },
      forceTypes: [
        {
          location: 232, // Bryn Mawr
          allowed: [1,2,4,5,6,8,9,10]
        },
        {
          location: 75, // CHS Hamilton
          allowed: [] // any type allowed, but MUST pick a type
        },
        {
          location: 36, // CHS Mercer
          constraints: [ // default tasktype for these conditions
            {
              user: [103,104,106,107,108,109,111,112,114,116,117,118,119], // DR physicists
              tasktype: 4
            }
          ]
        },
        {
          location: 131, // Clara Maass
          allowed: [] // any type allowed, but MUST pick a type
        },
        {
          location: 253, // Greenwich Hospital
          allowed: [],
          constraints: [
            {
              user: 114, // Mike
              tasktype: 1 // Nuc Med
            },
            {
              user: 117, // Terry
              tasktype: 4 // Diagnostic Rad
            }
          ]
        },
        {
          location: 820, // Greenwich Imaging Center
          allowed: [],
          constraints: [
            {
              user: 114, // Mike
              tasktype: 1 // Nuc Med
            },
            {
              user: 117, // Terry
              tasktype: 4 // Diagnostic Rad
            }
          ]
        },
        {
          location: 32, // Lankenau
          // allowed: [1,2,4,5,8,9,10] // only one of these types allowed (only these are shown)
          allowed: [1,2,4,5,6,7,8,9] // only one of these types allowed (only these are shown)
        },
        {
          location: 826, // Paoli
          allowed: [1,2,4,5,6,8,9,10]
        },
        {
          location: 25, // RWJ Ham
          allowed: [3,4,5,10],
          constraints: [
            {
              user: [105,115], // Bruce & Matt: therapy
              tasktype: 3
            },
            {
              user: [103,104,106,107,108,109,111,112,114,116,117,118,119], // DR physicists
              tasktype: 4
            }
          ]
        },
        {
          location: 828, // Princeton Rad Jamesburg
          allowed: [] // any type allowed, but MUST pick a type
        },
        {
          location: 829, // Princeton Rad Harrison
          allowed: [] // any type allowed, but MUST pick a type
        },
        {
          location: 836, // Hillsborough Rad
          allowed: [] // any type allowed, but MUST pick a type
        }
        // {
          // location: 640, // Alliance Imaging
          // allowed: [9,19] // only MRI or PET/CT
        // }
        
      ],
      yPos: 0,
      preferences: {
        display: {
          condensed: false,
          maximized: false,
          physicists: -1,
          recentHighlight: false,
          weekends: false,
          view: 'week' // 'month'
        }
      },
      visibleRows: [],
      restrictions: {
        minBillableHalfDay: 3,
        minBillableFullDay: 6
      }
      
    },
    
    billing = {
      start: {},
      end: {},
      accounts: [],
      selected: [],
      groupMarkers: {
        0: 'A',
        1: 'B',
        2: 'C',
        3: 'D',
        4: 'E',
        5: 'F',
        6: 'G',
        7: 'H',
        8: 'K',
        9: 'M',
        10: 'N',
        11: 'P',
        12: 'Q',
        13: 'R',
        14: 'S',
        15: 'T',
        16: 'U',
        17: 'V',
        18: 'W',
        19: 'X',
        20: 'Y',
        21: 'Z'
      },
      totalCost: 0,
      totalAccounts: 0,
      paperless: {
        inProgress: {
          accounts: [],
          periods: [],
          invoices: []
        }
      },
      
      paperlessStatus: [],
      
      initIdle: false,
      
      performance: {
        timeoutIdle: 300,
        timeoutGetProgressPaperless: 5 // short timeout, but only causes significant overhead when there are items in progress, which should be rarely
      }
    },
    
    clients = {
      active: {
        account: null,
        location: null
      },
      initIdle: false,
      performance: {
      
        // seconds
        timeoutSaveDirty: 60,
        // timeoutSavePreferences: 2,
        // timeoutGetUpdated: 120,
        timeoutIdle: 300
        
      },
      dirty: [],
      searchPreparse: {
        '/': ' ',
        '\\-': ' ',
        ',': ' ',
        '\\.': ' ',
        '\\(': ' ',
        '\\)': ' ',
        '\\[': ' ',
        '\\]': ' ',
        '&': ' ',
        ' and ': ' ',
        ' of ': ' ',
        '^the ': ' ',
        '^a ': ' ',
        "'": ' ',
        "\\'": ' ',
        '"': ' ',
        '  ': ' '
      },
      searchIgnored: [
        '/',
        '\\-',
        ',',
        '\\.',
        // '\\(',
        // '\\)',
        // '\\[',
        // '\\]',
        '\''
        // '"'
      ],
      billingLoaded: false
      
    },
    
    controls = {
      up: {keyCode: 38},
      down: {keyCode: 40},
      left: {keyCode: 37},
      right: {keyCode: 39},
      tab: {keyCode: 9},
      enter: {keyCode: 13},
      esc: {keyCode: 27},
      pageup: {keyCode: 33},
      pagedown: {keyCode: 34},
      insert: {keyCode: 45},
      shift: {keyCode: 16},
      ctrl: {keyCode: 17},
      alt: {keyCode: 18},
      f4: {keyCode: 115},
      f8: {keyCode: 119}
    };











  function timerGetName(args){
    var timerName;
    
    if (args.callee){
      var t1 = args.callee.toString();
      var pos = t1.indexOf(')');
      if (pos != -1){
        timerName = t1.substr(0, pos + 1).replace('function ', '').replace('function', '').replace(/\(.*\)/, '()');
      }
    }
    else if (String(args) === args) {
      timerName = args;
    }
    
    return timerName;
  }

  // start a stopwatch
  function timerStart(args){
    var timerName = timerGetName(args);
    
    if (timerName){
      ad.stopwatchStart(timerName);
    }
  }
  
  // end a stopwatch and output elapsed time
  function timerEnd(args, note){
    var timerName = timerGetName(args);
    
    if (timerName){
      fblog([timerName, note ? note : '', ': ',  (ad.stopwatchEnd(timerName) / 1000), 's'].join(''));
    }
  }

    
  function fblog(str, raw){
    // if (!(str instanceof String)){
      // str = String(str);
    // }
    
    if (raw){
      str = as.htmlEncode(str);
    }
    
    str = [timeMSString(), ': ', str].join('');
    
    logStringArr.push(str);

    if (!deploy){
      
      // var regex = /^([^(\s)]+\(\):)/;
      // var logEl = $('#log');
      // var content = '';
      
      // if (!logEl.length){
      // }
    
      // if (raw){
        // content = as.htmlEncode(str);
      // }
      // else {
        // content = str.replace(regex, '<span class="logFuncName">$1</span>(<span class="logFuncArgs">$2</span>)<span class="logFuncSection">$3</span>');
      // }
      
      // $('#log').append(['<tr><td class="lineNum">', logCount, ':</td><td>', content, '</td></tr>'].join(''));
      // logCount++;
      pageLogRefresh();
      
      Acat.fire.log(str);
    }
    
    return str;
  }
  
  // log object state
  // if forceLog == true:
  //     obj should be a string naming the obj, which will be eval'ed to traverse the actual object
  //     if passing a local object (or one whose scope is inaccessible to the Acat.jq namespace), pass in a reference to the actual object as scopedObj
  function fbdir(obj, forceLog, scopedObj){
    //logStringArr.push(Acat.array.var_dump(obj, 'html', 4));
    
    if (!deploy){
      var time = timeMSString();
      Acat.fire.log([time, ': ', (String(obj) === obj ? obj : '<unnamed>'), ' (Object) = ...'].join(''));
      Acat.fire.dir(scopedObj || eval(obj));
      
      if (forceLog){
        var objName = obj;
        obj = scopedObj || eval(obj);
        logStringArr.push([time, ': ', Acat.array.var_dump(obj, 'html', 2, null, objName)].join(''));
        pageLogRefresh();
        // $('#log').append(['<tr><td class="lineNum">', logCount++, ':</td><td>', Acat.array.var_dump(obj, 'html', 4), '</td></tr>'].join(''));
      }
    }    
  }

  function pageLogRefresh(){
    // var regex = /^([^(\s)]+)\((.*?)\)(.*?:)/;
    // var regex = /^([^\s]+\: )((([^(\s)]+)((\()([^\(\)\:]*)(\)))?(.*:))|(.+))(.*)/;
    // var regex = /^([^\s]+\: )((([^\s]+)((\()([^\(\)\:]*)(\)))?(.*:))|(.+))(.*)/;
    var regex = /^([\d\:\.]*)\s*([\w\.\s\/]+)(\([^\)]*\)[\s]*)?([\:\=])?(.*)/; //((([^\s]+)((\()([^\(\)\:]*)(\)))?(.*:))|(.+))(.*)/;
    var regexError = /[^\s]+\: ---/;
    var i;
    var outputLine;
    var outputArr = ['<table id="log" cellspacing="0" cellpadding="0">'];

    for (i = 0; i < logStringArr.length; i++){
      if (logStringArr[i].match(regexError)){
        outputLine = ['<span class="highlightWarning">', logStringArr[i], '</span>'].join('');
      }
      else {
        // outputLine = logStringArr[i].replace(regex, '<span class="lighter">$1</span><span class="logFuncName">$4$6</span><span class="logFuncArgs">$7</span><span class="logFuncName">$8$9</span>$10$11');
        outputLine = logStringArr[i].replace(regex, '<span class="lighter">$1</span> <span class="logFuncName">$2</span><span class="light">$3</span><span class="light">$4</span>$5');
      }
      outputArr.push('<tr><td class="lineNum">', i+1, ':</td><td>', outputLine, '</td></tr>');
    }
    
    outputArr.push('</table>');
    
    document.getElementById('logContainer').innerHTML = outputArr.join('');
    // $('#logContainer')[0].innerHTML = outputArr.join('');
  }
  
  
  
  
  
  function dateTimeString(dateObj){
    if (dateObj == undefined){
      dateObj = new Date();
    }
    return dateObj.toString('yyyy-MM-dd HH:mm:ss');
  }
  function dateString(dateObj){
    if (dateObj == undefined){
      dateObj = new Date();
    }
    return dateObj.toString('yyyy-MM-dd');
  }
  function dateTimeMSString(dateObj){
    if (dateObj == undefined){
      dateObj = new Date();
    }
    return dateObj.toString('yyyy-MM-dd HH:mm:ss.fff');
  }
  function timeMSString(dateObj){
    if (dateObj == undefined){
      dateObj = new Date();
    }
    return dateObj.toString('HH:mm:ss.fff');
  }
  function dateTimeFriendlyString(dateObj){
    if (dateObj == undefined){
      dateObj = new Date();
    }
    return dateObj.toString('MMMM dS @ h:mmt');
  }
  function dateFriendlyString(dateObj){
    if (dateObj == undefined){
      dateObj = new Date();
    }
    return dateObj.toString('MMMM d, yyyy');
  }
  function dateAPFriendlyShortString(dateObj){
    if (dateObj == undefined){
      dateObj = new Date();
    }
    return dateObj.toString('M/d/yy tt');
  }
  function dateAPFriendlyShortNoYearString(dateObj){
    if (dateObj == undefined){
      dateObj = new Date();
    }
    return dateObj.toString('M/d tt');
  }
  
  function dpExactDate(dateString){
    return dpExact(dateString, 'yyyy-MM-dd');
  }
  function dpExactDateTime(dateTimeString){
    return dpExact(dateTimeString, 'yyyy-MM-dd HH:mm:ss');
  }
  
  
  
  
  
  function dateElapsedMonths(start, end){
    // calculate number of months elapsed since start date
    var elapsed = ((end.toString('yyyy') - start.toString('yyyy')) * 12) + (end.toString('M') - start.toString('M'));
    // only count *whole* months, so subtract a month if today's day of the month is less than start day of the month
    if (Number(end.toString('d')) < Number(start.toString('d'))){
      elapsed--;
    }
    
    return elapsed;
  }
  
  
  
  // determine date beginning the week containing passed date
  function dateGetWeekStart(date){
    var dateSchedule = dpExactDate(date);
    var dateWeekStart;
    
    if (dateSchedule.is().sunday()){
      dateWeekStart = dateSchedule;
    }
    else {
      dateWeekStart = dateSchedule.last().sunday();
    }
    
    return dateString(dateWeekStart);
  }
  
   
  
  
  
  function getAdjacentWeekday(date, offset){
    var
      datewip = dpExactDate(date),
      dir = offset / Math.abs(offset);
    
    datewip.addDays(offset);
    
    while (!datewip.is().weekday()){
      datewip.addDays(dir);
    }
    
    return dateString(datewip);
  }
  
    
    
    
    
  
  // adjust Date object from UTC to local time zone
  function dateFromUTC(date){
    //return new Date(date).setTimezone(Date.today().getTimezone());
    return new Date(date).addMinutes(Date.today().getTimezoneOffset() * 1);
  }
      
  
  
  
  
  function formObject(config){

    // copy config properties to this object instance
    for (var i in config){
      this[i] = config[i];
    }
  
    // set defaults
    this.invalid = [];
    this.blurred = [];
    this.lookups = [];
    this.buttons = [];
    this.toggles = [];
    this.textareas = [];
    this.dropdowns = [];
    this.selects = [];
    this.ajaxes = {};
    //this.editTracks = [];
    this.requiredCount = 0;
    this.submitText = config.submitText || 'Submit';
    this.submitInvalidText = config.submitInvalidText || 'Form not complete';
    this.injectTarget = config.injectTarget || 'jq-form-inject';
    this.addedInjectTarget = config.addedInjectTarget || 'jq-formstatic-inject';

    // make sure all validation regexes are defined
    if (!config.dontValidate){
      this.validation = config.validation || {};
      this.validation = {
        standard: this.validation.standard || '^.{0,}$',
        required: this.validation.required || '^.{1,}$',
        fields: this.validation.fields || {}
      };
    }

  }

  
  
  
  
  
  function scheduleCellFromCoords(coords){
    var cells = shared.nullElement;
    
    if (coords){
      
      if (coords.length){
        for (var i = 0; i < coords.length; i++){
          //var selector = ['#currentPage > tbody > tr[row=', coords[i].row, '][timeblock=', coords[i].timeblock, '] > td[column=', coords[i].column, ']'].join('');
          var selector = ['#task', coords[i].row, coords[i].timeblock, coords[i].column].join('');
          if (!cells || !cells.length){
            cells = $(selector);
          }
          else {
            cells = cells.add(selector);
          }
          // fblog('added cell (count = ' + cells.length + ')');
        }
      }
      else if (coords.row){
        //cells = $(['#currentPage > tbody > tr[row=', coords.row, '][timeblock=', coords.timeblock, '] > td[column=', coords.column, ']'].join(''));
        cells = $(['#task', coords.row, coords.timeblock, coords.column].join(''));
      }
      
    }
    
    return cells;
  }
  
  function scheduleCellGetCoords(cell){
    if (cell && cell.length){
      return {
        row: cell.parent().attr('row'),
        timeblock: cell.parent().attr('timeblock'),
        user: cell.parent().attr('user'),
        column: cell.attr('column')
      };
    }
    else {
      return false;
    }
  }



  // return negative top and left positioning offsets to position passed element centered within parent
  // parent defaults to $(window) if not specified
  function centerOffsets(element, parent){
    if (!parent){
      parent = $(window);
    }
    // fblog('centerOffsets(): top = ' + parent.height() + ' - ' + element.height() + ' / 2');
    return {top: parseInt((parent.height() - element.height()) / 2, 10) * -1, left: parseInt((parent.width() - element.width()) / 2, 10) * -1};
  }
  
  

  
  
  
 
  
  // deselect all cells currently in schedule.selected object
  function scheduleDeselectCurrent(){
    schedule.selected.init(); // empty array of selections
    // var context = $('#currentPage > tbody > tr');
    // $('td.taskselected, td.taskselectedReadonly', context)
      // .removeClass('taskselected taskselectedReadonly');
    // $('th.headerselected, th.headerselectedReadonly', context)
      // .removeClass('headerselected headerselectedReadonly');
  }

  
  
  
  // add passed cell to schedule.selected collection
  // reflect selected status visually
  // passed config should contain one of the following:
  
  // 1) column = date spec ('2008-08-15') of desired column
  //    selects entire column
  
  // 2) row = physicist id of desired row
  //    selects entire row
  
  // 3) all = true/false
  //    selects all cells in table
  
  // 4) origin = {column, row, timeblock} of single cell
  //    direction (optional) = offset one cell from origin one of 'up', 'down', 'left', or 'right'
  
  function scheduleSelectCells(config){
  
  
    schedule.inputInProgress = true;
    
  
    schedule.selectedRow = null;
    
    var
      cells = [],
      cellReadOnly = [];
      
    // // remove header selected indicator
    // $('#currentPage > tbody > tr > th.headerselected, #currentPage > tbody > tr > th.headerselectedReadonly')
      // .removeClass('headerselected headerselectedReadonly');
      

    if (config.column){
      cells = $(['#currentPage > tbody > tr > td.task[column=', config.column, ']'].join(''));
      
      
      // $(['#currentPage > tbody > tr > th[column=', config.column, ']'].join('')).addClass('headerselected');
    }
    else if (config.row){
      if (schedule.preferences.display.weekends){
        cells = $([
          // '#tr', schedule.currentPage, '-', config.row, 'morning > td.task,',
          // '#tr', schedule.currentPage, '-', config.row, 'afternoon > td.task'
          '#tr', config.row, 'morning > td.task,',
          '#tr', config.row, 'afternoon > td.task'
        ].join(''));
      }
      else {
        cells = $([
          // '#tr', schedule.currentPage, '-', config.row, 'morning > td.task:not(.weekend),',
          // '#tr', schedule.currentPage, '-', config.row, 'afternoon > td.task:not(.weekend)'
          '#tr', config.row, 'morning > td.task:not(.weekend),',
          '#tr', config.row, 'afternoon > td.task:not(.weekend)'
        ].join(''));
      }
      
      // var cellReadOnly = scheduleCellFromCoords(schedule.selected.getReadOnly());      
      
      // $(['#th', schedule.currentPage, '-', config.row].join('')).addClass('headerselected');
      // if (!scheduleEditPermission(sel.user, sel.column)){
      // }
      // else {
        // $(['#th', config.row].join('')).addClass('headerselected');
      // }
      schedule.selectedRow = config.row;
    }
    else if (config.all){
      cells = $('#currentPage > tbody > tr > td.task');
    }
    
    else if (config.origin){
    
    
      var
        dateIndex,
        date = '',
        dateOffset = 0,
        dateDirection,
        page = scheduleGetPageById(schedule.currentPage),
        dates,
        columnIndex,
        week,
        weekIndex,
        start,
        end;
        
      if (page.view == 'month'){
        if (config.afterLoad){
          weekIndex = (config.origin.row > page.weeks.length - 1) ? (page.weeks.length - 1) : config.origin.row;
          config.origin.row = $('tr.timeblockRowFirst', page.element).eq(weekIndex).attr('row');
        }
        else {
          weekIndex = aa.indexOfObjectByKey(page.weeks, config.origin.row.substr(config.origin.row.length - 10), 'start')[0];
        }
        week = page.weeks[weekIndex] || page.weeks[0];
        dates = week.dates;
        columnIndex = aa.indexOf(dates, config.origin.column);
        start = week.start;
        end = week.end;
      }
      else {
        dates = schedule.dates;
        start = schedule.start;
        end = schedule.end;
      }
        
      if (config.direction){
    
        var row, user, timeblock, query;
        
        // one cell to right of origin
        if (config.direction == 'right'){
    
          if (!schedule.preferences.display.weekends){
            dateOffset = 1;
          }
          dateDirection = 1;
          
          dateIndex = aa.indexOf(dates, config.origin.column);
        
          if (
            (
              // date not at end of dates already retrieved
              (dateIndex < dates.length - 1)
              
              &&
                
              // date not at end of same week
              dates[dateIndex + dateOffset] < end
            )
          ){
            //fblog('same week, moving to ' + schedule.dates[dateIndex + dateDirection]);
            date = dates[dateIndex + dateDirection];
          }
          // next week
          else {
            // schedule.config = {
              // start: dateString(dpExact(schedule.start, 'yyyy-MM-dd').addWeeks(dateDirection)),
              // target: 'jq-formstatic-inject',
              // relationship: 'next'
            // };
          
            schedule.selectAfterLoad = true;
            mouseDown.column = 'first';
            if (page.view == 'month'){
              mouseDown.weekIndex = weekIndex;
            }
            scheduleGet('next');
            schedule.inputInProgress = false;
            return;
          }

          //cells = $(['#currentPage > tbody > tr[user=', config.origin.user, '][timeblock=', config.origin.timeblock, '] > td.task[column=', date, ']'].join(''));
          cells = $(['#task', config.origin.user, '-', start, config.origin.timeblock, date].join(''));
          if (!cells.length){
            //cells = $(['#currentPage > tbody > tr[user=', config.origin.user, '][timeblock=', (config.origin.timeblock == 'morning' ? 'afternoon' : 'morning'), '] > td.task[column=', date, ']'].join(''));
            cells = $(['#task', config.origin.user, '-', start, (config.origin.timeblock == 'morning' ? 'afternoon' : 'morning'), date].join(''));
          }

          if (!cells.length){
            scheduleSelectCells({origin: {user: config.origin.user, row: config.origin.row, timeblock: (config.origin.timeblock == 'morning' ? 'afternoon' : 'morning'), column: date}});
            schedule.inputInProgress = false;
            return;
          }
          

        }


        // one cell to left of origin
        else if (config.direction == 'left'){
    
          if (!schedule.preferences.display.weekends){
            dateOffset = -1;
          }
          dateDirection = -1;
          
          dateIndex = aa.indexOf(dates, config.origin.column);
          
          if (
            // date not at beginning of dates already retrieved
            (dateIndex > 0)
            &&
            // date not at beginning of same week
            dates[dateIndex + dateOffset] > start
          ){
            date = dates[dateIndex + dateDirection];
          }
          // previous week
          else {
            schedule.selectAfterLoad = true;
            mouseDown.column = 'last';
            if (page.view == 'month'){
              mouseDown.weekIndex = weekIndex;
            }
            scheduleGet('previous'); // hh:mm:ss') + ' GMT' + target.getUTCOffset());
            schedule.inputInProgress = false;
            return;
          }

          // cells = $(['#currentPage > tbody > tr[user=', config.origin.user, '][timeblock=', config.origin.timeblock, '] > td.task[column=', date, ']'].join(''));
          cells = $(['#task', config.origin.user, '-', start, config.origin.timeblock, date].join(''));
          if (!cells.length){
            // cells = $(['#currentPage > tbody > tr[user=', config.origin.user, '][timeblock=', (config.origin.timeblock == 'morning' ? 'afternoon' : 'morning'), '] > td.task[column=', date, ']'].join(''));
            cells = $(['#task', config.origin.user, '-', start, (config.origin.timeblock == 'morning' ? 'afternoon' : 'morning'), date].join(''));
          }
          if (!cells.length){
            scheduleSelectCells({origin: {user: config.origin.user, row: config.origin.row, timeblock: (config.origin.timeblock == 'morning' ? 'afternoon' : 'morning'), column: date}});
            schedule.inputInProgress = false;
            return;
          }

        }

          
        else if (config.direction == 'up'){
          
          // if this is an afternoon, just move up to same date morning
          if (config.origin.timeblock == 'afternoon'){
            timeblock = 'morning';
            row = config.origin.row;
            user = config.origin.user;
            // cells = $(['#currentPage > tbody > tr[row=', row, '][timeblock=', timeblock, '] > td.task[column=', config.origin.column, ']'].join(''));
            cells = $(['#task', row, timeblock, config.origin.column].join(''));
          }
          
          if (!cells.length){
            if (page.view == 'month'){
              // return if already at top
              if (weekIndex <= 0){
                schedule.inputInProgress = false;
                return;
              }
              
              // cells = $(['#currentPage tr[timeblock=afternoon] td.task[column=', page.weeks[weekIndex - 1].dates[columnIndex], ']'].join(''));
              cells = $(['#task', config.origin.user, '-', page.weeks[weekIndex - 1].start, 'afternoon', page.weeks[weekIndex - 1].dates[columnIndex]].join(''));
              if (!cells.length){
                // cells = $(['#currentPage tr[timeblock=morning] td.task[column=', page.weeks[weekIndex - 1].dates[columnIndex], ']'].join(''));
                cells = $(['#task', config.origin.user, '-', page.weeks[weekIndex - 1].start, 'morning', page.weeks[weekIndex - 1].dates[columnIndex]].join(''));
              }
            }
            
            else {
              // return if already at top
              // if (!(aa.indexOfObjectByKey(shared.physicists, config.origin.user, 'physicist_id') > 0)){
              if (!(aa.indexOf(schedule.visibleRows, Number(config.origin.user)) > 0)){
                schedule.inputInProgress = false;
                return;
              }
              
              timeblock = 'afternoon';
              
              // row = [shared.physicists[Number(aa.indexOfObjectByKey(shared.physicists, config.origin.user, 'physicist_id')[0]) - 1].physicist_id, '-', config.origin.row.substr(config.origin.row.indexOf('-') + 1)].join('');
              row = [schedule.visibleRows[aa.indexOf(schedule.visibleRows, Number(config.origin.user)) - 1], '-', config.origin.row.substr(config.origin.row.indexOf('-') + 1)].join('');

              // cells = $(['#currentPage > tbody > tr[row=', row, '][timeblock=', timeblock, '] > td.task[column=', config.origin.column, ']'].join(''));
              cells = $(['#task', row, timeblock, config.origin.column].join(''));
              
              if (!cells.length){
                config.origin.row = row;
                // config.origin.user = user;
                config.origin.timeblock = timeblock;
                scheduleSelectCells({origin: config.origin, direction: 'up', onKeydown: config.onKeydown});
                schedule.inputInProgress = false;
                return;
              }
            }
            
          }
          
          
        }
          

        else if (config.direction == 'down'){
          
          if (config.origin.timeblock == 'morning'){
            timeblock = 'afternoon';
            row = config.origin.row;
            user = config.origin.user;
            // cells = $(['#currentPage > tbody > tr[row=', row, '][timeblock=', timeblock, '] > td.task[column=', config.origin.column, ']'].join(''));
            cells = $(['#task', row, timeblock, config.origin.column].join(''));
          }
          
          if (!cells.length){
          
            if (page.view == 'month'){
              // return if already at bottom
              if (weekIndex >= page.weeks.length - 1){
                schedule.inputInProgress = false;
                return;
              }
              
              // cells = $(['#currentPage tr[timeblock=morning] td.task[column=', page.weeks[weekIndex + 1].dates[columnIndex], ']'].join(''));
              cells = $(['#task', config.origin.user, '-', page.weeks[weekIndex + 1].start, 'morning', page.weeks[weekIndex + 1].dates[columnIndex]].join(''));
              // if (!cells.length){
                // // cells = $(['#currentPage tr[timeblock=morning] td.task[column=', page.weeks[weekIndex - 1].dates[columnIndex], ']'].join(''));
                // cells = $(['#task', user, '-', page.weeks[weekIndex + 1].start, 'afternoon', page.weeks[weekIndex + 1].dates[columnIndex]].join(''));
              // }
            }
            else {
              // return if already at bottom
              // if (!(aa.indexOfObjectByKey(shared.physicists, config.origin.user, 'physicist_id') < shared.physicists.length - 1)){
              if (!(aa.indexOf(schedule.visibleRows, Number(config.origin.user)) < schedule.visibleRows.length - 1)){
                schedule.inputInProgress = false;
                return;
              }
              timeblock = 'morning';
              // row = [shared.physicists[Number(aa.indexOfObjectByKey(shared.physicists, config.origin.user, 'physicist_id')[0]) + 1].physicist_id, '-', config.origin.row.substr(config.origin.row.indexOf('-') + 1)].join('');
              row = [schedule.visibleRows[aa.indexOf(schedule.visibleRows, Number(config.origin.user)) + 1], '-', config.origin.row.substr(config.origin.row.indexOf('-') + 1)].join('');
            
              // cells = $(['#currentPage > tbody > tr[row=', row, '][timeblock=', timeblock, '] > td.task[column=', config.origin.column, ']'].join(''));
              cells = $(['#task', row, timeblock, config.origin.column].join(''));

              if (!cells.length){
                config.origin.row = row;
                // config.origin.user = user;
                config.origin.timeblock = timeblock;
                scheduleSelectCells({origin: config.origin, direction: 'down', onKeydown: config.onKeydown});
                schedule.inputInProgress = false;
                return;
              }
            }
            
          }
          
        }

      }
      
      // no offset applied
      else {
        // fblog('scheduleSelectCells(): no direction specified, use origin as cell to select');
        
        if (config.origin.element){
          cells = config.origin.element;
        }
        
        if (!cells.length){
          
          if (config.origin.user && !aa.existsIn(schedule.visibleRows, Number(config.origin.user))){
            return;
          }
        
          if (page.view == 'month'){
            if (config.origin.column == 'first'){
              // cells = $(['#currentPage > tbody > tr[row=', config.origin.row, '][timeblock=morning] > td.task'].join('')).eq(schedule.preferences.display.weekends ? 0 : 1);
              cells = $(['#task', config.origin.row, 'morning', dates[schedule.preferences.display.weekends ? 0 : 1]].join(''));
              // config.origin.column = (schedule.preferences.display.weekends ? schedule.start : schedule.dates[aa.indexOf(schedule.dates, schedule.start) + 1]);
            }
            else if (config.origin.column == 'last'){
              // cells = $(['#currentPage > tbody > tr[row=', config.origin.row, '][timeblock=morning] > td.task'].join('')).eq(schedule.preferences.display.weekends ? 6 : 5);
              cells = $(['#task', config.origin.row, 'morning', dates[schedule.preferences.display.weekends ? 6 : 5]].join(''));
              // config.origin.column = (schedule.preferences.display.weekends ? schedule.end : schedule.dates[aa.indexOf(schedule.dates, schedule.end) - 1]);
            }
            else {
              // cells = $(['#currentPage > tbody > tr[row=', config.origin.row, '][timeblock=', config.origin.timeblock, '] > td.task[column=', config.origin.column, ']'].join(''));
              cells = $(['#task', config.origin.row, '-', start, config.origin.timeblock, config.origin.column].join(''));
            }
          }
          else {
            if (config.origin.column == 'first'){
              config.origin.column = (schedule.preferences.display.weekends ? schedule.start : schedule.dates[aa.indexOf(schedule.dates, schedule.start) + 1]);
            }
            else if (config.origin.column == 'last'){
              config.origin.column = (schedule.preferences.display.weekends ? schedule.end : schedule.dates[aa.indexOf(schedule.dates, schedule.end) - 1]);
            }
            //cells = $('#currentPage.schedule tr[row=' + config.origin.row + '][timeblock=' + config.origin.timeblock + '] td.task[column=' + config.origin.column + ']');
            
            // cells = $(['#currentPage > tbody > tr[user=', config.origin.user, '][timeblock=', config.origin.timeblock, '] > td.task[column=', config.origin.column, ']'].join(''));
            cells = $(['#task', config.origin.user, '-', start, config.origin.timeblock, config.origin.column].join(''));
            
            if (!cells.length){
              // cells = $(['#currentPage > tbody > tr[user=', config.origin.user, '][timeblock=', (config.origin.timeblock == 'morning' ? 'afternoon' : 'morning'), '] > td.task[column=', config.origin.column, ']'].join(''));
              cells = $(['#task', config.origin.user, '-', start, config.origin.timeblock == 'morning' ? 'afternoon' : 'morning', config.origin.column].join(''));
            }
          }
        }
      }
      

    }
    
    if (cells.length){
    
      //fblog(['scheduleSelectCells before deselect: ', ad.stopwatchSegment('mousedown')].join(''));
      if (!config.keepCurrent){
        schedule.selected.init({dontClear: true});
      }
      
      var sel;
      
      //fblog(['scheduleSelectCells before highlight: ', ad.stopwatchSegment('mousedown')].join(''));

      
      cells.each(function (i){
        var
          element = $(this),
          row = element.parent().attr('row'),
          timeblock = element.parent().attr('timeblock'),
          column = element.attr('column'),
          user = element.parent().attr('user'),
          id = element.attr('id');
        
        sel = {
          row: row,
          user: user,
          timeblock: timeblock,
          column: column,
          id: id,
          element: element
        };
        
        if (schedule.selected.remove(sel)){
          // schedule.selected.remove(sel);
          element.removeClass('taskselected taskselectedReadonly');
        }
        else {
          schedule.selected.add(sel);
          if (
            !scheduleEditPermission(sel.user, sel.column)
            // !(permissions.admin || permissions.office)
            // &&
            // sel.row != user.id
          ){
            element.addClass('taskselectedReadonly');
          }
          else {
            element.addClass('taskselected');
          }
        }
      });
        
      
      //fblog('scheduleSelectCells after highlight: ' + ad.stopwatchSegment('mousedown'));
      
      
      mouseDown.set({
        row: cells.parent().attr('row'),
        user: cells.parent().attr('user'),
        timeblock: cells.parent().attr('timeblock'),
        column: cells.attr('column'),
        element: cells, //$(cells[cells.length - 1]),
        _id: cells.attr('_id') //$(cells[cells.length - 1]).attr('_id')
      });
      
      
      if (config.onKeydown && (config.direction == 'up' || config.direction == 'down')){
        // $(window).scrollTo(mouseDown.element, {offset: {top: parseInt(($(window).height() - mouseDown.element.height()) / 2) * -1, left: parseInt(($(window).width() - mouseDown.element.width()) / 2) * -1}});
        $(window).scrollTo(mouseDown.element, {offset: centerOffsets(mouseDown.element)});
      }
      
      // if (schedule.selected.count > 1){
        // fblog(['scheduleSelectCells(): ', schedule.selected.count, ' item(s) selected'].join(''));
      // }

      // remove header selected indicator
      $('#currentPage > tbody > tr > th.headerselected, #currentPage > tbody > tr > th.headerselectedReadonly')
        .removeClass('headerselected headerselectedReadonly');
      
      if (schedule.selected.data.length == 1){
        //var headerRow, headerTimeblock, headerColumn;
        
        if (
          !scheduleEditPermission(sel.user, sel.column)
          // !(permissions.admin || permissions.office)
          // &&
          // sel.row != user.id
        ){
          // $(['#currentPage > tbody > tr[row=', schedule.selected.data[0].row, '] > th'].join('')).addClass('headerselectedReadonly');
          $(['#th', schedule.selected.data[0].row].join('')).addClass('headerselectedReadonly');
          // $(['#currentPage > tbody > tr > th.taskheader[column=', schedule.selected.data[0].column, ']'].join('')).addClass('headerselectedReadonly');
          // $(['#th', schedule.selected.data[0].column].join('')).addClass('headerselectedReadonly');
          $(['#currentPage > tbody > tr > th[column=', schedule.selected.data[0].column, ']'].join('')).addClass('headerselectedReadonly');
        }
        else {
          // $(['#currentPage > tbody > tr[row=', schedule.selected.data[0].row, '] > th'].join('')).addClass('headerselected');
          $(['#th', schedule.selected.data[0].row].join('')).addClass('headerselected');
          // $(['#currentPage > tbody > tr > th.taskheader[column=', schedule.selected.data[0].column, ']'].join('')).addClass('headerselected');
          // $(['#th', schedule.selected.data[0].column].join('')).addClass('headerselected');
          $(['#currentPage > tbody > tr > th[column=', schedule.selected.data[0].column, ']'].join('')).addClass('headerselected');
        }
      }
      else if (config.column || config.row){
      }
      else {
      }
      
      //scheduleEditHide();
    }
    
    schedule.inputInProgress = false;
  }


  
  
  
  
  
  
  function scheduleEditPermission(row, column){
    var i, locked = false;
    
    if (!row && !column){
      if (aa.existsIn(permissions, 'admin') || aa.existsIn(permissions, 'schedule-admin')){
        return true;
      }
      else {
        return false;
      }
    }
    
    
    else {
      if (
        column
        && 
        column <= schedule.lock_lastday
      ){
        locked = true;
        
        for (i = 0; i < schedule.lock_exceptions.length; i++){
          if (column.indexOf(schedule.lock_exceptions[i]) == 0){
            locked = false;
            break;
          }
        }
      }
        
      if (
      
        (
          !locked
          &&
          (
            (row && (row == user.id))
            ||
            (aa.existsIn(permissions, 'admin') || aa.existsIn(permissions, 'schedule-admin'))
          )
        )
        
        ||
        
        (
          locked
          &&
          aa.existsIn(permissions, 'accounting-admin')
        )
        
      ){
        return true;
      }
      else {
        return false;
      }
    }
    
    
    return false;
  }
  
  
  
  
  
  // test whether user has passed permission(s)
  // target can be a string (single permission name) or an array of strings (multiple permission names)
  // if any passed permission name doesn't match, return false
  function userHasPermission(target){
    if (typeof(target) == 'string'){
      target = [target];
    }
    
    var i;
    
    for (i = 0; i < target.length; i++){
      if (!aa.existsIn(permissions, target[i])){
        return false;
      }
    }
    
    return true;
  }
  
  
  
  
  
  
  
  
  
  
  
  
  
  function scheduleSelectionObject(config){
  
    this.init = function(config){
      if (this.data){

        // if (this.data.length == 1){
          if (config && config.dontClear){
          }
          else {
            $('th.headerselected, th.headerselectedReadonly', schedule.currentPageElement)
              .removeClass('headerselected headerselectedReadonly');
          }
        // }
          
        for (var i in this.data){
          this.data[i].element.removeClass('taskselected taskselectedReadonly');
          // $(['#currentPage > tbody > tr[row=', this.data[i].row, '][timeblock=', this.data[i].timeblock, '] > td[column=', this.data[i].column, ']'].join('')).removeClass('taskselected taskselectedReadonly');
        }
        
        //schedule.selectedRow = null;

        

      }
      
      this.data = [];
      //this.count = 0;
    };

    this.add = function(sel){
      if (aa.findObjectByKey(this.data, sel.id, 'id').length){
      // if (this.data[sel.row] && this.data[sel.row][sel.timeblock] && this.data[sel.row][sel.timeblock][sel.column]){
        return false;
      }
    

      this.data.push(sel);
      
      //this.count++;
      
      return true;
    };

    this.remove = function(sel){
      var index = aa.indexOfObjectByKey(this.data, sel.id, 'id');
      
      if (!index.length){
      // if (!this.data[sel.row] || !this.data[sel.row][sel.timeblock] || !this.data[sel.row][sel.timeblock][sel.column]){ // make sure row exists in selected object
        return false;
      }
      
      
      // delete this.data[sel.row][sel.timeblock][sel.column];
      fblog(['deleting indexes [', index.join(','), ']...'].join(''));
      for (var i = index.length-1; i >= 0; i--){
        this.data.splice(index[i], 1);
      }
      
      //this.count = this.data.length; //--;
      
      return true;
    };
    
    this.exists = function(sel){
      if (aa.findObjectByKey(this.data, sel.id, 'id').length){
        return true;
      }
      else {
        return false;
      }
    };
    
    // returns array of cells with edit permissions
    this.getEditable = function(config){
      if (!config){
        config = {};
      }
      
      if (this.data.length){ //count) {
        var editable;
        
        if (
          !scheduleEditPermission()
          // !(permissions.admin || permissions.office)
          &&
          !config.includeReadonly
        ){
          // editable = aa.findObjectByKey(this.data, user.id, 'user');
          editable = aa.findObjectByKeys(
            this.data,
            {
              user: {value: user.id, rel: 'eq'},
              // column: {value: schedule.lock_lastday, rel: 'gt'}
              column: scheduleIsNotLocked
            }
          );
        }
        else if (config.includeReadonly || aa.existsIn(permissions, 'accounting-admin')){
          editable = this.data;
        }
        else {
          // editable = this.data;
          editable = aa.findObjectByKeys(
            this.data,
            {
              // column: {value: schedule.lock_lastday, rel: 'gt'}
              column: scheduleIsNotLocked
            }
          );
        }
        
        return editable;
      }
      
      return false;
    };
    
    // returns array of cells WITHOUT edit permissions
    this.getReadOnly = function(config){
      if (!config){
        config = {};
      }
      
      if (this.data.length){ //count) {
        var readOnly;
        
        if (
          !scheduleEditPermission()
          // !(permissions.admin || permissions.office)
          &&
          !config.includeReadonly
        ){
          // readOnly = aa.findObjectByKey(this.data, {value: user.id, opposite: true}, 'user');
          readOnly = aa.findObjectByKeys(
            this.data,
            {
              user: {value: user.id, rel: 'neq'},
              // column: {value: schedule.lock_lastday, rel: 'lte'}
              column: scheduleIsLocked
            },
            true
          );
        }
        else if (config.includeReadonly || aa.existsIn(permissions, 'accounting-admin')){
          readOnly = [];
        }
        else {
          // readOnly = [];
          readOnly = aa.findObjectByKeys(
            this.data,
            {
              // column: {value: schedule.lock_lastday, rel: 'lte'}
              column: scheduleIsLocked
            }
          );
        }
        
        return readOnly;
      }
      
      return false;
    };
    
    // returns leftmost cell in topmost row of current selection
    this.getOrigin = function(config){
      if (!config){
        config = {};
      }
      var origin = {
        row: null,
        timeblock: null,
        column: null,
        user: null,
        notFound: false
      };
      
      if (this.data.length) { //count) {
        var editable = [], temp;
        
        // if (
          // !(permissions.admin || permissions.office)
          // &&
          // !config.includeReadonly
        // ){
          // editable = aa.findObjectByKey(this.data, user.id, 'row');
        // }
        // else {
          // editable = this.data;
        // }

        
        temp = this.getEditable(config);
        for (var i = 0; i < temp.length; i++){
          editable.push(temp[i]);
        }
        // editable = aa.objectClone(this.getEditable(config));
        
        editable = aa.quickSort(editable, 'timeblock');
        editable.reverse();
        editable = aa.quickSort(editable, 'column');
        editable = aa.quickSort(editable, 'row');

        origin = editable[0];
      }
      else {
        if (config.deferToMousedown && mouseDown.row){
          var cell = scheduleCellFromCoords(mouseDown);
          if (cell.length){
            origin = scheduleCellGetCoords(cell);
          }
        }
        if (origin.row === null){
          // origin.user = shared.physicists[0].physicist_id;
          origin.user = schedule.visibleRows[0];
          origin.column = schedule.preferences.display.weekends ? schedule.start : dateString(dpExactDate(schedule.start).addDays(1));
          origin.row = [origin.user, '-', schedule.start].join('');
          origin.timeblock = 'morning';
          origin.notFound = true;
        }
      }
      
      return origin;
      
    };
    
    this.init(config);
    
  }







  // information about most recent mousedown event
  function activeEditObject(initialSettings){
  
    this.set = function(config){
      // create/empty data array if doesn't already exist, or no config passed
      if (!this.cells || !config){
        this.cells = [];
        this.data = {};
        this.isHistory = false;
        this.dirty = false;
        
        if (!config){ // just return if no config passed (used to clear activeEdit object)
          if (schedule && schedule.currentPageElement){
            $('#currentPage > tbody > tr > td.taskediting').removeClass('taskediting');
            $('#currentPage > tbody > tr > td.taskeditingReadonly').removeClass('taskeditingReadonly');
          }
          return;
        }
      }
     
      var outerThis = this; // assign activeEdit object to named var to be accessible within each function
      if (config.cells && config.cells.length){
        if (config.cells.jquery){
          // add each item to activeEdit object's data array
          config.cells.each(function(i){
            var element = $(this);
            outerThis.cells.push({
              _id: element.attr('_id'),
              row: element.parent().attr('row'),
              timeblock: element.parent().attr('timeblock'),
              column: element.attr('column'),
              element: element,
              date: dateTimeString(dpExactDate(element.attr('column')).set({hour: (element.parent().attr('timeblock') == 'morning' ? 8 : 12)})),
              physicist: aa.findObjectByKey(shared.physicists, Number(element.parent().attr('user')), 'physicist_id')[0]
            });
            
            if (element.hasClass('taskselectedReadonly')){
              element.addClass('taskeditingReadonly');
            }
            else {
              element.addClass('taskediting');
            }
          });
        }
        else {
          this.cells = config.cells;
        }
      }
    };
    
    this.add = function(config){
      // fblog('ae add');
      if (
        !config
        ||
        this.exists(config)
      ){
        return false;
      }
      
      this.cells.push({
        _id: config._id || null, //internal id in tasks data collection
        row: config.row || null, // schedule row
        timeblock: config.timeblock || null,
        column: config.column || null, // schedule column
        element: config.element || shared.nullElement, // jquery object for individual schedule 'day' TD, defaults to generic jquery object
        date: config.date || {},
        physicist: config.physicist || {}
      });
      return true;
    };
    
    this.exists = function(sel){
      if (aa.findObjectByKeys(this.cells, {row: sel.row, timeblock: sel.timeblock, column: sel.column}).length){
        return true;
      }
      else {
        return false;
      }
    };
    
    this.commit = function(config){
    
      var isHistory = false;
      
      if (config.target && config.target.isHistory){
        isHistory = true;
      }
      
      var existingTasks = [];
      var locationNameInputElement = $(['#', (isHistory ? forms.clientScheduleEditForm.prefix : forms.taskEditForm.prefix), 'location_name'].join(''));
      var cellsToUpdate = [];
      var i;
      var sameDay;
      var locationNamePlus;
      var pageId, thisPage;
      var task;
      var currentTaskId;
      var forceTypes;
      
      for (i in this.cells){
        if (isHistory || this.cells[i]._id){
          existingTasks.push(i);
        }
      }
  
      // if temp task has valid account_id, save it to permanent tasks collection
      if (
        !config.noSave // task should be saved
        &&
        !( // if flagged to delete this task, but it doesn't already exist, just skip this section and clear temp task
          config.deleteItem && !existingTasks.length
        )
      ){
      
        
        
        // remove duplicates
        if (!isHistory){
          for (i = this.cells.length - 1; i >= 0; i--){
            sameDay = aa.findObjectByKeys(this.cells, {row: this.cells[i].row, column: this.cells[i].column});
            if (
              sameDay.length > 1
              &&
              this.data.billable_quantity >= 8
              // (this.data.billingmatrix[1] >= 2 || this.data.billingmatrix[2] >= 8)
            ){
              this.cells.splice(i, 1);
            }
          }
        }
        
        fblog(['committing ', (isHistory ? 'history row' : [this.cells.length, ' cell(s)'].join(''))].join(''));
        
        locationNameInputElement[0].value = $.trim(locationNameInputElement[0].value);
        
        locationNamePlus = [this.data.location_name];
        if (this.data.unique_identifier){
          locationNamePlus.push(' (', this.data.unique_identifier, ')');
        }
        else if (this.data.num_locations > 1 && this.data.city){
          locationNamePlus.push(' (', this.data.city, ')');
        }
        locationNamePlus = locationNamePlus.join('');

        if (
          locationNameInputElement[0].value
          &&
          this.data.location_name != locationNameInputElement[0].value
          &&
          locationNamePlus != locationNameInputElement[0].value
        ){
          var newSummary = [locationNameInputElement[0].value];
          if (this.data.summary){
            newSummary.push(', ', this.data.summary);
          }
          this.data.summary = newSummary.join('');
          this.data.account_id = "0";
          this.data.location_id = "840";
          this.data.location_name = this.data.location_name_short = "[ Not Found ]";
          this.data.unique_identifier = "";
          this.data.city = "";
          locationNamePlus = locationNameInputElement[0].value;
        }
        
        forceTypes = aa.findObjectByKey(schedule.forceTypes, this.data.location_id, 'location');
        if (forceTypes.length){
          forceTypes = forceTypes[0].allowed;
        }
        else {
          forceTypes = null;
        }
        
        
        // iterate through all cells
        for (i in this.cells){

          if (!isHistory){
            pageId = schedule.currentPage; //Number(this.cells[i].element.parents('table.schedule').attr('page'));
            thisPage = scheduleGetPageById(pageId);
          }
          
          // flag task for deletion
          if (config.deleteItem){
            if (isHistory){
              task = this.original;
              fblog('deleting history task');
              
              task.deleteItem = true; // flag to mark this task deleted in database
              this.dirty = true;
            }
            else if (this.cells[i]._id){
              task = shared.tasks.get({value: this.cells[i]._id});
            
              fblog(['deleting existing cell _', this.cells[i]._id, ', task ', task.data.id].join(''));
              scheduleDoubleBilledRemove({
                pageId: pageId,
                startDate: task.data.schedule_start_date,
                locationId: task.data.location_id,
                taskId: ['_', task._id].join('')
              });
              scheduleDoubleBilledRemove({
                pageId: pageId,
                startDate: task.data.schedule_start_date,
                locationId: task.data.location_id,
                taskId: task.data.id
              });

              var thisTaskInPages = aa.indexOfObjectByKey(thisPage.tasks, this.cells[i]._id, '_id');
              if (thisTaskInPages.length){
                thisPage.tasks.splice(thisTaskInPages[0], 1);
              }

              if (!task.data.id){ // if this task hasn't been saved to the server yet, just remove it from the tasks collection
                shared.tasks.remove(this.cells[i]._id);
                $(['td[_id=', this.cells[i]._id, ']'].join('')).removeAttr('_id');
              }
              else {
                task.deleteItem = true; // flag to mark this task deleted in database
                this.dirty = true;
              }
            }
            // deleting an empty cell, treat same as discard
            else if (config.deleteItem){
              fblog('deleting empty cell');
              continue;
            }
            
          }
          // if this task doesn't have a location defined (facility field is empty)
          else if (
            !locationNameInputElement[0].value
            ||
            !Number(this.data.location_id)
          ){
            $('#taskEditMessage').append('<p><span class="highlightWarning bold">WARNING:</span> You must choose a valid facility before saving, or click the <b>Discard</b> button to close this editor.</p>').show();
            return false;
          }
          // if this task has no hours set, or if it's not (SDSC or Delmarva) and less than 3 hours
          else if (
            !am.round(this.data.billable_quantity, 1)
            ||
            (
              this.data.location_id != 260 // SDSC
              &&
              this.data.location_id != 396 // Delmarva
              &&
              am.round(this.data.billable_quantity, 1) < 3
            )
          ){
            $('#taskEditMessage').append('<p><span class="highlightWarning bold">WARNING:</span> You must set a valid amount of time allocated (all visits require a <b>minimum of 3 hours</b>) before saving, or click the <b>Discard</b> button to close this editor.</p>').show();
            return false;
          }
          // if this task's location requires a unit number for billing, make sure one is entered in the summary
          else if (
            !this.data.device_id
            &&
            !this.data.device_name
            &&
            (
              this.data.location_id == 1052
              ||
              this.data.location_id == 1053
            )
          ){
            $('#taskEditMessage').append('<p><span class="highlightWarning bold">WARNING:</span> You must enter a <b>unit number</b> in the Summary field before saving, or click the <b>Discard</b> button to close this editor.</p>').show();
            return false;
          }
          // enforce modality selection requirement
          else if (forceTypes && (!this.data.tasktype_id || (forceTypes.length && !aa.existsIn(forceTypes, this.data.tasktype_id)))){
            $('#taskEditMessage').append('<p><span class="highlightWarning bold">WARNING:</span> You must select a modality before saving, or click the <b>Discard</b> button to close this editor.</p>').show();
            return false;
          }
          else if (!scheduleEditShowConditions(this.isHistory)){
            return false;
          }
          // if this was a preexisting task, update it
          else if (isHistory || this.cells[i]._id){
            //fblog('updating existing task');

            if (isHistory){
              task = this.original;
            }
            else {
              task = shared.tasks.get({value: this.cells[i]._id});
            }
            
            // if this is a valid task entry (not empty), save it to the permanent collection
            if (
              (
                this.data.account_id
                ||
                this.data.account_id === 0
                ||
                this.data.account_id === '0'
              )
              &&
              (locationNamePlus == locationNameInputElement[0].value || this.data.location_name == locationNameInputElement[0].value)
              &&
              !config.deleteItem
            ){

              if (!isHistory){
                // remove existing double billed status (if applicable)
                scheduleDoubleBilledRemove({
                  pageId: pageId,
                  startDate: task.data.schedule_start_date,
                  locationId: task.data.location_id,
                  taskId: ['_', task._id].join('')
                });
                scheduleDoubleBilledRemove({
                  pageId: pageId,
                  startDate: task.data.schedule_start_date,
                  locationId: task.data.location_id,
                  taskId: task.data.id
                });
              }
              
              currentTaskId = task.data.id;
              
              task.element = this.cells[i].element;
              
              
              if (
                // task.data.location_id != this.data.location_id
                // ||
                task.data.account_id != this.data.account_id
              ){
                task.moveItem = true;
              }
              
              
              // should clone all EXCEPT billingdate, id, physicist_id, schedule_start, schedule_start_date, timeblock
              // task.data = aa.objectClone(this.data, true);
              task.data = _.clone(this.data, true);
              
              if (!isHistory){
                task.data.id = currentTaskId;
                task.data.schedule_start = task.data.billingdate = dateTimeString(dpExactDate(this.cells[i].column).set({hour: (this.cells[i].timeblock == 'morning' ? 8 : 12)}));
                task.data.schedule_start_date = ad.sqlGetDate(task.data.schedule_start);
                task.data.physicist_id = this.cells[i].physicist.id;
                task.data.timeblock = this.cells[i].timeblock;

                // determine if this is double billed
                cellsToUpdate = cellsToUpdate.concat(
                  scheduleIsDoubleBilled({
                    pageId: pageId,
                    startDate: task.data.schedule_start_date,
                    locationId: task.data.location_id
                  })
                );
              }
              
              fblog(['updated task for ', task.data.physicist_id, ', ', task.data.schedule_start].join(''));
            }
            // not a valid task, and not marked for deletion
            // else if (locationNamePlus != locationNameInputElement[0].value){
              
            // }
            else {
              fblog('scheduleEditHide(): unable to save (not a valid task, not marked for deletion)');
              fbdir('activeEdit.data', true, this.data);
              $('#taskEditMessage').append('<p><span class="highlightWarning bold">WARNING:</span> You must choose a valid facility before saving this task, or click the <b>Discard</b> button to close this editor.</p>').show();
              return false;
            }

          }
          // otherwise create a new task if valid account_id
          else if (this.data.account_id){
            //fblog('adding new task');
            // var taskData = aa.objectClone(this.data);
            var taskData = _.clone(this.data);
            // taskData.schedule_start = dateTimeString(dp(this.cells[i].column).set({hour: (this.cells[i].timeblock == 'morning' ? 8 : 12)}));
            // taskData.physicist = aa.findObjectByKey(shared.physicists, Number(this.cells[i].row), 'physicist_id')[0];
            
            // if editing multiple cells, update each cell's task appropriately
            // prevents invalid references when editing history rows
            if (this.cells.length > 1){
              taskData.schedule_start = [this.cells[i].column, (this.cells[i].timeblock == 'morning' ? ' 08:00:00' : ' 12:00:00')].join('');
              taskData.schedule_start_date = this.cells[i].column;
              taskData.physicist_id = this.cells[i].physicist.id;
            }
            
            //fblog(['new task for date ', taskData.schedule_start].join(''));
            
            var result = shared.tasks.add([taskData]);
            this.cells[i]._id = result.last_id;
            thisPage.tasks.push(shared.tasks.get({value: this.cells[i]._id}));
            cellsToUpdate = cellsToUpdate.concat(
              scheduleIsDoubleBilled({
                pageId: pageId,
                startDate: taskData.schedule_start_date,
                locationId: taskData.location_id
              })
            );
            
            
            //fblog(['added new task (', this.cells[i]._id, ') ', shared.tasks.get(this.cells[i]._id).data.schedule_start].join(''));
            
            this.cells[i].element.attr('_id', this.cells[i]._id);
            
            fblog(['new task for ', taskData.physicist_id, ', ', taskData.schedule_start].join(''));
          }
          else {
            return false;
          }

        }

        var taskToSave;
        
        if (isHistory){
          task.dirty = this.dirty;
        }
        else {
          for (i in this.cells){
            // flag this task to be saved to database
            taskToSave = shared.tasks.get({value: this.cells[i]._id});
            if (taskToSave){
              taskToSave.dirty = this.dirty;
            }
            else {
              fblog(['unable to find task (', this.cells[i]._id, ') for save flag'].join(''));
            }
          }
        }
        
        cellsToUpdate = this.cells.concat(cellsToUpdate);
        for (i in cellsToUpdate){
          if (isHistory){ 
            clientHistoryRowUpdateContent({
              row: cellsToUpdate[i].element,
              task: task
            });
          }
          else {
            // update contents of schedule table cell
            scheduleCellUpdateContent(cellsToUpdate[i].element);
          }
          
          //this.cells[i].element.removeClass('taskediting');
        }
        
        if (cellsToUpdate.length && isHistory){
          // invalidate billing, so it will be reloaded when tab is switched
          clients.billingLoaded = false;
        }

      }
      
      if (isHistory){
        if (!clients.dirty){
          clients.dirty = [];
        }
        
        if (shared.periods){
          for (i = 0; i < shared.periods.length; i++){
            if (shared.periods[i].tasks){
              clients.dirty = clients.dirty.concat(shared.periods[i].tasks.getDirty({forExport: true, includeFailed: true}));
            }
          }
        }
      }
      
      // clear this object
      this.set();
      
      return true;
      
    };
    
    this.set(initialSettings);

  }
  


  // information about most recent mousedown event
  function mouseDownObject(initialSettings){
  
    this.set = function(config){
      if (!config){
        config = {};
      }
      this._id = config._id || null; //internal id in tasks data collection
      this.row = config.row || null; // schedule row
      this.user = config.user || null;
      this.timeblock = config.timeblock || null;
      this.column = config.column || null; // schedule column
      this.element = config.element || shared.nullElement; // jquery object for individual task TD, defaults to generic jquery object
      this.timestamp = config.timestamp || null; // unique timestamp for mousedown event
    };
    
    this.set(initialSettings);

  }
  
  
  
  function taskObject(initialSettings, config){
    this.set = function(data, config){
      var i;
      
      if (!data){
        data = {};
      }
      
      if (!config){
        config = {};
      }

      // if this is a deleted task, add it to deleted collection instead of creating a standard task object
      if (data.audit_user_end){
        if (config.deleted){ // only if a deleted collection is passed
        
          // look for previous deleted items for this timeslot
          var foundIndex = aa.indexOfObjectByKeys(config.deleted, {
            schedule_start_date: data.schedule_start_date,
            physicist_id: data.physicist_id,
            timeblock: data.timeblock
          });
          
          data.weekStart = dateGetWeekStart(data.schedule_start_date);
          
          // remove any previous deleted items in this timeslot
          for (i = foundIndex.length - 1; i >= 0; i--){
            config.deleted.splice(foundIndex[i], 1);
          }
          
          // add this item (which should be the most recent)
          config.deleted.push(data);
          
        }
        return false;
      }

      this.isTraining = data.module_id ? true : false;

      // common items
      this.id = Number(data.id) || null;
      this.account_id = Number(data.account_id) || 0;
      this.location_id = Number(data.location_id) || null;
      this.location_name = data.location_name || null;
      this.location_name_short = data.location_name_short || data.location_name || null;
      this.num_locations = Number(data.num_locations) || 1;
      this.unique_identifier = data.unique_identifier || null;
      this.city = data.city || null;

      //if (shared.activeInterface != 'schedule'){
        this.location_name_billing = billingGetLocationName(this, data.invoice_show_locations) || data.location_name;
      //}
      
      this.state = data.state || null;
      this.tasktype_id = Number(data.tasktype_id) || null;
      
      // non-training items
      this.audit_start = (
        data.audit_start === null ?
          dateTimeString(new Date())
          :
          data.audit_start
      );
      this.audit_user = Number(data.audit_user) || user.id;
      this.audit_user_name = data.audit_user_name || user.name;
      
      this.billable_quantity = (data.billable_quantity == 0 || data.billable_quantity == '0' ? 0 : (am.round(data.billable_quantity, 1) || 8));
      this.billingmetric = Number(data.billingmetric_id) || 2; //Number(data.location_billingmetric) || Number(data.default_billingmetric) || null;
      this.billingmetric_override = Number(data.billingmetric_override) || Number(data.location_billingmetric) || Number(data.default_billingmetric) || null;
      this.billingmetric_nonroutine = Number(data.billingmetric_override) || null;
      
      // this.billingmatrix = (data.billingmatrix ? hoursToMatrix(data.billingmatrix) : {1: 2}); // default full day
      
      // if billable quantity set and metric is hourly, update matrix to reflect this
      if (data.billable_quantity){
        if (data.billingmetric_id == 2){
          this.billingmatrix = hoursToMatrix(am.round(data.billable_quantity, 1));
        }
        else if (data.billingmetric_id == 1){
          this.billingmatrix = {1:data.billable_quantity, 2:0};
        }
        else if (data.billingmetric_id == 3){
          this.billingmatrix = {1:0, 2:0, 3:data.billable_quantity};
        }
      }
      // otherwise if matrix is already set, copy as-is
      else if (data.billingmatrix){
        this.billingmatrix = data.billingmatrix;
      }
      // otherwise default to 8 hours (full day)
      else {
        this.billingmatrix = {2:0};
      }
      
      this.cod = Boolean(Number(data.cod)) || false;
      
      this.notes = data.notes || '';
      this.flagged = Boolean(Number(data.flagged)) || false;
      this.physicist_id = Number(data.physicist_id) || null;
      this.summary = data.summary || '';
      
      this.taskItems = data.taskItems || [{summary: this.summary}];
      
      this.device_id = Number(data.device_id) || null;
      this.device_name = data.device_name || null;
      
      // training items
      this.module_id = Number(data.module_id) || null;
      this.module_name = data.module_name || '';
      this.passed_datetime = data.passed_datetime || null;
      this.user_id = Number(data.user_id) || null;
      this.user_name = data.user_name || '';
      
      // hack to support existing code
      this.schedule_start = (this.isTraining ? this.passed_datetime : (data.schedule_start || null));
      this.billingdate = data.billingdate_override || this.schedule_start;
      
      this.cost_adjustment = Number(data.cost_adjustment) || 0;

      for (var i in this.billingmatrix){
        this.billingmatrix[i] = Number(Number(this.billingmatrix[i]).toFixed(2));
      }
      
      this.billable_quantity_adjustment = Number(data.billable_quantity_adjustment) || 0;
      //this.cost_adjustment = Number(data.cost_adjustment) || 0;
      
      
      this.is_billable = (data.is_billable == '0' || data.is_billable == 0) ? false : true;
      if (!this.billable_quantity){
        if (!this.is_billable){
          if (
            this.billingmetric_override == 1
            ||
            (!this.billingmetric_override && this.billingmetric == 1)
          ){
            this.billable_quantity = 1;
            this.billingmatrix = {1:1};
          }
          else {
            this.billable_quantity = 4;
            this.billingmatrix = {2:4};
          }
        }
        else {
          this.billingmatrix = {};
        }
      }
      this.dont_publish_dates = Number(data.dont_publish_dates) ? true : false;
      
      // if no billable quantity, default to half day and set billable flag
      // if (!this.billingmatrix[1] && !this.billingmatrix[2]){
        // this.is_billable = false;
        // this.billingmatrix = {1: 1};
      // }
      
      this.timeblock = (data.timeblock !== undefined ? data.timeblock : ad.sqlGetHour(this.schedule_start) < 12 ? 'morning' : 'afternoon');
      this.schedule_start_date = ad.sqlGetDate(this.schedule_start);
      
      this.weekStart = config.weekStart || dateGetWeekStart(this.schedule_start_date);
      
      // if (config.weekStart){
        // this.weekStart = config.weekStart;
      // }
      // else {
        // var dateSchedule = dpExactDate(this.schedule_start_date);
        // var dateWeekStart;
        // if (dateSchedule.is().sunday()){
          // dateWeekStart = dateSchedule;
        // }
        // else {
          // dateWeekStart = dateSchedule.last().sunday();
        // }
        // this.weekStart = dateString(dateWeekStart);
      // }
      
      // if (config.page !== undefined){
        // this.page = config.page;
      // }
      // else if (data.page !== undefined){
        // this.page = data.page;
      // }
      // else {
        // var thisDate = dpExact(this.schedule_start_date, 'yyyy-MM-dd');
        // var startDate;
        // if (thisDate.is().sunday()){
          // startDate = this.schedule_start_date;
        // }
        // else {
          // startDate = dateString(thisDate.previous().sunday());
        // }
        // var pageIndex = aa.indexOfObjectByKey(schedule.pages, startDate, 'start');
        // if (pageIndex.length){
          // pageIndex = pageIndex[0];
        // }
        // this.page = pageIndex;
        
      // }
      
      return true;
    };
    
    this.get = function(){
      var dataOnly = {};
      
      for (var i in this){
        if (this.hasOwnProperty(i) && typeof this[i] != 'function'){
          dataOnly[i] = this[i];
        }
      }
      
      return dataOnly;
    };
    
    this.getForExport = function(){
      if (!this.isTraining && !this.abort){
        var exp = {
          task_id: this.id,
          //account_id: this.account_id,
          location_id: this.location_id,
          physicist_id: this.physicist_id,
          device_id: this.device_id,
          device_name: this.device_name,
          //audit_start: this.audit_start,
          //audit_user: this.audit_user,
          // billingmatrix: {
            // 1: 0,
            // 2: matrixToHours(this.billingmatrix) // (this.billingmatrix[1] * 4) + this.billingmatrix[2]
          // }, //this.billingmatrix,
          billingmetric_id: this.billingmetric, //aa.getKeys(this.billingmatrix).join(',');
          billable_quantity: am.round(this.billable_quantity, 1), //aa.getValues(this.billingmatrix).join(',');
          is_billable: this.is_billable,
          billable_quantity_adjustment: am.round(this.billable_quantity_adjustment, 1),
          cost_adjustment: this.cost_adjustment,
          dont_publish_dates: this.dont_publish_dates,
          billingdate_override: this.billingdate != this.schedule_start ? this.billingdate : null,
          // billingmetric_override: this.billingmetric_override, // don't save this until it is implemented in the UI - should only be set in database for now
          summary: this.summary,
          notes: this.notes,
          flagged: this.flagged,
          schedule_start: this.schedule_start, //.toString('yyyy-MM-dd HH:mm:ss'), //this.schedule_start.valueOf()/1000
          tasktype_id: this.tasktype_id
        };
        
        return exp;
      }
      else {
        return false;
      }
    };
    
    this.abort = !this.set(initialSettings, config);
  }


  
  

  
  
  // generic data collection
  function dataCollection(initialSettings){
    
    // failsafe to ensure members exist before using
    this.init = function(initialSettings){
      // if (!this.dataPrototype){
      if (initialSettings.dataPrototype){
        this.dataPrototype = initialSettings.dataPrototype;
      }
    
      // if (!this._idKey){
      if (initialSettings._idKey){
        this._idKey = initialSettings._idKey;
      }
      
      //fblog(this._idKey);

      // main array of task items
      //if (!this.items){
        this.items = [];
      //}
      
      // internal id counter for each added item
      // used to differentiate items locally before saving to server,
      // upon which a more permanent id is assigned by server and (ideally) returned
      //if (this.last_id === undefined){
        this.last_id = 0;
      //}
    };

    // add new tasks from passed array
    this.add = function(items, config){
      //this.init();
      
      var added = [];
      var newData;
      var newItem;
      

      if (typeof items == 'object'){ // paramater is array
        for (var i = 0; i < items.length; i++){
          // remove any existing item with same _id (this should absolutely be unique under any circumstances)
          if (this._idKey){
            if (this._idKey === true){
              this.remove(items[i]);
            }
            else {
              this.remove(items[i][this._idKey]);
            }
          }
          else {
            //this.remove;
          }
          
          newData = new this.dataPrototype(items[i], config);
          
          if (newData.abort){
            delete newData;
          }
          else {
          
            newItem = {
              data: newData,
              deleteItem: false,
              dirty: false, // add dirty flag to mark edited items to be saved
              element: items[i].element,
              // add internal unique id for this item
              _id: (
                this._idKey ? // if a custom _id key is configured
                  (
                    this._idKey === true ?
                      (this.last_id = items[i]) // explicitly set to Boolean true if raw value of this item should be used as _id
                      :
                      (this.last_id = items[i][this._idKey]) // otherwise use the string in this._idKey as the name of the key within this item to use as _id
                  )
                  
                  // otherwise if no custom _id is configured, use internal counter
                  : ++this.last_id  // ensure counter is incremented before assigning to this item
              ) 
            };
            
            // add item to collection
            this.items.push(newItem);
            added.push(newItem);
            //fblog('new item added: ' + this.last_id);
            
          }
        }
      }
      else {
        newItem = {
          data: new this.dataPrototype(items),
          dirty: false, // add dirty flag to mark edited items to be saved
          // add internal unique id for this item
          _id: (this._idKey ? (this.last_id = items[this._idKey]) : ++this.last_id) // ensure counter is incremented before assigning to this item
        };
        this.items.push(newItem);
        added.push(newItem);
        //fblog('new task created: ' + this.last_id);
      }

      return ({
        last_id: this.last_id,
        added: added
      });
    };

    // update existing item
    // if overwriteDirty is not true, any dirty tasks will not be overwritten
    // this is to prevent updating a task which is flagged to be saved to the server
    // new tasks on server will still overwrite this when applicable AFTER the save has completed and dirty flag is reset
    this.update = function(data, keyVal, keyName, overwriteDirty){
      if (!keyName){
        keyName = '_id';
      }
      
      var existing = aa.findObjectByKey(this.items, keyVal, keyName);
      if (existing.length){
        existing = existing[0];
        
        if (
          (existing.dirty && overwriteDirty)
          ||
          (!existing.dirty && !overwriteDirty)
        ){
          // var newData = aa.objectClone(existing.data);
          var newData = _.clone(existing.data);
          for (var i in data){
            newData[i] = data[i];
          }
          
          existing.data = new this.dataPrototype(newData);
          
          return true;
        }
      }
      else {
        return false;
      }
    };
    
    // remove existing tasks (by _id) in passed array
    this.remove = function(items, key){
      //this.init();

      if (!key){
        key = '_id';
      }
      else {
        key = ['data.', key].join('');
      }

      if (typeof items == 'object'){ // paramater is array
        for (var i = 0; i < items.length; i++){
          if (aa.removeObjectByKey(this.items, items[i], '_id')){
            fblog(['removed item: ', items[i]].join(''));
          }
        }
      }
      else {
        if (aa.removeObjectByKey(this.items, items, key)){
          fblog(['removed item: ', items].join(''));
        }
      }
    };
    
    // get individual item by specified key, or internal _id property if none passed
    // or get array of all matching items if getAll parameter is true
    this.get = function(config){
      if (!config.key){
        config.key = '_id';
      }
      else {
        config.key = ['data.', config.key].join('');
      }
      var item = aa.findObjectByKey(this.items, config.value, config.key, config.returnKeys); 
      if (config.getAll || item.length){
        if (config.getAll || typeof config.value == 'object'){ // if a range was passed, or explicitly set getAll to true
          return item;
        }
        else {
          return item[0]; // this returns an array, but we're searching for a unique, so just return the first array element
        }
      }
      else {
        return false;
      }
    };
    
    // get array of all items marked as dirty
    this.getDirty = function(config){
      if (config.forExport){
        // get any items marked either dirty or to be deleted
        var dirty;
        var exports = [];
        var thisExport;
        var i, j;
        var data;
        var overlaps = [];
        
        dirty = aa.findObjectByKey(this.items, true, 'dirty'); //aa.findObjectByKeys(this.items, {dirty: true, deleteItem: true}, true);
        
        if (config.includeFailed){
          dirty = dirty.concat(aa.findObjectByKey(
            this.items,
            {
              callback: function(a, b){
                if (a instanceof Date) {
                  if (a.getElapsed() > (1000 * schedule.performance.timeoutSaveDirty)){
                    return true;
                  }
                }
                
                return false;
              }
            },
            'dirty'
          ));
        }
        
        // eliminate overlapping dirty tasks, so only the most recent edit is saved to server
        for (i = 0; i < dirty.length; i++){
          overlaps = aa.findObjectByKey(dirty, dirty[i].data.physicist_id, 'data.physicist_id');
          overlaps = aa.findObjectByKey(overlaps, dirty[i].data.schedule_start, 'data.schedule_start');
          
          if (overlaps.length > 1){
            aa.sortByKey(overlaps, 'audit_start', 'data', 'desc');
            
            // flag all but most recent index to be removed
            for (j = 1; j < overlaps.length; j++){
              overlaps[j].data.abort = true;
              //overlaps[j].deleteItem = true;
            }
          }
        }

        for (i = dirty.length - 1; i >= 0; i--){
          if (dirty[i].data.abort){
            continue;
          }
          
          data = dirty[i].data.getForExport();
          if (data !== false){
            if (!(dirty[i].dirty instanceof Date)){
              dirty[i].dirty = new Date();
            }
            thisExport = {
              deleteItem: dirty[i].deleteItem,
              dirty: dirty[i].dirty.valueOf(),
              data: data
            };
            
            if (dirty[i].output){ // is history item
              thisExport.period = dirty[i].data.schedule_start_date.substr(0, 7);
              thisExport.location = dirty[i].data.location_id;
            }
            else { // is schedule item
              thisExport._id = dirty[i]._id;
            }
            
            exports.push(thisExport);
          }
        }
        return exports;
      }
      else {
        return aa.findObjectByKey(this.items, true, 'dirty');
      }
    };
    
    // return array of ids and audit_start dates for each item in collection
    // used to compare against latest data on server to determine if updates are available
    this.getAuditDates = function(){
      var dates = [];
      
      for (var i in this.items){
        dates.push({
          id: this.items[i].data.id,
          audit_start: this.items[i].data.audit_start
        });
      }
      
      return dates;
    };
    
    this.clear = function(){
      // for (var i in this){
        // if (this.hasOwnProperty(i)){
          // if (!(typeof this.i == 'function')){
            // delete this.i;
          // }
        // }
      // }
      
      // just call init function to wipe all data
      this.init();
    };
    

    // call constructor
    this.init(initialSettings);
    

  }
    
  
  
  


  
  
  function scheduleGetPageById(pageId){
    var pageIndex = aa.indexOfObjectByKey(schedule.pages, pageId, 'id');
    if (pageIndex.length){
      return schedule.pages[pageIndex[0]];
    }
    else {
      return false;
    }
  }
  
  
  
  // return page object containing passed date
  // if view == month, get sunday before first of the month
  // otherwise get sunday before passed date
  function scheduleGetPageByDate(date, view){
    var startDate = dpExactDate(date);
    var startDateMonthly;
    
    if (view != 'week'){
      startDateMonthly = startDate.clone().set({day: 1});
      if (!startDateMonthly.is().sunday()){
        startDateMonthly.previous().sunday();
      }
      startDateMonthly = dateString(startDateMonthly);
    }
    
    if (view != 'month'){
      if (!startDate.is().sunday()){
        startDate.previous().sunday();
      }
      startDate = dateString(startDate);
    }
    
    var page;
    // var pageIndex = aa.indexOfObjectByKey(schedule.pages, startDate, 'start');
    if (!view){
      page = aa.findObjectByKey(schedule.pages, startDate, 'start');
    }
    else {
      page = aa.findObjectByKeys(schedule.pages, {start: startDate, view: view});
    }
    
    if (page.length){
      if (!view){
        return page;
      }
      else {
        return page[0];
      }
    }
    else {
      return false;
    }
    
  }
  
  
  
  function scheduleIsDoubleBilled(config){ // pageId, startDate, locationId
    
    var thisPage;
    
    var doubleMatches = [];
    
    for (thisPage in schedule.pages){
      var dateMatches = aa.findObjectByKey(schedule.pages[thisPage].tasks, config.startDate, 'data.schedule_start_date');
      if (dateMatches.length){
        //dateMatches = dateMatches[0];
        
        var locationMatches = aa.findObjectByKey(dateMatches, config.locationId, 'data.location_id');
        if (locationMatches.length > 1){
          
          for (var i in locationMatches){
            if (!aa.existsIn([0,99], locationMatches[i].data.account_id)){
              var element = $(['#task', locationMatches[i].data.physicist_id, '-', locationMatches[i].data.weekStart, locationMatches[i].data.timeblock, locationMatches[i].data.schedule_start_date].join(''));
              
              scheduleDoubleBilledAdd([{
                page: schedule.pages[thisPage], //config.pageId,
                startDate: locationMatches[i].data.schedule_start_date,
                locationId: locationMatches[i].data.location_id,
                taskId: locationMatches[i].data.id || ['_', locationMatches[i]._id].join(''),
                physicistId: locationMatches[i].data.physicist_id,
                timeblock: locationMatches[i].data.timeblock,
                element: element //$(['#task', locationMatches[i].data.physicist_id, '-', locationMatches[i].data.weekStart, locationMatches[i].data.timeblock, config.startDate].join(''))
              }]);
              
              //var element = $(['#task', locationMatches[i].data.physicist_id, '-', locationMatches[i].data.weekStart, locationMatches[i].data.timeblock, locationMatches[i].data.schedule_start_date].join(''));
              doubleMatches.push({
                _id: locationMatches[i]._id,
                row: locationMatches[i].data.physicist_id,
                timeblock: locationMatches[i].data.timeblock,
                column: locationMatches[i].data.schedule_start_date,
                element: element,
                date: [locationMatches[i].data.schedule_start_date, ' ', (locationMatches[i].data.timeblock == 'morning' ? '08' : '12'), ':00:00'].join(''),
                physicist: aa.findObjectByKey(shared.physicists, locationMatches[i].data.physicist_id, 'physicist_id')[0]
              });
            }
            
          }
          
        }
        
      }
    }
    
    return doubleMatches;
    
  }
  
  
  function scheduleDoubleBilledAdd(config){ // pageId, startDate, locationId, taskId, physicistId, timeblock){
    for (var i = 0; i < config.length; i++){
    
      var thisPage = config[i].page; //scheduleGetPageById(config[i].pageId);
      
      if (!aa.existsIn(thisPage.doubleBilled.list, config[i].taskId)){
      
        // get this date, or add if doesn't exist
        var thisDateIndex = aa.indexOfObjectByKey(thisPage.doubleBilled.tree, config[i].startDate, 'date');
        if (!thisDateIndex.length){
          thisPage.doubleBilled.tree.push({
            date: config[i].startDate,
            locations: []
          });
          thisDateIndex = thisPage.doubleBilled.tree.length - 1;
        }
        else {
          thisDateIndex = thisDateIndex[0];
        }
        var thisDate = thisPage.doubleBilled.tree[thisDateIndex];
        
        // get this location, or add if doesn't exist
        var thisLocationIndex = aa.indexOfObjectByKey(thisDate.locations, config[i].locationId, 'location');
        if (!thisLocationIndex.length){
          thisDate.locations.push({
            location: config[i].locationId,
            tasks: []
          });
          thisLocationIndex = thisDate.locations.length - 1;
        }
        else {
          thisLocationIndex = thisLocationIndex[0];
        }
        var thisLocation = thisDate.locations[thisLocationIndex];
            
        // overwrite this task, or add if doesn't exist
        var thisTaskIndex = aa.indexOfObjectByKey(thisLocation.tasks, config[i].taskId, 'task');
        var thisTask = {
          task: config[i].taskId,
          physicist: config[i].physicistId,
          timeblock: config[i].timeblock,
          element: config[i].element
        };
        if (!thisTaskIndex.length){
          thisLocation.tasks.push(thisTask);
          thisTaskIndex = thisLocation.tasks.length - 1;
        }
        else {
          thisTaskIndex = thisTaskIndex[0];
          thisLocation.tasks[thisTaskIndex] = thisTask;
        }
        
        thisPage.doubleBilled.list.push(config[i].taskId);
        
      }
    }
  }
  
  
  function scheduleDoubleBilledRemove(config){ // pageId, startDate, locationId, taskId
    
    var thisPage = scheduleGetPageById(config.pageId);
    var weekStart = thisPage.view == "week" ? thisPage.start : null;

    if (
      aa.existsIn(thisPage.doubleBilled.list, config.taskId)
    ){
    
      var thisDateIndex = aa.indexOfObjectByKey(thisPage.doubleBilled.tree, config.startDate, 'date');
      if (thisDateIndex.length){
        thisDateIndex = thisDateIndex[0];
        var thisDate = thisPage.doubleBilled.tree[thisDateIndex];
        
        var thisLocationIndex = aa.indexOfObjectByKey(thisDate.locations, config.locationId, 'location');
        if (thisLocationIndex.length){
          thisLocationIndex = thisLocationIndex[0];
          var thisLocation = thisDate.locations[thisLocationIndex];
          
          var thisTaskIndex = aa.indexOfObjectByKey(thisLocation.tasks, config.taskId, 'task');
          if (thisTaskIndex.length){
            thisTaskIndex = thisTaskIndex[0];
            //var thisTask = thisLocation.tasks[thisTaskIndex];
            
            // 2 or fewer INCLUDING this task BEFORE deleted this task - so only 1 will be remaining
            // so delete this entire branch
            if (thisLocation.tasks.length <= 2) {
              for (var j = 0; j < thisLocation.tasks.length; j++){
                aa.removeByValue(thisPage.doubleBilled.list, thisLocation.tasks[j].task);
                //var cellId = ['#task', thisLocation.tasks[j].physicist, thisLocation.tasks[j].timeblock, config.startDate].join('');
                
                // get cell for this task, and update its appearance
                var cell = $(['#task', thisLocation.tasks[j].physicist, '-', weekStart, thisLocation.tasks[j].timeblock, thisDate.date].join(''));
                if (cell.length){
                  scheduleCellUpdateContent(cell); //thisLocation.tasks[j].element);
                }
              }
              thisDate.locations.splice(thisLocationIndex, 1);
            }
            
            // more than 2 INCLUDING this task
            // so only delete this task
            else {
              aa.removeByValue(thisPage.doubleBilled.list, config.taskId);
              thisLocation.tasks.splice(thisTaskIndex, 1);
            }
          }
          
          if (!thisLocation.tasks.length){
            thisDate.locations.splice(thisLocationIndex, 1);
          }
        }
        
        if (!thisDate.locations.length){
          thisPage.doubleBilled.tree.splice(thisDateIndex, 1);
        }
      }
    }
  }










  
  function scheduleEditShow(cell){
  
    timerStart(arguments);
    
    var isHistory = false;

    var
      cellMeta,
      cellEditable,
      cellReadOnly,
      physicist,
      activeEdit,
      i;
      
    // if this called on an editable history row, NOT a schedule cell
    if (cell && cell.task && cell.el && cell.el.length){
      isHistory = true;
      activeEdit = clients.scheduleEdit;
      activeEdit.isHistory = true;
      cellMeta = cell;
      cell = cell.el;
      origin = scheduleCellGetCoords(cell);
      cellEditable = [];
      cellReadOnly = [];
    }
    
    // otherwise this is a schedule cell
    else {
    
      cellEditable = [];
      cellReadOnly = [];
      
      if (!cell || !cell.length){
        if (!schedule.selected.data.length){ //count){
          fblog('scheduleEditShow(): invalid cell passed, no items currently selected');
          return false;
        }
        else {
          cell = scheduleCellFromCoords(schedule.selected.data);
          cellEditable = scheduleCellFromCoords(schedule.selected.getEditable());
          cellReadOnly = scheduleCellFromCoords(schedule.selected.getReadOnly());
          
          if (cell && cell.length){
            cellMeta = schedule.selected.getOrigin({includeReadonly: true});
            cell = scheduleCellFromCoords(cellMeta);
            activeEdit = schedule.activeEdit;
          }
          else {
            return false;
          }
        }
      }
      else {
        cellMeta = scheduleCellGetCoords(cell);
      }
    }
    
    
    // determine if any of these cells are currently being edited
    var isEditing = false;
    for (i in schedule.selected.data){
      if (activeEdit.exists({row: schedule.selected.data[i].row, timeblock: schedule.selected.data[i].timeblock, column: schedule.selected.data[i].column})){
        isEditing = true;
        break;
      }
    }
    
    if (!isEditing){
    
      // only continue if cell currently being edited (if there is one) can be safely closed
      if (scheduleEditHide({noSave: true, target: activeEdit})){

        var
          //task_id,
          preexisting = true,
          previous = false,
          formId,
          formContainer,
          formPrefix,
          prefixLong,
          injectTarget,
          formDef = {},
          //billingRate = 0,
          //currentSchedule = $('#currentPage.schedule'),
          inlineClosure,
          isLocked = false;


          
        if (isHistory){
          activeEdit.set({cells: [{element: cell}]});
          var found = [];
          i = 0;
          
          while (!found.length && i < shared.periods.length){
            found = aa.findObjectByKey(shared.periods[i].tasks.items, cellMeta.task, 'data.id');
            i++;
          }
          
          if (found.length){
            cellMeta.task = found[0].data;
            activeEdit.original = found[0];
          }
          else {
            fblog('no matching task found (' + cellMeta.task + ')');
            return;
          }
          
          activeEdit.data = _.clone(cellMeta.task); //aa.objectClone(cellMeta.task);
          activeEdit.isHistory = true;
          cellMeta.column = activeEdit.data.schedule_start_date;
          cellMeta.timeblock = activeEdit.data.timeblock;
          cellMeta.user = activeEdit.data.physicist_id;
          physicist = shared.physicistsById[activeEdit.data.physicist_id]; //aa.findObjectByKey(shared.physicists, cell.parent().attr('user'), 'physicist_id')[0];

          // formId = 'clientScheduleEditForm';
          // formContainer = 'clientScheduleEditRow';
          prefixLong = 'clientScheduleEdit';
          formPrefix = 'cse_';
          // injectTarget = 'clientScheduleEditInject';

        }
        
        else {
        
          // set all selected cells as those being actively edited
          activeEdit.set({cells: scheduleCellFromCoords(schedule.selected.data)}); //cell);
        
          if (!cell.attr('_id')){
            preexisting = false;
            
            // determine whether other timeblock has a valid task
            var otherCell = scheduleCellGetOther(cell);
            var halfDays = 2;
            var timeblock = cell.parent().attr('timeblock');
            
            if (otherCell.attr('_id')){
              // fblog('other cell is task');
              halfDays = 1;
            }
            else {
              // fblog('other cell is empty');
              halfDays = 2;
            }
            
            // create new task for this cell
            activeEdit.data = new taskObject({
              physicist_id: cellMeta.user, //physicist.physicist_id,
              schedule_start: [cellMeta.column, (timeblock == 'morning' ? ' 08:00:00' : ' 12:00:00')].join(''), //activeEdit.date
              timeblock: timeblock,
              billingmatrix: {1: halfDays}
            });
            
            
            
            var page = aa.findObjectByKey(schedule.pages, schedule.currentPage);
            if (page.length){
              page = page[0];
            
              // determine if there was a previously deleted task in this cell
              var deletedMatch = aa.findObjectByKeys(page.deleted, {schedule_start_date: cellMeta.column, physicist_id: cellMeta.user, timeblock: timeblock});
              if (deletedMatch.length){
                previous = deletedMatch[0];
              }
              
            }
          
          }
          else {
            // activeEdit.data = aa.objectClone(shared.tasks.get({value: cell.attr('_id')}).data);
            activeEdit.data = _.clone(shared.tasks.get({value: cell.attr('_id')}).data);
          }
          
          physicist = shared.physicistsById[activeEdit.data.physicist_id]; //aa.findObjectByKey(shared.physicists, cell.parent().attr('user'), 'physicist_id')[0];

          // formId = 'taskEditForm';
          // formContainer = 'taskEditRow';
          prefixLong = 'taskEdit';
          formPrefix = 'te_';
          // injectTarget = 'taskEditInject';
        }
        

        // abort editor for special or invalid tasks
        if (!physicist){
          fblog('scheduleEditShow(): no physicist defined');
          activeEdit.set(); // clear activeEdit object
          return;
        }
        
        activeEdit.data.billingmatrix = hoursToMatrix(activeEdit.data.billingmatrix);
      
        
        
        
        // get task data for dates adjacent to the origin cell, in case they are on a different page (not retrieved yet)
        var saa = activeEdit.adjacentDates = [
          {},
          {},
          {},
          {}
        ];
        var dates = [];

        dates.push(saa[1].date = getAdjacentWeekday(cellMeta.column, -1));
        dates.push(saa[0].date = getAdjacentWeekday(saa[1].date, -1));
        dates.push(saa[2].date = getAdjacentWeekday(cellMeta.column, 1));
        dates.push(saa[3].date = getAdjacentWeekday(saa[2].date, 1));
        
        scheduleGetSpec({
          dates: dates,
          physicists: [cellMeta.user],
          
          callback: function(data){
            var parsed = shared.lastJSON.generic = eval(['(', data, ')'].join(''));
            var i, j;
            var thisDate, physicist, thisTime, type, typeStr, periodStr, taskYear, taskMonthDay,
              periodLowerMonthDay, periodHigherMonthDay, periodStartDate, periodStartDateTime, periodEndDate, periodStart, periodEnd;
            var saa = activeEdit.adjacentDates;
            var saait;
            
            // update physicist data
            if (parsed.physicists){
              for (i = 0; i < parsed.physicists.length; i++){
                if (shared.physicistsById[parsed.physicists[i].id]){
                  // make sure we copy over each property, so as to maintain references to this object within other arrays (shared.physicists, shared.physicistsByTasktype)
                  for (j in parsed.physicists[i]){
                    shared.physicistsById[parsed.physicists[i].id][j] = parsed.physicists[i][j];
                  }
                }
              }
              
              // show accrued status message if applicable to current task
              if (
                // aa.existsIn(permissions, 'hr-admin')
                // &&
                !cellReadOnly.length // make sure physicists can't see each other's vacation trackers
              ){
                scheduleEditShowAccruedStatus(isHistory);
              }
            }
            
            // sort tasks into dates
            for (i = 0; i < parsed.tasks.length; i++){
              thisDate = aa.findObjectByKey(saa, parsed.tasks[i].schedule_start_date, 'date', false, true);
              if (thisDate){
                if (!thisDate.tasks){
                  thisDate.tasks = [];
                }
                thisDate.tasks.push(parsed.tasks[i]);
              }
            }
            
            // step through dates, flagging as sick, holiday, or vacation
            for (i = 0; i < saa.length; i++){
              if (saa[i].tasks){
                for (j = 0; j < saa[i].tasks.length; j++){
                  saait = saa[i].tasks[j];
                  if (saait.location_id == 847 || saait.location_id == 848){
                    saa[i].sick = true;
                  }
                  if (saait.location_id == 841){
                    saa[i].vacation = true;
                  }
                  if (saait.location_id == 846){
                    saa[i].holiday = true;
                  }
                }
              }
            }
            
          }
          
        });
        
        
        
        
        // determine if the activeEdit cell is locked
        isLocked = cellReadOnly.length && scheduleIsLocked(activeEdit);       
        
        
        formDef = new formObject({
          id: [prefixLong, 'Form'].join(''), //formId,
          container: [prefixLong, 'Row'].join(''), //formContainer,
          injectTarget: [prefixLong, 'Inject'].join(''), //injectTarget,
          //addedInjectTarget: 'taskEditStaticInject',
          prefix: formPrefix,
          prefixLong: prefixLong,
          submitType: 'ajax',
          focus: 'location_name',
          focusCondition: function(){
            return !Boolean(activeEdit.data.location_name);
          },
          dontDifferentiateRequired: true,
          //editTrack: 'activeEdit.dirty',
          validation: $.extend(aa.objectClone(validationDefault), {
            // fields: {
              // billable_hours: function(formDef, val){
                // if (arguments.length){
                // }
                // else {
                  // fblog('no arguments passed');
                // }
              // }
            // }
          }),
          fields: [
            {
              name: 'location_name',
              readOnly: cellReadOnly.length,
              label: 'Facility',
              capitalize: true,
              //required: true,
              //'prefill': true,
              width: 'full',
              prefillVal: locationNameFull(activeEdit.data),
              lookup: 'taskEditFacility',
              lookupVal: 'location_id',
              lookupValPrefill: [(activeEdit.data.location_name || ''), (activeEdit.data.unique_identifier ? [' (', activeEdit.data.unique_identifier, ')'].join('') : '')].join(''),
              // lookupFilter: {
                // location_name: 'Facility name',
                // //contact_name: 'Contact',
                // city: 'City'
              // },
              whenValidCallback: function(){
                return scheduleOnTaskEditChange({isHistory: isHistory});
              },
              // help: '<p>Begin typing the name of a facility to search for it.</p>'
              helpIcon: 'Begin typing the name of a facility, city or contact person to search for it'
            },
            {
              name: 'search',
              readOnly: cellReadOnly.length,
              type: 'button',
              action: 'triggerLookup',
              target: 'location_name',
              attach: 'location_name',
              //style: 'alignForm big',
              text: 'Search'
            },
            {
              name: 'add',
              readOnly: cellReadOnly.length,
              type: 'button',
              action: function(){
                fblog('add new facility');
              },
              displayCondition: function(){
                return aa.existsIn(permissions, 'debug');
              },
              //target: 'location_name',
              attach: 'location_name',
              //style: 'alignForm big',
              text: 'Add New'
            },
            {
              type: 'eol'
            },
            {
              name: 'billable_time',
              readOnly: cellReadOnly.length,
              label: 'Time allocated',
              type: 'dropdown',
              width: 'third',
              //required: true,
              values: [
                { id: 0, name: 'Full day' },
                { id: 1, name: 'Half day' },
                { id: 2, name: 'Hourly' }
                // { id: 3, name: 'Not Billable' }
              ],
              separator: ' + ',
              selectExclusive: [0, 1],
              selectCallback: function(){
                return scheduleOnTaskEditChange({isHistory: isHistory});
              },
              prefillVal: function(){
                var metrics = [];
                // if (schedule.activeEdit.data.billingmatrix[1]){
                  // // full day = 2 half days
                  // if (schedule.activeEdit.data.billingmatrix[1] == 2){
                    // metrics.push(0);
                  // }
                  // // 1 half day
                  // else if (schedule.activeEdit.data.billingmatrix[1] == 1){
                    // metrics.push(1);
                  // }
                // }
                // if (schedule.activeEdit.data.billingmatrix[2]){
                  // metrics.push(2);
                // }
                
                if (activeEdit.data.billable_quantity >= 8){
                  metrics.push(0);
                  if (activeEdit.data.billable_quantity > 8){
                    metrics.push(2);
                  }
                }
                else if (activeEdit.data.billable_quantity >= 4){
                  metrics.push(1);
                  if (activeEdit.data.billable_quantity > 4){
                    metrics.push(2);
                  }
                }
                else if (activeEdit.data.billable_quantity) {
                  metrics.push(2);
                }
                return metrics;
              }() // execute immediately

              // (
                // //task.billingmetric_name || 1
                // (task.billingmetric_name == 'half day' ?
                  // (Number(task.billable_quantity) == 1 ?
                    // 1 // half day
                    // :
                    // (Number(task.billable_quantity) == 2 ?
                      // 0 // full day
                      // :
                      // 2 // not 1 or 2 half days, so default to hourly
                    // )
                  // )
                  // :
                  // 2 // billing metric is hourly
                // )
              // )
            },
            {
              name: 'billable_hours',
              readOnly: cellReadOnly.length,
              label: 'Number of hours',
              width: 'third',
              //required: true,
              prefillVal: 
                // (aa.objectLength(schedule.activeEdit.data.billingmatrix) ? schedule.activeEdit.data.billingmatrix[2] || '' : ''),
                (
                  activeEdit.data.billable_quantity > 8
                  ?
                    am.round(activeEdit.data.billable_quantity - 8, 1)
                    :
                    am.round(activeEdit.data.billable_quantity % 4, 1)
                ) || '',
                    
              displayCondition: function(){
                return scheduleEditActiveTimeIsHourly(isHistory);
              },
              //applicableIf: {billable_time: [2]}, // only show this field if billable_time == 2
              validationCallback: function(formDef, val){
                val = Number(val);
                if (
                  !(
                    isNaN(val) 
                    ||
                    val <= 0
                  )
                ){
                  return true;
                }
                
                return false;
              },
              whenValidCallback: function(){
                return scheduleOnTaskEditChange({isHistory: isHistory});
              },
              // help: '<p>Enter number of hours in decimal form (e.g.: <b>4.5</b>). Note that if you have selected Half Day or Full Day <i>plus</i> Hourly, this is only the number of <i>additional</i> hours.</p>'
              helpIcon: 'Enter number of hours in decimal form (e.g.: 4.5); for Half Day + Hourly or Full Day + Hourly, this is only the number of additional hours'
            },
            // {
              // type: 'eol'
            // },
            {
              name: 'tasktype',
              readOnly: cellReadOnly.length,
              label: 'Modality / department',
              type: 'dropdown',
              displayCondition: function(){
                return scheduleEditNotInternal(isHistory);
              },
              values: scheduleGetAllowedTasktypes(isHistory), //shared.tasktypes,
              selectExclusive: true,
              selectCallback: function(){
                return scheduleOnTaskEditChange({isHistory: isHistory});
              },
              prefillVal: [activeEdit.data.tasktype_id] || null
            },
            {
              name: 'is_billable',
              readOnly: cellReadOnly.length,
              label: 'Is this visit billable',
              type: 'dropdown',
              width: 'third',
              editPermissions: ['accounting', 'schedule-admin'],
              displayCondition: function(){
                return scheduleEditNotInternal(isHistory);
              },
              //required: true,
              values: [
                {id: 1, name: 'Billable'},
                {id: 0, name: 'Not Billable'}
              ],
              selectExclusive: true, //[0, 1],
              selectCallback: function(){
                return scheduleOnTaskEditChange({isHistory: isHistory});
              },
              prefillVal: !activeEdit.data.is_billable ? Number(activeEdit.data.is_billable) : 1
            },
            {
              type: 'eol'
            },
            {
              name: 'summary',
              readOnly: cellReadOnly.length,
              label: scheduleEditNotInternal(isHistory) ? 'Client Summary (will be displayed on invoice)' : 'Brief Summary (visible on schedule)',
              labelCondition: {
                test: function(){ return scheduleEditNotInternal(isHistory); },
                whenTrue: 'Client Summary (will be displayed on invoice)',
                whenFalse: 'Brief Summary (visible on schedule)'
              },
              // displayCondition: scheduleTaskIsInternal,
              capitalize: 'sentence',
              width: aa.existsIn(permissions, 'debug') ? 'threequarters' : 'full',
              autoClone: scheduleEditNotInternal(isHistory) && aa.existsIn(permissions, 'debug'), // automatically create an empty duplicate of this field type (after current field) when current field has text entered
              whenValidCallback: function(){
                return scheduleOnTaskEditChange({isHistory: isHistory});
              },
              prefillVal: activeEdit.data.summary || ''
            },
            {
              name: 'recurrence',
              readOnly: cellReadOnly.length,
              label: 'Recurrence',
              type: 'dropdown',
              width: 'quarter',
              //required: true,
              displayCondition: function(){
                return scheduleEditNotInternal(isHistory) && aa.existsIn(permissions, 'debug');
              },
              enableCondition: scheduleEditAllowUpload,
              values: shared.taskrecurrences, //shared.tasktypes,
              selectExclusive: true,
              selectCallback: function(){
                return scheduleOnTaskEditChange({isHistory: isHistory});
              },
              prefillVal: schedule.activeEdit.data.recurrence_id || 1,  // default to "annual"
              helpIcon: 'When a followup visit is required (if applicable), you will receive a reminder on the website and via email'
            },
            {
              name: 'upload',
              readOnly: cellReadOnly.length,
              type: 'button',
              attach: true,
              upload: true,
              displayCondition: function(){
                return scheduleEditNotInternal(isHistory) && aa.existsIn(permissions, 'debug');
              },
              enableCondition: scheduleEditAllowUpload,
              //action: 'triggerLookup',
              //target: 'location_name',
              // style: 'inline',
              //style: 'alignForm big',
              text: 'Attach File'
            },
            
            // {
              // name: 'recurrenceNotes',
              // readOnly: cellReadOnly.length,
              // label: 'Description of recurrence',
              // required: true,
              // displayCondition: function(){
                // return (schedule.activeEdit.data.recurrence_id == 6);
              // },
              // capitalize: 'sentence',
              // width: 'full',
              // whenValidCallback: scheduleOnTaskEditChange,
              // prefillVal: schedule.activeEdit.data.recurrenceNotes || 'test',
              // help: 'NOTE: This is only for reference - you won\'t receive an automated reminder.'
            // },
            {
              type: 'eol'
            },
            
            // {
              // name: 'upload',
              // readOnly: cellReadOnly.length,
              // type: 'button',
              // upload: true,
              // displayCondition: function(){
                // return scheduleEditNotInternal(isHistory) && aa.existsIn(permissions, 'debug');
              // },
              // //action: 'triggerLookup',
              // //target: 'location_name',
              // style: 'floatRight',
              // //style: 'alignForm big',
              // text: 'Add File'
            // },
            // {
              // type: 'eol'
            // },
            
            // {
              // type: 'static',
              // description: '<p>Report not submitted</p>'
            // },
            // {
              // type: 'eol'
            // },
            
            {
              name: 'notes',
              type: 'textarea',
              readOnly: cellReadOnly.length,
              label: scheduleEditNotInternal(isHistory) ? 'Notes (for internal use only)' : 'Detailed Notes (not visible on schedule)',
              labelCondition: {
                test: function(){ return scheduleEditNotInternal(isHistory); },
                whenTrue: 'Notes (for internal use only)',
                whenFalse: 'Detailed Notes (not visible on schedule)'
              },
              width: 'full',
              whenValidCallback: function(){
                return scheduleOnTaskEditChange({isHistory: isHistory});
              },
              prefillVal: activeEdit.data.notes || ''
            }
            
          ]
        });
        
        // add this form to global forms array
        forms[formDef.id] = formDef;
        
        var editHTML = [
          '<tr class="taskEditRow expand-child" id="', prefixLong, 'Row"><td colspan="'
        ];
        
        if (!isHistory){
          editHTML.push(
            // calculate total number of columns minus (2 navigation buttons on table sides) minus (number of weekend dates if they are currently not shown)
            schedule.currentPageElement[0].rows[1].cells.length - (schedule.preferences.display.weekends ? 0 : 2) - (schedule.preferences.display.view == 'month' ? 0 : 0) 
          );
        }
        else {
          //editHTML.push(cell[0].cells.length);
          editHTML.push(cell.closest('table').children('thead').children('tr')[0].cells.length);
        }
        
        editHTML.push(
          '"><div class="taskEdit', cellReadOnly.length ? ' taskEditReadonly' : '', '"',
          //(isHistory ? ' style="border-style: solid; border-color: #eee; border-width: 1px 0"' : ''),
          '>',
          '<div class="taskEditHeader">',
          '<div class="taskEditTitle"><span style="color: #fff">'
        );
        
        
        if (!isHistory && schedule.selected.data.length > 1){
          editHTML.push(
            (activeEdit.data.audit_user ? (cellReadOnly.length ? 'View Schedule Items' : 'Edit Schedule Items') : 'Schedule New Item'),
            ':</span> &lt;multiple items selected&gt;'
          );
        }
        else {
          editHTML.push(
            (activeEdit.data.audit_user ? (cellReadOnly.length ? 'View Schedule Item' : 'Edit Schedule Item') : 'Schedule New Item'),
            ':</span> ',
            physicist.name_first,
            ' ',
            physicist.name_last,
            ', ',
            dpExactDate(cellMeta.column /*activeEdit.column*/).toString('MMMM dS'),
            ' (',
            cellMeta.timeblock, //activeEdit.timeblock,
            ')'
          );
        }
        
        editHTML.push(
          '</div>',
          '<table class="taskEditSubtitle" cellspacing="0" cellpadding="0"><tr><td>'
        )
        
        if (cellReadOnly.length){
          editHTML.push(
            buttonCreate(formDef, {
              name: 'taskDiscard',
              target: activeEdit,
              action: 'taskEditDiscard',
              text: 'Close viewer',
              style: 'inline left-align',
              css: 'icon-undo iconleft'
            })
          );
        }
        else {
          editHTML.push(
            buttonCreate(formDef, {
              name: 'taskSave',
              target: activeEdit,
              action: 'taskEditSave',
              text: 'Save changes',
              style: 'inline left-align',
              css: 'icon-ok iconleft'
            }),
                      
            buttonCreate(formDef, {
              name: 'taskDiscard',
              target: activeEdit,
              action: 'taskEditDiscard',
              text: 'Discard changes',
              style: 'inline left-align',
              css: 'icon-undo iconleft'
            }),
            
            buttonCreate(formDef, {
              name: 'taskDelete',
              target: activeEdit,
              action: 'taskEditDelete',
              text: 'Delete this task',
              style: 'inline left-align',
              css: 'icon-cancel iconleft'
            })
          );
        }
        
        editHTML.push(
          '</td><td class="right">'
        );
        
        // var historyLink = true;
        
        // if (aa.existsIn(permissions, 'debug')){
          // historyLink = true;
        // }
        
        // if (historyLink){
          editHTML.push('<a id="link', prefixLong, 'TaskHistory" title="Click to view edit history" class="linkWhite" style="position: relative; top: .5em;">');
        // }

        if (preexisting){
          editHTML.push(['<span class="smallCaps">Last edited</span> ', dpExactDateTime(activeEdit.data.audit_start).toString('MMMM dS @ h:mmt'), ' <span class="smallCaps">by</span> <b>', (activeEdit.data.location_id == 992 && activeEdit.data.audit_user == 122 ? schedule.autoUserName : activeEdit.data.audit_user_name), '</b>'].join(''));
        }
        else if (previous){
          editHTML.push(['<span class="smallCaps">Previous task deleted</span> ', dpExactDateTime(previous.audit_end).toString('MMMM dS @ h:mmt'), ' <span class="smallCaps">by</span> <b>', (previous.location_id == 992 && previous.audit_user_end == 122 ? schedule.autoUserName : previous.audit_user_end_name), '</b>'].join(''));
        }
        else {
          editHTML.push('<span class="smallCaps">Search for previous edits</span>');
        }
        // if (historyLink){
          editHTML.push('</a>');
        // }
        
        editHTML.push(
          '</td></tr></table>',
          
          '</div>',
          
          '<div id="', prefixLong, 'HistoryInject" class="hidden taskEditContent taskEditHistory" />',
          
          '<div class="taskEditMessage callout calloutWide calloutStatic" id="', prefixLong, 'Message" style="margin-top: 0;', (!isLocked ? ' display: none;' : ''), '">',
          (isLocked ? '<span class="highlightWarning">LOCKED:</span> This month has been locked to prevent billing discrepancies.  Please contact Jennifer if you wish to make any further changes.' : ''),
          '</div>',

          '<div class="taskEditContent" id="taskEditContent">',
          
          // '<p>' + task.location_name + '</p>' +
          // as.echoIf(task.tasktype, '<p>', '</p>') +
          // '<p>' + task.schedule_start + '</p>' +
          // '<p>' + task.billable_quantity + ' ' + task.billingmetric_name + ' ($' + (task.billable_quantity * task.rate) + ')</p>' +
          
          '<div id="', formDef.injectTarget, '" />',
          
          '</div>',
          
          '<div id="taskEditStaticInject" class="body hidden" style="margin-top: 1em" />'
          
          
        );
        
        if (aa.existsIn(permissions, 'debug')){
          editHTML.push(['<div class="taskEditHeader right"><span class="smallCaps">Task ID:</span> <b>', activeEdit.data.id, '</b></div>'].join(''));
        }
        
        editHTML.push(
          '</div></td></tr>'
        );
        
        editHTML = editHTML.join('');
        
        // insert edit row below selected timeblock
        // $(['#currentPage > tbody > tr.timeblockRowLast[row=', originCell.parent().attr('row'), ']'].join('')).after(editHTML);
        // $(['#tr', originCell.parent().attr('row'), 'afternoon'].join('')).after(editHTML);
        if (!$('#jq-wip').length){
          $('#hidden_container').append('<div id="jq-wip" />');
        }
        $('#jq-wip').html(editHTML);
        
        var editRow = $(['#', prefixLong, 'Row'].join(''));
        
        formBuild(formDef, formDef.injectTarget, function(){
          // $('#taskEditShow').show();
          // scheduleEditOnShow(formDef);
        });
        
        
        


        // only show details link if this isn't part of a client history list
        if (!isHistory){
          $(['#tr', cell.parent().attr('row'), 'afternoon'].join('')).after(editRow);
        
          // insert details link
          var detailsLink = ['<td class="inputLabelRight">'];
          var detailsLinkFacility = ['<a id="linkDetailsFacility" '];
          var detailsLinkTime = ['<a id="linkDetailsTime" '];
          
          if (!activeEdit.data.account_id || activeEdit.data.account_id == 99) {
            detailsLinkFacility.push(' class="hidden"');
          }
          else {
            detailsLinkTime.push(' class="hidden"');
          }
          
          detailsLinkFacility.push('><span class="iconLink icon-view iconleftgapless" style="zoom: 1;">');
          detailsLinkTime.push('><span class="iconLink icon-view iconleftgapless" style="zoom: 1;">');
          
          detailsLinkFacility.push('View facility details');
          detailsLinkTime.push('View personal time tracker');
          
          detailsLinkFacility.push('</span></a>');
          detailsLinkTime.push('</span></a>');
          
          detailsLink = detailsLink.concat(detailsLinkFacility);
          
          // only show time tracker link if this isn't a read only editor
          if (
            // aa.existsIn(permissions, 'hr-admin')
            // &&
            !cellReadOnly.length // make sure physicists can't see each other's vacation trackers
            &&
            activeEdit.data.location_id != 840 // don't show time tracker link for "not found" tasks
          ){
            detailsLink = detailsLink.concat(detailsLinkTime);
          }
          
          detailsLink.push('</td>');
          
          $('#te_location_name_label').parent().after(detailsLink.join(''));
          
          
          
          // browse link click functionality
          $('#linkDetailsFacility').click(function(){
            scheduleTaskEditInjectDetail();
          });
          $('#linkDetailsTime').click(function(){
            scheduleTaskEditInjectDetail();
          });
          
          if (activeEdit.data.cod){
            $(['#', prefixLong, 'Message'].join('')).append('<p class="highlightWarning"><b>NOTE</b>: COD - you MUST coordinate payment with BioMed office staff BEFORE scheduling service.</p>').show();
          }
          
        }
        else {
          // blur schedule cell editor (if present)
          $('#taskEditRow div.taskEdit').addClass('taskEditBlurred');

          // insert history schedule editor
          cell.filter(':last').after(editRow);
          
          // send update trigger to tablesorter
          $('#historyTable').trigger('update'); 
        }
        
        // browse link click functionality
        $(['#link', prefixLong, 'TaskHistory'].join('')).click(function(){
          // if (schedule.activeEdit.data.location_id){ // only if this is a valid task
            scheduleGetTaskHistory($(['#', prefixLong, 'HistoryInject'].join('')), activeEdit);
          // }
        });
        
        scheduleEditOnShow(formDef);
        
        
        formSetupEventHandlers(formDef, editRow);
        $('#currentPage').enableTextSelect();


        

        
        // hack to work around firefox border bug when inserting new row
        // otherwise all rows above inserted row have collapsed borders rendered incorrectly
        // if ($.browser.mozilla){
          // $('table.schedule').css('border-collapse', 'separate');
          // setTimeout("$('table.schedule').css('border-collapse', 'collapse');", 1);
        // }
        
        
      } // end if scheduleEditHide
    
    } // end if not same as curent
    
    cell.removeClass('taskloading');
    
    timerEnd(arguments);

    
  }
  
  
  
  
  function locationNameFull(data){
    var nameArr = [];
    
    nameArr.push(data.location_name);

    if (data.unique_identifier){
      nameArr.push(' (', data.unique_identifier, ')');
    }
    else if (data.num_locations > 1 && data.city){
      nameArr.push(' (', data.city, ')');
    }
    
    return nameArr.join('');
  }
  
  
  
  // show message re accrued time usage IF applicable to current task (vacation, sick, etc.)
  function scheduleEditShowAccruedStatus(config){
    var physicist, thisTime, type, typeStr, periodStr, taskYear, taskMonthDay,
      periodLowerMonthDay, periodHigherMonthDay, periodStartDate, periodStartDateTime, periodEndDate, periodStart, periodEnd;
  
    var formDef, activeEdit;
    
    if (config && config.isHistory){
      formDef = forms.clientScheduleEditForm;
      activeEdit = clients.scheduleEdit;
    }
    else {
      formDef = forms.taskEditForm;
      activeEdit = schedule.activeEdit;
    }
    
    activeEdit.data.location_id = Number(activeEdit.data.location_id);
    
    if (
      // aa.existsIn(permissions, 'hr-admin')
      // &&
      // schedule.activeEdit.cells.length == 1 // only 1 cell being edited
      // &&
      activeEdit.data.account_id == 99 // internal task
      &&
      aa.existsIn([
        841, // vacation
        847, // sick self
        848, // sick family
        844, // seminar
        849, // jury duty
        999 // funeral
      ], activeEdit.data.location_id) // accrued time task
    ){
      physicist = shared.physicistsById[activeEdit.data.physicist_id];
      
      showTimeStatus:
      
      if (physicist.time){
        switch (activeEdit.data.location_id){
          case 841:
            thisTime = physicist.time.offset6;
            periodStr = 'accrual period';
            taskMonthDay = [ad.sqlGetMonth(activeEdit.data.schedule_start_date), ad.sqlGetDay(activeEdit.data.schedule_start_date)].join('-');
            taskYear = ad.sqlGetYear(activeEdit.data.schedule_start_date);
            if (taskMonthDay < thisTime.dateLower){
              periodStartDate = dpExactDate([taskYear - 1, thisTime.dateHigher].join('-'));
            }
            else if (taskMonthDay >= thisTime.dateHigher){
              periodStartDate = dpExactDate([taskYear, thisTime.dateHigher].join('-'));
            }
            else {
              periodStartDate = dpExactDate([taskYear, thisTime.dateLower].join('-'));
            }
            
            periodStart = periodStartDate.toString('M/d/yyyy');
            periodEndDate = periodStartDate.clone().addMonths(6).addDays(-1);
            periodEnd = periodEndDate.toString('M/d/yyyy');
            
            periodStartDateTime = dateTimeString(periodStartDate);
            if (periodStartDateTime == thisTime.curr.start){
              thisTime = thisTime.curr;
            }
            else if (periodStartDateTime == thisTime.prev.start){
              thisTime = thisTime.prev;
            }
            else if (periodStartDateTime == thisTime.next.start){
              thisTime = thisTime.next;
            }
            else {
              break showTimeStatus; //thisTime = null;
            }
            break;
            
          case 844:
          case 847:
          case 848:              
          
          
            thisTime = physicist.time.offset12;
            periodStr = 'accrual period';
            // taskMonthDay = [ad.sqlGetMonth(schedule.activeEdit.data.schedule_start_date), ad.sqlGetDay(schedule.activeEdit.data.schedule_start_date)].join('-');
            // taskYear = ad.sqlGetYear(schedule.activeEdit.data.schedule_start_date);
            if (activeEdit.data.schedule_start < thisTime.prev.start){
              break showTimeStatus; //thisTime = null;
            }
            else if (activeEdit.data.schedule_start < thisTime.curr.start){
              periodStartDate = dpExactDateTime(thisTime.prev.start);
              thisTime = thisTime.prev;
            }
            else if (activeEdit.data.schedule_start < thisTime.next.start){
              periodStartDate = dpExactDateTime(thisTime.curr.start);
              thisTime = thisTime.curr;
            }
            else {
              periodStartDate = dpExactDateTime(thisTime.next.start);
              thisTime = thisTime.next;
            }
            
            periodStart = periodStartDate.toString('M/d/yyyy');
            periodEndDate = periodStartDate.clone().addMonths(12).addDays(-1);
            periodEnd = periodEndDate.toString('M/d/yyyy');
            
            periodStartDateTime = dateTimeString(periodStartDate);
            break;
            
          default:
            thisTime = physicist.time.annual;
            periodStr = 'calendar year';
            taskYear = ad.sqlGetYear(activeEdit.data.schedule_start_date);
            if (taskYear == thisTime.curr.year){
              thisTime = thisTime.curr;
            }
            else if (taskYear == thisTime.prev.year){
              thisTime = thisTime.prev;
            }
            else if (taskYear == thisTime.next.year){
              thisTime = thisTime.next;
            }
            else {
              break showTimeStatus; //thisTime = null;
            }
            periodStartDate = dpExactDate([taskYear, '-01-01'].join(''));
            periodStart = periodStartDate.toString('M/d/yyyy');
            periodEndDate = periodStartDate.clone().addYears(1).addDays(-1);
            periodEnd = periodEndDate.toString('M/d/yyyy');
            break;
        }
        
        switch (activeEdit.data.location_id){
          case 841:
            type = typeStr = 'vacation';
            break;
            
          case 847:
          case 848:
            type = typeStr = 'sick';
            break;
            
          case 844:
            type = typeStr = 'seminar';
            break;
            
          case 849:
            typeStr = 'jury duty';
            type = 'juryduty';
            break;
            
          case 999:
            type = typeStr = 'funeral';
            break;
        }
        
        $(['#', formDef.prefixLong, 'Message'].join('')).append(['<p><b>NOTE:</b> You are currently using <span class="bold ', (thisTime[type] <= thisTime[[type, 'Available'].join('')] ? '' : 'highlightWarning'), '">', thisTime[type],' of ', thisTime[[type, 'Available'].join('')], '</span> available ', typeStr, ' days for this ', periodStr, ' <span class="smallCaps">(', periodStart, ' - ', periodEnd, ')</span>.</p>'].join('')).show();
      }
    }
  }
  
  
  
  

  
  
  function scheduleTaskEditInjectDetail() {
    if (!schedule.activeEdit.data.account_id || schedule.activeEdit.data.account_id == 99){
      var isPhysicist = aa.keyExists(shared.physicistsById, user.id),
          canViewMultiple = !isPhysicist || aa.existsIn(permissions, 'schedule-admin');
          
      var url = [
          shared.urlRoot,
          '/staff', 
          (!canViewMultiple ? '' : ['?physicist=', schedule.activeEdit.data.physicist_id].join('')),
          '#time'
        ].join('');
        
      if (!shared.windows){
        shared.windows = {};
      }
      if (!shared.windows.staff || shared.windows.staff.closed){
        shared.windows.staff = window.open(url);
      }
      else {
        var childDocument = $(shared.windows.staff.document);
        var childContext = shared.windows.staff.Acat.jq;
        var timeControlForm = childContext.forms.timeControls;
        
        // bring existing child window to foreground
        shared.windows.staff.focus();
        
        // change user if applicable
        if (canViewMultiple) { // !isPhysicist){
          if ($('#timeControls', childDocument).length){
            $(['#timeControls [name=displayPhysicists]'].join(''), childDocument).attr('checked', false);    
            $(['#timeControls [name=displayPhysicists][value=', schedule.activeEdit.data.physicist_id, ']'].join(''), childDocument).attr('checked', true);
            var dropdownId = [timeControlForm.prefix, 'displayPhysicists'].join('');
            $(['#', dropdownId].join(''), childDocument).next('.multiSelectOptions').multiSelectUpdateSelected(aa.findObjectByKey(timeControlForm.dropdowns, dropdownId)[0].config);
            childContext.staffTimeUpdate(null, schedule.activeEdit.data.physicist_id);
          }
        }
        
        // scroll to time tracker
        shared.windows.staff.location.hash = '#time';
        // scroll up slightly
        $(shared.windows.staff).scrollTo('-=50px', 0);
      }
    }
    else {
      var url = [
          shared.urlRoot,
          '/clients/',
          schedule.activeEdit.data.location_id
        ].join('');
        
      window.open(url);
    
      // var injectTarget = $('#taskEditStaticInject');
      
      // injectTarget
        // .removeClass('gapTop')
        // .css({
          // 'margin-top': '1em',
          // 'background-color': '#fff'
        // });
    
      // autocompleteResultClient('client', null, schedule.activeEdit.data.location_id, injectTarget);
    }
  }

  
  
  function scheduleGetAllowedTasktypes(isHistory){
    var location_id;
    
    if (
      (
        isHistory
        &&
        clients.scheduleEdit && clients.scheduleEdit.data && (location_id = clients.scheduleEdit.data.location_id)
      )
      ||
      (
        !isHistory
        &&
        schedule.activeEdit && schedule.activeEdit.data && (location_id = schedule.activeEdit.data.location_id)
      )
    ){
      var forceTypes = aa.findObjectByKey(schedule.forceTypes, location_id, 'facility');
      
      if (forceTypes.length){
        forceTypes = forceTypes[0].allowed;
        
        if (forceTypes && forceTypes.length){
          forceTypes = aa.findObjectByKey(shared.tasktypes, forceTypes);
          if (forceTypes.length){
            aa.sortByKey(forceTypes, 'name');
            return forceTypes;
          }
        }
      }
    }
    
    // don't return non-modality tasktypes
    var tt = aa.findObjectByKey(shared.tasktypes, 1, 'is_modality');
    
    if (tt.length){
      return tt;
    }
    
    return shared.tasktypes;
  }
  
  
  
  
  function scheduleIsLocked(param){
    var i, match = false;
    
    if (typeof param == 'object'){
      param = param.data.schedule_start_date;
    }
    
    if (param <= schedule.lock_lastday){
      for (i = 0; i < schedule.lock_exceptions.length; i++){
        if (param.indexOf(schedule.lock_exceptions[i]) == 0){
          match = true;
          break;
        }
      }
      
      if (!match){
        // is locked
        return true;
      }
    }
    
    return false;
  }
  
  
  
  function scheduleIsNotLocked(param){
    return !scheduleIsLocked(param);
  }
  
  
  function scheduleGetTaskHistory(target, activeEdit){
    ad.stopwatchStart('ajaxScheduleGetTaskHistory');
    
    target.html('<p style="margin: 0; padding: .5em">Searching...</p>').removeClass('hidden');
    var params = {
      // id: schedule.activeEdit.data.id,
      user_id: activeEdit.data.physicist_id,
      schedule_start: activeEdit.data.schedule_start
    };
    
    $.ajax({
      url: [shared.urlRoot, '/ajaxSSGet/scheduleGetTaskHistory'].join(''),
      data: params,
      //type: 'POST',
      
      success: function (data, textStatus){
        statusBannerHide();
        var logstringArr = [['scheduleGetTaskHistory(): retrieved from server (', (ad.stopwatchEnd('ajaxScheduleGetTaskHistory') / 1000), 's)'].join('')];
        var 
          decoded, 
          i, 
          set,
          historyHTML = [],
          rowClassArray,
          auditStart,
          auditEnd,
          timeString,
          quantity;
      
        try {
          decoded = shared.lastJSON.generic = eval(["(", data, ")"].join(''));
        }
        catch (err){
          fblog(['scheduleGetTaskHistory() decode failure: ', err.name, ', ', err.message].join(''));
          return;
        }
        
        if (!decoded.length){
          logstringArr.push('no history items found');
          historyHTML.push(
            '<p style="margin-top: 0; padding: .5em 0">No previous edits found.</p>'
          );
        }
        else {
          historyHTML.push(
            '<div class="flatContainer"><div class="body" style="background-color: #fff; padding-bottom: 0">',
            '<table id="', (activeEdit.isHistory ? 'clientSchedule' : 'task'), 'EditHistoryTable" class="taskEditHistory tablesorter">',
              '<thead>',
                '<tr>',
                '<th>Edited at</th><th>Edited by</th><th>Facility</th><th>Time allocated</th><th>Modality</th><th>Summary</th>'
          );
          
          if (aa.existsIn(permissions, 'admin')){
            historyHTML.push('<th>Task ID</th>');
          }
          
          historyHTML.push(
                '</tr>',
              '</thead>',
              '<tbody>'
          );
        
          for (i = 0; i < decoded.length; i++){
            timeString = [];
            rowClassArray = [];
            quantity = Number(decoded[i].billable_quantity);

            if (decoded[i].audit_start == activeEdit.data.audit_start){
              rowClassArray.push('current');
            }
            else {
              rowClassArray.push('past');
            }
            
            if (
              quantity >= 8
            ){
              timeString.push("full day");
              if (quantity > 8){
                timeString.push(" + ", Number((quantity - 8).toFixed(2)), " hour", (quantity - 8 != 1 ? 's' : ''));
              }
            }
            else if (
              quantity >= 4
            ){
              timeString.push("half day");
              if (quantity > 4){
                timeString.push(" + ", Number((quantity - 4).toFixed(2)), " hour", (quantity - 4 != 1 ? 's' : ''));
              }
            }
            else {
              timeString.push(Number(quantity.toFixed(2)), " hour", (quantity != 1 ? 's' : ''));
            }
            
            auditStart = dpExactDateTime(decoded[i].audit_start);

            historyHTML.push(
              '<tr class="', rowClassArray.join(' '), '">',
                '<td class="nowrap"><span class="hidden">', decoded[i].audit_start, '</span>', auditStart.toString('M/d'), '&nbsp;&nbsp;', auditStart.toString('h:mmt'), '</td>',
                '<td>', (decoded[i].location_id == 992 && decoded[i].audit_user == 122 ? schedule.autoUserName : decoded[i].audit_user_name), '</td>',
                '<td>', decoded[i].location_name
            );
            
            if (decoded[i].num_locations > 1){
              if (decoded[i].unique_identifier){
                historyHTML.push(' (', decoded[i].unique_identifier, ')');
              }
              else if (decoded[i].city){
                historyHTML.push(' (', decoded[i].city, ')');
              }
            }
              
            historyHTML.push(
                '</td>',
                '<td>', timeString.join(''), '</td>',
                '<td>', (decoded[i].tasktype_id ? aa.findObjectByKey(shared.tasktypes, decoded[i].tasktype_id)[0].name : ''), '</td>',
                '<td>', decoded[i].summary, '</td>'
            );
            
            if (aa.existsIn(permissions, 'admin')){
              historyHTML.push('<td>', decoded[i].id, '</td>');
            }
            
            historyHTML.push(
              '</tr>'
            );
            
            if (!Number(decoded[i].is_billable)){
              // aa.pushUnique(rowClassArray, 'notes');
              // aa.pushUnique(rowClassArray, 'expand-child');
              
              historyHTML.push([
                '<tr class="', rowClassArray.concat('notes expand-child').join(' '), '"><td class="notesSubtree"></td><td class="notesContent" colSpan="6"><div class="notesWarning">Not billable</div></td></tr>'
              ].join(''));
            }
            
            if (decoded[i].notes){
              decoded[i].notes = $.trim(decoded[i].notes);
              if (decoded[i].notes){
                // aa.pushUnique(rowClassArray, 'notes');
                // aa.pushUnique(rowClassArray, 'expand-child');
                
                historyHTML.push(
                  '<tr class="', rowClassArray.concat('notes expand-child').join(' '), '"><td class="notesSubtree"></td><td class="notesContent" colSpan="6"><div>', as.replaceAll(as.replaceAll(decoded[i].notes, '\n\\s*\n', '<hr />'), '\n', '<br />'), '</div></td></tr>'
                );
              }
            }
            
            // // insert deleted status as child row
            // if (decoded[i].audit_user_end){
              // rowClassArray.push('notes', 'expand-child');

              // auditEnd = dpExactDateTime(decoded[i].audit_end);

              // historyHTML.push([
                // '<tr class="', rowClassArray.join(' '), '"><td class="notesSubtree"></td><td class="notesContent" colSpan="6"><div class="notesWarning">Deleted ', auditEnd.toString('M/d @ h:mmt'), ' by ', decoded[i].audit_user_end_name, '</div></td></tr>'
              // ].join(''));
            // }
            
            if (decoded[i].audit_user_end){
              auditEnd = dpExactDateTime(decoded[i].audit_end);
              
              historyHTML.push(
                '<tr class="', rowClassArray.join(' '), '">',
                  '<td class="nowrap highlightMedium"><span class="hidden">', decoded[i].audit_end, '</span>', auditEnd.toString('M/d'), '&nbsp;&nbsp;', auditEnd.toString('h:mmt'), '</td>',
                  '<td class="highlightMedium">', (decoded[i].location_id == 992 && decoded[i].audit_user == 122 ? schedule.autoUserName : decoded[i].audit_user_end_name), '</td>',
                  '<td colSpan="4" class="highlightMedium">DELETED</td>'
              );
              
              if (aa.existsIn(permissions, 'admin')){
                historyHTML.push('<td class="highlightMedium">', decoded[i].id, '</td>');
              }
              
              historyHTML.push(
                '</tr>'
              );
              
            }
            
          }
          
          historyHTML.push(
              '</tbody>',
            '</table></div></div>'
          );
          
          //target.css('margin-bottom', '1em');
          
        }
        
        target.html(historyHTML.join('')).removeClass('hidden');

        var sortTable = $(['#', (activeEdit.isHistory ? 'clientSchedule' : 'task'), 'EditHistoryTable'].join(''));
        if (sortTable.length && sortTable.get(0).rows.length > 1){
          sortTable.tablesorter({
            headers: {0: {order: 1} },
            sortList: [[0,1]],
            textExtraction: 'simple',
            widgets: ['zebra'],
            widgetZebra: {css: ['evenRow', '']}
          });
          
          // manually apply widgets after applying pager
          //sortTable.trigger("applyWidgets");

          tableRowHoverSetup(sortTable, 'highlightgreen');
        }
        
        
        fblog(logstringArr.join(', '));
      },
      
      error: function (XMLHttpRequest, textStatus, errorThrown){
        fblog(['scheduleGetTaskHistory(): ajax failure = ', textStatus, ' (', errorThrown, ')'].join(''));
        statusBannerConnectionError();
      }
      
    });
  }
  
  
  


  
  // determine if task currently being edited is an internal (meeting, office, sick, etc.) or external (client facility) task
  function scheduleTaskIsInternal(task, isHistory){
    if (!task){
      task = (isHistory ? clients.scheduleEdit : schedule.activeEdit);
    }
    
    if (task.data.location_id != 840 && task.data.account_id == 99){
      // fblog(['editIsInternal = true, account_id = ', String(activeEdit.data.account_id)].join(''));
      return true;
    }
    else {
      // fblog(['editIsInternal = false, account_id = ', String(activeEdit.data.account_id)].join(''));
      return false;
    }
  }  
  
  // shortcut to return opposite of scheduleTaskIsInternal()
  function scheduleEditNotInternal(isHistory){
    return !scheduleTaskIsInternal(null, isHistory);
  }
  
  
  function scheduleEditAllowUpload(formDef, fieldDef){
    // fblog('scheduleEditAllowUpload():');
    // fbdir('arguments', true, arguments);
    var button = $(['#', formDef.prefix, fieldDef.name].join(''));
    //if (button.length && button.upload){
    if (button.parent().prev().prev().children('input').val()){
      return true;
    }
    
    return false;
  }

  
  
  function scheduleEditActiveTimeIsHourly(isHistory){
    var activeEdit = (isHistory ? clients.scheduleEdit : schedule.activeEdit);
    
    if (
      am.round(activeEdit.data.billable_quantity % 4, 1)
      ||
      am.round(activeEdit.data.billable_quantity > 8, 1)
    ){
      return true;
    }
    else {
      return false;
    }
  
    // if (
      // schedule.activeEdit.data.billingmatrix[2]
    // ){
      // return true;
    // }
    // else {
      // return false;
    // }
  }
  
  
  
  function scheduleEditOnShow(formDef){
    var 
      editRow = $(['#', formDef.container].join('')),
      viewport = $(window),
      i;
    
    
    // update each dropdown arrow icon's position relative to each input element
    editRow.find('input.multiSelect').each(function(){
      var input = $(this);
      
      var inputPos = input.position();
      var inputWidth = input.width();
      // var dda = input.siblings('div.dropdownArrow');
      // dda.css({
        // 'top': inputPos.top,
        // 'left': inputPos.left + inputWidth
      // }).removeClass('hidden');
    });
    
    // iterate through textareas, adding special behavior (autogrow)
    for (i = 0; i < formDef.textareas.length; i++){
      //fblog(formDef.textareas[i]);
      $(['#', formDef.textareas[i]].join('')).growfield({
        offset: 2,
        animate: false
      });
    }    
    
    // fblog('scheduleEditShow(): editRow.height = ' + editRow.height() + ', window.height = ' + viewport.height() + ', offsets = ' + aa.var_dump(centerOffsets(mouseDown.element)));
    if (editRow.height() >= viewport.height()){
      viewport.scrollTo(editRow);
    }
    else {
      viewport.scrollTo(editRow, {offset: centerOffsets(editRow)});
    }
    
    //forceFocus([formDef.prefix, formDef.focus].join(''));
  }

  
  

  // hide task editing panel
  function scheduleEditHide(config){
    if (!config){
      config = {};
    }
    
    // client history
    if (config.target && config.target.isHistory){
      if (config.target.cells.length){
        // clone cells array, since this will be emptied when calling commit
        var cells = _.clone(config.target.cells);

        if (!(config.noSave || config.deleteItem)){
          scheduleOnTaskEditChange({isHistory: true});
        }
        
        if (config.target.commit && typeof config.target.commit == 'function' && !config.target.commit(config)){
          return false;
        }

        // apply formatting to edited items
        _.each(cells, function(element){
          element.element.removeClass('highlightbluedark');
        });
        
        // remove dropdown results if visible
        $('div.ac_results').hide();
        
        var bound = $('#clientScheduleEditRow').find('input, textarea, .button');
        bound.unbind();
        $('#clientScheduleEditRow').remove();
        
        // unblur schedule cell editor (if present)
        $('#taskEditRow div.taskEdit').removeClass('taskEditBlurred');
      }
    }
    
    // schedule
    else {
      // first check for inline client schedule editor, and close it first
      if (clients.scheduleEdit.cells.length){
        scheduleEditHide({target: clients.scheduleEdit, noSave: true});
      }
      
      if (schedule.activeEdit.cells.length){
    
        if (!(config.noSave || config.deleteItem)){
          // first validate all fields and set dirty bit if necessary
          scheduleOnTaskEditChange(); //($('#te_location_name'));
        }

        // only do this if editor is currently active
        if (!schedule.activeEdit.commit(config)){
          return false;
        }

        // remove dropdown results if visible
        $('div.ac_results').hide();
        
        // $('div.taskEdit').slideUp(100, function (e){
          // parents('tr.taskEdit').remove();
        // });
        $('#taskEditRow').remove();

        // remove extra rowspan from side buttons
        //$('#buttonTableSideLeft, #buttonTableSideRight').attr('rowSpan', schedule.currentPageElement[0].rows.length + 1);

        $('#currentPage > tbody > tr > td.taskediting').removeClass('taskediting');
        $('#currentPage > tbody > tr > td.taskeditingReadonly').removeClass('taskeditingReadonly');

        var cellMeta = scheduleCellFromCoords(schedule.selected.getOrigin({includeReadonly: true}));
        $(window).scrollTo(cellMeta, {offset: centerOffsets(cellMeta)});

        updaterKill('billingProgressPaperless', 'itemSaveDirty');

      }
      
      // only disable text select if we're hiding a schedule cell editor
      $('#currentPage').disableTextSelect();
    
    }
    
    scheduleSaveDirty();
    
    return true;
  }
  

  
  
  // callback every time location is changed in task editor
  function scheduleOnTaskEditLocationChange(config){
  
    var formObj = config.el.parents('form');
    var formDef = forms[formObj.attr('id')];
    var i, field, fieldElement;
    var forceTypes;
    var sad = schedule.activeEdit.data;
    var saa = schedule.activeEdit.adjacentDates;
    
    forceTypes = aa.findObjectByKey(schedule.forceTypes, sad.location_id, 'location');
    if (forceTypes.length){
      //fblog('matchLocation');
      forceTypes = forceTypes[0];
    }
    else {
      forceTypes = null;
    }
  
    $('#taskEditMessage').hide().empty();
    
    if (!config.isHistory){
      $('#taskEditStaticInject').addClass('hidden');
    }
    
    // pick default task type for predefined locations/physicists
    var currTaskType = $(['#', formDef.id, ' [name=tasktype]:checked'].join(''));

    var textbox = $('#te_tasktype');
    var multiSelect = textbox.next('.multiSelectOptions');
    var allowedTypes = [];
    
    var displayElement;
    
    if (!scheduleEditNotInternal(config.isHistory)){
      sad.tasktype_id = null;
      if (currTaskType.length){
        currTaskType.attr('checked', false);
        
        multiSelect.find('LABEL.hover INPUT:checkbox').attr('checked', false);
        multiSelect.multiSelectUpdateSelected(schedule.multiselect.options);
        multiSelect.find('LABEL').removeClass('checked').find('INPUT:checked').parent().addClass('checked');
      }
    }
    else {
      if (forceTypes){
        if (forceTypes.allowed){
          if (currTaskType.length){
            currTaskType.attr('checked', false);
            
            multiSelect.find('LABEL.hover INPUT:checkbox').attr('checked', false);
            multiSelect.multiSelectUpdateSelected(schedule.multiselect.options);
            multiSelect.find('LABEL').removeClass('checked').find('INPUT:checked').parent().addClass('checked');
          }
          allowedTypes = forceTypes.allowed;
        }
        else if (!currTaskType.length){
          var matchUser = aa.findObjectByKey(forceTypes.constraints, sad.physicist_id, 'user');
          if (matchUser.length){
            //fblog('matchUser');
            matchUser = matchUser[0];
            
            var checkbox = $(['#', formDef.id, ' [name=tasktype][value=', matchUser.tasktype ,']'].join(''));
            checkbox.attr('checked', true);
            //textbox.attr('value', aa.findObjectByKey(shared.tasktypes, matchUser.tasktype)[0].name);
            multiSelect.find('LABEL.hover INPUT:checkbox').attr('checked', true);

            multiSelect.multiSelectUpdateSelected(schedule.multiselect.options);
            multiSelect.find('LABEL').removeClass('checked').find('INPUT:checked').parent().addClass('checked');
          }
        }
      }
      
      multiSelect.multiSelectUpdateOptions(schedule.multiselect.options, allowedTypes);
      
    }
    
    
    // show/hide fields with displayCondition function defined
    for (i in formDef.fields){
      field = formDef.fields[i];
      displayElement = $(['#', formDef.prefix, field.name, (field.isClone ? field.unique : '')].join('')).parent();
      if (field.hasContainer){
        displayElement = displayElement.parent();
      }
      
      // if displayCondition defined, test it and display only if true
      if (field.displayCondition !== undefined){
        if (
          (
            typeof field.displayCondition == 'function'
            &&
            !field.displayCondition()
          )
          ||
          !field.displayCondition
        ){
          displayElement.addClass('hidden');
        }
        else {
          displayElement.removeClass('hidden');
        }
      }
      
      
      // if labelCondition defined, test it and apply appropriate label
      if (
        field.labelCondition
        &&
        field.labelCondition.test && typeof field.labelCondition.test == 'function'
      ){
        if (field.labelCondition.test()){
          // fblog(['field.labelCondition.test == true, new label = ', field.labelCondition.whenTrue].join(''));
          displayElement.find('label').text(field.labelCondition.whenTrue);
        }
        else {
          // fblog(['field.labelCondition.test == false, new label = ', field.labelCondition.whenFalse].join(''));
          displayElement.find('label').text(field.labelCondition.whenFalse);
        }
        
      }
    }
    
    
    if (!config.isHistory){
      // show facility details link if applicable
      if (aa.existsIn([0,99], sad.account_id)){
        // $('#linkDetails span').text('View personal time tracker'); //addClass('hidden');
        $('#linkDetailsFacility').addClass('hidden');
        $('#linkDetailsTime').removeClass('hidden');
      }
      else {
        // $('#linkDetails span').text('View facility details'); //removeClass('hidden');
        $('#linkDetailsTime').addClass('hidden');
        $('#linkDetailsFacility').removeClass('hidden');
      }
    }
    
    scheduleEditShowConditions(config);
  }

  
  
  
  function scheduleEditShowConditions(config){
    var sad, saa;

    if (config && config.isHistory){
      sad = clients.scheduleEdit.data;
      saa = clients.scheduleEdit.adjacentDates;
    }
    else {
      sad = schedule.activeEdit.data;
      saa = schedule.activeEdit.adjacentDates;
    }
    
    // show accrued status message if applicable to current task
    // if (
      // aa.existsIn(permissions, 'hr-admin')
    // ){
      scheduleEditShowAccruedStatus(config);    
    // }

    if (aa.existsIn([847,848,993], sad.location_id)){
      // if "non-working" day (sick, snow) adjacent to a holiday
      if (saa[1].holiday || saa[2].holiday){
        $('#taskEditMessage').append('<p><span class="highlightWarning bold">WARNING:</span> A non-working day cannot be used adjacent to a holiday.  You must use vacation time instead.</p>').show();
        return false;
      }
    }

    if (sad.location_id == 847 || sad.location_id == 848){
      // if 3 (or more) adjacent sick days
      if (
        (saa[0].sick && saa[1].sick)
        ||
        (saa[1].sick && saa[2].sick)
        ||
        (saa[2].sick && saa[3].sick)
      ){
        $('#taskEditMessage').append('<p><b>NOTE:</b> Three (3) or more adjacent sick days will require a doctor\'s note or other documentation for administrative approval.</p>').show();
      }
      
      // // if sick day adjacent to a holiday
      // else if (saa[1].holiday || saa[2].holiday){
        // $('#taskEditMessage').html('NOTE: Sick time cannot be used adjacent to a holiday.  You must use vacation time instead.').show();
        // return false;
      // }
    }
    else if (sad.location_id == 999){
      $('#taskEditMessage').append('<p><b>NOTE:</b> Please understand that bereavement time is only to be used when a death occurs in your immediate family (defined as your parent, spouse, child, step-child, brother, sister, grandchild, grandparent, or parent-in-law).  Please discuss with an administrator if you have any questions.</p>').show();
    }
    
    return true;
    
  }
  
  
  
  
  function scheduleOnTaskEditChange(config){
  
    // clear message area
    var formDef, activeEdit;
    
    if (config && config.isHistory){
      formDef = forms.clientScheduleEditForm;
      activeEdit = clients.scheduleEdit;
    }
    else {
      formDef = forms.taskEditForm;
      activeEdit = schedule.activeEdit;
    }

    //$(['#', formDef.prefixLong, 'Message'].join('')).hide().empty();
    
    //var task = shared.tasks.get(activeEdit._id).data;
    
    var
      i,
      checked, 
      checkedVal,
      currTaskType,
      temp = {billingmatrix: {}},
      dirty = false;
    

    
    // update billing info
    checked = $(['#', formDef.id, ' [name=billable_time]:checked'].join(''));

    if (!checked.length){
      $(['#', formDef.id, ' [name=billable_time][value=0]'].join('')).attr('checked', true);
      $(['#', formDef.prefix, 'billable_time'].join('')).next('.multiSelectOptions').multiSelectUpdateSelected(schedule.multiselect.options);
      
      scheduleOnTaskEditChange(config);
      return;
    }
      
    for (i = 0; i < checked.length; i++){
      checkedVal = Number($(checked[i]).val());
      if (
        checkedVal === 0
      ){
        temp.billingmatrix[1] = 2;
      }
      else if (checkedVal == 1){
        temp.billingmatrix[1] = 1;
      }
      else if (checkedVal == 2){
        temp.billingmatrix[2] = Number($(['#', formDef.prefix, 'billable_hours'].join('')).val());
      }
    }
    
    if (!temp.billingmatrix[1]){
      temp.billingmatrix[1] = 0;
    }
    if (!temp.billingmatrix[2]){
      temp.billingmatrix[2] = 0;
    }
    
    temp.billable_quantity = am.round((temp.billingmatrix[1] * 4) + temp.billingmatrix[2], 1);
    
    
    checked = $(['#', formDef.id, ' [name=is_billable]:checked'].join(''));
      
    if (!checked.length){
      $(['#', formDef.id, ' [name=is_billable][value=1]'].join('')).attr('checked', true);
      $(['#', formDef.prefix, 'is_billable'].join('')).next('.multiSelectOptions').multiSelectUpdateSelected(schedule.multiselect.options);

      scheduleOnTaskEditChange(config);
      return;
    }
      

    for (i = 0; i < checked.length; i++){
      checkedVal = Number($(checked[i]).val());
      if (checkedVal === 0){
        if (activeEdit.data.is_billable){
          activeEdit.data.is_billable = false;
          dirty = true;
        }
      }
      else {
        if (!activeEdit.data.is_billable){
          activeEdit.data.is_billable = true;
          dirty = true;
        }
      }
    }
    
    if (
      // !aa.objectCompare(temp.billingmatrix, schedule.activeEdit.data.billingmatrix)
      temp.billable_quantity != am.round(activeEdit.data.billable_quantity, 1)
    ){
      // schedule.activeEdit.data.billingmatrix = temp.billingmatrix;
      activeEdit.data.billable_quantity = temp.billable_quantity;
      dirty = true;
    
      //fblog('scheduleOnTaskEditChange(): dirty = true (billingmatrix doesn\'t match');
    }

    
    // show/hide billable hours input
    var checked = $(['#', formDef.id, ' [name=billable_time][value=2]:checked'].join(''));
    var billableHoursEl = $(['#', formDef.prefix, 'billable_hours'].join(''));
    if (checked.length){ //.parent('label').hasClass('checked')){
      billableHoursEl.parents('.item').removeClass('hidden').end();
      
      // only focus hours element if it's currently empty (most likely first time showing it)
      if (!billableHoursEl.val()){
        forceFocus(billableHoursEl);
      }
    }
    else {
      billableHoursEl.parents('.item').addClass('hidden');
    }


    // // if no recurrence is checked, default to "none"
    // var checked = $(['#taskEditForm [name=', formDef['prefix'], 'recurrence]:checked'].join(''));
    // if (!checked.length){
      // $(['#taskEditForm [name=', formDef.prefix,
        // 'recurrence', // this is the form control to query
        // '][value=1]'].join('')).attr('checked', true);
      // $(['#', formDef.prefix, 'recurrence'].join('')).next('.multiSelectOptions').multiSelectUpdateSelected(schedule.multiselect.options);

      // scheduleOnTaskEditChange();
      // return;
    // }
    
    // if (schedule.activeEdit.data.recurrence_id != Number($(checked[0]).val())){
      // schedule.activeEdit.data.recurrence_id = Number($(checked[0]).val());
      // dirty = true;
    // }
    
    // // show/hide recurrence notes input
    // var checked = $(['#taskEditForm [name=', formDef['prefix'], 'recurrence][value=6]:checked'].join(''));
    // if (checked.length){ //.parent('label').hasClass('checked')){
    // // if (activeEdit.data.billingmatrix.hasOwnProperty(2) && activeEdit.data.billingmatrix[2] != 0){
      // forceFocus($(['#', formDef.prefix, 'recurrenceNotes'].join(''), schedule.currentPageElement).parents('.item').removeClass('hidden').end());
    // }
    // else {
      // $(['#', formDef.prefix, 'recurrenceNotes'].join(''), schedule.currentPageElement).parents('.item').addClass('hidden');
    // }
    
    
    // update task type
    
    currTaskType = $(['#', formDef.id, ' [name=tasktype]:checked'].join(''));
      
    if (currTaskType.length){
      if (activeEdit.data.tasktype_id != Number($(currTaskType[0]).val())){
        activeEdit.data.tasktype_id = Number($(currTaskType[0]).val());
        //fblog('scheduleOnTaskEditChange(): dirty = true (tasktype add)');
        dirty = true;
      }
    }
    else if (activeEdit.data.tasktype_id){
      activeEdit.data.tasktype_id = null;
      //fblog('scheduleOnTaskEditChange(): dirty = true (tasktype remove)');
      dirty = true;
    }
    
    

    // // reflect changes to billable cost on form
    // $(['#', formDef.id, 'BillableCost'].join(''), schedule.currentPageElement).html(aa.objectLength(activeEdit.data.billingmatrix) ? ['$', billingGetCost(activeEdit.data).toFixed(2)].join('') : 'N/A');
    
    
    //fblog('"' + activeEdit.data.summary + '" != "' + $('#' + formDef.prefix + 'summary').val() + '"');
    // update data from text inputs
    
    var
      locationVal = $.trim($(['#', formDef.prefix, 'location_name'].join('')).val()),
      summaryVal = $.trim($(['#', formDef.prefix, 'summary'].join('')).val()),
      notesVal = $.trim($(['#', formDef.prefix, 'notes'].join('')).val());
    
    // if (activeEdit.data.location_name != locationVal){
    if (locationNameFull(activeEdit.data) != locationVal){
      //schedule.activeEdit.data.location_name = locationVal;
      //fblog('scheduleOnTaskEditChange(): dirty = true (summary changed)');
      dirty = true;
    }
    
    // if (activeEdit.data.summary != summaryVal){
    // ALWAYS PARSE SUMMARY FIELD
      activeEdit.data.summary = summaryVal;
      //fblog('scheduleOnTaskEditChange(): dirty = true (summary changed)');
      var device = summaryVal.match(/\d+/g);
      if (device && device.length){
        activeEdit.data.device_name = device = device[device.length - 1]; // get last number in summary
        var deviceObject = aa.findObjectByKey(shared.devices, device, 'unique_id');
        if (deviceObject.length){
          activeEdit.data.device_id = deviceObject[0].device_id;
        }
        else {
          delete activeEdit.data.device_id;
        }
      }
      else {
        delete activeEdit.data.device_id;
      }
      
      dirty = true;
    // }
    
    var summaryEls = $(['#', formDef.id, ' input[name=summary]'].join(''));
    var recurrenceMatchRE = new RegExp(/(annual)|(semi[- ]?annual)|(quarterly)|(monthly)/i);
    
    summaryEls.each(function(){
      var summaryEl = $(this);
      var summaryVal;
      var uploadButton = summaryEl.parent().next().next().children('div.button');
      var recurrenceEl = summaryEl.parent().next().children('input.multiSelect');
      var recurrenceConfig = aa.findObjectByKey(formDef.dropdowns, recurrenceEl.attr('id'), 'id', null, true);
      var recurrenceMatch;
      
      summaryVal = $.trim(summaryEl.val());
      //summaryEl.val(summaryVal);
      
      if (uploadButton.length && recurrenceEl.length){
        if (summaryVal){
          if (recurrenceConfig){
            recurrenceEl.removeClass('disabled');
            recurrenceConfig.config.readOnly = false;
          }
          uploadButton.data('upload').enable();
          uploadButton.next('div.uploadStatus').removeClass('disabled');
          
          var taskItem = aa.findObjectByKey(activeEdit.data.taskItems, summaryEl.attr('unique'), 'unique');
          if (taskItem.length){
            taskItem = taskItem[0];
            if (taskItem.isNew){
              // update recurrence based on context
              recurrenceMatch = recurrenceMatchRE.exec(summaryVal);
              if (recurrenceMatch){
                if (recurrenceMatch[1]){
                  recurrenceMatch = 2;
                }
                else if (recurrenceMatch[2]){
                  recurrenceMatch = 3;
                }
                else if (recurrenceMatch [3]){
                  recurrenceMatch = 4;
                }
                else if (recurrenceMatch [4]){
                  recurrenceMatch = 5;
                }
                else {
                  delete recurrenceMatch;
                }
              }
              
              if (recurrenceMatch){
                delete taskItem.isNew;
                var select = summaryEl.parent().next().children('.multiSelectOptions');
                $('input[type=checkbox]', select).attr('checked', false).filter(['[value=', recurrenceMatch, ']'].join('')).attr('checked', true);
                select.multiSelectUpdateSelected(schedule.multiselect.options);
                
                taskItem.recurrence = recurrenceMatch;
              }
            }
            
            taskItem.summary = summaryVal;
          }

        }
        else {
          //fbdir(uploadButton.data('upload'));
          if (recurrenceConfig){
            recurrenceEl.addClass('disabled');
            recurrenceConfig.config.readOnly = true;
          }
          uploadButton.data('upload').disable();
          uploadButton.next('div.uploadStatus').addClass('disabled');
        }
      }
    });
    
    // fblog(activeEdit.data.notes);
    // fblog(notesVal);
    if (activeEdit.data.notes != notesVal){
      //fblog('"' + activeEdit.data.notes + '" != "' + $('#' + formDef.prefix + 'notes').val() + '"');
      activeEdit.data.notes = notesVal;
      //fblog('scheduleOnTaskEditChange(): dirty = true (notes changed)');
      dirty = true;
    }
    


    
    // update dirty disposition
    // ALWAYS SET DIRTY in case script bugs prevent change detection
    // if (!(config && config.onLoad) && dirty){
      activeEdit.dirty = true;
      activeEdit.audit_user = user.id;
      activeEdit.audit_user_name = user.name;
    // }



    
    if (activeEdit.dirty){
      fblog('scheduleOnTaskEditChange(): dirty');
    }
    else {
      fblog('scheduleOnTaskEditChange(): not dirty');
    }
    //fblog ('scheduleOnTaskEditChange ' + e.parents('form').attr('id') + ', dirty = ' + activeEdit.dirty);
    
    
    return true;
  }
  
  


  

  function billingGetRate(period, task, billingMetric, account){
    // calculate cost per task
    
    var
      rate = 0,
      smar,
      smard,
      a = task.account_id,
      l = task.location_id,
      m = billingMetric,
      t = task.tasktype_id;
    
    // get doublebilled rate if there are any defined for this account
    if (account.rates_doublebilled && account.rates_doublebilled.length){
    
      // if this task is [potentially] double billed
      if (aa.existsIn(period.double_billed.list, task.id)){
        smard = account.rates_doublebilled;
        rate = billingGetRateLookup(smard, l, t, m, task);
        
        // if a valid doublebilled rate was found
        if (rate){
        
          // make sure flag array exists for this location, tasktype, and date
          if (!account.rates_doublebilled_allow){
            account.rates_doublebilled_allow = [];
          }
          if (!account.rates_doublebilled_allow[l]){
            account.rates_doublebilled_allow[l] = [];
          }
          if (!account.rates_doublebilled_allow[l][t]){
            account.rates_doublebilled_allow[l][t] = [];
          }
          
          // task.doublebilled = true;
          
          // set a flag to use this rate for every other matching task EXCEPT this one
          if (!account.rates_doublebilled_allow[l][t][task.schedule_start_date]){
            account.rates_doublebilled_allow[l][t][task.schedule_start_date] = task.id;
            // if (rate){
              rate = 0;
            // }
          }
          else if (account.rates_doublebilled_allow[l][t][task.schedule_start_date] !== true){
            var oldTask = period.tasks.get({value: account.rates_doublebilled_allow[l][t][task.schedule_start_date], key: 'id'});
            if (oldTask){
              oldTask.data.doublebilled = true;
            }
            account.rates_doublebilled_allow[l][t][task.schedule_start_date] = true;
            task.doublebilled = true;
          }
          // else if (rate){
          else {
            task.rate_doublebilled = true;
          }
        }
      }
    }
    
    // otherwise get normal rate
    if (!rate){
      if (!account || !account.rates){
        smar = period.accountsById[0].rates;
      }
      else {
        smar = account.rates;
      }
      
      rate = billingGetRateLookup(smar, l, t, m, task);
    }
    
    return rate;

  }
  
  
  function billingGetRateLookup(smar, l, t, m, task){
    var rate = 0;
    var thismar;
    
    if ( // defined location, tasktype, and metric
      smar[l]
      &&
      smar[l][t]
      &&
      smar[l][t][m]
    ) {
      // rate = smar[l][t][m].rate;
      thismar = smar[l][t][m];
    }
       
    else if ( // defined location, default tasktype, defined metric
      smar[l]
      &&
      smar[l][0]
      &&
      smar[l][0][m]
    ) {
      // rate = smar[l][0][m].rate;
      thismar = smar[l][0][m];
    }
      
    else if ( // default location, defined tasktype and metric
      smar[0]
      &&
      smar[0][t]
      &&
      smar[0][t][m]
    ) {
      // rate = smar[0][t][m].rate;
      thismar = smar[0][t][m];
    }
      
    else if ( // default location and tasktype, defined metric
      smar[0]
      &&
      smar[0][0]
      &&
      smar[0][0][m]
    ) {
      // rate = smar[0][0][m].rate;
      thismar = smar[0][0][m];
    }
    
    else {
      return 0;
    }
                      
    rate = thismar.rate;
    if (!thismar.tasks){
      thismar.tasks = [];
    }
    aa.pushUnique(thismar.tasks, task.id);
    
    return rate;
  }

  
  // return cost for passed task
  function billingGetCost(period, task, account){
    var
      fullday = 0,
      halfday = 0,
      hourly = 0,
      unit = 0,
      thisCost = 0,
      adjustedQuantity;
      
    // if some billable time is specified as half-days
    if (aa.keyExists(task.billingmatrix, 1) && task.billingmatrix[1]){
      // if default metric for this account is half day
      if (task.billingmetric == 1){
        halfday += billingGetRate(period, task, 1, account) * Number(task.billingmatrix[1]);
      }
      // default metric for this account is hourly
      else {
        hourly += billingGetRate(period, task, 2, account) * (Number(task.billingmatrix[1]) * 4);
      }
    }

    // if some billable time is specified as hourly
    if (aa.keyExists(task.billingmatrix, 2) && task.billingmatrix[2]){
      adjustedQuantity = Number(task.billingmatrix[2]) + Number(task.billable_quantity_adjustment);
      // default metric is half day, so calculate 4 hour blocks (half days) at half day rate plus remainder at hourly rate
      if (
        (
          task.billingmetric_override == 1
          ||
          (!task.billingmetric_override && task.location_billingmetric == 1)
          ||
          (!task.billingmetric_override && !task.location_billingmetric && task.billingmetric == 1)
        )
        &&
        task.tasktype_id != 10 // don't change to half days if this is a shielding
      ){
        fullday += billingGetRate(period, task, 4, account) * (Math.floor(adjustedQuantity / 8));
        
        if (!fullday){
          halfday += billingGetRate(period, task, 1, account) * (Math.floor(adjustedQuantity / 4));
        }
        else {
          halfday += billingGetRate(period, task, 1, account) * (Math.floor((adjustedQuantity % 8) / 4));
        }
        
        if (adjustedQuantity % 4){
          hourly += billingGetRate(period, task, 2, account) * (adjustedQuantity % 4);
        }
      }
      // default metric is hourly
      else {
        hourly += billingGetRate(period, task, 2, account) * adjustedQuantity;
      }
    }
    
    if (aa.keyExists(task.billingmatrix, 3) && task.billingmatrix[3]){
      unit += billingGetRate(period, task, 3, account) * Number(task.billingmatrix[3]);
    }
    
    
    return fullday + halfday + hourly + unit;
  }


  
  
  
  
  function billingSelectedStatusUpdate(config) {
    config = config || {};
    
    var
      // i, j,
      // spl = shared.periods.length,
      // spis, spisl, spim;
      target = config.target || 'billingTableSelectedStatus',
      disable = config.disable || 'btc_button_billingTable_billingViewSelected',
      singular = config.singular || 'invoice',
      plural = config.plural,
      numItems = 0,
      numItemsStr,
      itemString;
      
    // for (i = 0; i < shared.periods.length; i++){
      // spis = shared.periods[i].selected;
      // spim = shared.periods[i].accountsById;
      
      // if (!spis){
        // spis = [];
      // }
      
      // spisl = spis.length;
      // // fblog('invoices = ' + spis);
      // for (j = 0; j < spisl; j++){
        // if (!spim[spis[j]].invoiceNumbers){
          // fblog('no invoice numbers: ' + spis[j]);
        // }
        // else {
          // numItems += spim[spis[j]].invoiceNumbers.length;
        // }
      // }
      
      // //numItems += shared.periods[i].selected.length;
    // }
    
    // numItems = billing.selected.length;
    numItems = aa.findObjectByKey(billing.invoices, true, 'selected').length;
      
    if (numItems){
      numItemsStr = ['<b>', numItems, '</b>'].join('');
      if (numItems == 1){
        itemString = singular;
      }
    }
    else {
      numItemsStr = 'No';
    }
    
    if (!itemString){
      if (plural){
        itemString = plural;
      }
      else {
        itemString = [singular, 's'].join('');
      }
    }
    
    $(['#', target].join('')).html([numItemsStr, ' ', itemString, ' selected'].join(''));
    
    if (disable){
      if (numItems){
        //fblog('selected > 0, removing disabled class');
        $(['#', disable].join('')).removeClass('buttonDisabled');
      }
      else {
        //fblog('selected == 0, adding disabled class');
        $(['#', disable].join('')).addClass('buttonDisabled');
      }
    }
  }
  
  
  
  function forceFocus(target){
    if (target && target.length){
      var element = target;
      
      // element passed as a string, so get matching jquery object
      if (typeof target == 'string'){
        element = $(['#', target].join(''));
      }

      // passed element is not a jquery object,
      // so assume it's a formDef
      else if (!target.jquery){
        element = $(['#', target.prefix, target.focus].join(''));
      }
      
      if (element.length){
      
        shared.inputFocused = element.get(0).id;
        
        // delay focus trigger slightly to work correctly in IE 8
        setTimeout(function(){
          element.trigger('focus').addClass('focused');
        }, 50);
        
        if (element.hasClass('help')){
          // $('div[helpFor=' + element.attr('id') + ']').slideDown(100);
          $(['div[helpFor=', element.attr('id'), ']'].join(''), element.parent()).addClass('calloutFocused');
        }
      }
    }
  }
  

  
  
  
  
  
  
  function autocompleteLoading(input, isLoading){
    //fblog('acLoading ' + isLoading);
    if (isLoading){
      input.addClass('iconrightgap icon-loading');
    }
    else {
      input.removeClass('icon-loading');
    }
    
    return true;
    
    // var dda = input.siblings('div.ac_loading');
    
    // if (isLoading){ 
      // var inputPos = input.position();
      // var inputWidth = input.width();
      // //fblog(inputPos.left + ' + ' + inputWidth);
      // dda
        // .html('Searching...')
        // .removeClass('hidden')
        // .css({
          // 'top': inputPos.top,
          // 'left': inputPos.left + inputWidth - dda.width() //inputWidth
        // }); //.removeClass('hidden');
    // }
    // else {
      // dda.addClass('hidden');
    // }
  }
  
  
  // custom search term highlighting
  function autocompleteHighlight(value, term){
    //var terms = term.split(" ");
    
    // for (var i = 0; i < terms.length; i++){
      // value = value.replace(new RegExp('(' + terms[i] + ')', 'gi'), '<strong>$1</strong>');
    // }
    // return value;
    
    //return formatString(value, terms, '<strong>', '</strong>');
    return value;
  }

  // format single result item for display
  function autocompleteFormatItem(data, i, max, value, query) {
    //fblog('query = ' + query);
    //fblog(data);
    var stringArray = [];
    var terms = query.split(' ');
    
    value = data.location_name; //data.default_locationname || data.value;

    var searchIgnoredString = clients.searchIgnored.join('');
    stringArray.push('<p class="main">', formatString(value, terms, '<span class="ac_resultsMatch">', '</span>', searchIgnoredString));
    
    if (data.unique_identifier && data.unique_identifier != 'null'){
      stringArray.push(' (', formatString(data.unique_identifier, terms, '<span class="ac_resultsMatch">', '</span>', searchIgnoredString), ')');
    }
    
    if (data.city && data.state){
      stringArray.push(' <span class="details nowrap">', formatString(data.city, terms, '<span class="ac_resultsMatch">', '</span>', searchIgnoredString), ', ', formatString(data.state, terms, '<span class="ac_resultsMatch">', '</span>', searchIgnoredString), '</span>');
    }

    if (aa.existsIn(permissions, 'accounting') && data.account_id){
      stringArray.push(' <span class="highlight">', formatString(String(data.account_id), terms, '<span class="ac_resultsMatch">', '</span>', searchIgnoredString), '</span>');
    }

    stringArray.push('</p>');
    
    if (data.aliases && data.aliases != 'null'){
      stringArray.push('<p class="detailsHanging"><span class="small">AKA: </span>', formatString(data.aliases, terms, '<span class="ac_resultsMatch">', '</span>', searchIgnoredString), '</p>');
    }
    
    if (data.contacts && data.contacts != 'null'){
      stringArray.push('<p class="detailsHanging"><span class="small">CONTACTS: </span>', formatString(data.contacts, terms, '<span class="ac_resultsMatch">', '</span>', searchIgnoredString), '</p>');
    }
    
    if (Number(data.billing_only) == 1){
      stringArray.push('<p class="detailsHanging"><span class="smallCaps highlightWarning">Billing / management address - no service performed at this location</span></p>');
    }
    
    if (Number(data.cod) == 1){
      stringArray.push('<p class="detailsHanging"><span class="smallCaps highlightWarning">COD - you MUST coordinate payment with BioMed office staff BEFORE scheduling service</span></p>');
    }
    
    if (data.effective_end < dateTimeString){
      stringArray.push('<p class="detailsHanging"><span class="smallCaps highlightWarning">Account no longer active - service ended <b>', dpExactDate(data.effective_end).toString('MMMM d, yyyy'), '</b></span></p>');
    }
    else if (data.location_effective_end < dateTimeString){
      stringArray.push('<p class="detailsHanging"><span class="smallCaps highlightWarning">Facility no longer active - service ended <b>', dpExactDate(data.location_effective_end).toString('MMMM d, yyyy'), '</b></span></p>');
    }
    
    //return [formatString(value, query.split(" "), '<strong>', '</strong>'), aliasString, as.echoIf(data.contact_name, ' <div class="details">', '</div>'), as.echoIf([as.echoIf(data.city), as.echoIf(data.state, ', ')].join(''), ' <div class="details">', '</div>')].join('');
    return stringArray.join('');
  }

  // determine which parts of this item's data to attempt to match against
  // creates a space delimited string made up of item elements  
  function autocompleteFormatMatch(row, i, max) {
    var matchArray = [];
    var match;

    // don't use this location if (this is the schedule AND it's a billing location)
    if (shared.activeInterface == 'schedule' && Number(row.billing_only) == 1){
      return '';
    }
    
    matchArray.push(row.value);
    
    if (row.unique_identifier && row.unique_identifier != 'null'){
      matchArray.push(' ', row.unique_identifier);
    }
    //matchArray.push('<div class="hidden">');
    if (row.aliases && row.aliases != 'null'){
      matchArray.push(' ', row.aliases);
    }
    if (row.contacts && row.contacts != 'null'){
      matchArray.push(' ', row.contacts);
    }
    matchArray.push(' ', row.city);

    if (aa.existsIn(permissions, 'accounting')){
      matchArray.push(' ', row.account_id);
    }

    match = matchArray.join('');
    return match;
  }
  
  
  // TODO change this
  function autocompleteFormatResult(row) {
    var resultArray = [row.value];
    
    // only include unique identifier if this is the schedule
    // prevents unique id from being saved as part of the company name for submitted forms
    if (shared.activeInterface == 'schedule'){
      if (row.unique_identifier) {
        resultArray.push(' (', row.unique_identifier, ')');
      }
      else if (row.num_locations > 1 && row.city){
        resultArray.push(' (', row.city, ')');
      }
    }
    
    return resultArray.join('');
  }
  
  function autocompleteResult(event, data, formatted){
    var subject = $(event.target).attr('lookup');
    var element = $(['#', event.target.id].join(''));
    var lookup = element.attr('lookup');
    var formObj = element.parents('form');
    var formDef = forms[formObj.attr('id')];
    
    var activeEdit = (formDef.id == 'clientScheduleEditForm' ? clients.scheduleEdit : schedule.activeEdit);
    var isHistory = (formDef.id == 'clientScheduleEditForm');
    
    var addedInjectTargetElement = $(['#', formDef.addedInjectTarget].join(''));

    // put lookupVal in hidden field attached to this lookup
    //$(['#', event.target.id, '_lookupVal'].join('')).val(data[formDef.fields[event.target.id.replace(formDef.prefix, '')].lookupVal]);
    $(['#', event.target.id, '_lookupVal'].join('')).val(data[aa.findObjectByKey(formDef.fields, event.target.id.replace(formDef.prefix, ''), 'name')[0].lookupVal]);                  
    formValidateItem(element);


    
    switch (subject){
    
    
    
      case 'taskEditFacility':
        // fblog('|' + data.effective_end + '|');
        // fblog('|' + activeEdit.data.schedule_start_date + '|');
        if (data.effective_end <= activeEdit.data.schedule_start_date){
          fblog('inactive account selected');
          $('#taskEditMessage').append(['<p><span class="highlightWarning bold">WARNING:</span> <b>', data.location_name, '</b> is not an active client as of this date - service discontinued <b>', dpExactDate(data.effective_end).toString('MMMM d, yyyy'), '</b>.</p>'].join('')).show();
          element.val('');
        }
        else if (data.location_effective_end <= activeEdit.data.schedule_start_date){
          fblog('inactive facility selected');
          $('#taskEditMessage').append(['<p><span class="highlightWarning bold">WARNING:</span> <b>', data.location_name, (data.num_locations > 1 ? [' (', (data.unique_identifier ? data.unique_identifier : data.city), ')'].join('') : ''), '</b> is not an active facility as of this date - service discontinued <b>', dpExactDate(data.location_effective_end).toString('MMMM d, yyyy'), '</b>.</p>'].join('')).show();
          element.val('');
        }
        else if (activeEdit.data.location_id != data.location_id){
          //fblog('activeEdit.location != data.location (' + activeEdit.data.account_id + ' ?= ' + data.account_id + ')');
          activeEdit.data.location_id = data.location_id;
          activeEdit.data.location_name = data.location_name;
          activeEdit.data.location_name_short = data.location_name_short || data.location_name;
          activeEdit.data.account_id = data.account_id;
          //activeEdit.data = aa.objectClone(data);
          activeEdit.data.num_locations = data.num_locations;
          activeEdit.data.city = data.city;
          activeEdit.data.state = data.state;
          activeEdit.data.unique_identifier = data.unique_identifier;
          activeEdit.dirty = true;
          
          element.removeClass('icon-loading').addClass('iconrightgap icon-ok');
    
          scheduleOnTaskEditChange({isHistory: isHistory});
          scheduleOnTaskEditLocationChange({isHistory: isHistory, el: element});
        }
        break;
        
    
    
      case 'company':
        //formValidateItem(element);

        // grey out lookupFill fields while ajaxing
        $(["input[lookupFill=", lookup, "]"].join(''), formObj).fadeTo("fast", 0.33).val('Loading...');
        $.post([shared.urlRoot, '/publicSSGet/details'].join(''),
          {
            subject: lookup,
            value: data.location_id
          },
          function(data){
            data = eval(['(', data, ')'].join(''));
            shared.lastJSON.generic = data;
            var formObj = element.parents('form');
            for (var field in data.result){
              //fblog('#' + forms[formDef.attr('id')].prefix + field + ' = ' + data[field]);
              formValidateItem($(['#', forms[formObj.attr('id')].prefix, field].join('')).val(data.result[field] || ''));  // need to access calling form field to determine which sibling fields to update
              // flag this field as having been blurred at least once
              aa.pushUnique(forms[formObj.attr('id')].blurred, [forms[formObj.attr('id')].prefix, field].join(''));
            }
            $(["input[lookupFill=", lookup, "]"].join(''), formObj).fadeTo("fast", 1);
          }
        );
        break;
        
        
        
        
      case 'client':
      
        if (event.pageLoad){
          element.val(event.searchText);
          autocompleteResultClient(lookup, formObj, data.location_id, addedInjectTargetElement);
        }
        
        else {
          var url = [
            shared.urlRoot,
            '/clients/',
            data.location_id,
            ($.trim(element.val()) ? ['/', as.urlEncode($.trim(element.val()))].join('') : '')
          ].join('');
            
          document.location.href = url;
        }
      
        break;
        
    }
  }  
  
  
  
  
  
  
  
  function autocompleteResultClient(lookup, formObj, location_id, injectTargetElement){
    injectTargetElement.removeClass('hidden noPaddingBottom').html('<table><tr><td><p style="margin-top: 1.25em"><img src="/common/assets/images/large-loading.gif" align="absmiddle"/>&nbsp;<span class="loadingText">Loading facility information...</span></p></td></tr></table>');

    setTimeout(function(){
      $(window).scrollTo($('#cm_client'), 250, {offset: {top: 0}});
    }, 500);
        
    if (aa.existsIn(permissions, 'accounting')){
      $('#jq-formbilling-inject').remove();
    }
    
    // grey out lookupFill fields while ajaxing
    if (formObj){
      $(["input[lookupFill=", lookup, "]"].join(''), formObj).fadeTo("fast", 0.33).val('Loading facility information...').parent('.item').removeClass('hidden');
    }
    
    clients.active = {
      id: location_id
    };
    
    var client = aa.findObjectByKey(shared.clients, location_id, 'location_id');
    if (client.length){
      clients.active = client[0];
    }
    else {
      fblog('autocompleteResultClient(): location_id not found in shared.clients');
    }
    
    // ajax lookup for client details
    $.ajax({
      url: [shared.urlRoot, '/ajaxSSGet/details'].join(''),
      data: {
        subject: lookup,
        //structure: true, // only needed for dynamic form generation
        value: location_id
      },
      success: function(data){
        statusBannerHide();
        
        var i;
      
        // convert JSON to object
        var result = shared.lastJSON.generic = eval(["(", data, ")"].join(''));
        
        if (result.success === false){
          fblog(result.message);
          if (result.auth === false){
            window.location.reload(); //href = "/login/clients";
          }
          return false;
        }
        
        clients.active = result.data;
        
        // $(window).scrollTo(injectTargetElement, 250, {offset: {top: -100}});
        
        if (formObj && formObj.length){
          for (var field in result['data']){
            // only validate
            if (aa.keyExists(forms[formObj.attr('id')].fields, field)){
              // insert retrieved data and validate
              formValidateItem($(['#', forms[formObj.attr('id')].prefix, field].join('')).val(result['data'][field]));  // need to access calling form field to determine which sibling fields to update
              
              // flag this field as having been blurred at least once
              aa.pushUnique(forms[formObj.attr('id')].blurred, [forms[formObj.attr('id')].prefix, field].join(''));
            }
          }
          $(["input[lookupFill=", lookup, "]"].join(''), formObj).fadeTo("fast", 1);
        }
        
        
        //var formId = element.parents('form').attr('id');
        //var insertTargetId = element.parents('form').attr('id');
        // var contentArray = ['<div id="clientDetailsLeft" class="item width47"><table class="formTable">'];
        var contentArray = ['<table id="clientDetailsContainer" border="0" cellspacing="0" cellpadding="0" style="width: 100%"><tr><td style="width: 50%; padding-top: .5em"><table class="formTable">'];
        
        var fields = clients.fields = [
          {fieldName: 'locationnames', friendlyName: '<span style="line-height: 2.25em">Facility</span>'},
          {fieldName: 'physicists', friendlyName: 'Physicists'}
        ];
        
        
        if (result.data.id != 840){
          fields.push(
            {fieldName: 'contacts', friendlyName: 'Contacts'},
            {fieldName: 'phone', friendlyName: 'Phone'},
            // {fieldName: 'phone2', friendlyName: 'Phone (other)'},
            {fieldName: 'fax', friendlyName: 'Fax'},
            // {fieldName: 'fax2', friendlyName: 'Fax (other)'},
            {fieldName: 'email', friendlyName: 'Email'},
            {fieldName: 'website', friendlyName: 'Website'},
            {fieldName: 'address', friendlyName: 'Address'}
          );
        }
        
        
        // get default location name and aliases
        var item = result.data['locationnames'];
        var itemIndex;
        var itemNames = [];
        
        if (
          result.data['default_locationname_id']
          &&
          result.data['default_locationname_id'] !== 0
          &&
          result.data['default_locationname_id'] !== '0'
        ){
          itemIndex = aa.indexOfObjectByKey(item, result.data['default_locationname_id']);
          itemIndex = itemIndex[0];
        }
        else {
          itemIndex = 0;
        }
        
        var facilityNameDefault = item[itemIndex].name;
        for (i = 0; i < item.length; i++){
          if (i != itemIndex){
            itemNames.push(item[i].name);
          }
        }
        
        document.title = [
          'Client: ',
          facilityNameDefault, 
          (result.data['unique_identifier'] ? [' (', result.data['unique_identifier'], ')'].join('') : ''),
          ' - Bio-Med Associates'
        ].join('');
        
        var facilityNameAliases = itemNames.join(', ');
        
        var fieldName;
        //var item;
        var subItems;
        var itemString;
        var itemStringArray;
        var facilityNameArray = [
        
          // '<div style="position: absolute; top: 1em; right: 1em; text-align: right">',
          // '<span class="light smallCaps">Last Visit:</span> ',
          // (
            // result.data.most_recent_date ?
              // [
                // dateFriendlyString(dpExactDateTime(result.data.most_recent_date)),
                // '<span class="light smallCaps"> by </span>',
                // result.data.most_recent_physicist
              // ].join('')
              // :
              // '<span class="light">Unknown</span>'
          // ),
          // '<br /><span class="light smallCaps">Last Update:</span> ',
          // (
            // result.data.audit_touched != '2008-07-01 00:00:00'
            // ?
              // [
                // dateFriendlyString(dpExactDateTime(result.data.audit_touched)),
                // '<span class="light smallCaps"> by </span>',
                // result.data.audit_user_name
              // ].join('')
              // :
              // '<span class="light">Unknown</span>'
          // ),
          // '</div>',
          
          '<div class="item width95 clientFacilityName" style="margin-top: .5em"><table class="formTable">'
        ];
        
        var tableBreak;
        var forceNone;
        var editable;
        var content;
        var hiddenLink;
        var mapQuery = {
          string: '',
          structure: {}
        };
        

        fieldLoop:
        for (var fieldNum = 0; fieldNum < fields.length; fieldNum++){
          // if (
            // !result.structure[field].hidden ||
            // aa.existsIn(permissions, result.structure[field].hidden)
          // ){
            
          fieldName = fields[fieldNum].fieldName;
          item = result.data[fieldName]; //.slice();
          subItems = [];
          itemString = '';
          itemStringArray = [];
          //facilityNameArray = [];
          forceNone = false;
          tableBreak = false;
          editable = false;
          content = {};
          hiddenLink = false;
          
          

          
          
            
          switch (fieldName){
          
            case "locationnames":
              facilityNameArray.push(
                '<tr><td colSpan="2" class="fieldValue">',
                '<p style="font-size: 1.88em; margin-top: .5em;"><span class="clientFacilityNamePrepend" style="display: none; color: #ccc; font-size: .67em; line-height: 1em;">CLIENT DETAILS: <br /></span><b>', facilityNameDefault, '</b></p>'
              );
              
              if (result.data['unique_identifier']){
                facilityNameArray.push('<p style="font-size: 1.88em; color: #bbb; margin-top: -.25em">', result.data['unique_identifier'], '</p>');
              }
              
              if (itemNames.length){
                // itemStringArray.push('</b><br><span class="smallCaps">a.k.a:</span> ', itemNames.join(', '), '<b>');
                facilityNameArray.push('<p style="color: #aaa; margin-top: 0"><span class="smallCaps">aka:</span> ', facilityNameAliases, '</span></p>');
              }
              
              if (Number(result.data['billing_only']) == 1){
                facilityNameArray.push('<p style="margin-top: 0"><span class="smallCaps highlightWarning">Billing / management address - no service performed at this location</span></p>');
              }
              
              if (Number(result.data['cod']) == 1){
                facilityNameArray.push('<p style="margin-top: 0"><span class="smallCaps highlightWarning">COD - you MUST coordinate payment with BioMed office staff BEFORE scheduling service</span></p>');
              }
              
              if (result.data['effective_end']){
                facilityNameArray.push('<p style="margin-top: 0"><span class="smallCaps highlightWarning">Account no longer active - service ended <b>', dpExactDate(result.data['effective_end']).toString('MMMM d, yyyy'), '</b></span></p>');
              }
              else if (result.data['location_effective_end']){
                facilityNameArray.push('<p style="margin-top: 0"><span class="smallCaps highlightWarning">Facility no longer active - service ended <b>', dpExactDate(result.data['location_effective_end']).toString('MMMM d, yyyy'), '</b></span></p>');
              }
              
              tableBreak = true;
              
              facilityNameArray.push(
                '</td></tr>'
              );
              
              continue fieldLoop;

            
            case "physicists":
              for (i = 0; i < item.length; i++){
                if (aa.keyExists(shared.physicistsById, item[i].id)){
                  subItems.push(['<nobr>', shared.physicistsById[item[i].id].name, ' <span class="smallCaps lighter">', item[i].frequency, '</span></nobr>'].join(''));
                }
              }
            
              subItems = subItems.join(', ');
              
              if (subItems){
                subItems = ['<span style="color: #5AAD52">', subItems, '</span>'].join('');
                itemStringArray.push('<div class="valueContent">', subItems, '</div>');
              }
              
              itemStringArray = itemStringArray.join('');
              break;
              
          
            case "contacts":
              content = contentBuildContacts(result.data);
              itemStringArray = content.string;
              if (content.hidden){
                hiddenLink = true;
              }
              tableBreak = true;
              break;


            case "address":
              //item = item[0];
              
              if (item['city']){
              
                itemStringArray.push('<div class="valueContent editable" editType="address" field="address">');
              
                itemStringArray.push(facilityNameDefault, '<br>');
                
                itemStringArray.push(as.echoIf(item['address1'], '', '<br>'),
                  as.echoIf(item['address2'], '', '<br>'),
                  as.echoIf(item['address3'], '', '<br>'), 
                  item['city'], ', ', item['state'], as.echoIf(item['zip'], ' &nbsp;'));
                  
                itemStringArray.push('</div>');
                

                mapQuery.structure = {
                  name: facilityNameDefault,
                  address1: item['address1'],
                  address2: item['address2'],
                  address3: item['address3'],
                  citystatezip: [
                    as.echoIf(item['city'], '', ', '),
                    as.echoIf(item['state'], '', ''),
                    as.echoIf(item['zip'], ' ', '', '', 0, 6) // only use first 5 digits of zip code; Google Maps doesn't understand ZIP+4
                  ].join(''),
                  city: item['city'],
                  state: item['state'],
                  zip: item['zip']
                };
                
                mapQuery.string = [
                  facilityNameDefault,
                  item['address1'] && item['address1'].indexOf('PO Box') == -1 ? [item['address1'], ', '].join('') : '',
                  item['address2'] && item['address2'].indexOf('PO Box') == -1 ? [item['address2'], ', '].join('') : '',
                  item['address3'] && item['address3'].indexOf('PO Box') == -1 ? [item['address3'], ', '].join('') : '',
                  as.echoIf(item['city'], '', ', '),
                  as.echoIf(item['state'], '', ''),
                  as.echoIf(item['zip'], ' ', '', '', 0, 6) // only use first 5 digits of zip code; Google Maps doesn't understand ZIP+4
                ].join('');
                
                // itemStringArray.push([
                  // '</b></p><div class="iconLink icon-view iconleftgapless" style="zoom: 1"><a href="http://maps.google.com/?q=',
                  // as.urlEncode(mapQuery.string),
                  // '" target="_blank" class="smallCaps">View Map</a></div>'
                // ].join(''));
                
              }
              
              itemStringArray = itemStringArray.join('');
              editable = true;
              break;


            default:
              if (result.data[fieldName]){
                editable = true;
                
                itemStringArray.push('<div class="valueContent editable" editType="', fieldName, '" field="', fieldName, '">');
                if (aa.existsIn(['phone','phone2','phone_home','phone_cell','pager','fax','fax2'], fieldName)){
                  itemStringArray.push(formatPhone(result.data[fieldName]));
                }
                else if (fieldName == 'email'){
                  itemStringArray.push('<a href="mailto:', result.data[fieldName], '">', result.data[fieldName], '<a/>');
                }
                else if (fieldName == 'website'){
                  result.data[fieldName] = result.data[fieldName].replace(/(http:\/\/)/i, '');
                  itemStringArray.push('<a href="http://', result.data[fieldName], '" target="_blank">', result.data[fieldName], '<a/>');
                }
                else {
                  //fblog('test (' + field + '): |' + result.data[field] + '|');
                  itemStringArray.push(result.data[fieldName]);
                }
                itemStringArray.push('</div>');
              }
              
              if (result.data[fieldName] !== undefined){
                editable = true;
              }
              
              itemStringArray = itemStringArray.join('');
          }
          
          //$('#' + formId + ' > :last-child').after('<div class="item staticLabel"><p>' + result.structure[field].friendlyName + ':</p></div><div class="item staticContent"><p><b>' + itemString + '</b></p></div><div class="clearfix"></div>');
          if (!$.trim(itemStringArray)){
            itemStringArray = ['<div class="valueContent' , (editable ? ' editable' : ''), '" editType="', fieldName, '" field="', fieldName, '"><span class="highlightLight', (editable ? ' iconright icon-edit dontprint">edit' : '">none'), '</span></div>'].join('');
          }
            
          contentArray.push('<tr valign="top"><td class="fieldName"><p><span class="smallCaps" style="line-height: 1.5em">', fields[fieldNum].friendlyName, ':</span></p>');
          
          if (fieldName == 'contacts'){
            contentArray.push('<div class="smallCaps" style="line-height: 1.25em; padding-right: 2em"><a id="', fieldName, 'LinkHidden" class="dontprint linkHidden', (hiddenLink ? '' : ' hidden'), '" field="', fieldName, '">Show<br>deleted</a></div>');
          }
          
          contentArray.push('</td><td class="fieldValue" id="', fieldName, 'ValueContainer">', itemStringArray, '</td></tr>');
          
          if (tableBreak){
            // contentArray.push('</table></div><div id="clientDetailsRight" class="item width47"><table class="formTable">');
            contentArray.push('</table></td><td style="width: 50%; padding-top: .5em; padding-right: .5em"><table class="formTable">');
          }
          
        }
        
        facilityNameArray.push('</table></div><div class="clear" />');
        
        // contentArray.push('</table></div><div class="clear" />');
        contentArray.push('</table></td></tr></table><div class="clear" />');






        
        // insert account ID and location ID
        if (aa.existsIn(permissions, 'accounting')){
          contentArray.push('<p class="light right" style="padding-bottom: 1em"><span class="smallCaps">Account ID:</span> <b>', result.data.account_id, '</b>');
          if (aa.existsIn(permissions, 'debug')){
            contentArray.push('<span class="smallCaps">&nbsp /&nbsp; Location ID:</span> <b>', result.data.id, '</b>');
          }
          contentArray.push('</p>');
        }
        else {
          contentArray.push('<p class="light right" style="padding-bottom: 1em"></p>');
        }
        
        // injectTargetElement.html(contentArray.join(''));
        
        $('#footer').css('padding-bottom', '50em');

        
        
        
        
        
        injectTargetElement.html(
          // facilityNameArray.concat(contentArray).join('')
          facilityNameArray.concat([
            '<div id="searchResultTabsContainer" class="tabsContainer"></div>',
            // '<div id="injectDetails" class="tabPage editableInline"><div class="tableControlsContainer tableControlsContainerBorder"></div>', contentArray.join(''), '<div id="map" style="height: 40em; width: 100%; border-top: 3px solid #e9e9e9; display: none;"></div></div>',
            '<div id="injectDetails" class="tabPage editableInline">',
              '<div class="tableControlsContainer tableControlsContainerBorder"></div>',
              contentArray.join(''),
              '<div id="mapBorder" class="hidden" style="border-top: 3px solid #e9e9e9" />',
              '<div id="mapContainer" style="padding: 1em; position: absolute; top: -2000px;">',
                '<div id="map" style="height: 40em;" />',
                '<div id="mapBlock" style="height: 40em; position: relative; margin-top: -40em; background-color: #fff; line-height: 10em;">',
                  // '<img src="/common/assets/images/loading_16x16.gif" align="absmiddle" />Loading...',
                  '<p style="margin: 0; padding: .5em"><img src="/common/assets/images/large-loading.gif" align="absmiddle"/>&nbsp;<span class="loadingText">Loading map...</span></p>',
                '</div>',
              '</div>',
              '<div id="mapContainerStatic" style="padding: 1em; display: none;">',
              '</div>',
            '</div>',
            '<div id="injectHistory" class="tabPage hidden"></div>',
            
            '<div id="clientUpdateStats" style="position: absolute; top: .5em; right: .75em; text-align: right">',
            '<span class="light smallCaps">Last Visit:</span> ',
            (
              result.data.most_recent_date ?
                [
                  dateFriendlyString(dpExactDateTime(result.data.most_recent_date)),
                  '<span class="light smallCaps"> by </span>',
                  result.data.most_recent_physicist
                ].join('')
                :
                '<span class="light">Unknown</span>'
            ),
            '<br /><span class="light smallCaps">Last Update:</span> ',
            (
              result.data.audit_touched != '2008-07-01 00:00:00'
              ?
                [
                  dateFriendlyString(dpExactDateTime(result.data.audit_touched)),
                  '<span class="light smallCaps"> by </span>',
                  result.data.audit_user_name
                ].join('')
                :
                '<span class="light">Unknown</span>'
            ),
            '</div>'
            
          ]).join('')
          
        ).addClass('noPaddingBottom');
        
        $('#injectDetails').addClass('noPaddingBottom');



        $('td.fieldName a.linkHidden').click(function(){
          var link = $(this);
          var deleted = $(['#', link.attr('field'), 'ValueContainer div.deleted'].join(''));
          
          if (deleted.hasClass('hidden')){
            deleted.removeClass('hidden');
            link.html('Hide<br>deleted');
          }
          else {
            deleted.addClass('hidden');
            link.html('Show<br>deleted');
          }
          
        });


        
        itemEditableSetup({
          editable: $('#injectDetails td.fieldValue div.editable'),
          // editTargetAfter: $('#clientDetailsRight').next('div.clear')
          editTargetAfter: $('#clientDetailsContainer')
        });
      
        // $('#injectDetails td.fieldValue div.editable').each(function(){
          // var item = $(this);
          // itemEditableSetup({
            // editable: item,
            // editShow: itemEditShow,
            // editTargetAfter: $('#clientDetailsRight').next('div.clear')
          // });
        // });
        
        
        
        var formDef = new formObject({
          id: 'searchResultTabs',
          injectTarget: 'searchResultTabsContainer',
          addedInjectTarget: 'none',
          prefix: 'srt_',
          noFrame: true,
          noGap: true,
          noFormElement: true,
          submitType: 'ajax',
          //focus: 'location_name',
          //editTrack: 'activeEdit.dirty',
          validation: aa.objectClone(validationDefault),
          fields: 
            aa.existsIn(permissions, 'accounting') ? 
              [
                {
                  name: 'clientDisplayDetails',
                  type: 'button',
                  action: 'clientDisplayDetails',
                  // target: 'billingTable',
                  style: 'tab active',
                  text: 'Contact Details'
                },
                {
                  name: 'clientDisplayHistory',
                  type: 'button',
                  action: 'clientDisplayHistory',
                  // target: 'billingTable',
                  style: 'tab',
                  // css: 'icon-loading iconright',
                  text: 'Service History'
                },
                {
                  name: 'clientDisplayInvoices',
                  type: 'button',
                  action: 'clientDisplayInvoices',
                  // target: 'billingTable',
                  style: 'tab',
                  // css: 'icon-loading iconright',
                  text: 'Invoices'
                }
              ]
            :
              [
                {
                  name: 'clientDisplayDetails',
                  type: 'button',
                  action: 'clientDisplayDetails',
                  // target: 'billingTable',
                  style: 'tab active',
                  text: 'Contact Details'
                },
                {
                  name: 'clientDisplayHistory',
                  type: 'button',
                  action: 'clientDisplayHistory',
                  // target: 'billingTable',
                  style: 'tab',
                  text: 'Service History'
                }
              ]
        });

        forms[formDef.id] = formDef;
        
        formBuild(formDef, formDef.injectTarget);
        
        //$('#map').css({width: '100%', height: '20em'});
        
        if (result.data.account_id != 0){
          $('#mapBorder').removeClass('hidden');
          $('#mapContainer').css({
            'position': 'relative',
            'top': '0'
          });
          // setTimeout(function(){
            mapEncodeStruct({struct: mapQuery.structure});
          // }, 100);
        }

        // var map = new GMap2(document.getElementById('map'));
        // var burnsvilleMN = new GLatLng(44.797916,-93.278046);
        // map.setCenter(burnsvilleMN, 8);
        
        //$("#mapInline").googlemap({addresses: [mapQuery]});
        
        // jQuery('#mapInline').jmap('init', {'mapType':'hybrid','mapCenter':[40.537677,-74.850712]});
        
        // jQuery('#mapInline').jmap('SearchAddress', {
            // 'query': mapQuery,
            // 'returnType': 'getLocations'
        // }, function(result, options) {
            
            // var valid = Mapifies.SearchCode(result.Status.code);
            // $('#mapInlineStatus').remove();
            // if (valid.success) {
            // jQuery.each(result.Placemark, function(i, point){
                // jQuery('#mapInline').jmap('AddMarker',{
                        // 'pointLatLng':[point.Point.coordinates[1], point.Point.coordinates[0]],
                        // 'pointHTML':point.address
                    // });
                // });
            // } else {
              // $('#mapInline').before(['<div id="mapInlineStatus">', valid.message, '</div>'].join(''));
            // }
        // });
        
        clients.billingLoaded = false;
        
      },
      error: function (XMLHttpRequest, textStatus, errorThrown){
        fblog('autocompleteResultClient(): ajax error');
        statusBannerConnectionError();
      }
    });
  }
  
  
  
  
  
  function contentBuildContacts(data){
    var
      i,
      today = dateString(),
      thisHidden,
      hidden = false,
      first = true,
      contacts = data.contacts,
      hasLocationContacts = false,
      hasAccountContacts = false,
      itemStringArray = [];
    
    if (contacts.length){

      for (i = 0; i < contacts.length; i++){
        if (contacts[i].account_contact == 0){
          thisHidden = false;
          
          itemStringArray.push(
            '<div class="valueContent editable'
          );
          
          if (contacts[i].effective_end && contacts[i].effective_end <= today){
            itemStringArray.push(' deleted hidden');
            thisHidden = true;
            hidden = true;
          }
          
          itemStringArray.push('" field="contacts" editType="contact" item="', contacts[i].id, '"');

          if (!first){
            itemStringArray.push(' style="margin-top: .5em;"');
          }
          
          first = false;
          
          itemStringArray.push('>', dataFormatContact(contacts[i], data, thisHidden), '</div>');
          
          hasLocationContacts = true;
        }
        else {
          hasAccountContacts = true;
        }
        
      }
      
    }
    
    if (hasAccountContacts){

      itemStringArray.push('<div class="smallCaps light valueContentGroupHeader" style="margin-top: ', (hasLocationContacts ? '1' : '1.75'), 'em;">Account Contacts</div>');
      
      for (i = 0; i < contacts.length; i++){
        if (contacts[i].account_contact == 1){
        
          thisHidden = false;
          
          itemStringArray.push('<div class="valueContent valueContentGroupItem editable');
          
          if (contacts[i].effective_end && contacts[i].effective_end <= today){
            itemStringArray.push(' deleted hidden');
            thisHidden = true;
            hidden = true;
          }
          
          itemStringArray.push('" editType="contact" field="contacts" item="', contacts[i].id, '"');

          // if (i > 0){
            itemStringArray.push(' style="margin-top: .5em;"');
          // }
          
          itemStringArray.push('>', dataFormatContact(contacts[i], data, thisHidden), '</div>');
        }
        
      }

      itemStringArray.push('<div class="smallCaps light valueContentGroupFooter" />');
    }
    
    itemStringArray.push('<div class="valueContent editable editableEmpty" ', (itemStringArray.length ? ' style="margin-top: .5em;" ' : ''), ' editType="contact" field="contacts"><span class="highlightLight iconright icon-edit dontprint">add new contact</span></div>');
    
    return {
      string: itemStringArray.join(''),
      hidden: hidden
    };
  }
  
  
  
  
  
  
  // setup editable "fields" in static content
  // requires
  //   config.editable = all editable fields (jQuery object)
  //   config.editShow = function which will be passed editable element which was double-clicked
  function itemEditableSetup(config){
    // if (!aa.existsIn(permissions, 'debug')){
      // return;
    // }
  
  
    var editable = config.editable;
    
    // handle hovering on field values
    editable.each(function(){
      var el = $(this);
      el.html(['<span class="content">', el.html(), '</span>'].join(''));
      
      // make sure we extend existing data rather than overwriting to keep events and other bits intact
      el.data($.extend(el.data(), {
        id: config.id,
        editType: config.editType || el.attr('editType'),
        friendlyName: config.friendlyName,
        title: config.title,
        subtitle: config.subtitle,
        store: config.store,
        keyName: config.keyName,
        data: config.data,
        editShow: config.editShow || itemEditShow,
        editTargetAfter: config.editTargetAfter
      }));
      
    });
    
    editable.append([
      '<div class="editableHint">Double-click<br>to edit</div>'
    ].join(''));
    
    // // make sure we remove any existing handlers first
    // editable.die();
  
    editable
    .bind('mouseenter', function(){
      var el = $(this);
      if (!el.hasClass('editing')){
        el.addClass('hover'); //.find('div.editableHint').show();
        var hint = el.find('div.editableHint');
        setTimeout(function(){
          var parent = hint.parent();
          if (parent.hasClass('hover') && !parent.hasClass('editing')){
            hint.show();
          }
        }, 500);
      }
    })
    .bind('mouseleave', function(){
      var el = $(this);
      if (!el.hasClass('editing')){
        el.removeClass('hover').find('div.editableHint').hide();
      }
    })
    
    // quickly disable text-selections to help prevent highlighting on double-click
    // but reenable after a short timeout in case this doesn't turn out to be a double-click
    .bind('click', function(){
      var el = $(this);
      el.disableTextSelect();
      setTimeout(function(){ el.enableTextSelect(); }, 500);
    })
    
    // enable editing
    .bind('dblclick', function(){
      var el = $(this);
      
      // start editing
      if (!el.hasClass('editing')){
        //el.append(['<div class="clientEdit"><div class="item" style="margin: 0"><table cellspacing="0" class="inputLabelContainer"><tbody><tr><td class="inputLabelLeft"><label for="te_summary" id="te_summary_label">', friendlyName, ':</label></td></tr></tbody></table><input type="text" value="', contentText, '" maxlength="100" size="10" titlecase="sentence" class="inputText" id="te_summary" name="te_summary"/></div><div class="clearfix" /></div>'].join(''));              
        //config.editShow(el);
        el.data('editShow')(el);
      }
      
      return false;
      
      // end editing
      // else {
        // el.removeClass('editing');
      // }
    });
  }
  









  function itemEditShow(el){
    // var el = config.el;
    
    //if (itemEditHide({target: el, noSave: true})){
    if (shared.items && shared.items.active && shared.items.active.el){
      shared.items.active.el.removeClass('editing hover');
    }
    $('#itemEdit').remove();
    $('#itemEditWrap').remove();

    var items = shared.items;
    var editType = el.data('editType');
    var fields = el.data('fields');
    var title = el.data('title');
    var subtitle = el.data('subtitle');
    var friendlyName = el.data('friendlyName') || as.toTitleCase(editType);
    var store = el.data('store');
    var storeindex = el.data('storeindex');
    var id = el.data('id');
    var data = el.data('data');
    var keyName = el.data('keyName') || 'id';
    var preexisting = true;
    
    if (!data){
      data = aa.findObjectByKey(store, id, keyName);
      if (data && data.length){
        data = data[0];
      }
      else {
        preexisting = false;
        data = {};
      }
    }
      
    var contentText = el.children('span.content').text();
    
    fblog(['item edit: ', editType, ' (', id, ') = ', contentText].join(''));
  
    el.addClass('editing').enableTextSelect().find('div.editableHint').hide();
  
  
    var
      //task_id,
      //preexisting = true,
      formId = 'itemEditForm',
      formDef,
      //billingRate = 0,
      //currentSchedule = $('#currentPage.schedule'),
      inlineClosure,
      isLocked = false;
      
      
      
    switch (editType){
    
      case "address":
        data = clients.active.address; //aa.objectClone(clients.active.address);
        store = clients.active;
        storeindex = 'address';
        preexisting = true;
        break;
        
        
        
      case "contact":
        //friendlyName = "Contact";
        
        //clients.editing.data = data;
        // if (itemIndex){
          // clients.editing.data = aa.findObjectByKey(clients.active.contacts, itemIndex, 'id', false, true);
        // }
        // else {
          // preexisting = false;
        // }

        storeindex = el.attr('field');
        store = clients.active;
        
        data = aa.findObjectByKey(store[storeindex], el.attr('item'));
        if (data && data.length){
          data = data[0];
          preexisting = true;
        }
        else {
          data = {};
        }
        
        // storeindex = 'contacts';
        id = data.id;
        break;
        
      
      
      case 'taskItem':
        break;
        
        
        
      default:
        // if (clients.active[editType] !== undefined){
          //clients.editing.data[editType] = clients.active[editType]; //aa.objectClone(clients.active.address);
          // formDef.focus = 'address1';
          
          var label = '', help = '';
          
          data = clients.active[editType];
          
          if (data){
            preexisting = true;
          }
          
          switch (editType){
            case 'phone':
              label = 'Main phone number (with area code)';
              help = 'To add more than one phone number for this location, please create a contact including a department, title, and/or person\'s name to differentiate it from this main number.'
              break;
              
            case 'fax':
              label = 'Main fax number (with area code)';
              help = 'To add more than one fax number for this location, please create a contact including a department, title, and/or person\'s name to differentiate it from this main number.'
              break;
              
            case 'email':
              label = 'Main email address';
              help = 'To add more than one email address for this location, please create a contact including a department, title, and/or person\'s name to differentiate it from this main address.'
              break;
              
            case 'website':
              label = 'Website link';
              help = 'NOTE: You don\'t need to enter http:// before the server name.'
              break;
              
          }
          
          store = clients.active;
          storeindex = editType;
          
          fields = [
            {
              name: editType,
              label: label,
              // required: true,
              //capitalize: true,
              help: help,
              width: 'full',
              prefillVal: data || ''
            }
          ];
        // }
        // else {
          // fblog(['editType "', editType, '" not found'].join(''));
          // return;
        // }
        
    }
        

        
        
    var active = items.active = {
      el: el,
      editType: editType,
      store: store,
      storeindex: storeindex,
      id: id,
      friendlyName: friendlyName,
      fields: fields || items.templates[editType],
      // data: aa.objectClone(data)
      data: _.clone(data)
    };
  

    formDef = {
      id: formId,
      container: 'itemEditRow',
      injectTarget: 'itemEditInject',
      addedInjectTarget: 'itemEditStaticInject',
      focus: active.fields[0].name, //'location_name',
      focusCondition: function(){
        return !preexisting; // !Boolean(schedule.activeEdit.data.location_name);
      },
      prefix: 'ie_',
      submitType: 'ajax',
      //editTrack: 'activeEdit.dirty',
      validation: $.extend(aa.objectClone(validationDefault), {
      }),
      data: active.data,
      fields: active.fields
    };
      
  
    
    
    formDef = new formObject(formDef);
    
    // add this form to global forms array
    forms[formDef.id] = formDef;

    var editHTML = [
      '<div id="itemEdit" class="itemEdit"><div class="itemEditHeader">',
      '<div class="itemEditTitle"><span style="color: #fff">',
      (preexisting ? 'Edit ' : 'Add '),
      friendlyName,
      (title ? [':</span> ', title].join('') : ''),
      // (subtitle ? ['<br /><span style="font-size: .7em">', subtitle, '</span>'].join('') : ''),
      '</div>',
      '<table class="itemEditSubtitle" cellspacing="0" cellpadding="0"><tr><td>'
    ];
    
    editHTML.push(
      buttonCreate(formDef, {
        name: 'itemSave',
        //target: schedule.activeEdit.data,
        action: 'itemEditSave',
        text: 'Save changes',
        style: 'inline left-align',
        css: 'icon-ok iconleft'
      }),
                
      buttonCreate(formDef, {
        name: 'itemDiscard',
        //target: schedule.activeEdit.data,
        action: 'itemEditDiscard',
        text: 'Discard changes',
        style: 'inline left-align',
        css: 'icon-undo iconleft'
      })
    );
    
    if (editType == 'contact'){
      editHTML.push(
        buttonCreate(formDef, {
          name: 'itemDelete',
          action: 'itemEditDelete',
          text: ['Delete this ', editType].join(''),
          style: 'inline left-align',
          css: 'icon-cancel iconleft'
        })
      );
    }
    
    editHTML.push(
      '</td>',
      
      (subtitle ? ['<td class="right"><span style="position: relative; top: .5em">', subtitle, '</span></td>'].join('') : ''),
      
      '</tr></table>',
      
      '</div>',
      
      '<div class="itemEditMessage callout calloutWide calloutStatic" id="itemEditMessage" style="margin-top: 0; display: none">',
      '</div>',

      '<div class="itemEditContent" id="itemEditContent">',
      
      '<div id="', formDef.injectTarget, '"></div>',
      
      '</div></div></td></tr>'
    );
    
    editHTML = editHTML.join('');
    
    // insert edit row below selected timeblock
    // $(['#currentPage > tbody > tr.timeblockRowLast[row=', originCell.parent().attr('row'), ']'].join('')).after(editHTML);
    // $(['#tr', originCell.parent().attr('row'), 'afternoon'].join('')).after(editHTML);
    if (!$('#jq-wip').length){
      $('#hidden_container').append('<div id="jq-wip" />');
    }
    $('#jq-wip').html(editHTML);
    
    var editEl = $('#itemEdit');

    formBuild(formDef, formDef.injectTarget, function(){
      // $('#taskEditShow').show();
      // scheduleEditOnShow(formDef);
    });
    
    //$(['#injectDetails div.tableControlsContainer'].join('')).after(editEl);
    // el.parent().parent().next('.clearfix').after(editEl);
    var target;
    var insertLocation;
    var insertEl = editEl;

    insertLocation = target = el.data('editTargetAfter');

    if (target && target.length){
      if (target.next('.clearfix').length){
        insertLocation = target.next('.clearfix');
      }
      
      insertLocation.after(insertEl);
      
      // if (target.hasClass('item')){
        // editEl.width(editEl.closest('.body').innerWidth() - (2 * parseInt(target.css('margin-left')))); //'<div id="itemEditWrap" class="item" style="width: 43.25em" />');
        // // insertEl = $('#itemEditWrap');
      // }
      
    }
    else {
      fblog('itemEditShow(): no target for editor insertion');
    }
    
    itemEditOnShow(formDef);

    // $('#taskEditRow div.taskEdit.').css('background-color', '#bbb');
    $('#taskEditRow div.taskEdit').addClass('taskEditBlurred');
    
    //itemEditOnShow(formDef);
    
    formSetupEventHandlers(formDef, editEl);
    $('#currentPage').enableTextSelect();
    
  }
  
  
  
  function itemEditOnShow(formDef){
    var 
      editEl = $('#itemEdit'),
      viewport = $(window),
      i;
  
  
    if (editEl.height() >= viewport.height()){
      viewport.scrollTo(editEl);
    }
    else {
      viewport.scrollTo(editEl, {offset: centerOffsets(editEl)});
    }
    
    //forceFocus([formDef.prefix, formDef.focus].join(''));
  }
  
  
  
  function itemEditHide(config){
    if (!config){
      config = {};
    }
    
    var item;
    
    // if (clients.editing && clients.editing.data !== undefined){
    if (shared.items && shared.items.active){
      item = shared.items.active;
      
      if (shared.inputFocused){
        $(['#', shared.inputFocused].join('')).trigger('blur');
      }
      
      var contentEl = $('span.content', item.el); //$('#injectDetails > div.item div.editing').removeClass('editing hover').children('span.content');
      
      if (!config.noSave){
        // first validate all fields and set dirty bit if necessary
        if (!itemEditCommit(config)){
          return false;
        }
        
        var contentArr = [], content;
        // var ced = clients.editing.data;
        var ced = item.data;
        //var field = clients.editing.field;
        
        switch (item.editType){
          case 'phone':
          case 'fax':
          case 'email':
          case 'website':
            if (item.editType == 'email'){
              contentEl.html(ced[item.editType] ? ['<a href="mailto:', ced[item.editType], '">', ced[item.editType], '</a>'].join('') : '<span class="highlightLight iconright icon-edit dontprint">edit</span>'); 
            }
            else if (item.editType == 'website'){
              ced[item.editType] = ced[item.editType].replace(/(http:\/\/)/i, '');
              contentEl.html(ced[item.editType] ? ['<a href="http://', ced[item.editType], '" target="_blank">', ced[item.editType], '</a>'].join('') : '<span class="highlightLight iconright icon-edit dontprint">edit</span>'); 
            }
            else {
              contentEl.html(ced[item.editType] || '<span class="highlightLight iconright icon-edit dontprint">edit</span>');
            }
            break;
            
          case 'address':
            contentArr.push(clients.active.location_name);
            aa.pushIf(contentArr, ced.address1, ced.address2, ced.address3);
            contentArr.push([ced.city, ', ', ced.state, ' ', ced.zip].join(''));
            contentEl.html(contentArr.join('<br />'));
            break;

          case 'contact':
            // if (ced.id){
              // contentEl.html(dataFormatContact(ced, shared.items.active.store));
            // }
            // else {
            
            
              $('#contactsValueContainer div.editable').unbind();
              content = contentBuildContacts(shared.items.active.store);
              if (content.hidden){
                $('#contactsLinkHidden').html('Show<br />hidden').removeClass('hidden');
              }
              else {
                $('#contactsLinkHidden').addClass('hidden');
              }
              
              // must delay this for IE8 compatibility
              setTimeout(function(){
                $('#contactsValueContainer').html(content.string);
                itemEditableSetup({
                  editable: $('#contactsValueContainer div.editable'),
                  // editTargetAfter: $('#clientDetailsRight').next('div.clear')
                  editTargetAfter: $('#clientDetailsContainer')
                });
              }, 1);
              
              
            // }
            break;

        }
        
        // itemSaveDirty();
        
      }
      // remove dropdown results if visible
      //$('div.ac_results').remove();
      
      item.el.removeClass('hover editing');
      var editEl = $('#itemEdit');
      formClearEventHandlers(editEl);
      editEl.remove();
      // delete clients.editing;
      delete shared.items.active;
      
      // $('#taskEditRow div.taskEdit.').css('background-color', '');
      $('tr.taskEditRow div.taskEdit').removeClass('taskEditBlurred');

    }
    
    //$('#currentPage').disableTextSelect();
    
    $(window).scrollTo(contentEl, {offset: centerOffsets(contentEl)});

    return true;
  }
  
  
  
  // function itemEditDiscard(){
    // clientEditHide({noSave: true});
  // }
  
  
  function itemEditOnChange(){
    var formDef = forms.itemEditForm;
    
    var i, checked, id, name, config, rerun = false;
    
    for (i = 0; i < formDef.dropdowns.length; i++){
      id = formDef.dropdowns[i].id;
      name = formDef.dropdowns[i].name;
      config = formDef.dropdowns[i].config;
      
      checked = $(['#itemEditForm [name=', name, ']:checked'].join(''));
        
      if (!checked.length){
        $(['#itemEditForm [name=', name, '][value=', config.defaultValue, ']'].join('')).attr('checked', true);
        $(['#', id].join('')).next('.multiSelectOptions').multiSelectUpdateSelected(config);
        rerun = true;
      }
    }
    
    // if (rerun){
      // itemEditOnChange();
      // return;
    // }

    // var vipchecked = $(['#itemEditForm [name=', formDef.prefix,
      // 'vip', // this is the form control to query
      // ']:checked'].join(''));
      
    // if (!vipchecked.length){
      // $(['#itemEditForm [name=', formDef.prefix, 'vip][value=0]'].join('')).attr('checked', true);
      // $(['#', formDef.prefix, 'vip'].join('')).next('.multiSelectOptions').multiSelectUpdateSelected(formDef.dropdowns[0].config);
      // itemEditOnChange();
      // return;
    // }


  }
  
  
  
  function itemEditCommit(config){
    fblog('itemEditCommit():');
    
    var
      messageEl = $('#itemEditMessage').html('').hide(),
      formDef = forms.itemEditForm,
      items = shared.items,
      item = items.active,
      field = item.editType,
      ced = item.data,
      original = item.store,
      store = item.store,
      storeindex = item.storeindex,
      dirtyType,
      dirtyId,
      dirtyExtra = {},
      found = false;
    
    if (item.storeindex && item.id){
      original = item.store[item.storeindex];

      found = aa.findObjectByKey(original, item.id);
      if (found){
        original = found[0];
      }
      else {
        original.push({});
        original = original[original.length - 1];
      }
    }
    
    var
      i,
      updateMap = false;
      dirty = false;

    var fieldList = formGetTextFieldNames(formDef); //['address1', 'address2', 'address3', 'city', 'state', 'zip'];
    var fields = formGetValues(formDef, fieldList);
    var missing = fields.missing;
    var checked;
    
    fields = fields.values;
      
    // apply capitalization to applicable fields (otherwise data only appears capitalized in form due to CSS rules)
    formDataCapitalize(formDef, fields);
    
    switch (field){
      case 'address':
        fblog('itemEditCommit(): changed address?');
        
        if (
          missing.length
          // !fields.address1
          // ||
          // !fields.city
          // ||
          // !fields.state
        ){
          messageEl.html('You must specify <b>at least</b> a street address, city, and state to continue.').show();
          return false;
        }
        
        updateMap = true;
        
        dirtyType = 'clients';
        dirtyId = clients.active.id;
        
        break;
        
      case 'contact':
        fblog('itemEditCommit(): changed contact?');
        
        if (
          missing.length > 2
          // !fields.name
          // &&
          // !fields.title
          // &&
          // !fields.department
        ){
          messageEl.html('You must specify <b>at least</b> a name, title, <b>and/or</b> department to continue (all 3 would be ideal).').show();
          return false;
        }

        $.extend(fields, item.fields[0].set(fields, fields.name));
        
        delete fields.name;
        
        checked = $(['#itemEditForm [name=vip]:checked'].join(''));
          
        if (checked.length){
          fields.vip = Number(checked.eq(0).val());
        }
        
        checked = $(['#itemEditForm [name=account_contact]:checked'].join(''));
          
        if (checked.length){
          fields.account_contact = Number(checked.eq(0).val());
        }

        // else {
          // $(['#itemEditForm [name=', formDef.prefix, 'vip][value=0]'].join('')).attr('checked', true);
          // setTimeout(function(){
            // $(['#', formDef.prefix, 'vip'].join('')).next('.multiSelectOptions').multiSelectUpdateSelected(formDef.dropdowns[0].config);
          // }, 100);
          // fields.vip = 0;
        // }
        
        $.extend(fields, {
          id: ced.id,
          //account_id: ced.account_id,
          //vip: ced.vip,
          phone_home: ced.phone_home
          //name_prefix: ced.name_prefix
        });
        
        if (config.deleteItem){
          fields.effective_end = dateString();
        }
        else {
          fields.effective_end = '';
        }
        
        dirtyType = 'clients';
        dirtyId = clients.active.id;
        
        break;
      
      default:
        if (aa.existsIn(['phone', 'fax', 'email', 'website'], field)){
          dirtyId = clients.active.id;
        }
        
        fblog(['itemEditCommit(): changed ', field, ' (', item, ')?'].join(''));
        
        if (field == 'phone' || field == 'fax'){
          fields[field] = formatPhone(fields[field]);
        }
        
        if (missing.length){
          messageEl.html('You must specify the fields highlighted in <span class="highlightWarning">red</span> to continue.').show();
          return false;
        }
          
    }

    // if new values aren't the same as previous values, save new values and flag to be saved
    if (!aa.objectCompare(fields, original, true)){
      //$.extend(original, fields);
      
      shared.items.active.data = fields;
      
      switch (field){
        case 'address':
          ced = clients.active.address = fields;
          break;
          
        case 'contact':
          var found = false;
          if (item.id){
            var index = aa.indexOfObjectByKey(store[storeindex], item.id);
            if (index.length){
              store[storeindex][index[0]] = fields;
              found = true;
            }
          }
          
          if (!found){
            if (!store[storeindex]){
              store[storeindex] = [];
            }
            store[storeindex].push(fields);
          }
          
          //dirtyExtra.account_id = store.account_id;
          dirtyExtra.location_id = store.id;
          dirtyExtra.account_id = store.account_id;
          break;
          
        default:
          $.extend(original, fields);
      }
      
      if (!items.dirty){
        items.dirty = [];
      }
      
      //aa.pushUnique(items.dirty, {id: dirtyId, type: dirtyType});
      
      var existing = aa.findObjectByKeys(items.dirty, {editType: item.editType, id: item.id});
      
      for (i = 0; i < existing.length; i++){
        existing.abort = true;
      }
      
      items.dirty.push({
        editType: item.editType,
        id: item.id || dirtyId,
        timestamp: new Date().valueOf(),
        // data: $.extend(aa.objectClone(item.data), dirtyExtra)
        data: $.extend(_.clone(item.data), dirtyExtra)
      });
      
      itemSaveDirty();
      
    }
    
    if (updateMap){
      mapEncodeStruct({struct: {
        name: clients.active.location_name, //facilityNameDefault,
        address1: fields.address1,
        address2: fields.address2,
        address3: fields.address3,
        citystatezip: [
          as.echoIf(fields.city, '', ', '),
          as.echoIf(fields.state, '', ''),
          as.echoIf(fields.zip, ' ', '', '', 0, 6) // only use first 5 digits of zip code; Google Maps doesn't understand ZIP+4
        ].join(''),
        city: fields.city,
        state: fields.state,
        zip: fields.zip
      }});
    }
      
    return true;
    
    
  }
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  






  
  
  

  
  function formGetTextFieldNames(formDef){
    var i, fields = formDef.fields, names = [];
    
    for (i = 0; i < fields.length; i++){
      if (fields[i].name && (!fields[i].type || fields[i].type == 'textarea')){
        names.push(fields[i].name);
      }
    }
    
    return names;
  }
  
  
  
  
  
  function formGetValues(formDef, fieldList){
    var i, values = {}, fieldDef, requiredMissing = [];
    
    for (i = 0; i < fieldList.length; i++){
      values[fieldList[i]] = $.trim($(['#', formDef.prefix, fieldList[i]].join('')).val());
      
      fieldDef = aa.findObjectByKey(formDef.fields, fieldList[i], 'name');
      if (fieldDef.length){
        fieldDef = fieldDef[0];
        if (fieldDef.required && !values[fieldList[i]]){
          requiredMissing.push(fieldList[i]);
        }
      }
    }
    
    return {values: values, missing: requiredMissing};
  }
  
  
  
  
  function formDataCapitalize(formDef, fields){
    var i, thisField;
    
    for (i in fields){
      thisField = aa.findObjectByKey(formDef.fields, i, 'name');
      
      if (thisField.length){
        thisField = thisField[0];
        
        if (thisField.capitalize){
          if (thisField.capitalize == 'sentence'){
            fields[i] = as.toSentenceCase(fields[i]);
          }
          if (thisField.capitalize == 'upper'){
            fields[i] = fields[i].toUpperCase();
          }
          else {
            fields[i] = as.toTitleCase(fields[i]);
          }
        }
      }
    }
  }
  
  
  
  
  // start ajax updater interval
  // should get all dirty tasks and save them to server, then retrieve any new tasks seamlessly in the background
  function clientUpdaterStart(){
    if (!false){
      clientUpdaterStop();
      $('#hidden_container').everyTime([clients.performance.timeoutSaveDirty, 's'].join(''), 'saveDirtyItems', function(){
          clientSaveDirty();
        }
      );  

      // $('#hidden_container').everyTime([clients.performance.timeoutGetUpdated, 's'].join(''), 'getUpdatedTasks', function(){
          // scheduleGetUpdated();
        // }
      // );
      
      if (!clients.initIdle){
      //$(document).unbind("idle.idleTimer active.idleTimer");
      //$.idleTimer('destroy');
      
        $(document).bind("idle.idleTimer", function(){
          fblog('idle: clients');
          clientUpdaterStop();
        });
        $(document).bind("active.idleTimer", function(){
          fblog('active: clients');
          clientUpdaterStart();
        });
        
        $.idleTimer(clients.performance.timeoutIdle * 1000);
        
        clients.initIdle = true;
      
      }
      
      // $('#hidden_container').everyTime([schedule.performance.timeoutPrecache, 's'].join(''), 'precacheSchedule', function(){
          // if (!schedule.precache.complete){
            // schedulePrecache();
          // }
          // else {
            // $('#hidden_container').stopTime('precacheSchedule');
          // }
        // }
      // );  
    }
  }
  
  // stop ajax updater interval
  function clientUpdaterStop(){
    $('#hidden_container').stopTime();
  }
  
  
  function itemSaveDirty(){
    fblog(['itemSaveDirty(): ', clients.dirty].join(''));
    
    var dirty = shared.items.dirty;
    
    if (dirty && dirty.length){
      fblog(['itemSaveDirty(): saving ', dirty.length, ' dirty item(s)'].join(''));
      
      var params = {
        items: JSON.stringify(dirty)
      };
      
      $.ajax({
        url: [shared.urlRoot, '/ajaxSSGet/itemSave'].join(''),
        data: params,
        // dataType: 'json',
        // beforeSend: function (request, arg2){
          // fblog(['ajax beforeSend, ', arguments.length, ' args'].join(''));
          // // request.channel.contentLength = data.length;
          // // request.channel.contentType = "text/html";
          
        // },
        //type: 'POST',
        success: function (data, textStatus){
          // fblog(['ajax args ', arguments.length].join(''));
          
          var i, thisPage;
          
          try {
            var decoded = shared.lastJSON.generic = eval(["(", data, ")"].join(''));
          }
          catch (err){
            fblog(['itemSaveDirty() decode failure: ', err.name, ', ', err.message].join(''));
            return;
          }

          fblog(['itemSaveDirty(): success = ', decoded.success].join(''));
          
          if (decoded.success === false){
            fblog(decoded.message);
            if (decoded.auth === false){
              window.location.reload(); //href = "/login/clients";
            }
            // return false;
          }
          
          // update local copies of saved tasks
          for (i = 0; i < decoded.saved.length; i++){
            var item = aa.removeObjectByKeys(shared.items.dirty, {editType: decoded.saved[i].editType, id: decoded.saved[i].id, timestamp: decoded.saved[i].timestamp});
          }
          
          if (decoded.failed.length){
            fblog('itemSaveDirty(): failed to saved ' + decoded.failed.length + ' item(s):');
            for (i = 0; i < decoded.failed.length; i++){
              fblog(decoded.failed[i].editType + ': ' + decoded.failed[i].id);
            }
          }
          
          if (!shared.items.dirty.length){
            updaterKill('itemSaveDirty');
          }
          
          // scheduleUpdateHighlightRecent();
          
        },
        error: function (XMLHttpRequest, textStatus, errorThrown){
          fblog(['scheduleSaveDirty(): ajax error = ', textStatus, ' (', errorThrown, ')'].join(''));
          // fblog(String(errorThrown === undefined));
        }
        
      });

    }

    
    
    updaterStart('itemSaveDirty');
  }
  
  
  
  
  function mapInitGeocoder(continueAfterInit){
    if (!shared.geo.geocoder){
      if (!window.google){
        fblog('unable to load Google API');
        return false;
      }
      else if (!window.google.maps || !window.google.maps.ClientGeocoder){
        initMapsAPI(continueAfterInit);
        return false;
      }
      
      //fblog(Boolean(google.maps.ClientGeocoder));
      shared.geo.geocoder = new google.maps.ClientGeocoder();
                     
      shared.geo.reasons = [];
      shared.geo.reasons[G_GEO_SUCCESS] = "Success";
      shared.geo.reasons[G_GEO_MISSING_ADDRESS] = "Missing Address";
      shared.geo.reasons[G_GEO_UNKNOWN_ADDRESS] = "Unknown Address";
      shared.geo.reasons[G_GEO_UNAVAILABLE_ADDRESS] = "Unavailable Address";
      shared.geo.reasons[G_GEO_BAD_KEY] = "Bad API Key";
      shared.geo.reasons[G_GEO_TOO_MANY_QUERIES] = "Too Many Queries";
      shared.geo.reasons[G_GEO_SERVER_ERROR] = "Server error";
      
    }
    
    return true;
  }
  
  
  function mapEncodeStruct(params){ //attemptNum, result, target, success){
    if (!params.struct){
      return false;
    }
    
    if (params.result){
      var reason="Code " + params.result.Status.code;
      if (shared.geo.reasons[params.result.Status.code]) {
        reason = shared.geo.reasons[params.result.Status.code];
      }
      fblog([reason, ': ', params.result.name].join(''));
    }
    
    var newAddress = [];
    
    if (!params.attemptNum){
      params.attemptNum = 0;
    }
    
    switch (params.attemptNum){
      case 0:
        newAddress.push(params.struct.address1, params.struct.address2, params.struct.address3, params.struct.citystatezip);
        break;
        
      case 1:
        newAddress.push(params.struct.address1, params.struct.address2, params.struct.citystatezip);
        break;
        
      case 2:
        newAddress.push(params.struct.address1, params.struct.citystatezip);
        break;
      
      case 3:
        newAddress.push(params.struct.address2, params.struct.citystatezip);
        break;
        
      case 4:
        newAddress.push(params.struct.address1, params.struct.city, params.struct.state);
        break;
        
      case 5:
        newAddress.push(params.struct.address1, params.struct.zip);
        break;
        
      case 6:
        newAddress.push(params.struct.citystatezip);
        break;
        
      case 7:
        newAddress.push(params.struct.city, params.struct.state);
        break;
        
      case 8:
        newAddress.push(params.struct.zip);
        break;
        
      default: 
        fblog('mapEncodeStruct() failed after all attempts');
        if (params.target){
          params.target.hide();
        }
        else {
          $('#map').hide();
        }
        return;
      
    }
    
    mapEncode({
      address: newAddress.join(' '),
      success: params.success,
      failure: function(result){
        mapEncodeStruct({
          struct: params.struct,
          attemptNum: params.attemptNum + 1,
          success: params.success,
          result: result,
          target: params.target
        })
      },
      struct: params.struct,
      saveTo: params.saveTo,
      target: params.target
    });
    
      
      // mapEncode({
        // address: [params.struct.address1, params.struct.address2, params.struct.address3, params.struct.citystatezip].join(', '),
        // success: params.success,
        // failure: function(result){ mapEncodeStruct({struct: params.struct, attemptNum: params.attemptNum + 1, success: params.success, result: result, target: params.target}) },
        // struct: params.struct,
        // saveTo: params.saveTo,
        // target: params.target
      // });
    // }
    // else if (params.attemptNum == 1){
      // mapEncode({
        // address: [params.struct.address1, params.struct.address2, params.struct.citystatezip].join(', '),
        // success: params.success,
        // failure: function(result){ mapEncodeStruct({struct: params.struct, attemptNum: params.attemptNum + 1, success: params.success, result: result, target: params.target}) },
        // struct: params.struct,
        // saveTo: params.saveTo,
        // target: params.target
      // });
      // // mapEncode([struct.address1, struct.address2, struct.citystatezip].join(', '), params.success, function(params){ mapEncodeStruct(struct, 2, params, params.target) }, struct, params.target );
    // }
    // else if (params.attemptNum == 2){
      // mapEncode({
        // address: [params.struct.address1, params.struct.citystatezip].join(', '),
        // success: params.success,
        // failure: function(result){ mapEncodeStruct({struct: params.struct, attemptNum: params.attemptNum + 1, success: params.success, result: result, target: params.target}) },
        // struct: params.struct,
        // saveTo: params.saveTo,
        // target: params.target
      // });
      // // mapEncode([struct.address1, struct.citystatezip].join(', '), params.success, function(params){ mapEncodeStruct(struct, 3, params, params.target) }, struct, params.target );
    // }
    // else if (params.attemptNum == 3){
      // mapEncode({
        // address: [params.struct.address2, params.struct.citystatezip].join(', '),
        // success: params.success,
        // failure: function(result){ mapEncodeStruct({struct: params.struct, attemptNum: params.attemptNum + 1, success: params.success, result: result, target: params.target}) },
        // struct: params.struct,
        // saveTo: params.saveTo,
        // target: params.target
      // });
      // // mapEncode([struct.address2, struct.citystatezip].join(', '), params.success, function(params){ mapEncodeStruct(struct, 4, params, params.target) }, struct, params.target );
    // }
    // else if (params.attemptNum == 4){
      // mapEncode({
        // address: [params.struct.address1, params.struct.zip].join(' '),
        // success: params.success,
        // failure: function(result){ mapEncodeStruct({struct: params.struct, attemptNum: params.attemptNum + 1, success: params.success, result: result, target: params.target}) },
        // struct: params.struct,
        // saveTo: params.saveTo,
        // target: params.target
      // });
      // // mapEncode([struct.address2, struct.citystatezip].join(', '), params.success, function(params){ mapEncodeStruct(struct, 4, params, params.target) }, struct, params.target );
    // }
    // else if (params.attemptNum == 5){
      // mapEncode({
        // address: [params.struct.address2, params.struct.zip].join(' '),
        // success: params.success,
        // failure: function(result){ mapEncodeStruct({struct: params.struct, attemptNum: params.attemptNum + 1, success: params.success, result: result, target: params.target}) },
        // struct: params.struct,
        // saveTo: params.saveTo,
        // target: params.target
      // });
      // // mapEncode([struct.address2, struct.citystatezip].join(', '), params.success, function(params){ mapEncodeStruct(struct, 4, params, params.target) }, struct, params.target );
    // }
    // else if (params.attemptNum == 6){
      // mapEncode({
        // address: params.struct.citystatezip,
        // success: params.success,
        // failure: function(result){ mapEncodeStruct({struct: params.struct, attemptNum: params.attemptNum + 1, success: params.success, result: result, target: params.target}) },
        // struct: params.struct,
        // saveTo: params.saveTo,
        // target: params.target
      // });
      // // mapEncode([struct.citystatezip].join(', '), params.success, function(params){ mapEncodeStruct(struct, 5, params, params.target) }, struct, params.target );
    // }
    // else if (params.attemptNum == 7){
      // mapEncode({
        // address: params.struct.zip,
        // success: params.success,
        // failure: function(result){ mapEncodeStruct({struct: params.struct, attemptNum: params.attemptNum + 1, success: params.success, result: result, target: params.target}) },
        // struct: params.struct,
        // saveTo: params.saveTo,
        // target: params.target
      // });
      // // mapEncode([struct.citystatezip].join(', '), params.success, function(params){ mapEncodeStruct(struct, 5, params, params.target) }, struct, params.target );
    // }
    
    
  }
  
  
  function mapEncode(params){ //address, callbackSuccess, callbackFailure, struct, target){
    if (!mapInitGeocoder(function(){mapEncode(params)})) {
      return false;
    }
    
    params.address = $.trim(params.address);
    
    fblog(['encoding: ', params.address].join(''));

    if (
      !params.address
      // ||
      // !params.address.replace(',', '')
      // ||
      // !/\d/.test(params.address.charAt(0)) // if first character is not a digit
    ){
      fblog('mapEncode(): invalid address passed');
      if (params.failure && typeof params.failure == 'function'){
        params.failure();
      }
      return false;
    }
    shared.geo.geocode = false;
    
    shared.geo.geocoder.getLocations(params.address, function (result){
      if (result.Status.code == G_GEO_SUCCESS) {
        if (params.saveTo && typeof params.saveTo == 'object'){
          params.saveTo.geocode = result;
        }
        
        if (params.success && typeof params.success == 'function'){
          params.success(result);
        }
        else {
          shared.geo.geocode = result.Placemark[0].Point.coordinates;
          fblog(['found: ', result.Placemark[0].address, ' (', result.Placemark[0].AddressDetails.Accuracy, '; ', result.Placemark[0].Point.coordinates[1], ',', result.Placemark[0].Point.coordinates[0], ')'].join(''));
          mapShowLocations(
            {
              //result: result,
              //struct: params.struct,
              locations: [
                {
                  lat: shared.geo.geocode[1],
                  lng: shared.geo.geocode[0],
                  name: params.struct.name,
                  address: result.Placemark[0].address
                }
              ],
              target: params.target
            }
          );
        }
      }
      else {
        if (params.failure && typeof params.failure == 'function'){
          params.failure(result);
        }
        else {
          var reason="Code " + result.Status.code;
          if (shared.geo.reasons[result.Status.code]) {
            reason = shared.geo.reasons[result.Status.code];
          }
          fblog([reason, ': ', result.name].join(''));
          //$("#add-point .error").html(reason).fadeIn();
          shared.geo.geocode = false;
        }
        return false;
      }
    });
    
    return true;
  }
  
  
  function mapShowLocations(params){
    if (!params){
      params = {};
    }
    
    if (!shared.geo.currentLocations){
      shared.geo.currentLocations = [];
    }
    
    // create map
    var 
      target = params.target || $('#map'),
      map,
      loc,
      marker,
      infoHTML,
      infoWindow,
      items = params.locations || shared.geo.currentLocations,
      item,
      i;
      
    // target.show();
    // $('#mapBorder').removeClass('hidden');
    // $('#mapContainer').css({
      // 'position': 'relative',
      // 'top': '0'
    // });
    
    shared.geo.map = map = new google.maps.Map2(target[0], {
      backgroundColor: '#fff',
      googleBarOptions: {
        // showOnLoad: true
      }
    });
    
    
    // hide map loading placeholder
    google.maps.Event.addListener(map, "tilesloaded", function(){    
      setTimeout(function(){
        $('#mapBlock').fadeOut(250);
      }, 100);
    });
    
    
    // map.addMapType(G_SATELLITE_3D_MAP); // requires Google Earth browser plugin - doesn't seem to autoload if needed (just hangs)
    map.addControl(new google.maps.LargeMapControl3D());
    map.addControl(new google.maps.ScaleControl());
    // map.addControl(new google.maps.LocalSearch());
    // map.enableGoogleBar();
    // map.addMapType(G_PHYSICAL_MAP);
    // map.addControl(new google.maps.MapTypeControl());
    
    // change offset of infoWindow relative to marker icons
    //G_DEFAULT_ICON.infoWindowAnchor = new GPoint(9,40);
    for (i = 0; i < items.length; i++){
      item = items[i];
      // fblog(['showing location at ', geocode[1], ', ', geocode[0]].join(''));
      loc = new google.maps.LatLng(item.lat,item.lng);
      
      if (params.circleLastPoint && i == items.length - 1){
        // add circle overlay
        //var homeLoc = new google.maps.LatLng(shared.homeLatLng.lat, shared.homelatLng.lng);
        var circle = new CircleOverlay(loc, params.circleLastPoint, "#336699", 1, 1, '#336699', 0.1);
        map.addOverlay(circle);
        var home = new CircleOverlay(loc, .5, "#993333", 1, 1, '#993333', 0.5);
        map.addOverlay(home);
      }
      
      // create location marker
      marker = new google.maps.Marker(loc);
      map.addOverlay(marker);
      
      if (!item.address){
        item.address = [item.address1];
        aa.pushIf(item.address, item.address2);
        aa.pushIf(item.address, item.address3);
        item.address.push([item.city, ', ', item.state, ' ', item.zip].join(''));
        item.address = item.address.join(', ');
      }
      
      mapInfoWindowUpdate(item, params);
      infoWindow = item.map.infoWindow.get(0);
      
      marker.bindInfoWindowHtml(infoWindow);
      
      google.maps.Event.addListener(marker, "infowindowopen", function(thisItem, thisIndex){
        // $('div.mapInfoWindow').children('p.last').removeClass('last');
        // fblog($('div.mapInfoWindow').children('p.last').length);
        // fblog('test');
        // $(shared.geo.map.getInfoWindow().getContentContainers()).children('div.mapInfoWindow').children('p:last').removeClass('last');
        
        return function(){
          // $('div.mapInfoWindow').children('p.last').removeClass('last');
          // fblog('testopen');
          // fblog('infoWindow height = ' + shared.geo.map.getInfoWindow().getContentContainers()[0].parentNode.style.height);
          
          // mapInfoWindowUpdate(thisItem, {inject: true});
          
          //fblog(thisItem.address);
          if (shared.geo.infoWindowCurrentItem && shared.geo.infoWindowCurrentItem.map && shared.geo.infoWindowCurrentItem.map.overlay){
            shared.geo.map.removeOverlay(shared.geo.infoWindowCurrentItem.map.overlay);
          }
          if (thisItem.map.overlay){
            shared.geo.map.addOverlay(thisItem.map.overlay);
          }
          else {
            if (params.getDirections){
              mapGetDirections(thisItem);
            }
          }
          shared.geo.infoWindowCurrent = thisIndex;
          shared.geo.infoWindowCurrentItem = thisItem;
        };
      }(item, i));
      
      google.maps.Event.addListener(marker, "infowindowclose", function(thisItem, thisIndex){
        return function(){
          // fblog('testclose');
          // $('div.mapInfoWindow').children('p:last').addClass('last');
          
          //fblog(thisItem.address);
          if (thisItem.map.overlay){
            setTimeout(function(innerItem, innerIndex){
              return function(){
                var hidden = shared.geo.map.getInfoWindow().isHidden();
                //fblog(['isHidden = ', hidden, ', index = ', innerIndex, ', infoWindowCurrent = ', shared.geo.infoWindowCurrent].join(''));
                if (hidden || (!hidden && innerIndex != shared.geo.infoWindowCurrent)){
                  shared.geo.map.removeOverlay(innerItem.map.overlay);
                }
              };
            }(thisItem, thisIndex), 1000);
          }
        };
      }(item, i));
      
      if (!params.target){ 
        $('#mapContainerStatic').html([
          '<img src="',
          'http://maps.google.com/maps/api/staticmap?size=640x320&maptype=roadmap&markers=size:mid|color:red|', item.lat, ',', item.lng, '&zoom=12&mobile=true&sensor=false',
          '" style="width: 100%;"/>'
        ].join(''));
      }
    }
    
    // pan to this location
    mapRecenter(params.zoom);
    // map.checkResize();
    // // map.setCenter(loc, params.zoom || 10); //14);
    
    // map.setCenter(new google.maps.LatLng(shared.geo.geocode[1], shared.geo.geocode[0]), 10);
    // if (target.is(':visible')){
      // shared.geo.mapVisible = true;
    // }
    
    //map.panTo(loc);
    marker.openInfoWindowHtml(infoWindow); //infoHtml);
    
    // make sure infoWindow is closed whenever map is clicked (even on an overlay)
    google.maps.Event.addListener(map, "click", function(target, latLng, arg){
      // if clicked on infoWindow, don't hide it
      if (!target || !Boolean(target.getTabs)){
        shared.geo.map.closeInfoWindow();
      }
    });
    
    // add colored states overlay
    // if (!shared.geo.states){
      // shared.geo.states = new google.maps.GeoXml("http://dev.biomedphysics.com/common/assets/ci/kml/states.kmz");
      // map.addOverlay(shared.geo.states);
    // }
    
  }
  
  
  
  function mapRecenter(zoom, loc){
    if (shared.geo.map){
      if (
        !shared.geo.currentLocations
        ||
        !shared.geo.currentLocations.length
        ||
        shared.geo.infoWindowCurrent === undefined
      ){
        if (!shared.geo.geocode){
          shared.geo.geocode = [shared.geo.locations[0].lng, shared.geo.locations[0].lat, 0];
        }
      }
      else {
        shared.geo.geocode = [shared.geo.currentLocations[shared.geo.infoWindowCurrent].lng, shared.geo.currentLocations[shared.geo.infoWindowCurrent].lat, 0];
      }
      
      shared.geo.map.checkResize();
      shared.geo.map.setCenter(loc || new google.maps.LatLng(shared.geo.geocode[1], shared.geo.geocode[0]), zoom || shared.geo.map.getZoom() || 12);
      
      var hidden = shared.geo.map.getInfoWindow().isHidden();
      
      if (!hidden){
        shared.geo.map.updateInfoWindow();
      }
    }
  }
  
  
  
  
  function mapInfoWindowUpdate(item, params){
  
    if (!params){
      params = {};
    }
    if (params.linkDirections === undefined){
      params.linkDirections = true;
    }
    
    var target = params.target || shared.geo.directions.target;
    
    var link = [];
    
    var from = false;
    
    var html = [
      '<p>',
      params.suppressNearest ? '' : '<span class="smallCaps light">Nearest Matching Address:</span><br />',
      '<b><span class="big">', item.name, '</span></b><br />',
      item.address.replace(', USA', ''), '</p>',
      
      (
        item.drivingTime ? 
          [
            '<p class="small"><span class="smallCaps light">Driving Time:</span> ',
            item.drivingTime,
            ' <span class="light">(',
            item.drivingDistance,
            ' miles)</span>',
            (item.hasTolls ? '<br /><span class="highlightWarning smallCaps">Route may have tolls</span>' : ''),
            '</p>'
          ].join('')
          :
          ''
      ),
      
      (item.description ? ['<div style="border-top: 1px solid #ddd; margin-top: 1em">', item.description, '</div>'].join('') : '')
    ];
    
    if (params.linkDirections){
      // html.push (
        // '<p class="small"><a href="http://maps.google.com/maps?f=d&hl=en&saddr=',
        // as.urlEncode(params.suppressSearchName ? item.address : [item.name, ', ', item.address].join('')), '+(', as.urlEncode(item.name), ')',
        // ((item.lat == target.lat && item.lng == target.lng) ? '' : ['&daddr=', as.urlEncode(target.address)].join('')),
        // '" target="_blank">Get directions from this location</a></p>'
      // );
      html.push (
        '<p class="last"><span class="small"><a href="'
      );
      
      
      
      link.push('http://maps.google.com/maps?f=d&hl=en');
      
      // this is a starting address
      if (target && !(target.lat == item.lat && target.lng == item.lng)){
        from = true;
        link.push(
          '&saddr=',
          item.lat, ',', item.lng, '+(', as.urlEncode(item.name), ')',
          ((item.lat == target.lat && item.lng == target.lng) ? '' : ['&daddr=', target.lat, ',', target.lng, '+(', target.name, ')'].join(''))
        );
      }
      
      // this is a destination address
      else {
        var addressStr, location_name;
          
        if (clients.active && clients.active.location){
          var client = clients.active;
          var addressArr = [];
          
          location_name = client.location_name;
          
          aa.pushIf(addressArr, client.address.address1);
          aa.pushIf(addressArr, client.address.address2);
          aa.pushIf(addressArr, client.address.address3);
          aa.pushIf(addressArr, client.address.city);
          aa.pushIf(addressArr, client.address.state);
          
          var addressStr = addressArr.join(', ');
          
          if (client.address.zip){
            addressStr = [addressStr, client.address.zip].join(' ');
          }
        }
        else {
          location_name = item.name;
          addressStr = item.address;
        }
          
        link.push(
          '&daddr=',
          // params.suppressSearchName ? as.urlEncode([item.address, ' (', item.name, ')'].join('')) : as.urlEncode([item.name, ', ', item.address].join(''))
          as.urlEncode([addressStr, ' (', location_name, ')'].join('')) // : as.urlEncode([client.location_name, ', ', addressStr].join(''))
        );
      }
      
      
      
      html = html.concat(link);
      
      html.push('" target="_blank">Get directions ', (from ? 'from' : 'to'), ' this location</a></span></p>');
    }
    
    html = html.join('');

    
    if (!item.map){
      item.map = {};
    }
    
    if (!item.map.infoWindow){
      item.map.infoWindow = $(['<div class="mapInfoWindow">', html, '</div>'].join('')).appendTo($('#hidden_container'));
    }
    else {
    // if (params.fake){
      // fblog('fake');
      // item.map.infoWindow.get(0).innerHTML = '<p>test<br>test<br>test</p>';
    // }
    // else if (params.inject) {
      item.map.infoWindow.get(0).innerHTML = html;
    // }
    }
    
    $('#linkAddressDirections').remove();
    $('div.valueContent[field=address]').after(['<div class="dontprint" id="linkAddressDirections"><a href="', link.join(''), '" target="_blank">Get directions to this location</a></div>'].join(''));
    
    
    return;
  }

  
  
  
  
  
  function autocompleteKeydown(e){
    // prevent form submission by not passing enter key to window
    if (e.which == 13){
      return false;
    }
  }  
  

  
  


  
  
  
  function formBuild(formDef, injectTarget, onVisible, dontFocus){
    if (formDef.fields){
      var
        fieldIndex,
        contentArray = [],
        i;

      injectTarget = $(['#', injectTarget].join(''));

      // beginning of form
      contentArray.push('<div class="flatContainer">');
      
      if (formDef.fields.length){
        if (formDef.noFormElement){
          contentArray.push('<div');
        }
        else {
          contentArray.push('<form method="post" name="', formDef.id, '"');
        }
         
        contentArray.push(' id="', formDef.id, '" class="flat', (formDef.noGap ? ' noGap' : ''), (formDef.category || formDef.title ? ' widthAuto' : ''), '">');
      }

      // only use title and body frames if desired
      if (!formDef.noFrame){
      
        if (formDef.category || formDef.title){
          contentArray.push('<div class="title title-blue"><p>', (formDef.category ? ['<span class="small">', formDef.category, '</span><br>'].join('') : ''), '<b>', formDef.title, '</b></p>', (formDef.headerNav ? ['<div class="nav">', formDef.headerNav, '</div>'].join('') : ''), '</div>');
          contentArray.push('<div id="', formDef.id, 'Body" class="body bodyWithTitle">');
        }
        else {
          contentArray.push('<div id="', formDef.id, 'Body" class="body" style="padding-right: 1.5em">');
        }
      }
      
      if (formDef.fields.length){

        //var prefills = [];
        formDef.lookups = [];
        formDef.autoClones = [];

        if (formDef.messagePreamble){
          contentArray.push('<div id="', formDef.prefix, 'messagePreamble" class="callout calloutWide clear">', formDef.messagePreamble, '</div>');
        }


        
        // generate form based on fields array
        for (fieldIndex in formDef.fields){
          contentArray.push(formCreateField(formDef, fieldIndex));
        }


        
        if (formDef.messageInvalid){
          contentArray.push('<div class="clearfix"></div><div id="', formDef.prefix, 'messageInvalid" class="callout calloutWide calloutStatic" style="display: none">', formDef.messageInvalid, '</div>');
        }



        // end of form
        contentArray.push('<div class="clearfix"></div>');

        if (formDef.static || formDef.submitType == 'standard'){
          contentArray.push('<div class="row button-submit-container">');
        }
          
        if (formDef.submitType == 'standard') {
          contentArray.push(
            '<input type="submit" id="',
            formDef.prefix,
            'buttonPost" class="button-submit" ',
            (
              formDef.dontValidate ? 
                ['value="', formDef.submitText, '"'].join('')
                :
                ['value="', formDef.submitInvalidText, '"'].join('')
            ),
            ' />',
            //onClick=\"document." + formDef.id + "." + formDef.prefix + "buttonPost.style.display = 'none'; document.getElementById('" + formDef.prefix + "buttonPostReplace').style.display = 'block';\"' />\n" +
            (
              formDef.dontValidate ?
              ''
              :
              ['<input type="hidden" id="', formDef.prefix, 'valid" name="valid" value="false" />'].join('')
            ),
            '<div id="',
            formDef.prefix,
            'buttonPostReplace" style="display: none"><img src="/common/assets/images/loading_16x16.gif" align="absmiddle"/>&nbsp;',
            formDef.submitInProgressText,
            '</div>'
          );
            
        }
        else if (!formDef.submitType){
          if (formDef.staticLink){
            contentArray.push('<a href="', formDef.staticLink, '">Click here to continue</a>');
          }
          else {
            contentArray.push('<a href="home">Return to home page</a>');
          }
        }
          
        if (formDef.static || formDef.submitType == 'standard'){
          contentArray.push('</div>');
        }

      } // end if formDef.fields.length
      
      
      
      if (formDef.addedInjectTarget && formDef.addedInjectTarget != 'none'){
        contentArray.push('</div><div id="', formDef.addedInjectTarget, '" class="body', /*(formDef.noGapTop && formDef.noGapTop === true ? '' : ' gapTop'),*/ ' hidden"></div>');
      }

      if (!formDef.noFrame){
        // this breaks formbilling-inject on Client Search page in IE
        // contentArray.push("</div>");
      }

      if (formDef.fields.length){
        if (formDef.noFormElement){
          contentArray.push('</div>');
        }
        else {
          contentArray.push('</form>');
        }
      }
      
      // insert form into document
      injectTarget.html(contentArray.join(''));
      
      if (onVisible){
        onVisible();
      }
      
      // round corners
      // $('div.flatContainer div.title').css('border-top-width', 0).css('border-left-width', 0).css('border-right-width', 0).nifty('top');
      // $('div.flatContainer div.body').css('border-width', 0).nifty('bottom');
      // $('div.flatContainer').nifty();

      
      // attach autocomplete to applicable fields
      for (i in formDef.lookups){
        if (formDef.lookups.hasOwnProperty(i)){
          
          //fblog([shared.clients.length, ' clients'].join(''));
          
          var
            lookupElement = $(['#', i].join('')),
            currentVal = lookupElement.val(),
            lookupForm = lookupElement.parents('form');
            fieldDef = aa.findObjectByKey(formDef.fields, i.replace(formDef.prefix, ''), 'name')[0];
            //lookupFormId = lookupForm.attr('id');
          
          
          lookupElement
            .addClass([
              'iconrightgap',
              ((currentVal && !aa.existsIn(['[ Not Found ]', '[ Unknown ]'], currentVal)) ? ' icon-ok' : '') // add ok icon if current location is valid
            ].join(''))
            // setup autocomplete functionality
            //.autocomplete('ajaxSearch/' + formDef.lookups[i], {
            .autocomplete(shared.clients, { //'ajaxSearch/client', {
              delay: 500,
              field: i,
              matchContains: true,
              //autoFill: true,
              //cacheLength: 0,
              scroll: true,
              scrollHeight: 400,
              mustMatch: false,
              max: 1000,
              inputInvalidClass: 'ac_noresults',
              loadingClass: 'icon-loading',
              //selectedClass: 'icon-ok',
              persistInput: fieldDef.lookupPersistInput ? true : false,  // allow input to remain unchanged if defined in form
              preparse: clients.searchPreparse,
              // {
                // '/': '',
                // '\\-': '',
                // ',': '',
                // '\\.': '',
                // '\\(': ' ',
                // '\\)': ' ',
                // '\\[': ' ',
                // '\\]': ' ',
                // "'": '',
                // "\\'": '',
                // '"': '',
                // '  ': ' '
              // },
              minChars: fieldDef.lookupRestrict ? 3 : 2,
              trim: fieldDef.lookupRestrict ? true : false,
              //loadingProc: autocompleteLoading,
              highlight: autocompleteHighlight,
              formatItem: autocompleteFormatItem,
              formatMatch: autocompleteFormatMatch,
              formatResult: autocompleteFormatResult
            })
            
            // callback when item selected
            .result(autocompleteResult)
            
            .keydown(autocompleteKeydown);
            
          // update each dropdown arrow icon's position relative to each input element
          lookupElement.after('<div class="ac_loading hidden" />');
          
        }
      }
      
      
      
      
      // // call any ajaxes defined for individual items
      // for (i in formDef.ajaxes){
        // config = formDef.ajaxes[i];
        // // for (var j in config){
          // //fblog(formDef.id + j);
          // // $.post({
            // // port: formDef.id + 
          // // });
        // // }
      // }
      
      // setup custom button click handlers
      for (i in formDef.buttons){
        formSetupButton(formDef, formDef.buttons[i]);
      }
      
      
      
      // // iterate through textareas, adding special behavior (autogrow)
      // for (i = 0; i < formDef.textareas.length; i++){
        // fblog(formDef.textareas[i]);
        // $(['#', formDef.textareas[i]].join('')).growfield({
          // offset: 2,
          // animate: false
        // });
      // }
      
      
      
      
      
      for (i in formDef.autoClones){
        formSetupAutoClone(formDef, formDef.autoClones[i]);
      }
      
      
      
      
      
      // iterate through dropdowns, adding special formatting
      for (i = 0; i < formDef.dropdowns.length; i++){
        formSetupDropdown(formDef, formDef.dropdowns[i]);
      }
      
      
      
      // add custom callback (if defined) for each select control
      for (i = 0; i < formDef.selects.length; i++){
      
        // fblog('select' + i);
      
        var fieldDef = aa.findObjectByKey(formDef.fields, formDef.selects[i].id.replace(formDef.prefix, ''), 'name');
        
        if (fieldDef && fieldDef.length){
          fieldDef = fieldDef[0];
        
          if (fieldDef.selectCallback && typeof fieldDef.selectCallback == 'function'){
            $(['#', formDef.selects[i].id].join('')).change(fieldDef.selectCallback);
          }
        }
      }
      

      
      // setup form for validation
      if (!formDef.dontValidate){
        // copy to new array to iterate through, since source array will be modified during this process
        var invalids = $.map(formDef.invalid, function(i){
          return i;
        });
        
        // validate initially after form creation (in case default values passed in)
        for (i in invalids){
          if (invalids.hasOwnProperty(i)){
            var element = $(['#', invalids[i]].join(''));
            if (element.length){
              formValidateItem(element, (formDef.invalidatePrefills ? false : true));
            }
          }
        }
      }
      
      
      // don't intercept submits in IE6 or below
      // some older versions of IE6 seem to hang indefinitely at this step...
      if(!($.browser.msie && $.browser.version <= 6)){
        // setup submit handler
        $(['#', formDef.id].join('')).submit(function(){
          // setTimeout(function(){
            if (shared.inputFocused){
              $(['#', shared.inputFocused].join('')).blur();
            }
            //if (formValidate(form, true)){
               $(['#', formDef.prefix, 'buttonPostReplace'].join('')).show(); //css('display', 'block');
               $(['#', formDef.prefix, 'buttonPost'].join('')).hide(); //css('display', 'none');
              //$('#' + formDef.id + ' .inputText').attr('disabled','disabled');
              // return true;
            //}
            //return false;
          // }, 100);
          
          return true;
        });
      }
      
      
      // // setup typeWatch plugin
      // $(['#', formDef.id, ' input.inputText, #', formDef.id, ' textarea.inputTextarea'].join('')).typeWatch({
      // // $(['#li_password'].join('')).typeWatch({
        // wait: 250,
        // captureLength: 0,
        // callback: function(){
          // fblog('typeWatch ' + this.el.id);
          // formValidateItem($(['#', this.el.id].join('')));
        // }
      // });
      

      $(['#', formDef.id, ' input.inputText, #', formDef.id, ' textarea.inputTextarea'].join('')).keyup(function(){
        if (!validating){
          formValidateItem($(this));
        }
      });
      
      
      formSetupEventHandlers(formDef);
      

      // focus applicable form field
      if (!dontFocus){
        if (formDef.invalidatePrefills && formDef.invalid.length > 0){
          // focus first invalid field
          //fblog('forceFocusing invalid[0] ' + $('#' + formDef.invalid[0]).length);
          forceFocus($(['#', formDef.invalid[0]].join('')));
          formValidateItem($(['#', formDef.invalid[0]].join('')));
        }
        else {
          var doFocus = true;
          if (formDef.focusCondition !== undefined){
            if (typeof formDef.focusCondition == 'function'){
              if (!formDef.focusCondition()){
                doFocus = false;
              }
            }
            else if (!formDef.focusCondition){
              doFocus = false;
            }
          }
          
          if (doFocus){
            // if focus not defined, try to set focus to first text input field in form
            i = 0;
            if (!formDef.focus && formDef.fields){
              while (formDef.fields[i]){
                if (!formDef.fields[i].type){ // field is a text input - all other fields have a type defined
                  formDef.focus = formDef.fields[i].name;
                  break;
                }
                
                i++;
              }
            }
            
            // only try to focus if a valid focus is set
            if (formDef.focus){
              // focus passed element
              //fblog('forceFocusing ' + $('#' + formDef.prefix + formDef.focus).length);
              forceFocus($(['#', formDef.prefix, formDef.focus].join('')));
            }
          }
        }
      }
      
    }
    
  }
  
  
  
  
  
  function formSetupButton(formDef, fieldDef){
    var button;

    if (typeof fieldDef == 'object'){
      button = $(['#', fieldDef.id].join(''));
      
      button.click(function(buttonDef){
        return function(e){
          var element = $(this);
          $(e.currentTarget).children('.body').addClass('bodyClick');
          
          var possibleActionFunction;
          if (typeof buttonDef.action == 'string'){
            try {
              possibleActionFunction = eval(['(', buttonDef.action, ')'].join(''));
              if (typeof possibleActionFunction == 'function'){
                buttonDef.action = possibleActionFunction;
              }
            }
            catch (exception){
              // fblog(['action for button "', buttonDef.name, '" is not a function'].join(''));
            }
          }
          
          if (typeof buttonDef.action == 'function'){
            
            // var fieldDef = aa.findObjectByKey(formDef.fields, as.replaceAll(element.attr('id'), formDef.prefix, ''), 'name');
            // if (fieldDef.length){
              // fieldDef = fieldDef[0];
            // }
            // else {
              // //fblog(['button.mousedown: unable to find fieldDef for ', element.attr('id')].join(''));
            // }
           
            var targetId, target;
            
            if (formDef){
              targetId = [formDef.prefix, buttonDef.target].join('');
              target = $(['#', targetId].join(''));
            }

            buttonDef.action({
              element: element,
              formDef: formDef,
              fieldDef: buttonDef,
              target: target,
              event: e
            });
          }
          else {
            // fblog(['action "', buttonDef.action, '" for button ', i, ' is a string'].join(''));
            
            switch (buttonDef.action){
              case 'itemEditSave':
                itemEditHide();
                break;
                
              case 'itemEditDiscard':
                itemEditHide({noSave: true});
                break;
                
              case 'itemEditDelete':
                itemEditHide({deleteItem: true});
                break;
                
              case 'taskEditSave':
                scheduleEditHide({target: buttonDef.target});
                break;
                
              case 'taskEditDiscard':
                scheduleEditHide({target: buttonDef.target, noSave: true});
                break;
                
              case 'taskEditDelete':
                scheduleEditHide({target: buttonDef.target, deleteItem: true});
                break;
                
              case 'billingViewSelected':
                // billingInvoiceView({target: billing.selected});
                billingInvoiceView({selected: true});
                break;
                
            }

            
            
          }
          
          // prevent this control from taking focus
          if (e.preventDefault) { 
            e.preventDefault();
          }
          
        };
      }(fieldDef));
      
      button.mouseup(function(){
        return function(e){
          if (e.button < 2) {
            $(this).children('.body').removeClass('bodyClick');
            // e.preventDefault();
          }
          
        };
      }());

      // button.click(function(){
        // return function(e){
          // if (e.button < 2) {
            // e.preventDefault();
          // }
          
        // };
      // }());

    }


    else if (typeof fieldDef == 'string'){
      button = $(['#', fieldDef].join(''));
      switch (button.attr('action')) {
      
        case 'filterToggle':
          // default to ON state
          aa.pushUnique(formDef.toggles, button.attr('target'));
          button.addClass('buttonToggleOn').children('.check').html('+'); //prepend("<span class='check'>+</span>");
          button.click(function(){
            var
              thisEl = $(this),
              thisTarget = thisEl.attr('target');
            //$('#' + $(e.target).parents('label').attr('for')).autocomplete();
            if (aa.existsIn(formDef.toggles, thisTarget)) {
              aa.removeByValue(formDef.toggles, thisTarget);
              thisEl.removeClass('buttonToggleOn').children('.check').html('-'); //remove();
            }
            else {
              aa.pushUnique(formDef.toggles, thisTarget);
              thisEl.addClass('buttonToggleOn').children('.check').html('+'); //prepend("<span class='check'>+</span>");
            }
          });
          break;

        case 'scheduleToggleCondensed':
          button.click(function(e){
            var element = $(this);
            var formDef = forms[element.parents('form').attr('id')];
            var fieldDef = aa.findObjectByKey(formDef.fields, as.replaceAll(element.attr('id'), formDef.prefix, ''), 'name');
            if (fieldDef.length){
              fieldDef = fieldDef[0];
            }
            else {
              fblog('button.mousedown: unable to find fieldDef');
            }
           
            var targetId = [formDef.prefix, fieldDef.target].join('');
            var target = $(['#', targetId].join(''));

            scheduleToggleCondensed({
              element: element,
              formDef: formDef,
              fieldDef: fieldDef,
              target: target,
              event: e
            });
          });
          break;
                    
        
        case 'dateNext':
          button.click(function(e){
            var thisEl = $(this);
            
            thisEl.contents('.body').removeClass('bodyClick');
            scheduleEditHide({noSave: true});
            if (shared.activeInterface == 'billing'){
              billingGet({
                start: dateString(dpExactDate(billing.start).addMonths(1)),
                target: forms[thisEl.parents('form').attr('id')].addedInjectTarget
              });
            }
          });
          break;
          
        case 'datePrevious':
          button.click(function(e){
            var thisEl = $(this);
            
            thisEl.contents('.body').removeClass('bodyClick');
            scheduleEditHide({noSave: true});
            if (shared.activeInterface == 'billing'){
              billingGet({
                start: dateString(dpExactDate(billing.start).addMonths(-1)),
                target: forms[thisEl.parents('form').attr('id')].addedInjectTarget
              });
            }
          });
          break;
          
        // case 'taskEditSave':
          // button.click(function(){
            // scheduleEditHide();
          // });
          // break;
          
        // case 'taskEditDiscard':
          // button.click(function(){
            // scheduleEditHide({noSave: true});
          // });
          // break;
          
        // case 'taskEditDelete':
          // button.click(function(){
            // scheduleEditHide({deleteItem: true});
          // });
          // break;
          
        case 'billingSelectAll':
          button.click(function(){
            billingSelectAll();
          });
          break;

        case 'billingSelectNone':
          button.click(function(){
            billingSelectNone();
          });
          break;
          
        case 'billingViewSelected':
          button.click(function(){
            // billingInvoiceView({target: billing.selected});
            billingInvoiceView({selected: true});
          });
          break;

      }
    }
    
    
    
    
    
    // setup upload control, including associated file list
    if (button.length && fieldDef.upload){
      buttonSetupUpload(button, fieldDef, formDef);
    }
    
    
    
    
    
    if (button.length && fieldDef.attach && !fieldDef.isClone){  
      button.parent().css('padding-top', '2.01em');
      
      // var attachTarget = $(['#', formDef.prefix, formDef.buttons[i].attach].join(''));
      // if (attachTarget.length){
      
        // // attachTarget.width(attachTarget.width() - button.outerWidth() - 2);
        // button
          // .insertBefore(attachTarget)
          // .removeClass('buttonFramedContainerAdjacent')
          // .css({
            // //'font-size': '.77em',
            // //'position': 'absolute',
            // 'float': 'right',
            // margin: 0
            // //top: '1.325em',
            // //right: 0
          // });
        
        // // use closure to capture current values of target and button
        // setTimeout(function(target, b){
          // return function(){
            // target.width(target.width() - b.outerWidth() - 2);
          // };
        // }(attachTarget, button), 100);
      // }
    }
  }
  
  
  
  
  
  
  function formSetupAutoClone(formDef, fieldDef){
    var thisEl = $(['#', formDef.prefix, fieldDef.name].join(''));
    
    //thisEl.attr('cloneType', thisItem.cloneType = thisItem.name);
    


    
    inputCloneOnVal(thisEl, fieldDef, true, function(el, def, cloneSiblings){
      inputClone({
        el: el,
        def: def,
        cloneSiblings: cloneSiblings
      }); 
    });
    
    // trigger autoclone if there is a prefill value
    if (thisEl.val()){
      setTimeout(function(){
        thisEl.trigger('lookup');
      }, 250);
    }

    

    
    // thisEl.change(function(){
      // $('body').stopTime(timer);
      // $('body').oneTime(2000, timer, function(scope){
        // if ($.trim($(scope).val())){
          // $(scope).parent().after('<div class="clone">clone</div>');
        // }
        // else {
          // $(scope).parent().siblings('.clone').remove();
        // }
      // });
    // });
  }
  
  
  
  
  
  function formSetupDropdown(formDef, dropdownDef, fieldDef){
  
    if (!fieldDef){
      fieldDef = aa.findObjectByKey(formDef.fields, dropdownDef.id.replace(formDef.prefix, ''), 'name')[0];
    }
    
    var inputClass = ['inputText'];
    var readOnly = false;
    
    if (fieldDef.required){
      inputClass.push('required');
    }
    
    if (fieldDef.help){
      inputClass.push('help');
    }
    
    if (fieldDef.width){
      inputClass.push(['width-', fieldDef['width']].join(''));
    }
    
    if (
      fieldDef.editPermissions
      &&
      fieldDef.editPermissions.length
      &&
      !aa.existsIn(fieldDef.editPermissions, permissions)
    ){
      // inputClass.push('disabled');
      readOnly = true;
    }
    
    if (
      fieldDef.displayPermissions
      &&
      fieldDef.displayPermissions.length
      &&
      !aa.existsIn(fieldDef.displayPermissions, permissions)
    ){
      inputClass.push('hidden');
    }
    
    if (
      fieldDef.enableCondition
      &&
      typeof fieldDef.enableCondition == 'function'
      &&
      !fieldDef.enableCondition(formDef, fieldDef)
    ){
      readOnly = true;
    }
    
    // MultiSelect plugin
    schedule.multiselect.options = dropdownDef.config = $(['#', dropdownDef.id].join('')).multiSelect({
      selectAll: false,
      defaultValue: fieldDef.defaultValue,
      noneSelected: '- not applicable -',
      noneSelectedClass: 'multiSelectEmpty',
      oneOrMoreSelected: '*',
      inputClass: inputClass.join(' '),
      optionsClass: '',
      readOnly: fieldDef.readOnly || readOnly,
      readOnlyClass: 'disabled',
      selectExclusive: fieldDef.selectExclusive,
      selectExclusiveAll: fieldDef.selectExclusiveAll,
      separator: fieldDef.separator
    }, fieldDef.selectCallback);
  
    // mcDropdown plugin
    // $('#' + formDef.dropdowns[i]).mcDropdown('#' + formDef.dropdowns[i] + '_dropdownValues');
    // $('#' + formDef.dropdowns[i]).mcDropdown().setValue(3);
  }
  
  
  
  
  // for inputting dynamic lists of items
  // generate a copy of the passed input element immediately after it if its value is non-null
  function inputCloneOnVal(originalEl, originalDef, cloneSiblings, callback){
    // var isHistory = originalEl[0].form.id == 'clientScheduleEditForm';
    // var activeEdit = (isHistory ? clients.scheduleEdit : schedule.activeEdit);
    // var originalUnique = originalDef.unique;
    
    // if (!originalUnique){
      // originalUnique = new Date().valueOf();
      // originalDef.unique = originalUnique;
      // originalEl.attr('unique', originalUnique);
      // activeEdit.data.taskItems[0].unique = originalUnique;
      // if (!activeEdit.data.taskItems[0].summary){
        // activeEdit.data.taskItems[0].isNew = true;
      // }
    // }
    
    

    // var formDef = forms[originalEl[0].form.id];
    // if (!formDef.taskItems){
      // formDef.taskItems = [
        // {
          // summary: originalDef
        // }
      // ];
    // }
    
    
    // use autocomplete plugin to detect data entry on the fly
    originalEl.autocomplete(null, { //'ajaxSearch/client', {
      delay: 500,
      minChars: 2,
      trim: true,
      
      // call loadingProc (and return FALSE) before autocomplete functionality kicks in to avoid unnecessary processing
      loadingProc: function(el, isLoading){
        if (isLoading){
        
          inputClone({
            el: originalEl, // el
            def: originalDef,
            cloneSiblings: cloneSiblings
          });
          
          // test
          
          // el.parent().removeClass('clone');
          // var clone = el.parent().siblings('.clone');
          // if (!clone.length){
            // var insertTarget = (cloneSiblings ? el.parent().nextAll('div.clearfix:first') : el.parent());
            // var unique = new Date().valueOf();
            // // var newInput = $(['<input id="', el.attr('id'), unique, '" name="', el.attr('name'), '" unique="', unique, '" class="inputText width-full sentenceCase" type="text" value=""/>'].join('')).insertAfter(insertTarget).wrap('<div class="item clone" style="margin-top: .5em;" />');
            // var newInput = el.clone();
            // var formDef = forms[el[0].form.id];
            // var fieldDefIndex = aa.indexOfObjectByKey(formDef.fields, originalUnique, 'unique');
            // var fieldDef, newFieldDef;
            // if (fieldDefIndex.length){
              // fieldDefIndex = fieldDefIndex[0];
              // fieldDef = formDef.fields[fieldDefIndex];
              // newFieldDef = _.clone(fieldDef);
              
              // var fieldDefIndexEOL = aa.indexOfObjectByKey(formDef.fields, 'eol', 'type', fieldDefIndex);
              // if (fieldDefIndexEOL.length){
                // fieldDefIndexEOL = fieldDefIndexEOL[0];
                // formDef.fields.splice(fieldDefIndexEOL + 1, 0, newFieldDef, _.clone(formDef.fields[fieldDefIndexEOL]));
              // }
              // else {
                // fieldDefIndexEOL = formDef.fields.length - 1;
                // formDef.fields.push(newFieldDef, _.clone(formDef.fields[fieldDefIndexEOL]));
              // }
              
              
              
              // // newFieldDef.name = [newFieldDef.cloneType, unique].join('');
              // newFieldDef.unique = unique;
              // newFieldDef.isClone = true;
              // if (!newFieldDef.displayCondition){
                // newFieldDef.displayCondition = function(){
                  // return scheduleEditNotInternal(isHistory);
                // };
              // }
              
              // activeEdit.data.taskItems.push({
                // unique: unique,
                // isNew: true
              // });
            // }
            // else {
              // fblog('inputCloneOnVal(): no fieldDef');
            // }
            
            // newInput
              // .attr('id', [formDef.prefix, newInput.attr('name'), unique].join(''))
              // .attr('unique', unique)
              // // .attr('isClone', 'true')
              // .val('')
              // .removeClass('focused')
              // .insertAfter(insertTarget)
              // .wrap('<div class="item clone" style="margin-top: .5em;" />')
              // .parent()
              // .after('<div class="clearfix" />');
              
            // // replicate sibling elements
            // if (fieldDefIndex && fieldDefIndexEOL && cloneSiblings){
            
              // var newSibling, originalFieldDef;
              // //var newFieldDefs = _.clone(formDef.fields.slice(fieldDefIndex, fieldDefIndexEOL));
              // for (var i = fieldDefIndexEOL - 1; i > fieldDefIndex; i--){
                // originalFieldDef = formDef.fields[i];
                // newFieldDef = _.clone(originalFieldDef);
                // if (!originalFieldDef.unique){
                  // originalFieldDef.unique = originalUnique;
                  // $(['#', formDef.prefix, originalFieldDef.name].join('')).attr('unique', originalUnique);
                  // if (originalFieldDef.upload){
                    // $(['#', formDef.prefix, 'uploadStatus'].join('')).attr('unique', originalUnique);
                  // }
                  // // $(['#', formDef.prefix, originalFieldDef.name, originalFieldDef.unique].join('')).attr('unique', originalUnique);
                  // // $(['#', formDef.id, ' [name=', originalFieldDef.name, '][unique=', originalFieldDef.unique, ']'].join('')).attr('unique', originalUnique);
                // }
                // newFieldDef.unique = unique;
                // delete newFieldDef.label;
                // delete newFieldDef.helpIcon;
                // newFieldDef.isClone = true;
                // if (!newFieldDef.displayCondition){
                  // newFieldDef.displayCondition = function(){
                    // return scheduleEditNotInternal(isHistory);
                  // };
                // }
                // formDef.fields.splice(fieldDefIndexEOL + 2, 0, newFieldDef);
                
                // newSibling = $(formCreateField(formDef, newFieldDef));
                // newSibling.children('table').remove();
                // // newSibling.attr('isClone', 'true');
                // var newSiblingId = [formDef.prefix, newFieldDef.name, newFieldDef.unique].join('');
                
                // if (newFieldDef.type != 'button') {
                  // newSibling.children().attr('id', newSiblingId);
                  // //newSibling.wrap('<div class="item" />');
                  // //newSibling = newSibling.parent();
                  // inputSetupHover(newSibling, formDef);
                // }
                
                // newSibling.insertAfter(newInput.parent()).css('margin-top', '.5em');
                
                // if (newFieldDef.type == 'button'){
                  // formSetupButton(formDef, newFieldDef);
                // }
                // if (newFieldDef.type == 'dropdown'){
                  // formSetupDropdown(formDef, aa.findObjectByKey(formDef.dropdowns, newSiblingId, 'id', null, true), newFieldDef);
                // }
                
                
              // }
              
              // // var newSiblings = el.parent().nextUntil('div.clearfix').clone(true);
              // // newSiblings.children('table').remove();
              // // newSiblings.children('input, .button').attr('unique', unique).each(function(){
                // // var curr = $(this);
                // // curr.attr('id', [formDef.prefix, curr.attr('name'), unique].join(''));
                // // if (curr.hasClass('button')){
                  // // curr.css('margin-top', '');
                // // }
              // // });
              
              // // newSiblings.insertAfter(newInput.parent()).css('margin-top', '.5em');
              
              
            // }
            // else {
            // }
            
            // //var formEl = el.parents('form');
            // //var formId = formEl.attr('id');
            // //formDef = forms[formId];
            // //fieldDef = aa.findObjectByKey(formDef.fields, 'name');
            
            // inputSetupHover(newInput.parent(), formDef);
            // inputCloneOnVal(newInput, newFieldDef, cloneSiblings);
          // }
          // else {
            // // el.siblings('.clone').remove();
          // }
          
          // scheduleOnTaskEditChange({isHistory: isHistory});
        }
        
        return false; // stop any further autocomplete processing
      }
    });
    
    // if (callback && typeof callback == 'function'){
      // callback(el, def, cloneSiblings);
    // }
  }
  
  
  
  
  
  
  
  
  function inputClone(config){
    var el = config.el;
    var def = config.def;
    var cloneSiblings = config.cloneSiblings;
    var isHistory = el[0].form.id == 'clientScheduleEditForm';
    var activeEdit = (isHistory ? clients.scheduleEdit : schedule.activeEdit);
    
    // if passed item isn't already assigned a unique ID
    // this should never happen when taskItems are correctly supported
    if (!def.unique){
      def.unique = new Date().valueOf();
      el.attr('unique', def.unique);
      activeEdit.data.taskItems[0].unique = def.unique;
      if (!activeEdit.data.taskItems[0].summary){
        activeEdit.data.taskItems[0].isNew = true;
      }
    }
    
    var originalUnique = def.unique;
    
    
    
    el.parent().removeClass('clone');
    var clone = el.parent().siblings('.clone');
    if (!clone.length){
      var insertTarget = (cloneSiblings ? el.parent().nextAll('div.clearfix:first') : el.parent());
      var unique = new Date().valueOf();
      // var newInput = $(['<input id="', el.attr('id'), unique, '" name="', el.attr('name'), '" unique="', unique, '" class="inputText width-full sentenceCase" type="text" value=""/>'].join('')).insertAfter(insertTarget).wrap('<div class="item clone" style="margin-top: .5em;" />');
      var newInput = el.clone();
      var formDef = forms[el[0].form.id];
      var fieldDefIndex = aa.indexOfObjectByKey(formDef.fields, originalUnique, 'unique');
      var fieldDef, newFieldDef;
      if (fieldDefIndex.length){
        fieldDefIndex = fieldDefIndex[0];
        fieldDef = formDef.fields[fieldDefIndex];
        newFieldDef = _.clone(fieldDef);
        
        var fieldDefIndexEOL = aa.indexOfObjectByKey(formDef.fields, 'eol', 'type', fieldDefIndex);
        if (fieldDefIndexEOL.length){
          fieldDefIndexEOL = fieldDefIndexEOL[0];
          formDef.fields.splice(fieldDefIndexEOL + 1, 0, newFieldDef, _.clone(formDef.fields[fieldDefIndexEOL]));
        }
        else {
          fieldDefIndexEOL = formDef.fields.length - 1;
          formDef.fields.push(newFieldDef, _.clone(formDef.fields[fieldDefIndexEOL]));
        }
        
        
        
        // newFieldDef.name = [newFieldDef.cloneType, unique].join('');
        newFieldDef.unique = unique;
        newFieldDef.isClone = true;
        if (!newFieldDef.displayCondition){
          newFieldDef.displayCondition = function(){
            return scheduleEditNotInternal(isHistory);
          };
        }
        
        activeEdit.data.taskItems.push({
          unique: unique,
          isNew: true
        });
      }
      else {
        fblog('inputCloneOnVal(): no fieldDef');
      }
      
      newInput
        .attr('id', [formDef.prefix, newInput.attr('name'), unique].join(''))
        .attr('unique', unique)
        // .attr('isClone', 'true')
        .val('')
        .removeClass('focused')
        .insertAfter(insertTarget)
        .wrap('<div class="item clone" style="margin-top: .5em;" />')
        .parent()
        .after('<div class="clearfix" />');
        
      // replicate sibling elements
      if (fieldDefIndex && fieldDefIndexEOL && cloneSiblings){
      
        var newSibling, originalFieldDef;
        //var newFieldDefs = _.clone(formDef.fields.slice(fieldDefIndex, fieldDefIndexEOL));
        for (var i = fieldDefIndexEOL - 1; i > fieldDefIndex; i--){
          originalFieldDef = formDef.fields[i];
          newFieldDef = _.clone(originalFieldDef);
          if (!originalFieldDef.unique){
            originalFieldDef.unique = originalUnique;
            $(['#', formDef.prefix, originalFieldDef.name].join('')).attr('unique', originalUnique);
            if (originalFieldDef.upload){
              $(['#', formDef.prefix, 'uploadStatus'].join('')).attr('unique', originalUnique);
            }
            // $(['#', formDef.prefix, originalFieldDef.name, originalFieldDef.unique].join('')).attr('unique', originalUnique);
            // $(['#', formDef.id, ' [name=', originalFieldDef.name, '][unique=', originalFieldDef.unique, ']'].join('')).attr('unique', originalUnique);
          }
          newFieldDef.unique = unique;
          delete newFieldDef.label;
          delete newFieldDef.helpIcon;
          newFieldDef.isClone = true;
          if (!newFieldDef.displayCondition){
            newFieldDef.displayCondition = function(){
              return scheduleEditNotInternal(isHistory);
            };
          }
          formDef.fields.splice(fieldDefIndexEOL + 2, 0, newFieldDef);
          
          newSibling = $(formCreateField(formDef, newFieldDef));
          newSibling.children('table').remove();
          // newSibling.attr('isClone', 'true');
          var newSiblingId = [formDef.prefix, newFieldDef.name, newFieldDef.unique].join('');
          
          if (newFieldDef.type != 'button') {
            newSibling.children().attr('id', newSiblingId);
            //newSibling.wrap('<div class="item" />');
            //newSibling = newSibling.parent();
            inputSetupHover(newSibling, formDef);
          }
          
          newSibling.insertAfter(newInput.parent()).css('margin-top', '.5em');
          
          if (newFieldDef.type == 'button'){
            formSetupButton(formDef, newFieldDef);
          }
          if (newFieldDef.type == 'dropdown'){
            formSetupDropdown(formDef, aa.findObjectByKey(formDef.dropdowns, newSiblingId, 'id', null, true), newFieldDef);
          }
          
          
        }
        
        // var newSiblings = el.parent().nextUntil('div.clearfix').clone(true);
        // newSiblings.children('table').remove();
        // newSiblings.children('input, .button').attr('unique', unique).each(function(){
          // var curr = $(this);
          // curr.attr('id', [formDef.prefix, curr.attr('name'), unique].join(''));
          // if (curr.hasClass('button')){
            // curr.css('margin-top', '');
          // }
        // });
        
        // newSiblings.insertAfter(newInput.parent()).css('margin-top', '.5em');
        
        
      }
      else {
      }
      
      //var formEl = el.parents('form');
      //var formId = formEl.attr('id');
      //formDef = forms[formId];
      //fieldDef = aa.findObjectByKey(formDef.fields, 'name');
      
      inputSetupHover(newInput.parent(), formDef);
      inputCloneOnVal(newInput, newFieldDef, cloneSiblings);
    }
    else {
      // el.siblings('.clone').remove();
    }
    
    scheduleOnTaskEditChange({isHistory: isHistory});
  }
  
  
  
  
  
  
  
  function formSetupEventHandlers(formDef, container){
    if (!container) {
      container = $('#col1'); // schedule.currentPageElement
    }
  
    //formClearEventHandlers(container);
    
    // set up input hover handlers
    inputSetupHover($("input.inputText, textarea.inputTextarea", container).parents('div.item'), formDef);
    
    // set up button hover handlers
    $("div.buttonFramedContainer > div.frame", container).hover(  
      function () {
        $(this).addClass('frameHover').children('.body').addClass('bodyHover');
      },
      function () {
        $(this).removeClass('frameHover').children('.body').removeClass('bodyHover bodyClick');
      }
    );

    // set up inline button hover handlers
    $("div.button:not(.buttonFramedContainer), span.button:not(.buttonFramedContainer)", container).hover(  
      function () {
        $(this).addClass('buttonYellow');
      },
      function () {
        //if (!aa.existsIn(formDef.toggles, $(this).attr('target'))) {
          $(this).removeClass('buttonYellow');
        //}
      }
    );

  }
  
  
  
  function formClearEventHandlers(container){
    if (!container) {
      container = $('#col1'); // schedule.currentPageElement
    }
  
    $("input.inputText, textarea.inputTextarea", container).parents('div.item').unbind('mouseenter mouseleave');
    $("div.buttonFramedContainer > div.frame", container).unbind('mouseenter mouseleave');
    $("div.button:not(.buttonFramedContainer), span.button:not(.buttonFramedContainer)", container).unbind('mouseenter mouseleave');
  }
  
  

  function inputSetupHover(el, formDef){
    el.hover(
      function () {
        var
          thisEl = $(this),
          thisElInput = thisEl.contents('input.inputText, textarea.inputTextarea');
          
        thisEl.removeClass('itemInvalid');
        thisElInput.addClass('focused');
        if (thisElInput.hasClass('help')){
          $(['div[helpFor=', thisElInput.attr('id'), ']'].join(''), thisEl).addClass('calloutFocused');
        }
      },
      function () {
        // if (formDef && formDef.id){
        
        var
          // thisEl = $(this),
          // thisElInput = thisEl.contents('input.inputText, textarea.inputTextarea'),
          // thisElInputId = thisElInput.attr('id'),
          thisElFormId = formDef.id,
          allInputs = $(['#', thisElFormId, ' input.inputText, #', thisElFormId, ' textarea.inputTextarea'].join('')); //thisEl.parents('form').attr('id');

        // dehover all text inputs on this form
        allInputs.each(function(){
          var
            thisElInput = $(this),
            thisElInputId = thisElInput.attr('id'),
            thisElFormId = formDef.id;
            
          // only remove hover indication if element is not already focused
          if (thisElInputId != shared.inputFocused){
          
            if (thisElInput.hasClass('help')){
              $(['div[helpFor=', thisElInputId, ']'].join(''), thisElInput.parent()).removeClass('calloutFocused');
            }
            thisElInput.removeClass('focused');
            if (aa.existsIn(forms[thisElFormId].blurred, thisElInputId) && aa.existsIn(forms[thisElFormId].invalid, thisElInputId)){
              thisElInput.parent().addClass('itemInvalid');
            }
          }
        });
        
        // }
      }
    );
  }


  
  
  
  
  
  function dataFormatContact(thisItem, compareTo, deleted){
    var outputArray = [];
    var temp;
    var vip = false;

    vip = Boolean(Number(thisItem['vip'])) || false;

    outputArray.push(
      '<span class="', (vip ? 'vip' : ''), (deleted ? 'iconright icon-delete highlightLight' : ''), '">'
    );
    
    temp = $.trim(
      [
        as.echoIf(thisItem['name_prefix'], '', ' '),
        as.echoIf(thisItem['name_first'], '', ' '),
        as.echoIf(thisItem['name_middle'], '', ' '),
        as.echoIf(thisItem['name_last']),
        as.echoIf(thisItem['name_suffix'], ', ')
      ].join('')
    );
    
    if (temp){
      outputArray.push(temp);
    }
    
    outputArray.push(
      '<span class="contactDetails">'
    );
    
    as.pushIf(outputArray, thisItem['title'], ' &nbsp;');
    
    if (temp && vip){
      // outputArray.push('<span class="iconinlinegap icon-vip" />');
      outputArray.push('<span class="vipMarker">VIP</span>');
    }
    
    outputArray.push('</span></span>');

    outputArray.push(
      '<span class="contactDetails">'
    );
    
    as.pushIf(outputArray, thisItem['department'], temp ? '<br>' : '');
    
    if (thisItem.account_contact == 1){
      var contactLocation = aa.findObjectByKey(shared.clients, thisItem.location_id, 'location_id', null, true);
      var locationNameFriendly = [contactLocation.location_name];
      if (Number(contactLocation.num_locations) > 1){
        locationNameFriendly.push(
          ' (',                               
          (contactLocation.unique_identifier || contactLocation.city),
          ')'
        );
      }
      
      locationNameFriendly = locationNameFriendly.join('');
      outputArray.push('<br>');
      outputArray.push('<span class="smallCaps light">');
      if (thisItem.location_id == compareTo.id){
        outputArray.push(locationNameFriendly);
      }
      else {
        outputArray.push('<a class="icon-view iconright" href="/clients/', thisItem.location_id, '">', locationNameFriendly, '</a>');
      }
      outputArray.push('</span>');
    }
      
    if (thisItem['phone']){
      temp = formatPhone(thisItem['phone']);
      if (formatPhone(compareTo['phone']) != temp){
        outputArray.push('<br>', temp);
      }
    }
    if (thisItem['phone2']){
      temp = formatPhone(thisItem['phone2']);
      if (formatPhone(compareTo['phone']) != temp){
        outputArray.push('<br>', temp, ' <span class="smallCaps">alternate phone</span>');
      }
    }
    
    as.pushIf(outputArray, formatPhone(thisItem['phone_cell']), '<br>', ' <span class="smallCaps">cell</span>');
    as.pushIf(outputArray, formatPhone(thisItem['pager']), '<br>', ' <span class="smallCaps">pager</span>');
    as.pushIf(outputArray, formatPhone(thisItem['phone_home']), '<br>', ' <span class="smallCaps">home</span>');
      
    if (thisItem['fax']){
      temp = formatPhone(thisItem['fax']);
      if (formatPhone(compareTo['fax']) != temp){
        outputArray.push('<br>', temp, ' <span class="smallCaps">fax</span>');
      }
    }
    if (thisItem['fax2']){
      temp = formatPhone(thisItem['fax2']);
      if (formatPhone(compareTo['fax']) != temp){
        outputArray.push('<br>', temp, ' <span class="smallCaps">alternate fax</span>');
      }
    }
    
    as.pushIf(outputArray, thisItem['email'], '<br><a href="mailto:', ['">', thisItem['email'], '</a>'].join('')); 
    as.pushIf(outputArray, thisItem['email2'], '<br><a href="mailto:', ['">', thisItem['email2'], '</a> <span class="smallCaps">alternate email</span>'].join('')); 
      // as.echoIf(thisItem['email2'], ['<br><a href="mailto:', thisItem['email'], '">'].join(''), '</a> (alternate email)') );

    outputArray.push('</span>');
      
    return outputArray.join('');
  }
  
  
  
  function formatPhone(input){
  
    if (input){
      var output;

      var parts = /(?:[1]?[\-\/)\.]?(?:[ ]+)?)?(?:[(]?([\d]{3})?[\-\/)\.]?)?(?:[ ]+)?([\d]{3})[\-\/\.]?(?:[ ]+)?([\d]{4})(?:(?:[ ]+)?(?:[xX]|(?:ext[\.]?))(?:[ ]+)?([\d]{1,5}))?/.exec(input);
      
      if (parts && parts.length){
        output = [(parts[1] ? ['(', parts[1], ') '].join('') : ''), parts[2], '-', parts[3], (parts[4] ? [' x', parts[4]].join('') : '')].join('');

        return $.trim(output);
      }
      else {
        return input;
      }
    }
    
    else {
      return '';
    }
  }
  




  // for each instance of each element of terms (array) in str, insert prepend before and append after
  // e.g., used for search match highlighting
  function formatString(str, terms, prepend, append, ignored){
  
    var i, j;
    var thisTerm;
    var regex;
    var regexArr = [];
    
    if (!str){
      return '';
    }

    if (!ignored){
      ignored = '';
    }
    
    for (i = 0; i < terms.length; i++){
      thisTerm = terms[i];
      regex = [];
      
      thisTerm = $.trim(thisTerm);
      
      for (j = 0; j < thisTerm.length - 1; j++){
        regex.push(thisTerm[j], '[', ignored, ']?');
      }
      
      // append final character
      regex.push(thisTerm[j]);
      
      regexArr.push(regex.join(''));
    }
    
    // create regex object
    regex = new RegExp(eval(['/(', regexArr.join('|'), ')/gi'].join('')));
    
    str = str.replace(regex, [prepend, '$1', append].join(''));
    
    return str;
    
    // if (preparse){
      // for (token in preparse){
        // str = str.replace(new RegExp(token, 'gi'), preparse[token]);
      // }
    // }
    
    // strip html tags
    //str = str.replace(/<("[^"]*"|'[^']*'|[^'">])*>/gi, '');
    
    // // for each search term
    // for (i = 0; i < terms.length; i++){
      // thisTerm = terms[i];
      
      // // trim whitespace
      // thisTerm = $.trim(thisTerm);

      // // removed ignored tokens
      // thisTerm = thisTerm.replace(new RegExp(eval('/[' + ignored + ']/g')), '');
      
      // // if this is a non-whitespace string
      // if (thisTerm){
        
        // // insert regex for ignored tokens between each character in needle
        // for (j = 0; j < thisTerm.length - 1; j++){
          // regex += thisTerm[i] + '[' + ignored + ']?';
        // }
        
        // // append final character
        // regex += thisTerm[j + 1]; 
        
        // // create regex object
        // regex = new RegExp(eval('/' + regex + '/i');
        
        // // find each instance of formatted needle
        // match = regex.exec(str);
        // while (match != null){ // get next match, null if no match
          // newStr = 
          // for (var k = 0; k < terms[i].length; k++){ // for each character in matched string
            // aa.pushUnique(matches, k + offset); // add index to matches array
          // }
          // offset += 1;
          
          // j = str.substr(offset).search(regex);
        // }
      
      // }

      // // if this is a non-whitespace string
      // if (terms[i]){
        // var offset = 0;
        // var regex = new RegExp(terms[i], 'i');
        // var j = str.substr(offset).search(regex);
        // //fblog(j);
        // while (j != -1){ // get index of next match, -1 if no match
          // offset += j; // update offset to current position + 1 to start searching at next character
          // for (var k = 0; k < terms[i].length; k++){ // for each character in matched string
            // aa.pushUnique(matches, k + offset); // add index to matches array
          // }
          // offset += 1;
          
          // j = str.substr(offset).search(regex);
        // }
      
      // }
      
      
      // // if this is a non-whitespace string
      // if (thisTerm){
        // var offset = 0;
        // var regex = new RegExp(thisTerm, 'i');
        // var sub = str.substr(offset);
        // var prereplaceLength = sub.length;
        // var replaceLengthDiff = 0;
        
        // // remove tokens from haystack
        // if (preparse){
          // for (token in preparse){
            // sub = sub.replace(new RegExp(token, 'gi'), preparse[token]);
          // }
        // }
        
        // // calculate total number of tokens removed
        // replaceLengthDiff = prereplaceLength - sub.length;
        
        // var j = sub.search(regex);
        
        // while (j != -1){ // get index of next match, -1 if no match
          // offset += j; // update offset to current position + 1 to start searching at next character
          // for (var k = 0; k < terms[i].length; k++){ // for each character in matched string
            // aa.pushUnique(matches, k + offset); // add index to matches array
          // }
          // offset += 1;
          
          // sub = str.substr(offset);
          
          // prereplaceLength = sub.length;
          
          // if (preparse){
            // for (token in preparse){
              // sub = sub.replace(new RegExp(token, 'gi'), preparse[token]);
            // }
          // }
          
          // replaceLengthDiff = prereplaceLength - sub.length;
          
          // j = sub.search(regex);
        // }
      
      // }      
      
    // }
    
    // if (!matches.length){
      // return str;
    // }
    
    // matches.sort(function(a,b){return a-b;});
    
    // //var newstr = '';
    // var newstrArray = [];
    // for (i = 0; i < matches.length; i++){
      // if (matches[i] != (matches[i-1] + 1)){ // if this matched character is not exactly one position after the previous character, assume there is a gap between previous match and this
        // if (i > 0){ // if this isn't the first character in the string, assume there was a previous match which now must be followed by the append string
            // newstrArray.push(append);
        // }
        // newstrArray.push(str.substring(i === 0 || i === '0' ? 0 : matches[i-1] + 1, matches[i])); // add all characters between last match and this
        // newstrArray.push(prepend); // add prepend string
      // }

      // newstrArray.push(str.charAt(matches[i])); // finally add this matched character
    // }
    
    // if (matches[matches.length - 1] < str.length){ // add closing string if still in match after looping through matches
      // newstrArray.push(append); // add append string after last match
      // newstrArray.push(str.substring(matches[matches.length - 1] + 1)); // add any more unmatched characters between last match and end of string
    // }
    
    // //fblog ('//// ' + newstr + ' //// ' + matches);
    // return newstrArray.join('');
  }




  
  
  function formCreateField(formDef, index){
    var field;

    if (Number(index) == index){
      field = formDef.fields[index];
    }
    else {
      field = index;
    }
    
    var fieldName = field.name; // || index;
    var fieldId = [formDef.prefix, fieldName, field.unique || ''].join('');
    //var content = '';
    var contentArr = [];
    var i;
    var cssArr = [];
    
    // only parse if this appears to be a valid field definition
    if (field) {
    
      // add autocomplete fields to lookups array
      if (field.lookup){
        formDef.lookups[fieldId] = field.lookup;
      }
      
      if (field.autoClone){
        formDef.autoClones.push(field);
      }
      
      // if this field requests an ajax call after being shown, add its config object to formDef's list
      if (field.ajaxOnShow){
        formDef.ajaxes[fieldId] = field.ajaxOnShow;
      }
      
      // // add fields requiring change tracking to array
      // if (field['editTrack'] && (field['type'] != 'dropdown')){
        // formDef.editTracks[fieldId] = field['editTrack'];
      // }
      
      // add required fields to invalid array before validation
      if (!formDef.dontValidate && field.required){
        aa.pushUnique(formDef.invalid, fieldId);
        if (field.confirm){
          aa.pushUnique(formDef.invalid, [fieldId, '_confirm'].join(''));
        }
      }
      
      // if displayCondition defined, test it and apply hidden class if false
      if (field.displayCondition !== undefined){
        if (
          (
            typeof field.displayCondition == 'function'
            &&
            !field.displayCondition()
          )
          ||
          !field.displayCondition
        ){
          cssArr.push('hidden');
        }
      }
      // if (field.displayCondition && typeof field.displayCondition == 'function'){
        // if (!field.displayCondition()){
          // cssArr.push('hidden');
        // }
      // }
      
      if (field.style){
        cssArr.push(field.style);
      }
      
      
      switch (field.type){
      
        case 'raw':
          contentArr.push(field.content);
          break;
          
      
        // end of form line, start next field on new line
        case 'eol':
        // if 'type' is the only field set, use a plain separator
          contentArr.push('<div class="clearfix"></div>');
          if (field['divider']){
            // contentArr.push("<div class='divider'></div>\n");
            contentArr.push('</div><div class="body gapTop">');
          }
          break;
          

        // static text content
        case 'static':
          // otherwise parse additional content
          if (field['description']){
            contentArr.push('<div class="clearfix"></div>');
            contentArr.push(field['description']);
          }
          
          if (formDef.data && formDef.data[field.name]){
            contentArr.push('<div class="item ', cssArr.join(' '), '">');
            if (field.label){
              contentArr.push('<label id="');
              contentArr.push(fieldId);
              contentArr.push('_label" for="');
              contentArr.push(fieldId);
              contentArr.push('"');
              
              if (field['labelInline'] && field['labelInline'] == true){
                contentArr.push(' style="display: inline; margin-right: .5em;"');
              }
              else {
                contentArr.push(' style="padding-left: 0"');
              }
              
              contentArr.push('>');
              contentArr.push(field['label'] ? [field['label'], ':'].join('') : '&nbsp;');
              contentArr.push('</label>');
            }
            contentArr.push('<div class="large bold" style="line-height: 1.25em">', formDef.data[field.name], '</div></div>');
          }
          
          if (field['help']){
            contentArr.push('<div class="callout calloutWide calloutStatic">');
            contentArr.push(field['help']);
            contentArr.push('</div>');
          }
          break;

          
        // button
        case 'button':
          // var buttonId;
          // if (field['name']){
            // buttonId = [formDef.prefix, field['name']].join('');
          // }
          // else {
            // buttonId = [formDef.prefix, "button_", field['target'], "_", field['action']].join('');
          // }
          // contentArr.push(buttonCreate({
            // formDef: formDef,
            // fieldDef: field,
            // id: buttonId,
            // name: field['name'],
            // target: field['target'],
            // action: field['action'],
            // text: field['text'],
            // textToggled: field['textToggled'],
            // style: field['style']
          // }));
          
          if (!field.readOnly){
            contentArr.push(buttonCreate(formDef, field));
          }
          break;

          
          
        // all input fields, including those with no type specified
        default:
      
          if (field['description']){
            contentArr.push('<div class="clearfix"></div>');
            contentArr.push(field['description']);
          }
          
          // otherwise display a normal text input
          else {
          
            // outer item container
            if (field.inline){
              contentArr.push('<span style="margin-left: .5em; margin-right: .5em" class="');
            }
            else {
              contentArr.push('<div class="item ');
            }
            
            if (
              field['hideUntilLookup']
              ||
              (
                field['applicableIf']
                &&
                // (function(){ return function(){ return false; }(); )
                (function (){
                  return function(){
                    for (var i in field['applicableIf']){
                      var iField = aa.findObjectByKey(formDef.fields, i, 'name')[0];
                      switch (typeof iField.prefillVal){
                      
                        case 'string':
                          if ($.trim(iField.prefillVal.toLowerCase()) != $.trim(field['applicableIf'][i].toLowerCase())){
                            return true;
                          }
                          break;
                          
                        case 'number':
                          // fblog(['prefillVal (', iField.prefillVal, ') ?= ', field['applicableIf'][i]].join(''));
                          if (iField.prefillVal != field['applicableIf'][i]){
                            return true;
                          }
                          break;
                          
                        case 'object':
                          //fblog('formCreateField(): applicableIf object');
                          if (iField.type == 'dropdown'){
                            //fblog('formCreateField(): applicableIf dropdown');
                            if (field['applicableIf'][i] instanceof Array){ // setting is an array
                              //fblog(['formCreateField(): applicableIf is Array (', field['applicableIf'][i], ' ?= ', formDef.fields[i].prefillVal, ')'].join(''));
                              if (!aa.objectCompare(field['applicableIf'][i], iField.prefillVal)){
                                //fblog('formCreateField(): applicableIf compare != true');
                                return true;
                              }
                            }
                            else { // setting is a single value
                              //fblog('formCreateField(): applicableIf not Array');
                              if (iField.prefillVal.length != 1 || iField.prefillVal[0] != field['applicableIf'][i]){
                                //fblog('formCreateField(): applicableIf values not equal');
                                return true;
                              }
                            }
                          }
                        
                          else if (!aa.existsIn(iField.prefillVal, field['applicableIf'][i])){
                            return true;
                          }
                          break;
                      }
                    }
                    return false;
                  };
                }())
              )
            ){
              cssArr.push('hidden');
            }
              
            contentArr.push(cssArr.join(' '), '">');
            
            // input label
            //content += "        <label id='" + fieldId + "_label' for='" + fieldId + "'" + (!field['required'] && !form['dontDifferentiateRequired'] ? " class='optional'" : "") + ">" + field['label'] + ":";
            //content += "        <label id='" + fieldId + "_label' for='" + fieldId + "'><table class='inputLabelContainer" + (field['width'] == 'full' ? 'Wide' : '') + "' cellspacing='0'><tr><td class='inputLabelLeft" + (!field['required'] && !formDef['dontDifferentiateRequired'] ? " inputLabelOptional" : "") + "'>" + field['label'] + ":</td>";
            
            if (!(field['labelInline'] && field['labelInline'] == true)){
              contentArr.push('<table class="');
              if (field['width'] == 'full'){
                // contentArr.push('inputLabelContainerWide');
                //contentArr.push('width100');
                contentArr.push('labelwidth-full');
              }
              else if (!field['width']){
                contentArr.push('');
              }
              contentArr.push('" cellspacing="0"><tr><td class="inputLabelLeft');
              contentArr.push(!field['required'] && !formDef['dontDifferentiateRequired'] ? ' inputLabelOptional' : '');
              contentArr.push('">');
            }
            
            contentArr.push('<label id="');
            contentArr.push(fieldId);
            contentArr.push('_label" for="');
            contentArr.push(fieldId);
            contentArr.push('"');
            
            if (field['labelInline'] && field['labelInline'] == true){
              contentArr.push(' style="display: inline; margin-right: .5em"');
            }
            
            if (field.helpIcon){
              contentArr.push(' class="icon-help iconright" title="', field.helpIcon, '"');
            }
            
            contentArr.push('>');
            contentArr.push(field['label'] ? [field['label'], ':'].join('') : '&nbsp;');
            contentArr.push('</label>');
            
            if (!(field['labelInline'] && field['labelInline'] == true)){
              contentArr.push('</td>');
              
              if (field['lookupFilter']){
                contentArr.push('<td class="inputLabelRight"><span class="smallCaps">Search by:</span>');
                for (i in field['lookupFilter']){
                  contentArr.push(buttonCreate(formDef, {
                    // formDef: formDef,
                    // id: [formDef.prefix, "button_", fieldId, "_filter_", i].join(''),
                    target: i,
                    action: 'filterToggle',
                    text: field['lookupFilter'][i],
                    style: 'inline toggle'
                  }));
                }
                contentArr.push('</td>');
              }
              
              //content += "</tr></table></label>\n";
              contentArr.push('</tr></table>');
            }
            
           
            // determine which type of input this is (if not specified, use standard input control)
            switch (field['type']){
            
              case 'readonly':
                contentArr.push('<div class="readOnly">');
                contentArr.push(field['content']);
                contentArr.push('</div>');
                break;
                
                
              case 'select':
                formDef.selects.push({id: fieldId});
                
                contentArr.push(
                  '<select name="',
                  fieldName,
                  '" id="',
                  fieldId,
                  '"');
                  
                if (field.unique){
                  contentArr.push(' unique="', field.unique, '"');
                }
                
                contentArr.push('>');
                
                for (i = 0; i < field.values.length; i++){
                  contentArr.push('<option value="', field.values[i].id, '" ', (field.values[i].selected ? ' selected="selected"' : ''), '>', field.values[i].name, '</option>');
                }
                
                contentArr.push(
                  '</select>'
                );
                break;
                
            
              case 'dropdown':
                formDef.dropdowns.push({id: fieldId});
              
              
                // create SELECT element for MultiSelect plugin
                contentArr.push('<select multiple="multiple" name="');
                contentArr.push(fieldName);
                contentArr.push('" id="');
                contentArr.push(fieldId);
                contentArr.push('" class="hidden inputSelect');
                contentArr.push(field.required ? ' required' : '');
                contentArr.push(field.help ? ' help' : '');
                contentArr.push(field.width ? [' width-', field.width].join('') : '');
                contentArr.push('"');
                contentArr.push(field.regex ? [' regex="', field.regex, '"'].join('') : '');
                
                if (field.unique){
                  contentArr.push(' unique="', field.unique, '"');
                }
                
                contentArr.push('>');
                             
                if (typeof field.values == 'function'){
                  field.values = field.values();
                }
                
                outer: for (i = 0; i < field.values.length; i++){
                  contentArr.push('<option value="');
                  contentArr.push(field.values[i].id);
                  contentArr.push('"');
                  
                  if (field.prefillVal == undefined && formDef.data && formDef.data[field.name]){
                    if (field.values[i].id == formDef.data[field.name]){
                      contentArr.push(' selected="selected"');
                    }
                  }
                  else {
                    // add default selection if applicable
                    switch (typeof field.prefillVal){
                      case 'string':
                        if ($.trim(field.prefillVal.toLowerCase()) == $.trim(field.values[i].name.toLowerCase())){
                          contentArr.push(' selected="selected"');
                          //break outer;
                        }
                        break;
                        
                      case 'number':
                        if (field.prefillVal == field.values[i].id){
                          contentArr.push(' selected="selected"');
                          //break outer;
                        }
                        break;
                        
                      case 'function':
                        if (field.prefillVal() == field.values[i].id){
                          contentArr.push(' selected="selected"');
                        }
                        break;
                        
                      case 'object': // array
                        for (var j in field.prefillVal){
                          if (field.prefillVal[j] == field.values[i].id){
                            contentArr.push(' selected="selected"');
                            break;
                          }
                        }
                        break;
                    }
                  }
                  
                  if (field.values[i].description){
                    contentArr.push(' description="', field.values[i].description, '"');
                  }
                  contentArr.push('>');
                  contentArr.push(as.toTitleCase(field.values[i].name));
                  contentArr.push('</option>');
                }
                
                contentArr.push('</select>');
                break;
            
              case 'textarea':
                formDef.textareas.push(fieldId);
              
                if (field.readOnly){
                  contentArr.push('<div style="height: auto"');
                }
                else {
                  contentArr.push('<textarea name="');
                  contentArr.push(fieldName);
                  contentArr.push('"');
                }
                contentArr.push(' id="');
                contentArr.push(fieldId);
                contentArr.push('" class="inputTextarea');
                contentArr.push(field['required'] ? ' required' : '');
                contentArr.push(field['help'] ? ' help' : '');
                contentArr.push(field['width'] ? [' width-', field['width']].join('') : '');
                contentArr.push('"');
                contentArr.push(field['regex'] ? [' regex="', field['regex'], '"'].join('') : '');

                if (field.unique){
                  contentArr.push(' unique="', field.unique, '"');
                }
                
                contentArr.push('>');

                if (field.readOnly){
                  if (field.prefillVal !== undefined){
                    contentArr.push(field.prefillVal);
                  }
                  else if (formDef.data && formDef.data[field.name]){
                    contentArr.push(formDef.data[field.name]);
                  }
                  else {
                    contentArr.push('&nbsp;');
                  }
                  contentArr.push('</div>');
                }  
                else {
                  if (field.prefillVal !== undefined){
                    contentArr.push(field.prefillVal);
                  }
                  else if (formDef.data && formDef.data[field.name]){
                    contentArr.push(formDef.data[field.name]);
                  }
                  else {
                    contentArr.push('');
                  }
                  contentArr.push('</textarea>');
                }
                
                // if (field.readOnly){
                  // contentArr.push(field['prefillVal'] ? field['prefillVal'] : '&nbsp;');
                  // contentArr.push('</div>');
                // }
                // else {
                  // contentArr.push(field['prefillVal'] ? field['prefillVal'] : '');
                  // contentArr.push('</textarea>');
                // }
                break;
            
              default:
                if (field.readOnly){
                  contentArr.push('<div');
                }
                else {
                  contentArr.push('<input type="');
                  contentArr.push(field['mask'] ? 'password' : 'text');
                  contentArr.push('"');
                }
                
                contentArr.push(' name="');
                contentArr.push(fieldName);
                contentArr.push('" id="');
                contentArr.push(fieldId);
                contentArr.push('" class="inputText');
                contentArr.push(field['capitalize'] === true ? ' titleCase' : '');
                contentArr.push(field['capitalize'] == 'sentence' ? ' sentenceCase' : '');
                contentArr.push(field['capitalize'] == 'upper' ? ' upperCase' : '');
                contentArr.push(field['required'] ? ' required' : '');
                contentArr.push(field['help'] ? ' help' : '');
                contentArr.push(field['width'] ? [' width-', field['width']].join('') : '');
                contentArr.push('"');
                contentArr.push(field['lookup'] ? [' lookup="', field['lookup'], '"'].join('') : '');
                contentArr.push(field['lookupFill'] ? [' lookupFill="', field['lookupFill'], '"'].join('') : '');
                contentArr.push(field['confirm'] ? [' confirm="', fieldId, '"'].join('') : '');
                // contentArr.push(field['capitalize'] && field['capitalize'] !== true ? [' titleCase="', field['capitalize'], '"'].join('') : '');
                contentArr.push(field['regex'] ? [' regex="', field['regex'], '"'].join('') : '');
                contentArr.push(' size="10" maxlength="200"');

                if (field.unique){
                  contentArr.push(' unique="', field.unique, '"');
                }
                
                if (field.readOnly){
                  contentArr.push('>');
                  if (field.get && typeof field.get == 'function'){
                    contentArr.push(field.get(formDef.data));
                  }
                  else if (field.prefillVal !== undefined){
                    contentArr.push(field.prefillVal);
                  }
                  else if (formDef.data && formDef.data[field.name]){
                    contentArr.push(formDef.data[field.name]);
                  }
                  else {
                    contentArr.push('&nbsp;');
                  }
                  contentArr.push('</div>');
                }  
                else {
                  contentArr.push(' value="');
                  // contentArr.push(field['prefillVal'] ? field['prefillVal'] : '');
                  if (field.get && typeof field.get == 'function'){
                    contentArr.push(field.get(formDef.data));
                  }
                  else if (field.prefillVal !== undefined){
                    contentArr.push(field.prefillVal);
                  }
                  else if (formDef.data && formDef.data[field.name]){
                    contentArr.push(formDef.data[field.name]);
                  }
                  else {
                    contentArr.push('');
                  }
                  contentArr.push('" />');
                }
            }

            
            // display attached help information
            if (field['help'] && !field.readOnly){
              contentArr.push('<div class="callout calloutAttached');
              contentArr.push(field['width'] ? [' calloutWidth', as.toTitleCase(field['width'])].join('') : '');
              contentArr.push('"');
              contentArr.push('" helpFor="');
              contentArr.push(fieldId);
              contentArr.push('">');
              contentArr.push(field['help']);
              contentArr.push('</div>');
            }
            
            
            // add hidden field for extra value (usually id or index) associated with lookup field
            if (field['lookup']){
              contentArr.push('<input type="hidden" id="');
              contentArr.push(fieldId);
              contentArr.push('_lookupVal" name="');
              contentArr.push(fieldName);
              contentArr.push('_lookupVal" value="');
              contentArr.push(field['lookupValPrefill']);
              contentArr.push('" />');
            }

            
            // close input div
            if (field.inline){
              contentArr.push('</span>');
            }
            else {
              contentArr.push('</div>');
            }
            

            // display second confirm input if needed
            if (field['confirm']){
              contentArr.push('<div class="item');
              contentArr.push(field['hideUntilLookup'] ? ' hidden' : '');
              contentArr.push('">');
              contentArr.push('<table class="');
              contentArr.push(field['width'] == 'full' ? 'Wide' : '');
              contentArr.push('" cellspacing="0"><tr><td class="inputLabelLeft"><label id="');
              contentArr.push(fieldId);
              contentArr.push('_confirm_label" for="');
              contentArr.push(fieldId);
              contentArr.push('_confirm">');
              contentArr.push(field['label']);
              contentArr.push(' again to confirm:</label></td></tr></table>');
              
              contentArr.push('<input type="');
              contentArr.push(field['mask'] ? 'password' : 'text');
              contentArr.push('" name="');
              contentArr.push(fieldName);
              contentArr.push('_confirm" id="');
              contentArr.push(fieldId);
              contentArr.push('_confirm" class="inputText');
              contentArr.push(field['required'] ? ' required' : '');
              contentArr.push(field['help'] ? ' help' : '');
              contentArr.push('"');
              contentArr.push(field['lookup'] ? [' lookup="', field['lookup'], '"'].join('') : '');
              contentArr.push(field['lookupFill'] ? [' lookupFill="', field['lookupFill'], '"'].join('') : '');
              contentArr.push(field['confirm'] ? [' confirm="', fieldId, '"'].join('') : '');
              contentArr.push(field['regex'] ? [' regex="', field['regex'], '"'].join('') : '');
              contentArr.push(' size="10" maxlength="200" value="');
              contentArr.push(field['prefillVal'] ? field['prefillVal'] : '');
              contentArr.push('" />');
              
              if (field['helpConfirm']){
                contentArr.push('<div class="callout calloutAttached" helpFor="');
                contentArr.push(fieldId);
                contentArr.push('_confirm">');
                contentArr.push(field['helpConfirm']);
                contentArr.push('</div>');
              }
              contentArr.push('</div>');
            }
          }
          
          // end of default case for outer field[type] switch
        
      }
    }
    
    return contentArr.join('');
      
  }
  
  
  
  
  function buttonCreate(formDef, fieldDef){
    var buttonId, css, hidden = false, disabled = false;
    var contentArray = [];
    var iconArray = [];
    var buttonText = 'button';
    var testResult;
    
    var action;
    
    if (typeof fieldDef.action == 'function'){
      action = null;
    }
    else {
      action = fieldDef.action;
    }
    
    if (!formDef){
      formDef = {buttons: []};
    }

    if (fieldDef.textCondition){
      if (typeof fieldDef.textCondition.test == 'function'){
        testResult = fieldDef.textCondition.test();
      }
      else {
        testResult = fieldDef.textCondition.test;
      }
        
      if (testResult){
        buttonText = fieldDef.textCondition.whenTrue;
      }
      else {
        buttonText = fieldDef.textCondition.whenFalse;
      }
    }
    else if (fieldDef.text) {
      buttonText = fieldDef.text;
    }

    
    if (fieldDef.name){
      buttonId = [formDef.prefix, fieldDef.name, fieldDef.unique || ''].join('');
    }
    else {
      buttonId = [formDef.prefix, "button_", (fieldDef.target ? ([fieldDef.target, "_"].join('')) : ''), (action ? action : '')].join('');
    }
  
  
    if (fieldDef.name){
      formDef.buttons[fieldDef.name] = fieldDef;
    }
    else {
      formDef.buttons.push(buttonId);
    }
    
    
    css = fieldDef.css ? [fieldDef.css] : [];
    
    // if displayCondition defined, test it and apply hidden class if false
    if (fieldDef.displayCondition !== undefined){
      if (
        (
          typeof fieldDef.displayCondition == 'function'
          &&
          !fieldDef.displayCondition()
        )
        ||
        !fieldDef.displayCondition
      ){
        hidden = true;
      }
    }
    
    // // if enableCondition defined, test it and apply disabled class if false
    // if (fieldDef.enableCondition !== undefined){
      // if (
        // (
          // typeof fieldDef.enableCondition == 'function'
          // &&
          // !fieldDef.enableCondition(formDef, fieldDef)
        // )
        // ||
        // !fieldDef.enableCondition
      // ){
        // disabled = true;
      // }
    // }
    
    
    if (as.wordIn(fieldDef.style, 'inline')){
      if (as.wordIn(fieldDef.style, 'toggle')){
        // if (!config.textToggled){
          // iconArray.push("<span class='check'>-</span>");
        // }
        css.push("buttonToggle");
      }
    
      contentArray.push("<span class='button buttonInline", as.wordIn(fieldDef.style, 'left-align') ? ' buttonInlineLeft ' : ' ', css.join(' '), (hidden ? " hidden" : ""), "' id='", buttonId, "'", (action ? [" action='", action, "'"].join('') : ''), ">", iconArray.join(''), buttonText, "</span>");    
    }
    
    else if (as.wordIn(fieldDef.style, 'tab')){
      contentArray.push("<span class='button buttonInline buttonTab ", (as.wordIn(fieldDef.style, 'active') ? 'buttonTabActive ' : ''), css.join(' '), "' id='", buttonId, (fieldDef.target ? ["' target='", fieldDef.target].join('') : ''), "'", (action ? [" action='", action, "'"].join('') : ''), ">", iconArray.join(''), buttonText, "</span>");    
    }
    
    else if (as.wordIn(fieldDef.style, 'small')){
      contentArray.push("<div class='item", (as.wordIn(fieldDef.style, 'floatRight') ? ' floatRight' : ''), (hidden ? " hidden" : ""), "'><span class='button buttonInline", as.wordIn(fieldDef.style, 'left-align') ? ' buttonInlineLeft ' : ' ', css.join(' '), "' id='", buttonId, "'", (action ? [" action='", action, "'"].join('') : ''), ">", iconArray.join(''), buttonText, "</span></div>");    
    }
    
    else if (as.wordIn(fieldDef.style, 'big')){
      contentArray.push("<div class='item", (hidden ? " hidden" : ""), "'><div class='buttonFramedContainer", (as.wordIn(fieldDef.style, 'alignForm') ? " buttonFramedContainerAdjacent" : ''), "'><div class='frame' id='", buttonId, "' target='", fieldDef.target, "'", (action ? [" action='", action, "'"].join('') : ''), "><div class='body", (as.wordIn(fieldDef.style, 'big') ? " big" : ''), ' ', css.join(''), "'>", iconArray.join(''), buttonText, "</div></div></div></div>");
    }

    else {
      contentArray.push("<div class='item", (as.wordIn(fieldDef.style, 'floatRight') ? ' floatRight' : ''), (hidden ? " hidden" : ""), "'><div class='button", (disabled ? " buttonDisabled": ""), "' id='", buttonId, "' name='", fieldDef.name, "' unique='", fieldDef.unique, "' target='", fieldDef.target, "'", (action ? [" action='", action, "'"].join('') : ''), ">", iconArray.join(''), buttonText, "</div></div>");
    }
    
    fieldDef.id = buttonId;
    
    // if (typeof fieldDef.action == 'function'){
      // // fblog(buttonId);
      // $(['#', buttonId].join('')).live('click', function (e){
        // if (e.button < 2){
          // fieldDef.action({fieldDef: fieldDef});
        // }
      // });
    // }
    
    
    return contentArray.join('');
  }

  
  function buttonUpdate(config){
    var buttonText, result;
    
    if (config.fieldDef.textCondition){
      if (typeof config.fieldDef.textCondition.test == 'function'){
        result = config.fieldDef.textCondition.test();
        // fblog(['textCondition is function, result = ', result].join(''));
      }
      else {
        result = config.fieldDef.textCondition.test;
      }
      
      if (result){
        buttonText = config.fieldDef.textCondition.whenTrue;
      }
      else {
        buttonText = config.fieldDef.textCondition.whenFalse;
      }
    }
    else if (config.fieldDef.text){
      buttonText = config.fieldDef.text;
    }
    
    config.element.text(buttonText);
  }
  
  
  
  
  
  
  function buttonAddHoverHandler(buttons, offset){
    // var buttons = $(['.icon-hover', offset].join(''), container);
    var hoverClass = ['icon-hover', offset, '-hover'].join('');
    var activeClass = ['icon-hover', offset, '-active'].join('');
    
    buttons.unbind('mouseenter mouseleave');
    
    buttons
      .hover(
        function(){
          $(this).addClass(hoverClass);
        },
        function(){
          $(this).removeClass([hoverClass, ' ', activeClass].join(''));
        }
      )
      .mousedown(
        function(){
          $(this).addClass(activeClass);
        }
      )
      .mouseup(
        function(){
          $(this).removeClass(activeClass);
        }
      );
  }
  
  
  
  
  
  function buttonSetupUpload(button, buttonDef, formDef){
    var activeEdit = (button.closest('form').attr('id') == 'clientScheduleEditForm' ? clients.scheduleEdit : schedule.activeEdit);
    if (buttonDef.attach){
      // button.parent().after(['<div class="item', (button.parent().hasClass('hidden') ? ' hidden' : ''), '"><div id="uploadStatus', buttonDef.unique, '">No files uploaded</div></div>'].join(''));
      
      var taskItem;
      var uploadedFileStatus;
      
      if (buttonDef.unique){
        taskItem = aa.findObjectByKey(activeEdit.data.taskItems, buttonDef.unique, 'unique');
        if (taskItem.length){
          taskItem = taskItem[0];
        }
      }
      else {
        taskItem = activeEdit.data.taskItems[0];
      }
      
      if (taskItem && taskItem.taskFiles && taskItem.taskFiles.length){
        uploadedFileStatus = ['<a class="iconrighttop icon-ok taskItemLink" id="taskItemLinkFileStatus', buttonDef.unique, '" unique="', buttonDef.unique, '">', taskItem.taskFiles.length, ' file', (taskItem.taskFiles.length == 1 ? '' : 's'), ' uploaded</a>'].join('');
      }
      else {
        uploadedFileStatus = 'No files uploaded';
      }
      
      
      $([
        '<div id="', formDef.prefix, 'uploadStatus', buttonDef.unique, '" unique="', buttonDef.unique,
        '" class="uploadStatus inlineBlock disabled">',
        uploadedFileStatus,
        // 'No files uploaded',
        '</div>'
      ].join(''))
      .insertAfter(button.addClass('inlineBlock'))
      .children('a.taskItemLink')
      .tooltip({
        //track: true,
        //fade: true,
        delay: 0,
        bodyHandler: function() {
          var content = 'n/a';
          var el = $(this);
          var unique = el.attr('unique');
          var recurrence;

          var content = [];
          for (var i = 0; i < taskItem.taskFiles.length; i++){
            recurrence = aa.findObjectByKey(shared.taskrecurrences, taskItem.taskFiles[i].recurrence);
            if (recurrence.length){
              recurrence = recurrence[0].name;
            }
            else {
              recurrence = false;
            }
            
            content.push([
              '<p><b>', taskItem.taskFiles[i].description, '</b></p>',
              (recurrence ? ['<p><span class="title">', recurrence, '</span></p>'].join('') : '')
            ].join(''));
          }
          
          return content.join('<hr style="color: #99bbe8; border-color: #99bbe8; margin: .5em 0; padding: 0; border-width: 0 0 1px;">');
        }
      });
      
    }
    // else {
      // buttonDef.hasContainer = true;
      // // add list container adjacent to upload button
      // button.parent()
        // .wrap(['<div id="uploadContainer" class="item', (button.parent().hasClass('hidden') ? ' hidden' : ''), '" style="margin: 0; width: 44.75em" />'].join(''))
        // .after(['<div class="item itemlist editableInline"><div class="placeholder lighter listitem" style="font-size: 1.5em; text-transform: uppercase; padding-top: .375em">No files uploaded for this visit</div></div>'].join(''));
      
      // button.parent().removeClass('hidden');
      // // fblog($('#uploadContainer').outerWidth(true) + ' - ' + button.parent().outerWidth(true) + ' - ' + parseInt($('#uploadContainer div.itemlist').css('margin-left')));
      // // $('#uploadContainer div.itemlist').width($('#uploadContainer').outerWidth(true) - button.parent().outerWidth(true) - parseInt($('#uploadContainer div.itemlist').css('margin-left')));
      
      
      
      // // adjust list width after everything is rendered to ensure it stays next to button
      // // use closure to capture current value of button
      // setTimeout(function(b){
        // return function(){
          // var list = b.parent().next('.itemlist');
          // list.width(list.parent().width() - list.siblings('.item').outerWidth(true) - parseInt(list.css('margin-left')) - 2);
        // };
      // }(button), 100);
    // }
    
    // create new invisible upload control
    var ajaxUpload = new AjaxUpload(button, {
      action: [shared.urlRoot, '/ajaxSSGet/reportUpload'].join(''),
      autoSubmit: true,
      // The type of data that you're expecting back from the server.
      // HTML (text) and XML are detected automatically.
      // Useful when you are using JSON data as a response, set to "json" in that case.
      // Also set server response type to text/html, otherwise it will not work in IE6
      responseType: "json",
      hoverClass: "buttonHover",
      disabledClass: "buttonDisabled",
      // Fired after the file is selected
      // Useful when autoSubmit is disabled
      // You can return false to cancel upload
      // @param file basename of uploaded file
      // @param extension of that file
        // onChange: function(file, extension){
          // $.facebox('test');
          // return false;
        // },
      // Fired before the file is uploaded
      // You can return false to cancel upload
      // @param file basename of uploaded file
      // @param extension of that file
      onSubmit: function(file, extension) {
        var button = $(this._button);
        var data = activeEdit.data;
        var uploadId = new Date().valueOf();
        var upload = {
          id: uploadId,
          unique: buttonDef.unique,
          el: button,
          field: buttonDef,
          data: {
            id: uploadId,
            local_file: file,
            location_id: data.location_id,
            location_name: data.location_name_short || data.location_name,
            location_city: data.city,
            location_unique_identifier: data.unique_identifier,
            account_id: data.account_id,
            account_num_locations: data.num_locations,
            task_id: data.id,
            taskitem_id: button.attr('unique'),
            date: data.schedule_start_date,
            physicist_id: data.physicist_id,
            physicist: shared.physicistsById[data.physicist_id].name,
            summary: data.summary,
            modality: data.tasktype_id ? aa.findObjectByKey(shared.tasktypes, data.tasktype_id)[0].name : '',
            recurrence: data.recurrence_id ? aa.findObjectByKey(shared.taskrecurrences, data.recurrence_id)[0].name : ''
          }
        };
        // var locationName = data.location_name;
        
        // if (data.num_locations > 1){
          // if (data.unique_identifier){
            // locationName = [locationName, ' (', unique_identifier, ')'].join('');
          // }
          // else {
            // locationName = [locationName, ' (', city, ')'].join('');
          // }
        // }
        
        if (!shared.uploads){
          shared.uploads = [];
        }
        
        shared.uploads.push(upload);
        
        this.setData(upload.data);
        
        // var output = [
          // '<div class="',
          // 'icon-loading iconlefttop2 uploadStatus listitem" id="upload', upload.id, '">',
          // file,
          // '<br /><span class="details">Uploading...</span>',
          // '</div>'
        // ];
        var output = [
          '<span class="icon-loading iconrighttop">Uploading...</span>'
        ];
        
        if (buttonDef.attach){
          $(['#', formDef.id, ' div.uploadStatus[unique=', buttonDef.unique, ']'].join('')).html(output.join(''));
        }
        else {
          button.parent().next('.itemlist').find('div.placeholder').remove().end().removeClass('hidden').prepend(output.join(''));
        }
        
        // button.addClass('buttonUploading').text('Sending...');
        // this.disable();
      },
      // Fired when file upload is completed
      // WARNING! DO NOT USE "FALSE" STRING AS A RESPONSE!
      // @param file basename of uploaded file
      // @param response server response
      onComplete: function(file, response) {
        // if (window.testdone == undefined){
          // window.testdone = 0;
        // }
        
        // if (window.testdone++ > 1){
          // return;
        // }
        
        
        var message = response.success_message;
        var upload, base, period, task, taskItem, taskFile;
        
        if (!response || !response.id){
          upload = aa.findObjectByKey(shared.uploads, file, 'local_file');
          if (!upload.length){
            return;
          }
          else {
            upload = upload[0];
          }
          
          response = upload;
          response.success = false;
        }
        else {
          upload = aa.findObjectByKey(shared.uploads, response.id, 'id', null, true);
          if (!upload){
            return;
          }
          
          $.extend(upload.data, response);
          
          // get base collection (containing tasks, taskItems, etc.)
          if (shared.activeInterface == 'clients'){ 
            period = aa.findObjectByKey(shared.periods, upload.data.date.substr(0, 7), 'period', null, true);
            if (!period){ fblog('no period'); }
            else {
              base = period;
            }
          }
          else {
            base = shared;
          }
          
          if (base){
            
            // find matching taskItem for this upload
            taskItem = aa.findObjectByKey(period.taskItems, upload.data.taskitem_id, 'taskitem_id', null, true);
            if (!taskItem){ // no match in main taskItems, so look under this upload's task
              task = getTaskById(period.tasks, upload.data.task_id);
              if (!task){ fblog('no task'); }
              else {
                taskItem = aa.findObjectByKey(task.data.taskItems, upload.data.taskitem_id, 'unique', null, true);
                if (!taskItem){ fblog('no taskItem'); }
              }
            }

            // find matching taskFile definition (or create if it doesn't exist)
            if (taskItem){
              if (!taskItem.taskFiles){
                taskItem.taskFiles = [];
              }
              
              taskFile = aa.findObjectByKey(taskItem.taskFiles, upload.data.taskfile_id, 'taskfile_id', null, true);
              if (!taskFile){
                taskFile = {};
                taskItem.taskFiles.push(taskFile);
              }
              
              // update taskFile with this upload's data
              $.extend(taskFile, {
                taskfile_id: upload.data.taskfile_id,
                taskitem_id: upload.data.taskitem_id,
                task_id: upload.data.task_id,
                filename: upload.data.server_file,
                filepath: upload.data.server_folder,
                instructions: upload.data.instructions,
                upload_date: upload.data.server_timestamp,
                upload_user: user.id
              });
              
            }
          }
          
        }
        
        var button = $(this._button);
        var output = ['<div id="upload', upload.id, '" upload="', upload.id, '" class="uploadStatus listitem '];
        var outputShort = ['<span class="'];
        // button.removeClass('buttonUploading').text('Upload Report');
        // //button.parent().removeClass('floatRight').css('margin', '1.5em 0 0 1.5em');
        // this.enable();

        // if (!button.parent().next('.list').next('.clearfix').length){
          // button.parent().next('.list').after('<div class="clearfix" />');
        // }
        
        if (upload.data.success){
          fblog(['Uploaded "', file, '" to "', upload.data.server_folder, '/', upload.data.server_file, '"'].join(''));
          output.push('icon-ok editable');
          outputShort.push('icon-ok');
          
          
          
        }
        else {
          fblog(['Error uploading "', file, (upload.data.server_folder ? ['" to "', upload.data.server_folder, '/', upload.data.server_file, '"'].join('') : '')].join(''));
          output.push('icon-error');
          outputShort.push('icon-error');
        }
        
        output.push(' iconlefttop2">');
        outputShort.push(' iconrighttop');
        
        // if (!upload.success && !upload.timestamp){
          // output.push('<span class="smallCaps light">Error uploading</span> ');
        // }
        
        output.push(file);
        
        output.push('<br /><span class="details">');
        
        // specific error message returned by server
        if (!upload.data.success && message){
          output.push('<span class="highlightRed">Error: </span>', message, '</span>');
          outputShort.push(' highlightRed">Error: ', message);
        }
        
        // no error message (but may still have success == false)
        // create generic output
        else if (upload.data.server_timestamp){
          if (upload.data.success){ // everything worked
            output.push('Uploaded');
          }
          else { // upload failed, but no error message
            output.push('Error uploading');
          }
          
          output.push(
            ' ',
            // upload.timestamp,
            dpExactDateTime(upload.data.server_timestamp).toString('MMMM dS @ h:mmt'), 
            ' by ',
            (upload.data.uploader = user.name)
            // '<br /><span class="smallCaps">Saved as</span> ', upload,
          );
          
          outputShort.push('">File uploaded');
        }
        
        output.push('</span></div>');
        outputShort.push('</span>');

        if (buttonDef.attach){
          // $(['#uploadStatus', buttonDef.unique].join('')).html(output.join(''));
          $(['#', formDef.id, ' div.uploadStatus[unique=', buttonDef.unique, ']'].join('')).html(outputShort.join(''));
        }
        // else {
          // button.parent().next('.itemlist').find('div.placeholder').remove().end().removeClass('hidden');
          // $(['#upload', upload.id].join('')).replaceWith(output.join(''));
          
          // var newEl = $(['#upload', upload.id].join(''));
          
          // itemEditableSetup({
            // editable: newEl,
            // id: upload.id,
            // editType: 'taskItem',
            // friendlyName: 'File Details',
            // store: shared.uploads,
            // keyName: 'id',
            // data: upload.data,
            // title: upload.data.local_file,
            // subtitle: ['<span class="smallCaps">Uploaded</span> ', dpExactDateTime(upload.data.server_timestamp).toString('MMMM dS @ h:mmt'), ' <span class="smallCaps">by</span> <b>', (upload.data.uploader), '</b>'].join(''),
            // editShow: itemEditShow,
            // editTargetAfter: $('#uploadContainer')
            // // function(el){
              // // fblog('editing ' + el.attr('upload'));
            // // }
          // });
        // }
        
      }
      
    });
    
    button.data('upload', ajaxUpload);
    
    if (buttonDef.enableCondition && typeof buttonDef.enableCondition == 'function' && !buttonDef.enableCondition(formDef, buttonDef)){
      ajaxUpload.disable();
    }
  }
  
  

  // send lookup command to autocomplete control
  function triggerLookup(config, event, element){
    config.target.focus();
    
    setTimeout(function(){
      // config.target.fastTrigger('click');
      config.target.trigger('lookup');
      var temp = config.target[0].value;
      config.target[0].value = '';
      config.target[0].value = temp;
    }, 100);
  }
  
  
  // function parseHTML(target, replacements, tokenStart, tokenEnd){
    // if (!tokenStart){
      // tokenStart = '^';
    // }
    // if (tokenStart && !tokenEnd){
      // tokenEnd = tokenStart;
    // }
    
    // for (old in replacements){
      // target = target.replace([tokenStart, old, tokenEnd].join(''), replacements[old]);
    // }
  
    // return target;
  // }

  
  
  // check input value against regex if present
  // check input value against values from other elements with same 'confirm' attribute
  function formValidateItem(element, invalidUnmarked){
    if (element && element.length){
    
      validating = true;
      
      //var thisEl = $(this);
      var parent = element.parents('item');
      var formEl = element.parents('form');
      var formId = formEl.attr('id');
      var formDef = forms[formId];
      var id = element.attr('id');
      if (!formDef){
        fblog(['no formDef for ', id, ' (parent = ', formId, ')'].join(''));
      }
      
      var fieldDef = aa.findObjectByKey(formDef.fields, id.replace(formDef.prefix, ''), 'name')[0]; //formDef.fields[id.replace(formDef.prefix, '')];
      if (!fieldDef){
        return;
      }
      
      var validationField = id.replace(formDef.prefix, '').replace('_confirm', '');
      
      var regex = formDef.validation.fields[validationField] ? formDef.validation.fields[validationField] :
          (element.hasClass('required') ? formDef.validation.required : formDef.validation.standard);
          
      switch (element.attr('titleCase')){
        case 'true':
          // element.val(as.toTitleCase(element.val()));
          break;
          
        case 'sentence':
          //element.val(as.toSentenceCase(element.val()));
          break;
      }
      

      var valid = true;
      //var legalVal = as.replaceAll(element.val(), '\\s', '');
      //element.val(legalVal);
      var val = element.val(); //legalVal;
      var confirm = element.attr('confirm');
      
      //fblog('validating ' + id + ' (' + val + ') against ' + regex);

      if (!(val == val.match(new RegExp(regex)))){
        //fblog("invalid, doesn't match regex");
        aa.pushUnique(formDef.invalid, id);
        if (!invalidUnmarked){
          parent.addClass('itemInvalid'); //css("border-color", "#f99");
          //fblog('invalid 1');
        }
        valid = false;
      }
      else {
        //fblog('valid');
      }
      
      // check if other confirm elements match this
      if (confirm){
        var confirmSame = $(['input[confirm=', confirm, ']'].join(''), formEl); 
        var mismatched = confirmSame
          .filter(function(){
            // return true if NOT a match, so that zero mismatches = pass
            return $(this).val() != val;
          });
          
        if (mismatched.length > 0) {
          if (!invalidUnmarked){
            confirmSame.each(function(){
              var thisEl = $(this);
              var thisId = thisEl.attr('id');
              if (thisId != shared.inputFocused){
                thisEl.parents('div.item').addClass('itemInvalid'); //css("border-color", "#f99");
              }
            });
            //fblog('invalid 2');
          }
            
          confirmSame.each(function(){
            aa.pushUnique(formDef.invalid, $(this).attr('id'));
            aa.pushUnique(formDef.blurred, $(this).attr('id'));
          });
          
          valid = false;
        }
      }


      // call callback function for additional validation
      if (!confirm && $.isFunction(fieldDef.validationCallback)){
        if (!fieldDef.validationCallback(formDef, val)){
          //fblog('validationCallback ' + id + ' = false');
          parent.addClass('itemInvalid');
          //fblog('invalid 3');
          valid = false;
        }
        else {
          //fblog('validationCallback ' + id + ' = true');
        }
      }
      

      if (valid !== false){
        var el = $((confirm ? ['input[confirm=', element.attr('confirm'), ']'].join('') : element), formEl);
        var elsToChange = el.parents('div.item'); //$('input.inputText, textarea.inputTextarea, td.inputLabelLeft', el.parent());
        elsToChange.removeClass('itemInvalid'); //.addClass('valid'); //css("border-color", "#bbb");
          
        el.each(function(){
          aa.removeByValue(formDef.invalid, $(this).attr('id'));
          //fblog('removed from invalid: ' + $(this).attr('id'));
        });

        // call callback function after validation successful
        if (!confirm && $.isFunction(fieldDef.whenValidCallback)){
          //var result = fieldDef.whenValidCallback(element);
          //fblog('whenValidCallback ' + id + ' = ' + result);
        }
        
      }
      else {
        //fblog('input invalid: ' + id + ', ' + regex);
      }

      var result = formValidate(formDef);
      
      validating = false;
      
      return result;
    }
    else {
      fblog('formValidateItem(): undefined element passed');
      //return false;
    }
  }

  
  
  // check valid state of all applicable form items
  function formValidate(formDef, validateAll){
    if (validateAll){
      $(['#', formDef.id, ' input.inputText, #', formDef.id, ' textarea.inputTextarea'].join('')).each(function(index, domEl){
        formValidateItem($(this));
        aa.pushUnique(formDef.blurred, this.id);
      });
      
    }
    
    if (!formDef.invalid.length){ //(invalid.length == 0){
      $(['#', formDef.prefix, 'buttonPost'].join('')).val(formDef.submitText);
      $(['#', formDef.prefix, 'messageInvalid'].join('')).hide(); //slideUp(100); //css('display', 'none');
      return true;
    }
    else {
      $(['#', formDef.prefix, 'buttonPost'].join('')).val(formDef.submitInvalidText); //.attr('disabled', 'disabled');
      $(['#', formDef.prefix, 'messageInvalid'].join('')).show(); //slideDown(100); //css('display', 'block');
    }
    return false;
  }
  
  
  
  // find task in tasks collection by task id
  function getTaskById(tasks, id){
    if (tasks.items){
      tasks = tasks.items;
    }
    
    return aa.findObjectByKey(tasks, id, 'data.id', null, true);
  }
  
  
  
  





  
























  
  
  
  
  
  
  
  function scheduleGet(pageId, precache){
    if (!precache){
      schedule.retrieving = true;
      scheduleBlock();
    }
    
    // // if date string (yyyy-mm-dd) passed
    // if (pageId && pageId.length && pageId.length == 10){
      // schedule.config.start = pageId;
      // delete pageId;
    // }
    
    setTimeout(function(){
      scheduleGetProc(pageId);
    }, 0);
  }
  
  
  // get schedule data via ajax
  function scheduleGetProc(pageId){
  
      var i, j, currentPage, prevConfig;
      
      currentPage = aa.findObjectByKey(schedule.pages, schedule.currentPage);
      if (currentPage.length){
        currentPage = currentPage[0];
      }
      
      prevConfig = {
        start: currentPage.start,
        end: currentPage.end,
        period: currentPage.period,
        view: currentPage.view
      };
      
      if (pageId === undefined && !scheduleEditHide({noSave: true})){
        scheduleUnblock();
        return schedule.retrieving = false;
      }
      
      $.extend(schedule.config, {
        target: 'jq-formstatic-inject', //forms[$(this).parents('form').attr('id')].addedInjectTarget
        view: schedule.preferences.display.view
      });
      
      if (pageId == 'next' || pageId == 'previous'){
        schedule.config.relationship = pageId;
        if (schedule.config.view == 'month'){
          schedule.config.start = dateString(dpExactDate([currentPage.period, '-01'].join('')).addMonths(pageId == 'next' ? 1 : -1));
        }
        else {
          schedule.config.start = dateString(dpExactDate(schedule.start).addWeeks(pageId == 'next' ? 1 : -1));
        }
        
        delete schedule.config.period;
        delete schedule.config.end;
        pageId = undefined;
      }
      
      scheduleDatesConvertToViewStart(schedule.config);
      
      var skip = false;
      
      // if (schedule.config.view == prevConfig.view && prevConfig.view == 'month' && schedule.config.period == prevConfig.period){
        
      // }
      
      if (
        skip
        ||
        (schedule.config.view == prevConfig.view && prevConfig.view == 'week' && schedule.config.start == prevConfig.start)
      ){
        fblog('requested currently displayed page, cancelling');
        schedule.config = prevConfig;
        scheduleUnblock();
        return schedule.retrieving = false;
      }
      
      
    
      if (pageId === undefined){
        pageId = scheduleAddPage(schedule.config);
        //scheduleGetPageById(pageId).physicist_id = schedule.preferences.display.physicists[0];
        schedule.selected.init();
      }
      
      var page = aa.findObjectByKey(schedule.pages, pageId, 'id')[0];
      var currentGet = aa.findObjectByKey(schedule.pages, true, 'retrieving');
      var currentPrecache = aa.findObjectByKey(schedule.pages, true, 'precache');
      
      for (i in currentPrecache){
        for (j in currentGet){
          if (currentPrecache[i].id == currentGet[i].id){
            currentGet.splice(i,1);
          }
        }
      }
      
      var removed;
      
      // if this is a live request, but there is already a live request in progress
      if (
        !page.precache
        &&
        currentGet.length
        &&
        schedule.config.start == currentGet[0].start //schedule.precache.start
        &&
        schedule.config.end == currentGet[0].end //schedule.precache.end
      ){
          removed = aa.removeObjectByKey(schedule.pages, pageId, 'id');
          fblog(['scheduleGet(currentGet): current request same as live get in progress, cancelling current (removed ', removed, ' page(s))'].join(''));
          return schedule.retrieving = false;
      }
      // if this is not a precache request, but there is a precache in progress
      else if (!page.precache && currentPrecache.length){
        // if the precache in progress matches the current request,
        // cancel the current request and allow the precache to complete as a normal request
        if (
          schedule.config.start == currentPrecache[0].start //schedule.precache.start
          &&
          schedule.config.end == currentPrecache[0].end //schedule.precache.end
        ){
          removed = aa.removeObjectByKey(schedule.pages, pageId, 'id');
          fblog(['scheduleGet(currentPrecache): current request same as precache in progress, cancelling current (removed ', removed, ' page(s))'].join(''));
          currentPrecache[0].precache = false;
          return schedule.retrieving = false;

        }
        else {
          fblog('scheduleGet(currentPrecache): current request doesn\'t match precache in progress, cancelling in progress');
          for (i in currentPrecache){
            currentPrecache[i].cancel = true;
          }
        }
      }
      else if (page.precache){
        schedule.config.start = page.start; //schedule.precache.start;
        schedule.config.end = page.end; //schedule.precache.end;
      }
      
      fblog([
        'scheduleGet',
        (page.precache ? ['(pageId = ', pageId, ')'].join('') : '()' ),
        (currentPrecache.length ? ['(', currentPrecache.length, ' item(s) in progress)'].join('') : ''),
        ': ',
        schedule.config.start, ' - ', schedule.config.end,
        (schedule.config.view ? [' (', schedule.config.view, ')'].join('') : ''),
        (schedule.preferences.display.physicists && schedule.preferences.display.physicists.length ? [' [', schedule.preferences.display.physicists.join(','), ']'].join('') : '')
      ].join(''));
      

    
      if (
        (!page.precache && scheduleEditHide({noSave: true})) // only continue if any open editors can be safely closed
        ||
        page.precache // or if this is a precache call (in which case open editors can remain open)
      ){ 
      
        // make sure to stop updater interval
        updaterStop();
        
        if (!page.precache){
          
          // deselect any selected cells
          schedule.selected.init();

          var injectTarget = $(['#', schedule.config.target].join(''));
          
          // if (forms.formSchedule.fields.length){
            // injectTarget.addClass('borderTop gapTop');
          // }
          
          
          // if (schedule.currentPage === null){
            // injectTarget.prevAll().remove().end().append('<div><br><br><br><br><br><br><br><br><br></div>');
            // //injectTarget.css('height', '300px');
          // }
          
          
        } // end !inProgress
        
        
        if (schedule.preferences.display.view == "month"){
          schedule.preferences.display.physicistsMonthly = schedule.preferences.display.physicists;
        }
        else if (schedule.preferences.display.view == "week"){
          schedule.preferences.display.physicistsWeekly = schedule.preferences.display.physicists;
        }
        
        // var params = {};
        var params = {
          preferences: JSON.stringify(schedule.preferences)
        };
        
        if (
          !schedule.config.view
          ||
          schedule.config.view != 'month'
        ){

          // determine if this date range has already been ajaxed
          var dateTemp = schedule.config.start; //dateString(dp(schedule.config.start));
          
          params.dates = [];
          
          // only do this if dates collection has already been created
          // loop while temp date is less than or equal to end date
          while (dateTemp <= schedule.config.end){
            if (
              !schedule.dates // if dates array doesn't exist
              ||
              !aa.existsIn(schedule.dates, dateTemp) // or if this date doesn't exist in dates collection
            ){
              params.dates.push(dateTemp); // add this date to array to be ajaxed
            }
            
            dateTemp = dateString(dpExactDate(dateTemp).addDays(1)); // increment day
          }
          
          params.dateStart = schedule.config.start;
          params.dateEnd = schedule.config.end;
          
        }
        else {
          params.weeks = [];
          // if (schedule.weeks && schedule.weeks.length){
            // for (i = schedule.config.weeks.length - 1; i >= 0; i--){
            for (i = 0; i < schedule.config.weeks.length; i++){
              var found = false;
              
              loop1:
              if (schedule.weeks && schedule.weeks.length){
                for (j = 0; j < schedule.weeks.length; j++){
                  if (
                    schedule.config.weeks[i].start == schedule.weeks[j].start
                    &&
                    schedule.config.weeks[i].end == schedule.weeks[j].end
                    &&
                    schedule.preferences.display.physicists.length == 1
                    && 
                    aa.findObjectByKey(schedule.weeks[j].physicists, schedule.preferences.display.physicists[0], 'physicist_id').length
                  ){
                    // schedule.config.weeks.splice(i, 1);
                    found = true;
                    schedule.config.weeks[i].get = true; //false;
                    //break loop1;
                  }
                }
              }
              
              var foundDates = [];
              if (schedule.dates && schedule.dates.length){
                for (j = 0; j < schedule.dates.length; j++){
                  if (schedule.config.weeks[i].start <= schedule.dates[j] && schedule.dates[j] <= schedule.config.weeks[i].end){
                    foundDates.push(schedule.dates[j]);
                  }
                }
                if (foundDates.length > 6){
                  found = true;
                }
              }
              
              if (!found){
                params.weeks.push(schedule.config.weeks[i]);
                schedule.config.weeks[i].get = true;
              }
              else {
                schedule.config.weeks[i].get = true; //false;
              }
            }
          // }
          params.dateStart = schedule.config.start;
          params.dateEnd = schedule.config.end;
          //params.weeks = schedule.config.weeks;
          params.physicists = JSON.stringify([schedule.preferences.display.physicists[0]]); //,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120 /* user.id */]);
        }
        
        // if some dates are missing, these need to be ajaxed
        if (
          (params.dates && params.dates.length)
          ||
          (
            params.dateStart && params.dateEnd && params.weeks
            &&
            (
              params.weeks.length
              ||
              !page.weeks
              ||
              !page.weeks.length
            )
          )
        ){
        
          if (params.dates){
            params.dates = JSON.stringify(params.dates);
          }
          
          if (params.weeks){
            params.weeks = JSON.stringify(schedule.config.weeks); // use schedule.config instead of just not found weeks
          }
          
          if (!shared.metaLoaded){
            params.getMeta = true;
          }
          
          if (!page.precache){
            schedule.retrieving = true;
          }
          
          page.retrieving = true;
          
          ad.stopwatchStart(['ajaxSchedule', params.dates || params.period].join(''));
          
          //params.revision = shared.revision;
          

          $.ajax({
            url: [shared.urlRoot, '/ajaxSSGet/scheduleGet', (page.precache ? 'Precache' : ''), '/'].join(''),
            //mode: 'abort',
            //port: 'scheduleGet',
            data: params,
            //type: 'POST',
            success: function (data, textStatus){
              fblog(['scheduleGet(): schedule data retrieved from server (', (ad.stopwatchEnd(['ajaxSchedule', params.dates || params.period].join('')) / 1000), 's)'].join(''));
              //fblog(textStatus);
              //schedulePage(schedule.config);
              scheduleParse(data, pageId);
              //scheduleUnblock(schedule.config);
            },
            error: function (XMLHttpRequest, textStatus, errorThrown){
              fblog(['scheduleGet(): schedule data retrieved from server (', (ad.stopwatchEnd(['ajaxSchedule', params.dates || params.period].join('')) / 1000), 's)'].join(''));
              fblog(['scheduleGet(): ajax error = ', textStatus, ' (', errorThrown, ')'].join(''));
            }
          });
          
        }
        else if (!page.precache){ // all needed data has already been ajaxed
        
          var existing;
          var existingTable;
          
          if (schedule.preferences.display.view == 'month'){
            existing = aa.findObjectByKeys(schedule.pages, {period: schedule.config.period, view: 'month'});
          }
          
          else {
            existing = aa.findObjectByKeys(schedule.pages, {start: schedule.config.start, view: 'week', rendered: true});
            // pageId only refers to a superfluous page in weekly view
            if (existing.length){
              removed = aa.removeObjectByKey(schedule.pages, pageId, 'id');
            }
          }
          
          if (existing.length){
          
            //removed = aa.removeObjectByKey(schedule.pages, pageId, 'id');
            fblog(['scheduleGet(): already ajaxed, getting prerendered page ', existing[0].id, ' from cache, cancelling current (removed ', removed, ' page(s))'].join(''));
            
            if (schedule.preferences.display.view == 'month'){
              existingTable = $('#currentPage.month');

              if (!existingTable.length){
                existingTable = $('#scheduleMonth');
                schedulePage();
                injectTarget.append(existingTable.attr('id', 'currentPage'));
              }
            }
            else {
              existingTable = $(['#schedulePage', existing[0].id].join(''));
              schedulePage();
              injectTarget.append(existingTable.attr('id', 'currentPage'));
            }

            schedule.currentPage = existing[0].id;
            schedule.currentPageElement = existingTable;
            
            scheduleOnDisplayPhysicistsChange(null, existing[0]);
            scheduleUpdateColumnCount();
            scheduleUpdateDisplayWeekends();
            
            //slideIn(existingTable, {direction: (schedule.config.relationship == 'previous' ? 'right' : 'left')});
            //existingTable.show();
            
            //injectTarget.append(existingTable.attr('id', 'currentPage').show());
            existing[0].checkedOut = true;
            scheduleUnblock();
            // fblog('stopwatch after get from cache: ' + ad.stopwatchSegment('scheduleGet'), 'ms');
          }
          else {
            fblog('scheduleGet(): rendering already ajaxed data');
            scheduleRender(pageId);
          }
          
        }
        
        return true;
        
      } // end if scheduleEditHide
      
      return schedule.retrieving = false;
      
  }
 
  
  
  
  
  function scheduleGetSpec(config){
    // return if necessary parameters not passed
    if (!config || !config.dates){
      return false;
    }
    
    var params = {
      dates: JSON.stringify(config.dates),
      tasksOnly: true,
      total: true
    };
    
    if (config.physicists){
      params.physicists = JSON.stringify(config.physicists);
    }
    
    ad.stopwatchStart('ajaxScheduleGetSpec');    
    
    $.ajax({
      url: [shared.urlRoot, '/ajaxSSGet/scheduleGetSpec'].join(''),
      //mode: 'abort',
      //port: 'scheduleGetSpec',
      data: params,
      //type: 'POST',
      success: function (data, textStatus){
        statusBannerHide();
        fblog(['scheduleGetSpec(): schedule data retrieved from server (', (ad.stopwatchEnd('ajaxScheduleGetSpec') / 1000), 's)'].join(''));
        // fblog(textStatus);
        // fblog(data);
        if (config.callback){
          config.callback(data);
        }
        //scheduleParse(data, pageId);
      },
      error: function (XMLHttpRequest, textStatus, errorThrown){
        fblog(['scheduleGetSpec(): schedule data retrieved from server (', (ad.stopwatchEnd('ajaxScheduleGetSpec') / 1000), 's)'].join(''));
        fblog(['scheduleGetSpec(): ajax error = ', textStatus, ' (', errorThrown, ')'].join(''));
        statusBannerConnectionError();
      }
    });
    
    
  }
  
  
  
  
  // convert start and end dates to configured week start day (sunday or monday)
  function scheduleDatesConvertToViewStart(config){
  
    var startDate = dpExactDate(config.start);
    var endDate;
    
    // calculate start/end dates based on full-month view
    if (config.view == 'month'){
      if (config.period){
        startDate = dpExactDate([config.period, '-01'].join(''));
      }
      else {
        config.period = startDate.toString('yyyy-MM');
      }
      
      config.weeks = [];
      
      startDate.set({day: 1});
      endDate = new Date(startDate);
      endDate.addMonths(1).addDays(-1);

      // get dates based on schedule.weekStartsOn parameter
      switch (schedule.weekStartsOn){
        case 'sunday':
          // if start date is a saturday, set start date to following sunday (to begin on next business week)
          // if (startDate.is().saturday()){
            // startDate.next().sunday();
          // }
          // otherwise if start date is NOT sunday (ie, is mon-fri), change to sunday
          if (!startDate.is().sunday()){
            startDate.last().sunday();
          }
          
          config.start = dateString(startDate);
          
          if (!endDate.is().saturday()){
            endDate.next().saturday();
          }
          
          config.end = dateString(endDate);
          break;
          
        // case 'monday':
          // // if start date is a weekend, set start date to following monday
          // if (startDate.is().saturday() || startDate.is().sunday()){
            // startDate.next().monday();
          // }
          // // otherwise if start date is NOT monday (ie, is tues-fri), change to monday
          // else if (!startDate.is().monday()){
            // startDate.last().monday();
          // }
          
          // config.start = dateString(startDate);
          
          // if (!endDate.is().sunday()){
            // endDate.next().sunday();
          // }
          
          // config.end = dateString(endDate);
          // break;
      }
      
      while (dateString(startDate) < dateString(endDate)){
        var weekStart = dateString(startDate);
        var weekEnd = dateString(startDate.addDays(6));
        config.weeks.push({
          start: weekStart,
          end: weekEnd
        });
        startDate.addDays(1);
      }
      
      
    }
    
    // calculate start/end dates based on full-week view
    else {
    
      // get dates based on schedule.weekStartsOn parameter
      switch (schedule.weekStartsOn){
        case 'sunday':
          // // if start date is a saturday, set start date to following sunday (to begin on next business week)
          // if (startDate.is().saturday()){
            // startDate.next().sunday();
          // }
          // otherwise if start date is NOT sunday (ie, is mon-fri), change to sunday
          if (!startDate.is().sunday()){
            startDate.last().sunday();
          }
          
          // set end to following saturday @ 11:59:59 PM
          config.start = dateString(startDate);
          config.end = dateString(startDate.next().sunday().addDays(-1));
          break;
          
        case 'monday':
          // // if start date is a weekend, set start date to following monday
          // if (startDate.is().saturday() || startDate.is().sunday()){
            // startDate.next().monday();
          // }
          // otherwise if start date is NOT monday (ie, is tues-fri), change to monday
          if (!startDate.is().monday()){
            startDate.last().monday();
          }
          
          // set end to following sunday @ 11:59:59 PM
          config.start = dateString(startDate);
          config.end = dateString(startDate.next().monday().addDays(-1));
          break;
      }
    }
  }
  
  
  
  // add new page metadata entry to pages array
  function scheduleAddPage(config){
    var page, pageId;
    if (config.period){
      page = aa.findObjectByKey(schedule.pages, config.period, 'period');
      if (page.length){
        return page[0].id;
      }
    }
    
    pageId = schedule.maxPage++;
    schedule.pages.push({
      start: config.start, //schedule.config.start, // dateObject.start, //schedule.start.valueOf(),
      end: config.end, //schedule.config.end, //dateObject.end, //schedule.end.valueOf(),
      checkedOut: false,
      view: config.view,
      physicist_id: config.physicist_id || null,
      period: config.period || null,
      id: pageId
    });
    
    return pageId;
  }

        

  // hide and move currently rendered schedule table to end of html body for later use
  function schedulePage(){
    
    if (schedule.currentPage || schedule.currentPage === 0){
      schedule.yPos = $(window).scrollTop() / ($(document).height() - $(window).height());
      // fblog(['schedulePage(): page ', schedule.currentPage, ' currently displayed, moving to cache'].join(''));
      
      var currentPageEl = $('#currentPage');
      
      if (currentPageEl.hasClass('month')){
        schedulePageMoveToBodyEnd($('#currentPage').attr('id', 'scheduleMonth'), schedule);
      }
      // if (schedule.preferences.display.view != 'month'){
      else {
        schedulePageMoveToBodyEnd($('#currentPage').attr('id', ['schedulePage', schedule.currentPage].join('')), schedule);
      }
    }
    else {
      // fblog('schedulePage(): no page currently displayed');
      //injectTarget.html('<table><tr><td><p><img src="/common/assets/images/large-loading.gif" align="absmiddle"/>&nbsp;Loading...</p></td></tr></table>');
    }  
    
    schedule.start = schedule.config.start;
    schedule.end = schedule.config.end;  
  }
  
  
  
  // move passed content to end of document,
  // and add metadata to pages array
  function schedulePageMoveToBodyEnd(content, dateObject){
    // var pageId = schedule.maxPage++;

    var existing = aa.findObjectByKey(schedule.pages, dateObject.start, 'start');
    //var pageId = content.attr('page');
    
    if (!existing.length){
      fblog(['schedulePageMoveToBodyEnd(): page (', dateObject.start, ') not in schedule.pages array, unable to process'].join(''));
      return;
    }
    
    existing = existing[0];
    
    //if (schedule.preferences.display.view == 'month'){
      scheduleDim(schedule.dimmed, true);
    //}
    //schedule.dimmed = scheduleDim($(['th.taskheader.colth[period!=', page.period, '], td.task[period!=', page.period, ']'].join(''), schedule.currentPageElement));
    
    // if (existing.toDim){
      // scheduleDim(existing.toDim, true);
    // }
    // else {
      // fblog(['currentPage (', existing.id, ') does not contain toDim property'].join(''));
    // }
    
    content/*.hide()*/.appendTo('#hidden_container'); // hide and move to end of DOM
    existing.checkedOut = false;
  }
  
  
  
  // block schedule from input, show loading mask
  function scheduleBlock(){
    var inject = $('#jq-form-inject');
    
    inject.block({
      message: [
        // 'test'
        '<p style="margin-bottom: 20em;"><img src="/common/assets/images/large-loading.gif" align="absmiddle"/>&nbsp;Loading...</p>',
        '<p style="margin-bottom: 20em;"><img src="/common/assets/images/large-loading.gif" align="absmiddle"/>&nbsp;Loading...</p>',
        '<p style="margin-bottom: 20em;"><img src="/common/assets/images/large-loading.gif" align="absmiddle"/>&nbsp;Loading...</p>',
        '<p style="margin-bottom: 20em;"><img src="/common/assets/images/large-loading.gif" align="absmiddle"/>&nbsp;Loading...</p>',
        '<p style="margin-bottom: 20em;"><img src="/common/assets/images/large-loading.gif" align="absmiddle"/>&nbsp;Loading...</p>',
        '<p style="margin-bottom: 20em;"><img src="/common/assets/images/large-loading.gif" align="absmiddle"/>&nbsp;Loading...</p>',
        '<p style="margin-bottom: 20em;"><img src="/common/assets/images/large-loading.gif" align="absmiddle"/>&nbsp;Loading...</p>',
        '<p style="margin-bottom: 20em;"><img src="/common/assets/images/large-loading.gif" align="absmiddle"/>&nbsp;Loading...</p>',
        '<p style="margin-bottom: 20em;"><img src="/common/assets/images/large-loading.gif" align="absmiddle"/>&nbsp;Loading...</p>',
        '<p style="margin-bottom: 20em;"><img src="/common/assets/images/large-loading.gif" align="absmiddle"/>&nbsp;Loading...</p>'
      ].join('')
    });
    
    $('#page-title-date').hide();
    $('#page-title-date-loading').show();
    
    schedule.blocked = true;
  }

  // perform any last-minute formatting (updating page heading, hiding/showing weekend columns),
  // then hide schedule loading mask
  function scheduleUnblock(){
    var startDate = dpExactDate(schedule.start);
    var endDate = dpExactDate(schedule.end);
    
    // if (schedule.selectedRow){
      // scheduleSelectCells({row: schedule.selectedRow});
    // }

    schedule.precache.complete = false;
    schedule.precache.offset = 0;
    var page;
    
    if (schedule.preferences.display.view == 'month'){
      page = aa.findObjectByKeys(schedule.pages, {period: schedule.config.period, view: 'month'})[0];
    }
    else {
      page = aa.findObjectByKeys(schedule.pages, {start: schedule.start, view: 'week'})[0];
    }
    
    if (!page){
      fblog('scheduleUnblock(): unable to find current page');
    }

    // schedule.currentPage = page.id;
    // if (schedule.preferences.display.view != 'month'){
      // schedule.currentPageElement = $(['#schedulePage', schedule.currentPage].join(''));
      // schedule.currentPageElement.attr('id', 'currentPage');
    // }
    // else {
      // schedule.currentPageElement = $('#currentPage');
    // }
    
    // update page title
    if (page.view == 'month'){
      $('#page-title-dynamic').html([
        dpExact(page.period, 'yyyy-MM').toString('MMMM yyyy')
      ].join(''));
    }
    else {
      $('#page-title-dynamic').html([
        startDate.toString('MMMM d'),
        (startDate.toString('yyyy') != endDate.toString('yyyy') ? [', ', startDate.toString('yyyy')].join('') : ''),
        ' - ',
        (startDate.toString('MMMM') != endDate.toString('MMMM') ? endDate.toString('MMMM') : ''),
        endDate.toString(' d'),
        ', ',
        endDate.toString('yyyy')
      ].join(''));
    }

    scheduleTableUpdateSideButtons();
    
    if (page.view == 'month'){
      
      scheduleReDim();
      // scheduleDim(schedule.dimmed, true);
      // schedule.dimmed = scheduleDim($(['th.taskheader.colth[period!=', page.period, '], td.task[period!=', page.period, ']'].join(''), schedule.currentPageElement));
      
    }
    else {
      // $('th[scope=row]', schedule.currentPageElement).show();
      $(window).scrollTop(($(document).height() - $(window).height()) * schedule.yPos);    
    }
    
    $('#page-title-date-loading').hide();
    $('#page-title-date').show();
    
    $('#jq-form-inject').unblock();
    schedule.blocked = false;
    
    if (schedule.selectAfterLoad){
      schedule.selectAfterLoad = false;
      schedule.selected.init();
      scheduleSelectCells({
        origin: {
          row: page.view == 'month' ? mouseDown.weekIndex : mouseDown.row,
          user: mouseDown.user,
          column: mouseDown.column,
          timeblock: mouseDown.timeblock
        },
        afterLoad: true,
        onKeydown: true
      });
    }
    
    schedule.retrieving = false;
    
    updaterStart('scheduleGetUpdated', 'scheduleSaveDirty');
    
    schedule.blockInput = false;
    
    scheduleUpdateMaximized();
    scheduleUpdateHighlightRecent();
    scheduleUpdateToday();

    
    scheduleUpdateLinkPrint();
    
    
    page.element.show();
    

    //document.body.style.cursor = 'default';
    //$(body).css('cursor', 'default');
  }
    
  
  
  // get array of timestamps for weekend dates between current start and end dates
  function scheduleGetWeekends(obj){
    var dateTemp = dpExactDate(obj.start);
    var endDate = dpExactDate(obj.end);
    //var dateTemp = startDate.clone();
    var weekendDates = [];
    
    // only do this if dates collection has already been created
    // loop while temp date is less than or equal to end date
    while (dateTemp.compareTo(endDate) <= 0){
      //fblog(dateTemp.valueOf());
      
      if (
        dateTemp.is().saturday()
        ||
        dateTemp.is().sunday()
      ){
        weekendDates.push(dateString(dateTemp)); // add this date to array to be ajaxed
      }
      
      dateTemp.addDays(1); // increment day
    }
    
    return weekendDates;
  }

  
  // calculate width for each schedule column based on total number of visible columns
  function scheduleGetColumnWidth(columns){
    if (schedule.preferences.display.weekends){
      return Math.round(10000 / (columns)) / 100;
    }
    else {
      return Math.round(10000 / (columns - 2)) / 100;
    }
  }


  
  
  
  
  
  
  
  
  
  function scheduleToggleLock(config){
    config.element = config.element || $(['#', config.fieldDef.id].join(''));
    
    config.period = billing.start.substr(0, 7);
    
    config.isLocked = scheduleIsLocked(config.period);
    
    var params = {};
    var statusEl = $('#billingLockStatus');
    
    if (config.isLocked){
    
      if (config.period >= schedule.lock){
      
        var previousHighestLock = schedule.lock;
        
        do {
          previousHighestLock = dpExact(previousHighestLock, 'yyyy-MM').addMonths(-1).toString('yyyy-MM');
          if (aa.existsIn(schedule.lock_exceptions, previousHighestLock)){
            aa.removeByValue(schedule.lock_exceptions, previousHighestLock);
          }
          else {
            break;
          }
        } while (true);
        
        params.schedule_lock = previousHighestLock;
        params.schedule_lock_exceptions = schedule.lock_exceptions.join(',');
      }
      else {
        params.schedule_lock = schedule.lock;
        params.schedule_lock_exceptions = schedule.lock_exceptions.slice(0);
        params.schedule_lock_exceptions.push(config.period);
        params.schedule_lock_exceptions.sort().reverse();
        params.schedule_lock_exceptions = params.schedule_lock_exceptions.join(',');
      }
      
      statusEl.addClass('highlightLight').removeClass('highlightWarning').text('UNLOCKING...');
    }
    else {
      // don't lock if this period is present or future
      // only lock the current month if less than 5 days remaining in month (allows for closing billing before long holiday weekends at end of month)
      if (config.period >= Date.today().addDays(5).toString('yyyy-MM')){
        fblog('scheduleToggleLock(): lock prevented (possibly accidental?), this is a current or future month');
        return;
      }
      
      if (config.period >= schedule.lock){
        params.schedule_lock = config.period;
        params.schedule_lock_exceptions = '';
      }
      else {
        params.schedule_lock = schedule.lock;
        params.schedule_lock_exceptions = schedule.lock_exceptions.slice(0);
        aa.removeByValue(params.schedule_lock_exceptions, config.period);
        params.schedule_lock_exceptions = params.schedule_lock_exceptions.join(',');
      }
      
      statusEl.addClass('highlightLight').removeClass('highlight').text('LOCKING...');
    }
    
    $.ajax({
      url: [shared.urlRoot, '/ajaxSSGet/scheduleSetConfig'].join(''),
      data: params,
      success: function (data, textStatus){
        fblog('scheduleToggleLock(): success');
        
        try {
          var decoded = eval(["(", data, ")"].join(''));
          
          if (decoded.success === false){
            fblog(decoded.message);
            if (decoded.auth === false){
              window.location.reload(); //href = "/login/clients";
            }
            return false;
          }
          
        }
        catch (err){
          fblog(['scheduleToggleLock() decode failure: ', err.name, ', ', err.message].join(''));
          return;
        }
        
        scheduleParseLock(decoded.data);
        billingLockUpdateStatus();
        buttonUpdate(config);
      },
      error: function (XMLHttpRequest, textStatus, errorThrown){
        fblog(['scheduleToggleLock(): ajax error = ', textStatus, ' (', errorThrown, ')'].join(''));
      }
    
    });
  }
  
  
  
  
  // toggle weekend display flag
  function scheduleToggleWeekends(config){
    schedule.preferences.display.weekends = !schedule.preferences.display.weekends;
    buttonUpdate(config);
    scheduleUpdateColumnCount();
    scheduleUpdateDisplayWeekends();
    // scheduleUpdateLinkPrint();
    
    scheduleSavePreferences();
    
  }
  
  
  // toggle weekend display flag
  function scheduleToggleMaximized(config){
    schedule.preferences.display.maximized = !schedule.preferences.display.maximized;
    buttonUpdate(config);
    scheduleUpdateMaximized();
    
    scheduleSavePreferences();
    
  }
  
  
  // toggle recent change highlighting
  function scheduleToggleHighlightRecentChanges(config){
    schedule.preferences.display.recentHighlight = !schedule.preferences.display.recentHighlight;
    buttonUpdate(config);
    scheduleUpdateHighlightRecent();
    
    scheduleSavePreferences();
    
  }
  
  
  // toggle recent change highlighting
  function scheduleToggleCondensed(config){
    schedule.preferences.display.condensed = !schedule.preferences.display.condensed;
    buttonUpdate(config);
    scheduleUpdateCondensed();
    
    scheduleSavePreferences();

  }
  
  
  // switch between weekly and monthly views
  function scheduleToggleView(config){
    if (schedule.preferences.display.view == 'week'){
      schedule.preferences.display.view = 'month';
      //schedule.preferences.display.physicists = schedule.preferences.display.physicistsMonthly;
      // schedule.preferences.display.physicistsWeekly = schedule.preferences.display.physicists.slice();
    }
    else {
      schedule.preferences.display.view = 'week';
      //schedule.preferences.display.physicists = schedule.preferences.display.physicistsWeekly;
      // schedule.preferences.display.physicistsMonthly = schedule.preferences.display.physicists.slice();
    }
    
    if (!config){
      config = {
        element: $('#sc_toggleView'),
        fieldDef: forms.scheduleControls.buttons.toggleView
      };
    }
    scheduleViewConfig();
    buttonUpdate(config);
    scheduleOnDisplayPhysicistsChange();
    // scheduleUpdateView();
    
    datepickerHide();
    
    scheduleSavePreferences();
  }
  
  
  // ensure settings are correct before updating view
  function scheduleViewConfig(config){
    var spd = schedule.preferences.display;
    var sf = schedule.config;
    
    if (spd.view == 'month'){
      // spd.physicistsWeekly = spd.physicists.slice();
      
      if (
        !(spd.physicistsMonthly && spd.physicistsMonthly.length)
        ||
        spd.physicistsMonthly < 0
      ){
        //spd.physicistsWeekly = spd.physicists.slice();
        if (shared.physicistsById[user.id]){
          spd.physicistsMonthly = [user.id];
        }
        else if (spd.physicists && spd.physicists.length && spd.physicists[0] > 0) {
          spd.physicistsMonthly = [spd.physicists[0]];
        }
        else {
          spd.physicistsMonthly = [103];
        }
      }
      
      spd.physicists = spd.physicistsMonthly;
    }
    
    else if (spd.view == 'week'){
      // spd.physicistsMonthly = spd.physicists.slice();
      
      if (sf.period){
        sf.start = [sf.period, '-01'].join('');
        delete sf.period;
      }
      
      if (!(spd.physicistsWeekly && spd.physicistsWeekly.length)){
        if (spd.physicists && spd.physicists.length){
          spd.physicistsWeekly = spd.physicists.slice();
          // // physicistsWeekly already set
          // if (spd.physicistsWeekly && spd.physicistsWeekly.length){
            // // spd.physicists = spd.physicistsWeekly.slice();
          // }
          // // more than one physicist selected
          // if (spd.physicists.length > 1){
          // }
          // // group selected
          // else if (spd.physicists[0] < 0){
          // }
        }
        // single physicist selected, or nothing set
        else {
          spd.physicistsWeekly = [-1];
        }
      }
      
      spd.physicists = spd.physicistsWeekly;
    }

    // update state of dropdown
    if ($('#scheduleControls').length){
      $(['#scheduleControls [name=displayphysicist]'].join('')).attr('checked', false);    
      for (var i = 0; i < spd.physicists.length; i++){
        $(['#scheduleControls [name=displayphysicist][value=', spd.physicists[i], ']'].join('')).attr('checked', true);
      }
      var dropdownId = [forms.scheduleControls.prefix, 'displayphysicist'].join('');
      $(['#', dropdownId].join('')).next('.multiSelectOptions').multiSelectUpdateSelected(aa.findObjectByKey(forms.scheduleControls.dropdowns, dropdownId)[0].config);
    }
    
  }
    
  function scheduleUpdateCondensed(){
    //if (!schedule.blocked){
      if (schedule.preferences.display.condensed){
        $('#currentPage').addClass('scheduleCondensed');
      }
      else {
        $('#currentPage').removeClass('scheduleCondensed');
      }
    //}
  }
  
  // maximize or unmaximize schedule table based on currently set flag
  function scheduleUpdateMaximized(){
    if (schedule.preferences.display.maximized){
      $('#viewport_middle > div').removeClass('widthCap');
    }
    else {
      $('#viewport_middle > div').addClass('widthCap');
    }
  }
  
  
  
  function scheduleUpdateToday(){
    $('th[scope=col].coltoday').removeClass('coltoday');
    $(['th[scope=col][column=', schedule.today, ']'].join('')).addClass('coltoday');
  }
  
  
  
  
  // show or hide weekend columns based on currently set flag
  function scheduleUpdateDisplayWeekends(){
    //if (!schedule.blocked){
      var weekendDates = scheduleGetWeekends(schedule);
      var pageEl = $('#currentPage');
      var page = scheduleGetPageById(schedule.currentPage);
      // var widthOffset = page.view == 'month' ? 0 : 1; // add extra column for row headers in weekly view

      //scheduleUpdateColumnCount(page);
      
      // update colspan for divider rows
      // var dividers = $('tbody > tr.divider > td[colSpan]', pageEl).not('.buttonTableSide');
      // var dividers = $('tbody > tr.divider > td[colSpan]', pageEl).not('.buttonTableSide');
      
      // show or hide columns as appropriate
      // for (var i in weekendDates){
        // var weekendElements = $(['tbody > tr > td[column=', weekendDates[i], '], tbody > tr > th[column=', weekendDates[i], ']'].join(''), pageEl);
        var weekendElements = $(['tbody > tr > td.weekend, tbody > tr > th.weekend'].join(''), pageEl);
        if (schedule.preferences.display.weekends){
          weekendElements.show();
          
          // hide hidden weekend task indicators
          $('td.taskHiddenSiblingRight').removeClass('taskHiddenSiblingRight');
          $('td.taskHiddenSiblingLeft').removeClass('taskHiddenSiblingLeft');
          
          if (page.view == 'month'){
            scheduleReDim();
          }
          
        }
        else {
          weekendElements.hide();
          
          // show hidden weekend task indicators
          var rows = $('table.schedule tr.timeblockRowFirst');
          rows.each(function(){
            var
              count = this.cells.length,
              sunday = $(this.cells[1]), // cells[0] is TH
              sundayAfternoon = $(['#', sunday.attr('id').replace(/morning/, 'afternoon')].join('')),
              monday = $(this.cells[2]),
              mondayAfternoon = $(['#', monday.attr('id').replace(/morning/, 'afternoon')].join('')),
              friday = $(this.cells[count - 2]),
              fridayAfternoon = $(['#', friday.attr('id').replace(/morning/, 'afternoon')].join('')),
              saturday = $(this.cells[count - 1]),
              saturdayAfternoon = $(['#', saturday.attr('id').replace(/morning/, 'afternoon')].join(''));
            
            if (!sunday.hasClass('taskempty') || !sundayAfternoon.hasClass('taskempty')){
              monday.addClass('taskHiddenSiblingLeft');
              mondayAfternoon.addClass('taskHiddenSiblingLeft');
            }
            
            if (!saturday.hasClass('taskempty') || !saturdayAfternoon.hasClass('taskempty')){
              friday.addClass('taskHiddenSiblingRight');
              fridayAfternoon.addClass('taskHiddenSiblingRight');
            }
          });
        }
      // }
      
      // if (schedule.selectedRow){
        // scheduleSelectCells({row: schedule.selectedRow});
      // }
      
      // update colspan for task editor if currently shown
      var taskEditCell = $('#taskEditRow > td');
      if (taskEditCell.length){
        // calculate total number of columns minus (2 navigation buttons on table sides) minus (number of weekend dates if they are currently not shown)
        taskEditCell.attr('colSpan', (pageEl[0].rows[1].cells.length - (schedule.preferences.display.weekends ? 0 : 2) - (schedule.preferences.display.view == 'month' ? 1 : 0)));
      }
      
      
      
      // update column widths
      $('tbody > tr > th[scope=col][column]', pageEl).css('width', [scheduleGetColumnWidth($('#currentPage > tbody > tr.dateHeader:first > th[scope=col][column]').length), '%'].join(''));
      //$('#currentPage #colgroupWeekdays col').css('width', [scheduleGetColumnWidth($('#currentPage > tbody > tr:first > th[scope=col][column]').length), '%'].join(''));
      
      scheduleUpdateHighlightRecent();
      scheduleUpdateLinkPrint();
    //}
  }
  
  
  
  
  function scheduleUpdateColumnCount(page){
    if (!page){
      page = scheduleGetPageById(schedule.currentPage);
    }
    
    var widthOffset = (page.view == 'month' ? 0 : 1); // add extra column for row headers in weekly view

    // update colspan for divider rows
    var dividers = $('tbody > tr.divider > td.dividercell', page.element);
    
    if (schedule.preferences.display.weekends){
      dividers.attr('colSpan', 7 + widthOffset);
    }
    else {
      dividers.attr('colSpan', 5 + widthOffset);
      // $(dividers[0]).attr('colSpan', 5 + widthOffset);
      // for (var i = 0; i < dividers.length; i++){
        // $(dividers[i]).attr('colSpan', 5 + widthOffset);
      // }
    }

  }
  
  // update highlighting for recent changes
  function scheduleUpdateHighlightRecent(){
    if (!schedule.blocked){
  
      var i, tasks;
    
    
      // // remove any existing highlights
      
      // yellow border on recent tasks
      if (schedule.highlightTasks.length){
        for (i in schedule.highlightTasks){
          $(['#task', schedule.highlightTasks[i].physicist_id, '-', schedule.highlightTasks[i].weekStart, schedule.highlightTasks[i].timeblock, schedule.highlightTasks[i].schedule_start_date].join('')).removeClass('taskRecent');
        }
      }
      // dim non-recent tasks
      // if (schedule.highlightElements){
        // // schedule.highlightElements.removeClass('taskRecent');
        // $('#currentPage td.task').not(schedule.highlightElements).fadeTo(0, 1); //.css('background-color', '#fbb');
        // delete schedule.highlightElements;
      // }
      
      
      
      // if recent highlight should be shown
      if (schedule.preferences.display.recentHighlight){
        //'Changes made <b>since your previous session</b> are <span class="backgroundAttention highlightAttention">highlighted</span>'
        var rangeEnd = Date.today();
        var rangeStart = Date.today().addDays(-1 * schedule.highlightRecentRange);
        var currentPageObject = aa.findObjectByKey(schedule.pages, schedule.currentPage, 'id');
        var highlightCurrent, highlightDeleted;
        var highlightElements = $([]);
        
        
        
        if (currentPageObject.length){
          currentPageObject = currentPageObject[0];
          tasks = currentPageObject.tasks;
        }
        else {
          return;
        }
        
        // look for recent changes
        highlightCurrent = aa.findObjectByKey(
          tasks, //aa.findObjectByKey(schedule.pages, schedule.currentPage, 'id')[0].tasks,
          {
            end: [dateString(rangeEnd), ' 23:59:59'].join(''), //'2008-11-24 23:59:59'
            start: [dateString(rangeStart), ' 00:00:00'].join('') //'2008-11-19 00:00:00',
          },
          'data.audit_start'
        );

        if (highlightCurrent.length){
          for (i in highlightCurrent){
            highlightCurrent[i] = highlightCurrent[i].data;
          }
          
          schedule.highlightTasks = highlightCurrent;
        }
        else {
          schedule.highlightTasks = [];
        }
        
        highlightDeleted = aa.findObjectByKey(
          currentPageObject.deleted,
          {
            end: [dateString(rangeEnd), ' 23:59:59'].join(''), //'2008-11-24 23:59:59'
            start: [dateString(rangeStart), ' 00:00:00'].join('') //'2008-11-19 00:00:00',
          },
          'audit_end'
        );
        
        if (highlightDeleted.length){
          schedule.highlightTasks = schedule.highlightTasks.concat(highlightDeleted);
        }
        
        
        // if recent changes found, highlight them
        // if (schedule.highlightTasks.length){
          for (i in schedule.highlightTasks){
            var id = ['#task', schedule.highlightTasks[i].physicist_id, '-', schedule.highlightTasks[i].weekStart, schedule.highlightTasks[i].timeblock, schedule.highlightTasks[i].schedule_start_date].join('');
            // var cell = $(id);
            highlightElements = highlightElements.add(id);
            // cell.addClass('taskRecent');
            
            // if (
              // schedule.highlightTasks[i].data.audit_user != 1
              // &&
              // schedule.highlightTasks[i].data.audit_user != 122
              // &&
              // schedule.highlightTasks[i].data.audit_user != 123
            // ){
              // $(['#task', schedule.highlightTasks[i].data.physicist_id, '-', schedule.highlightTasks[i].data.weekStart, schedule.highlightTasks[i].data.timeblock, schedule.highlightTasks[i].data.schedule_start_date].join('')).addClass('taskRecent');
            // }
            // else {
              // $(['#task', schedule.highlightTasks[i].data.physicist_id, '-', schedule.highlightTasks[i].data.weekStart, schedule.highlightTasks[i].data.timeblock, schedule.highlightTasks[i].data.schedule_start_date].join('')).addClass('taskRecent');
            // }
          }
          
          
          
          // yellow border on recent tasks
          highlightElements.addClass('taskRecent');



          
          // dim non-recent tasks
          
          // highlightElements
            // .filter(':visible')
            // .not('.toDim').fadeTo(0, 1)
            // .end()
            // .filter('.toDim').fadeTo(0, .5);
            
          // // fix cleartype in IE
          // if (!$.support.opacity){
            // highlightElements.not('.toDim').each(function(){
              // this.style.removeAttribute('filter');
            // });
          // }

          // $('#currentPage td:not(.taskRecent, .buttonTableSide)')
            // .filter(':visible')
            // .not('.toDim').fadeTo(0, .125)
            // .end()
            // .filter('.toDim').fadeTo(0, .125);



            
          schedule.highlightElements = highlightElements;
        // }
        
      }
      else {
      
      

        // un-dim all tasks
      
        // $('#currentPage td')
          // .filter(':visible')
          // .not('.toDim').fadeTo(0, 1)
          // .end()
          // .filter('.toDim').fadeTo(0, .5);

        // // fix cleartype in IE
        // if (!$.support.opacity){
          // // $('#currentPage td, #currentPage th').not('.toDim').each(function(){
          // $('#currentPage td').not('.toDim').each(function(){
            // this.style.removeAttribute('filter');
          // });
        // }

        
        
        
      }
    }
    
  }






  function scheduleUpdateLinkPrint(){
    var linkSchedulePrint = $('#linkSchedulePrint');
    var linkScheduleMonths = [];
    var i;
    var linkScheduleInnerArr = [];
    var linkScheduleOuterArr = [];
    
    var startDate, endDate;
    
    var skipTop = false;
    
    var page = aa.findObjectByKey(schedule.pages, schedule.currentPage);
    
    if (page.length){
      page = page[0];
    }
    else {
      return;
    }
    
    if (page.view == 'month'){
      startDate = dpExact(page.period, 'yyyy-MM');
      linkScheduleMonths.push({
        monthName: startDate.toString('MMMM'),
        monthNum: startDate.toString('MM'),
        year: startDate.toString('yyyy')
      });
    }
    else {
    
      startDate = dpExactDate(schedule.start);
      endDate = dpExactDate(schedule.end);
  
      linkScheduleMonths.push({
        monthName: startDate.toString('MMMM'),
        monthNum: startDate.toString('MM'),
        year: startDate.toString('yyyy')
      });
      linkScheduleMonths.push({
        monthName: endDate.toString('MMMM'),
        monthNum: endDate.toString('MM'),
        year: endDate.toString('yyyy')
      });
      
      if (linkScheduleMonths[1].monthName == linkScheduleMonths[0].monthName){
        linkScheduleMonths.pop();
      }
    }

    for (i = 0; i < linkScheduleMonths.length; i++){
      linkScheduleInnerArr.push('<a href="/schedule.pdf?print&date=', linkScheduleMonths[i].year, '-', linkScheduleMonths[i].monthNum);
      
      
      if (schedule.preferences.display.physicists[0] == [-1]){
      }
      else if (schedule.preferences.display.physicists[0] == [-2]){
        linkScheduleInnerArr.push('&diagnostic');
      }
      else if (schedule.preferences.display.physicists[0] == [-3]){
        linkScheduleInnerArr.push('&therapy');
      }
      else {
        linkScheduleInnerArr.push(['&physicist=', schedule.preferences.display.physicists.join(',')].join(''));
        if (schedule.preferences.display.physicists.length == 1){
          skipTop = true;
        }
      }
      
      
      if (schedule.preferences.display.weekends){
        linkScheduleInnerArr.push('&weekends');
      }
      
      var index = aa.indexOfObjectByKey(shared.physicists, user.id, 'physicist_id');
      if (index.length && !skipTop){
        linkScheduleInnerArr.push(['&top=', user.id].join(''));
      }

      
      linkScheduleInnerArr.push('" target="_blank">Print ', linkScheduleMonths[i].monthName, ' schedule</a>');
      linkScheduleOuterArr.push(linkScheduleInnerArr.join(''));
      linkScheduleInnerArr = [];
    }
      
    linkSchedulePrint.html(linkScheduleOuterArr.join(' | ')).show();
    
  }

  

  
  // fade passed jQuery object
  // or restore opacity if undo == true
  function scheduleDim (input, undo){
    if (input && input.length){
      var toDim = input.filter(':visible');
      if (undo){
      
        // doesn't seem to fix cleartype in IE when attr removed via jQuery
        // toDim.fadeTo(0, 1).removeAttr('filter');
        
        toDim.each(function(){
          $(this).fadeTo(0, 1).removeClass('toDim');
          
          // fix cleartype in IE
          if (!$.support.opacity){
            this.style.removeAttribute('filter');
          }
        });
        
        // toDim.filter(); // remove all elements
        toDim = $([]);
      }
      else {
        toDim.fadeTo(0, 0.5).addClass('toDim');
      }
      
      return toDim;
    }
  }
  
  function scheduleReDim(){
    var page = scheduleGetPageById(schedule.currentPage);
    scheduleDim(schedule.dimmed, true);
    schedule.dimmed = scheduleDim($(['th.taskheader.colth[period!=', page.period, '], td.task[period!=', page.period, ']'].join(''), schedule.currentPageElement));
  }
  
  
  
  
  




  
  
  

  
  // parse ajaxed schedule data
  // imports and prepares data for processing and display
  function scheduleParse (data, pageId) {
  
    ad.stopwatchStart('scheduleParse');
    
    var page = aa.findObjectByKey(schedule.pages, pageId, 'id')[0];
    var spdp = schedule.preferences.display.physicists;
    
    var i;
    
    if (!page){
      return;
    }
    
    if (page.cancel){
      scheduleDeletePage(page.id);
      return;
    }

    //fblog('before eval');

    var decoded = shared.lastJSON.schedule = eval(["(", data, ")"].join(''));
    
    if (decoded.success === false){
      fblog(decoded.message);
      if (decoded.auth === false){
        window.location.reload(); //href = "/login/clients";
      }
      return false;
    }

    if (typeof(decoded) != 'object'){
      throw {name: 'AjaxError', message: decoded};
      //return;
    }
    
    scheduleParseLock(decoded);

    
    if (!shared.tasks){
      shared.tasks = new dataCollection({dataPrototype: taskObject});
    }

    
    // if this page is a monthly view, parse data by weeks
    if (page.view == 'month'){
      var week;
      var physicist;
      
      if (!schedule.weeks){
        schedule.weeks = []; //decoded.weeks;
      }
      
      if (!page.weeks){
        page.weeks = [];
      }
      
      if (!page.tasks){
        page.tasks = [];
      }
      
      if (!page.deleted){
        page.deleted = [];
      }
      
      for (week = 0; week < decoded.weeks.length; week++){
        var weekExists = aa.indexOfObjectByKey(schedule.weeks, decoded.weeks[week].start, 'start');
        // if this week doesn't exist, add a new branch
        if (!weekExists.length){
          // schedule.weeks.push(decoded.weeks[week]);
          schedule.weeks.push({
            start: decoded.weeks[week].start,
            end: decoded.weeks[week].end,
            //get: decoded.weeks[week].get,
            physicists: []
          });
          weekExists[0] = schedule.weeks.length - 1;
        }
        else {
          //schedule.weeks[weekExists[0]].get = false;
        }
        
        weekExists = weekExists[0];
          
        if (decoded.weeks[week].get){
          // for each retrieved physicist
          for (physicist = 0; physicist < decoded.weeks[week].physicists.length; physicist++){
            var physicistExists = aa.indexOfObjectByKey(schedule.weeks[weekExists].physicists, decoded.weeks[week].physicists[physicist].physicist_id, 'physicist_id');
            var result = shared.tasks.add(decoded.weeks[week].physicists[physicist].tasks, {page: page.id, weekStart: decoded.weeks[week].start, deleted: page.deleted});
              
            // if this physicist doesn't exist in this week, add it
            if (!physicistExists.length){
              schedule.weeks[weekExists].physicists.push({
                physicist_id: decoded.weeks[week].physicists[physicist].physicist_id,
                tasks: result.added
              })
              // decoded.weeks[week].physicists[physicist].tasks = result.added;
            }
            
            // otherwise just reference array of tasks added to collection
            else {
              schedule.weeks[weekExists].physicists[physicistExists].tasks = result.added;
            }
            
            page.tasks = page.tasks.concat(result.added);
          }
        }
        else {
          for (physicist = 0; physicist < schedule.weeks[weekExists].physicists.length; physicist++){
            page.tasks = page.tasks.concat(schedule.weeks[weekExists].physicists[physicist].tasks);
          }
        }
          
        if (!aa.findObjectByKey(page.weeks, schedule.weeks[weekExists].start, 'start').length){
          page.weeks.push(schedule.weeks[weekExists]);
        }
        
        if (!schedule.weeks[weekExists].dates){
          schedule.weeks[weekExists].dates = [];
          var thisDate = schedule.weeks[weekExists].start;
          while (thisDate <= schedule.weeks[weekExists].end){
            schedule.weeks[weekExists].dates.push(thisDate);
            thisDate = dpExactDate(thisDate).addDays(1).toString('yyyy-MM-dd');
          }
        }
          
        
      }
      
    }
    else {
      if (!schedule.dates){
        schedule.dates = [];
      }
      schedule.dates = schedule.dates.concat(decoded.dates);
      schedule.dates.sort();
      
      if (!page.deleted){
        page.deleted = [];
      }
      
      var result = shared.tasks.add(decoded.tasks, {page: pageId, weekStart: page.start, deleted: page.deleted});
      page.tasks = result.added;

      // // link this page's tasks from task collection to this page's metadata
      // page.tasks = shared.tasks.get({value: {start: [schedule.config.start, ' 00:00:00'].join(''), end: [schedule.config.end, ' 23:59:59'].join('')}, key: 'schedule_start'}) || [];
  

    }
    
    //fblog('before tasks add');

    page.doubleBilled = decoded.double_billed;
    
    if (!page.doubleBilled){
      page.doubleBilled = {
        list: [],
        tree: []
      };
    }

    
    //fblog('after tasks add');
    shared.metaLoaded = true;
   
    schedule.today = decoded.today;

    fblog('scheduleParse(): schedule parsed (' + (ad.stopwatchEnd('scheduleParse') / 1000) + 's)');
    
    scheduleRender(page);
    
    
  }
  
  
  
  
  
  function scheduleParseLock(decoded){
    schedule.lock = decoded.schedule_lock;
    schedule.lock_lastday = dpExact(schedule.lock, 'yyyy-MM').addMonths(1).addDays(-1).toString('yyyy-MM-dd'); 
    if (decoded.schedule_lock_exceptions && $.trim(decoded.schedule_lock_exceptions)){
      schedule.lock_exceptions = decoded.schedule_lock_exceptions.split(',').sort().reverse();
    }
    else {
      schedule.lock_exceptions = [];
    }
  }
  
  
  
  // render schedule data to html table
  function scheduleRender(page){
  
    ad.stopwatchStart('scheduleRender');
    ad.stopwatchStart('scheduleRender framework');
  
    schedule.rendering = true;
    
    var
      tableId = ['#schedulePage', page.id].join(''),
      tableElement = [],
      injectTarget = $(['#', schedule.config.target].join('')),
      sp = shared.physicists,
      //sd = aa.getRange(schedule.dates, schedule.config.start, schedule.config.end),
      contentArray = [],
      i;
      // page = aa.findObjectByKey(schedule.pages, pageId, 'id')[0];
    
    page.rendering = true;
    
    if (page.view == 'month'){
    
      var tableElement = scheduleTableGet();
      
      page.element = tableElement;
      
      // scheduleTableAddEventHandlers(tableElement);
      // scheduleTableUpdateSideButtons();
      
      // var tableElement = $('#currentPage');
      // if (!tableElement.length){
        // tableElement = scheduleCreateTable();
      // }

      
      var week;
      
      for (week = 0; week < page.weeks.length; week++){
        var existingRow = $(['#tr', schedule.preferences.display.physicists[0], '-', page.weeks[week].start, 'morning'].join(''));
        if (!existingRow.length){ //page.weeks[week].get){
          var rows = [];
          var physicist = aa.findObjectByKey(shared.physicists, schedule.preferences.display.physicists[0], 'physicist_id')[0];

          var headerArray = [
            '<p class="taskheader namefirst">', physicist.name_first, '</p>',
            '<p class="taskheader namelast">', physicist.name_last, '</p>'
          ];
          
          if (physicist['phone_cell']){
            headerArray.push('<p class="taskheader phone">C&nbsp;&nbsp;', physicist.phone_cell, '</p>');
          }
          if (physicist['pager']){
            headerArray.push('<p class="taskheader phone">P&nbsp;&nbsp;', physicist.pager, '</p>');
          }
          
          if (physicist['phone_extension']){
            headerArray.push('<p class="taskheader phone">VM&nbsp;&nbsp;', physicist.phone_extension, '</p>');
          }
          
          rows.push({
            id: [physicist.physicist_id, '-', page.weeks[week].start].join(''),
            user: physicist.physicist_id,
            // id: physicist.physicist_id,
            header: headerArray.join(''),
            columns: page.weeks[week].dates
          });


        
          // generate empty cells
          contentArray.push(scheduleCreateRows({
            page: page,
            rows: rows,
            dates: page.weeks[week].dates,
            headerInterval: 5,
            timeblocks: ['morning', 'afternoon']
          }, (week == 0 ? false : true)));
        
        }
        
      }
      
      $('#hidden_container').append(contentArray.join(''));
      //tableElement.append(contentArray.join(''));
    }
    else {
      contentArray.push('<table id="schedulePage', page.id, '" class="schedule" page="', page.id, '" cellspacing="1">');
      
      var sd = aa.getRange(schedule.dates, schedule.config.start, schedule.config.end);
    
      contentArray.push(
        '<tr id="divider', page.id, '-top', '" class="divider"><td class="dividercell" colspan="',
        sd.length + 1, // add one column for row headers
        '"></td></tr>'
      );
    
      var rows = [];
      for (i = 0; i < sp.length; i++){
      // for (i = sp.length - 1; i >= 0; i--){
        var headerArray = [
          '<p class="taskheader namefirst">', sp[i]['name_first'], '</p>',
          '<p class="taskheader namelast">', sp[i]['name_last'], '</p>'
        ];
        
        if (sp[i]['phone_cell']){
          headerArray.push('<p class="taskheader phone"><span class="taskheaderCategory">C</span>', sp[i]['phone_cell'], '</p>');
        }
        if (sp[i]['pager']){
          headerArray.push('<p class="taskheader phone"><span class="taskheaderCategory">P</span>', sp[i]['pager'], '</p>');
        }
        
        if (sp[i]['phone_extension']){
          headerArray.push('<p class="taskheader phone"><span class="taskheaderCategory">VM</span>', sp[i]['phone_extension'], '</p>');
        }
        
        rows.push({
          id: [sp[i].physicist_id, '-', page.start].join(''), //sp[i]['physicist_id'],
          user: sp[i].physicist_id,
          header: headerArray.join(''),
          columns: sd
        });
      }

      // generate empty cells
      contentArray.push(scheduleCreateRows({
        page: page,
        rows: rows,
        dates: sd,
        headerInterval: 5,
        timeblocks: ['morning', 'afternoon']
      }));
      
      contentArray.push('</table>');
      
      // render html to hidden div for further manipulation before displaying
      if (!$('#jq-wip').length){
        $('#hidden_container').append('<div id="jq-wip" class="hidden"></div>');
        // fblog('scheduleRender(): wip div added to document, ' + (ad.stopwatchSegment('scheduleRender') / 1000) + 's');
      }
      
      // add table content to hidden wip div
      // tableElement = $(contentArray.join(''));
      
      // $('#jq-wip').empty().append(tableElement);
      document.getElementById('jq-wip').innerHTML = contentArray.join('');
      // $('#jq-wip')[0].innerHTML = contentArray.join('');
      //$('#jq-wip').html(contentArray.join(''));
      tableElement = $(['#schedulePage', page.id].join('')); //$('#jq-wip > table').eq(0);
      page.element = tableElement;
      
      //scheduleTableAddEventHandlers(tableElement);
      
    }
    

    // hide table element before modifying to prevent IE8 crash when changing colSpan
    // if ($.browser.msie && $.browser.version.substring(0,2) == '8.'){
    if (document.documentMode == '8'){
      fblog(['document mode = ', document.documentMode].join(''));
      page.element.hide();
    }
    
    
    // // prevent selection marks from dragging and shift/ctrl-clicking table cells
    // tableElement.find('th, td.task, p:not(p.phone)').disableTextSelect();
    fblog(['scheduleRender(): framework ', ad.stopwatchEnd('scheduleRender framework') / 1000, 's'].join(''));
    
    

    
    if (!page.precache){
      scheduleCreateControls();
    }
    
    
    // iterate through tasks, placing in appropriate table cells
    // calls asynchronously to prevent browser from becoming unresponsive

    page.renderI = 0;
    
    
    //fblog('before async');

    ad.stopwatchStart('scheduleRender loop');
    
    $.whileAsync({
      cfg: page,
      delay: (page.precache ? schedule.performance.renderDelayPrecache : schedule.performance.renderDelay), //25,
      bulk: (page.precache ? schedule.performance.renderBulkPrecache : schedule.performance.renderBulk), //10,
      test: function(page) {
        return (page.renderI < page.tasks.length && !page.cancel);
      },
      loop: function(page) {
        var st = page.tasks;
        var i = page.renderI;
        
        
        if (st[i].data.physicist_id){
          
          var cell = scheduleCellGet(st[i], page); //$('table.schedule tr[row=' + st[i].data.physicist_id + '][timeblock=' + (getTimeblock(startDate)) + '] td.task[column=' + (getUTCDatest[i](startDate)) + ']');

          var currentCellId = cell.attr('_id');
          if (currentCellId){
            //fblog(['scheduleRender() loop: overwriting non-empty cell (', cell.attr('id'), ') with ', st[i].data.physicist_id, '-', st[i].data.schedule_start].join(''));
          }
          else {
          
            if (!cell.length){
              fblog(['scheduleRender() loop: other cell is full day (rowspan = 2), no cell to write this task to (', i, ' - ', st[i].data.physicist_id, '-', st[i].data.schedule_start, ' - ', st[i].data.id, ')'].join(''));
              //scheduleCellGetOther(st[i]).attr('rowSpan', 1);
              cell = scheduleCellGetOther(st[i], page);
              if (!cell.parent()[0]){
                fblog('scheduleRender() loop: !cell.length && !cell.parent()[0]');
              }
              cell = scheduleCellInsertBelow(cell, page);
            }
            //.attr('taskIndex', i)
            cell.attr('_id', st[i]._id);
            
            scheduleCellUpdateContent(cell, page);      
          }
        }
        page.renderI++;
        
      },
      end: function(page) {
        fblog(['scheduleRender() loop: ', ad.stopwatchEnd('scheduleRender loop') / 1000, 's'].join(''));
        
        ad.stopwatchStart('scheduleRender end');

        // if cancel flag is set, 
        if (page.cancel){
          scheduleDeletePage(page.id);
        }
      
        // if this is a precache page
        else if (page.precache){
          page.rendering = false;
          page.rendered = true;
          page.precache = false;
          // if this is a precache page, just add it to the collection
          schedulePageMoveToBodyEnd(tableElement, page);
        }
        
        // if this is NOT a precache, render it
        else {
          scheduleUpdateDeletedStatus(page);
          
          page.rendering = false;
          page.rendered = true;
          schedulePage();
          
          // move wip content to visible container
          //fblog(tableElement.parent().attr('id'));
          // var html = document.getElementById('jq-wip').innerHTML;
          // document.getElementById('jq-wip').innerHTML = '';
          // document.getElementById('jq-formstatic-inject').innerHTML = html;
          injectTarget.empty().append(tableElement.attr('id', 'currentPage'));
          
          schedule.currentPage = page.id;
          schedule.currentPageElement = page.element;
          
          scheduleOnDisplayPhysicistsChange(null, page);
          scheduleUpdateColumnCount();
          scheduleUpdateDisplayWeekends();

          //slideIn(tableElement, {direction: (schedule.config.relationship == 'previous' ? 'right' : 'left')});
          
          $('#footer').css('padding-bottom', '50em');

          tableElement.parents('div.flatContainer').children('div.body').css('border-width', '0');        
          
          scheduleUnblock();
        }
        
        var inProgress = aa.findObjectByKey(schedule.pages, true, 'precache');
        if (!inProgress.length){
          //schedule.precache.inProgress = false;
        }

        page.retrieving = false;
        // enable updater intervals
        //updaterStart('scheduleGetUpdated', 'scheduleSaveDirty');

        fblog(['scheduleRender() end: ', ad.stopwatchEnd('scheduleRender end') / 1000, 's'].join(''));
        
        fblog(['scheduleRender(): page ', page.id, ' finished (', (ad.stopwatchEnd('scheduleRender') / 1000), 's, ', page.tasks.length, ' tasks)'].join(''));
        schedule.rendering = false;

      }
    });
    
  }




  function scheduleUpdateDeletedStatus(page){
    var updateCell;
    var deletedItem;
    var deletedStartDate;
    var shown;
    
    if (!page){
      page = scheduleGetPageById(schedule.currentPage);
    }
    
    if (page){
      for (i = 0; i < page.deleted.length; i++){
        deletedItem = page.deleted[i];
        shown = false;
        deletedStartDate = deletedItem.schedule_start_date;
        updateCell = $(['#task', deletedItem.physicist_id, '-', dateGetWeekStart(deletedStartDate), deletedItem.timeblock, deletedStartDate].join(''));
        if (updateCell.hasClass('taskOpen') || updateCell.hasClass('taskEmpty')){
          updateCell.addClass('taskDeleted');
          shown = true;
        }
        //fblog(['previous deleted at ', deletedItem.physicist_id, '-', deletedStartDate, deletedItem.timeblock, ': ', shown.toString()].join(''));
      }
    }
    
  }
  
  
  
  
  function scheduleCreateControls(){
    if (!$('#scheduleControlsContainer').length){
  
      var physicistSelectors = [
        {
          id: -1,
          name: 'Everyone',
          param: 'all'
        },
        {
          id: -2,
          name: 'Diagnostic only',
          param: 'diagnostic'
        },
        {
          id: -3,
          name: 'Therapy only',
          param: 'therapy'
        }
      ].concat(shared.physicists);
    
      $('#jq-form-inject').before('<div id="scheduleControlsContainer" class="tableControlsContainer scheduleControlsContainer dontprint">container</div>');
      
      var formDef = new formObject({
        id: 'scheduleControls',
        injectTarget: 'scheduleControlsContainer',
        addedInjectTarget: 'none',
        prefix: 'sc_',
        noFrame: true,
        noGap: true,
        submitType: 'ajax',
        //focus: 'location_name',
        //editTrack: 'activeEdit.dirty',
        validation: aa.objectClone(validationDefault),
        fields: 
          aa.existsIn(permissions, 'debug')

          ?
          
          [
            {
              name: 'displayphysicist',
              label: 'Display schedule for',
              type: 'dropdown',
              style: 'scheduleDisplayPhysicistContainer',
              values: physicistSelectors,
              //noneSelected: physicistSelectors[0].name,
              selectExclusiveAll: true, //[-1,-2,-3],
              selectCallback: scheduleOnDisplayPhysicistsChange,
              prefillVal: schedule.preferences.display.physicists
            },
            {
              type: 'eol'
            },
            {
              type: 'raw',
              content: '<span class="icon-print iconleftgapless" id="linkSchedulePrint" style="display: none"><a href="?print" target="_blank">Print schedule</a></span>'
            },
            {
              name: 'toggleView',
              type: 'button',
              action: 'scheduleToggleView',
              //target: 'schedule',
              style: 'inline',
              textCondition: {
                test: function(){ return schedule.preferences.display.view == 'month'; },
                whenTrue: 'Weekly view',
                whenFalse: 'Monthly view'
              }
            },
            {
              name: 'toggleCondensed',
              type: 'button',
              action: 'scheduleToggleCondensed',
              //target: 'schedule',
              style: 'inline',
              textCondition: {
                test: function(){ return schedule.preferences.display.condensed; },
                whenTrue: 'Detailed view',
                whenFalse: 'Condensed view'
              }
            },
            {
              name: 'toggleRecentHighlight',
              type: 'button',
              action: 'scheduleToggleHighlightRecentChanges',
              //target: 'schedule',
              style: 'inline',
              textCondition: {
                test: function(){ return schedule.preferences.display.recentHighlight; },
                whenTrue: "Don't highlight recent changes",
                whenFalse: "Highlight recent changes"
              }
            },
            // {
              // name: 'toggleMaximized',
              // type: 'button',
              // action: 'scheduleToggleMaximized',
              // //target: 'schedule',
              // style: 'inline',
              // textCondition: {
                // test: function(){ return schedule.preferences.display.maximized; },
                // whenTrue: 'Reduce',
                // whenFalse: 'Maximize'
              // }
            // },
            {
              name: 'toggleWeekends',
              type: 'button',
              action: 'scheduleToggleWeekends',
              //target: 'schedule',
              style: 'inline',
              textCondition: {
                test: function(){ return schedule.preferences.display.weekends; },
                whenTrue: 'Hide weekends',
                whenFalse: 'Show weekends'
              }
            }
          ]
          
          :
          
          [
            {
              name: 'displayphysicist',
              label: 'Display schedule for',
              type: 'dropdown',
              style: 'scheduleDisplayPhysicistContainer',
              values: physicistSelectors,
              //noneSelected: physicistSelectors[0].name,
              selectExclusiveAll: true, //[-1,-2,-3],
              selectCallback: scheduleOnDisplayPhysicistsChange,
              prefillVal: schedule.preferences.display.physicists
            },
            {
              type: 'eol'
            },
            {
              type: 'raw',
              content: '<span class="icon-print iconleftgapless" id="linkSchedulePrint" style="display: none"><a href="?print" target="_blank">Print schedule</a></span>'
            },
            {
              name: 'toggleView',
              type: 'button',
              action: 'scheduleToggleView',
              //target: 'schedule',
              style: 'inline',
              textCondition: {
                test: function(){ return schedule.preferences.display.view == 'month'; },
                whenTrue: 'Weekly view',
                whenFalse: 'Monthly view'
              }
            },
            {
              name: 'toggleCondensed',
              type: 'button',
              action: 'scheduleToggleCondensed',
              //target: 'schedule',
              style: 'inline',
              textCondition: {
                test: function(){ return schedule.preferences.display.condensed; },
                whenTrue: 'Detailed view',
                whenFalse: 'Condensed view'
              }
            },
            {
              name: 'toggleRecentHighlight',
              type: 'button',
              action: 'scheduleToggleHighlightRecentChanges',
              //target: 'schedule',
              style: 'inline',
              textCondition: {
                test: function(){ return schedule.preferences.display.recentHighlight; },
                whenTrue: "Don't highlight recent changes",
                whenFalse: "Highlight recent changes"
              }
            },
            // {
              // name: 'toggleMaximized',
              // type: 'button',
              // action: 'scheduleToggleMaximized',
              // //target: 'schedule',
              // style: 'inline',
              // textCondition: {
                // test: function(){ return schedule.preferences.display.maximized; },
                // whenTrue: 'Reduce',
                // whenFalse: 'Maximize'
              // }
            // },
            {
              name: 'toggleWeekends',
              type: 'button',
              action: 'scheduleToggleWeekends',
              //target: 'schedule',
              style: 'inline',
              textCondition: {
                test: function(){ return schedule.preferences.display.weekends; },
                whenTrue: 'Hide weekends',
                whenFalse: 'Show weekends'
              }
            }
          ]
            
      });

      forms[formDef.id] = formDef;
      
      formBuild(formDef, formDef.injectTarget);

    }
  }
  
  





  // function scheduleCreateTable(){
    // var dayNames = [
      // {name: 'Sunday', css: 'weekend'},
      // {name: 'Monday'},
      // {name: 'Tuesday'},
      // {name: 'Wednesday'},
      // {name: 'Thursday'},
      // {name: 'Friday'},
      // {name: 'Saturday', css: 'weekend'}
    // ];
    
    // var tableArray = [];
    
    // var tableElement;
    
    // tableArray.push(
      // '<table id="currentPage" class="schedule month" cellspacing="1">',
      // '<tr id="divider-topA', '" class="divider"><td colspan="7"></td></tr>',
      // '<tr id="divider-top', '" class="dayNameHeader">',
        // '<th scope="row" class="rowth colthDayNames"></th>' // blank cell at top-left
    // );
    
    // for (i = 0; i < 7; i++){
      // tableArray.push(
        // '<th column="', i, '" class="colth colthDayNames ', dayNames[i].css, '"><p class="taskheader dayname">', dayNames[i].name, '</p></th>'
      // );
    // }
    
    // tableArray.push(
      // '</tr>',
      // '<tr id="divider-topB', '" class="divider"><td colspan="7"></td></tr>',
      // '</table>'
    // );
    
    // tableElement = $(tableArray.join(''));
    
    // $('#hidden_container').append(tableElement);
    
    
    // // scheduleTableAddEventHandlers(tableElement);
    // // scheduleTableUpdateSideButtons();
    
    // return tableElement;
  // }
  


  // get already rendered table element, or create one if it doesn't exist
  function scheduleTableGet (){
    
    var tableElement, i;
  
    if (schedule.preferences.display.view == 'month'){
      tableElement = $('#currentPage.month');
      
      if (!tableElement.length){
        tableElement = $('#scheduleMonth');
      }
      
      if (!tableElement.length){
        tableElement = $(['<table id="scheduleMonth" class="schedule month" cellspacing="1"></table>'].join(''));
        $('#hidden_container').append(tableElement);
      }

      if (!$('#divider-top').length){
      
        var dayNames = [
          {name: 'Sunday', css: 'weekend'},
          {name: 'Monday'},
          {name: 'Tuesday'},
          {name: 'Wednesday'},
          {name: 'Thursday'},
          {name: 'Friday'},
          {name: 'Saturday', css: 'weekend'}
        ];
        
        var tableArray = [];
        
        tableArray.push(
          '<tr id="divider-topA', '" class="divider"><td class="dividercell" colspan="7"></td></tr>',
          '<tr id="divider-top', '" class="dayNameHeader">'
            //'<th scope="row" class="rowth colthDayNames"></th>' // blank cell at top-left
        );
        
        for (i = 0; i < 7; i++){
          tableArray.push(
            '<th column="', i, '" class="colth colthDayNames ', dayNames[i].css, '"><p class="taskheader dayname">', dayNames[i].name, '</p></th>'
          );
        }
        
        tableArray.push(
          '</tr>',
          '<tr id="divider-topB', '" class="divider"><td class="dividercell" colspan="7"></td></tr>'
        );
        
        tableElement.prepend($(tableArray.join('')));
      }
    }
    
    return tableElement;
    
  }
  
  
  
  
  
  
  function scheduleTableAddEventHandlers(tableElement){
    
    ad.stopwatchStart('scheduleTableAddEventHandlers');
  
    if (!tableElement){
      tableElement = $('#currentPage');
    }

    // prevent selection marks from dragging and shift/ctrl-clicking table cells
    //tableElement.find('th, td.task, p:not(p.phone)').disableTextSelect();
    tableElement.disableTextSelect();
    
    // remove existing event handlers if present
    tableElement
      .unbind('click')
      .unbind('dblclick')
      .unbind('contextmenu');
  
    //tableElement.bind('contextmenu', scheduleTableClickHandler);
  
    // add mouse event delegators
    tableElement.click(scheduleTableClickHandler);
   
    tableElement.dblclick(scheduleTableDoubleClickHandler);

    $('body').append([
      '<div class="contextMenu" id="scheduleContextMenu" style="display: none">',
        '<ul>',
          '<li id="contextEdit" class="iconleft icon-edit">Edit</li>',
          // '<li id="contextCut" class="iconleft icon-cut">Cut</li>',
          '<li id="contextCopy" class="iconleft icon-copy">Copy</li>',
          '<li id="contextPaste" class="iconleft icon-paste">Paste</li>',
          // '<li id="contextDelete" class="iconleft icon-delete">Delete</li>',
        '</ul>',
      '</div>'].join('')
    );
    
    
    
    // schedule right-click context menu
    if (aa.existsIn(permissions, 'debug')){
      tableElement.contextMenu('scheduleContextMenu', {
        onContextMenu: scheduleTableClickHandler,
        onShowMenu: function(e, menu){
          $('#contextEdit', menu).addClass('darker bold');
          return menu;
        },
        itemStyle: {
          color: '#666'
        },
        itemHoverStyle: {
          border: '1px solid #dada88',
          backgroundColor: '#ffc'
        },
        bindings: {
          contextEdit: function(target, context){
            fblog(['right-clicked: ', context.target.attr('id'), ', ', schedule.selected.data.length, ' cell(s) selected'].join(''));
            scheduleTableDoubleClickHandler(context.event);
          }
        },
        includeOnly: [
          // '.task',             // ignore task cells for testing purposes
          'th.taskheader',
          'th.taskheader p',
          'th.taskheader span'
        ]
      });
    }
    
    fblog(['scheduleTableAddEventHandlers(): ', ad.stopwatchEnd('scheduleTableAddEventHandlers') / 1000, 's'].join(''));
  

    
  }
  
  function scheduleTableClickHandler(e){ //}
    var el = $(e.target);
    
    
    datepickerHide();
    
    
    // clicked a task cell
    if (el.hasClass('task')){
    
      while (!el.is('td')){ //(el[0].tagName == 'TD')){
        el = el.parent();
      }
      
      var
        elParent = el.parent(),
        sel = {
          row: elParent.attr('row'),
          timeblock: elParent.attr('timeblock'),
          column: el.attr('column'),
          element: el,
          id: el.attr('id')
        };
        

      if (e.altKey){
      }
      else if (e.ctrlKey || e.shiftKey){ // ctrl key held down
        scheduleSelectCells({origin: sel, keepCurrent: true});
        // e.cancelBubble = true;
        // e.preventDefault();
        // e.stopPropagation();
        // return false;
      }
      else { // no modifier key held down
        if (!schedule.selected.exists(sel)){
          scheduleSelectCells({origin: sel});
        }
        else {
          scheduleTableDoubleClickHandler(e);
        }
      }
    
    
    }
    
    
    // clicked a row or column header, so select all cells
    else if (el.is('th.taskheader, th.taskheader p, th.taskheader span')){

      while (!el.is('th')){
        el = el.parent();
      }
    
      var elScope = el.attr('scope');
    
      schedule.selectedRow = null;
      $(['#th', schedule.currentPage, '-', mouseDown.row].join('')).removeClass('headerselected');
      $(['#th', mouseDown.column].join(''), schedule.currentPageElement).removeClass('headerselected');
      
      
      if (elScope == 'col'){
        scheduleSelectCells({column: el.attr('column')});
      }
      else if (elScope == 'row'){
        scheduleSelectCells({row: el.parent().attr('row')});
      }
      else {
        scheduleSelectCells({all: true});
      }

    
    }
    
    
    // this is not any of the targeted cases, so allow click to pass through
    else {
      return true;
    }
    
    
    // return clicked element
    return el;
    
    // e.preventDefault();
    // e.stopPropagation();
    // return false;
  }

  function scheduleTableDoubleClickHandler(e){
    datepickerHide();
 
    var el = $(e.target);
    
    if (el.hasClass('task')){

      while (!el.is('td.task')){
        el = el.parent();
      }

      var sel = {
        row: el.parent().attr('row'),
        timeblock: el.parent().attr('timeblock'),
        column: el.attr('column')
      };        
      
      // if (
        // mouseDown.row == sel.row
        // &&
        // mouseDown.timeblock == sel.timeblock
        // &&
        // mouseDown.column == sel.column
      // ){
      
        if (el.hasClass('taskselectedReadonly')){
          el.addClass('taskeditingReadonly');
        }
        else {
          el.addClass('taskediting');
        }
      
        setTimeout(function(){
          scheduleEditShow();
        }, 10);
      // }
      
    }
    else {
      scheduleEditShow();
    }
  }




  
  
  
  function scheduleTableUpdateSideButtons(tableElement){
    if (!tableElement || !tableElement.length){
      tableElement = $('#currentPage');
      if (!tableElement.length){
        tableElement = schedule.pages[0].element;
      }
    }
    
    if (!schedule.buttonPrevious || !schedule.buttonNext){
      schedule.buttonPrevious = $(['<td id="buttonTableSideLeft" class="buttonTableSide buttonTableSideLeft dontprint" rowSpan="', tableElement[0].rows.length + 1, '">&nbsp;</td>'].join(''));
      schedule.buttonNext = $(['<td id="buttonTableSideRight" class="buttonTableSide buttonTableSideRight dontprint" rowSpan="', tableElement[0].rows.length + 1, '">&nbsp;</td>'].join(''));
    }
    
    $('tr:first', tableElement)
      .prepend(schedule.buttonPrevious)
      .append(schedule.buttonNext);
      
    schedule.buttonPrevious
      .unbind('hover')
      .unbind('click')
      .hover(
        function () {
          $(this).addClass('buttonTableSideLeftHover');
        },
        function () {
          $(this).removeClass('buttonTableSideLeftHover');
        }
      )
      .click(
        function(){
          $(this).removeClass('buttonTableSideLeftHover');
          datepickerHide();
          scheduleGet('previous'); // hh:mm:ss') + ' GMT' + target.getUTCOffset());                  
        }
      );
    
    schedule.buttonNext
      .unbind('hover')
      .unbind('click')
      .hover(
        function () {
          $(this).addClass('buttonTableSideRightHover');
        },
        function () {
          $(this).removeClass('buttonTableSideRightHover');
        }
      )
      .click(
        function(){
          $(this).removeClass('buttonTableSideRightHover');
          datepickerHide();
          scheduleGet('next'); // hh:mm:ss') + ' GMT' + target.getUTCOffset());                  
        }
      );
    
    // ensure that next/previous buttons are reset to full table height, in case they were originally created at less than full height
    schedule.buttonPrevious.attr('rowSpan', tableElement[0].rows.length + 1);
    schedule.buttonNext.attr('rowSpan', tableElement[0].rows.length + 1);
    
  }
  


  
  
  // callback each time the physicist selection dropdown has its state changed (values checked/unchecked)
  function scheduleOnDisplayPhysicistsChange(e, page){
    var i, j, thisVal, showEl = [], hideEl = [], count = 0;
    var checked;
    var toBeShown = [];
    // var tableElement;
    var inputElement = $(['#', forms.scheduleControls.prefix, 'displayphysicist'].join(''));
    var inputContent;
    var monthlyChangePhysicist = false;
    var pageElement;
    
    // var page = aa.findObjectByKey(schedule.pages, schedule.currentPage);
      
    if (!page){ // || page.id === schedule.currentPage){
      if (schedule.preferences.display.view == 'month'){
        page = aa.findObjectByKeys(schedule.pages, {period: schedule.config.period, view: 'month'});
      }
      else {
        page = aa.findObjectByKey(schedule.pages, schedule.currentPage);
      }
      
      if (!page.length || page[0].view != schedule.preferences.display.view){
        scheduleGet();
        return;
      }
      else {
        page = page[0];
      }
      
      // tableElement = $('#currentPage');
      // fblog('scheduleOnDisplayPhysicistsChange(): current page');
    }
    else {
      // tableElement = $(['#schedulePage', pageId].join(''));
      // fblog(['scheduleOnDisplayPhysicistsChange(): pageId = ', pageId].join(''));
    }

    var
      weekStart = page.start,
      weekEnd = page.end,
      selectExclusive;
      
    // update selectExclusive condition for dropdown based on view type
    // monthly should only display a single physicist, but weekly can display more than one selected physicist
    if (page.view == 'month'){
      selectExclusive = true;
    }
    else {
      selectExclusive = [-1, -2, -3];
    }
    
    aa.findObjectByKey(forms.scheduleControls.dropdowns, [forms.scheduleControls.prefix, 'displayphysicist'].join(''))[0].config.selectExclusiveAll = selectExclusive;
    
    // if (aa.existsIn(permissions, 'debug')){
    
      checked = $(['#scheduleControls [name=displayphysicist]:checked'].join(''));
      
      
      // no items checked, so set to default (-1 in this case)
      if (!checked.length){
        $(['#scheduleControls [name=displayphysicist][value=-1]'].join('')).attr('checked', true);
        inputElement.next('.multiSelectOptions').multiSelectUpdateSelected(schedule.multiselect.options);
        
        scheduleOnDisplayPhysicistsChange(e, page);
        //$('#currentPage > tbody > tr').show();
        return;
      }
    // }
    // else {
      // checked = [-1];
    // }
      
    
    loop: // label for breaking out of substructure
    for (i = 0; i < checked.length; i++){
      // if (aa.existsIn(permissions, 'debug')){
        thisVal = Number($(checked[i]).attr('value'));
        //fblog('test |' + thisVal + '|');
      // }
      // else {
        // thisVal =  checked[i];
      // }
      
      switch (thisVal){
        
        case -1: // default
          if (schedule.preferences.display.view == 'month'){
            schedule.preferences.display.physicistsWeekly = [-1];
            scheduleToggleView();
            return;
          }
          toBeShown = toBeShown.concat(getActivePhysicistIds(shared.physicists, page));
          
          // for (j = 0; j < shared.physicists.length; j++){
            // if (
              // // only show active physicists
              // (
                // !shared.physicists[j].active_start
                // ||
                // shared.physicists[j].active_start < weekEnd
              // )
              // &&
              // (
                // !shared.physicists[j].active_end
                // ||
                // shared.physicists[j].active_end > weekStart
              // )
            // ){
              // toBeShown.push(shared.physicists[j].id);
            // }
          // }
          break loop;
          
        case -2:
          if (schedule.preferences.display.view == 'month'){
            schedule.preferences.display.physicistsWeekly = [-2];
            scheduleToggleView();
            return;
          }
          toBeShown = toBeShown.concat(getActivePhysicistIds(shared.physicistsByTasktype[4], page));
          
          // for (j = 0; j < shared.physicistsByTasktype[4].length; j++){
            // if (
              // // only show active physicists
              // (
                // !shared.physicistsByTasktype[4][j].active_start
                // ||
                // shared.physicistsByTasktype[4][j].active_start < weekEnd
              // )
              // &&
              // (
                // !shared.physicistsByTasktype[4][j].active_end
                // ||
                // shared.physicistsByTasktype[4][j].active_end > weekStart
              // )
            // ){
              // toBeShown.push(shared.physicistsByTasktype[4][j].id);
            // }
          // }
          break loop;
        
        case -3:
          if (schedule.preferences.display.view == 'month'){
            schedule.preferences.display.physicistsWeekly = [-3];
            scheduleToggleView();
            return;
          }
          toBeShown = toBeShown.concat(getActivePhysicistIds(shared.physicistsByTasktype[3], page));
          
          // for (j = 0; j < shared.physicistsByTasktype[3].length; j++){
            // if (
              // // only show active physicists
              // (
                // !shared.physicistsByTasktype[3][j].active_start
                // ||
                // shared.physicistsByTasktype[3][j].active_start < weekEnd
              // )
              // &&
              // (
                // !shared.physicistsByTasktype[3][j].active_end
                // ||
                // shared.physicistsByTasktype[3][j].active_end > weekStart
              // )
            // ){
              // toBeShown.push(shared.physicistsByTasktype[3][j].id);
            // }
          // }
          break loop;
        
        default:
          // if (
            // // only show active physicists
            // (
              // !shared.physicistsById[thisVal].active_start
              // ||
              // shared.physicistsById[thisVal].active_start < weekEnd
            // )
            // &&
            // (
              // !shared.physicistsById[thisVal].active_end
              // ||
              // shared.physicistsById[thisVal].active_end > weekStart
            // )
          // ){
            toBeShown.push(thisVal);
          // }
        
          
      }
    }
    
    schedule.visibleRows = toBeShown;
    
    var prevPhysicists = schedule.preferences.display.physicists;
    
    // update preferences with current setting
    if (aa.existsIn([-1,-2,-3], thisVal)){
      schedule.preferences.display.physicists = [thisVal];
    }
    else {
      // if (schedule.preferences.display.physicists !== toBeShown){
        // monthlyChangePhysicist = true;
        schedule.preferences.display.physicists = toBeShown;
      // }
    }
    
    // if (schedule.preferences.display.view == "week"){
      // schedule.preferences.display.physicistsWeekly = schedule.preferences.display.physicists;
    // }
    // else if (schedule.preferences.display.view == "month"){
      // schedule.preferences.display.physicistsMonthly = schedule.preferences.display.physicists;
    // }
    
    if (!aa.objectCompare(prevPhysicists, schedule.preferences.display.physicists)){
      scheduleSavePreferences();
    }
    
    // if (false){ // autoswitch to month view if single physicist selected
    // // single physicist selected
    // if (toBeShown.length == 1 && toBeShown[0] > 0){
      // schedule.preferences.display.view = 'month';
      // if (page.view != 'month' || monthlyChangePhysicist){
        // var target;
        
        // if (schedule.today >= schedule.start && schedule.today <= schedule.end){
          // target = schedule.today;
        // }
        // else {
          // target = dateString(dpExactDate(schedule.start).addDays(3))
        // }
        
        // schedule.config = {
          // start: target,
          // view: schedule.preferences.display.view,
          // target: schedule.config.target
        // };
        
        // scheduleGet();
        // return;
      // }
    // }
    // else {
      // schedule.preferences.display.view = 'week';
    // }
    // }
    
    
    if (page.view == 'month'){
    
      var physicist = toBeShown[0];
      pageElement = $('#currentPage.month');
      if (!pageElement.length){
        fblog('scheduleOnDisplayPhysicistsChange(): unable to find currentPage.month table, looking for scheduleMonth');
        pageElement = $('#scheduleMonth');
      }
      if (!pageElement.length){
        fblog('scheduleOnDisplayPhysicistsChange(): unable to find month table');
      }
      
      // first make sure this physicist's data has been retrieved and rendered
      // if not, end this function and get data
      for (i = 0; i < page.weeks.length; i++){
        var thisPhysicist = aa.findObjectByKey(page.weeks[i].physicists, physicist, 'physicist_id');
        if (!thisPhysicist.length){
          schedule.config.start = [page.period, '-01'].join('');
          scheduleGet();
          return;
        }
      }
      
      //showEl.push('#divider-topA', '#divider-top', '#divider-topB');
      
      for (i = 0; i < page.weeks.length; i++){
        if (
          schedule.preferences.display.weekends
          ||
          true
        ){
          for (j = 0; j < page.weeks[i].physicists.length; j++){
            if (physicist == page.weeks[i].physicists[j].physicist_id){
              showEl.push(['#dateHeader',
                physicist, '-', page.weeks[i].start].join(''));
          
              showEl.push(
                ['#tr',
                  physicist, '-', page.weeks[i].start, 'morning'].join(''),
                ['#tr',
                  physicist, '-', page.weeks[i].start, 'afternoon'].join(''),
                ['#divider',
                  physicist, '-', page.weeks[i].start].join('')
              );
            }
            // else {
              // hideEl.push(['#dateHeader',
                // schedule.weeks[i].physicists[j].physicist_id, '-', schedule.weeks[i].start].join(''));
          
              // hideEl.push(
                // ['#tr',
                  // schedule.weeks[i].physicists[j].physicist_id, '-', schedule.weeks[i].start, 'morning'].join(''),
                // ['#tr',
                  // schedule.weeks[i].physicists[j].physicist_id, '-', schedule.weeks[i].start, 'afternoon'].join(''),
                // ['#divider',
                  // schedule.weeks[i].physicists[j].physicist_id, '-', schedule.weeks[i].start].join('')
              // );
            // }
          }
        }
      }
      
      $('tr', pageElement).not('#divider-topA, #divider-top, #divider-topB').appendTo('#hidden_container');
      
    }
    else {
    
      pageElement = page.element;
      var physicistIndex = aa.indexOfObjectByKey(shared.physicists, user.id, 'physicist_id');
      
      if (physicistIndex.length){
        physicistIndex = physicistIndex[0];
        count = schedulePageAppendRow(toBeShown, showEl, hideEl, weekStart, count, physicistIndex);
      }
      else {
        physicistIndex = null;
      }
      
      for (i = 0; i < shared.physicists.length; i++){
      // for (i = shared.physicists.length - 1; i >= 0; i--){
        if (i != physicistIndex){
          count = schedulePageAppendRow(toBeShown, showEl, hideEl, weekStart, count, i);
        }
      }
      
      $(hideEl.join(', ')).appendTo($('#hidden_container')); //hide();
      
    }
    
    
    // tableElement.find('tr').appendTo('#hidden_container');
    //$(showEl.join(', ')).show();
    // fblog(showEl.join(', '));
    
    // tableElement.append($(showEl.join(', ')));
    // step through elements, appending one at a time IN ORDER
    // jQuery's (at least 1.31) aggregate append steps through in arbitrary order under Chrome
    if (page.view == 'month'){
      for (i = 0; i < showEl.length; i++){
        $(showEl[i]).children('th[scope=row]').hide().end().appendTo(pageElement);
      }
    }
    else {
      for (i = 0; i < showEl.length; i++){
        $(showEl[i]).children('th[scope=row]').show().end().appendTo(pageElement);
      }
    }
    
    // for (i = 0; i < showEl.length; i++){
      // pageElement.append($(showEl[i]));
    // }
    
    
    // fblog(showEl[0]);

    
    
    
    //$(['#currentPage > tbody > tr:first'].join('')).prepend(schedule.buttonPrevious).append(schedule.buttonNext);

      
      // var rowSideButtons = $('<tr><td class="hidden"><td class="hidden"><td class="hidden"><td class="hidden"><td class="hidden"><td class="hidden"><td class="hidden"></tr>');
      // var rowSideButtons = $('<tr><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>');
      // rowSideButtons.prepend(buttonPrevious).append(buttonNext);
      // tableElement.prepend(rowSideButtons);
      
      
      
      // $(showEl[0]).prepend(schedule.buttonPrevious).append(schedule.buttonNext);
      
    scheduleTableAddEventHandlers(pageElement);
    scheduleTableUpdateSideButtons();
    scheduleUpdateCondensed();

    // if (schedule.preferences.display.view == 'month'){
      // $('th[scope=row]', schedule.currentPageElement).hide();
    // }
    // else {
      // $('th[scope=row]', schedule.currentPageElement).show();
    // }
    
 
      
    // scale input size to (roughly) fit content
    inputContent = inputElement.val();
    // fblog(inputContent.length);
    if (inputContent.length < 25){
      inputElement.removeClass('widthX2 widthX3 widthX4');
    }
    else if (inputContent.length < 50){
      inputElement.addClass('widthX2').removeClass('widthX3 widthX4');
    }
    else if (inputContent.length < 75){
      inputElement.addClass('widthX3').removeClass('widthX2 widthX4');
    }
    else {
      inputElement.addClass('widthX4').removeClass('widthX2 widthX3');
    }
    
    var inputPos = inputElement.position();
    // inputElement.siblings('.dropdownArrow').css({
      // // 'border-color': '#99f',
      // 'top': inputPos.top,
      // 'left': inputPos.left + inputElement.width()
    // }).removeClass('hidden');
    
    inputElement.blur();
    
    scheduleUpdateDisplayWeekends();
    scheduleUpdateLinkPrint();
  }
  
  
  
  
  function schedulePageAppendRow(toBeShown, showEl, hideEl, weekStart, count, i){
    if (
      aa.existsIn(toBeShown, shared.physicists[i].id)
    ){
      // fblog(['scheduleOnDisplayPhysicistsChange(): showing physicist ', shared.physicists[i].id].join(''));
      
      if (count % schedule.headerInterval === 0){
        showEl.push(['#dateHeader',
          shared.physicists[i].id, '-', weekStart].join(''));
        showEl.push(['#dateHeaderDivider',
          shared.physicists[i].id, '-', weekStart].join(''));
      }
      else {
        hideEl.push(['#dateHeader',
          shared.physicists[i].id, '-', weekStart].join(''));
        hideEl.push(['#dateHeaderDivider',
          shared.physicists[i].id, '-', weekStart].join(''));
      }
    
      showEl.push(
        ['#tr',
          shared.physicists[i].id, '-', weekStart, 'morning'].join(''),
        ['#tr',
          shared.physicists[i].id, '-', weekStart, 'afternoon'].join(''),
        ['#divider',
          shared.physicists[i].id, '-', weekStart].join('')
      );
      
      count++;
    }
    else {
      hideEl.push(
        ['#dateHeader',
          shared.physicists[i].id, '-', weekStart].join(''),
        ['#dateHeaderDivider',
          shared.physicists[i].id, '-', weekStart].join(''),
        ['#tr',
          shared.physicists[i].id, '-', weekStart, 'morning'].join(''),
        ['#tr', 
          shared.physicists[i].id, '-', weekStart, 'afternoon'].join(''),
        ['#divider',
          shared.physicists[i].id, '-', weekStart].join('')
      );
    }

    // if (aa.existsIn(toBeShown, shared.physicists[i].id)){
      // showEl.push(
        // '#tr', schedule.currentPage, '-', shared.physicists[i].id, 'morning, ',
        // '#tr', schedule.currentPage, '-', shared.physicists[i].id, 'afternoon, ',
        // '#divider', schedule.currentPage, '-', shared.physicists[i].id, ', '
      // );

      // if (count % schedule.headerInterval === 0){
        // showEl.push('#dateHeader', schedule.currentPage, '-', shared.physicists[i].id);
      // }
      // else {
        // hideEl.push('#dateHeader', schedule.currentPage, '-', shared.physicists[i].id);
      // }
    
      // count++;
    // }
    // else {
      // hideEl.push(
        // '#tr', schedule.currentPage, '-', shared.physicists[i].id, 'morning, ',
        // '#tr', schedule.currentPage, '-', shared.physicists[i].id, 'afternoon, ',
        // '#divider', schedule.currentPage, '-', shared.physicists[i].id, ', ',
        // '#dateHeader', schedule.currentPage, '-', shared.physicists[i].id
    // }
    return count;
  }
  
  
  
  
  function getActivePhysicistIds(physicists, range){
    return getActivePhysicists(physicists, range, true);
  }
  
  function getActivePhysicists(physicists, range, idOnly){
    var i, active = [];
    
    for (i = 0; i < physicists.length; i++){
      if (
        // only show active physicists
        (
          !physicists[i].active_start
          ||
          physicists[i].active_start < range.end
        )
        &&
        (
          !physicists[i].active_end
          ||
          physicists[i].active_end > range.start
        )
      ){
        if (idOnly){
          active.push(physicists[i].id);
        }
        else {
          active.push(physicists[i]);
        }
      }
    }
    
    return active;
      
  }
  
  
  
  // handler browser script errors
  function errorHandler(){
  
    if (deploy){
      if (shared.activeInterface == 'schedule'){
        schedule.precache.cancel = true;
        for (var i = 0; i < schedule.pages.length; i++){
          if (schedule.pages[i]){
            schedule.pages[i].cancel = true;
          }
        }
        schedule.blockInput = true;
        updaterStop();
        $('#jq-form-inject').unblock();
        $('#scheduleControlsContainer').hide();
      }
      
      $('div.alphaShadowOuter').hide();
      
      var newLogArr = logStringArr.slice(0);
      
      aa.forEach(newLogArr, function(value, index, collection){
        collection[index] = value.replace(/\<br\>/g, '\n');
      });
        
      var content = ['<p><span class="large highlightMedium" style="font-weight: bold;">An error has occurred</span></p><p>I\'m sorry for the trouble, but to help prevent this error from occurring again, please do the following:</p><ol><li><b>Right-click</b> the error log below and click <b>Select All</b>.</li><li><b>Right-click</b> the selected text and click <b>Copy</b>.</li><li>Paste the error text into an email and send to Nathan at <a href="mailto:nsmith@biomedphysics.com">nsmith@biomedphysics.com</a>.</li></ol><p>If you need any assistance, please contact Nathan at 908-788-9440 x23.</p><br><textarea readonly id="logStatic">'].concat(newLogArr, ['</textarea>']).join('\n');
      document.getElementById('jq-form-inject').innerHTML = content;
      // $('#jq-form-inject')[0].innerHTML = content;
      var logStaticEl = $('#logStatic');
      logStaticEl.scrollTo(logStaticEl[0].scrollHeight + logStaticEl[0].clientHeight);
    }
    else {
      $(window).scrollTo('max');
      pageLogRefresh();
    }
  }
  
  
  function slideIn(element, config){
    var
      parentWidth = element.parent().width(),
      defaultDuration = 500,
      easing = 'easeOutQuad';
    
    
    
    if (config.direction == 'left' || !config.direction){
      // slide left from (width of parent element) to zero
      element.css('margin-left', [Math.floor(parentWidth / 3), 'px'].join('')).show().animate({marginLeft: '0px'}, config.duration || defaultDuration, easing);
    }
      // slide right from negative(width of parent element) to zero
    else if (config.direction == 'right'){
      element.css('margin-left', ['-', Math.floor(parentWidth / 3), 'px'].join('')).show().animate({marginLeft: '0px'}, config.duration || defaultDuration, easing);
    }
    
    return element;
  }
  
  
  function scheduleDeletePage(pageId){
    fblog(['scheduleDeletePage(): deleting schedule page ', pageId, ', discarding rendered page and metadata'].join(''));

    $(['#schedulePage', pageId].join('')).remove();
    var pagesDeleted = aa.removeObjectByKey(schedule.pages, pageId, 'id');
    fblog(['scheduleDeletePage(): removed ', pagesDeleted, ' page(s) from schedule.pages'].join(''));
  }

  

  
  function clientUTCOffset(){
    return Date.today().getTimezoneOffset() * -60;
  }
  
  

  
  // convert hours (as integer or billingmatrix object containing only hours)
  // to billingmatrix object with half days and hours
  function hoursToMatrix(hours){
    if (typeof hours != 'object'){ // passed in integer
      hours = { // create matrix object with passed hours value
        1: 0, 
        2: hours
      };
    }
    
    // make sure any undefined properties are set to zero
    if (!hours[1]){
      hours[1] = 0;
    }
    if (!hours[2]){
      hours[2] = 0;
    }
    
    // if (
      // aa.existsIn(['schedule', 'staff'], shared.activeInterface)
    // ){
      // // convert to half days + hours
      // hours = {
        // 1: Number(hours[1]) + Math.floor(hours[2] / 4),
        // 2: Number(hours[2]) % 4
      // };
    // }
    // else {
      // hours = {
        // 1: 0,
        // 2: (Number(hours[1]) * 4) + Number(hours[2])
      // };
    // }
    
    return hours;
  }
  
  
  // convert billingmatrix to integer hours
  function matrixToHours(matrix){
    var hours = 0;
    if (!matrix[1]){
      matrix[1] = 0;
    }
    if (!matrix[2]){
      matrix[2] = 0;
    }
    // convert half days to hours
    hours += (matrix[1] * 4) + matrix[2];
    //fblog(['matrixToHours(', aa.var_dump(matrix), '): ', hours, ' hours'].join(''));
    return hours;
  }
  
  
  function matrixToHalfdays(matrix){
    if (!matrix[1]){
      matrix[1] = 0;
    }
    if (!matrix[2]){
      matrix[2] = 0;
    }
    
    return (
      Number(matrix[2]) >= 6 ?
        2
        :
        Number(matrix[2]) > 0 ?
          1
          :
          0
    ) + Number(matrix[1]);
  }
  
  
  // return cell based on task data
  function scheduleCellGet(task, page){
    return $(['#task', task.data.physicist_id, '-', task.data.weekStart, task.data.timeblock, task.data.schedule_start_date].join(''));
  }
  // return first cell of this task's day
  function scheduleCellGetMorning(cellOrTask, page){
    if (cellOrTask.hasOwnProperty('dirty')){ // passed object is a task
      return $(['#task', cellOrTask.data.physicist_id, '-', cellOrTask.data.weekStart, 'morning', cellOrTask.data.schedule_start_date].join(''));
    }
    else { // passed object is a cell
      // return $(['#', cellOrTask.attr('id').replace('afternoon', 'morning')].join(''));
      return $(['#task', cellOrTask.parent().attr('row'), 'morning', cellOrTask.attr('column')].join(''));
    }
  }
  // return last cell of this task's day
  function scheduleCellGetAfternoon(cellOrTask, page){
    if (cellOrTask.hasOwnProperty('dirty')){ // passed object is a task
      return $(['#task', cellOrTask.data.physicist_id, '-', cellOrTask.data.weekStart, 'afternoon', cellOrTask.data.schedule_start_date].join(''));
    }
    else { // passed object is a cell
      // return $(['#', cellOrTask.attr('id').replace('morning', 'afternoon')].join(''));
      return $(['#task', cellOrTask.parent().attr('row'), 'afternoon', cellOrTask.attr('column')].join(''));
    }
  }
  // return cell for whichever timeblock this task isn't (morning if passed afternoon, or vice versa)
  function scheduleCellGetOther(cellOrTask, page){
    // fblog(['scheduleCellGetOther(', cellOrTask, ', page ', page.id, '): starting'].join(''));
    
    var element;

    if (cellOrTask.hasOwnProperty('dirty')){ // passed object is a task
      element = ['#task', cellOrTask.data.physicist_id, '-', cellOrTask.data.weekStart, (cellOrTask.data.timeblock == 'morning' ? 'afternoon' : 'morning'), cellOrTask.data.schedule_start_date].join('');
      fblog(['scheduleCellGetOther(): element = ', element].join(''));
      return $(element);
    }
    else { // passed object is a cell
      element = ['#task', cellOrTask.parent().attr('row'), (cellOrTask.parent().attr('timeblock') == 'morning' ? 'afternoon' : 'morning'), cellOrTask.attr('column')].join('');
      // element = ['#', (cellOrTask.parent().attr('timeblock') == 'morning' ? cellOrTask.attr('id').replace('morning', 'afternoon') : cellOrTask.attr('id').replace('afternoon', 'morning'))].join('');
      fblog(['scheduleCellGetOther(): element = ', element].join(''));
      return $(element);
    }
  }
  
  
  
  // inserts an empty cell below a cell which is currently spanning multiple rows
  function scheduleCellInsertBelow(cell, page){
    cell.attr('rowSpan', '1');
    
    var weekends = scheduleGetWeekends(page);
    var isWeekend = aa.existsIn(weekends, cell.attr('column'));
    
    if (!cell.attr('column')){
      fblog('scheduleCellInsertBelow(): no column defined in cell')
      fbdir('cell', true, cell);
    }
    
    var newCellArray = [
      '<td id="task', cell.parent().attr('row'), 'afternoon', cell.attr('column'), '"',
      
      ' class="task taskempty taskOpen',
      (isWeekend ? ' weekend' : ''),
      '"',
      
      ' column="', cell.attr('column'), '"',
      
      (isWeekend ? ' style="display: none;"' : ''),
      
      (cell.attr('column') ? [' period="', cell.attr('column').substr(0,7), '"'].join('') : ''),
      
      '>afternoon</td>'
    ];
    
    var newCell = [];
    
    fblog(['inserting cell below: ', cell.attr('id')].join(''));
    
    
    
    // cellIndex is calculated incorrectly in IE when preceding cell(s) have display:none style,
    // so we must calculate the accurate cell index manually
    var cellParent = cell.parent();
    var rowSpan2 = 0;
    var attribute, siblingTag;
    
    // for (var realIndex = 0; realIndex < cellParent[0].cells.length; realIndex++){
      // if (cellParent[0].cells[realIndex] == cell[0]){
    for (var realIndex = 0; realIndex < cellParent[0].childNodes.length; realIndex++){
      if (cellParent[0].childNodes[realIndex] == cell[0]){
        break;
      }
      
      attribute = cellParent[0].childNodes[realIndex].rowSpan;
      siblingTag = cellParent[0].childNodes[realIndex].tagName;
      // fblog(attribute + ' ' + siblingTag);
      
      // make sure we only consider TD cells with rowSpan of 2
      if (attribute == 2 && siblingTag == 'TD'){
        rowSpan2++;
      }
    }
    
    var cellParentNext = cellParent.next();
    
    // fblog(['scheduleCellInsertBelow(): if realIndex+1(', (realIndex + 1), ') - rowSpan2(', cell.prevAll('[rowSpan=2]').length, ') <= nextRowLength(', cellParentNext[0].cells.length, ') ...'].join(''));
    // fblog(['scheduleCellInsertBelow(): if realIndex+1(', (realIndex + 1), ') - rowSpan2(', cell.prevAll('[rowSpan=2]').length, ') <= nextRowLength(', cellParentNext[0].childNodes.length, ') ...'].join(''));
    fblog(['scheduleCellInsertBelow(): if realIndex(', realIndex, ') - rowSpan2(', rowSpan2, ') <= nextRowLength(', cellParentNext[0].childNodes.length, ') ...'].join(''));

    // cell not at end of row, so insert it before adjoining cell below
    // if (/*cell[0].cellIndex*/ (realIndex + 1) - cell.prevAll('[rowSpan=2]').length <= cell.parent().next()[0].cells.length){
    // if ((realIndex + 1) - cell.prevAll('[rowSpan=2]').length <= cellParentNext[0].childNodes.length){
    if (realIndex - rowSpan2 <= cellParentNext[0].childNodes.length){
      fblog('scheduleCellInsertBelow(): true, cell not at end of row');
      
      //fblog(cell[0].cellIndex + ' - ' + (cell.prevAll('[rowSpan=2]').length)); // number of cells in afternoon row
      
      var insertBefore =
        $(cellParentNext[0]
          // .cells[ /*cell[0].cellIndex*/ realIndex - (cell.prevAll('[rowSpan=2]').length)]);
          .childNodes[ /*cell[0].cellIndex*/ realIndex - rowSpan2 - 1]);
      
      newCell = $(newCellArray.join(''));
      newCell.insertBefore(insertBefore);
      //insertBefore.before(newCell);
    }
    
    // cell is at end of row, so just insert it at the end of the row below
    else {
      fblog('scheduleCellInsertBelow(): false, cell is at end of row');
      
      newCell = $(newCellArray.join(''));

      cellParentNext.append(newCell); // insert within row following parent row at end
      fbdir('cellParentNext', true, cellParentNext);
      
    }
    
    //var startDate = new Date(task.data.schedule_start);
    //newCell = $('table.schedule tr[row=' + cell.parent().attr('row') + '][timeblock=afternoon] td.task[column=' + cell.attr('column') + ']');
    return newCell;
    
  }
  
  
  // converts partial-day cell to full day
  // only succeeds if other cells for this day are empty
  // always returns current cell
  function scheduleCellMakeFullDay(cell, page, task){
    var cellToRemove;
    
    //fblog($('table.schedule tr[row=' + cell.parent().attr('row') + '][timeblock=morning] td.task[column=' + getUTCDateValue(task.data.schedule_start) + ']').attr('_id'));
    
    // if this isn't currently the first cell of the day
    if ( // if this cell isn't morning
      task.data.timeblock != 'morning'
      // ||
      // cell.parent().attr('timeblock') != 'morning' 
    ){
      
      // if cell above isn't in use
      
      if (!scheduleCellGetMorning(cell, page).attr('_id')){
      
        // var _id = cell.attr('_id');
        // var id = cell.attr('id');
        var _id = task._id;
        var id = task.data.id;

        // store existing cell
        cellToRemove = cell;
        
        // move task to first cell of the day
        cell = scheduleCellGetMorning(cell, page);
        
        //var task = shared.tasks.get({value: _id});

        task.data.schedule_start = task.data.billingdate = /*dateTimeString(dp(task.data.schedule_start).set({hour: 8}))*/ ad.sqlSetHour(task.data.schedule_start, 8);
        task.data.timeblock = 'morning';
        
        // assign task_id to new cell
        cellToRemove.removeAttr('_id');
        cell.attr('_id', _id);

        // remove old cell's TD from table
        cellToRemove.remove();
    
        // make morning cell span 2 rows
        cell.attr('rowSpan', 2);
        
        // if old cell was in the current selection, convert from afternoon to morning
        if (schedule.selected.exists({id: id})){
          var thisSelection = aa.findObjectByKey(schedule.selected.data, id, 'id');
          if (thisSelection.length){
            thisSelection[0].timeblock = 'morning';
            thisSelection[0].id = ['task', thisSelection[0].row, thisSelection[0].timeblock, thisSelection[0].column].join('');
            thisSelection[0].element = $(['#', thisSelection[0].id].join(''));
            thisSelection[0].element.addClass('taskselected');
          }
        }
      }
      else { // cell above IS in use
        fblog(['scheduleCellMakeFullDay(): trying to override cell in use at ', cell.parent().attr('row'), 'morning', (dpExactDate(cell.attr('column')).toString('yyyy-MM-dd'))].join(''));
        //return false;
      }
    }
    
    
    // if this is already the first cell of the day
    else {
      
      // get other cell for this day to be removed
      cellToRemove = scheduleCellGetAfternoon(cell, page);
      if (cellToRemove.attr('_id')){
        fblog(['scheduleCellMakeFullDay(): trying to override cell in use at row ', cell.parent().attr('row'), ' (afternoon), date ', dpExactDate(cell.attr('column')).toString('MMMM d'), ' - current _id = ', cellToRemove.attr('_id')].join(''));
        //return false;
      }
      else {
        //fblog('removing cell at ' + cellToRemove.parent().attr('row') + ', ' + cellToRemove.attr('column'));
      
        // remove old cell's TD from table
        cellToRemove.remove();
    
        // make morning cell span 2 rows
        cell.attr('rowSpan', 2);
      }
    }
    
    
    return cell;
      
  }
  
  
  
  
  // update client history task row
  function clientHistoryRowUpdateContent(config){
    if (!config.row){
      return;
    }
    
    var cells = config.row.children('td');
    var width = cells.length;
    
    var done = false;
    var allRows = config.row;
    var nextRow;
    
    while (!done){
      nextRow = allRows.eq(allRows.length - 1).next();
    
      // if (rowNext.hasClass('expand-child')){
      if (nextRow.hasClass('expand-child')){
        allRows = allRows.add(nextRow);
      }
      else {
        done = true;
      }
    }
    
    
    if (config.task.moveItem || config.task.deleteItem){
    
      // // make row uneditable
      allRows.attr('task', 'unknown');
      
      // fade out rows
      allRows
        .addClass('highlightred')
        .delay(1000)
        .fadeTo(2000, .2, function(){
          allRows.remove();
          $('#historyTable').trigger('applyWidgets'); 
        });
        
    }
    
    else { // task changed
    
      // remove child rows
      allRows.filter(':gt(0)').remove();

      allRows.addClass('highlightred');
        
      // remove extra cells and make one multi-column cell
      cells.filter(':gt(2)').remove();
      cells.eq(2).attr('colSpan', width - 2).css('text-align', 'center').html('<span class="caps bold">Schedule item changed - <a class="linkRefreshHistory">Click here to refresh</a></span>');
      
      // add click handler to refresh link
      cells.find('a.linkRefreshHistory').click(function(){
        clientDisplayHistory();
      });
    
      
    }
    
    // NEED TO UPDATE INVOICES TAB
    // MAYBE FORCE RELOAD FROM SERVER IF CLICKED?
  }
  
  
  
  
  // update contents of schedule table task cell to reflect state of task data
  function scheduleCellUpdateContent(cell, page, task){
    
    if (!page){ // && pageId !== 0){
      page = scheduleGetPageById(schedule.currentPage);
    }
    if (!task){
      task = shared.tasks.get({value: cell.attr('_id')});
    }
    
    var
      contentArr = [],
      cellClassAddArr = [],
      cellClassRemoveArr = [];
      
    if (task && !task.deleteItem){ // && task.data.account_id != null){
    
      // if full day, move to morning timeblock and use full day on table
      //ifFullDay: // label to break out of this construct from inner if statements
      if (
        // task.data.billingmatrix[1] == 2 || task.data.billingmatrix[2] >= 8
        task.data.billable_quantity >= 8
      ){
        cell = scheduleCellMakeFullDay(cell, page, task);
      }
      
      // if this was previously a full day
      if (
        // (task.data.billingmatrix[1] != 2 && task.data.billingmatrix[2] < 8)// currently NOT a full day
        task.data.billable_quantity < 8
        &&
        cell.attr('rowSpan') > 1 // however, rowspan is currently more than a single row
      ){
        scheduleCellInsertBelow(cell, page);
      }
      
      //cell.removeClass('taskempty taskOpen taskRecent taskRecent');
      cellClassRemoveArr.push(
        'taskempty',
        'taskOpen',
        'taskRecent',
        'taskdirty',
        'taskAttention',
        'taskDoubleBilled',
        'taskNotes',
        'taskDeleted',
        'taskSpecial',
        'taskUnknown',
        'taskediting',
        'taskeditingReadonly'
      );
    
      
      var
        substLocationName = '',
        classLocationName = '';
      
      if (task.dirty){
        cellClassAddArr.push('taskdirty');
      }

      if (task.data.flagged){
        cellClassAddArr.push('taskAttention');
      }

      // var thisPage = scheduleGetPageById(pageId);
      if (
        aa.existsIn(page.doubleBilled.list, task.data.id)
        ||
        aa.existsIn(page.doubleBilled.list, ['_', task._id].join(''))
      ){
        cellClassAddArr.push('taskDoubleBilled');
      }
      
      if (task.data.notes){
        cellClassAddArr.push('taskNotes');
      }
      
      if (task.data.account_id === '0' || !task.data.account_id){ // "unknown" catch-all account
        cellClassAddArr.push('taskUnknown');
      }
      else if (task.data.account_id == 99){
        if (task.data.location_id == 843){
          cellClassAddArr.push('taskOpen');
        }
        else {
          cellClassAddArr.push('taskSpecial');
        }
        
        substLocationName = $.trim(task.data.location_name.replace('[', '').replace(']', ''));
        
        if (task.data.location_id == 992){
          classLocationName = 'smallCaps highlightMedium';
        }
        else {
          classLocationName = 'smallCaps';
        }
        
      }
      
      
      contentArr.push(
        '<p class="task taskLocation ',
        classLocationName, 
        '">',
        (substLocationName || task.data.location_name),
        '</p><p class="task taskLocation taskLocationShort ',
        classLocationName, 
        '">',
        (substLocationName || task.data.location_name_short),
        '</p>'
      );
      
      // if (task.data.unique_identifier && task.data.location_name.indexOf(task.data.unique_identifier) == -1){
      if (task.data.unique_identifier){
        if (task.data.location_name.indexOf(task.data.unique_identifier) == -1){
          contentArr.push('<p class="task taskLocation taskUnique smallCaps highlightSubtle">', task.data.unique_identifier, '</p>');
        }
      }
      else if (task.data.num_locations > 1 && task.data.city && task.data.location_name.indexOf(task.data.city) == -1){
      // else if (task.data.num_locations > 1 && task.data.city){
        contentArr.push('<p class="task taskLocation taskUnique smallCaps highlightSubtle">', task.data.city, '</p>');
      }
      // if (task.data.unique_identifier && task.data.location_name.indexOf(task.data.unique_identifier) == -1){
      if (task.data.unique_identifier){
        if (task.data.location_name_short.indexOf(task.data.unique_identifier) == -1){
          contentArr.push('<p class="task taskLocationShort taskUnique smallCaps highlightSubtle">', task.data.unique_identifier, '</p>');
        }
      }
      else if (task.data.num_locations > 1 && task.data.city && task.data.location_name_short.indexOf(task.data.city) == -1){
      // else if (task.data.num_locations > 1 && task.data.city){
        contentArr.push('<p class="task taskLocationShort taskUnique smallCaps highlightSubtle">', task.data.city, '</p>');
      }
      
      contentArr.push(
        (task.data.summary ? ['<p class="task taskSummary smallCaps highlightMedium">', task.data.summary, '</p>'].join('') : ''),
        // (task.data.summary ? ['</p><p class="task taskSummary highlightMedium" style="font-size: .9em">', task.data.summary].join('') : ''),
        //(task.data.tasktype_id ? ['</p><p class="taskType smallCaps highlightBright">', as.toTitleCase(aa.findObjectByKey(shared.tasktypes, task.data.tasktype_id)[0].name)].join('') : ''),
        (task.data.tasktype_id && !scheduleTaskIsInternal(task) ? ['<p class="task taskType smallCaps highlightBright">', aa.findObjectByKey(shared.tasktypes, task.data.tasktype_id)[0].name, '</p>'].join('') : ''),
        '<p class="task taskTime smallCaps '
      );

        
      // if (
        // task.data.billingmatrix // if billingmatrix is defined
        // &&
        // (
          // Number(task.data.billingmatrix[1]) || Number(task.data.billingmatrix[2]) // if at least one billingmatrix entry has a non-zero value
        // ) 
      // ){    
        // if (task.data.billingmatrix.hasOwnProperty(1) && task.data.billingmatrix[1]){
          // if (task.data.billingmatrix[1] == 2){
            // contentArr.push('highlight">full day');
          // }
          // else if (task.data.billingmatrix[1] == 1){
            // contentArr.push('highlight">half day');
          // }
          
          // if (task.data.billingmatrix.hasOwnProperty(2) && task.data.billingmatrix[2]){
            // contentArr.push(
              // ' + ',
              // task.data.billingmatrix[2],
              // ' hour',
              // task.data.billingmatrix[2] == 1 ? '' : 's'
            // );
          // }
        // }
        
        // else if (task.data.billingmatrix.hasOwnProperty(2) && task.data.billingmatrix[2]){
          contentArr.push('highlight">');
          
          var hours = task.data.billable_quantity; //task.data.billingmatrix[2];
          // var remainderHours = 0;
          
          if (hours >= 8){
            contentArr.push('full day');
            hours = am.round(hours - 8, 1);
            if (hours){
              contentArr.push(' + ');
            }
          }
          else if (hours >= 4){
            contentArr.push('half day');
            hours = am.round(hours - 4, 1);
            if (hours){
              contentArr.push(' + ');
            }
          }
          
          if (hours){
            contentArr.push(hours, ' hour', hours == 1 ? '' : 's');
          }
        // }

        if (task.data.cod){
          contentArr.push('</p><p class="task taskWarning smallCaps highlightWarning">COD');
        }
      
        if (!task.data.is_billable){
          contentArr.push('</p><p class="task taskWarning smallCaps highlightWarning">not billable');
        }
      
      // }
      // else { // if false, display 'no billing' value
        // contentArr.push('highlightWarning">not billable');
      // }
      
      contentArr.push('</p>');

      cell.removeClass(cellClassRemoveArr.join(' ')).addClass(cellClassAddArr.join(' '));
      
      task.element = cell;
      
    }
    
    
    // invalid task or task marked for deletion
    else {
      var otherCell = scheduleCellGetOther(cell, page);
      var otherTask = shared.tasks.get({value: otherCell.attr('_id')});
      
      cellClassRemoveArr.push(
        'taskempty',
        'taskOpen',
        'taskRecent',
        'taskdirty',
        'taskAttention',
        'taskDoubleBilled',
        'taskNotes',
        'taskDeleted',
        'taskSpecial',
        'taskUnknown'
      );
      
      if (
        otherTask
        &&
        (
          (
            otherTask.data.billable_quantity && otherTask.data.billable_quantity >= 8
          )
          ||
          (
            otherTask.data.billingmatrix && otherTask.data.billingmatrix[1] && otherTask.data.billingmatrix[1] == 2
          )
        )
      ){
        scheduleCellMakeFullDay(otherCell, page, otherTask);
        return;
      }
    
    
      // task entry exists, but is an empty task (usually an indicator that this task is to be deleted from database)
      if (task && task.dirty){
        cellClassAddArr.push('taskdirty');
      }
      
      if (cell.attr('rowSpan') == 2){
        cell.attr('rowSpan', 1);
        scheduleCellInsertBelow(cell, page);
      }

      cellClassAddArr.push('taskempty', 'taskOpen', 'taskDeleted');
      
      contentArr.push(cell.parent().attr('timeblock')); //(cell.parent().attr('timeblock') == 'morning' ? 'AM' : 'PM');
      
      cell.removeClass(cellClassRemoveArr.join(' ')).addClass(cellClassAddArr.join(' '));
    }

    if (contentArr.length){
      // attempt to use innerHTML to update content
      var cellId = cell.attr('id');
      if (cellId){
        var cellEl = document.getElementById(cellId);
        if (cellEl){
          document.getElementById(cell.attr('id')).innerHTML = contentArr.join('');
          return;
        }
      }
      
      // if that fails, use jquery .html() to update
      cell.html(contentArr.join(''));
    }
    
    
  }
  
  
  
  // create header row displaying date and day of week
  function scheduleCreateHeaderRow(dates, weekends, page, rowId){
    var stringArr = [];
    var width = scheduleGetColumnWidth(dates.length, page);
    var period;
    
    if (page.view == 'month'){
      period = dpExact(page.period, 'yyyy-MM');
    }
    
    if (!$(['#dateHeader', rowId].join('')).length){
    
      stringArr.push('<tr id="dateHeader',
        // page.id, '-',
        rowId, '" class="dateHeader">');
      
      // if (page.view != 'month'){
        stringArr.push('<th scope="row"></th>');
      // }

      for (var i = 0; i < dates.length; i++){
        var isWeekend = aa.existsIn(weekends, dates[i]);
        var thisDate = dpExactDate(dates[i]);

        stringArr.push(
          '<th id="th', dates[i], '" scope="col"',

          ' class="taskheader colth',
          isWeekend ? ' weekend' : '',
          schedule.today == dates[i] ? ' coltoday' : '',
          
          // period && (period.toString('MM') != thisDate.toString('MM')) ? ' otherPeriod' : '',
          
          '"',
          
          ' style="',
          'width: ', width, '%;',
          isWeekend ? ' display: none;' : '',
          '"',

          ' period="',
          // period.toString('yyyy-MM'),
          dates[i].substr(0,7),
          '"',
          
          ' column="', dates[i], '">',
          
          '<p class="taskheader dayname">', thisDate.toString('dddd'), '</p>',
          '<p class="taskheader daycondensed">', '<span class="monthNameLight">', thisDate.toString('MMM'), '</span> ', thisDate.toString('d'), '</p>',
          '<p class="taskheader daynum">', '<span class="monthNameLight">', thisDate.toString('MMMM'), '</span> ', thisDate.toString('d'), '</p>',
          
          '</th>'
        );
      }

      stringArr.push('</tr>');
    
    }
    
    // insert a divider row below each header row
    if (page.view != 'month'){
      if (!$(['#dateHeaderDivider', rowId].join('')).length){
        stringArr.push(
          '<tr id="dateHeaderDivider',
          // page.id, '-',
          rowId, '" class="divider"><td class="dividercell" colspan="',
          dates.length + 1,
          '"></td></tr>'
        );
      }
    }
    
    return stringArr.join('');
  }
  
  
  
  // create empty task rows
  function scheduleCreateRows(config, append){
    var weekends = scheduleGetWeekends(config.page);
    var stringArr = [];
    var period;
    
    var rowId;
    
    if (config.page.view == 'month'){
      period = dpExact(config.page.period, 'yyyy-MM');
    }
    
    // // insert a divider row at top of table
    // if (!append){
      // stringArr.push(
        // '<tr id="divider', config.page.id, '-top', '" class="divider"><td colspan="',
        // config.dates.length + 1,
        // '"></td></tr>'
      // );
    // }
    
    for (var iRow = 0; iRow < config.rows.length; iRow++){
    // for (var iRow = config.rows.length - 1; iRow >= 0; iRow--){
    
      // if (iRow % config.headerInterval === 0){
        stringArr.push(scheduleCreateHeaderRow(config.dates, weekends, config.page, config.rows[iRow].id));
      // }

      for (var iTime = 0; iTime < config.timeblocks.length; iTime++){
      
        // only create this row if it doesn't already exist
        if (!$(['#tr', config.rows[iRow].id, config.timeblocks[iTime]].join('')).length){
        
          stringArr.push(
            '<tr',
            ' id="tr',
            // config.page.id,
            // '-',
            config.rows[iRow].id,
            config.timeblocks[iTime],
            '"',
            ' row="',
            config.rows[iRow].id,
            '"',
            ' user="',
            config.rows[iRow].user,
            '"',
            ' timeblock="',
            config.timeblocks[iTime],
            '"'
          );

          if (iTime === 0) {
            stringArr.push(' class="timeblockRowFirst"');
          }
          else if (iTime == config.timeblocks.length - 1){
            stringArr.push(' class="timeblockRowLast"');
          }

          stringArr.push('>');

          if (iTime === 0){ // && config.page.view != 'month'){
            stringArr.push(
              '<th id="th',
              // config.page.id, '-',
              config.rows[iRow].id,
              '" scope="row" class="taskheader rowth', config.rows[iRow].user == user.id ? ' rowuser' : '', '" rowSpan="', config.timeblocks.length, '">', config.rows[iRow].header, '</th>'
            ); //aa.findObjectByKey(shared.physicists, rows[i], 'physicist_id')['name_last']
          }
          
          for (var iCol = 0; iCol < config.rows[iRow].columns.length; iCol++){
            var isWeekend = aa.existsIn(weekends, config.rows[iRow].columns[iCol]);
            var thisDate = dpExactDate(config.rows[iRow].columns[iCol]);
            stringArr.push(
              '<td id="task', config.rows[iRow].id, config.timeblocks[iTime], config.rows[iRow].columns[iCol],
              '" class="task taskempty taskOpen',
              (isWeekend ? ' weekend' : ''),
              
              // period && (period.toString('MM') != thisDate.toString('MM')) ? ' otherPeriod' : '',
              
              '"',

              ' period="',
              
              // period.toString('yyyy-MM'),
              
              config.rows[iRow].columns[iCol].substr(0,7),
              '"',

              ' column="',
              config.rows[iRow].columns[iCol],
              '"',
              
              (isWeekend && !schedule.preferences.display.weekends ? ' style="display: none;"' : ''),
              '>',
              config.timeblocks[iTime],
              '</td>'
            );
          }
          
          stringArr.push('</tr>');
          
        }
      }
      
      if (iRow < config.rows.length){
        if (!$(['#divider', config.rows[iRow].id].join('')).length){
          stringArr.push(
            '<tr id="divider',
            // config.page.id, '-',
            config.rows[iRow].id, '" class="divider"><td class="dividercell" colspan="',
            config.dates.length + 1,
            '"></td></tr>'
          );
        }
      }
      
    }
    return stringArr.join('');
  }
  


  
  
  // start ajax updater interval
  // should get all dirty tasks and save them to server, then retrieve any new tasks seamlessly in the background
  function scheduleUpdaterStart(){
    if (!false){
      scheduleUpdaterStop();
      $('#hidden_container').everyTime([schedule.performance.timeoutSaveDirty, 's'].join(''), 'saveDirtyTasks', function(){
          scheduleSaveDirty();
        }
      );  

      $('#hidden_container').everyTime([schedule.performance.timeoutGetUpdated, 's'].join(''), 'getUpdatedTasks', function(){
          scheduleGetUpdated();
        }
      );
      
      if (!schedule.initIdle){
      //$(document).unbind("idle.idleTimer active.idleTimer");
      //$.idleTimer('destroy');
      
        $(document).bind("idle.idleTimer", function(){
          fblog('idle: schedule');
          scheduleUpdaterStop();
        });
        $(document).bind("active.idleTimer", function(){
          fblog('active: schedule');
          scheduleGetUpdated();
          scheduleUpdaterStart();
        });
        
        $.idleTimer(schedule.performance.timeoutIdle * 1000);
        
        schedule.initIdle = true;
      
      }
      
      // $('#hidden_container').everyTime([schedule.performance.timeoutPrecache, 's'].join(''), 'precacheSchedule', function(){
          // if (!schedule.precache.complete){
            // schedulePrecache();
          // }
          // else {
            // $('#hidden_container').stopTime('precacheSchedule');
          // }
        // }
      // );  
    }
  }
  
  // stop ajax updater interval
  function scheduleUpdaterStop(){
    $('#hidden_container').stopTime();
  }


  
  
  // save any dirty (edited but unsaved) tasks to database via ajax
  function scheduleSaveDirty(){
    if (!false){
      // var dirty = shared.tasks.getDirty({forExport: true});
      
      var i, dirty = [];

      if (shared.tasks){
        dirty = dirty.concat(shared.tasks.getDirty({forExport: true, includeFailed: true}));
      }
      
      if (clients.dirty){
        dirty = dirty.concat(clients.dirty);
      }
      
      // if (shared.periods){
        // for (i = 0; i < shared.periods.length; i++){
          // if (shared.periods[i].tasks){
            // dirty = dirty.concat(shared.periods[i].tasks.getDirty({forExport: true, includeFailed: true}));
          // }
        // }
      // }

      // only ajax if there are items to save
      if (dirty.length){
        fblog(['scheduleSaveDirty(): saving ', dirty.length, ' dirty task(s)'].join(''));
      
        var params = {
          tasks: JSON.stringify(dirty)
        };
        
        if (!shared.metaLoaded){
          params.getMeta = true;
        }
        
        //params.revision = shared.revision;
        
        if (!false){
        $.ajax({
          url: [shared.urlRoot, '/ajaxSSGet/scheduleSave'].join(''),
          data: params,
          // dataType: 'json',
          // beforeSend: function (request, arg2){
            // fblog(['ajax beforeSend, ', arguments.length, ' args'].join(''));
            // // request.channel.contentLength = data.length;
            // // request.channel.contentType = "text/html";
            
          // },
          //type: 'POST',
          success: function (data, textStatus){
            // fblog(['ajax args ', arguments.length].join(''));
            
            var i, thisPage, task, isHistory;
            
            try {
              var decoded = shared.lastJSON.generic = eval(["(", data, ")"].join(''));
            }
            catch (err){
              fblog(['scheduleSaveDirty() decode failure: ', err.name, ', ', err.message].join(''));
              return;
            }

            fblog(['scheduleSaveDirty(): success = ', decoded.success, ', ', decoded.saved.length, ' item(s) saved, ', decoded.failed.length, ' item(s) failed'].join(''));
            
            if (decoded.success === false){
              fblog(decoded.message);
              if (decoded.auth === false){
                window.location.reload(); //href = "/login/clients";
              }
              // return false;
            }
            
            // log failed items
            for (i = 0; i < decoded.failed.length; i++){
              fblog(['scheduleSaveDirty() failed item message = ', decoded.failed[i].message].join('')); 
            }
            
            // update local copies of saved tasks
            for (i = 0; i < decoded.saved.length; i++){
              task = null;
              
              if (decoded.saved[i].period && decoded.saved[i].location){ // is history
                isHistory = true;
                
                // remove entry from clients.dirty collection
                var clientsDirtyTaskIndex = aa.indexOfObjectByKeys(clients.dirty, {
                  location: decoded.saved[i].location,
                  period: decoded.saved[i].period,
                  'data.task_id': decoded.saved[i].task_id
                });
                if (clientsDirtyTaskIndex.length){
                  clients.dirty.splice(clientsDirtyTaskIndex[0], 1);
                }
                
                // get local copy of this task to be updated
                if (clients.active && clients.active.id == decoded.saved[i].location){ // this location is still currently loaded
                  var thisPeriod = aa.findObjectByKey(shared.periods, decoded.saved[i].period, 'period');
                  if (thisPeriod.length){
                    thisPeriod = thisPeriod[0];
                    task = aa.findObjectByKey(thisPeriod.tasks.items, decoded.saved[i].task_id, 'data.id');
                  }
                }
                
                if (!task || !task.length){
                  fblog('scheduleSaveDirty(): saved task ' + decoded.saved[i].task_id + ' not found (maybe changed location?)');
                  fbdir(decoded.saved[i], true);
                  continue;
                }
                else {
                  task = task[0];
                }
              }
              else if (decoded.saved[i]._id) { // schedule item
                isHistory = false;
                task = shared.tasks.get({value: decoded.saved[i]._id});
              }
              else {
                fblog('scheduleSaveDirty(): improperly encoded saved dirty item ' + i);
                fbdir(decoded.saved[i], true);
                continue;
              }
              
              // var thisPage = scheduleGetPageById(task.data.page);
              
              // task was deleted
              if (task.deleteItem){
              
                if (isHistory){
                  delete task;
                }
                else {
                  // delete all references to task in pages collection
                  for (thisPage in schedule.pages){
                    var pageTasksIndex = aa.indexOfObjectByKey(schedule.pages[thisPage].tasks, decoded.saved[i]._id, '_id');
                    if (pageTasksIndex.length){
                      thisPage.tasks.splice(pageTasksIndex[0], 1);
                    }
                  }
                  
                  // delete task from master task list
                  shared.tasks.remove(decoded.saved[i]._id);
                  
                  // update cell
                  scheduleCellUpdateContent($(['td.task[_id=', decoded.saved[i]._id, ']'].join('')).removeAttr('_id'));
                }
              }
              
              else {
                if (task.dirty && (task.dirty.valueOf() == decoded.saved[i].dirty)){
                  task.dirty = false; // remove dirty flag
                  task.data.id = decoded.saved[i].task_id; // add database-assigned id
                  task.data.audit_start = decoded.saved[i].audit_start; // add server-based audit timestamp
                  task.data.audit_user = user.id;
                  task.data.audit_user_name = user.name;
                  if (!isHistory){
                    // remove dirty flag
                    scheduleCellUpdateContent($(['td.task[_id=', task._id, ']'].join('')));
                  }
                }
                else {
                  fblog(['scheduleSaveDirty(): local task (', task._id, ':', (task.dirty && task.dirty.valueOf() ? task.dirty.valueOf() : task.dirty), ') != saved task (', decoded.saved[i]._id, ':', decoded.saved[i].dirty, ')'].join(''));
                }
              }
            }
            
            scheduleUpdateHighlightRecent();
            
          },
          error: function (XMLHttpRequest, textStatus, errorThrown){
            fblog(['scheduleSaveDirty(): ajax error = ', textStatus, ' (', errorThrown, ')'].join(''));
            // fblog(String(errorThrown === undefined));
          }
          
        });

        }
      }

    }
  }
  
  
  
  
  function scheduleSavePreferences(){
    
    $('body').stopTime('savePreferences');
    $('body').oneTime([schedule.performance.timeoutSavePreferences, 's'].join(''), 'savePreferences', function(){
    
      var spd = schedule.preferences.display;
      
      if (spd.view == "month"){
        spd.physicistsMonthly = spd.physicists;
      }
      else if (spd.view == "week"){
        spd.physicistsWeekly = spd.physicists;
      }
      
      // remove physicistsWeekly from display preferences before saving
      // var preferences = aa.objectClone(schedule.preferences);
      var preferences = _.clone(schedule.preferences);
      //delete preferences.display.physicistsWeekly;
      
      var params = {
        preferences: JSON.stringify(preferences)
      };
      
      ad.stopwatchStart(['ajaxScheduleSavePreferences', params.dates].join(''));
      
      $.ajax({
        url: [shared.urlRoot, '/ajaxSSGet/scheduleSavePreferences'].join(''),
        data: params,
        //type: 'POST',
        success: function (data, textStatus){
          var logstringArr = [['scheduleSavePreferences(): saved (', (ad.stopwatchEnd(['ajaxScheduleSavePreferences', params.dates].join('')) / 1000), 's)'].join('')];
        
        },
        error: function (XMLHttpRequest, textStatus, errorThrown){
          fblog(['scheduleSavePreferences(): ajax failure = ', textStatus, ' (', errorThrown, ')'].join(''));
        }
        
      });
      
    });  
    
  
    
  }


  
  
  // compare any currently loaded tasks against server, and retrieve those that are newer or not yet loaded
  function scheduleGetUpdated(){
    if (schedule.start && !schedule.gettingUpdates){
    
      schedule.gettingUpdates = true;
      // fblog('getting updated tasks (if any) from server');
    
      var spd = schedule.preferences.display;
      var i;
    
      // get all currently loaded tasks for this time period
      var current = [];
      
      // remove physicistsWeekly from display preferences before saving
      //var preferences = aa.objectClone(schedule.preferences);
      //delete preferences.display.physicistsWeekly;
      
      var params = {
        preferences: JSON.stringify(schedule.preferences)
      };
      
      // setup params for month view
      if (spd.view == 'month'){
        var currentPage = scheduleGetPageById(schedule.currentPage);
        var currentWeeks = [];
        var currentPhysicist;
       
        for (i = 0; i < currentPage.weeks.length; i++){
          // only get subset of tasks for this physicist
          currentPhysicist = aa.findObjectByKey(currentPage.weeks[i].physicists, spd.physicists[0], 'physicist_id');
          if (currentPhysicist.length){
            current = aa.getSubset(currentPhysicist[0].tasks, ['id', 'audit_start'], 'data'); //JSON.stringify(findObjectByKey(current, spd.physicists[0], 'physicist_id'));
          }
          
          currentWeeks.push({
            start: currentPage.weeks[i].start,
            end: currentPage.weeks[i].end,
            current: current
          });
        }
        
        params.weeks = JSON.stringify(currentWeeks);
        params.dateStart = currentPage.start;
        params.dateEnd = currentPage.end;
        params.physicists = JSON.stringify([spd.physicists[0]]);
      }
    
      // setup params for week view
      else {
        // determine if this date range has already been ajaxed
        var dateTemp = schedule.start;
        var dates = [];
          
        // only do this if dates collection has already been created
        // loop while temp date is less than or equal to end date
        while (dateTemp <= schedule.end){
          dates.push(dateTemp); // add this date to array to be ajaxed
          dateTemp = dateString(dpExactDate(dateTemp).addDays(1)); // increment day
        }  
      
        params.dates = JSON.stringify(dates);
        params.current = JSON.stringify(
          shared.tasks.get({
            value: {start: [schedule.start, ' 00:00:00'].join(''), end: [schedule.end, ' 23:59:59'].join('')},
            key: 'schedule_start',
            returnKeys: ['id', 'audit_start']
          }) || []
        );
        
        params.dateStart = schedule.start;
        params.dateEnd = schedule.end;
        
      }
      
      // fblog(dates.join(', '));
      
      //params.revision = shared.revision;
      
      ad.stopwatchStart(['ajaxScheduleGetUpdated', params.dates].join(''));
      
      $.ajax({
        url: [shared.urlRoot, '/ajaxSSGet/scheduleGetUpdated'].join(''),
        data: params,
        //type: 'POST',
        success: function (data, textStatus){
          statusBannerHide();
          var logstringArr = [['scheduleGetUpdated(): retrieved from server (', (ad.stopwatchEnd(['ajaxScheduleGetUpdated', params.dates].join('')) / 1000), 's)'].join('')];
        
          try {
            var decoded = shared.lastJSON.generic = eval(["(", data, ")"].join(''));
            
            if (decoded.success === false){
              fblog(decoded.message);
              if (decoded.auth === false){
                window.location.reload(); //href = "/login/clients";
              }
              return false;
            }
            
          }
          catch (err){
            fblog(['scheduleGetUpdated() decode failure: ', err.name, ', ', err.message].join(''));
            schedule.gettingUpdates = false;
            return;
          }
          
          // highlight current day if changed
          if (decoded.today != schedule.today){
            fblog(['scheduleGetUpdated(): updating current day from ', schedule.today, ' to ', decoded.today].join(''));
            schedule.today = decoded.today;
            scheduleUpdateToday();
            // $('th[scope=col].coltoday').removeClass('coltoday');
            // $(['th[scope=col][column=', schedule.today, ']'].join('')).addClass('coltoday');
          }
          
          if (decoded.schedule_lock){
            scheduleParseLock(decoded);
          }
          
          var taskCount = 0;
          var deletedCount = 0;
          
          if (decoded.weeks){
            for (var i = 0; i < decoded.weeks.length; i++){
              for (var j = 0; j < decoded.weeks[i].physicists.length; j++){
                scheduleParseUpdated(decoded.weeks[i].physicists[j]);
                taskCount += decoded.weeks[i].physicists[j].tasks.length;
                deletedCount += decoded.weeks[i].physicists[j].deleted.length;
              }
            }
          }
          else {
            scheduleParseUpdated(decoded);
            taskCount = decoded.tasks.length;
            deletedCount = decoded.deleted.length;
          }
          
          if (taskCount){
            logstringArr.push([taskCount, ' task', (taskCount == 1 ? '' : 's'), ' added/updated'].join(''));
          }
          if (deletedCount){
            logstringArr.push([deletedCount, ' task', (deletedCount == 1 ? '' : 's'), ' deleted'].join(''));
          }
          if (!taskCount && !deletedCount){
            logstringArr.push('no changes');
          }
          
          fblog(logstringArr.join(', '));
          
          scheduleUpdateHighlightRecent();
          
          schedule.gettingUpdates = false;
        },
        error: function (XMLHttpRequest, textStatus, errorThrown){
          fblog(['scheduleGetUpdated(): ajax failure = ', textStatus, ' (', errorThrown, ')'].join(''));
          schedule.gettingUpdates = false;
          statusBannerConnectionError();
        }
        
      });
    }
  }


  
  function statusBanner(content){
    var bannerEl = $('#statusBanner');
    
    if (!bannerEl.length){
      bannerEl = $('<div id="statusBanner" style="border-bottom: 1px solid #646464; position: fixed; width: 100%; z-index: 10000; text-align: center; background-color: #ffb; display: none;" />').prependTo('body');
    }
    
    // bannerEl.html('<b>WARNING:</b> Lost connection with server.  Changes might not be saved.  <br/>If this message persists, please check your internet connection and try reloading this page.').show();
    bannerEl.html(content).show();
    $('#viewport_top').css('border-top-width', '4em');
  }
  
  function statusBannerConnectionError(){
    statusBanner('<b>WARNING:</b> Lost connection with server.  Changes might not be saved.<br/>If this message persists, please check your internet connection and try reloading this page.');
  }
  
  function statusBannerHide(){
    $('#statusBanner').hide();
    $('#viewport_top').css('border-top-width', '1em');
  }

  
  
  function scheduleParseUpdated (decoded){

    var task, cell, cellsToUpdate;
    var i, j, k;
    
    var dti;
    
    var page;
    var deletedPages = [];
    var found;
    
    // update local copies of saved tasks
    for (i = 0; i < decoded.tasks.length; i++){
    
      dti = decoded.tasks[i];
    
      // add tasks with previous deletions to page.deleted to be flagged when open
      if (dti.audit_user_end){
        page = scheduleGetPageByDate(dti.schedule_start_date);
        for (j = 0; j < page.length; j++){
          found = aa.findObjectByKeys(page[j].deleted, {
            schedule_start_date: dti.schedule_start_date,
            physicist_id: dti.physicist_id,
            timeblock: dti.timeblock
          });
          
          if (!found.length){
            page[j].deleted.push(dti);
            aa.pushUnique(deletedPages, page[j].id);
          }
        }
        continue;
      }
      
      task = shared.tasks.get({value: dti.id, key: 'id', getAll: true});
      if (task.length){
        for (j = 0; j < task.length; j++){
          fblog(['scheduleGetUpdated(): finding existing task id ', dti.id, ', found ', task.length, ' items, _id = ', task[j]._id].join(''));
          // update current task with new data from server
          shared.tasks.update(dti, task[j]._id);
          cell = scheduleCellGet(task[j]);
          if (!cell.attr('_id')){
            cell.attr('_id', task[j]._id);
          }
          scheduleCellUpdateContent(cell, null, task[j]);
          cell.addClass('taskRecent');
        }
        
        task = task[0];
      }
      else {
        var result = shared.tasks.add([dti]);
        var _id = result.last_id;
        task = shared.tasks.get({value: _id});
        
        // var page = scheduleGetPageById(task.data.page);
        // if (page){
          // page.tasks.push(task);
        // }
        
        for (j in schedule.pages){
          if (task.data.schedule_start_date >= schedule.pages[j].start && task.data.schedule_start_date <= schedule.pages[j].end){
            schedule.pages[j].tasks.push(task);
          }
        }
        
        cell = scheduleCellGet(task);
        cell.attr('_id', _id);
        fblog(['scheduleGetUpdated(): adding new task id ', dti.id, ', _id = ', _id].join(''));
        scheduleCellUpdateContent(cell);
        cell.addClass('taskRecent');
      }
      
      cellsToUpdate = scheduleIsDoubleBilled({
        // pageId: task.data.page, //pageId,
        startDate: task.data.schedule_start_date,
        locationId: task.data.location_id
      });
      
      for (j in cellsToUpdate){
        // update contents of schedule table cell
        scheduleCellUpdateContent(cellsToUpdate[j].element);
      }
      
      scheduleUpdateHighlightRecent();


    }
    
    // delete any tasks that are now missing
    for (i = 0; i < decoded.deleted.length; i++){
    
      dti = decoded.deleted[i];
      
      task = shared.tasks.get({value: dti, key: 'id', getAll: true});
      if (task.length){
        for (j = 0; j < task.length; j++){
          if (task[j].dirty){
            fblog(['scheduleGetUpdated(): attempting to delete unsaved dirty task (task WILL NOT be deleted) - id ', dti, ', found ', task.length, ' items, _id = ', task[j]._id].join(''));
          }
          else {
            fblog(['scheduleGetUpdated(): deleting existing task id ', dti, ', found ', task.length, ' items, _id = ', task[j]._id].join(''));
            shared.tasks.remove(task[j]._id);
            scheduleCellUpdateContent($(['td.task[_id=', task[j]._id, ']'].join('')).removeAttr('_id'));
            
            // add task to deleted collection
            page = scheduleGetPageByDate(task[j].data.schedule_start_date);
            for (k = 0; k < page.length; k++){
              found = aa.findObjectByKeys(page[k].deleted, {
                schedule_start_date: task[j].data.schedule_start_date,
                physicist_id: task[j].data.physicist_id,
                timeblock: task[j].data.timeblock
              });
              
              if (!found.length){
                page[k].deleted.push(task[j].data);
                aa.pushUnique(deletedPages, page[k].id);
              }
              // page[k].deleted.push(task[j].data);
              
              // aa.pushUnique(deletedPages, page[k].id);
            }
          }
          
        }
        
      }
    }
    
    // add deleted flags to all modified schedule pages
    for (i = 0; i < deletedPages.length; i++){
      scheduleUpdateDeletedStatus(schedule.pages[deletedPages[i]]);
    }
  }
  
  
  

  
  // precache adjacent schedule pages before they are requested manually
  // attempts to improve apparent loading speed
  function schedulePrecache(){
    var currentPrecache = aa.findObjectByKey(schedule.pages, true, 'precache');
    var currentGet = aa.findObjectByKey(schedule.pages, true, 'retrieving');
    
    if (!currentPrecache.length && !currentGet.length){ // only continue if precaching not already in progress
      
      fblog('schedulePrecache(): in progress');
      var dates = schedulePrecacheFindNext();
      
      if (dates.length){
        fblog('schedulePrecache(): dates found, getting schedule');
        var pageId = scheduleAddPage(schedule.precache);
        aa.findObjectByKey(schedule.pages, pageId, 'id')[0].precache = true;
        scheduleGet(pageId, true);
      }
      else {
        ad.stopwatchStart('countCells');
        fblog('schedulePrecache(): all precaching complete');
        schedule.precache.complete = true;
      }
    }
  }
  
  
  
  // find next series of uncached dates
  function schedulePrecacheFindNext(){
    // precache offset should follow pattern of 1, -1, 2, -2, 3, -3, etc.
  
    schedule.precache.offset *= -1; // always invert

    if (schedule.precache.offset >= 0){ // if currently zero or positive, increment
      schedule.precache.offset++;
    }
    
    // end search if we've exceeded max offset (number of weeks to cache forward and backward)
    if (schedule.precache.offset > schedule.precache.maxOffset){
      return [];
    }
    
    schedule.precache.start = dateString(dpExactDate(schedule.start).addWeeks(schedule.precache.offset));
    schedule.precache.end = dateString(dpExactDate(schedule.end).addWeeks(schedule.precache.offset));

    fblog(['schedulePrecacheFindNext(): offset = ', schedule.precache.offset, ' (', schedule.precache.start, ' - ', schedule.precache.end, ')'].join(''));

    var dates = scheduleGetDatesNotLoaded({
      start: schedule.precache.start,
      end: schedule.precache.end
    });
    
    if (!dates.length){
      dates = schedulePrecacheFindNext();
    }
    
    return dates;
  }
  
  
  
  // pass in object containing either an array of dates:
  //   config = {
  //     dates: ['2008-01-01', '2008-01-02', '2008-01-03']
  //   }
  // or an object containing start and end dates for a range
  //   config = {
  //     start: '2008-01-01',
  //     end: '2008-01-07'
  //   }
  function scheduleGetDatesNotLoaded(config){
    var dates = [];
    
    if (config.dates){ // array passed
      dates = config.dates;
    }
    
    else if (config.start && config.end){ // range passed
      while (config.start <= config.end){
        dates.push(config.start);
        config.start = dateString(dpExactDate(config.start).addDays(1));
      }
    }
    
    for (var i = dates.length - 1; i >= 0; i--){
      if (aa.existsIn(schedule.dates, dates[i])){
        dates.splice(i, 1);
      }
    }
    
    
    return dates;
  }
  
  
  
  
  
  
  








  
  
















  
  
  
  
  
  
  
  function billingGetNext(config, event, element){
    $('#billingLockControls').hide();
    $('#billingSummary').hide();
    $('#breakdown').remove();
  
    billingGet({
      start: dateString(dpExactDate(billing.start).addMonths(1)),
      target: config.formDef.addedInjectTarget
    });
  }
  
  function billingGetPrevious(config, event, element){
    $('#billingLockControls').hide();
    $('#billingSummary').hide();
    $('#breakdown').remove();
    
    billingGet({
      start: dateString(dpExactDate(billing.start).addMonths(-1)),
      target: config.formDef.addedInjectTarget
    });
  }


  
  
  
  // get billing information
  function billingGet(config){
  
    // make sure to stop updater interval
    updaterStop();
    $('body').stopTime('billingGet');
    
    if (!config.start){
      config.start = shared.systemStart; //'2008-07-01';
      config.end = Date.today().addYears(1).addMonths(1).set({day: 1}).addSeconds(-1).toString('yyyy-MM-dd'); //Date.today().toString('yyyy-MM-dd');
    }
    else {
      // start date equals first of the month
      config.start = dateString(dpExactDate(config.start).set({day: 1}));
      // end date equals last second of last day of the month
      config.end = dateString(dpExactDate(config.start).addMonths(1).addSeconds(-1));
      //config.end = new Date(config.start.valueOf()).addDays(2).addSeconds(-1);
    }

    billing.start = config.start;
    billing.end = config.end;
    
    // fblog(['billingGet(', aa.var_dump(config), ')'].join(''));
    
    
    var startDate = dpExactDate(billing.start);
    var endDate = dpExactDate(billing.end);

    // update page title
    if (shared.activeInterface == 'billing'){
      $('#page-title-dynamic').html(
        startDate.toString('MMMM yyyy')

      );    
    }
    
    
    setTimeout(function(){
      billingGetProc(config);
    }, 10);
  }
  
  function billingGetProc(config){

    var params = {};
    var dates = [];
    
    var startDate = dpExactDate(billing.start);
    var endDate = dpExactDate(billing.end);
    
    // billingSelectNone();
  
    // var injectTarget = $(['#', config.target].join(''));
    // var injectTargetHistory = $(['#', config.targetHistory].join(''));
    var injectTargets = [];
    if (config.target){
      injectTargets.push(['#', config.target].join(''));
    }
    if (config.targetHistory){
      injectTargets.push(['#', config.targetHistory].join(''));
    }
    
    injectTargets = $(injectTargets.join(', '));

    $('#formBilling .body:first')
      .css({
        'border-bottom-width': 0,
        'padding-bottom': '1em'
      })
      .children('.item')
      .css({
        'margin-top': '1em',
        'margin-left': '1em'
      });
    
    if (shared.activeInterface == 'billing'){
      injectTargets.css('border-top-width', 0);
    }
    
    injectTargets
      .css({
        'padding-bottom': '1.5em'
      })
      .html([
        '<div class="tableControlsContainer tableControlsContainerBorder"></div>',
        '<table><tr><td><p style="margin-top: 1.25em"><img src="/common/assets/images/large-loading.gif" align="absmiddle"/>&nbsp;<span class="loadingText">Retrieving account information...</span></p></td></tr></table>'
      ].join(''));
    
    
    params.getMeta = true;
    
    var periods = [];
    
    while (startDate < endDate){
      periods.push(startDate.toString('yyyy-MM'));
      startDate = startDate.addMonths(1);
    }
    
    periods.reverse();
        
    params.periods = JSON.stringify(periods);
    params.start = billing.start;
    params.end = billing.end;
      
    if (config.accounts){
      params.accounts = JSON.stringify(config.accounts);
    }

    if (config.location_id){
      params.location_id = config.location_id;
    }
    
    $('body').oneTime(1000, 'billingGet', function(){
        
        ad.stopwatchStart('ajaxBilling');
        
        //params.revision = shared.revision;

        $.ajax({
          url: [shared.urlRoot, '/ajaxSSGet/billingGet'].join(''),
          mode: 'abort',
          port: 'billingGet',
          data: params,
          //type: 'POST',
          success: function (data, textStatus){
            //fblog(textStatus);
            fblog(['billingGet(): billing data retrieved from server (', (ad.stopwatchEnd('ajaxBilling') / 1000), 's)'].join(''));
            $('span.loadingText', injectTargets).text('Preparing data...');
            setTimeout(function(){
              billingParse(data, config);
            }, 10);
          },
          error: function (XMLHttpRequest, textStatus, errorThrown){
            fblog(['billingGet(): billing data retrieved from server (', (ad.stopwatchEnd('ajaxBilling') / 1000), 's)'].join(''));
            fblog(['billingGet(): ajax failure = ', textStatus, ' (', errorThrown, ')'].join(''));
          }
        });
        
    });
      

  }
  









  
  

  function billingParse (data, config) {
  
    ad.stopwatchStart('billingParse');
  
    try { // this JSON.decode will fail if PHP passes back an error code, usually because of DB connection failure
      // convert JSON response to a MixedCollection object
      var decoded = shared.lastJSON.billing = eval(["(", data, ")"].join(''));
      
      if (decoded.success === false){
        fblog(decoded.message);
        if (decoded.auth === false){
          window.location.reload(); //href = "/login/clients";
        }
        return false;
      }
      
    }
    catch (err){
      fblog(['billingParse(): JSON decode error = ', err.name, ', ', err.message].join(''));
    }
    
    // only reset shared periods collection if this request wasn't specified to be appended to currently loaded data
    if (!config.append){
      shared.periods = [];
      billing.invoices = [];
    }

    scheduleParseLock(decoded);
    
    // aa.concatUnique(billing.paperlessStatus, decoded.progressPaperless, 'invoice');
    billing.paperlessStatus = decoded.progressPaperless;
    
    //billing.parseCurrent = 0;
    
    billingParsePeriods(0, decoded, config);
    
    // var i, j, thisPeriod;
    
    // for (i = 0; i < decoded.length; i++){
    
      // thisPeriod = aa.findObjectByKey(shared.periods, decoded[i].period, 'period');
      // if (!thisPeriod.length){
        // shared.periods.push({
          // period: decoded[i].period,
          // start: dateString(dpExact(decoded[i].period, 'yyyy-MM').set({day: 1})), // decoded[i].start,
          // end: dateString(dpExact(decoded[i].period, 'yyyy-MM').addMonths(1).addSeconds(-1)) //decoded[i].end
        // });
        // thisPeriod = shared.periods[shared.periods.length - 1];
      // }
      // else {
        // thisPeriod = thisPeriod[0];
      // }
      
      

      // if (!thisPeriod.metaLoaded){
        // thisPeriod.physicists = decoded[i].physicists;
        // thisPeriod.physicistList = [];
        // for (j in thisPeriod.physicists){
          // thisPeriod.physicistList[thisPeriod.physicists[j].physicist_id] = {
            // name_first: thisPeriod.physicists[j].name_first,
            // name_last: thisPeriod.physicists[j].name_last,
            // tasktype_id: thisPeriod.physicists[j].tasktype_id
          // };
        // }
        // thisPeriod.tasktypes = decoded[i].tasktypes;
        // // thisPeriod.billingrates = decoded.billingrates;
      // }
      
      // thisPeriod.meta = decoded[i].meta;
      // thisPeriod.double_billed = decoded[i].double_billed;
      // thisPeriod.tasks = new dataCollection({dataPrototype: taskObject});

      // thisPeriod.tasks.add(decoded[i].tasks);
      // thisPeriod.tasks.add(decoded[i].training);
      // thisPeriod.metaLoaded = true;
     
      // thisPeriod.serverUTCOffset = Number(decoded[i].UTCOffset) || 0;
      
    // }
    
  }
  
  
  
  
  
  
  function billingParsePeriods(index, decoded, config){
    // fblog(['billingParsePeriods(index = ', index, ', decoded.length = ', decoded.length, ')'].join(''));
    if (index >= decoded.periods.length){
      billingParseFinish(decoded, config);
      return;
    }
    
    var j;
    var period = decoded.periods[index];
    var thisPeriod = aa.findObjectByKey(shared.periods, period, 'period');
    if (!thisPeriod.length){
      shared.periods.push({
        period: period.period,
        start: dateString(dpExact(period.period, 'yyyy-MM').set({day: 1})), // decoded[i].start,
        end: dateString(dpExact(period.period, 'yyyy-MM').addMonths(1).addSeconds(-1)) //decoded[i].end
      });
      thisPeriod = shared.periods[shared.periods.length - 1];
    }
    else {
      thisPeriod = thisPeriod[0];
    }
    
    

    if (!thisPeriod.metaLoaded){
      thisPeriod.physicists = period.physicists;
      thisPeriod.physicistList = [];
      for (j in thisPeriod.physicists){
        thisPeriod.physicistList[thisPeriod.physicists[j].physicist_id] = {
          name_first: thisPeriod.physicists[j].name_first,
          name_last: thisPeriod.physicists[j].name_last,
          tasktype_id: thisPeriod.physicists[j].tasktype_id
        };
      }
      shared.tasktypes = thisPeriod.tasktypes = period.tasktypes;
      shared.taskrecurrences = period.taskrecurrences;
      // thisPeriod.billingrates = decoded.billingrates;
    }
    
    //thisPeriod.meta = period.meta;
    thisPeriod.accounts = period.accounts;
    thisPeriod.progressPaperless = period.progressPaperless;
    
    thisPeriod.double_billed = period.double_billed;
    thisPeriod.tasks = new dataCollection({dataPrototype: taskObject});

    thisPeriod.tasks.add(period.tasks);
    thisPeriod.tasks.add(period.training);
    
    thisPeriod.taskItems = period.taskItems;
    
    thisPeriod.metaLoaded = true;
   
    thisPeriod.serverUTCOffset = Number(period.UTCOffset) || 0;
    
    if (++index < decoded.periods.length){
      setTimeout(function(){
        billingParsePeriods(index, decoded, config);
      }, 50);
    }
    else {
      billingParseFinish(decoded, config);
    }
  }
  
  
  
  
  function billingParseFinish(decoded, config){
  
    var i;
  
    fblog(['billingParse(): billing parsed (', (ad.stopwatchEnd('billingParse') / 1000), 's)'].join(''));
    ad.stopwatchStart('billingOutput');
    
    if (shared.activeInterface != 'billing') {
      if (decoded.periods.length > 0){
        var outputResult;
        var renderable = config.append; //false;
        var historyArray = [];
        var renderablePeriods = [];
        var thisMonth = Date.today().addMonths(1).set({day: 1}).addSeconds(-1).toString('yyyy-MM');

        // if (config.append){
          // var previouslyRenderable = aa.findObjectByKey(shared.periods, true, 'renderable');
          // for (i = 0; i < previouslyRenderable.length; i++){
            // renderablePeriods.push(previouslyRenderable[i].period);
          // }
          
        // }
        
      
        for (i = 0; i < shared.periods.length; i++){
        
          if (!shared.periods[i].renderedBilling){
          
            outputResult = billingOutput({
              target: config.target,
              targetHistory: config.targetHistory,
              period: shared.periods[i],
              clear: (i == 0 ? true : false),
              clearHistory: !renderable, //(i == 0 ? true : false),
              endHistory: (((i == shared.periods.length - 1) && !config.append) ? true : false),
              showPeriod: true
            });
            
            if (outputResult.renderable){
              shared.periods[i].renderable = renderable = true;
              
              renderablePeriods.push(shared.periods[i].period);
              
              if (outputResult.historyHTML){
                // fblog(outputResult.historyHTML.join(''), true);
                historyArray = historyArray.concat(outputResult.historyHTML);
              }
            }
            
            shared.periods[i].renderedBilling = true;
          
          }
          
        }
        
        if (renderable && !config.append){
          billingDisplayControls();
        }
        
        
        if (config.targetHistory){
        
          var injectHistory = $(['#', config.targetHistory].join(''));
          
          if (historyArray.length){
            //$('.tableControlsContainer', injectHistory).removeClass('tableControlsContainerBorder');
            
            // update history print dropdown with available periods
            var selected = false;
            var selectEl = $('#selectHistoryPrintPeriod');
            var optionHTML = [];
            var hasThisMonth = aa.existsIn(renderablePeriods, thisMonth);
            var iDate;
            
            for (i = 0; i < renderablePeriods.length; i++){
              optionHTML.push('<option value="', renderablePeriods[i], '"');
              if (!config.append && !selected){ // only change if this is initially being created
                if (
                  (hasThisMonth && renderablePeriods[i] == thisMonth)
                  ||
                  (!hasThisMonth)
                ){
                  optionHTML.push(' selected="selected"');
                }
              }
              iDate = dpExactDate([renderablePeriods[i], '-01'].join(''));
              optionHTML.push('>', iDate.toString('MMMM yyyy'), '</option>');

            }
            selectEl.append(optionHTML.join(''));
            
            if (!config.append){ // don't assign more than once
              // add history print link functionality
              $('#linkHistoryPrint').click(function(){
                // var selectedPeriod = Date.parse($('#selectHistoryPrint option:selected').text()).toString('yyyy-MM');
                var selectedPeriod = $('#selectHistoryPrintPeriod option:selected').val();
                var selectedPhysicists = $('#selectHistoryPrintPhysicists option:selected').val();
                
                if (config.accounts[0] && config.accounts[0] > 0){
                  window.open([
                    shared.urlRoot,
                    '/schedule.pdf?print',
                    '&date=', selectedPeriod,
                    '&location=', config.location, //config.accounts[0],
                    selectedPhysicists ? ['&', selectedPhysicists].join('') : '',
                    '&weekends'
                    
                  ].join(''));
                }
                
              });
            }
          
            // display history content
            if (config.append){
              var current = $('#historyTable > tbody');
              // if (!current.length){ // some browsers don't create the tbody by default
                // current = $('#historyTable table.billingTableItemization');
              // }
              current.append(historyArray.join(''));
              // current.replaceWith(clone);
              // $('#historyTable table.billingTableItemization').after(['<table>', historyArray.join(''), '</table>'].join(''));
              
              // update display of locations based on dropdown
              historyTableLocationDisplayUpdate($('#selectHistoryShowLocations option:selected').attr('value'));
              
            }
            else {
              injectHistory.append(historyArray.join(''));
            }
            
            $('#historyTableContainer table.groupContainer').css('border-top-width', 0);
            // injectHistory[0].innerHTML = historyArray.join('');
            // if (outputResult.historyForm){
              //formBuild(outputResult.historyForm, outputResult.historyForm.injectTarget);
            // }
            // make sortable
            var sortTable = $('#historyTableContainer table.billingTableItemization').attr('id', 'historyTable');
            if (sortTable.length && sortTable.get(0).rows.length > 1){
              sortTable.tablesorter({
                deferWidgets: false,
                headers: {0: {order: 1} },
                
                sortList: [[0,1]],
                textExtraction: 'simple',
                widgets: ['zebra'],
                widgetZebra: {css: ['evenRow', '']}
              });
              
              // manually apply widgets after applying pager
              //sortTable.trigger("applyWidgets");

              //tableRowHoverSetup(sortTable, 'highlightgreen');
              
              
              // enable row click and double-click behavior
              tableRowClickSetup({
                table: sortTable,
                cssClick: 'highlightblue',
                cssDoubleClick: 'highlightbluedark',
                onDoubleClick: function(el){
                
                  if (aa.existsIn(permissions, 'debug')){

                    var task = el.attr('task');
                    
                    if (task){
                      fblog('dblclick ' + task);
                      
                      if (task != 'unknown'){
                        setTimeout(function(){
                          scheduleEditShow({
                            task: task,
                            el: el
                          });
                        }, 50);
                        
                        return true;
                      }
                    }

                  }
                  
                  return false;
                }
              });
            }
          }
          else if (!$('#historyTable tr').length){
          // else {
            injectHistory.html('<div class="tableControlsContainer tableControlsContainerBorder"></div><div class="billingEmpty">No schedule items found</div>');
          }
        }
        
        
        if (decoded.oldest == true){
          billingOlderLink($.extend(config, {forceOldest: true}));
        }
        else {
          billingOlderLink(config);
        }
        
      }
      else if (config.append) {
        billingOlderLink($.extend(config, {forceOldest: true}));
      }
      else {
        fblog('billingParse(): no billing periods found');
        $(['#', config.targetHistory].join('')).html('<div class="tableControlsContainer tableControlsContainerBorder"></div><div class="billingEmpty">No schedule items found</div>');
        $('#injectBilling').html('<div class="tableControlsContainer tableControlsContainerBorder"></div><div class="billingEmpty">No billing items found</div>');
      }
    
      
    }
    
    else if (decoded.periods.length == 1){
      thisPeriod = aa.findObjectByKey(shared.periods, decoded.periods[0].period, 'period');
      if (thisPeriod.length){
        billingOutput({
          target: config.target,
          targetHistory: config.targetHistory,
          period: thisPeriod[0],
          clear: true,
          clearHistory: true,
          endHistory: true,
          showAccountName: true
        });
        
        billingDisplayControls();
        billingPaperlessSendingUpdate();
        billingPaperlessUnsentUpdate();
      }
      else {
        fblog('period not found');
        return;
      }
    }

    if (!$('#billingTableControlsContainer').next().length){
      $('#billingTableControlsContainer').empty().after('<div class="billingEmpty">No billing items found</div>');
    }
    else {
      // updaterStart('billingProgressPaperless');
    }
    
    $('#srt_clientDisplayHistory, #srt_clientDisplayInvoices').removeClass('icon-loading iconright');
    
    // billingOutput({target: target});
    
    fblog(['billingOutput(): billing rendered (', (ad.stopwatchEnd('billingOutput') / 1000), 's)'].join(''));

  }
  
  
  
  function billingDisplayControls(){
    var formDef = new formObject({
      id: 'billingTableControls',
      injectTarget: 'billingTableControlsContainer',
      addedInjectTarget: 'none',
      prefix: 'btc_',
      noFrame: true,
      noGap: true,
      noFormElement: true,
      submitType: 'ajax',
      //focus: 'location_name',
      //editTrack: 'activeEdit.dirty',
      validation: aa.objectClone(validationDefault),
      fields: [
        {
          name: 'displayInvoices',
          label: 'Display',
          labelInline: true,
          type: 'select',
          inline: true,
          // required: true,
          values: [
            { id: 0, name: 'All invoices', selected: true },
            { id: 1, name: 'Paperless invoices' },
            { id: 2, name: 'Postal invoices' }
            // { id: 3, name: 'Just Me' }
          ],
          // separator: ' + ',
          // selectExclusive: [0, 1, 2], //, 3],
          selectCallback: function() {
            var val = $("option:selected", $(this)).attr('value');
            switch (val){
              case "1":
                $('table.billingTable tr.account.postal').hide();
                $('table.billingTable tr.account.paperless').show();
                break;
                
              case "2":
                $('table.billingTable tr.account.paperless').hide();
                $('table.billingTable tr.account.postal').show();
                break;
                
              case "0":
              default:
                $('table.billingTable tr.account').show();
                break;
            }
            
            billingSelectNone();
          }

        },
        
      
        {
          name: 'displayLocations',
          label: 'for',
          labelInline: true,
          type: 'select',
          inline: true,
          // required: true,
          values: [
            { id: 'allLocations', name: 'All account locations', selected: true },
            { id: 'thisLocation', name: 'Just this location' }
          ],
          displayCondition: shared.activeInterface != 'billing' && clients.active && clients.active.id && Number(clients.active.num_locations > 1),
          selectCallback: function() {
            // var val = $("option:selected", $(this)).attr('value');
            
            billingTableLocationDisplayUpdate();
            
            billingSelectNone();
          }

        },
        
      
        {
          type: 'raw',
          content: '<span style="margin-left: .5em; margin-right: .5em" id="billingTableSelectedStatus"></span>'
        },
        {
          type: 'button',
          action: 'billingViewSelected',
          target: 'billingTable',
          style: 'inline',
          text: 'View Selected Invoices'
        },
        {
          type: 'button',
          action: 'billingSelectAll',
          target: 'billingTable',
          style: 'inline',
          text: 'Select All'
        },
        {
          type: 'button',
          action: 'billingSelectNone',
          target: 'billingTable',
          style: 'inline',
          text: 'Select None'
        },
        {
          type: 'button',
          name: 'billingSendPaperlessUnsent',
          action: 'billingSendPaperlessUnsent',
          target: 'billingTable',
          style: 'inline',
          //css: 'hidden',
          css: 'buttonDisabled', // disabled by default, only enabled if unsent paperless invoices are parsed
          displayCondition: function(){
            return shared.activeInterface == 'billing' && scheduleIsLocked(billing.start);
          },
          text: 'Send Unsent Paperless Invoices'
        }
      ]
    });

    forms[formDef.id] = formDef;

    formBuild(formDef, formDef.injectTarget);
    
    billingSelectedStatusUpdate();
    // {
      // target: 'billingTableSelectedStatus',
      // disable: 'btc_button_billingTable_billingViewSelected',
      // singular: 'invoice' //(shared.activeInterface == 'billing' ? 'account' : 'billing period')
    // });
  }
  
  
  
  
  
  function billingTableLocationDisplayUpdate(){
    var val = $("#btc_displayLocations > option:selected").attr('value');
    var container = $('#injectBilling');
    var billingTables = container.children('table.billingTable');
    var hide = (val == "thisLocation");
    
    // $('#historyTable tbody tr:not(.activeLocation)').hide();
    // $('#historyTable tbody tr.activeLocation').removeClass('activeLocationHighlight');
    // $('#historyTable').removeClass('highlightActive');
    
    billingTables.each(function(){
      var billingTable = $(this);
      var accounts = $('tbody > tr.account', billingTable);
      var accountCount = (hide ? accounts.length : 0);
      
      accounts.each(function(){
        var account = $(this);
        var groups = $('td.invoiceContainer > table.groupContainer > tbody > tr', account); //this.rows[0].cells[1].
        var groupCount = (hide ? groups.length : 0);
        
        groups.each(function(){
          var group = $(this);
          var itemization = $('td.billingGroupRow > table.billingGroup > tbody > tr.invoiceItemization > td.billingTableItemizationContainer > table.billingTableItemization', group);
          
          if (!itemization.has('tr.active').length){
            if (hide){
              group.hide();
              groupCount--;
            }
            else {
              group.show();
              groupCount++;
            }
          }
          
        });
        
        if (hide){
          if (groupCount <= 0){
            account.hide();
            accountCount--;
          }
        }
        else {
          if (groupCount > 0){
            account.show();
            accountCount++;
          }
        }
        
      });
      
      if (hide){
        if (accountCount <= 0){
          billingTable.hide();
        }
      }
      else {
        if (accountCount > 0){
          billingTable.show();
        }
      }
    });
        
    // send update trigger to tablesorter
    $('#historyTable').trigger('applyWidgets'); 
  }
  
  
  
  
  
  
  
  
  function billingGetOldestParsedPeriod(){
    var i, oldest = '2037-12-01', thisPeriod;
    
    for (i = 0; i < shared.periods.length; i++){
      thisPeriod = [shared.periods[i].period, '-01'].join('');
      if (oldest > thisPeriod){
        oldest = thisPeriod;
      }
    }
    
    return oldest;
  }
  
  
  
  function billingOlderLink(config){
    var oldest = billingGetOldestParsedPeriod();
    var injectBilling, injectHistory;

    if (aa.existsIn(permissions, 'accounting')){
      injectBilling = $(['#', config.target].join(''));
    }
    else {
      injectBilling = shared.nullElement;
    }
    
    injectHistory = $(['#', config.targetHistory].join(''));
    
    // oldest data already retrieved
    if  (oldest <= shared.systemStart || config.forceOldest) {
      $('.billingSearchOlder').remove();
      
      var billingSearchOlder = ['<div class="billingSearchOlder"><p>Displaying <span style="color: #333; font-weight: bold">all available entries</span></p><p>NOTE: Data before ', dpExactDate(shared.systemStart).toString('MMMM d, yyyy'), ' doesn\'t contain physicist name, modality, or other details</p></div>'].join('');
      injectBilling.append(billingSearchOlder);
      injectHistory.append(billingSearchOlder);
    }

    // older data not yet retrieved
    else {
    
      $('.billingSearchOlder').remove();
      
      var billingSearchOlder = ['<div class="billingSearchOlder"><p>Displaying entries back to <span style="color: #333; font-weight: bold">', dateFriendlyString(dpExactDate(oldest)), '</span></p><p><a class="linkBillingSearchOlder iconleft icon-view">Search for older data</a></p></div>'].join('');
      injectBilling.append(billingSearchOlder);
      injectHistory.append(billingSearchOlder);
      
      var params = {
        accounts: JSON.stringify([clients.active.account_id]),
        getMeta: true,
        // add: true,
        periods: [],
        start: shared.systemStart,
        end: dpExactDate(oldest).addSeconds(-1).toString('yyyy-MM-dd')
      };
      
      var startDate = dpExactDate(params.start);
      var endDate = dpExactDate(params.end);
      
      while (startDate < endDate){
        params.periods.push(startDate.toString('yyyy-MM'));
        startDate = startDate.addMonths(1);
      }
      
      params.periods.reverse();
          
      params.periods = JSON.stringify(params.periods);

      $('.linkBillingSearchOlder').click(function(){
        var linkEl = $(this);
        linkEl.hide().after('<span class="billingSearchOlderStatus">Retrieving from server...</span>');
        var statusEl = linkEl.next('span.billingSearchOlderStatus');
        
        ad.stopwatchStart('ajaxBilling');
      
        $.ajax({
          url: [shared.urlRoot, '/ajaxSSGet/billingGet'].join(''),
          data: params,
          port: 'billingGet',
          mode: 'abort',
          success: function (data, textStatus){
            fblog(['billingGet(): billing data retrieved from server (', (ad.stopwatchEnd('ajaxBilling') / 1000), 's)'].join(''));
            statusEl.text('Preparing data...');
            setTimeout(function(){
              billingParse(data, $.extend(config, {append: true}));
              // billingSearchOlder.remove();
              // setTimeout(function(){
                // statusEl.remove();
                // billingSearchOlder.appendTo($(['#', config.target].join(''))).show();
                // linkEl.show();
              // }, 100);
            }, 10);
          },
          error: function (XMLHttpRequest, textStatus, errorThrown){
            fblog(['billingGet(): billing data retrieved from server (', (ad.stopwatchEnd('ajaxBilling') / 1000), 's)'].join(''));
            fblog(['billingGet(): ajax failure = ', textStatus, ' (', errorThrown, ')'].join(''));
          }
        });
      });
    }
    
  }
  
  
  
  function billingOutput(config){
  
    ad.stopwatchStart('billingOutputPeriod');

    var period = config.period;
    
    var today = Date.today().toString('yyyy-MM-dd');
    var thisMonth = Date.today().addMonths(1).set({day: 1}).addSeconds(-1).toString('yyyy-MM-dd');
    
    var
      injectTarget = [],
      injectTargetHistory = [],
      renderBilling = false,
      renderHistory = false,
      // bt = shared.tasks.get({value: {start: [billing.start, ' 00:00:00'].join(''), end: [billing.end, ' 23:59:59'].join('')}, key: 'billingdate', getAll: true});
      bt = period.tasks.get({value: {start: [period.start, ' 00:00:00'].join(''), end: [period.end, ' 23:59:59'].join('')}, key: 'billingdate', getAll: true});
      
    if (config.target){
      injectTarget = $(['#', config.target].join(''));
    }
    
    renderBilling = Boolean(injectTarget.length);

    if (config.targetHistory){
      injectTargetHistory = $(['#', config.targetHistory].join(''));
    }
    
    renderHistory = Boolean(injectTargetHistory.length);
    
    if (!renderHistory && !renderBilling){
      return {};
    }
      
    var contentArray = []; //'<table id="billing" cellspacing="1" style="width: 100%"><tr><td>';
    var accountNavArray = [];
    var accountMainArray = [];
  
    var accountTables = [];
      
    var i, j, k;
    
    var account = {};
    
    var historyArray = [];
    var historyRows = [];
    
    var showLocation = true;

    period.totalCost = 0;
    period.totalCostAdjusted = 0;
    period.totalAccounts = 0;
    period.totalInvoices = 0;
    
    var periodLocked = scheduleIsLocked(period.period);
    
    if (aa.existsIn(permissions, 'accounting-admin') && $('#formBilling').length){
      var lockControls = $('#billingLockControls');
      
      if (!lockControls.length){
        var htmlLockControls = [
          '<div id="billingLockControls">',
            '<span id="billingLockStatus" class="large150'
        ];
        
        if (periodLocked){
          htmlLockControls.push(' highlightWarning">LOCKED');
        }
        else {
          htmlLockControls.push(' highlight">UNLOCKED');
        }
        
        htmlLockControls.push(
            '</span>',
            '<br/>'
        );
        
        var fieldDef = {
          name: 'toggleLock',
          type: 'button',
          action: scheduleToggleLock, 
          style: 'inline',
          textCondition: {
            test: function(){ return scheduleIsLocked(billing.start); },
            whenTrue: 'Unlock',
            whenFalse: 'Lock'
          }
        };
        
        htmlLockControls.push(
          buttonCreate(null, fieldDef)
        );
            // '<span id="buttonLockToggle" class="button buttonInline">'
        // );
            
        // if (periodLocked){
          // htmlLockControls.push('Unlock');
        // }
        // else {
          // htmlLockControls.push('Lock');
        // }
        
        // htmlLockControls.push(
          // '</span>',
          
        htmlLockControls.push(
          '</div>'
        );
        
        $(htmlLockControls.join('')).appendTo($('#formBilling > div.body:first'));
        
        formSetupButton(null, fieldDef);
        
      }
      else {
        billingLockUpdateStatus();
      }
    }
    
    
    
    
    if (config.clear){
      $('#formBilling > .body:first .item:last').after('<div id="billingSummary" class="item" style="margin-top: 1em; margin-left: 1em; margin-right: 9em" />');
      contentArray.push(
        // '<div id="billingSummary"></div>',
        '<div id="billingTableControlsContainer" class="tableControlsContainer"></div>'
      );
    }
    if (config.clearHistory){
      historyArray.push(
        '<table id="historyTableContainer" class="billingTable" cellspacing="0">',
          '<tr class="account">',
            '<td class="noPadding">',
              '<table id="historyTableGroupContainer" class="groupContainer" cellspacing="0">',
                '<tr>',
                  '<td class="billingGroupRow">',
                    '<table class="billingGroup">'

      );
    }
      
    if (!bt.length && !period.accounts.length){ // no tasks or installments for this timeframe
      // contentArray.push('<div class="billingEmpty">No billing items found');
      // if (period.start && period.end){
        // // fblog(period.start);
        // var
          // start = dpExactDate(period.start),
          // startMonth = start.toString('MMMM'),
          // startYear = start.toString('yyyy'),
          // end = dpExactDate(period.end),
          // endMonth = end.toString('MMMM'),
          // endYear = end.toString('yyyy');
        
        // if (period.end == Date.today().toString('yyyy-MM-dd')){
          // contentArray.push(' since ', startMonth);
          // if (startYear != endYear){
            // contentArray.push(' ', startYear);
          // }
        // }
        // else {
          // contentArray.push(' for ', startMonth);
          // if (startYear != endYear){
            // contentArray.push(' ', startYear);
          // }
          // if (startMonth != endMonth){
            // contentArray.push(' through ', endMonth);
          // }
          // // if (startYear != endYear){
            // contentArray.push(' ', endYear);
          // // }
        // }
        
        
      // }
      // contentArray.push('.</div>');
    }
    else {
    
      if (!period.accountsById){
        period.accountsById = [];
      }

      // alias accounts array to associative array to speed up lookups
      for (i = 0; i < period.accounts.length; i++){
        period.accountsById[period.accounts[i].account_id] = period.accounts[i];
      }
      
      contentArray.push('<table id="billingTable', period.period, '" class="billingTable invoiceList" cellspacing="0">',
        //'<thead><tr><th class="center">Account</th><th>Details</th></tr></thead>' +
        '<tbody>');
        
      period.taskDates = [];

      // loop through tasks to calculate billing
      for (i = 0; i < bt.length; i++){
        var bti = bt[i];
        var account = period.accountsById[bti.data.account_id]; //aa.findObjectByKey(period.accounts, bti.data.account_id, 'account_id');
        
        aa.pushUnique(period.taskDates, bti.data.schedule_start_date);
        
        if (!account.account_name){
          account.account_name = account.location_name || bti.data.location_name;
        }
        
        if (!account.items){
          account.items = [];
        }

        if (!account.billingmatrix){
          account.billingmatrix = [];
        }
        
        if (!account.billingmetric){
          account.billingmetric = 0;
        }
        

        // add this item to account's items list
        account.items.push(bti);
        
        // set this account's default billing metric to that specified in task
        account.billingmetric = bti.data.billingmetric;

        // initialize entry in this account's billingmatrix for this modality
        if (!account.billingmatrix[Number(bti.data.tasktype_id)]){
          account.billingmatrix[Number(bti.data.tasktype_id)] = {
            0: 0,
            1: 0,
            2: 0,
            3: 0,
            total: 0
          };
        }
        
        if (!bti.data.billingmatrix){
          fblog('no billingmatrix for this task');
          fbdir('bti', true, bti);
          bti.data.billingmatrix = {1:0, 2:0, 3:0};
        }
        
        if (bti.data.is_billable){
          // increment account's billingmatrix by this item's billingmatrix
          account.billingmatrix[Number(bti.data.tasktype_id)][0] += (bti.data.billingmatrix[1] == 2 ? 1 : 0);
          account.billingmatrix[Number(bti.data.tasktype_id)][1] += (bti.data.billingmatrix[1] != 2 ? bti.data.billingmatrix[1] : 0);
          account.billingmatrix[Number(bti.data.tasktype_id)][2] += (bti.data.billingmatrix[2] ? bti.data.billingmatrix[2] : 0);
          account.billingmatrix[Number(bti.data.tasktype_id)][3] += (bti.data.billingmatrix[3] ? bti.data.billingmatrix[3] : 0);
          
          // get total cost for this task
          bti.data.billingmatrix.total = Number(billingGetCost(period, bti.data, account)) + Number(bti.data.cost_adjustment);

          account.billingmatrix[Number(bti.data.tasktype_id)].total += Number(bti.data.billingmatrix.total); // + Number(bti.data.cost_adjustment));
          
          if (period.physicistList[bti.data.physicist_id]){
            if (!period.physicistList[bti.data.physicist_id].total){
              period.physicistList[bti.data.physicist_id].total = 0;
            }
            
            period.physicistList[bti.data.physicist_id].total += Number(bti.data.billingmatrix.total);
            
            if (!period.physicistList[bti.data.physicist_id].slots){
              period.physicistList[bti.data.physicist_id].slots = {
                booked: 0,
                personal: 0,
                zz: 0
              };
            }

            if (bti.data.account_id == 99){
              //fblog(bti.data.location_id);
              if (aa.existsIn(
                [
                  841, // vacation
                  //842, // zz
                  844, // seminar
                  845, // meeting
                  846, // holiday
                  847, // sick self
                  848, // sick family
                  849, // jury duty
                  935, // comp time
                  992, // no show
                  993, // snow
                  999, // funeral
                  1000 // travel
                ],
                Number(bti.data.location_id)
              )){ // personal time
                period.physicistList[bti.data.physicist_id].slots.personal += matrixToHalfdays(bti.data.billingmatrix);
              }
              else if (aa.existsIn(
                [
                  842 // zz
                ],
                Number(bti.data.location_id)
              )){ // part time off
                period.physicistList[bti.data.physicist_id].slots.zz += matrixToHalfdays(bti.data.billingmatrix);
              }
              else if (aa.existsIn(
                [
                  843 // office
                ],
                Number(bti.data.location_id)
              )){ // unbooked time
              }
              else { // booked time
                period.physicistList[bti.data.physicist_id].slots.booked += matrixToHalfdays(bti.data.billingmatrix);
              }
            }
            else { // booked time
              period.physicistList[bti.data.physicist_id].slots.booked += matrixToHalfdays(bti.data.billingmatrix);
            }
          }
        }
        else {
          bti.data.billingmatrix.total = 0;
        }
        
        // if (bti.data.isTraining){
          // account.trainingCount++;
        // }
        
 

      }
      
      period.taskDates.sort();

      // add accounts data generated from tasks to global accounts array
      for (i = 0; i < period.accounts.length; i++){
        // var sa = period.meta[i]; //aa.findObjectByKey(period.accounts, i, 'account_id');
        // if (sa){ //.length){
          // //sa = sa[0];
          // sa.data = period.accounts[i];
        // }
        // else {
          // fblog(['billingOutput(): account ', i, ' not found in sa'].join(''));
        // }
        
        period.accounts[i].billingtotal = 0;
        for (j in period.accounts[i].billingmatrix){
          period.accounts[i].billingtotal += period.accounts[i].billingmatrix[j].total;
        }
        
        period.totalCost += period.accounts[i].billingtotal;
        period.totalAccounts++;

        // period.meta[i].items = period.accounts[i].items;
        
        if (!period.accounts[i].account_name){
          period.accounts[i].account_name = period.accounts[i].location_name;
        }
        
        
      }


      // convert period.meta from associative array (indexed by account number) to arbitrarily indexed array
      // var newMeta = [];
      // for (i in period.meta){
        // newMeta.push(period.meta[i]);
      // }
      // aa.sortByKey(newMeta, 'location_name');
      // period.metaById = period.meta;
      // period.meta = newMeta;
      
      if (
        period.accounts[period.accounts.length - 1].account_id === 0
        ||
        period.accounts[period.accounts.length - 1].account_id === '0'
      ){
        var removed = period.accounts[period.accounts.length - 1];
        period.accounts.splice(0, 0, removed);
        period.accounts.splice(period.accounts.length - 1, 1);
      }
      
      period.invoiceCount = 0;
      period.invoices = [];
      

      // step through processed accounts to generate html
      for (i = 0; i < period.accounts.length; i++){
        account = period.accounts[i];
        
        // don't output Bio-Med data
        if (account.account_id == 99){
          continue;
        }





        
        // if (account.items && account.items.length){
        
          accountNavArray = [];
          accountMainArray = [];
          
          if (
            (shared.activeInterface == 'billing' && account.num_locations == 1)
            ||
            (shared.activeInterface != 'billing' && period.accounts.length == 1 && account.num_locations == 1)
          ){
            showLocation = false;
            // fblog('showLocation = ' + String(Boolean(showLocation)));
          }
          else {
            showLocation = true;
            // fblog('showLocation = ' + String(Boolean(showLocation)));
          }
        
          account.groupCount = 0;
          
          account.hasPaperless = false;
          account.hasPostal = false;
          
          account.invoiceNumbers = [];
          

          var groups = [];
          //var rows = [];
          var predefinedMarkers = false;
          
            
          // loop through account groups if present
          if (account.groups){
            for (j in account.groups){
              var group = account.groups[j];
              group.rates = account.rates;
              group.rates_doublebilled = account.rates_doublebilled;
              
              group.account = account;
              
              if (group.group_location.length){
                group.num_locations = group.group_location.length;
              }
              
              if (group.group_marker){
                predefinedMarkers = true;
              }
              
              if (!group.invoice_number){
              
                if (ad.sqlGetYearShort(period.start) + ad.sqlGetMonth(period.start) == '0807'){
                  group.invoice_number = ad.sqlGetYear(period.start) + '-' + ad.sqlGetMonth(period.start) + '-' + group.account_id + '-' + billing.groupMarkers[account.groupCount];
                }
                else if ([ad.sqlGetYearShort(period.start), ad.sqlGetMonth(period.start)].join('') == '0808'){
                  group.invoice_number = [0, Number(ad.sqlGetMonth(period.start)).toString(), ad.sqlGetYearShort(period.start), group.account_id, (group.group_marker ? group.group_marker : billing.groupMarkers[account.groupCount])].join('');
                }
                else if (ad.sqlGetYear(period.start) <= '2009'){
                  group.invoice_number = [Number(ad.sqlGetMonth(period.start)).toString(), ad.sqlGetYearShort(period.start), group.account_id, (group.group_marker ? group.group_marker : billing.groupMarkers[account.groupCount])].join('');
                }
                else {
                  group.invoice_number = [ad.sqlGetYearShort(period.start), billing.groupMarkers[ad.sqlGetMonth(period.start) - 1], account.account_id, (group.group_marker ? group.group_marker : billing.groupMarkers[account.groupCount])].join('');
                }

              
              }
              
              var outputConfig = {
                thisMonth: thisMonth,
                today: today,
                showLocation: showLocation //account.num_locations > 1
              };
              if (account.groupCount > 0){
                outputConfig.notFirst = true;
              }

              // make sure invoice email info defaults to main account info if none specified for this group
              if (!group.email_contact_email && account.email_contact_email){
                group.email_contact_email = account.email_contact_email;
                group.email_contact_name = account.email_contact_name;
                if (!group.email_cc_contact_email && account.email_cc_contact_email){
                  group.email_cc_contact_email = account.email_cc_contact_email;
                  group.email_cc_contact_name = account.email_cc_contact_name;
                }
              }
              
              var groupHTML = billingOutputGroup(period, group, account.items, outputConfig);
              if (
                (groupHTML.rows && groupHTML.rows.length)
                ||
                groupHTML.force
              ){
                var groupTable = [].concat(
                  groupHTML.header,
                  (
                    groupHTML.rows.length ?
                      groupHTML.rows
                      :
                      (
                        groupHTML.force ?
                          ['<p class="detail">No service performed this period</p>']
                          :
                          []
                      )
                  ),
                  groupHTML.footer
                ).join('');
                groups[account.groupCount] = {
                  invoiceNum: group.invoice_number,
                  email: false,
                  content: ''
                };
                
                if (Number(group.invoice_email)){
                  if (group.email_contact_email){
                    groups[account.groupCount].email = {address: group.email_contact_email, name: group.email_contact_name};
                  }
                  else if (account.email_contact_email){
                    groups[account.groupCount].email = {address: account.email_contact_email, name: account.email_contact_name};
                  }
                  else {
                    groups[account.groupCount].email = {address: 'no address specified'};
                  }
                  account.hasPaperless = true;
                }
                
                if (Number(group.invoice_print)){
                  account.hasPostal = true;
                }

                // if (account.groupCount > 0){
                  // groups[account.groupCount].content = [groups[account.groupCount].content, '<tr><td colspan="2" class="groupSeparator"><hr /></td></tr>'].join('');
                // }
                groups[account.groupCount].content = [groups[account.groupCount].content, groupTable].join('');
                historyRows = historyRows.concat(groupHTML.rows);
                
                account.invoiceNumbers.push(group.invoice_number);
                
                account.groupCount++;
                
                period.invoiceCount++;
                
                // period.invoices.push({
                  // number: group.invoice_number,
                  // paperless: Number(group.invoice_email)
                // });
              }
            }
          }
          
          // add any remaining tasks as additional account group
          if (
            (account.items && account.items.length)
            ||
            account.installment_rate
          ){ // some tasks left that didn't belong to groups
          
            if (!account.invoice_number){
              if ([ad.sqlGetYearShort(period.start), ad.sqlGetMonth(period.start)].join('') == '0807'){
                account.invoice_number = ad.sqlGetYear(period.start) + '-' + ad.sqlGetMonth(period.start) + '-' + account.account_id;
              }
              else if ([ad.sqlGetYearShort(period.start), ad.sqlGetMonth(period.start)].join('') == '0808'){
                account.invoice_number = ['0', Number(ad.sqlGetMonth(period.start)).toString(), ad.sqlGetYearShort(period.start), account.account_id].join('');
              }
              else if (ad.sqlGetYear(period.start) <= '2009'){
                account.invoice_number = [Number(ad.sqlGetMonth(period.start)).toString(), ad.sqlGetYearShort(period.start), account.account_id].join('');
              }
              else {
                account.invoice_number = [ad.sqlGetYearShort(period.start), billing.groupMarkers[ad.sqlGetMonth(period.start) - 1], account.account_id].join('');
              }
            
            }
            if (!predefinedMarkers && account.groupCount > 0){
              account.invoice_number = [account.invoice_number, billing.groupMarkers[account.groupCount]].join('');
            }
            
            var outputConfig = {
              thisMonth: thisMonth,
              today: today,
              showLocation: showLocation //account.num_locations > 1
            };
            if (account.account_id === 0 || account.account_id === '0'){
              outputConfig.hideHeader = true;
              outputConfig.hideTotal = true;
            }
            if (account.groupCount > 0){
              outputConfig.notFirst = true;
            }
            
            var groupHTML = billingOutputGroup(period, account, account.items, outputConfig);
            if (groupHTML.rows && groupHTML.rows.length){
              var remainderTable = [].concat(groupHTML.header, groupHTML.rows, groupHTML.footer).join('');
              groups[account.groupCount] = {
                invoiceNum: account.invoice_number,
                email: false,
                content: ''
              };
              
              if (Number(account.invoice_email)){
                if (account.email_contact_email){
                  groups[account.groupCount].email = {address: account.email_contact_email, name: account.email_contact_name};
                }
                else {
                  groups[account.groupCount].email = {address: 'no address specified'};
                }
                account.hasPaperless = true;
              }
              
              if (Number(account.invoice_print)){
                account.hasPostal = true;
              }
              
              // if (account.groupCount > 0){
                // groups[account.groupCount].content = [groups[account.groupCount].content, '<tr><td colspan="2" class="groupSeparator"><hr /></td></tr>'].join('');
              // }
              groups[account.groupCount].content = [groups[account.groupCount].content, remainderTable].join('');
              historyRows = historyRows.concat(groupHTML.rows);
              
              account.invoiceNumbers.push(account.invoice_number);
              
              period.invoiceCount++;
            }
          }
          
          
          account.invoiceNumbers.sort();
          
          
          if (i == 0){ // only do this for the first account, in the rare case that there are multiple "related" accounts
            var orphans = aa.findObjectByKeys(period.taskItems, {service_date: {value: period.taskDates, rel: 'notin'}});
            if (orphans.length){
              var orphanRows = [];
              var thisDate;
              for (j = 0; j < orphans.length; j++){
                thisDate = dpExactDate(orphans[j].service_date);             
                
                orphanRows.push(
                  '<tr class="historyTaskRow cursorPointer',
                  // ((clients.active && clients.active.id && Number(clients.active.num_locations) > 1 && orphans[j].location_id == clients.active.id) ? ' activeLocation activeLocationHighlight' : ''),
                  // ((clients.active && clients.active.id && Number(clients.active.num_locations) > 1 && orphans[j].location_id == clients.active.id) ? ' activeLocation' : ''),
                  ((clients.active && clients.active.id && Number(clients.active.num_locations) > 1 && orphans[j].location_id == clients.active.id) ? ' active' : ''),
                  '" task="unknown"><td class="center">',
                  '<span class="hidden">', orphans[j].service_date, ' 08:00:00</span>',
                  (orphans[j].service_date < '2001-01-01' ? 
                    '<span class="light">Unknown</span>'
                    :
                    [
                      thisDate.toString('M/d'), '<span class="rowYear">', thisDate.toString('/yy'), '</span>'
                    ].join('')
                  ),
                  '</td>',
                  (showLocation ? ['<td>', billingGetLocationName(orphans[j], account.invoice_show_locations), '</td>'].join('') : ''),
                  '<td class="highlightWarningLight">Unknown</td><td>&nbsp;</td>',
                  '<td><div class="notesReport"><p>', orphans[j].description,
                  (aa.existsIn(permissions, 'debug') ? [' (', orphans[j].flag1, ', ', orphans[j].flag2, ', ', orphans[j].flag3, ')'].join('') : ''),
                  '</td>',
                  '<td class="center nowrap light"></td><td class="right"></td></tr>'
                );
              }
              
              historyRows = historyRows.concat(orphanRows);
            }
          }

          
          // create beginning of account html
          accountNavArray.push( 
              '<tr id="account', account.account_id, '" class="account', (account.hasPaperless ? ' paperless' : ''), (account.hasPostal ? ' postal' : ''), '" row="', account.account_id, '">',
              '<td class="accountContainer accountColumn">'
          );
          
          
          accountNavArray.push('<div class="accountName" style="margin-bottom: .5em">');
          if (config.showPeriod){
            accountNavArray.push('<p class="large150 bold', (period.start > today ? ' lighter' : ''), '">', dpExact(period.period, 'yyyy-MM').toString('MMMM yyyy'));
            
            if (period.accounts.length > 1){
              accountNavArray.push('<br /><span class="light">', account.account_name, '</span>');
            }
            
            accountNavArray.push('</p>');
          }
          if (config.showAccountName){
            accountNavArray.push('<p class="large150 bold">', period.accountsById[account.account_id].account_name /* account.location_name*/, '</p>');
            // aliases for this account name
            accountNavArray.push(as.echoIf(account.aliases, '<p><span class="smallCaps">AKA:</span> ', '</p>'));
          }
          accountNavArray.push('</div>');
          
          
          // output schedule locked indicator, but only show if applicable
          accountNavArray.push('<div class="smallCaps highlightWarning scheduleLockedIndicator iconLink"');
          if (period.period > schedule.lock || aa.existsIn(schedule.lock_exceptions, period.period)){
            accountNavArray.push(' style="display: none"');
          }
          accountNavArray.push('>Locked</div>');
          
          
          // show paperless and/or postal indicator
          if (account.account_id != 0 && (account.hasPaperless || account.hasPostal)){
          
            accountNavArray.push(
              '<div class="iconLink smallCaps">'
            );
            
            if (account.hasPaperless && account.hasPostal){
              accountNavArray.push('<span class="highlightRed">Postal and Paperless</span>');
            }
            else if (account.hasPaperless){
              accountNavArray.push('<span class="highlight">Paperless Only</span>');
            }
            else if (account.hasPostal){
              accountNavArray.push('<span class="highlightRed">Postal Only</span>');
            }
            
            accountNavArray.push('</div>');
            
          }
          
              
          // html for pdf link
          accountNavArray.push(
            '<div class="iconLink icon-print iconleftgapless"><a href="/invoice/',
            account.account_id, '/', ad.sqlGetYear(period.start), '-', ad.sqlGetMonth(period.start),
            '/', account.invoiceNumbers.join('_'),
            '/Bio-Med_Invoice_', 
            account.invoiceNumbers.join('_'),
            // Number(ad.sqlGetMonth(period.start)), ad.sqlGetYearShort(period.start), account.account_id,
            '.pdf" target="_blank" title="Click to view printable PDF" class="smallCaps">View invoice', (account.invoiceNumbers.length > 1 ? 's' : ''), '</a></div>'
          );
          
          // if (aa.existsIn(permissions, 'debug')){
            // // html for paperless link
            // if (account.hasPaperless){
              // accountNavArray.push(
                // '<div class="iconLink icon-email iconleftgapless"><a',
                // // ' href="invoiceEmail/',
                // // account.account_id, '/', ad.sqlGetYear(period.start), '-', ad.sqlGetMonth(period.start),
                // // '"',
                // ' class="linkPaperless smallCaps" account="', account.account_id, '" period="', period.start.substr(0, 7), '" invoices="', account.invoiceNumbers.join('_'), '">Send paperless</a></div>'
              // );
            // }
          // }
          
          accountNavArray.push('</td>');        
            
          // create beginning of group table HTML
          accountNavArray.push('<td class="invoiceContainer noPadding accountColumn"><table class="groupContainer" cellspacing="0">');

          contentArray = contentArray.concat(accountNavArray);
          
          
          aa.sortByKey(groups, 'invoiceNum');
          
          for (k = 0; k < groups.length; k++){
            if (k > 0){
              groups[k].content = groups[k].content.replace('"billingGroupRow"', '"billingGroupRow billingGroupRowNotFirst"');
            }
            contentArray.push(groups[k].content);
            //historyArray.push(rows);
          }
          
          contentArray.push('</table></td></tr>'); // end account row
        
        // }





        
      }
          
      contentArray.push('</tbody></table>'); // end billing table
      

      
      
      
    }
    
    
    
    // fblog('final showLocation = ' + String(Boolean(showLocation)));
    
    var contentItemHeadersArray = [// start billing group itemization
      '<tr class="invoiceItemization"><td colspan="2" class="billingTableItemizationContainer"><table class="tablesorter billingTableItemization', (showLocation ? ' highlightActive' : ''), '" cellspacing="0">',
      '<thead><tr><th class="center">Date</th>'
    ];
    
    if (showLocation){
      contentItemHeadersArray.push('<th>Location</th>');
    }
    
    contentItemHeadersArray.push('<th>Physicist</th><th>Modality</th><th>Summary</th><th class="center">Time</th>');
      
    if (account.account_id != 0 && aa.existsIn(permissions, 'accounting')){
      contentItemHeadersArray.push('<th class="right">Cost</th>');
    }
    
    contentItemHeadersArray.push('</tr></thead><tbody>');
    
    
    
    // render history html
    // $('#jq-wip').html(historyArray.join(''));
    if (renderHistory){
      if (config.clearHistory){
        injectTargetHistory.html([
          '<div id="historyTableControlsContainer" class="tableControlsContainer" style="line-height: 2em">',
            '<div id="historyControls" style="line-height: 2em">',

              (account.num_locations > 1 ? [
              
                '<span>Display:</span>&nbsp;',
                '<select style="margin-right: 2em" id="selectHistoryShowLocations">',
                  '<option value="allLocations" selected="selected">All account locations</option>',
                  '<option value="thisLocation">Just this location</option>',
                '</select>',
                
              ].join('') : ''),
              
              '<a id="linkHistoryPrint" style="line-height: 2em">Print facility calendar for:</a>&nbsp;',
              '<select id="selectHistoryPrintPeriod">',
                // '<option value="10">10</option>',
                // '<option value="20" selected="selected">20</option>',
                // '<option value="30">30</option>',
                // '<option value="40">40</option>',
              '</select>',
              '<select id="selectHistoryPrintPhysicists">',
                '<option value="" selected="selected">Everyone</option>',
                '<option value="diagnostic">Diagnostic</option>',
                '<option value="therapy">Therapy</option>',
              '</select>',
            '</div>',

          '</div>'].join('')
        ).css('padding-bottom', 0);
        // injectTargetHistory.html('<div id="historyTableControlsContainer" class="tableControlsContainer tableControlsContainerBorder"></div>').css('padding-bottom', 0);
        
        $('#selectHistoryShowLocations').change(function(){
          var val = $("option:selected", $(this)).attr('value');
          
          historyTableLocationDisplayUpdate(val);
        
        });
        
        historyArray = historyArray.concat(contentItemHeadersArray);
      }
      
      historyArray = historyArray.concat(historyRows);
      
      if (config.endHistory){
        historyArray.push('</table></td></tr></table></td></tr></table>');
      }
      
      // var historyFormDef = new formObject({
        // id: 'historyTableControls',
        // injectTarget: 'historyTableControlsContainer',
        // addedInjectTarget: 'none',
        // prefix: 'htc_',
        // noFrame: true,
        // noGap: true,
        // noFormElement: true,
        // submitType: 'ajax',
        // //focus: 'location_name',
        // //editTrack: 'activeEdit.dirty',
        // validation: aa.objectClone(validationDefault),
        // fields: [
          // {
            // type: 'raw',
            // content: '<span id="historyDropdownText">Print facility calendar for </span>'
          // },
          // {
            // name: 'historyPrintMonth',
            // //label: 'Display for',
            // type: 'dropdown',
            // required: true,
            // values: [
              // { id: 0, name: 'Everyone' },
              // { id: 1, name: 'Diagnostic Physicists' },
              // { id: 2, name: 'Therapy Physicists' }
              // // { id: 3, name: 'Just Me' }
            // ],
            // separator: ' + ',
            // selectExclusive: true, //[0, 1, 2], //, 3],
            // selectCallback: staffScheduleRecentDisplayChange,
            // prefillVal: !staff.preferences.scheduleRecent || staff.preferences.scheduleRecent === true ? 0 : staff.preferences.scheduleRecent
          // },
          // {
            // type: 'button',
            // action: 'historyPrintCalendar',
            // target: 'historyTableContainer',
            // style: 'inline',
            // text: 'Print'
          // }
        // ]
      // });

      // forms[historyFormDef.id] = historyFormDef;
      
      // formBuild(formDef, formDef.injectTarget);
      
    }
    
    
    
    
    
    
    
    
    
    
    
    
    if (renderBilling){
      // render html to hidden div for further manipulation before displaying
      if (!$('#jq-wip').length){
        $('#hidden_container').append('<div id="jq-wip" class="hidden"></div>');
      }
      $('#jq-wip').html(contentArray.join(''));
      
      
      
      // move wip content to visible container
      
      if (config.clear){
        injectTarget.empty().css('padding-bottom', 0);
        //$('div.flatContainer > div.body').css('padding-bottom', 0);
        if (shared.activeInterface != 'billing'){
          //$('#billingSummary').html(['<p class="large150 bold">Invoices for account: ', account.account_name, '</p>'].join(''));
        }
        else {
          // $('#billingSummary').html(['<p class="large150">Accounts due: ', period.totalAccounts, ' &nbsp;&nbsp; Total amount due: $', as.addCommas(period.totalCost.toFixed(2)), '</p>'].join(''));
          var billingTotalHTML = ['<p class="large150">Invoices due: ', period.invoiceCount];
          
          if (aa.existsIn(permissions, 'accounting-admin') || aa.existsIn(permissions, 'accounting-admin-readonly')){
            billingTotalHTML.push('<br />Total amount due: <a id="linkTotalBreakdown" title="View breakdown by physicist"><span class="icon-view iconright">$', as.addCommas(period.totalCostAdjusted.toFixed(2)), '</span></a>');
          }
          
          billingTotalHTML.push('</p>');
          $('#billingSummary')
            .html(billingTotalHTML.join(''))
            .show();
            
          $('#linkTotalBreakdown').click(function(){
            var breakdownEl = $('#breakdown');
            if (!breakdownEl.length){
              breakdownEl = $('<div id="breakdown" style="margin: 1em; padding: 1em; background-color: #fff"></div>').appendTo($('#jq-wip'));
            }
            else {
              breakdownEl.remove();
              return;
            }
            
            var breakdownContent = [];
            var ppli;
            var max = 0;
            var percent;
            var stats;
            var heatColor;
            
            for (i in period.physicistList){
              ppli = period.physicistList[i];
              if (max < ppli.total){
                max = ppli.total;
              }
            }
            
            // breakdownContent.push('<div id="breakdownlegend" style="border-top: 1px solid #ddd; padding-top: 1em; margin-top: 1em"><table width="100%"><tr>',
              // '<td style="text-align: center; background-color: #bfb; color: #7b7;">worked days at client sites<br>(darker green on the left)</td>',
              // '<td style="text-align: center; background-color: #dfd; color: #7b7;">sick / vacation / comp / other absences<br>(lighter green in the middle)</td>',
              // '<td style="text-align: center; background-color: #ffb; color: #bb7;">office / unscheduled days<br>(yellow on the right)</td></tr></table></div>');
              
            breakdownContent.push('<table id="breakdownTable" class="tablesorter"><thead><tr><th>Name</th><th>Total</th><th>&nbsp;</th></tr></thead><tbody>');
            for (i in period.physicistList){
              ppli = period.physicistList[i];
              if (ppli.total){
                percent = Number(ppli.total / max * 100).toFixed(0);
                stats = ppli.stats = getMonthlyStats(ppli, period);
                heatColor = colorHeat({value: percent}).join();
                
                breakdownContent.push(
                  '<tr><td style="white-space: nowrap"><span class="hidden">', ppli.name_last, '</span>', ppli.name_first, ' ', ppli.name_last,
                  // '</td>',
                  // '<td>',
                  '</td>',
                  '<td style="text-align: right;"><span class="smallCaps">$</span>', as.addCommas(String(ppli.total || 0)),
                  '</td>',
                  '<td style="width: 100%"><span class="hidden">', ppli.total || 0, '</span>',
                  '<div style="background-color: rgb(', heatColor, '); width: ', percent, '%">&nbsp;</div>',
                  '<div style="float: left; background-color: #bfb; color: #7b7; width: ', stats.ratio.booked, '%; text-align: center">', stats.raw.booked, '</div>',
                  '<div style="float: left; background-color: #dfd; color: #7b7; width: ', stats.ratio.personal, '%; text-align: center">', stats.raw.personal, '</div>',
                  (stats.ratio.open ? ['<div style="float: left; background-color: #ffb; color: #bb7; width: ', stats.ratio.open, '%; text-align: center">', stats.raw.open, '</div>'].join('') : ''),
                  '</td>',
                  '</tr>'
                );
              }
            }
            
            breakdownContent.push('</tbody></table>');
            
            breakdownContent.push('<div id="breakdownlegend" style="border-top: 1px solid #ddd; padding-top: 1em; margin: 1em .5em 0 auto; text-align: center">',
              // '<span class="smallCaps">Legend</span>',
              '<table width="100%"><tr>',
              '<td style="text-align: center; background-color: #bfb; color: #7b7;">worked days at client sites<br>(darker green on the left)</td>',
              '<td style="text-align: center; background-color: #dfd; color: #7b7;">sick / vacation / comp / other absences<br>(lighter green in the middle)</td>',
              '<td style="text-align: center; background-color: #ffb; color: #bb7;">office / unscheduled days<br>(yellow on the right)</td></tr></table></div>');
            
            breakdownEl.html(breakdownContent.join(''));
            
            breakdownEl.appendTo($('#billingSummary').parent());
            
            $('#breakdownTable').tablesorter({
              sortList: [[1,1]],
              headers: {2: {sorter: false}},
              textExtraction: 'complex'
            });
            
            $('#breakdownlegend').width($('#breakdownTable tbody tr:first td:last').width());
            
          });

        }
      
      }
      
      injectTarget.append($('#jq-wip').children());
      
      billingTableLocationDisplayUpdate();





      // set up click handler
      var tableElement = $(['#billingTable', period.period].join(''));
      

      // set up table sorter and row hover handler
      // $('table.billingTableItemization', tableElement).each(function(){
        // thisTable = $(this);
        // thisTable.tablesorter( {sortList: [[0,0]], textExtraction: 'simple', widgets: ['zebra'], widgetZebra: {css: ['evenRow', '']} } );
        // tableRowHoverSetup(thisTable, 'highlightgreen');
      // });
      
      
      tableElement.mousedown(
        function(e){
          if (e.button < 2) {
            var el = $(e.target);
            var i;
            
            if (!el.is('a')){ //e.target.tagName != "A"){ // only change select state if the clicked element is not an anchor

              while (!el.is('tr.account')){ //(el[0].tagName == 'TD')){
                el = el.parent();
              }
              
              var row = el.attr('row');
                
              mouseDown.set({
                element: el,
                _id: row
              });
              
              // get VISIBLE (not filtered) invoice numbers under this account row
              var invoiceNumbers = $('> td.invoiceContainer > table.groupContainer > tbody > tr.billingGroupRowInvoice:visible', el)
                .map(function(){
                  return $(this).attr('invoice');
                })
                .get();
              
              // get invoice objects
              var invoices = aa.findObjectByKey(billing.invoices, invoiceNumbers, 'number');
              
              if (e.ctrlKey){
                
                // if any of these invoices aren't currently selected, select all
                if (aa.findObjectByKey(invoices, true, 'selected').length < invoices.length){
                  _.each(invoices, function(element){
                    element.selected = true;
                  });
                  
                  el.addClass('accountselected');
                }
                
                // if all of them are already selected, deselect all
                else {
                  _.each(invoices, function(element){
                    delete element.selected;
                  });
                  
                  el.removeClass('accountselected');
                }
                
                billingSelectedStatusUpdate();
                // {
                  // target: 'billingTableSelectedStatus',
                  // disable: 'btc_button_billingTable_billingViewSelected',
                  // singular: 'invoice'
                // });
              
                
                // prevent default event handling only when CTRL key pressed
                // prevents selection boxes around elements in firefox
                e.preventDefault();
                e.stopPropagation();
                return false;
              }
              else {
                // deselect all currently selected rows
                billingSelectNone();
                
                _.each(invoices, function(element){
                  element.selected = true;
                });
                  
                el.addClass('accountselected'); 
                billingSelectedStatusUpdate();
                // {
                  // target: 'billingTableSelectedStatus',
                  // disable: 'btc_button_billingTable_billingViewSelected',
                  // singular: 'invoice'
                // });
              }
            }
            
            
            else { // el is an A
              if (el.hasClass('linkPaperless')){
                var paperless = {
                  invoices: el.attr('invoices').split('_'),
                  account: el.attr('account'),
                  period: el.attr('period')
                };
                
                el.next('span.paperlessInProgress').remove();
                el.after('<span class="iconleftgapless icon-email paperlessInProgress highlight smallCaps">Sending</span>').hide();
                
                billingInvoiceView($.extend(paperless, {email: true}));
                updaterStart('billingProgressPaperless');
                
              }
            }


          }
        }
      );

      
      
      
      
      
      
      
      // var formDef = new formObject({
        // id: 'billingTableControls',
        // injectTarget: 'billingTableControlsContainer',
        // addedInjectTarget: 'none',
        // prefix: 'btc_',
        // noFrame: true,
        // noGap: true,
        // noFormElement: true,
        // submitType: 'ajax',
        // //focus: 'location_name',
        // //editTrack: 'activeEdit.dirty',
        // validation: aa.objectClone(validationDefault),
        // fields: [
          // {
            // name: 'displayInvoices',
            // label: 'Display',
            // labelInline: true,
            // type: 'select',
            // inline: true,
            // // required: true,
            // values: [
              // { id: 0, name: 'All invoices', selected: true },
              // { id: 1, name: 'Paperless invoices' },
              // { id: 2, name: 'Postal invoices' }
              // // { id: 3, name: 'Just Me' }
            // ],
            // // separator: ' + ',
            // // selectExclusive: [0, 1, 2], //, 3],
            // selectCallback: function() {
              // var val = $("option:selected", $(this)).attr('value');
              // switch (val){
                // case "1":
                  // $('table.billingTable tr.account:not(.paperless)').hide();
                  // $('table.billingTable tr.account.paperless').show();
                  // break;
                  
                // case "2":
                  // $('table.billingTable tr.account.paperless').hide();
                  // $('table.billingTable tr.account:not(.paperless)').show();
                  // break;
                  
                // case "0":
                // default:
                  // $('table.billingTable tr.account').show();
                  // break;
              // }
            // }

          // },
          
        
          // {
            // type: 'raw',
            // content: '<span style="margin-left: .5em; margin-right: .5em" id="billingTableSelectedStatus"></span>'
          // },
          // {
            // type: 'button',
            // action: 'billingViewSelected',
            // target: 'billingTable',
            // style: 'inline',
            // text: 'View Selected Invoices'
          // },
          // {
            // type: 'button',
            // action: 'billingSelectAll',
            // target: 'billingTable',
            // style: 'inline',
            // text: 'Select All'
          // },
          // {
            // type: 'button',
            // action: 'billingSelectNone',
            // target: 'billingTable',
            // style: 'inline',
            // text: 'Select None'
          // }
        // ]
      // });

      // forms[formDef.id] = formDef;
      
      // formBuild(formDef, formDef.injectTarget);
      
      // billingSelectedStatusUpdate({
        // target: 'billingTableSelectedStatus',
        // disable: 'btc_button_billingTable_billingViewSelected',
        // // source: billing.selected,
        // singular: (shared.activeInterface == 'clients' ? 'billing period' : 'account')
      // });
      
    }
    
    // fblog(['billingOutput(): billing rendered (', (ad.stopwatchEnd('billingOutput') / 1000), 's)'].join(''));
    
    
    // enable interval to save dirty tasks
    //billingUpdaterStart();
    
    var data = {
      renderable: bt.length ? true : false
    };
    
    if (renderHistory){
      data.historyHTML = historyArray;
      //data.historyForm = historyFormDef;
    }
    
    return data;
  }
  
  
  
  
  
  
  function historyTableLocationDisplayUpdate(val){
    switch (val){
      case "thisLocation":
        // $('#historyTable tbody tr:not(.activeLocation)').hide();
        $('#historyTable tbody tr:not(.active)').hide();
        // $('#historyTable tbody tr.activeLocation').removeClass('activeLocationHighlight');
        $('#historyTable').removeClass('highlightActive');
        break;
        
      default:
        // $('#historyTable tbody tr:not(.activeLocation)').show();
        $('#historyTable tbody tr:not(.active)').show();
        // $('#historyTable tbody tr.activeLocation').addClass('activeLocationHighlight');
        $('#historyTable').addClass('highlightActive');
        break;
    }
    
    // send update trigger to tablesorter
    $('#historyTable').trigger('applyWidgets'); 
  }
  
  
  
  
  
  
  
  
  
  function colorHeat(config){
    var heat = [];
    
    heat[0] = Math.round(255 - (config.value / 100 * (255-128)));
    heat[1] = Math.round((config.value / 100 * (235-128)) + 128);
    heat[2] = 128;
    
    return heat;
  }
  
  
  
  function getMonthlyStats(ppli, period){
    var totalDays = 0, weekDays = 0;
    var currentDate = dpExactDate(period.period + '-01');
    var endDate = currentDate.clone().addMonths(1);
    var stats = {
      raw: {
        total: 0,
        booked: 0,
        personal: 0,
        open: 0
      },
      ratio: {
        booked: 0,
        personal: 0,
        zz: 0,
        open: 0
      }
    };
    var content = [];
    
    while (currentDate < endDate){
      if (currentDate.is().weekday()){
        weekDays++;
      }
      currentDate.addDays(1);
    }
    
    stats.raw.total = weekDays;
    
    if (ppli.slots){
      stats.raw.total = am.round(weekDays - ppli.slots.zz / 2, 2);
      stats.raw.booked = am.round(Math.max(0, ppli.slots.booked / 2), 2);
      stats.raw.personal = am.round(Math.max(0, ppli.slots.personal / 2), 2);
      
      stats.ratio.booked = Math.min(100, am.round(stats.raw.booked / stats.raw.total * 100, 0));
      stats.ratio.personal = Math.min(100 - stats.ratio.booked, am.round(stats.raw.personal / stats.raw.total * 100, 0));
    }
    
    stats.raw.open = Math.max(0, stats.raw.total - stats.raw.booked - stats.raw.personal); //am.round(weekDays - (ppli.slots.zz / 2 + ppli.slots.personal / 2 + ppli.slots.booked / 2), 2);
    stats.ratio.open = Math.max(0, 100 - stats.ratio.booked - stats.ratio.personal);
    
    return stats;
  }
  
  
  
  function billingPaperlessInProgressAdd(invoices){
    // var ip = billing.paperless.inProgress;
    // aa.concatUnique(ip.invoices, data.invoices);

    var pp = aa.findObjectByKeys(billing.paperlessStatus, {invoice: {value: invoices, rel: 'in'}});
    var plink;
    
    for (var i = 0; i < pp.length; i++){
      // el.next('span.paperlessInProgress').remove();
      // el.after('<span class="iconleftgapless icon-email paperlessInProgress highlight smallCaps">Sending</span>').hide();
      
      plink = $(['a.linkPaperless[invoices=', pp[i].invoice, ']'].join(''));
      
      plink.next('span.paperlessInProgress').remove();
      plink.after('<span class="iconleftgapless icon-email paperlessInProgress highlight smallCaps">Sending</span>');
      plink.hide(); // hide link, so only status span shows

      pp[i].status = 'sending';
    }

    // aa.concatUnique(ip.accounts, data.accounts
    // ip.accounts = 
    // billing.paperlessInProgress
    // invoices: el.attr('invoices').split('_'),
    // account: el.attr('account'),
    // period: el.attr('period')
  }
  
  function billingPaperlessInProgressRemove(data){
    // var ip = billing.paperless.inProgress;
    // aa.removeByValue(ip.invoices, data.invoices);
  }
  
  
  function billingLockUpdateStatus(){
    var statusEl = $('#billingLockStatus');
    var invoiceStatusEl = $('div.scheduleLockedIndicator');
    var buttonEl = $('#toggleLock');
    
    var isLocked = scheduleIsLocked(billing.start);
    
    var unsentButton = $('#btc_billingSendPaperlessUnsent');
    
    if (isLocked){
      statusEl.removeClass('highlight highlightLight').addClass('highlightWarning').text('LOCKED');
      buttonEl.text('Unlock');
      invoiceStatusEl.show();
      unsentButton.removeClass('hidden');
    }
    else {
      statusEl.removeClass('highlightLight highlightWarning').addClass('highlight').text('UNLOCKED');
      buttonEl.text('Lock');
      invoiceStatusEl.hide();
      unsentButton.addClass('hidden');
    }
    
    $('#billingLockControls').show();
    
  }
  
  
  
  // if any paperless invoices are currently "sending", make sure their status
  // is properly displayed and that updater is running
  function billingPaperlessSendingUpdate(){
    var pp = aa.findObjectByKeys(billing.paperlessStatus, {status: 'sending'});
    
    if (pp.length){
      billingPaperlessInProgressAdd(_.pluck(pp, 'invoice'));
      updaterStart('billingProgressPaperless');
    }
  }
  
  
    
  function billingPaperlessUnsentUpdate(){
    var pp = aa.findObjectByKeys(billing.paperlessStatus, {status: {value: ['sent', 'sending'], rel: 'notin'}});
    
    //fbdir(pp);
    
    var unsentButton = $('#btc_billingSendPaperlessUnsent');
    var unsent = pp.length;
    
    if (unsent){
      unsentButton.removeClass('buttonDisabled').html(['Send <span style="color: #494; font-weight: bold;">', unsent, '</span> Unsent Paperless Invoice', (unsent != 1 ? 's' : '')].join(''));
    }
    else {
      unsentButton.addClass('buttonDisabled').html('Send Unsent Paperless Invoices');
    }

  }
  
  
  
  function billingGetProgressPaperless(){
    fblog('billingGetProgressPaperless()');
    var ip = billing.paperless.inProgress;
    var pp = aa.findObjectByKey(billing.paperlessStatus, 'sending', 'status');
    var invoices = [];

    // return if there are no items in progress
    if (!pp.length){
      updaterKill('billingProgressPaperless');
      return;
    }
    
    for (var i = 0; i < pp.length; i++){
      invoices.push(pp[i].invoice);
    }
    
    fblog(['billingGetProgressPaperless(): ', pp.length, ' items in progress (', invoices.join(','), ')'].join(''));
    
    var billingGetProgressPaperlessAjax = $.ajax({
      url: [shared.urlRoot, '/ajaxSSGet/progressPaperless'].join(''),
      //data: '+',
      data: {
        invoices: JSON.stringify(invoices)
      },
      port: 'billingGetProgressPaperless',
      mode: 'abort',
      success: function (data, textStatus){
        fblog('billingGetProgressPaperless(): success');
        
        try {
          var decoded = eval(["(", data, ")"].join(''));
          
          if (decoded.success === false){
            fblog(decoded.message);
            if (decoded.auth === false){
              window.location.reload(); //href = "/login/clients";
            }
            return false;
          }
          
        }
        catch (err){
          fblog(['billingGetProgressPaperless() decode failure: ', err.name, ', ', err.message].join(''));
          return;
        }
        
        //billingGetProgressPaperlessAjax.abort();
        
        var i, pp;
        
        for (var i = 0; i < decoded.length; i++){
          if (pp = aa.findObjectByKey(billing.paperlessStatus, decoded[i].invoice, 'invoice', null, true)){
            $.extend(pp, decoded[i]);
            if (decoded[i].status == 'sent'){
              $(['a.linkPaperless[invoices=', decoded[i].invoice, ']'].join('')).show().removeClass('icon-email').addClass('icon-ok').next('span.paperlessInProgress').removeClass('iconleftgapless icon-email smallCaps').addClass('small').html([' (sent ', dpExactDateTime(decoded[i].timestamp).toString('M/d h:mmt'), ')'].join('')).parent().addClass('paperlessStatusSent');
            }
            else if (decoded[i].status == 'sending'){
              statusSpan = $(['a.linkPaperless[invoices=', decoded[i].invoice, ']'].join('')).next('span.paperlessInProgress');
              if (statusSpan.html().indexOf('....') == -1){
                statusSpan.append('.');
              }
              else {
                statusSpan.html(statusSpan.html().replace('....', ''));
              }
            }
            else {
              $(['a.linkPaperless[invoices=', decoded[i].invoice, ']'].join('')).show().next('span.paperlessInProgress').removeClass('iconleftgapless icon-email smallCaps').addClass('small highlightWarning').html([' (', decoded[i].status, ': ', decoded[i].details, ')'].join(''));
            }
          }
          else {
            fblog('billingGetProgressPaperless(): returned invoice (' + decoded[i].invoice + ') not found');
          }
        }
      },
      error: function (XMLHttpRequest, textStatus, errorThrown){
        fblog(['billingGetProgressPaperless(): ajax error = ', textStatus, ' (', errorThrown, ')'].join(''));
      }
    
    });
    
  }

  
  
  function billingGetLocationName(task, showLocations){
    if (task.data){
      task = task.data;
    }
    
    //fblog('showLocations = ' + showLocations + ' (' + typeof showLocations + ')');
    
    if (showLocations === undefined || showLocations === null){
      showLocations = '1';
      // if (task.invoice_show_locations){
        // showLocations = task.invoice_show_locations;
      // }
      // else {
        // return task.location_name;
      // }
    }
    
    var name = [];
    var taskName = task.location_name_short || task.location_name;
    
    if (!taskName){
      return false;
    }

    // loc name (and city if not in loc name)
    if (showLocations === '1'){
      name.push(taskName);
      if (task.unique_identifier){
        if (taskName.indexOf(task.unique_identifier) === -1){
          name.push(' (', task.unique_identifier, ')');
        }
      }
      else if (taskName.indexOf(task.city) === -1){
        name.push(' (', task.city, ')');
      }
    }
    
    // just unique
    else if (
      showLocations === '2'
      &&
      task.unique_identifier
    ){
      name.push(task.unique_identifier);
    }
    
    // just city
    else if (showLocations === '3'){
      name.push(task['city']);
    }
    
    // loc name and force unique or city
    else if (showLocations === '4'){
      name.push(taskName);
      if (task.unique_identifier){
        name.push(' (', task.unique_identifier, ')');
      }
      else {
        name.push(' (', task.city, ')');
      }
    }
    
    // else if (showLocations === '5'){
      // name.push(task['location_name']);
    // }
    
    
    // default to just loc name
    if (!name.length){
      name.push(taskName);
    }
    
    return name.join('');
  }
  
  
  
  function billingOutputGroup (period, group, items, config){
  
    if (!config){
      config = {};
    }
    
    var i, j;
    
    // start billing group row
    var contentHeaderArray = [];
    var contentHeaderRightTopArray = [
      // '<div class="detail"><div class="iconLink icon-print iconleftgapless"><a href="invoice/',
      // group.account_id, '/', ad.sqlGetYear(period.start), '-', ad.sqlGetMonth(period.start),
      // '/Bio-Med_Invoice_', 
      // group.invoice_number,
      // // Number(ad.sqlGetMonth(period.start)), ad.sqlGetYearShort(period.start), account.account_id,
      // '.pdf" target="_blank" class="smallCaps">View invoice</a></div></div>'
    ];
    var contentHeaderRightBottomArray = [];
    var contentItemHeadersArray = [];
    
    var contentFooterArray = [];
    
    contentHeaderArray.push('<tr id="billingGroupRowInvoice', group.invoice_number, '" invoice="', group.invoice_number, '" class="billingGroupRowInvoice"><td class="billingGroupRow"><table class="billingGroup"><tr>');
    
    if (group.account_id == 0){
      contentHeaderArray.push('<td colSpan="2"><p class="detail"><b>These items are not assigned to any valid account:</b> they will need to be manually billed, or to be assigned in the schedule to a valid facility in order to automatically generate an invoice.');
    }
    
    else {
    
      contentHeaderArray.push('<td class="headerLeft">');
      
      contentHeaderArray.push(
        '<p class="detail"><span class="smallCaps">Invoice No.:</span> <a class="bold icon-print iconright" title="Click to view printable PDF" href="/invoice/',
        group.account_id, '/', ad.sqlGetYear(period.start), '-', ad.sqlGetMonth(period.start),
        '/', group.invoice_number,
        '/Bio-Med_Invoice_', 
        group.invoice_number,
        // Number(ad.sqlGetMonth(period.start)), ad.sqlGetYearShort(period.start), account.account_id,
        '.pdf" target="_blank">',
        group.invoice_number,
        '</a></p>'
      );
      
      // if (group.invoice_table_header){
        // contentHeaderArray.push(
          // '<p class="detail bold">', group.invoice_table_header,
          // //(group.invoice_table_subheader ? ['<br /><span class="smallCaps">', group.invoice_table_subheader, '</span>'].join('') : ''),
          // '</p>'
        // );
      // }
      
      contentHeaderArray.push(
        '<p class="detail"><span class="smallCaps">Billing address:</span><br>',
      
        as.echoIf(group.location_name.replace('\\', '<br>'), '', '<br>')
      );
      
      if (!Number(group.invoice_hide_contact)){
        contentHeaderArray.push(as.echoIf(group.contact_name, 'Attn: ', [as.echoIf(group.contact_title, ', '), '<br>'].join(''), 'Attn: Accounts Payable<br>'));
      }
      
      contentHeaderArray.push(
        as.echoIf(group.contact_department, '', '<br>'),
        as.echoIf(group.address1, '', '<br>'),
        as.echoIf(group.address2, '', '<br>'),
        as.echoIf(group.address3, '', '<br>'),
      
        group.city, ', ', group.state, '  ', group.zip, '</p>',
        
        '</td>',
        '<td class="headerRight">'
      );
      
      contentHeaderRightBottomArray.push(
        as.echoIf(group.invoice_po, '<p class="detail"><span class="smallCaps">PO#:</span> ', '</p>'),
        as.echoIf(group.invoice_default_comment, '<p class="detail"><span class="smallCaps">Comment:</span> ', '</p>'),
        
        // billing rates
        '<p class="detail"><span class="smallCaps">Rate:</span><br>'
      );
    }
        


    var
      groupTaskCount = 0,
      groupTotalCost = 0,
      groupInstallmentExclusiveCost = 0,
      groupTrainingCount = 0;
      
      
      // content += ratePieces.join('<br>') + '</p></td>';
    
    
    // end billing group details
    var contentHeaderEndArray = ['</p>'];
    
    // if (group.email_contact_email){
      // contentHeaderEndArray.push(
        // '<p class="detail"><span class="smallCaps">Billing email:</span><br>',
        // '<a href="mailto:', group.email_contact_email, '">', group.email_contact_email
      // );
      
      // if (group.email_contact_name){
        // contentHeaderEndArray.push(
          // ' (', group.email_contact_name, ')'
        // );
      // }
      
      // contentHeaderEndArray.push(
        // '</a></p>'
      // );
    // }
    
    contentHeaderEndArray.push(
      '</td></tr>'
    );
    
    var contentItemRowsArray = [];
    
    var contentItemArray;

    var
      stripeRow = false,
      showLocation,
      activeLocation,
      rowClassArray;
      
      
    for (j in items){
      var task = items[j];
      if (
        (
          !task['output']
        )
        &&
        (
          (
            aa.existsIn(group['group_tasktype'], Number(task.data['tasktype_id'])) && aa.existsIn(group['group_location'], task.data['location_id'])
          )
          ||
          (
            aa.existsIn(group['group_tasktype'], Number(task.data['tasktype_id'])) && !group['group_location'].length
          )
          ||
          (
            aa.existsIn(group['group_location'], task.data['location_id']) && !group['group_tasktype'].length
          )
          ||
          (
            group['group_tasktype'] === undefined && group['group_location'] === undefined // this is the case when not in an account group, but the parent account
          )
        )
      ){      
      
        contentItemArray = [];
        rowClassArray = [];
      
        task.output = true;
        groupTaskCount++;
        if (task.data['isTraining']){
          groupTrainingCount++;
        }
        
        groupTotalCost += task.data.billingmatrix.total;
        // if (Number(task.data.installment_exclusive)){
        if (Number(task.data.billingmetric_nonroutine == 2)){
          groupInstallmentExclusiveCost += task.data.billingmatrix.total;
        }
        
        var billingPieces = [];

        var metric = task.data.billingmetric;
        var quantity = am.round(task.data.billable_quantity, 1);
        var metricOverride = task.data.billingmetric_override;
        var allowed = true;
        var otherQuantity = 0;
        
        if (clients.active && clients.active.id && Number(clients.active.num_locations) > 1 && task.data.location_id == clients.active.id){
          activeLocation = true;
          // rowClassArray.push('activeLocation activeLocationHighlight');
          // rowClassArray.push('activeLocation');
          rowClassArray.push('active');
          if (group.hasCurrentLocation === undefined){
            group.hasCurrentLocation = true;
          }
        }
        else {
          activeLocation = false;
        }
        
        // determine if other tasks for this day add up to a full day
        if (quantity && quantity < schedule.restrictions.minBillableHalfDay){
          var allowed = false;
          var k;
          
          var sameDay = aa.findObjectByKey(period.tasks.items, task.data.schedule_start_date, 'data.schedule_start_date');
          if (sameDay.length){
            var sameUser = aa.findObjectByKey(sameDay, task.data.physicist_id, 'data.physicist_id');
            if (sameUser.length){
              for (k = 0; k < sameUser.length; k++){
                if (sameUser[k].data.id != task.data.id && sameUser[k].data.location_id != 99){
                  otherQuantity += am.round(sameUser[k].data.billable_quantity, 1);
                }
              }
              if (otherQuantity >= schedule.restrictions.minBillableFullDay){
                allowed = true;
              }
            }
          }
        }
        
        if (metric == 1){
          if (
            metricOverride == 2
          ){
            billingPieces.push(quantity * 4, ' hour', (quantity != 1 ? 's' : ''));
          }
          else {
            if (quantity == 1){
              billingPieces.push('half day');
            }
            else if (quantity == 2){
              billingPieces.push('full day');
            }
            else {
              billingPieces.push(quantity, ' half days');
            }
          }
        }
        else if (metric == 2){
          if (
            metricOverride == 1
          ){
            if (
              quantity >= 8
              &&
              task.data.tasktype_id != 10 // don't change to full days if this is a shielding
            ){
              billingPieces.push("full day", (quantity > 8 ? ["<br>+ ", (quantity - 8), " hour", (quantity - 8 != 1 ? 's' : '')].join('') : ''));
            }
            else if (
              quantity >= 4
              &&
              task.data.tasktype_id != 10 // don't change to half days if this is a shielding
            ){
              billingPieces.push("half day");
              if (quantity > 4){
                billingPieces.push("<br>+ ", (quantity - 4), " hour", (quantity - 4 != 1 ? 's' : ''));
              }
            }
            else {
              billingPieces.push(quantity, " hour", (quantity != 1 ? 's' : ''));
            }
          }
          else {
            billingPieces.push(quantity, " hour", (quantity != 1 ? 's' : ''));
          }
        }
        
        if (metric == 3){
          billingPieces.push([task.data.billingmatrix[3], ' unit', (task.data.billingmatrix[3] != 1 ? 's' : '')].join(''));
        }
        
        if (!billingPieces.length){
          billingPieces.push('N/A');
        }

        if (stripeRow){
          rowClassArray.push('evenRow');
          stripeRow = false;
        }
        else {
          stripeRow = true;
        }
        
        if (task.data.schedule_start_date > config.today){ //thisMonth){
          rowClassArray.push('future');
        }
        
        var thisDate = dpExactDateTime(task.data.schedule_start);
        
        contentItemArray.push(
          '<tr id="historyTask', task.data.id, '" task="', task.data.id, '" class="historyTaskRow ', (rowClassArray.length ? rowClassArray.join(' ') : ''), '">',
          '<td class="center', (task.data.dont_publish_dates ? ' lighter' : ''), /* (task.data.doublebilled ? ' highlight' : ''), */ '"><span class="hidden">', task.data.schedule_start_date, '</span>', thisDate.toString('M/d'), '<span class="rowYear">', thisDate.toString('/yy'), '</span></td>'
        );
        
        if (config.showLocation){
          // contentItemArray.push(
            // '<td>', (task.data.account_id == 0 ? ['<nobr>', task.data.location_name, '</nobr>'].join('') : task.data.location_name),
            // (task.data.num_locations > 1 ? [' (', (task.data.unique_identifier || task.data.city), ')'].join('') : ''),
            // '</td>'
          // );
          contentItemArray.push(
            '<td class="locationName" style="white-space: nowrap">', 
            task.data.location_name_billing,
            '</td>'
          );
        }
        
        contentItemArray.push(
          '<td>', (task.data.isTraining || !Number(task.data.physicist_id) ? 'N/A' : period.physicistList[task.data.physicist_id].name_last), '</td>',
          '<td class="nowrap">', (task.data.tasktype_id ? aa.findObjectByKey(period.tasktypes, task.data.tasktype_id)[0].name : '&nbsp;'), '</td>',
          '<td>'
        );
        


        var summaryArray = [task.data.summary];
        
        // display associated reports with summary
        if (!task.data.isTraining && userHasPermission('debug')){
          var taskItems = aa.findObjectByKeys(period.taskItems, {
            // task_id: null,
            // location_id: {value: group.locations, rel: 'in'},
            location_id: task.data.location_id,
            service_date: task.data.schedule_start_date
          });
          
          if (taskItems.length){
            for (i = 0; i < taskItems.length; i++){
            
              // only display if report has no task specified,
              // or if report is explicitly associated with this task
              if (
                !taskItems[i].task_id
                ||
                taskItems[i].task_id == task.data.id
              ){
                taskItems[i].description = $.trim(taskItems[i].description);
                taskItems[i].notes = $.trim(taskItems[i].notes);
                summaryArray.push(
                
                  '<div class="notesReport"><p>',
                  //'TASK: ',
                  taskItems[i].description, ' (', taskItems[i].flag1, ', ', taskItems[i].flag2, ', ', taskItems[i].flag3, ')</p>',
                  
                  // notes regarding FOLLOWUP to this report, NOT this report
                  //(reports[i].notes ? ['<p>NOTES: ', reports[i].notes, '</p>'].join('') : ''),
                  
                  '</div>'
                  
                );
                
                // if (!task.data.taskFiles){
                  // task.data.taskFiles = [];
                // }
                
                // task.data.taskFiles.push(taskItems[i]);
                
                if (task.data.taskItems && task.data.taskItems[0]){
                  if (!task.data.taskItems[0].taskFiles){
                    task.data.taskItems[0].taskFiles = [];
                  }
                  task.data.taskItems[0].taskFiles.push(taskItems[i]);
                }
              }
            }
          }
        }
        
        // ensure the final cell content is at least one space, otherwise cell borders won't render properly in older IE versions
        summaryArray = summaryArray.join('');
        if (!summaryArray){
          summaryArray = '&nbsp;';
        }
        
        contentItemArray.push(task.data.isTraining ? task.data.user_name : summaryArray);         
        


        contentItemArray.push(
          '</td>',
          '<td class="center nowrap', (am.round(task.data.billable_quantity, 1) < 3 ? ' highlightWarning' : ''), '">', billingPieces.join(''), '</td>'
        );
        
        if (group.account_id != 0 && aa.existsIn(permissions, 'accounting')){
          contentItemArray.push(
            '<td class="right', /*(!task.data.is_billable ? ' highlightWarning' : ''), */ '">$', task.data.billingmatrix.total.toFixed(2), '</td>'
          );
        }
        
        contentItemArray.push(
          '</tr>'
        );
          
        // show "not billable" flag 
        if (!task.data.is_billable){
          rowClassArray.push('notes', 'expand-child');
          contentItemArray.push([
            '<tr class="', rowClassArray.join(' '), '"><td class="notesSubtree"></td><td class="notesContent" colSpan="', (config.showLocation ? '6' : '5'), '"><div class="notesWarning">Not billable</div></td></tr>'
          ].join(''));
        }
        // if billable but less than minimum number of hours, show details
        else if (task.data.billable_quantity > 0 && task.data.billingmetric == 2 && task.data.billable_quantity < schedule.restrictions.minBillableHalfDay){
          rowClassArray.push('notes', 'expand-child');
          contentItemArray.push([
            '<tr class="', rowClassArray.join(' '), '"><td class="notesSubtree"></td><td class="notesContent" colSpan="', (config.showLocation ? '6' : '5'),
            '"><div class="notesWarning">Billable time less than 3 hours',
            '<br>Other task(s) for this physicist on this day add up to ', otherQuantity, ' hours', (allowed ? '': [' (less than minimum of ', schedule.restrictions.minBillableFullDay, ' hours)'].join('')),
            '</div></td></tr>'
          ].join(''));
        }
        
        if (Number(task.data.billingmetric_nonroutine == 2)){
          rowClassArray.push('notes', 'expand-child');
          contentItemArray.push([
            '<tr class="', rowClassArray.join(' '), '"><td class="notesSubtree"></td><td class="notesContent" colSpan="', (config.showLocation ? '6' : '5'), '"><div class="notesWarning">Non-routine visit (billed hourly)</div></td></tr>'
          ].join(''));
        }
        
        
        
        // if (task.data.doublebilled){
          // contentItemsArray.push([
            // '<tr class="', rowClass, ' notes"><td class="notesSubtree"></td><td class="notesContent" colSpan="6"><div class="notesDoubleBooked">Double booked</div></td></tr>'
          // ].join(''));
        // }
        
        if (task.data.notes){
          task.data.notes = $.trim(task.data.notes);
          if (task.data.notes){
            rowClassArray.push('notes', 'expand-child');
            contentItemArray.push(
              '<tr class="', rowClassArray.join(' '), '"><td class="notesSubtree"></td><td class="notesContent" colSpan="', (config.showLocation ? '6' : '5'), '"><div>', as.replaceAll(as.replaceAll(task.data.notes, '\n\\s*\n', '<hr />'), '\n', '<br />'), '</div></td></tr>'
            );
          }
        }
        
        // var reports = aa.findObjectByKeys(period.reports, {
          // task_id: null,
          // location_id: {value: group.locations, rel: 'in'},
          // service_date: task.data.schedule_start_date
        // });
        
        // if (reports.length){
          // rowClassArray.push('notes', 'expand-child', 'billingHide');
          
          // for (i = 0; i < reports.length; i++){
            // reports[i].description = $.trim(reports[i].description);
            // reports[i].notes = $.trim(reports[i].notes);
            // contentItemArray.push(
              // '<tr class="', rowClassArray.join(' '), '"><td class="notesSubtree"></td><td class="notesContent" colSpan="', (config.showLocation ? '6' : '5'), '">',
              // '<div class="notesReport',
              
              // // (
                // // !reports[i].task_id ?
                
                // // [' notesReportUncertain" report="', reports[i].report_id, '" taskCandidate="', task.data.id, '"><p><a>Confirm</a>'].join('')

                // // :

                // // '"><p>'
              // // ),
              
              // '"><p>',
              
              // 'REPORT: ', reports[i].description, '</p>',
              // (reports[i].notes ? ['<p>NOTES: ', reports[i].notes, '</p>'].join('') : ''),
              // '</div></td></tr>'
            // );
          // }
        // }
        
          
        // break;
        
        
        contentItemRowsArray.push(contentItemArray.join(''));
      }
    }
    
    if (groupTaskCount || group.installment_rate){

      contentItemHeadersArray.push( // start billing group itemization
        '<tr class="invoiceItemization"><td colspan="2" class="billingTableItemizationContainer">',
        
        (group.invoice_table_header ? [
          '<p class="detail bold">', group.invoice_table_header,
          //(group.invoice_table_subheader ? ['<br /><span class="smallCaps">', group.invoice_table_subheader, '</span>'].join('') : ''),
          '</p>'
          ].join('')
          
          :
          
          ''
        )
      );
      
      if (groupTaskCount){
        contentItemHeadersArray.push(
          '<table class="tablesorter billingTableItemization', (config.showLocation ? ' highlightActive' : ''), '" cellspacing="0">',
          '<thead><tr><th class="center">Date</th>'
        );
      
        if (config.showLocation){
          contentItemHeadersArray.push('<th>Location</th>');
        }
        
        contentItemHeadersArray.push('<th>Physicist</th><th>Modality</th><th>Summary</th><th class="center">Time</th>');
          
        if (group.account_id != 0 && aa.existsIn(permissions, 'accounting')){
          contentItemHeadersArray.push('<th class="right">Cost</th>');
        }
        
        contentItemHeadersArray.push('</tr></thead><tbody>');
      }
      
    
      

    
      group.statementFee = 0;
      
      // fblog('billingOutputGroup(): groupTotalCost = ' + groupTotalCost + ', + adjustment (' + group.invoice_adjustment + ') = ' + (groupTotalCost + Number(group.invoice_adjustment)));
      
      // add $5 statement fee to postal invoices after 1/1/2010
      if (
        period.period >= '2010-01'
        &&
        Number(group.invoice_print)
        &&
        !Number(group.invoice_waive_fee)
      ){
        group.invoice_adjustment = Number(group.invoice_adjustment) + 5;
        group.statementFee = 5;
      }
      
      // if (groupTotalCost){
        // groupTotalCost += Number(group.invoice_adjustment);
      // }
      
      if (groupTaskCount){
        contentFooterArray.push('</tbody></table>'); // end billing group itemization
      }
        
      // calculate total amount to display based on installment disposition
      var groupTotalDisplayCost = 0;
      if (Number(group.installment_rate)){ // installment rate in effect
        if (Number(group.installment_exclusive)){ // installment rate doesn't include any specific service costs
          groupTotalDisplayCost = Number(group.installment_rate) + groupTotalCost;
        }
        else { // installment rate includes all service costs except those marked as exclusive
          groupTotalDisplayCost = Number(group.installment_rate) + groupInstallmentExclusiveCost;
        }
      }
      else { // no installment rate, just show total of service costs
        groupTotalDisplayCost = groupTotalCost;
      }
      
      if (groupTotalDisplayCost){
        //groupTotalCost += Number(group.invoice_adjustment);
        groupTotalDisplayCost += Number(group.invoice_adjustment);
      }
      
      period.totalCostAdjusted += groupTotalDisplayCost;
      
      
      
      if (!config.hideTotal){
        contentFooterArray.push('<div class="billingItemizationTotal">');
        
        var totalGap = false;
        
        if (groupTotalCost != groupTotalDisplayCost){
          contentFooterArray.push('<p><span class="smallCaps"><span class="', (Number(group.installment_rate) ? 'lighter' : 'light'), '">Total Cost: </span><span class="', (Number(group.installment_rate) ? 'lighter' : ''), '">$', groupTotalCost.toFixed(2), '</span></span></p>');
          totalGap = true;
        }
        if (Number(group.installment_rate)){
          contentFooterArray.push('<p><span class="smallCaps"><span class="light">Installment Fee: </span>$', Number(group.installment_rate).toFixed(2), '</span></p>');
          totalGap = true;
          if (Number(groupInstallmentExclusiveCost)){
            contentFooterArray.push('<p><span class="smallCaps"><span class="light">Non-Routine Fee: </span>$', Number(groupInstallmentExclusiveCost).toFixed(2), '</span></p>');
            totalGap = true;
          }
        }
        if (Number(group.statementFee)){
          contentFooterArray.push('<p><span class="smallCaps"><span class="light">Statement Fee: </span>$', Number(group.statementFee).toFixed(2), '</span></p>');
          totalGap = true;
        }

        contentFooterArray.push('<p', (totalGap ? ' style="margin-top: .75em"' : ''), '><span class="smallCaps">Amount Due: </span> <b>$', as.addCommas(groupTotalDisplayCost.toFixed(2)), '</b></div>');
      }
      
      contentFooterArray.push(
        '</td></tr></table>', // end billing group table
        '</td></tr>' // end billing group row
      );
      
      
      var contentRates = [];
      
      
      if (group.account_id != 0){
        // calculate and display rates
        var ratePieces = [];
        var rateMap = {
          defaults: [],
          others: []
        };
        var qualifierNameArray;
        var thisRateGroup;
        var thisRateGroupQualifiers;
        
        var location_id, tasktype_id, metric_id;
        
        // fblog('new group ' + group.invoice_number);
        
        // loop through rates in meta object
        for (location_id in group.rates){
        
          // if no group location defined
          // or if group location defined, but matching location not in rates (use default)
          // or if group location defined, and matching location in rates (use matching)
          // otherwise skip (if location defined, and matching location in rates, don't show default...)
            
          // if (
            // !group.group_location.length
            // ||
            // (
              // group.group_location.length
              // &&
              // aa.existsIn(group.group_location, location_id)
            // )
          // ){
          
          if (group.groupCount !== undefined || location_id == 0 || (group.group_location && aa.existsIn(group.group_location, location_id))){
            for (tasktype_id in group.rates[location_id]){
              if (group.groupCount !== undefined || tasktype_id == 0 || (group.group_tasktype && aa.existsIn(group.group_tasktype, tasktype_id))){
                for (metric_id in group.rates[location_id][tasktype_id]){
                  
                  if (
                    metric_id == group.default_billingmetric
                  ){
                    thisRateGroup = rateMap.defaults;
                  }
                  else {
                    thisRateGroup = rateMap.others;
                  }

                  if (!thisRateGroup[metric_id]){
                    thisRateGroup[metric_id] = [];
                  }
                  
                  // only include if there are applicable tasks
                  // if (
                    // (
                      // group.rates[location_id][tasktype_id][metric_id].tasks
                      // &&
                      // group.rates[location_id][tasktype_id][metric_id].tasks.length
                    // )
                    // ||
                    // (
                      // !group.rates[location_id][tasktype_id][metric_id].tasks
                      // &&
                      // group.rates[location_id][tasktype_id][metric_id].tasks
                    // )
                    
                      
                    
                  // ){
                  
                    if (!thisRateGroup[metric_id][group.rates[location_id][tasktype_id][metric_id].rate]){
                      thisRateGroup[metric_id][group.rates[location_id][tasktype_id][metric_id].rate] = [];
                    }
                    thisRateGroupQualifiers = thisRateGroup[metric_id][group.rates[location_id][tasktype_id][metric_id].rate];
                    
                    if (tasktype_id !== 0 && tasktype_id !== '0'){
                      var temp = aa.findObjectByKey(period.tasktypes, tasktype_id, 'id');
                      thisRateGroupQualifiers.push(aa.findObjectByKey(period.tasktypes, tasktype_id, 'id')[0].name);
                      //thisRateGroupQualifiers.push(group.rates[location_id][tasktype_id][metric_id].name);
                    }
                    
                    if (location_id !== 0 && location_id !== '0'){
                      var matchingLocation = aa.findObjectByKey(items, location_id, 'data.location_id');
                      if (matchingLocation.length){
                        // qualifierNameArray = [matchingLocation[0].data.location_name];
                        // if (matchingLocation[0].data.unique_identifier){
                          // qualifierNameArray.push(' (', matchingLocation[0].data.unique_identifier, ')');
                        // }
                        // else if (matchingLocation[0].data.num_locations > 1 && matchingLocation[0].data.city){
                          // qualifierNameArray.push(' (', matchingLocation[0].data.city, ')');
                        // }
                        // thisRateGroupQualifiers.push(qualifierNameArray.join(''));
                        thisRateGroupQualifiers.push(matchingLocation[0].data.location_name_billing);
                      }
                    }
                    
                    // var rateClass = '';
                    // if (metric_id == group.default_billingmetric){
                      // rateClass = 'bold';
                    // }

                    // if (metric_id == group.default_billingmetric){
                      // rateMap.defaults = thisRateGroup;
                    // }
                    // else {
                      // rateMap.others = thisRateGroup;
                    // }
                    
                    //fblog('tasks for ' + location_id + ',' + tasktype_id + ',' + metric_id + ' [' + group.rates[location_id][tasktype_id][metric_id].tasks.join(',') + ']');
                  // }
                  //else {
                    //fblog('no tasks for ' + location_id + ',' + tasktype_id + ',' + metric_id);
                  //}

                }
              }
            }
          }
        }
        
        // loop through rates in meta object
        for (location_id in group.rates_doublebilled){
          if (group.groupCount !== undefined || location_id == 0 || (group.group_location && aa.existsIn(group.group_location, location_id))){
            for (tasktype_id in group.rates_doublebilled[location_id]){
              if (group.groupCount !== undefined || tasktype_id == 0 || (group.group_tasktype && aa.existsIn(group.group_tasktype, tasktype_id))){
                for (metric_id in group.rates_doublebilled[location_id][tasktype_id]){
                  
                  if (
                    metric_id == group.default_billingmetric
                  ){
                    thisRateGroup = rateMap.defaults;
                  }
                  else {
                    thisRateGroup = rateMap.others;
                  }

                  if (!thisRateGroup[metric_id]){
                    thisRateGroup[metric_id] = [];
                  }
                  if (!thisRateGroup[metric_id][group.rates_doublebilled[location_id][tasktype_id][metric_id].rate]){
                    thisRateGroup[metric_id][group.rates_doublebilled[location_id][tasktype_id][metric_id].rate] = [];
                  }
                  thisRateGroupQualifiers = thisRateGroup[metric_id][group.rates_doublebilled[location_id][tasktype_id][metric_id].rate];
                  
                  if (tasktype_id !== 0 && tasktype_id !== '0'){
                    thisRateGroupQualifiers.push([aa.findObjectByKey(period.tasktypes, tasktype_id, 'id')[0].name, ' double booked'].join(''));
                  }
                  
                  if (location_id !== 0 && location_id !== '0'){
                    var matchingLocation = aa.findObjectByKey(items, location_id, 'data.location_id');
                    if (matchingLocation.length){
                      // qualifierNameArray = [matchingLocation[0].data.location_name];
                      // if (matchingLocation[0].data.unique_identifier){
                        // qualifierNameArray.push(' (', matchingLocation[0].data.unique_identifier, ')');
                      // }
                      // else if (matchingLocation[0].data.num_locations > 1 && matchingLocation[0].data.city){
                        // qualifierNameArray.push(' (', matchingLocation[0].data.city, ')');
                      // }
                      // thisRateGroupQualifiers.push([qualifierNameArray.join(''), ' double booked'].join(''));
                      thisRateGroupQualifiers.push([matchingLocation[0].data.location_name_billing, ' double booked'].join(''));
                    }
                  }
                  
                  // var rateClass = '';
                  // if (metric_id == group.default_billingmetric){
                    // rateClass = 'bold';
                  // }

                  // if (metric_id == group.default_billingmetric){
                    // rateMap.defaults = thisRateGroup;
                  // }
                  // else {
                    // rateMap.others = thisRateGroup;
                  // }

                }
              }
            }
          }
        }
        
        var thisRatePiece;

        for (i in rateMap.defaults){
            for (j in rateMap.defaults[i]){
                thisRatePiece = billingRateDisplay(group, groupTrainingCount, i, j, rateMap.defaults[i][j], 'bold');
                if (thisRatePiece){
                  ratePieces.push(thisRatePiece);
                }
            }
        }
        for (i in rateMap.others){
            for (j in rateMap.others[i]){
                thisRatePiece = billingRateDisplay(group, groupTrainingCount, i, j, rateMap.others[i][j], '');
                if (thisRatePiece){
                  ratePieces.push(thisRatePiece);
                }
            }
        }
        
        
        contentRates.push(ratePieces.join('<br>'));
      }
      
      
      if (!groupTotalCost && !group.installment_rate){
        contentHeaderRightTopArray.push('<p class="detail detailBox backgroundWarning highlightRed"><span class="smallCaps">No billable items - invoice won\'t print</span></p>');
      }
      
      if (Number(group.invoice_waive_fee)){
        contentHeaderRightTopArray.push('<p class="detail detailBox backgroundWarning highlightRed"><span class="smallCaps">Invoice fee waived</span></p>');
      }
      
      // show installment details if applicable
      if (Number(group.installment_rate)){
        contentHeaderRightTopArray.push(billingInstallmentOutput(group, config, period));
      }
      

      
      if (Number(group.invoice_email)){
        if (group.email_contact_email){
          var pp;
          var sent = false;
          
          // var pp = aa.findObjectByKey(period.progressPaperless, group.invoice_number, 'invoice');
          pp = aa.findObjectByKey(billing.paperlessStatus, group.invoice_number, 'invoice', null, true);
          
          if (!pp){
            //pp = false;
            pp = {
              account: group.account_id,
              email: group.email_contact_email,
              name: group.email_contact_name,
              invoice: group.invoice_number,
              period: period.period
            };
            
            billing.paperlessStatus.push(pp);
          }
          
          if (pp.status == 'sent'){
            sent = true;
          }
          
          contentHeaderRightTopArray.push(
            '<div class="detail highlight paperlessStatus', (sent ? ' paperlessStatusSent' : ''), '"',
            //' style="',
            //'border: 1px solid #6a6; ',
            //'background-color: #efd; padding: .25em .5em .25em .25em; margin-left: 0"',
            '>',
            //'<span class="smallCaps bold">Paperless Invoice</span><br>',
            '<a title="Click to email invoice to contact(s) below" class="iconleftgapless ', (sent ? 'icon-ok' : 'icon-email'), ' smallCaps linkPaperless"',
            ' account="', group.account_id, '" period="', period.period, '" invoices="', group.invoice_number, '"',
            // ' href="mailto:', group.email_contact_email, '"',
            '>',
            'Send paperless',
            '</a>'
          );
          
          if (pp.status){
            contentHeaderRightTopArray.push(
              '<span class="paperlessInProgress small', (!aa.existsIn(['sent', 'sending'], pp.status) ? ' highlightWarning' : ''), '"> (', pp.status, (pp.status == 'sent' ? [' ', dpExactDateTime(pp.timestamp).toString('M/d h:mmt')].join('') : [': ', pp.details].join('')), ')</span>'
            );
          }
          
          contentHeaderRightTopArray.push(
            '<br />',
            
            '<p class="iconleftgapless small">',
            //'<span class="smallCaps">Contact: </span>',
            // '<a href="mailto:', group.email_contact_email, '">',
            '<b>',
            group.email_contact_email,
            '</b>'
          );
          
          if (group.email_contact_name){
            contentHeaderRightTopArray.push(
              ' (', group.email_contact_name, ')'
            );
          }
          
          if (group.email_cc_contact_email){
            contentHeaderRightTopArray.push(
              '<br />',
              
              '<p class="iconleftgapless small">',
              'cc: <b>',
              group.email_cc_contact_email,
              '</b>'
            );
            
            if (group.email_cc_contact_name){
              contentHeaderRightTopArray.push(
                ' (', group.email_cc_contact_name, ')'
              );
            }
          }
          
          contentHeaderRightTopArray.push(
            // '</a>',
            '</p>',
            '</div>'
          );
        }
        else {
          contentHeaderRightTopArray.push(
            '<div class="detail paperlessStatus backgroundWarning highlightRed" style="padding-left: .5em"><span class="smallCaps">No email specified for invoices - can\'t send paperless</span></div>'
          );
        }
      }


      if (!period.invoices){
        period.invoices = [];
      }
      if (!billing.invoices){ 
        billing.invoices = [];
      }
      if (!group.invoices){
        group.invoices = [];
      }
      
      var invoiceMeta = {
        number: group.invoice_number,
        account: group.account_id,
        period: period.period,
        paperless: Number(group.invoice_email),
        postal: Number(group.invoice_print)
      };
      
      if (group.hasCurrentLocation){
        invoiceMeta.hasCurrentLocation = true;
      }
      
      period.invoices.push(invoiceMeta);
      billing.invoices.push(invoiceMeta);
      group.invoices.push(invoiceMeta);
      
    
      // if (groupTaskCount){
        // var total = [];
        
        // total.push('<tr><td class="billingGroupRow"><table class="billingGroup">');
        
        // if (!config.hideHeader){
          // total = total.concat(contentHeaderArray, [contentRates], contentHeaderEnd);
        // }
        
        // total = total.concat(contentItemsArray);
        
        // return total.join('');
        
      return {
        header: contentHeaderArray.concat(
          contentHeaderRightTopArray,
          contentHeaderRightBottomArray,
          contentRates,
          contentHeaderEndArray,
          contentItemHeadersArray
        ).join(''),
        //itemHeaders: contentItemHeadersArray.join(''),
        rows: contentItemRowsArray,
        force: Boolean(Number(group.installment_rate)),
        footer: contentFooterArray.join('')
      };
    }
    else {
      // return false;
      return {
        // header: contentHeaderArray.concat([contentRates], contentHeaderEnd, contentItemHeadersArray).join(''),
        // rows: contentItemRowsArray,
        // footer: contentFooterArray.join(''),
        //itemHeaders: contentItemHeadersArray.join('')
      }
    }
  }
  
  
  
  
  
  
  function billingInstallmentOutput(group, config, period){
    var outputArray = [];
    // var startDate = dpExactDate(group.installment_start_date);
    var startDate;
    var endDate;
    var periodDate = [period.period, '-01'].join('');
    var periodInstallmentStartDate = group.installment_start_date;
    var tasks;
    
    var isCurrentPeriodSummary = false;
    
    
    if (period){
      while (periodInstallmentStartDate <= (dpExactDate(periodDate).addMonths(-12).toString('yyyy-MM-dd'))){
        periodInstallmentStartDate = dpExactDate(periodInstallmentStartDate).addMonths(12).toString('yyyy-MM-dd');
      }
      startDate = dpExactDate(periodInstallmentStartDate);
      endDate = dpExactDate(periodDate).addMonths(1).addDays(-1);
      
      if (startDate.toString('yyyy-MM') == dpExactDate(periodDate).addMonths(-11).toString('yyyy-MM')){
        isCurrentPeriodSummary = true;
      }
      
      tasks = aa.findObjectByKey(
        group.installment_tasks,
        {
          start: startDate.toString('yyyy-MM-dd 00:00:00'),
          end: endDate.toString('yyyy-MM-dd 23:59:59')
        },
        'schedule_start'
      );
    }
    else {
      var currentPeriod = new Date();
      while (periodInstallmentStartDate <= (currentPeriod.addMonths(-12).toString('yyyy-MM-dd'))){
        periodInstallmentStartDate = dpExactDate(periodInstallmentStartDate).addMonths(12).toString('yyyy-MM-dd');
      }

      startDate = dpExactDate(periodInstallmentStartDate);
      endDate = startDate.clone().addMonths(12).addDays(-1);
      tasks = group.installment_tasks_account;
    }
    
    outputArray.push(
      '<div class="detail detailBox installmentContainer backgroundYellow light"><p><span class="smallCaps dark">Installment billing',
      (aa.existsIn(permissions, 'accounting') ? [': <b><span class="large">$', as.addCommas(Number(group.installment_rate).toFixed(2)), '</span></b>/month'].join('') : ''),
      '</span></p>'
    );
    
    if (group.installment_description){
      // outputArray.push('<br /><span class="small" style="line-height: 1.25em">', group.installment_description.replace(';', '<br />'), '</span>');
      outputArray.push('<p><span class="small">', group.installment_description.replace(/\(/g, '<br>(').replace(';', '</span></p><p><span class="small">'), '</span></p>');
      var re = /\)/;
    }
    outputArray.push('</div>');
    
    // list tasks so far for this installment period
    if (tasks && tasks.length){
      
      outputArray.push(
        '<div class="detail detailBox installmentContainer backgroundYellowLight light" style="padding: .25em .5em"><p><span style="color: #666"><span class="smallCaps dark">',
        'Installment usage: <b><span class="large">', startDate.toString('M/d/yy'), ' - ', endDate.toString('M/d/yy'),
        '</span></b></span></p>'
      );
      
      var tasksByDevice = [];
      var git;
      var thisDevice = 0;
      var thisDeviceTasks = [];
      for (i = 0; i < tasks.length; i++){
        git = tasks[i];
        if (git.device_id != thisDevice){
          thisDevice = git.device_id;
          thisDeviceTasks = {
            id: thisDevice,
            unique_id: git.unique_id,
            total: 0,
            max: git.installment_allowance,
            tasks: []
          };
          tasksByDevice.push(thisDeviceTasks);
        }
        thisDeviceTasks.tasks.push(git);
        thisDeviceTasks.total += Number(git.billable_quantity);
        if (thisDeviceTasks.total > thisDeviceTasks.max){
          git.nonRoutine = true;
        }
      }
      
      var thisDeviceTasksHTML;
      var startDate;
      var tbdt;
      var isThisPeriod;
      for (i = 0; i < tasksByDevice.length; i++){
        thisDeviceTasksHTML = [];
        outputArray.push(
          '<p><span class="small"><b>Unit ', (tasksByDevice[i].unique_id || tasksByDevice[i].id),
          ' (<span', (tasksByDevice[i].total > tasksByDevice[i].max ? ' class="highlightWarning"' : ''), '>', tasksByDevice[i].total, ' of ', tasksByDevice[i].max, '</span>):</b> ');
        for (j = 0; j < tasksByDevice[i].tasks.length; j++){
          tbdt = tasksByDevice[i].tasks[j];
          startDate = dpExactDateTime(tbdt.schedule_start);
          tbdt.schedule_start_date = startDate.toString('M/d');
          tbdt.timeblock = startDate.toString('tt');
          isThisPeriod = (startDate.toString('yyyy-MM') == period.period);
          thisDeviceTasksHTML.push([
            '<span style="cursor: help" class="',
            (tbdt.nonRoutine ? ' highlightWarning' : ''),
            (isThisPeriod ? ' dark bold' : ''),
            '" title="', tbdt.schedule_start_date, ' ',
            (tbdt.billable_quantity == 1 ? [tbdt.timeblock, ' (half day)'].join('') : '(full day)'), ' ', tbdt.physicist, (tbdt.summary ? [': ', tbdt.summary].join('') : ''),
            (tbdt.nonRoutine ? ' [OVER INSTALLMENT ALLOWANCE]' : ''), '">', tbdt.schedule_start_date,
            (tbdt.billable_quantity == 1 ? [' <span class="smallCaps">', tbdt.timeblock, '</span>'].join('') : ''),
            '</span>'
          ].join(''));
        }
        
        outputArray.push(thisDeviceTasksHTML.join(', '), '</span></p>');
      }
      
      outputArray.push('</div>');
      
    }
    
    // outputArray.push('</div>');
    outputArray = outputArray.join('');
    
    if (isCurrentPeriodSummary){
      if (group.account){
        group.account.currentInstallmentSummary = outputArray;
      }
      else {
        group.currentInstallmentSummary = outputArray;
      }
    }
    
    return outputArray;
  }
  
  
  
  
  

  function billingRateDisplay(group, groupTrainingCount, metric_id, rate, qualifiers, cssClass){
    //fblog('test');
    var metricNames = ['', 'half day', 'hour', 'training unit', 'full day'];
    // don't display this rate if...
    if (
      
      !(
        (group.default_billingmetric != 1 && metric_id == 1)  // default billingmetric is NOT 1 and this rate's metric IS 1
        ||
        (
          (
            groupTrainingCount === 0
            ||
            groupTrainingCount === '0'
          )
          && metric_id == 3
        )  // there are NO training items and this rate's metric IS 3
      )
    ){
      return ['<span class="', cssClass, '">$', rate, ' / ', metricNames[metric_id], (qualifiers.length ? [' <span class="small">(', qualifiers.join(', '), ')</span>'].join('') : ''), '</span>'].join('');
    }
    else {
      return false;
    }
  }
    







  
  
  
  function billingInvoiceView(config){
    
    var
      href = '',
      // periodString = config.period || '',
      invoiceNumbers = config.invoices || [],
      // accountString = config.account || '',
      periods = config.periods || (config.period ? [config.period] : []),
      shortDates = [],
      accounts = config.accounts || (config.account ? [config.account] : []),
      target = config.target;
    
    // if (target){
    // // if (!periodString || !accountString){
    
      // var i, j, shortDate, thisAccount;
      
      // for (i = 0; i < shared.periods.length; i++){
        // for (j = 0; j < shared.periods[i].selected.length; j++){
          // aa.pushUnique(periods, shared.periods[i].period);
          // aa.pushUnique(accounts, shared.periods[i].selected[j]);
          // shortDate = [Number(ad.sqlGetMonth(shared.periods[i].start)), ad.sqlGetYearShort(shared.periods[i].start)].join('');
          // aa.pushUnique(shortDates, shortDate);
          // // aa.pushUnique(invoiceNumbers, [shortDate, shared.periods[i].selected[j]].join(''));
          // thisAccount = aa.findObjectByKey(shared.periods[i].accounts, shared.periods[i].selected[j], 'account_id');
          // if (thisAccount.length){
            // aa.concatUnique(invoiceNumbers, thisAccount[0].invoiceNumbers);
          // }
        // }
      // }
        
      
      
      // // if (typeof target == 'object' && target.length > 1){ // passed target is an array
        // // accounts = target.join('_');
        // // accountString = '_multiple';
      // // }
      // // else {
        // // accounts = target;
        // // accountString = target;
      // // }
      
      // periodString = periods.sort().join('_');
      // accountString = accounts.sort().join('_');
    
    // }
    
    
    if (config.selected){
      var selectedInvoices = _.select(billing.invoices, function(element){
          return element.selected;
      });
      
      invoiceNumbers = _.pluck(selectedInvoices, 'number').sort();
      accounts = _.uniq(_.pluck(selectedInvoices, 'account'));
      periods = _.uniq(_.pluck(selectedInvoices, 'period'));
    }
    

    href = [
      shared.urlRoot,
      '/invoice'
      // '/',
      // accountString,
      // '/',
      // periodString
    ];

    
    // fblog(invoiceNumbers.length);
    
    
    if (config.email){
      // href.push('/email');
      
      var invoiceEmailAjax = $.ajax({
        url: href.join(''),
        //data: '+',
        //mode: 'abort',
        data: {
          email: true,
          accounts: JSON.stringify(accounts),
          periods: JSON.stringify(periods),
          invoices: JSON.stringify(invoiceNumbers)
        },
        success: function (data, textStatus){
          fblog('billingInvoiceView(email): success');
          
          try {
            var decoded = eval(["(", data, ")"].join(''));
            
            if (decoded.success === false){
              fblog(decoded.message);
              if (decoded.auth === false){
                window.location.reload(); //href = "/login/clients";
              }
              return false;
            }
            
          }
          catch (err){
            fblog(['billingInvoiceView(email) decode failure: ', err.name, ', ', err.message].join(''));
            return;
          }
          
          // abort this ajax call, since it continues to run on the server
          invoiceEmailAjax.abort();
          
          fbdir('decoded', true, decoded);
        },
        error: function (XMLHttpRequest, textStatus, errorThrown){
          fblog(['billingInvoiceView(email): ajax error = ', textStatus, ' (', errorThrown, ')'].join(''));
        }
      
      });
      
      billingPaperlessInProgressAdd(invoiceNumbers);
      
    }



    
    else {
      href.push('/Bio-Med_Invoice_');
      
      if (accounts.length > 1){
        href.push('multiple');
      }
      else if (periods.length > 1){
        href.push(accounts.join('_'), '_multiple');
      }
      else {
        href.push(invoiceNumbers.sort().join('_'));
      }
      
      href.push('.pdf');
      
      postLink({
        href: href.join(''),
        newWindow: true,
        data: {
          accounts: accounts,
          periods: periods,
          invoices: invoiceNumbers
        }
      });
      //window.open(href.join(''));
    }
    
  }
  
  
  
  
  function billingSendPaperlessUnsent(){
    var pp = aa.findObjectByKeys(billing.paperlessStatus, {status: {value: ['sent', 'sending'], rel: 'notin'}});
    // var pp = billing.paperlessStatus;
    var accounts, periods, invoices;
    
    
    if (pp.length){
      
      accounts = [], periods = [], invoices = [];
      
      for (var i = 0; i < pp.length; i++){
        
        aa.pushUnique(accounts, pp[i].account);
        aa.pushUnique(periods, pp[i].period);
        aa.pushUnique(invoices, pp[i].invoice);
        
      }
      
      billingInvoiceView({
        accounts: accounts,
        periods: periods,
        invoices: invoices,
        email: true
      });
        
      billingPaperlessUnsentUpdate();
      updaterStart('billingProgressPaperless');
      
    }
    
    else {
      fblog('billingSendPaperlessUnsent(): no unsent paperless invoices');
    }
  }
  
  
  
  
  // send an HTTP request (like clicking an A link) including POST variable(s)
  // config.href = url of link to open
  // config.data = object defining name/value pairs to be passed as POST data
  // config.newWindow = true to open link in new window
  function postLink(config){
    var form = [
      '<form method="post"',
      config.newWindow ? ' target="_blank"' : '',
      ' action="', config.href, '">'
    ];
    
    for (var i in config.data){
      form.push('<input type="hidden" name="', i, '" value="', as.htmlEncode(JSON.stringify(config.data[i])), '" />');
    }
    
    form.push('</form>');
    
    $(form.join('')).appendTo($('body')).submit(); //.remove();
  }


  
  
  
  // select all rows in billing table
  function billingSelectAll(){
    billingSelectNone();
    
    var i;
    var bi = billing.invoices;
    var invoiceView = $('#btc_displayInvoices option:selected').attr('value');
    var locationView = $('#btc_displayLocations option:selected').attr('value');
    var selectedElements = $([]);
    
    for (i = 0; i < bi.length; i++){
      if (
        (
          invoiceView == 0
          ||
          (invoiceView == 1 && bi[i].paperless)
          ||
          (invoiceView == 2 && bi[i].postal)
        )
        &&
        (
          locationView == 'allLocations'
          ||
          (locationView == 'thisLocation' && bi[i].hasCurrentLocation)
        )
      ){
        bi[i].selected = true;
        selectedElements = selectedElements.add($(['#billingGroupRowInvoice', bi[i].number].join('')));
      }
    }
    
    selectedElements.closest('tr.account').addClass('accountselected'); 

    billingSelectedStatusUpdate();
    // {
      // target: 'billingTableSelectedStatus',
      // disable: 'btc_button_billingTable_billingViewSelected'
      // singular: 'invoice'
    // });
  }
  
  
  
  
  // deselect all rows in billing table
  function billingSelectNone(){
    if (billing.invoices && billing.invoices.length){
      var i;
      // for (i = 0; i < shared.periods.length; i++){
        // // $(['#billingTable', shared.periods[i].period, ' tr.account'].join('')).removeClass('accountselected'); 
        // shared.periods[i].selected = [];
        // // billingSelectedStatusUpdate({
          // // target: 'billingTableSelectedStatus',
          // // disable: 'btc_button_billingTable_billingViewSelected',
          // // // source: billing.selected,
          // // // singular: (shared.activeInterface == 'clients' ? 'billing period' : 'account')
          // // singular: 'invoice'
        // // });
      // }
      
      var billingContainer = (shared.activeInterface == 'billing' ? '#jq-formstatic-inject' : '#injectBilling');
      
      $([billingContainer, ' > table.billingTable > tbody > tr.account'].join('')).removeClass('accountselected'); 
      
      for (i = 0; i < billing.invoices.length; i++){
        billing.invoices[i].selected = false;
      }
      
      // billing.selected = [];
      // billing.selectedElements = $([]);
      
      billingSelectedStatusUpdate();
      // {
        // target: 'billingTableSelectedStatus',
        // disable: 'btc_button_billingTable_billingViewSelected'
        // // source: billing.selected,
        // // singular: (shared.activeInterface == 'clients' ? 'billing period' : 'account')
        // // singular: 'invoice'
      // });
      
      // // search as you type
      // var found = $('div.accountName').highlight('beebe');
      
      // if (found.length){
        // $(document).scrollTo($(found[0]), {
          // offset: {left: 0, top: -20},
        // });
      // }
    }
  }







  // accepts any number of string arguments, one for each timer to start
  // or zero arguments will start all timers previously started (listed in shared.timers.active array)
  function updaterStart(){
    var i, timer, newIdle = false, args = aa.toArray(arguments);
    
    fblog('updaterStart(' + args + '):');
    updaterStop();
    
    for (i = 0; i < args.length; i++){
      timer = args[i];
    
      if (timer && !aa.existsIn(shared.timers.active, timer)){
      
        // if (shared.timers.active.length){ //clients.initIdle || schedule.initIdle || staff.initIdle){
          // $.idleTimer('destroy');
        // }
        
        shared.timers.active.push(timer);
        
        newIdle = true;
      }
    }
    
    if (newIdle){
      $.idleTimer('destroy');
      // $(document).unbind("idle.idleTimer");
      $(document).bind("idle.idleTimer", function(){
        fblog('idle');

        updaterStop();
      });
      
      // $(document).unbind("active.idleTimer");
      $(document).bind("active.idleTimer", function(){
        fblog('active');

        // get a fresh update right away (don't wait for timeoutGetUpdated to elapse first)
        for (var i = 0; i < shared.timers.active.length; i++){
          shared.timers[shared.timers.active[i]].callback();
        }
        
        updaterStart();
      });
      
      $.idleTimer(shared.timers.timeoutIdle * 1000);
    }
    
    for (var i = 0; i < shared.timers.active.length; i++){
      shared.timers.start(shared.timers.active[i]);
    }
    

  }
  
  // stop ajax updater interval
  function updaterStop(){
    var i;
    var args = aa.toArray(arguments);
    
    //fblog('updaterStop(' + args + '):');
    
    if (!args.length){
      $('body').stopTime();
    }
    
    else {
      for (i = 0; i < args.length; i++){
        $('body').stopTime(args[i]);
      }
    }
  }
  
  function updaterKill(){
    var i, args = aa.toArray(arguments);
    
    fblog('updaterKill(' + args + '):');
    
    for (i = 0; i < args.length; i++){
      $('body').stopTime(args[i]);
      aa.removeByValue(shared.timers.active, args[i]);
    }
    
  }
  
  
  
  
  
  
  
  
  
  function pingStart(){
    $(document).stopTime('ping');
    
    $(document).everyTime([shared.performance.timeoutPing, 's'].join(''), 'ping', ping); 
  }

  
  // ping server for authentication status
  // if logged out in another window or at another computer, reload page (force login prompt)
  function ping(){
    ad.stopwatchStart('ajaxPing');
    
    $.ajax({
      url: [shared.urlRoot, '/ajaxPing'].join(''),
      // data: params,
      //type: 'POST',
      success: function (data, textStatus){
        var logstringArr = [['ajaxPing(): retrieved from server (', (ad.stopwatchEnd('ajaxPing') / 1000), 's)'].join('')];
        var decoded;
      
        try {
          decoded = eval(["(", data, ")"].join(''));
        }
        catch (err){
          fblog(['ajaxPing() decode failure: ', err.name, ', ', err.message].join(''));
          return;
        }
        
        if (decoded.auth === false){
          window.location.reload(); //href = "/login/clients";
          return false;
        }
        
        if (decoded.success === false){
          fblog(['ajaxPing(): success = false, message = ', decoded.message].join(''));
          return false;
        }
        
        //fblog(logstringArr.join(', '));
      },
      error: function (XMLHttpRequest, textStatus, errorThrown){
        fblog(['ajaxPing(): ajax failure = ', textStatus, ' (', errorThrown, ')'].join(''));
      }
    });
  }
  
  














  
  
  
  
  
  
  
  function staffAvailability(){
    var 
      i,
      outputStr = [],
      sva = shared.availability.items,
      svai,
      thisLocationName,
      physicist,
      stripe = false,
      today = new Date().toString('yyyy-MM-dd'),
      thisYear = new Date().toString('yyyy'),
      scheduleDate,
      dateStart,
      indexStart = 0,
      monthThis,
      monthPrevious,
      timeDiff,
      accrualDef,
      past = true,
      create = false,
      hidden = false;
      
    dateStart = new Date();
    dateStart = dateStart.addMonths(-1).set({'day': 1}).toString('yyyy-MM-dd');
    
    while (sva[indexStart] && sva[indexStart].schedule_start_date < dateStart){
      indexStart++;
    }
    
    if (!$('#staffAvailability').length){
      create = true;
      if (aa.existsIn(staff.preferences.hidden, 'staffAvailability')){
        hidden = true;
      }
      
      outputStr.push('<div id="staffAvailability" class="tableContainer');
      
      if (staff.config.availabilityList.width){
        outputStr.push(' tableContainer', staff.config.availabilityList.width);
      }
      
      if (staff.config.availabilityList.align){
        outputStr.push(' tableContainer', as.toSentenceCase(staff.config.availabilityList.align));
      }
      
      outputStr.push(
        '">',
        '<div id="availabilityHeader" class="tableContainerHeader">Physicist Availability</div>',
        '<div id="availabilityToggleContainer" class="toggleContainer">',
        '<div id="availabilityControlsContainer" class="tableContainerControls"></div>',
        '<div id="availabilityResizeContainer" class="tableContainerBody winResizeContainer" resizeDiff="availabilityControlsContainer">'
      );
    }
    
    outputStr.push('<table id="availabilityContent"');
    
    if (aa.existsIn(permissions, 'hr-admin')){
      outputStr.push(' class="highlightVacation"');
    }
    
    outputStr.push('>');
    
    for (i = indexStart; i < sva.length; i++){
      svai = sva[i];
      
      
      
      if (
        aa.existsIn([841,844,847,848,849,842,845], svai.location_id) // everything except Office and Holiday
        &&
        svai.is_adjustment != 1 // don't show adjustments
        &&
        (
          staff.preferences.hasOwnProperty('availabilityList')
          &&
          
          (
          
            staff.preferences.availabilityList == 0

            ||
            
            (
              staff.preferences.availabilityList == 2
              &&
              shared.physicistsById[svai.physicist_id].tasktype_id == 3
            )
          
            ||

            (
              staff.preferences.availabilityList == 1
              &&
              shared.physicistsById[svai.physicist_id].tasktype_id == 4
            )

            ||

            (
              staff.preferences.scheduleRecent === 3
              &&
              svai.physicist_id == user.id
            )
            
          )
          
        )
      ){
        outputStr.push('<tr class="');
        
        scheduleDate = dpExactDateTime(svai.schedule_start);
        
        physicist = shared.physicistsById[svai.physicist_id];
        
        monthThis = scheduleDate.toString('M');
        if (!monthPrevious){
          monthPrevious = monthThis;
        }
        
        if (svai.schedule_start_date >= today){
          if (past){
            outputStr.push(' today');
            past = false;
          }
        }
        
        if (monthThis != monthPrevious){
          outputStr.push(' dividerTop');
          monthPrevious = monthThis;
        }
        
        if (stripe && past){
          outputStr.push(' evenRowPast');
        }
        else if (stripe){
          outputStr.push(' evenRow');
        }
        else if (past){
          outputStr.push(' past');
        }
        
        if (svai.location_name == '[ Vacation ]'){
          outputStr.push(' vacation');
          
          if (svai.schedule_start_date >= physicist.time.offset6.curr.end){ // next accrual period
            accrualDef = physicist.time.offset6.next;
            timeDiff = accrualDef.vacationAvailable - physicist.time.offset6.next.vacation;
          }
          else if (svai.schedule_start_date >= physicist.time.offset6.curr.start){ // current accrual period
            accrualDef = physicist.time.offset6.curr;
            timeDiff = accrualDef.vacationAvailable - physicist.time.offset6.curr.vacation;
          }
          else if (svai.schedule_start_date >= physicist.time.offset6.prev.start){ // previous accrual period
            accrualDef = physicist.time.offset6.prev;
            timeDiff = accrualDef.vacationAvailable - physicist.time.offset6.prev.vacation;
          }
          else { // older than previous accrual period
            accrualDef = physicist.time.offset6.prev;
            timeDiff = physicist.time.offset6.prev.vacationAvailable; // - physicist.time.offset.next.vacation;
          }
          
          if (timeDiff > staff.vacationWarningThreshold.low){
            //outputStr.push('ok');
          }
          else if (timeDiff >= staff.vacationWarningThreshold.out){
            outputStr.push('Low');
          }
          else {
            outputStr.push('Out');
          }
        }
        
        thisLocationName = svai.location_name.replace('[ ', '').replace(' ]', '');
        
        outputStr.push(physicist.tasktype_id == 3 ? ' ttTherapy' : ' ttDiagnostic', '">',
          '<td class="center">', scheduleDate.toString('M/d'), (scheduleDate.toString('yyyy') != thisYear ? scheduleDate.toString('/yy') : ''), '</td>',
          '<td>', physicist.name, ' <span class="smallCaps light">', (thisLocationName == 'ZZ Scheduled Time Off' ? 'ZZ' : thisLocationName), '</span>'
        );
        
        if (svai.billable_quantity < 8){
          outputStr.push(' <span class="smallCaps highlight">', svai.timeblock, '</span>');
        }
        
        outputStr.push(
          '</td></tr>'
        );
        stripe = !stripe;
      }
    }
    
    outputStr.push('</table>');
    
    if (create){
      outputStr.push('</div></div></div>');
      $('#jq-form-inject').append(outputStr.join(''));
      
      $('#availabilityResizeContainer').data('resizeCallback', staffAvailabilityCallback);
           
      toggleVisAddButton(
        $('#staffAvailability'), 
        function(){ // make sure list scrolls to current day when shown - needed for Chrome
          $('#staffAvailability div.tableContainerBody').scrollTo($('#staffAvailability tr.today'), {
            offset: {left: 0, top: 1}
          });
        }
      );

      // toggleVisOnClick({
        // click: $('#availabilityHeader'),
        // toggle: $('#availabilityToggleContainer')
        // reflectState: {
          // target: $('#availabilityHeaderButtonMinimize'),
          // classVisible: 'icon-hover-minus',
          // classHidden: 'icon-hover-plus'
        // }
      // });
      
      // buttonAddHoverHandler($('#availabilityHeaderButtonMinimize'), 16);
      
      var formDef = new formObject({
        id: 'availabilityControls',
        injectTarget: 'availabilityControlsContainer',
        addedInjectTarget: 'none',
        prefix: 'ac_',
        noFrame: true,
        noGap: true,
        submitType: 'ajax',
        validation: aa.objectClone(validationDefault),
        fields: [
          {
            name: 'displayPhysicists',
            label: 'Display',
            labelInline: true,
            type: 'dropdown',
            required: true,
            values: [
              { id: 0, name: 'Everyone' },
              { id: 1, name: 'Diagnostic Physicists' },
              { id: 2, name: 'Therapy Physicists' }
              // { id: 3, name: 'Just Me' }
            ],
            separator: ' + ',
            selectExclusive: true, //[0, 1, 2], //, 3],
            selectCallback: staffAvailabilityDisplayChange,
            prefillVal: staff.preferences.availabilityList || 0
          }
        ]
      });
      
      // if current user is a physicist, add "Just Me" option
      if (aa.keyExists(shared.physicistsById, user.id)){
        formDef.fields[0].values.push({ id: 3, name: 'Just Me' });
        //formDef.fields[0].selectExclusive.push(3);
      }
          
      forms[formDef.id] = formDef;
      
      formBuild(formDef, formDef.injectTarget);
      
      //Acat.windowResizeChildren({forceResize: true});
      
    }
    else {
      $('#staffAvailability div.tableContainerBody').html(outputStr.join(''));
    }
    
    
    

    
  }
  
  function staffAvailabilityCallback(){
    $('#staffAvailability div.tableContainerBody').scrollTo($('#staffAvailability tr.today'), {
      offset: {left: 0, top: 1},
      onAfter: function(){
        if (aa.existsIn(staff.preferences.hidden, 'staffAvailability')){
          $('#availabilityHeader .buttonHeaderMinimize').trigger('click');
        }
      }
    });
  }
  
  
  
  
  
  
  
  function staffAvailabilityDisplayChange(){
    var checked = $('#availabilityControls [name=displayPhysicists]:checked').val();
      
    if (!checked){
      $('#availabilityControls [name=displayPhysicists][value=0]').attr('checked', true);
      $('#ac_displayPhysicists').next('.multiSelectOptions').multiSelectUpdateSelected(schedule.multiselect.options);
      
      staffAvailabilityDisplayChange();
      return;
    }
    
    checked = Number(checked);
    
    staff.preferences.availabilityList = checked;

    staffAvailability();
    
    staffSavePreferences();
    
  }
  



  
  
  function staffScheduleRecent(){
    var
      i,
      timeMatrix,
      hours,
      timeString,
      showTimeblock,
      outputStr = [],
      svr = shared.scheduleRecent.items,
      svri,
      physicist,
      partTime,
      startDate,
      activeMonths,
      fiveYears,
      //timeAvailable,
      timeDiff,
      accrualDef,
      stripe = false,
      vacation,
      todayDate = new Date(),
      today = todayDate.toString('yyyy-MM-dd'),
      thisYear = todayDate.toString('yyyy'),
      auditThis,
      auditPrevious,
      auditDate,
      scheduleDate,
      scheduleYear,
      past = true,
      create = false,
      hidden = false;
    
    if (!$('#staffScheduleRecent').length){
      create = true;
      if (aa.existsIn(staff.preferences.hidden, 'staffScheduleRecent')){
        hidden = true;
      }
      
      outputStr.push('<div id="staffScheduleRecent" class="tableContainer');
      
      if (staff.config.scheduleRecent.width){
        outputStr.push(' tableContainer', staff.config.scheduleRecent.width);
      }
      
      if (staff.config.scheduleRecent.align){
        outputStr.push(' tableContainer', as.toSentenceCase(staff.config.scheduleRecent.align));
      }
      
      outputStr.push(
        '">',
        '<div id="scheduleRecentHeader" class="tableContainerHeader">Recent Schedule Changes</div>',
        '<div id="scheduleRecentToggleContainer" class="toggleContainer"',
        // hidden ? ' style="display: none"' : '',
        '>',
        '<div id="scheduleRecentControlsContainer" class="tableContainerControls"></div>',
        '<div id="scheduleRecentResizeContainer" class="tableContainerBody winResizeContainer" resizeDiff="scheduleRecentControlsContainer">'
      );

      // outputStr.push('"><div class="tableContainerHeader">Recent Schedule Changes</div>');
      // outputStr.push('<div class="tableContainerBody winResizeContainer">');
    }
    
    outputStr.push('<table id="scheduleRecentContent"');
    
    if (aa.existsIn(permissions, 'schedule-admin')){
      outputStr.push(' class="highlightVacation"');
    }
    
    outputStr.push('>');
    
    for (i = 0; i < svr.length; i++){
      svri = svr[i];
      physicist = shared.physicistsById[svri.physicist_id];
      if (
        (
          staff.preferences.hasOwnProperty('scheduleRecent')
          &&
          
          (
          
            staff.preferences.scheduleRecent === 0

            ||
            
            staff.preferences.scheduleRecent === true

            ||
            
            (
              staff.preferences.scheduleRecent == 2
              &&
              physicist.tasktype_id == 3
            )
          
            ||

            (
              staff.preferences.scheduleRecent == 1
              &&
              physicist.tasktype_id == 4
            )

            ||

            (
              staff.preferences.scheduleRecent === 3
              &&
              svri.physicist_id == user.id
            )
            
          )
          
        )
      ){
    
    
    
        showTimeblock = true;
        vacation = false;
        timeString = [];
        hours = am.round(svri.billable_quantity, 1);

        
        // timeMatrix = hoursToMatrix(svri.billable_quantity);
        
        // if (timeMatrix[1] == 2){
          // timeString.push('full day');
          // showTimeblock = false;
        // }
        // else if (timeMatrix[1] == 1){
          // timeString.push('half day');
        // }
        
        // if (timeMatrix[1] && timeMatrix[2]){
          // timeString.push(' + ');
        // }
        
        // if (timeMatrix[2]){
          // timeString.push(Number(timeMatrix[2].toFixed(2)), ' hour', timeMatrix[2] == 1 ? '' : 's');
        // }
        
        if (hours >= 8){
          timeString.push('full day');
          showTimeblock = false;
          hours -= 8;
        }
        else if (hours >= 4){
          timeString.push('half day');
          hours -= 4;
        }
        
        if (hours){
          if (timeString.length){
            timeString.push(' + ');
          }
          timeString.push(Number(hours.toFixed(2)), ' hour', hours == 1 ? '' : 's');
        }
        
        
        auditDate = dpExactDateTime(svri.audit_start);
        auditThis = auditDate.toString('yyyy-MM-dd');
        if (!auditPrevious){
          auditPrevious = auditThis;
        }
        scheduleDate = dpExactDateTime(svri.schedule_start);
        
        if (aa.existsIn(permissions, 'hr-admin')){
          
          if (svri.location_name == '[ Vacation ]'){
            vacation = true;
            
            if (svri.schedule_start >= physicist.time.offset6.next.start){ // next accrual period
              accrualDef = physicist.time.offset6.next;
              timeDiff = accrualDef.vacationAvailable - accrualDef.vacation;
            }
            else if (svri.schedule_start_date >= physicist.time.offset6.curr.start){ // current accrual period
              accrualDef = physicist.time.offset6.curr;
              timeDiff = accrualDef.vacationAvailable - accrualDef.vacation;
            }
            else if (svri.schedule_start_date >= physicist.time.offset6.prev.start){ // previous accrual period
              accrualDef = physicist.time.offset6.prev;
              timeDiff = accrualDef.vacationAvailable - accrualDef.vacation;
            }
            else { // older than previous accrual period
              accrualDef = physicist.time.offset6.prev;
              timeDiff = accrualDef.vacationAvailable; // - physicist.time.offset.next.vacation;
            }
            
            
            if (timeDiff > staff.vacationWarningThreshold.low){
              vacation = 'ok';
            }
            else if (timeDiff >= staff.vacationWarningThreshold.out){
              vacation = 'low';
            }
            else {
              vacation = 'out';
            }
            
          }
          else {
            vacation = false;
          }
          
        }
      
        outputStr.push(
          '<tr class="', (stripe ? ' evenRow' : '')
        );
        
        switch (vacation){
          case 'ok':
            outputStr.push(' vacation');
            break;
            
          case 'low':
            outputStr.push(' vacationLow');
            break;
            
          case 'out':
            outputStr.push(' vacationOut');
            break;
            
          default:
        }
        
        outputStr.push(
          // (vacation ? ' vacation' : ''),
          (auditThis != auditPrevious ? ' dividerTop' : ''), '">',
          '<td class="right"><p class="title light">', auditDate.toString('M/d'), '&nbsp;&nbsp;', auditDate.toString('h:mm'), '<span class="smallCaps">', auditDate.toString('tt'), '</span></p><p><span class="smallCaps light">', (svri.location_id == 992 && svri.audit_user == 122 ? schedule.autoUserName : svri.audit_user_name), '</span></p></td>',
          '<td>',
          '<p class="title">', svri.location_name
        );
        
        if (vacation){
          outputStr.push(' <span class="color"><b>', accrualDef.vacation, '</b> <span class="small">/ ', accrualDef.vacationAvailable, '</span></span>');
        }
        
        if (svri.num_locations > 1 || svri.unique_identifier){
          outputStr.push(' <span class="smallCaps light">', (svri.unique_identifier || svri.city), '</span>');
        }
        outputStr.push('</p>');
        
        if ($.trim(svri.summary)){
          outputStr.push('<p><span class="smallCaps light">Summary:</span> ', svri.summary, '</p>');
        }
        
        outputStr.push(
          '<p><span class="smallCaps light">Physicist:</span> ', shared.physicistsById[svri.physicist_id].name, '</p>',
          '<p><span class="smallCaps light">Date:</span> ', scheduleDate.toString('MMMM d'),
          (scheduleDate.toString('yyyy') != thisYear ? scheduleDate.toString(', yyyy') : '')
        );
        
        auditPrevious = auditThis;
        
        outputStr.push(' <span class="smallCaps highlight', (am.round(svri.billable_quantity, 1) < 3 ? 'Warning' : ''), '">', timeString.join(''), '</span>');
        
        if (showTimeblock){
          outputStr.push(' (', Number(ad.sqlGetHour(svri.schedule_start)) < 12 ? 'morning' : 'afternoon', ')');
        }
        
        if (svri.is_billable == 0){
          outputStr.push(' <span class="smallCaps highlightWarning">Not billable</span>');
        }
        
        outputStr.push('</p>');
        
        if ($.trim(svri.notes)){
          outputStr.push('<p><span class="smallCaps light">Notes:</span> ', svri.notes, '</p>');
        }
        
        // if (vacation){
          // outputStr.push('<p class="smallCaps">', physicist.name_first, ' has used <b>', physicist.time.offset.curr.vacation, '</b> of ', timeAvailable.vacation, ' vacation days</p>');
        // }
        
        outputStr.push('</td></tr>');
        
        stripe = !stripe;
        
      }
      
    }
    
    outputStr.push('</table>');
    
    if (create){
      outputStr.push('</div></div>');
      $('#jq-form-inject').append(outputStr.join(''));
      
      $('#scheduleRecentResizeContainer').data('resizeCallback', staffScheduleRecentCallback);
      
      toggleVisAddButton($('#staffScheduleRecent'));
      
      var formDef = new formObject({
        id: 'scheduleRecentControls',
        injectTarget: 'scheduleRecentControlsContainer',
        addedInjectTarget: 'none',
        prefix: 'src_',
        noFrame: true,
        noGap: true,
        submitType: 'ajax',
        validation: aa.objectClone(validationDefault),
        fields: [
          {
            name: 'displayPhysicists',
            label: 'Display',
            labelInline: true,
            type: 'dropdown',
            required: true,
            values: [
              { id: 0, name: 'Everyone' },
              { id: 1, name: 'Diagnostic Physicists' },
              { id: 2, name: 'Therapy Physicists' }
              // { id: 3, name: 'Just Me' }
            ],
            separator: ' + ',
            selectExclusive: true, //[0,1,2], //, 3],
            selectCallback: staffScheduleRecentDisplayChange,
            prefillVal: !staff.preferences.scheduleRecent || staff.preferences.scheduleRecent === true ? 0 : staff.preferences.scheduleRecent
          }
        ]
      });

      // if current user is a physicist, add "Just Me" option
      if (aa.keyExists(shared.physicistsById, user.id)){
        formDef.fields[0].values.push({ id: 3, name: 'Just Me' });
        //formDef.fields[0].selectExclusive.push(3);
      }
          
      forms[formDef.id] = formDef;
      
      formBuild(formDef, formDef.injectTarget);
      
      
      // Acat.windowResizeChildren({forceResize: true, callback: function(){
          // if (hidden){
            // $('#scheduleRecentHeader .buttonHeaderMinimize').trigger('click');
          // }
        // }
      // });
      

    
    }
    else {
      $('#staffScheduleRecent div.tableContainerBody').html(outputStr.join(''));
    }
    
    
    
    // $('#staffScheduleRecent tr.ttTherapy').show();
    
  }
  
  
  function staffScheduleRecentCallback(){
    if (aa.existsIn(staff.preferences.hidden, 'staffScheduleRecent')){
      $('#scheduleRecentHeader .buttonHeaderMinimize').trigger('click');
    }
  }



  
  function staffScheduleRecentDisplayChange(){
    var checked = $('#scheduleRecentControls [name=displayPhysicists]:checked').val();
      
    if (!checked){
      $('#scheduleRecentControls [name=displayPhysicists][value=0]').attr('checked', true);
      $('#src_displayPhysicists').next('.multiSelectOptions').multiSelectUpdateSelected(schedule.multiselect.options);
      
      staffScheduleRecentDisplayChange();
      return;
    }
    
    checked = Number(checked);
    
    staff.preferences.scheduleRecent = checked;

    staffScheduleRecent();
    
    staffSavePreferences();
    
  }




  
  
  function mapPhysicistsGetGeocodes(){
    var
      today = dateTimeString(),
      svp = shared.physicists,
      svpi,
      i;
  
    if (!shared.ajaxCount){
      shared.ajaxCount = {};
    }
    shared.ajaxCount.addresses = 0;

    // step through physicists
    for (i = 18; i < svp.length; i++){
      svpi = svp[i];
      // only continue if this is a currently active employee
      if (
        !(
          (svpi.active_start && svpi.active_start > today)
          ||
          (svpi.active_end && svpi.active_end < today)
        )
      )
      {
        address = [svpi.address1];
        
        if (svpi.address2){
          address.push(svpi.address2);
        }
        
        address.push(
          svpi.city,
          svpi.state,
          svpi.zip
        );
        
        address.join(', ');
        
        fblog([svpi.physicist_id, ': ', address].join(''));
        shared.ajaxCount.addresses++;
        
        mapEncodeStruct({
          struct: {
            name: svpi.name,
            address1: svpi.address1,
            address2: svpi.address2,
            citystatezip: [svpi.city, ', ', svpi.state, ' ', svpi.zip].join(''),
            zip: svpi.zip
          }, 
          success: function (result){ // success
            fblog(['FOUND: ', result.Placemark[0].address, ' (', result.Placemark[0].Point.coordinates, ')'].join(''));
            mapPhysicistsAfterAjax();
          },
          saveTo: svpi
        });
        
        // mapEncode(
          // address
          // // ,
          // // function (result){ // success
            // // fblog('success');
          // // },
          // // function (result){ // failure
            // // fblog('failure');
          // // }
        // );
        
        
        
      }
    }
  }
  
  
  
  
  
  
  
  function mapGetDirections(item, target){
    // if (!shared.geo.directions){
      // shared.geo.directions = {target: shared.geo.locations[0]};
    // }
  
    // if (!shared.geo.directions.gdir){
      // shared.geo.directions.gdir = new google.maps.Directions();
    // }
    
    if (!target){
      target = shared.geo.directions.target; //locations[0];
    }
    
    if (item.lat == target.lat && item.lng == target.lng){
      return false;
    }
    
    var address = ['from: ', item.lat, ',', item.lng, ' to: ', target.lat, ',', target.lng].join('');
    
    fblog(['getting directions for "', address, '"' /*, item.address, '" to "', target.address, '"'*/].join(''));
    
    // clear any existing load handlers first
    google.maps.Event.clearListeners(shared.geo.directions.gdir, "load");
    
    // add load handler
    google.maps.Event.addListener(shared.geo.directions.gdir, "load", function(result){
      //fblog(result.getStatus().code);
      
      var item = shared.geo.directions.item;
      
      fblog(['gmaps directions loaded (', result.getStatus().code, '): ', ad.secondsToString(result.getDuration().seconds, {roundTo: 'minutes'}), ' (', Math.round(result.getDistance().meters * 0.000621371192 * 10) / 10, ' miles)'].join(''));
      item.drivingTime = ad.secondsToString(result.getDuration().seconds, {roundTo: 'minutes'});
      item.drivingDistance = Math.round(result.getDistance().meters * 0.000621371192 * 10) / 10;
      if (!item.map){
        item.map = {};
      }
      item.map.overlay = result.getPolyline();
      
      var
        i, j,
        numRoutes = result.getNumRoutes(),
        route,
        numSteps,
        step,
        steps = [],
        hasTolls = false;
        
      if (numRoutes > 0){
        loop:
        for (i = 0; i < numRoutes; i++){
          route = result.getRoute(i);
          numSteps = route.getNumSteps();
          if (numSteps > 0){
            for (j = 0; j < numSteps; j++){
              step = route.getStep(j);
              steps.push('<p>', j, ': ', step.getDescriptionHtml(), '</p>'); 
              if (/toll road<\/div/.test(step.getDescriptionHtml())){
                item.hasTolls = true;
                break loop;
              }
            }
          }
        }
      }
      
      //item.description = steps.join('');
      
      
      mapInfoWindowUpdate(item, {suppressSearchName: true, suppressNearest: true});
      
      var hidden = shared.geo.map.getInfoWindow().isHidden();
      //fblog(['isHidden = ', hidden, ', index = ', innerIndex, ', infoWindowCurrent = ', shared.geo.infoWindowCurrent].join(''));
      if (!hidden && item.id == shared.geo.infoWindowCurrentItem.id){
        if (item.map.overlay){
          shared.geo.map.addOverlay(item.map.overlay);
        }
        shared.geo.map.updateInfoWindow();
      }
      
      
      
    });
    
    shared.geo.directions.item = item;
    shared.geo.directions.gdir.load(
      address,
      {
        locale: 'en_US',
        getSteps: true,
        getPolyline: true
      }
    );
    
  }
  
  
  
  
  
  
  function mapPhysicistsGetDirections(){
    shared.geo.directions.items = shared.physicists;
    shared.geo.directions.index = 0;
    if (!shared.geo.directions.gdir){
      shared.geo.directions.gdir = new google.maps.Directions();
    }
    
    shared.geo.directions.filter = function(){
      if (shared.geo.directions.items[shared.geo.directions.index].lat && shared.geo.directions.items[shared.geo.directions.index].lng){
        return true;
      }
      else {
        return false;
      }
    }
    
    shared.geo.directions.success = function(){
      fblog('all directions found');
    }
    
    shared.geo.directions.callback = function(){
      var address;
      if (shared.geo.directions.index < shared.geo.directions.items.length){
        if (shared.geo.directions.filter()){
          // address = ['from: ', svp[i].address, ' to: ', shared.shared.geo.locations[0].lat, ',', shared.shared.geo.locations[0].lng].join('');
          address =  ['from: ', shared.geo.directions.items[shared.geo.directions.index].lat, ',', shared.geo.directions.items[shared.geo.directions.index].lng, ' to: ', shared.geo.locations[0].lat, ',', shared.geo.locations[0].lng].join(''),
          fblog(['getting driving time for "', address, '"'].join(''));
          shared.geo.directions.gdir.load(
            address,
            {
              locale: 'en_US',
              getSteps: true,
              getPolyline: true
            }
          );
        }
        else {
          fblog(['no address for this item (', shared.geo.directions.index, '), skipping'].join(''));
          shared.geo.directions.index++;
          shared.geo.directions.callback();
        }
      }
      else {
        shared.geo.directions.success();
      }
    }
    
    google.maps.Event.clearListeners(shared.geo.directions.gdir, "load");
    
    google.maps.Event.addListener(shared.geo.directions.gdir, "load", function(result){
      var item = shared.geo.directions.items[shared.geo.directions.index];
      fblog(['gmaps directions loaded (', result.getStatus().code, '): ', ad.secondsToString(result.getDuration().seconds, {roundTo: 'minutes'}), ' (', Math.round(result.getDistance().meters * 0.000621371192 * 10) / 10, ' miles)'].join(''));
      item.drivingTime = ad.secondsToString(result.getDuration().seconds, {roundTo: 'minutes'});
      item.drivingDistance = Math.round(result.getDistance().meters * 0.000621371192 * 10) / 10;
      if (!item.map){
        item.map = {};
      }
      item.map.overlay = result.getPolyline();
      mapInfoWindowUpdate(item, {suppressSearchName: true, suppressNearest: true});
      shared.geo.directions.index++;
      setTimeout(shared.geo.directions.callback, 100);
    });
    
    shared.geo.directions.callback();
    
    // for (i = 0; i < 1 /*svp.length*/; i++){
      // if (svp[i].lat && svp[i].lng){
        // //fblog([svp[i].physicist_id, ': ', svp[i].lat, ',', svp[i].lng].join(''));
        // gdir = new google.maps.Directions();
        // google.maps.Event.addListener(gdir, "load", function(directions){
          // fblog(['gmaps directions loaded (', directions.getStatus().code, '): ', ad.secondsToString(directions.getDuration().seconds, {roundTo: 'minutes'}), ' (', directions.getDistance().html, ')'].join(''));
        // });
        // // address = ['from: ', svp[i].address, ' to: ', shared.geo.locations[0].lat, ',', shared.geo.locations[0].lng].join('');
        // address =  ['from: ', svp[i].lat, ',', svp[i].lng, ' to: ', shared.geo.locations[0].lat, ',', shared.geo.locations[0].lng].join(''),
        // fblog(['getting driving time for "', address, '"'].join(''));
        // gdir.load(
          // address,
          // {
            // locale: 'en_US'
          // }
        // );
      // }
    // }
    
  }
  
  
  
  
  
  
  
  function mapPhysicists(){
    if (!window.google || !window.google.maps){
      initMapsAPI(mapPhysicists);
      return;
    }
    
    var svp = shared.physicists;
    
    if (shared.geo.locations && shared.geo.locations.length){
      shared.geo.directions.target = shared.geo.locations[0];
    }
    
    shared.geo.currentLocations = svp.concat(shared.geo.locations[0]);
    
    // mapShowLocations(svp.concat(shared.geo.locations[0]), {zoom: 9, circleLastPoint: 45, suppressNearest: true, suppressSearchName: true, getDirections: true});
    mapShowLocations({zoom: 9, circleLastPoint: 45, suppressNearest: true, suppressSearchName: true, getDirections: true});
    //mapPhysicistsGetDirections();
  }
  
  
  
  
  
  // function staffNotices(){
    // var
      // i,
      // outputStr = [],
      // unique = 'notices',
      // title = 'Notices',
      // hidden = false,
      // today = dateString(new Date()),
      // isPhysicist = aa.keyExists(shared.physicistsById, user.id);
      
    // if (physicist){
      // staff.preferences.noticesPhysicist = physicist;
      // staffSavePreferences();
    // }
    // else if (!staff.preferences.timePhysicist){
      // if (aa.keyExists(shared.physicistsById, user.id)){
        // staff.preferences.timePhysicist = user.id;
      // }
      // else {
        // staff.preferences.timePhysicist = 103;
      // }
      
      // staffSavePreferences();
        
    // }
    
    // //if (aa.existsIn(permissions, 'debug')){
    
      // if (aa.existsIn(staff.preferences.hidden, 'staffTime')){
        // hidden = true;
      // }
      
      // if (!$('#staffTime').length){
        
        // outputStr.push(
          // '<div id="staffTime" class="tableContainer">',
            // '<a id="time"></a>',
            // '<div id="timeHeader" class="tableContainerHeader">', title, '</div>',
            // '<div id="timeToggleContainer" class="toggleContainer">',
              // '<div id="timeControlsContainer" class="tableContainerControls"></div>',
              // '<div id="timeContent" class="tableContainerBody" resizeDiff="timeControlsContainer">',
              // '<div id="loading-small" style="margin: 1em"><img src="/common/assets/images/large-loading.gif" align="absmiddle" />&nbsp;Loading...</div></div>',
            // '</div>',
          // '</div>'
        // );
        // $('#jq-form-inject').append(outputStr.join(''));

        // toggleVisAddButton($('#staffTime'), staffTimeUpdate);

        // var fields = [];
        
        // if (!isPhysicist) {
          // fields.push(
            // {
              // name: 'displayPhysicists',
              // label: 'Physicist',
              // labelInline: true,
              // type: 'dropdown',
              // required: true,
              // values: getActivePhysicists(shared.physicists, {start: today, end: '2037-12-31 00:00:00'}),
              // // [
                // // { id: 0, name: 'Everyone' },
                // // { id: 1, name: 'Diagnostic Physicists' },
                // // { id: 2, name: 'Therapy Physicists' }
                // // // { id: 3, name: 'Just Me' }
              // // ],
              // separator: ' + ',
              // selectExclusive: true, //, 3],
              // selectCallback: staffTimeDisplayChange,
              // displayCondition: function(){
                // return !(aa.keyExists(shared.physicistsById, user.id));
              // },
              // prefillVal: staff.preferences.timePhysicist
            // }
          // );
        // }
        
        // fields.push(
          // {
            // name: 'displayPage',
            // label: 'Page',
            // labelInline: true,
            // type: 'dropdown',
            // required: true,
            // values: [
              // { id: 0, name: 'Accrued time' },
              // { id: 1, name: 'Other statistics' }
            // ],
            // selectExclusive: true,
            // selectCallback: staffTimePageChange,
            // prefillVal: staff.preferences.timePage || 0
          // }
        // );
        
        // // if current user is not a physicist, show physicist selector
        // var formDef = new formObject({
          // id: 'timeControls',
          // injectTarget: 'timeControlsContainer',
          // addedInjectTarget: 'none',
          // prefix: 'tc_',
          // noFrame: true,
          // noGap: true,
          // submitType: 'ajax',
          // validation: aa.objectClone(validationDefault),
          // fields: fields
        // });
        
        // forms[formDef.id] = formDef;
        
        // formBuild(formDef, formDef.injectTarget);
        
        // Acat.windowResizeChildren({forceResize: true});
        
        // if (hidden){
          // $('#timeHeader').triggerHandler('click');
        // }
        
      // }
      
      
      
      // // if (!window.google || !window.google.maps){
        // // initMapsAPI(mapPhysicists);
        // // return;
      // // }
      // // else {
      // if (!hidden){
        // staffTimeUpdate();
      // }
    
    // //}
    
  // }






  // display staff time tracker widget
  // set scrollTo to true to scroll window to this widget after creation
  function staffTime(physicist, scrollTo){
    var
      i,
      outputStr = [],
      unique = 'time',
      title = 'Personal Time Tracker',
      hidden = false,
      today = dateString(new Date()),
      isPhysicist = aa.keyExists(shared.physicistsById, user.id),
      canViewMultiple = aa.existsIn(permissions, 'hr-admin');
      
    if (!(
      canViewMultiple
      ||
      isPhysicist
    )){
      return;
    }

    if (physicist){
      staff.preferences.timePhysicist = physicist;
      staffSavePreferences();
    }
  
    else if (!staff.preferences.timePhysicist){
      if (aa.keyExists(shared.physicistsById, user.id)){
        staff.preferences.timePhysicist = user.id;
      }
      else {
        staff.preferences.timePhysicist = 103;
      }
      
      staffSavePreferences();
    }
    
    
    //if (aa.existsIn(permissions, 'debug')){
    
      if (aa.existsIn(staff.preferences.hidden, 'staffTime')){
        hidden = true;
      }
      
      if (!$('#staffTime').length){
        
        outputStr.push(
          '<div id="staffTime" class="tableContainer">',
            '<a id="time"></a>',
            '<div id="timeHeader" class="tableContainerHeader">', title, 
            //'<div class="icon-hover16 icon-hover-help">',
            '</div>',
            '<div id="timeToggleContainer" class="toggleContainer">',
              '<div id="timeControlsContainer" class="tableContainerControls"></div>',
              '<div id="timeContent" class="tableContainerBody" resizeDiff="timeControlsContainer">',
              '<div id="loading-small" style="margin: 1em"><img src="/common/assets/images/large-loading.gif" align="absmiddle" />&nbsp;Loading...</div></div>',
            '</div>',
          '</div>'
        );
        $('#jq-form-inject').append(outputStr.join(''));

        toggleVisAddButtonProc({
          addButtonTo: $('#timeHeader'),
          toggle: $('#staffTimeHelp'),
          toggleSelector: '#staffTimeHelp',
          classVisible: 'icon-hover-help'
        });
        toggleVisAddButton($('#staffTime'), staffTimeUpdate);

        var fields = [];
        
        // if current user is not a physicist, show physicist selector
        if (canViewMultiple) { // !isPhysicist) {
          fields.push(
            {
              name: 'displayPhysicists',
              label: 'Physicist',
              labelInline: true,
              type: 'dropdown',
              required: true,
              values: getActivePhysicists(shared.physicists, {start: today, end: '2037-12-31 00:00:00'}),
              // [
                // { id: 0, name: 'Everyone' },
                // { id: 1, name: 'Diagnostic Physicists' },
                // { id: 2, name: 'Therapy Physicists' }
                // // { id: 3, name: 'Just Me' }
              // ],
              separator: ' + ',
              selectExclusive: true, //, 3],
              selectCallback: staffTimeDisplayChange,
              // displayCondition: function(){
                // return !(aa.keyExists(shared.physicistsById, user.id));
              // },
              prefillVal: staff.preferences.timePhysicist
            }
          );
        }
        
        fields.push(
          {
            name: 'displayPage',
            label: 'Page',
            labelInline: true,
            type: 'dropdown',
            required: true,
            values: [
              { id: 0, name: 'Accrued time' },
              { id: 1, name: 'Other statistics' }
            ],
            selectExclusive: true,
            selectCallback: staffTimePageChange,
            prefillVal: staff.preferences.timePage || 0
          }
        );
        
        // fields.push(
          // {
            // name: 'help',
            // label: 'Help',
            // text: 'Help',
            // type: 'button',
            // style: 'small floatRight',
            // css: 'icon-help iconleftgapless'
          // }
        // );
        
        var formDef = new formObject({
          id: 'timeControls',
          injectTarget: 'timeControlsContainer',
          addedInjectTarget: 'none',
          prefix: 'tc_',
          noFrame: true,
          noGap: true,
          submitType: 'ajax',
          validation: aa.objectClone(validationDefault),
          fields: fields
        });
        
        forms[formDef.id] = formDef;
        
        formBuild(formDef, formDef.injectTarget);
        
        // Acat.windowResizeChildren({forceResize: true});
        
        if (hidden){
          $('#timeHeader .buttonHeaderMinimize').triggerHandler('click');
        }
        
      }
      
      
      if (scrollTo){
        setTimeout(function(){
          $(window).scrollTo($('#staffTime'), {offset: {top: -50}});
        }, 1);
      }
      // if (!window.google || !window.google.maps){
        // initMapsAPI(mapPhysicists);
        // return;
      // }
      // else {
      if (!hidden){
        staffTimeUpdate();
      }
      
    
    //}
    
  }
  
  
  
  
  function staffTimeDisplayChange(){

    var checked = $('#timeControls [name=displayPhysicists]:checked').val();
      
    if (!checked){
      checked = 103;
      $('#timeControls [name=displayPhysicists][value=103]').attr('checked', true);
      $('#tc_displayPhysicists').next('.multiSelectOptions').multiSelectUpdateSelected(schedule.multiselect.options);
    }
    
    checked = Number(checked);
    
    staff.preferences.timePhysicist = checked;

    $('#timeContent').block({
      message: ''
    });
    setTimeout(staffTimeUpdate, 1);
    
    staffSavePreferences();
    
  }

  
  function staffTimePageChange(){
    
    var checked = $('#timeControls [name=displayPage]:checked').val();
      
    if (!checked){
      checked = 0;
      $('#timeControls [name=displayPage][value=0]').attr('checked', true);
      $('#tc_displayPage').next('.multiSelectOptions').multiSelectUpdateSelected(schedule.multiselect.options);
    }
    
    checked = Number(checked);
    
    var 
      wip = $('#jq-wip'),
      timeTableInsert = $('#timeTableInsert'),
      timeTableOffset = $('#timeTableOffset'),
      timeTableRolling = $('#timeTableRolling');

    switch (checked){
    
      case 1: // show rolling
        wip.append(timeTableOffset);
        timeTableInsert.append(timeTableRolling);
        break;
      
      default: // show offset/annual
        wip.append(timeTableRolling);
        timeTableInsert.append(timeTableOffset);
      
    }
    
    staff.preferences.timePage = checked;

    // $('#timeContent').block({
      // message: ''
    // });
    setTimeout(staffTimeUpdate, 1);
      
    
    staffSavePreferences();
    
  }
  
  
  
  
  
  function staffTimeUpdate(target, physicist, accruedOnly){
    
    var
      i, j,
      today = new Date(),
      startDate,
      partTime,
      activeMonths,
      accrualMonths,
      // sixMonths,
      // twelveMonths,
      fullYear,
      fiveYears,
      accrualDateA, accrualDateB,
      timeCurrent,
      timePrevious,
      time12Current,
      time12Previous,
      timeOffsetCurr,
      timeOffsetPrev,
      timeOffset12Curr,
      timeOffset12Prev,
      timeAnnual,
      timeAnnualCurr,
      timeOffice,
      timeRolling,
      //timeAvailable,
      svp = shared.physicists, svpi,
      stripe = false,
      isPhysicist = aa.keyExists(shared.physicistsById, user.id),
      canViewMultiple = !isPhysicist || aa.existsIn(permissions, 'schedule-admin');
      
    
    var contentArray = [], offsetArray = [], rollingArray = [];
        
    var detail;

    if (physicist){
      svpi = shared.physicistsById[physicist];
    }
    else {
      svpi = shared.physicistsById[staff.preferences.timePhysicist];
    }
    
    
    contentArray.push('<table id="timeTablePageContainer" cellspacing="0"><tbody>');    
    
    
    if (!svpi.active_end || svpi.active_end > dateTimeString(today)){
      detail = staffTimeDetailUpdate(svpi.id);
      // fbdir('detail.counts', false, detail.counts);
      
      accrualDateA = dpExactDateTime(svpi.time.offset6.dateA);
      accrualDateB = dpExactDateTime(svpi.time.offset6.dateB);
      
      timeOffsetCurr = svpi.time.offset6.curr;
      timeOffsetPrev = svpi.time.offset6.prev;
      timeOffset12Curr = svpi.time.offset12.curr;
      timeOffset12Prev = svpi.time.offset12.prev;
      timeAnnualCurr = svpi.time.annual.curr;
      timeRolling = svpi.time.rolling;
      timeOffice = svpi.time.office;
      
      
      // beginning of physicist html
      contentArray.push(
          '<tr>',
          '<td class="timeTablePage">'
      );
      
      // if (canViewMultiple) { // !isPhysicist){
        // contentArray.push('<p class="large150 bold" style="margin-bottom: .5em">', svpi.name, '</p>');
      // }
      
      contentArray.push(
        '<div style="font-size: 1.125em; line-height: 2em; padding: 0 .75em; background-color: #f6f6f6; border-bottom: 1px solid #eee;">',
        '<div style="float: right; padding: 0"><a class="linkStaffTimeHelp"><span class="iconLink icon-help iconleftgapless" style="padding: 0"><span class="smallCaps">Help</span></span></a></div>',
        '<span style="margin-right: 2em;"><span class="smallCaps light">Accrual Dates:</span> ', accrualDateA.toString('M/d'), ' <span class="light small">&amp;</span> ', accrualDateB.toString('M/d'), '</span>',
        '<span style="margin-right: 2em;"><span class="smallCaps light">Hire Date:</span> ', dpExactDateTime(svpi.active_start).toString('M/d/yy'), '</span>'
      );

      // html for report link
      if (aa.existsIn(permissions, 'debug')){
        // contentArray.push(
          // '<a href="report/physicistTime/', svpi.id, '/Physicist_Time_-_', svpi.name_first, '_', svpi.name_last, '_-_', today.toString('M-d-yy'), '.pdf" target="_blank" class="smallCaps"><span class="iconLink icon-print iconleftgapless" style="padding: .5em 0">Print report</span></a>'
        // );
      }

      
      
      
      contentArray.push('</div>');
      
      contentArray.push(
        // '<div id="staffTimeHelp" class="legend dontprint">',
        // '<div class="title icon-help iconleftgapless">Help</div>',
        // '<p class="">Mouse over individual dates to view details</p>',
        // '<p style="padding-left: 1em" class="timeTableDateNoteMarker">Dates with a yellow marker have notes / summary information (mouse over to view)</p>',
        // '<p class="lighter">Light text indicates dates in the future</p>',
        // // '<p class="colorKey dateCurr">White: current year / accrual period</p>',
        // '<p class="colorKey dateCurr">Yellow: current year / accrual period</p>',
        // '<p class="colorKey datePrev">Grey: previous year / accrual period</p>',
        
        // '<p class="colorKey dateOlder">Grey: Older than previous accrual period</p>',
        // '<div class="legend dontprint">',
        // '<div class="smallCaps title">Legend</div>',
        // '<p class="">Current year / accrual period</p>',
        // '<p style="padding-left: 1em" class="timeTableDateNoteMarker">Mouse over to view notes/summary</p>',
        // '<p class="lighter">Future / tentative</p>',
        // '<p class="" style="border-style: solid; border-width: 1px 0; border-color: #dd9; background-color: #ffd">Next year / accrual period</p>',
        // '<p class="" style="border-style: solid; border-width: 1px 0; border-color: #9d9; background-color: #efe">Previous year / accrual period</p>',
        // '<p class="" style="border-style: solid; border-width: 1px 0; border-color: #ddd; background-color: #eee">Older than previous accrual period</p>',
        
        // '</div>',
        //'</td>'
      );


      

      
      // create beginning of group table HTML
      // contentArray.push('<td class="noPadding accountColumn">');
      
      
      contentArray.push('<table cellspacing="0">');
      
      contentArray.push('<tr><td id="timeTableInsert" class="timeTableContainer">');
      
      // end billing group row
      contentArray.push(
        '</td></tr>'
      );
      
      contentArray.push('</table></td></tr>'); // end account row
      


      
      // start offset and annual table
      offsetArray.push('<table cellspacing="20" id="timeTableOffset" class="timeTable">');
      
      offsetArray.push(
        '<tr><td colSpan="3" style="padding: 0; font-size: 1.275em">This is a summary of all <b>accrued</b> time currently tracked by the schedule.  Each column contains ',
        'a total at the top for the current accrual period (currently used days / total accrued days), followed by a breakdown of applicable dates within each accrual period.  See ',
        'the <a class="linkStaffTimeHelp">Help</a> for more details.</td></tr>'
      );
      
      offsetArray.push(
        '<tr>'
      );
      
      // start offset section
      offsetArray.push(
        '<td class="timeTableColumn" style="width: 23%">',
        '<table cellspacing="0">',
          '<thead>',
            '<tr>',
              '<th class="center timeTableColumnHeader">Per 6 mos employed<br><span style="color: #aaa">', dpExactDateTime(svpi.time.offset6.curr.start).toString('M/d/yy'), ' - ', dpExactDateTime(svpi.time.offset6.curr.end).addDays(-1).toString('M/d/yy'), '</span></th>',
            '</tr>',
            '<tr>',
              '<th class="center timeTableColumnSubHeader">Vacation</th>',
            '</tr>',
          '</thead>',
          '<tbody>'
      );
      
      offsetArray.push(
        '<tr>',
        
        // '<td class="center"><b class="large150', (timeOffsetCurr.vacation > timeOffsetCurr.vacationAvailable ? ' highlightWarning' : ''), '">', (timeOffsetCurr.vacation), '</b> <span class="lighter">/</span> <span class="light">', timeOffsetCurr.vacationAvailable, '</span></td>',
        '<td class="center"><b class="large150', (detail.counts.offset6.vacation.curr > timeOffsetCurr.vacationAvailable ? ' highlightWarning' : ''), '">', detail.counts.offset6.vacation.curr, '</b> <span class="lighter">/</span> <span class="light">', timeOffsetCurr.vacationAvailable, '</span></td>',
        
        '</tr>'
      );
      
      offsetArray.push(detail.content.offset6);
      offsetArray.push('</tbody></table></td>');
      // end offset section
      
      // start offset section
      offsetArray.push(
        '<td class="timeTableColumn" style="width: 43%">',
        '<table cellspacing="0">',
          '<thead>',
            '<tr>',
              '<th colspan="2" class="center timeTableColumnHeader">Per 12 mos employed<br><span style="color: #aaa">', dpExactDateTime(svpi.time.offset12.curr.start).toString('M/d/yy'), ' - ', dpExactDateTime(svpi.time.offset12.curr.end).addDays(-1).toString('M/d/yy'), '</span></th>',
            '</tr>',
            '<tr>',
              '<th class="center timeTableColumnSubHeader">Sick</th>',
              '<th class="center timeTableColumnSubHeader">Seminar</th>',
            '</tr>',
          '</thead>',
          '<tbody>'
      );
      
      offsetArray.push(
        '<tr>',
        
        // '<td class="center"><b class="large150', (timeOffset12Curr.sick > timeOffset12Curr.sickAvailable ? ' highlightWarning' : ''), '">', timeOffset12Curr.sick, '</b> <span class="lighter">/</span> <span class="light">', timeOffset12Curr.sickAvailable, '</span></td>',
        // '<td class="center"><b class="large150', (timeOffset12Curr.seminar > timeOffset12Curr.seminarAvailable ? ' highlightWarning' : ''), '">', timeOffset12Curr.seminar, '</b> <span class="lighter">/</span> <span class="light">', timeOffset12Curr.seminarAvailable, '</span></td>',
        '<td class="center"><b class="large150', (detail.counts.offset12.sick.curr > timeOffset12Curr.sickAvailable ? ' highlightWarning' : ''), '">', detail.counts.offset12.sick.curr, '</b> <span class="lighter">/</span> <span class="light">', timeOffset12Curr.sickAvailable, '</span></td>',
        '<td class="center"><b class="large150', (detail.counts.offset12.seminar.curr > timeOffset12Curr.seminarAvailable ? ' highlightWarning' : ''), '">', detail.counts.offset12.seminar.curr, '</b> <span class="lighter">/</span> <span class="light">', timeOffset12Curr.seminarAvailable, '</span></td>',
        
        '</tr>'
      );
      
      offsetArray.push(detail.content.offset12);
      offsetArray.push('</tbody></table></td>');
      // end offset section
            
      // start annual section
      offsetArray.push(
        '<td class="timeTableColumn">',
        '<table cellspacing="0">',
          '<thead>',
            '<tr>',
              '<th colspan="5" class="center timeTableColumnHeader">Per calendar year<br><span style="color: #aaa">', today.toString('yyyy'), '</span></th>',
            '</tr>',
            '<tr>',
              // '<th class="center">Sick</th>',
              // '<th class="center">Seminar</th>',
              '<th class="center timeTableColumnSubHeader">Jury duty</th>',
              '<th class="center timeTableColumnSubHeader">Funeral</th>',
              '<th class="center timeTableColumnSubHeader">Holidays</th>',
            '</tr>',
          '</thead>',
          '<tbody>'
      );
      
      offsetArray.push(
        '<tr>',
        
        // '<td class="center"><b class="large150', (timeCurrent.sicktimeself + timeCurrent.sicktimefamily + timePrevious.sicktimeself + timePrevious.sicktimefamily > timeAvailable.sick ? ' highlightWarning' : ''), '">', (timeCurrent.sicktimeself + timeCurrent.sicktimefamily + timePrevious.sicktimeself + timePrevious.sicktimefamily), '</b> <span class="lighter">/</span> <span class="light">', timeAvailable.sick, '</span></td>',
        // '<td class="center"><b class="large150', (timeCurrent.sicktimeself + timeCurrent.sicktimefamily > timeAvailable.sick ? ' highlightWarning' : ''), '">', (timeCurrent.sicktimeself + timeCurrent.sicktimefamily), '</b> <span class="lighter">/</span> <span class="light">', timeAvailable.sick, '</span></td>',
        // '<td class="center"><b class="large150', (timeAnnualCurr.sick > timeAnnualCurr.sickAvailable ? ' highlightWarning' : ''), '">', timeAnnualCurr.sick, '</b> <span class="lighter">/</span> <span class="light">', timeAnnualCurr.sickAvailable, '</span></td>',
        // '<td class="center"><b class="large150', (timeAnnualCurr.seminar > timeAnnualCurr.seminarAvailable ? ' highlightWarning' : ''), '">', timeAnnualCurr.seminar, '</b> <span class="lighter">/</span> <span class="light">', timeAnnualCurr.seminarAvailable, '</span></td>',
        // '<td class="center"><b class="large150', (timeAnnualCurr.juryduty > timeAnnualCurr.jurydutyAvailable ? ' highlightWarning' : ''), '">', timeAnnualCurr.juryduty, '</b> <span class="lighter">/</span> <span class="light">', timeAnnualCurr.jurydutyAvailable, '</span></td>',
        // '<td class="center"><b class="large150', (timeAnnualCurr.funeral > timeAnnualCurr.funeralAvailable ? ' highlightWarning' : ''), '">', timeAnnualCurr.funeral, '</b> <span class="lighter">/</span> <span class="light">', timeAnnualCurr.funeralAvailable, '</span></td>',
        // '<td class="center"><b class="large150', (timeAnnualCurr.holiday > timeAnnualCurr.holidayAvailable ? ' highlightWarning' : ''), '">', timeAnnualCurr.holiday, '</b> <span class="lighter">/</span> <span class="light">', timeAnnualCurr.holidayAvailable, '</span></td>',
        '<td class="center"><b class="large150', (detail.counts.annual.juryduty.curr > timeAnnualCurr.jurydutyAvailable ? ' highlightWarning' : ''), '">', detail.counts.annual.juryduty.curr, '</b> <span class="lighter">/</span> <span class="light">', timeAnnualCurr.jurydutyAvailable, '</span></td>',
        '<td class="center"><b class="large150', (detail.counts.annual.funeral.curr > timeAnnualCurr.funeralAvailable ? ' highlightWarning' : ''), '">', detail.counts.annual.funeral.curr, '</b> <span class="lighter">/</span> <span class="light">', timeAnnualCurr.funeralAvailable, '</span></td>',
        '<td class="center"><b class="large150', (detail.counts.annual.holiday.curr > timeAnnualCurr.holidayAvailable ? ' highlightWarning' : ''), '">', detail.counts.annual.holiday.curr, '</b> <span class="lighter">/</span> <span class="light">', timeAnnualCurr.holidayAvailable, '</span></td>',
        
        '</tr>'
      );
      
      offsetArray.push(detail.content.annual);
      offsetArray.push('</tbody></table></td>');
      // end annual section
      
      // end offset and annual table
      offsetArray.push(
        '</tr></table>'
      );
      
      

      
     
      // start rolling table
      rollingArray.push('<table cellspacing="20" id="timeTableRolling" class="timeTable">');
      
      rollingArray.push(
        '<tr><td colSpan="3" style="padding: 0; font-size: 1.275em">This is a summary of any days classified for <b>special purposes other than accrued time</b>.  ',
        'Each column contains a total at the top for all days used under this category (<b>not</b> days available to be used) within this past 12 full calendar months, followed by a breakdown of applicable dates.  See ',
        'the <a class="linkStaffTimeHelp">Help</a> for more details.</td></tr>'
      );
      
      rollingArray.push(
        '<tr>'
      );
      
      // start rolling section
      rollingArray.push(
        '<td class="timeTableColumn">',
        '<table cellspacing="0">',
          '<thead>',
            '<tr>',
              '<th colspan="7" class="center timeTableColumnHeader">Past 12 full months</th>',
            '</tr>',
            '<tr>',
              '<th class="center timeTableColumnSubHeader">Office</th>',
              '<th class="center timeTableColumnSubHeader">Meeting</th>',
              '<th class="center timeTableColumnSubHeader">ZZ</th>',
              '<th class="center timeTableColumnSubHeader">Comp</th>',
              '<th class="center timeTableColumnSubHeader">Weather</th>',
              '<th class="center timeTableColumnSubHeader">Travel</th>',
              '<th class="center timeTableColumnSubHeader">No Show</th>',
            '</tr>',
          '</thead>',
          '<tbody>'
      );
      
      rollingArray.push(
        '<tr>',
        
        '<td class="center"><b class="large150">', timeOffice.total, '</b></td>',
        // '<td class="center"><b class="large150">', timeRolling.meeting, '</b></td>',
        // '<td class="center"><b class="large150">', timeRolling.zzscheduledtimeoff, '</b></td>',
        // '<td class="center"><b class="large150">', timeRolling.comptime, '</b></td>',
        // '<td class="center"><b class="large150">', timeRolling.snow, '</b></td>',
        // '<td class="center"><b class="large150">', timeRolling.travel, '</b></td>',
        // '<td class="center"><b class="large150">', timeRolling.noshow, '</b></td>',
        '<td class="center"><b class="large150">', detail.counts.rolling.meeting.curr, '</b></td>',
        '<td class="center"><b class="large150">', detail.counts.rolling.zzscheduledtimeoff.curr, '</b></td>',
        '<td class="center"><b class="large150">', detail.counts.rolling.comptime.curr, '</b></td>',
        '<td class="center"><b class="large150">', detail.counts.rolling.snow.curr, '</b></td>',
        '<td class="center"><b class="large150">', detail.counts.rolling.travel.curr, '</b></td>',
        '<td class="center"><b class="large150">', detail.counts.rolling.noshow.curr, '</b></td>',
        
        '</tr>'
      );
      
      rollingArray.push(detail.content.rolling);
      rollingArray.push('</tbody></table></td>');
      // end rolling section
      
      // end rolling table
      rollingArray.push(
        '</tr></table>'
      );       


      
      
    }
    
    // timeHTML.push('</table>');
    
    contentArray.push('</tbody></table>'); // end table
    
    contentArray.push(
      '<div id="staffTimeHelp" class="legend dontprint hidden">',
      '<div class="title icon-help iconleftgapless">Help</div>',
      '<p class="">Mouse over individual dates to view details</p>',
      '<p style="padding-left: 1em" class="timeTableDateNoteMarker">Dates with a yellow marker have notes / summary information (mouse over to view)</p>',
      '<p class="lighter">Light text indicates dates in the future</p>',
      '<p class="colorKey dateCurr">Yellow: current year / accrual period</p>',
      '<p class="colorKey datePrev">Grey: previous year / accrual period</p>',
      '</div>'
    );
      
    // fblog(contentArray.join(''), true);

    
    
    // create table
    var wip = $('#jq-wip');
    if (!wip.length){
      $('#hidden_container').append('<div id="jq-wip" class="hidden" />');
      wip = $('#jq-wip');
    }
    
    $('#timeTablePageContainer').remove();
    $('#timeTableOffset').remove();
    $('#timeTableRolling').remove();
    
    
    wip.append(contentArray.join(''));
    wip.append(offsetArray.join(''));
    wip.append(rollingArray.join(''));
    
    $('#timeControlsContainer').append($('#staffTimeHelp'));
    
    // wip.append(contentArray.concat(offsetArray, rollingArray));
    // fblog(contentArray.join('') + offsetArray.join('') + rollingArray.join(''), true);
    // fblog(wip.html(), true);
    
  
    var
      timeTable = $('#timeTablePageContainer'),
      timeTableParent = timeTable.parent(),
      timeTableInsert = $('#timeTableInsert'),
      timeTableOffset = $('#timeTableOffset'),
      timeTableRolling = $('#timeTableRolling');

    var timeContent;

    if (target && target.length){
      timeContent = target;
    }
    else {
      timeContent = $('#timeContent');
    }
    
    var existing = $('.tooltipTarget', timeContent);
    
    // fblog('timeTable' + timeTable.length);
    // fblog('timeTableInsert' + timeTableInsert.length);
    // fblog('timeTableOffset' + timeTableOffset.length);
    // fblog('timeTableRolling' + timeTableRolling.length);
    
    // remove any existing event handlers
    existing.unbind('mouseenter').unbind('mouseleave').unbind('click');
    $(document.body).unbind('mousemove');
    
    // delete existing content
    timeContent.empty();
    
    // // only show accrued subtable
    // if (accruedOnly){
      // timeContent.append(timeTableOffset);
    // }
    // // show complete time tracker table with controls
    // else {

      // show table
      timeContent
        .addClass('flatContainer')
        .css('padding-bottom', 0)
        .append(timeTable);
      
      var pageToShow;
      
      switch (staff.preferences.timePage){
        case 1: // rolling
          pageToShow = timeTableRolling;
          break;
        
        default: // offset
          pageToShow = timeTableOffset;
      }
      
      timeTableInsert.empty().append(pageToShow);
      
    // }
      
    $('a.linkStaffTimeHelp').click(function(){
      toggleElement($('#staffTimeHelp'));
    });
    
    $('#staffTimeHelp').click(function(){
      $(this).addClass('hidden');
    });
    
    // add new event handlers
    $('#timeTableOffset .tooltipTarget, #timeTableRolling .tooltipTarget')
      // .unbind('mouseover').unbind('mouseout').unbind('click')
      .tooltip({
        //track: true,
        //fade: true,
        delay: 0,
        bodyHandler: function() {
          var content = 'n/a';
          var el = $(this);
          var scheduleStart, auditStart, allotedTime, halfday = false;
          var physicist = shared.physicistsById[svpi.physicist_id]; //$(this).parents('tr.account').attr('row');
          var item;
          var type = el.attr('type');
          var group = el.attr('group');
          
          if (type == 'office'){
            item = [physicist.time.office.items[el.attr('taskIndex')]];
          }
          else {
            item = aa.findObjectByKey(shared.availability.items, el.attr('task'));
          }
          
          if (item.length){
            item = item[0];
          
            var todayString = dateString(new Date());
            var thisStartDateString;
            var future = false, prevAccrualPeriod = false, nextAccrualPeriod = false;
            
            // var accrualDateA = dpExactDateTime(physicist.time.offset.dateA);
            // var accrualDateB = dpExactDateTime(physicist.time.offset.dateB);
            
            // var timeCurrent = physicist.time.offset.curr;
            // var timePrevious = physicist.time.offset.prev;
            var prevAccrualEnd;
            var nextAccrualStart;

            if (item.location_id == 841){
              prevAccrualEnd = dateString(dpExactDateTime(physicist.time.offset6.prev.start).addMonths(6));
              nextAccrualStart = dateString(dpExactDateTime(physicist.time.offset6.prev.start).addYears(1));
            }
            else if (group == 'rolling'){
              prevAccrualEnd = dateString((new Date()).addYears(-1).set({day: 1}));
              nextAccrualStart = dateString((new Date()).set({day: 1}));
            }
            else {
              prevAccrualEnd = dateString((new Date()).set({month: 0, day: 1}));
              nextAccrualStart = dateString((new Date()).set({month: 0, day: 1}).addYears(1));
            }
            
            scheduleStart = dpExactDate(item.schedule_start_date);
            auditStart = dpExactDateTime(item.audit_start);
            
            
            if (item.schedule_start_date > todayString){
              future = true;
            }
            
            if (item.schedule_start_date >= nextAccrualStart){
              nextAccrualPeriod = true;
            }
            else if (item.schedule_start_date < prevAccrualEnd){
              if (type != 'rolling'){
                prevAccrualPeriod = true;
              }
            }
            

            if (item.billable_quantity == 4){
              allotedTime = 'Half Day';
              halfday = true;
            }
            else if (item.billable_quantity == 8){
              allotedTime = 'Full Day';
            }
            else {
              allotedTime = [item.billable_quantity, ' hours'].join('');
              if (item.billable_quantity < 6){
                halfday = true;
              }
            }
            
            
            // content = [
              // '<ul>',
                // '<li><span class="header">', scheduleStart.toString('M/d/yy'),
                // (halfday ? (item.timeblock == 'afternoon' ? '<span class="smallCaps"> pm</span>' : '<span class="smallCaps"> am</span>') : ''),
                // ' - ', (type == 'office' ? 'Office' : item.location_name.replace('[ ', '').replace(' ]', '')),
                // (item.is_adjustment == 1 ? ' Adjustment' : ''),
                // '</span></li>',
                // '<li>', future ? '<span class="title highlightWarning">Future</span> ' : '', '<span class="title highlight', 
                // (item.is_adjustment == 1 ? 'Red">deduct ' : '">'),
                // allotedTime, '</span></li>',
                // prevAccrualPeriod ? '<li><span class="title highlightWarning">Previous Accrual Period</span> <span class="title">(Doesn\'t contribute to current total)</span></li>' : '',
                // nextAccrualPeriod ? '<li><span class="title highlightWarning">Next Accrual Period</span> <span class="title">(Doesn\'t contribute to current total)</span></li>' : '',
                // item.summary ? ['<li><span class="title">Summary:</span> ', item.summary, '</li>'].join('') : '',
                // item.notes ? ['<li><span class="title">Notes:</span> ', item.notes, '</li>'].join('') : '',
                // item.audit_start ? ['<li class="light"><span class="title">Last edited </span> ', auditStart.toString('MMMM dS @ h:mmt'), ' <span class="title">by</span> ', ((item.location_id == 992 && item.audit_user == 122) ? schedule.autoUserName : (item.audit_user_name || schedule.autoUserName)), '</li>'].join('') : '',
              // '</ul>'
            // ].join('');
            
            content = [
              '<p class="header">', scheduleStart.toString('M/d/yy'),
              (halfday ? (item.timeblock == 'afternoon' ? '<span class="smallCaps"> pm</span>' : '<span class="smallCaps"> am</span>') : ''),
              ' - ', (type == 'office' ? 'Office' : item.location_name.replace('[ ', '').replace(' ]', '')),
              (item.is_adjustment == 1 ? ' Adjustment' : ''),
              '</p>',
              '<p>', future ? '<span class="title highlightWarning">Future</span> ' : '', '<span class="title highlight', 
              (item.is_adjustment == 1 ? 'Red">deduct ' : '">'),
              allotedTime, '</span></p>',
              prevAccrualPeriod ? '<p><span class="title highlightWarning">Previous Accrual Period</span> <span class="title">(Doesn\'t contribute to current total)</span></p>' : '',
              nextAccrualPeriod ? '<p><span class="title highlightWarning">Next Accrual Period</span> <span class="title">(Doesn\'t contribute to current total)</span></p>' : '',
              item.summary ? ['<p><span class="title">Summary:</span> ', item.summary, '</p>'].join('') : '',
              item.notes ? ['<p><span class="title">Notes:</span> ', item.notes, '</p>'].join('') : '',
              item.audit_start ? ['<p class="light"><span class="title">Last edited </span> ', auditStart.toString('MMMM dS @ h:mmt'), ' <span class="title">by</span> ', ((item.location_id == 992 && item.audit_user == 122) ? schedule.autoUserName : (item.audit_user_name || schedule.autoUserName)), '</p>'].join('') : ''
            ].join('');
            
            //fblog(content, true);
          }
          
          return content;
        }
      })
      
      .hover(
        function (){
          // $(this).css({'background-color': '#ffb', 'border': '1px solid #dd9', 'margin': 0}); //, 'border-color': '#dd9'});
          $(this).css({'background-color': '#dfe8f6'}); //, 'border-color': '#dd9'});
        },
        function (){
          // $(this).css({'background-color': 'transparent', 'border': 'none', 'margin': '1px'}); //, 'border-color': 'transparent'});
          $(this).css({'background-color': 'transparent'}); //, 'border-color': 'transparent'});
        }
      );
      
      
    //timeContent.html('<div style="height: 100px">test</div>');

  }
  
  
  

  



  function staffTimeDetailUpdate(physicistId){
    // if (!container || !container.length){
      // // container = $(['#timeTable tr.expand-child[physicist=', physicistId, ']'].join(''));
      // container = $(['#timeTable', physicistId].join(''));
    // }

    var h, i, j;
    var dates, thisDate, tentative, noteMarker, thisYear, thisStartDate, thisStartDateString, nextStartDate, nextStartDateString;
    
    // var todayString = dateTimeString(new Date().set({hour: 23, minute: 59, second: 59}));
    var todayString = dateString(new Date());
    var thisYearString = new Date().toString('yyyy');
    var thisYearStart = [thisYearString, '-01-01'].join('');
    var prevYearStart = [thisYearString - 1, '-01-01'].join('');
    
    var itemsAll = aa.findObjectByKey(
      shared.availability.items, 
      physicistId,
      'physicist_id'
    );
    
    var physicist = shared.physicistsById[physicistId];
    
    var annualStart = dateTimeString(new Date().set({month: 0, day: 1}).clearTime());
    var rollingStart = dateTimeString(new Date().addYears(-1).set({day: 1}).clearTime());
    
    var items = {
      rolling: [
        {
          name: 'office',
          items: physicist.time.office.items
        },
        {
          name: 'meeting',
          items: aa.findObjectByKeys( 
            itemsAll, 
            {
              location_id: 845,
              schedule_start: {value: rollingStart, rel: 'gte'}
            }
          )
        },
        {
          name: 'zzscheduledtimeoff',
          items: aa.findObjectByKeys( 
            itemsAll, 
            {
              location_id: 842,
              schedule_start: {value: rollingStart, rel: 'gte'}
            }
          )
        },
        {
          name: 'comptime',
          items: aa.findObjectByKeys( 
            itemsAll, 
            {
              location_id: 935,
              schedule_start: {value: rollingStart, rel: 'gte'}
            }
          )
        },
        {
          name: 'snow',
          items: aa.findObjectByKeys( 
            itemsAll, 
            {
              location_id: 993,
              schedule_start: {value: rollingStart, rel: 'gte'}
            }
          )
        },
        {
          name: 'travel',
          items: aa.findObjectByKeys( 
            itemsAll, 
            {
              location_id: 1000,
              schedule_start: {value: rollingStart, rel: 'gte'}
            }
          )
        },
        {
          name: 'noshow',
          items: aa.findObjectByKeys( 
            itemsAll, 
            {
              location_id: 992,
              schedule_start: {value: rollingStart, rel: 'gte'}
            }
          )
        }
        
      ],
      
      offset6: [
        {
          name: 'vacation',
          items: aa.findObjectByKeys(
            itemsAll,
            {
              location_id: 841,
              schedule_start: {value: '2008-07-01', rel: 'gt'}
            }
          )
        }
      ],

      offset12: [
        {
          name: 'sick',
          items: aa.findObjectByKeys( 
            itemsAll, 
            {
              location_id: {value: [847,848], rel: 'in'},
              schedule_start: {value: prevYearStart, rel: 'gt'}
            }
          )
        },
        {
          name: 'seminar',
          items: aa.findObjectByKeys( 
            itemsAll, 
            {
              location_id: 844,
              schedule_start: {value: prevYearStart, rel: 'gte'}
            }
          )
        }
      ],
      
      annual: [
        {
          name: 'juryduty',
          items: aa.findObjectByKeys( 
            itemsAll, 
            {
              location_id: 849,
              schedule_start: {value: prevYearStart, rel: 'gte'}
            }
          )
        },
        {
          name: 'funeral',
          items: aa.findObjectByKeys( 
            itemsAll, 
            {
              location_id: 999,
              schedule_start: {value: prevYearStart, rel: 'gte'}
            }
          )
        },
        {
          name: 'holiday',
          items: aa.findObjectByKeys( 
            itemsAll, 
            {
              location_id: 846,
              schedule_start: {value: prevYearStart, rel: 'gte'}
            }
          )
        }
      ]
    }
    
    var
      accrualStartDate,
      prevAccrualStart,
      currAccrualStart,
      nextAccrualStart,
      prevAccrualStarted,
      prevAccrualFinished,
      currAccrualStarted,
      currAccrualFinished,
      nextAccrualStarted,
      nextAccrualFinished,
      offset,
      currYear,
      countCurrYear,
      countOlder,
      countOlderPeriod,
      oldAccrualStart,
      addTotal,
      addInlineTotal,
      addInlineTotalPrev,
      addInlineTotalCurr,
      addInlineTotalNext,
      addInlineTotalPrevAbove,
      addInlineTotalCurrAbove,
      addTotalYear;
      
    var
      // countOlder, countOlderPeriod, countPrev, countCurr, countNext, countCurrYear, currYear,
      oldAccrualStart,
      timePrevious = physicist.time.offset6.prev,
      timeCurrent = physicist.time.offset6.curr,
      timeNext = physicist.time.offset6.next,
      time12Previous = physicist.time.offset12.prev,
      time12Current = physicist.time.offset12.curr,
      time12Next = physicist.time.offset12.next,
      timeAnnualPrevious = physicist.time.annual.prev,
      timeAnnualCurrent = physicist.time.annual.curr;

    var content = {
      // offset6: [],
      // offset12: [],
      // annual: [],
      // rolling: []
    };
    
    var counts = {
    };
    
    var billableDays;
    
    var thisItem, thisTaskId, nextItem, thisCount;
    
    for (h in items){
      if (!content[h]){
        content[h] = [];
      }
      
      if (!counts[h]){
        counts[h] = {};
      }
      
      content[h].push('<tr>');
      for (i = 0; i < items[h].length; i++){
        content[h].push('<td class="center">');
        if (items[h][i]){
          prevAccrualStarted = false;
          prevAccrualFinished = false;
          currAccrualStarted = false;
          currAccrualFinished = false;
          nextAccrualStarted = false;
          nextAccrualFinished = false;
          offset = false;
          
          counts[h][items[h][i].name] = thisCount = items[h][i].count = {
            next: 0,
            curr: 0,
            prev: 0
            // older: 0,
            // olderPeriod: 0,
            // currYear: 0
          }
          
          // countNext = 0;
          // countCurr = 0;
          // countPrev = 0;
          countOlder = 0;
          countOlderPeriod = 0;
          countCurrYear = 0;
          oldAccrualStart = [];

          
          // if (items[h][i].name == 'vacation'){
          if (h == 'offset6'){
            accrualStartDate = dpExactDateTime(physicist.time.offset6.curr.start);
            currAccrualStart = dateString(accrualStartDate);
            nextAccrualStart = dateString(accrualStartDate.addMonths(6));
            prevAccrualStart = dateString(accrualStartDate.addYears(-1));
            while (dateString(accrualStartDate) > shared.systemStart){
              oldAccrualStart.push(dateString(accrualStartDate.addMonths(-6)));
            }
          }
          else if (h == 'offset12'){
            accrualStartDate = dpExactDateTime(physicist.time.offset12.curr.start);
            currAccrualStart = dateString(accrualStartDate);
            nextAccrualStart = dateString(accrualStartDate.addYears(1));
            prevAccrualStart = dateString(accrualStartDate.addYears(-2));
            while (dateString(accrualStartDate) > shared.systemStart){
              oldAccrualStart.push(dateString(accrualStartDate.addYears(-1)));
            }
          }
          else if (h == 'annual'){
            accrualStartDate = (new Date()).set({month: 0, day: 1});
            currAccrualStart = dateString(accrualStartDate);
            nextAccrualStart = dateString(accrualStartDate.addYears(1));
            prevAccrualStart = dateString(accrualStartDate.addYears(-2));
          }
          else { // rolling
            accrualStartDate = (new Date()).addYears(-1).set({day: 1});
            currAccrualStart = dateString(accrualStartDate);
            nextAccrualStart = dateString(accrualStartDate.addYears(1));
            prevAccrualStart = dateString(accrualStartDate.addYears(-2));
          }
          
          //content.push('<p><b>', items[i].name, '</b>: ');
          dates = [];
          
          
          
          
          
          
          
          
          
          
          
          // step through items in reverse order (newest first)
          for (j = items[h][i].items.length - 1; j >= 0; j--){
            thisItem = items[h][i].items[j];
            nextItem = (j == 0 ? null : items[h][i].items[j-1]);
            addInlineTotal = false;
            addInlineTotalPrev = false;
            addInlineTotalCurr = false;
            addInlineTotalNext = false;
            addInlineTotalPrevAbove = false;
            addInlineTotalCurrAbove = false;
            addTotalYear = false;
            
            thisTaskId = thisItem.id || 0;
            thisStartDateString = thisItem.schedule_start_date;
            thisStartDate = dpExactDate(thisStartDateString);
            if (nextItem){
              nextStartDateString = nextItem.schedule_start_date;
              nextStartDate = dpExactDate(nextStartDateString);
            }
            thisDate = ['<div class="'];

            if (h == 'offset6' || h == 'offset12'){
              thisDate.push('dateOffset ');
            }
            currYear = thisStartDate.toString('yyyy');

            
            
            
            if ( // start next accrual period
              !nextAccrualStarted
              &&
              thisStartDateString >= nextAccrualStart
            ){
              nextAccrualStarted = true;
            }
            
            else if ( // start current accrual period
              !currAccrualStarted
              &&
              thisStartDateString >= currAccrualStart
              &&
              thisStartDateString < nextAccrualStart
            ){

              nextAccrualStarted = true;
              nextAccrualFinished = true;
              currAccrualStarted = true;

            }
            
            else if ( // start previous accrual period
              !prevAccrualStarted
              &&
              thisStartDateString >= prevAccrualStart
              &&
              thisStartDateString < currAccrualStart
            ){

              nextAccrualStarted = true;
              nextAccrualFinished = true;
              currAccrualStarted = true;
              currAccrualFinished = true;
              prevAccrualStarted = true;
              
            }
            
            else if ( // start first period older than previous accrual period
              !prevAccrualFinished
              &&
              thisStartDateString < prevAccrualStart
            ){
            
              nextAccrualStarted = true;
              nextAccrualFinished = true;
              currAccrualStarted = true;
              currAccrualFinished = true;
              prevAccrualStarted = true;
              prevAccrualFinished = true;
            }
            
            else { // all other older periods
            }
            
            
            addInlineTotal = true;
            if (nextItem){
              if (prevAccrualFinished){
                if (oldAccrualStart[countOlderPeriod] && nextStartDateString < oldAccrualStart[countOlderPeriod]){
                  thisDate.push('dateOlderEnd ');
                  addTotal = 'older';
                  countOlderPeriod++;
                }
              }
              else if (prevAccrualStarted){
                if (nextStartDateString < prevAccrualStart){
                  thisDate.push('datePrevEnd ');
                  addTotal = 'prev';
                }
              }
              else if (currAccrualStarted){
                if (nextStartDateString < currAccrualStart){
                  thisDate.push('dateCurrEnd ');
                  addTotal = 'curr';
                }
              }
              else if (nextAccrualStarted){
                if (nextStartDateString < nextAccrualStart){
                  thisDate.push('dateNextEnd ');
                  addTotal = 'next';
                }
              }
              
              if (h == 'offset6' || h == 'offset12'){
                if (nextStartDate.toString('yyyy') < currYear){ 
                  addTotalYear = true;
                  //currYear = nextStartDate.toString('yyyy');
                }
              }
            }
            else {
              if (!nextAccrualFinished){
                thisDate.push('dateNextEnd ');
                addTotal = 'next';
              }
              else if (!currAccrualFinished){
                thisDate.push('dateCurrEnd ');
                addTotal = 'curr';
              }
              else if (!prevAccrualFinished){
                  thisDate.push('datePrevEnd ');
                addTotal = 'prev';
              }
              else {
                  thisDate.push('dateOlderEnd ');
                addTotal = 'older';
              }
            }
            
          
            
            
            
            
            // update count of billable days for totals
            billableDays = 1;
            
            if (thisItem.billable_quantity == 4){
              billableDays = .5;
            }
            else if (thisItem.billable_quantity == 8){
            }
            else if (thisItem.billable_quantity > 8){
              billableDays = thisItem.billable_quantity / 8;
            }
            else {
              if (thisItem.billable_quantity < 6){
                billableDays = .5;
              }
            }

            
            // if (thisItem.is_adjustment != 1){

              if (!nextAccrualFinished){
                thisCount.next += billableDays;
              }
              else if (!currAccrualFinished){
                thisCount.curr += billableDays;
              }
              else if (!prevAccrualFinished){
                thisCount.prev += billableDays;
              }
              else {
                countOlder += billableDays;
              }
              
              countCurrYear += billableDays;
              
            // }
            
            
            if (!nextAccrualFinished){
              thisDate.push('dateNext ');
            }
            else if (!currAccrualFinished){
              thisDate.push('dateCurr');
            }
            else if (!prevAccrualFinished){
              thisDate.push('datePrev');
            }
            else {
              thisDate.push('dateOlder ');
            }
            
            thisDate.push('"><span id="timeTableDate', thisTaskId, '" task="', thisTaskId, '" taskIndex="', j, '" group="', h, '" type="', items[h][i].name, '" class="tooltipTarget');
            tentative = false;
            thisYear = false;
            noteMarker = false;
            
            
            if (thisStartDateString > todayString){
              tentative = true;
            }
            
            if (thisStartDateString.substr(0, 4) == thisYearString){
              thisYear = true;
            }
            
            if (thisItem.notes || thisItem.summary){
              noteMarker = true;
              thisDate.push(' timeTableDateNoteMarker');
            }
            
            thisDate.push('"><span class="');
            
            if (thisItem.is_adjustment == 1){
              thisDate.push('highlightRed ');
            }
            else if (tentative){
              thisDate.push('dateTentative ');
            }
            
            thisDate.push('">');
            
            if (thisYear){
              thisDate.push(thisStartDate.toString('M/d'));
            }
            else {
              thisDate.push(thisStartDate.toString('M/d/yy'));
            }
            
            if (thisItem.billable_quantity < 6){
              thisDate.push('<span class="smallCaps">', thisItem.timeblock == 'morning' ? ' am' : ' pm', '</span>'); //'.5');
            }
            
            thisDate.push('</span>');
            
            thisDate.push('</span>');
            
            if (addTotal){
              thisDate.push('<div class="totalInline');
              
              if (!addInlineTotal){
                thisDate.push(' totalInlineAbove');
              }
              
              switch (addTotal){
              
                case 'next':
                  // if (timeNext[items[h][i].name] > timeNext[[items[h][i].name, 'Available'].join('')]){
                  if (thisCount.next > timeNext[[items[h][i].name, 'Available'].join('')]){
                    thisDate.push(' highlightWarning');
                  }
                  // thisDate.push(' totalInlineNext">', timeNext[items[h][i].name]);
                  thisDate.push(' totalInlineNext">', thisCount.next);
                  break;
                  
                case 'curr':
                  // if (timeCurrent[items[h][i].name] > timeCurrent[[items[h][i].name, 'Available'].join('')]){
                  if (thisCount.curr > timeCurrent[[items[h][i].name, 'Available'].join('')]){
                    thisDate.push(' highlightWarning');
                  }
                  thisDate.push(' totalInlineCurr">');
                  // if (physicist.time[h] && physicist.time[h].curr && physicist.time[h].curr[items[h][i].name]){
                    // thisDate.push(physicist.time[h].curr[items[h][i].name]);
                    thisDate.push(thisCount.curr);
                  // }
                  break;
                
                case 'prev':
                  // if (timePrevious[items[h][i].name] > timePrevious[[items[h][i].name, 'Available'].join('')]){
                  if (thisCount.prev > timePrevious[[items[h][i].name, 'Available'].join('')]){
                    thisDate.push(' highlightWarning');
                  }
                  thisDate.push(' totalInlinePrev">');
                  // if (physicist.time[h] && physicist.time[h].prev && physicist.time[h].prev[items[h][i].name]){
                    // thisDate.push(physicist.time[h].prev[items[h][i].name]);
                    thisDate.push(thisCount.prev);
                  // }
                  break;
                  
                case 'older':
                  if (countOlder > timePrevious[[items[h][i].name, 'Available'].join('')]){
                    thisDate.push(' highlightWarning');
                  }
                  thisDate.push(' totalInlineCurr">');
                  thisDate.push(countOlder);
                  countOlder = 0;
                  break;
                  
                default:
                  thisDate.push('">', timePrevious[items[h][i].name]);
                
              }
              
              addTotal = false;
              addInlineTotal = false;
              
              thisDate.push('</div>');
            }
            else {
              // thisDate.push('">');
            }
            
            if (addTotalYear || ((h == 'offset6' || h == 'offset12') && !nextItem)){
              if (items[h][i].name == 'vacation' && !nextItem && (h == 'offset6' || h == 'offset12') && physicist.employee && physicist.employee.accrualbaseline_vacation){
                countCurrYear += Number(physicist.employee.accrualbaseline_vacation);
                thisDate.push(
                  '<div class="totalInlineOffsetYear"><span style="font-size: .75em; line-height: .8em">', currYear, '</span><br>', countCurrYear, '</div>',
                  '</div>',
                  '<div style="background-color: #fed; border: 1px solid #dcb; color: #987; margin-top: 1.25em; padding: 0.5em 0; position:relative; width:94%;">Vacation total for<br>calendar year 2008<br>includes an additional<br><span class="bold large">', Number(physicist.employee.accrualbaseline_vacation), ' day', (physicist.employee.accrualbaseline_vacation == 1 ? '' : 's'), ' </span>between<br>1/1/08 and 6/30/08<br>(not shown)</div>'
                  // '<div style="background-color: #fed; border: 1px solid #dcb; color: #987; padding: 0.5em 1em; position: absolute; bottom: -1px; left: 133%">Vacation total for<br>calendar year 2008<br>includes an additional<br><span class="bold large">', Number(physicist.employee.accrualbaseline_vacation), ' day', (physicist.employee.accrualbaseline_vacation == 1 ? '' : 's'), ' </span>between<br>1/1/08 and 6/30/08<br>(not shown)</div>',
                  // '</div>'
                );
              }
              else {
                thisDate.push(
                  '<div class="totalInlineOffsetYear"><span style="font-size: .75em; line-height: .8em">', currYear, '</span><br>', countCurrYear, '</div>',
                  '</div>'
                );
              }
              countCurrYear = 0;
            }
            else {
              thisDate.push('</div>');
            }
            
            dates.push(thisDate.join(''));
          }

          
          // // copy over counts to physicist definition
          // switch (h){
            // case 'rolling':
              // physicist.time[h][items[h][i].name] = thisCount.curr;
              // break;
                
            // case 'offset6':
            // case 'offset12':
            // case 'annual':
              // $.each(['prev', 'curr', 'next'], function(index, value){
                // if (!thisCount[value]){
                  // thisCount[value] = 0;
                // }
                
                // if (!physicist.time[h][value]){
                  // physicist.time[h][value] = {};
                // }
                
                // physicist.time[h][value][items[h][i].name] = thisCount[value];
              // });
              // break;
            
            
          // }

          content[h].push(dates.join(''));
        }
        content[h].push('</td>');
      }
      content[h].push('</tr>');
      
      content[h] = content[h].join('');
    }
    
    
    // $('table.billingTableItemization', container).append('<tr>', content.join(''), '</tr>');
    
    return {content: content, counts: counts};
      
    
  }




  function staffProximity(){
    var
      i,
      outputStr = [],
      address = '',
      hidden = false;
      
    if (aa.existsIn(staff.preferences.hidden, 'staffProximity')){
      hidden = true;
    }
      
    if (!$('#staffProximity').length){
      outputStr.push(
        '<div id="staffProximity" class="tableContainer">',
          '<div id="proximityHeader" class="tableContainerHeader">Physicist Proximity</div>',
          '<div id="proximityToggleContainer" class="toggleContainer"',
          (hidden ? ' style="display: none"' : ''),
          '>',
            //'<div id="proximityControlsContainer" class="tableContainerControls"></div>',
            // '<div id="mapContainer" class="tableContainerBody" resizeOffset="0" resizeDiff="proximityControlsContainer" style="padding: 1em;"><div id="map" style="height: 40em"><div id="loading-small" style="margin: 1em"><img src="/common/assets/images/large-loading.gif" align="absmiddle" />&nbsp;Loading...</div></div></div>',
            '<div id="mapContainer" class="tableContainerBody" style="padding: 1em;"><div id="map" class="winResizeContainer" resizeMin="300" resizeOffset="150" resizeDiff="proximityControlsContainer" style="height: 40em"><div id="loading-small" style="margin: 1em"><img src="/common/assets/images/large-loading.gif" align="absmiddle" />&nbsp;Loading...</div></div></div>',
          '</div>',
        '</div>'
      );
      $('#jq-form-inject').append(outputStr.join(''));
      
      $('#map').data('resizeCallback', staffProximityCallback);

      toggleVisAddButton($('#staffProximity'), function(){
        // Acat.windowResizeChildren({forceResize: true, callback: mapPhysicists()});
        // mapPhysicists();
        mapRecenter();
      });
      // toggleVisOnClick({
        // click: $('#proximityHeader'),
        // toggle: $('#proximityToggleContainer')
      // });
      
    }
    
    if (hidden){
      return;
    }
      
    if (!$('#proximityControls').length){
    
      var formDef = new formObject({
        id: 'proximityControls',
        injectTarget: 'proximityControlsContainer',
        addedInjectTarget: 'none',
        prefix: 'pc_',
        noFrame: true,
        noGap: true,
        submitType: 'ajax',
        validation: aa.objectClone(validationDefault),
        fields: [
          {
            name: 'displayPhysicists',
            label: 'Display for',
            type: 'dropdown',
            required: true,
            values: [
              { id: 0, name: 'Everyone' },
              { id: 1, name: 'Diagnostic Physicists' },
              { id: 2, name: 'Therapy Physicists' }
              // { id: 3, name: 'Just Me' }
            ],
            separator: ' + ',
            selectExclusive: true, //, 3],
            //selectCallback: staffProximityDisplayChange,
            prefillVal: staff.preferences.proximity || 0
          }
        ]
      });
      
      // // if current user is a physicist, add "Just Me" option
      // if (aa.keyExists(shared.physicistsById, user.id)){
        // formDef.fields[0].values.push({ id: 3, name: 'Just Me' });
        // formDef.fields[0].selectExclusive.push(3);
      // }



      
      // forms[formDef.id] = formDef;
      
      // formBuild(formDef, formDef.injectTarget);
      
    }
      
    // Acat.windowResizeChildren({forceResize: true});
    
    // if (hidden){
      // $('#proximityHeader .buttonHeaderMinimize').triggerHandler('click');
    // }
    
      
    // if (!window.google || !window.google.maps){
      // initMapsAPI(mapPhysicists);
      // return;
    // }
    // else {
    
    if (!hidden){
      mapPhysicists();
    }
    
    
  }
  
  
  
  function staffProximityCallback(){
    if (shared.geo.loaded){
      // mapPhysicists();
      mapRecenter();
    }
  }

  
  
  
  
  
  
  
  

  function staffGetUpdated(config){
    if (!staff.gettingUpdates){
      staff.gettingUpdates = true;
      var getArr = [];
      var paramsArr = {};
      var params;
      var i;
      
      if (!config){
        config = {};
      }

      if (config.subject){
        getArr.push(config.subject);
      }
      else {
        getArr.push('availability');
        getArr.push('scheduleRecent');
        if ($('#staffAvailability').length){
          paramsArr.availability = {timestamp: shared.availability.timestamp};
        }
        if ($('#staffScheduleRecent').length){
          paramsArr.scheduleRecent = {timestamp: shared.scheduleRecent.timestamp};
        }
      }
      
      if (config.physicist){
        for (i in getArr){
          if (!paramsArr[getArr[i]]){
            paramsArr[getArr[i]] = {};
          }
          paramsArr[getArr[i]].physicist = config.physicist;
        }
      }
      
      params = {
        get: JSON.stringify(getArr),
        params: JSON.stringify(paramsArr)
        //preferences: JSON.stringify(staff.preferences)
      };
      
      
      ad.stopwatchStart('ajaxStaffGetUpdated');
      
      $.ajax({
        url: [shared.urlRoot, '/ajaxSSGet/staffGetUpdated'].join(''),
        data: params,
        //type: 'POST',
        success: function (data, textStatus){
          var logstringArr = [['staffGetUpdated(): retrieved from server (', (ad.stopwatchEnd('ajaxStaffGetUpdated') / 1000), 's)'].join('')];
          var decoded, i, set;
        
          try {
            ad.stopwatchStart('ajaxStaffGetUpdated eval');
            decoded = shared.lastJSON.generic = eval(["(", data, ")"].join(''));
            // decoded = shared.lastJSON.generic = JSON.parse(data);
            fblog(['staffGetUpdated(): eval (', (ad.stopwatchEnd('ajaxStaffGetUpdated eval') / 1000), 's)'].join(''));
          }
          catch (err){
            fblog(['staffGetUpdated() decode failure: ', err.name, ', ', err.message].join(''));
            staff.gettingUpdates = false;
            return;
          }
          
          if (decoded.success === false){
            fblog(decoded.message);
            if (decoded.auth === false){
              window.location.reload(); //href = "/login/clients";
            }
            return false;
          }
          
          
          if (decoded.physicists){
            shared.physicists = decoded.physicists;
            shared.physicistsById = {};
            for (i = 0; i < shared.physicists.length; i++){
              shared.physicistsById[Number(shared.physicists[i].physicist_id)] = shared.physicists[i];
            }
          }
          
          if (!decoded.returned || !decoded.returned.length){
            logstringArr.push('no changes');
          }
          else {
            ad.stopwatchStart('ajaxStaffGetUpdated loop');
            for (i = 0; i < decoded.returned.length; i++){
              if (decoded[decoded.returned[i]].items){
                set = decoded[decoded.returned[i]].items;
                shared[decoded.returned[i]] = decoded[decoded.returned[i]];
                if (!config.callback){
                  staffWidgetUpdate(decoded.returned[i]);
                }
                logstringArr.push(['"', decoded.returned[i], '" (', set.length, ' item', (set.length == 1 ? '' : 's'), ')'].join(''));
              }
            }
            fblog(['staffGetUpdated(): loop (', (ad.stopwatchEnd('ajaxStaffGetUpdated loop') / 1000), 's)'].join(''));
            
            if (!config.callback){
              ad.stopwatchStart('ajaxStaffGetUpdated callback');
              staffTime();
              fblog(['staffGetUpdated(): callback (', (ad.stopwatchEnd('ajaxStaffGetUpdated callback') / 1000), 's)'].join(''));
            }
          
          }
          
          if (config.callback){
            config.callback();
          }
          
          fblog(logstringArr.join(', '));
          staff.gettingUpdates = false;
        },
        error: function (XMLHttpRequest, textStatus, errorThrown){
          fblog(['staffGetUpdated(): ajax failure = ', textStatus, ' (', errorThrown, ')'].join(''));
          staff.gettingUpdates = false;
        }
        
      });
    }
  }
  
  
  
  // save staff preferences on demand
  // includes a buffer timeout, so that if multiple calls are made within a short period of time, only the last will be performed
  function staffSavePreferences(){
    
    $('body').stopTime('savePreferences');
    $('body').oneTime([staff.performance.timeoutSavePreferences, 's'].join(''), 'savePreferences', function(){
    
      var params = {
        preferences: JSON.stringify(staff.preferences)
      };
      
      ad.stopwatchStart(['ajaxStaffSavePreferences', params.dates].join(''));
      
      $.ajax({
        url: [shared.urlRoot, '/ajaxSSGet/staffSavePreferences'].join(''),
        data: params,
        //type: 'POST',
        success: function (data, textStatus){
          var logstringArr = [['staffSavePreferences(): saved (', (ad.stopwatchEnd(['ajaxStaffSavePreferences', params.dates].join('')) / 1000), 's)'].join('')];
        
        },
        error: function (XMLHttpRequest, textStatus, errorThrown){
          fblog(['staffSavePreferences(): ajax failure = ', textStatus, ' (', errorThrown, ')'].join(''));
        }
        
      });
      
    });  
    
  
    
  }  
  
  
  
  
  function staffWidgetUpdate(widget){
    switch (widget){
      case 'availability':
        staffAvailability();
        //staffTime();
        break;
        
      case 'scheduleRecent':
        staffScheduleRecent();
        break;
    }
  }
  
  
  
  // start ajax updater interval
  // should get all dirty tasks and save them to server, then retrieve any new tasks seamlessly in the background
  function staffUpdaterStart(){
    staffUpdaterStop();
    $('#hidden_container').everyTime([staff.performance.timeoutGetUpdated, 's'].join(''), 'getUpdatedStaff', function(){
        staffGetUpdated();
      }
    ); 

    if (!staff.initIdle){
    //$(document).unbind("idle.idleTimer active.idleTimer");
    //$.idleTimer('destroy');
    
      $(document).bind("idle.idleTimer", function(){
        fblog('idle: staff');
        staffUpdaterStop();
      });
      $(document).bind("active.idleTimer", function(){
        fblog('active: staff');
        // get a fresh update right away (don't wait for timeoutGetUpdated to elapse first)
        //setTimeout(staffGetUpdated, 2000);
        staffGetUpdated();
        staffUpdaterStart();
      });
      
      $.idleTimer(staff.performance.timeoutIdle * 1000);
      
      staff.initIdle = true;
    
    }

  }
  
  // stop ajax updater interval
  function staffUpdaterStop(){
    $('#hidden_container').stopTime();
  }

  
  





  // when config.click is clicked, toggle visibility of config.toggle
  // if config.reflectState is passed, change class of config.reflectState.target
  function toggleVisOnClick(config){
    // fill in unspecified parameters with defaults
    config = $.extend({
      click: shared.nullElement, // element to be clicked
      toggle: shared.nullElement, // element whose visibility is toggled
      reflectState: {
        target: shared.nullElement, // element which will have its CSS class changed
        classVisible: '', // class to apply when element visible
        classHidden: '' // class to apply when element hidden
      },
      refresh: null, // function to call after element is shown
      preferences: [] // array of elements which are currently hidden
    }, config);
    
    config.click.addClass('cursorPointer');
    
    config.click.click(function(e){
      var toggle = $(config.toggleSelector);
      if (e.button < 2 || e.button == undefined) {
        toggleElement(toggle, config);
        
        if (e.button !== undefined){
          staffSavePreferences();
        }
      }
    });
  }


  
  function toggleElement(toggle, config){
    if (toggle.is(':visible')){
      // config.toggle.css({'position': 'absolute', 'z-index': '100'}); //hide();
      toggle.addClass('hidden');
      if (config){
        if (config.reflectState.classHidden != config.reflectState.classVisible && config.reflectState.target.length) {
          config.reflectState.target
            .addClass(config.reflectState.classHidden)
            .removeClass(config.reflectState.classVisible);
        }
        if (config.preferences){
          aa.pushUnique(config.preferences, config.click.parent().parent().attr('id'));
        }
      }
    }
    else {
      // config.toggle.css({'position': '', 'z-index': ''}); //show();
      toggle.removeClass('hidden').show();
      if (config){
        if (config.refresh && typeof config.refresh == 'function'){
          setTimeout(function() {config.refresh()}, 50);
        }
        if (config.reflectState.classHidden != config.reflectState.classVisible && config.reflectState.target.length) {
          config.reflectState.target
            .addClass(config.reflectState.classVisible)
            .removeClass(config.reflectState.classHidden);
        }
        if (config.preferences){
          aa.removeByValue(config.preferences, config.click.parent().parent().attr('id'));
        }
      }
    }
  
  }
  
  
  // function toggleVisAddButton(container, showRefresh){
  
    // var
      // header = $('.tableContainerHeader', container),
      // toggle = $('.toggleContainer', container),
      // visible = toggle.is(':visible'),
      // classVisible = 'icon-hover-minus',
      // classHidden = 'icon-hover-plus';
      
    // header.append(['<div class="buttonHeaderMinimize icon-hover16 ', visible ? classVisible : classHidden, '"></div>'].join(''));
    
    // var button = $('.buttonHeaderMinimize', container);
    
    // toggleVisOnClick({
      // click: header,
      // toggle: toggle,
      // reflectState: {
        // target: button,
        // classVisible: classVisible,
        // classHidden: classHidden
      // },
      // refresh: showRefresh,
      // preferences: staff.preferences.hidden || (staff.preferences.hidden = [])
    // });
    
    // buttonAddHoverHandler(button, 16);
      
  // }
  
  function toggleVisAddButton(container, showRefresh){
  
    toggleVisAddButtonProc({
      addButtonTo: $('.tableContainerHeader', container),
      toggle: $('.toggleContainer', container),
      toggleSelector: ([container.selector, ' .toggleContainer'].join('')),
      classVisible: 'icon-hover-minus',
      classHidden: 'icon-hover-plus',
      buttonCss: 'buttonHeaderMinimize',
      refresh: showRefresh,
      preferences: staff.preferences.hidden || (staff.preferences.hidden = [])
    });
      
  }

  function toggleVisAddButtonProc(config){
    config = $.extend(config, {
      //addButtonTo: container to add button element to
      //toggle: container whose visibility will be toggled
      //buttonCss: extra class(es) to apply to button
      //classVisible: css to apply to button while element is visible
      //classHidden: css to apply to button while element is hidden
    });
    
    var visible = config.toggle.is(':visible');
    
    var buttonHTML = ['<div class="', config.buttonCss, ' icon-hover16 ', (visible ? config.classVisible : (config.classHidden || config.classVisible)), '"></div>'].join('');
    
    var button = $(buttonHTML).prependTo(config.addButtonTo);
    
    toggleVisOnClick({
      click: button,
      toggle: config.toggle,
      toggleSelector: config.toggleSelector,
      reflectState: {
        target: button,
        classVisible: config.classVisible,
        classHidden: config.classHidden || config.classVisible
      },
      refresh: config.refresh,
      preferences: config.preferences || []
    });
    
    buttonAddHoverHandler(button, 16);
    
  }
  
  
  
  function getElementFamily(el, classChild){
    var done = false;
    var elNext;
    
    if (!classChild){
      classChild = 'expand-child';
    }
    
    while (!done){
      // if (row.hasClass('expand-child')){
      if (el.hasClass(classChild)){
        el = el.prev();
      }
      else {
        done = true;
      }
    }
    
    done = false;
    while (!done){
      elNext = el.eq(el.length - 1).next();
    
      // if (rowNext.hasClass('expand-child')){
      if (elNext.hasClass(classChild)){
        el = el.add(elNext);
      }
      else {
        done = true;
      }
    }
    
    return el;
  }
  
  
  
  function tableRowClickSetup(config){ //table, cssSelected, cssChild){
    if (!config.table || !config.table.length){
      return;
    }
    
    // if (!config.cssClick){
      // config.cssSelected = 'highlightblue';
    // }
    // if (!config.cssDblclick){
      // config.cssSelected = 'highlightblue';
    // }
    if (!config.cssChild){
      config.cssChild = 'expand-child';
    }
    
    var rows = $('tbody > tr', config.table);
    
    rows.addClass('cursorPointer');
    
    rows.click(function(){
      var row = $(this);
      
      row = getElementFamily(row);
        
      // prevent text highlight on double-click
      row.disableTextSelect();
      setTimeout(function(){ row.enableTextSelect(); }, 500);
      
      if (config.cssClick){
        if (row.hasClass(config.cssClick)){
          // row.removeClass(config.cssSelected);
        }
        else {
          $(['tbody > tr.', config.cssClick].join(''), config.table).removeClass(config.cssClick);
          row.addClass(config.cssClick);
        }
      }
      
      if (typeof config.onClick == 'function'){
        setTimeout(function(){
          config.onClick(row);
          return false;
        }, 50);
      }
      else {
        return false;
      }
    });
    
    if (config.cssDoubleClick || config.onDoubleClick){
      
      rows.dblclick(function(){
        var row = $(this);
        
        row = getElementFamily(row);
        
        if (typeof config.onDoubleClick == 'function'){
          // run handler
          if (config.onDoubleClick(row)){
            // only set css if handler returns true
            $(['tbody > tr.', config.cssDoubleClick].join(''), config.table).removeClass(config.cssDoubleClick);
            row.addClass(config.cssDoubleClick);
          }
        }
        else if (config.cssDoubleClick){
          if (row.hasClass(config.cssDoubleClick)){
            // row.removeClass(config.cssSelected);
          }
          else {
            $(['tbody > tr.', config.cssDoubleClick].join(''), config.table).removeClass(config.cssDoubleClick);
            row.addClass(config.cssDoubleClick);
          }
        }
        
        return false; // don't bubble click back to schedule table cell
        
      });
    }
        
    return;
  }
  
  
  // add row hover functionality to table
  // cssHover class should apply to tr element
  // cssChild class denotes a child row which should stay "glued" to its immediately preceding row
  function tableRowHoverSetup(table, cssHover, cssChild){
    if (!table.length){
      return;
    }
    
    if (!cssHover){
      cssHover = 'highlightblue';
    }
    if (!cssChild){
      cssChild = 'expand-child';
    }
    
    $('tbody > tr', table).hover(
      function(){
      
        // alias scope to be called in closure
        // can't use "this" as that would refer to scope of timer function
        var that = this;
        
        // kill any timers in progress
        $('#hidden_container').stopTime('hoverInTimer');
        
        // create brief timer to prevent multiple calls stacking faster than the browser can render them
        $('#hidden_container').oneTime(10, 'hoverInTimer', function(){
          return function(scope, classHover, classChild){
            // var row = $(this);
            var row = $(scope);
            var rowNext;
            
            var done = false;
            while (!done){
              // if (row.hasClass('expand-child')){
              if (row.hasClass(classChild)){
                row = row.prev();
              }
              else {
                done = true;
              }
            }
            
            done = false;
            while (!done){
              rowNext = row.eq(row.length - 1).next();
            
              // if (rowNext.hasClass('expand-child')){
              if (rowNext.hasClass(classChild)){
                row = row.add(rowNext);
              }
              else {
                done = true;
              }
            }
            
            // row.children('td').addClass('highlightblue');
            row.children('td').addClass(classHover);
            
          }(that, cssHover, cssChild);
        });  
      },
      function(){
      
        // kill any hover timers in progress
        $('#hidden_container').stopTime('hoverInTimer');
        
        // return closure to remove hover class from most recent row
        return function(scope, classHover, classChild){
          // var row = $(this);
          var row = $(scope);
          var rowNext;
          
          var done = false;
          while (!done){
            // if (row.hasClass('expand-child')){
            if (row.hasClass(classChild)){
              row = row.prev();
            }
            else {
              done = true;
            }
          }
          
          done = false;
          while (!done){
            rowNext = row.eq(row.length - 1).next();
          
            // if (rowNext.hasClass('expand-child')){
            if (rowNext.hasClass(classChild)){
              row = row.add(rowNext);
            }
            else {
              done = true;
            }
          }
          
          // row.children('td').addClass('highlightblue');
          row.children('td').removeClass(classHover);
        }(this, cssHover, cssChild);
      }
    );
  }





  
  
  
  
  function clientLoadBilling(){
    if (!clients.billingLoaded){
      clients.billingLoaded = true;
      // insert billing/invoice information for this account below location details
      if (aa.existsIn(permissions, 'accounting')){
        $('#injectBilling').remove();
        
        $('#injectDetails').after('<div id="injectBilling" class="tabPage hidden"></div>');
      
        fblog(['clientLoadBilling(): location_id = ', clients.active.id].join(''));
        
        $('#srt_clientDisplayHistory, #srt_clientDisplayInvoices').addClass('icon-loading iconright');
        
        billingGet({target: 'injectBilling', targetHistory: 'injectHistory', accounts: [clients.active.account_id], location: clients.active.id});
      }
      else {
        $('#srt_clientDisplayHistory').addClass('icon-loading iconright');
        
        billingGet({targetHistory: 'injectHistory', accounts: [clients.active.account_id], location: clients.active.id});
      }
    }
  }
  
  
  
  function clientDisplayDetails(){
    $('#searchResultTabs span.button').removeClass('buttonTabActive');
    $('#srt_clientDisplayDetails').addClass('buttonTabActive');
    $('#jq-formstatic-inject div.tabPage').not('#injectDetails').addClass('hidden');
    $('#injectDetails').removeClass('hidden');
    mapRecenter(); // ensure map is drawn correctly if it was hidden when first rendered
  }
  
  
  function clientDisplayHistory(){
    clientLoadBilling();
    $('#searchResultTabs span.button').removeClass('buttonTabActive');
    $('#srt_clientDisplayHistory').addClass('buttonTabActive');
    $('#jq-formstatic-inject div.tabPage').not('#injectHistory').addClass('hidden');
    $('#injectHistory').removeClass('hidden');
  }


  function clientDisplayInvoices(){
    clientLoadBilling();
    $('#searchResultTabs span.button').removeClass('buttonTabActive');
    $('#srt_clientDisplayInvoices').addClass('buttonTabActive');
    $('#jq-formstatic-inject div.tabPage').not('#injectBilling').addClass('hidden');
    $('#injectBilling').removeClass('hidden');
  }
  
  
  
  
  
  
  
  
  
  




  function init(params){
    var i; 
    
    permissions = params.permissions || [0];
    
    params.target = document.location.hash.substring(1);

    user = params.user || {
      id: '',
      name: ''
    };
    
    params.vars = params.vars || {};
    
    deploy = (params.deploy !== false ? true : false);
    
    // insert log output div
    if (!deploy) { 
      if (!$('#viewport_log').length){
        $('#viewport_bottom').after('<div id="viewport_log" class="dontprint"><div class="widthCap"><a name="log"></a><div id="logtitle">Application Log (scroll up to return to main document)</div><div id="logsubtitle">To submit this information, use your mouse to click and drag over the text below to highlight it, then right-click and copy the highlighted text (or press Ctrl-C on your keyboard) to copy the text to your clipboard.  You may then paste the text into an email or other document.</div><div id="logContainer" /></div>');
      }
      
      // add links for viewing and copying log
      $('#footerContent').append('<span class="dontprint">&nbsp; &#0183; &nbsp;<a id="link-showlog">Hide Log</a></span>');
      
      $('#link-showlog').click(function(){
        var logViewportEl = $('#viewport_log');
        //var logEl = $('#log');
        var thisEl = $(this);
        
        if (logViewportEl.css('display') == 'none'){
          logViewportEl.css('display', 'block');
          $(window).scrollTo(logViewportEl);
          thisEl.html('Hide Log');
          
        }
        else {
          logViewportEl.css('display', 'none');
          thisEl.html('Show Log');
        }
      });
    }
    
    
    
    // log user-agent string for debugging
    fblog(navigator.userAgent);
    
    //fblog('target = ' + params.target);
    
    for (i in params.vars){
      shared[i] = params.vars[i];
    }
    // shared.vars = params.vars;
    if (shared.physicists){
      shared.physicistsById = {};
      for (i = 0; i < shared.physicists.length; i++){
        shared.physicistsById[Number(shared.physicists[i].physicist_id)] = shared.physicists[i];
      }
      
      shared.physicistsByTasktype = {};
      shared.physicistsByTasktype[3] = aa.findObjectByKey(shared.physicists, 3, 'tasktype_id');
      shared.physicistsByTasktype[4] = aa.findObjectByKey(shared.physicists, 4, 'tasktype_id');
      
    }
    
    shared.revision = params.revision;
    shared.revisionRaw = params.revisionRaw;
    
    shared.urlRoot = params.urlRoot;

    
    // catch all ajax requests, appending revision number to data
    $(document).bind('ajaxSend', function(event, XMLHttpRequest, ajaxOptions){
      if (ajaxOptions.data){
        ajaxOptions.data = [ajaxOptions.data, '&revision=', shared.revision].join('');
      }
      else {
        ajaxOptions.data = ['revision=', shared.revision].join('');
      }
      // XMLHttpRequest.abort();
    });

    
    $.ajaxSetup({
      type: 'post',
      
      // if data isn't defined in a call, set this to a urlencoded space to enable all data-sending procedures in jQuery.ajax
      // otherwise, extra data added in ajaxSend event will be ignored
      data: '+'
    });
    

    fblog(['init(): id = ', user.id, ', name = ', user.name, ', permissions = ', permissions.join(', ')].join(''));
    
    if (aa.existsIn(permissions, 'employee')){
      pingStart();
    }
    
    
    // if nothing else to do, return
    if (!params.loadInterface){
      return;
    }


    shared.activeInterface = params.loadInterface;
    
    
    
    
    schedule.activeEdit = new activeEditObject();
    clients.scheduleEdit = new activeEditObject();
    mouseDown = new mouseDownObject();
    schedule.selected = new scheduleSelectionObject();
    shared.clientUTCOffset = clientUTCOffset();
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    shared.serverUTCOffset = params.utcOffset;
    
    $.blockUI.defaults.css = {
      padding:        '3em', 
      margin:         '0px', 
      width:          '30%',  
      top:            '0px',  
      left:           '35%',  
      textAlign:      'center',  
      color:          '#666',  
      border:         '0px', 
      backgroundColor:'', 
      //position: 'static',
      overflow:       'hidden'
    };
     
    $.blockUI.defaults.overlayCSS = {
      width:          '100%',
      overflow:       'hidden',
      backgroundColor:'#fff',  
      opacity:        '0.6',
      cursor:         'pointer'
    };
    
    $.blockUI.defaults.fadeIn = 0;
    $.blockUI.defaults.fadeOut = 0;
    
    $.easing.def = 'easeOutQuad';
    
    // $.facebox.settings.opacity = 0.5;

    

    // add passed form data to forms collection
    if (params.formDef){
      var formDef = new formObject(params.formDef);
      
      // add this form to collection
      forms[formDef.id] = formDef;
      // generate form
      formBuild(forms[formDef.id], forms[formDef.id].injectTarget, null, (params.loadInterface == 'clients' && params.query && params.query[0]));

      
      // set up focus/blur handlers
      $.listen(
        'focus',
        'input.inputText, textarea.inputTextarea',
        
        function (e) {
          var thisEl = $(this);
          var thisElParent = thisEl.parents('div.item');
          shared.inputFocused = thisEl.get(0).id;
          //fblog('focused ' + shared.inputFocused);
          thisElParent.removeClass('itemInvalid')
            .contents('input.inputText, textarea.inputTextarea').addClass('focused')
            .filter('input.inputText').select();
          if (thisEl.hasClass('help')){
            $(['div[helpFor=', this.id, ']'].join(''), thisElParent).addClass('calloutFocused');
          }
        }
      );      
      
      $.listen(
        'blur',
        'input.inputText, textarea.inputTextarea',
        
        function (e) {
      //$.intercept('blur', {'input.inputText, textarea.inputTextarea': function(e){
        var thisEl = $(this);
        var thisElId = thisEl.attr('id');
        var thisElParent = thisEl.parents('div.item');
        var formDef = forms[thisEl.parents('form').attr('id')];
        var inputs = $('input.inputText, textarea.inputTextarea, td.inputLabelLeft', thisElParent);
        //fblog('blurred ' + thisElId);
        shared.inputFocused = '';
        // validate contents of this element
        if (!formDef.dontValidate){
          formValidateItem(thisEl);
        }
        
        // flag this field as having been blurred at least once
        aa.pushUnique(formDef.blurred, thisElId);
        
        // always remove focused on blur
        inputs.removeClass('focused');
        if (aa.existsIn(formDef.invalid, thisElId)){
          thisElParent.addClass('itemInvalid');
        }
        if (thisEl.hasClass('help')){
          $(['div[helpFor=', this.id, ']'].join(''), thisElParent).removeClass('calloutFocused');
        }
      });


    }





    // respond to key presses that apply to entire window
    $(document).keydown(function(e) {
    
      switch (shared.activeInterface){
      
        case 'schedule':
        
          if (!schedule.blockInput){
            // fblog('input');
          
            // delay ajax operations by 2 seconds after last keypress
            // prevents browser from noticeably bogging down (from ajax parsing) during rapid keystroke entry
            updaterStop();
            $('body').stopTime('keyDownWait');
            $('body').oneTime(2000, 'keyDownWait', function(){
              updaterStart();
            });
            
            // setTimeout(function(){
              // updaterStart();
            // }, 2000);
      
            if (
              !aa.existsIn([
                controls.shift.keyCode,
                controls.ctrl.keyCode,
                controls.alt.keyCode
              ], e.keyCode)
            ){
              // fblog('keydown(' + e.keyCode + ')');
              
              if (e.keyCode == controls.f8.keyCode){
                fblog('--- Execution halted');
                errorHandler();
              }
            
              else if (
                !schedule.retrieving
                // && !schedule.inputInProgress
              ){

                // if task editor isn't in use
                if (!$('tr.taskEditRow').length){
                
                  var direction = '';
                  
                  switch (e.keyCode){
                    case controls.esc.keyCode:
                      e.preventDefault();
                      datepickerHide();
                      schedule.selected.init();
                      return false; // prevent ESC from killing ajax in progress
                      break;
                      
                    case controls.right.keyCode:
                    case controls.left.keyCode:
                    case controls.up.keyCode:
                    case controls.down.keyCode:
                      direction = aa.keyOfObject(controls, e.keyCode, 'keyCode');
                      if (direction.length){
                        direction = direction[0];
                        e.preventDefault();
                        var origin = schedule.selected.getOrigin({deferToMousedown: true, includeReadonly: true});
                        scheduleSelectCells({
                          origin: origin,
                          onKeydown: true,
                          direction: (origin.notFound ? '' : direction)
                        });
                      }
                      break;
                      
                    case controls.enter.keyCode:
                      e.preventDefault();
                      scheduleEditShow();
                      break;
                      
                  }
                  
                }
                
                
                // task editor IS currently showing
                else {
                  switch (e.keyCode){
                  
                    // save current changes on ctrl-insert
                    case controls.insert.keyCode:
                      if (e.ctrlKey){
                        scheduleEditHide();
                      }
                      break;
                    
                    // discard current changes on esc
                    case controls.esc.keyCode:
                      // MUST PREVENT DEFAULT IF ESC KEY PRESSED
                      // if not, Firefox will immediately cancel current AJAX request
                      e.preventDefault();
                      scheduleEditHide({noSave: true});
                      return false;
                      break;
                      
                  }
                    
                }
                
                
                
                // else if (e.keyCode == controls.esc.keyCode){
                  // // MUST PREVENT DEFAULT IF ESC KEY PRESSED
                  // // if not, Firefox will immediately cancel current AJAX request
                  // e.preventDefault();
                  
                  // scheduleEditHide({noSave: true});
                // }
              }
              else {
                fblog('schedule retrieving');
              }
              
            }
          }
          break;
          
          
        case 'billing':
          switch (e.keyCode){
            case controls.esc.keyCode:
              e.preventDefault();
              billingSelectNone();
              return false;
              break;
          }
          
          break;

          
        case 'clients':
          switch (e.keyCode){
            case controls.esc.keyCode:
              e.preventDefault();
              if ($('#injectHistory').is(':visible')){
                if (clients.scheduleEdit.cells.length){
                  scheduleEditHide({noSave: true});
                }
                else {
                  $('#historyTable tr.highlightblue').removeClass('highlightblue');
                }
              }
              else if ($('#injectBilling').is(':visible')){
                billingSelectNone();
              }
              // else if (clients.editing){
              else if (shared.items.active){
                itemEditHide({noSave: true});
              }
              return false;
              break;
              
            case controls.enter.keyCode:
              if (clients.editing){
                e.preventDefault();
                itemEditHide();
              }
              break;
              
          }
          
          break;
          
        default:
        
      }
    });
    

    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    


    
    
    
    
    
    
    if (shared.noContent === true){
      // $('#page-title, p.page-title').css({'margin-bottom': 0, 'border-bottom-width': '0'});
      // $('#referencePageList .bodyInline').css('margin-top', 0);
    }
    
    
    
    
    
    
    
    
    
    
    var today, target;
    
    switch(shared.activeInterface){


      case "schedule":
      
        // delay loading schedule in IE6
        // seems to help with intermittent 10-20 second hang on page load
        if ($.browser.msie && $.browser.version.substr(0,1) == 6){
          fblog('IE6, delaying schedule load to prevent hang');
          setTimeout(function(){
            initSchedule(formDef, params);
          }, 500);
        }
        else {
          initSchedule(formDef, params);
        }
        
        break;












      case "billing":
      
        // $('#page-title').css({'margin-bottom': 0, 'border-bottom-width': '0'});
      
        $('#jq-formstatic-inject').removeClass('hidden').css('padding-bottom', 0);
      
        //activeInterface = 'billing';
        
        if (!$('#page-title-dynamic').length){
          $('#page-title').append(' <span id="page-title-dynamic"></span> <span id="page-title-buttons"></span>');
        }
      
        today = dateString(Date.today().clearTime());
        target = shared.systemStart; //'2008-07-01';
        
        billingGet({
          start: today, // target,
          accounts: null, //[232],
          target: formDef.addedInjectTarget
        });
        
        break;
        
        
        
        
        
        
        
      case "staff":
        initStaff(params);
      
        break;
        
        
        
        
        
      case "clients":
      
        initClients(params);
        
        break;
        
        
        
      case "files":
      case "download":
        // $('#page-title').css({'margin-bottom': 0, 'border-bottom-width': '0'});
      
        break;
        
        
        
        
        
        
        
        
      case "photos-index":
        var galleryElement = $('#galleryContainer');
        
        var galleryItem = $('div.galleryItem');
        
        galleryItem.each(
          function(){
            $(this).click(
              function(e){
                window.location.href = ['photos/', $(this).attr('link')].join('');
              }
            );
          }
        );
        
        // galleryElement.hover(
          // function(e){
            // var el = $(e.target);
            
            // if (!el.is('div.galleryItem')){
              // el = el.parents('div.galleryItem');
            // }
            
            // el.addClass('galleryItemHover');
          // },
          // function(e){
            // var el = $(e.target);
            
            // if (!el.is('div.galleryItem')){
              // el = el.parents('div.galleryItem');
            // }
            
            // el.removeClass('galleryItemHover');
          // }
        // );
        break;


        
      case "photos-gallery":
        var galleryElement = $('#galleryContainer');
      
        $('#galleryContainer a').lightBox({
          imageLoading:      ['/common-', shared.revisionRaw, '/jquery-lightbox-0.5/images/lightbox-ico-loading.gif'].join(''),    // (string) Path and the name of the loading icon
          imageBtnPrev:      ['/common-', shared.revisionRaw, '/jquery-lightbox-0.5/images/lightbox-btn-prev.gif'].join(''),      // (string) Path and the name of the prev button image
          imageBtnNext:      ['/common-', shared.revisionRaw, '/jquery-lightbox-0.5/images/lightbox-btn-next.gif'].join(''),      // (string) Path and the name of the next button image
          imageBtnClose:    ['/common-', shared.revisionRaw, '/jquery-lightbox-0.5/images/lightbox-btn-close.gif'].join(''),    // (string) Path and the name of the close btn
          imageBlank:        ['/common-', shared.revisionRaw, '/jquery-lightbox-0.5/images/lightbox-blank.gif'].join('')    // (string) Path and the name of a blank image (one pixel)
        });
        
        // $('.galleryContainer galleryRow').each(function(){
          // $(this).find('a').lightBox();
        // });
        break;
    }
    
    
    // switch (params.target){
      // case 'time':
        // $(window).scrollTo($('#staffTime'), {offset: {top: -50}});
        // break;
        
      // default:
    // }

  }




  
  
  
  
  function initSchedule(formDef, params){
    var today, target, spd;
    // if (params.preferences){
      // for (var pref in params.preferences){
        // schedule.preferences[pref] = params.preferences[pref];
      // }
    // }
    
    // aa.objectMerge(schedule.preferences, params.preferences);
    $.extend(schedule.preferences, params.preferences);
    
    spd = schedule.preferences.display;
    
    spd.physicists = aa.toArray(spd.physicists);
    spd.physicistsWeekly = aa.toArray(spd.physicistsWeekly);
    spd.physicistsMonthly = aa.toArray(spd.physicistsMonthly);
    
    
    // schedule.preferences.display.view = 'month';
    
    // if (schedule.preferences.display.physicists.length > 1){
      // schedule.preferences.display.physicists = [schedule.preferences.display.physicists[0]];
    // }
    // if (schedule.preferences.display.physicists[0] < 0){
      // schedule.preferences.display.physicists[0] = 103;
    // }

    //schedule.preferences.display.view = 'week';
    // schedule.preferences.display.physicists = [-1];
    
    scheduleViewConfig();
    
    fbdir("schedule.preferences", true);
    
    $('#jq-formstatic-inject').removeClass('hidden').css('padding-bottom', 0);
  
    //activeInterface = 'schedule';
  
    $('#page-title').css('margin-bottom', 0);
    if (!$('#page-title-date').length){
      $('#page-title').append(' <div id="date-picker" class="datepickerTarget" title="Click to choose time period"><span id="page-title-date-loading" class="lighter" style="margin-left: .5em; display: none">Loading...</span><a id="page-title-date"><span id="page-title-dynamic"></span><img class="dontprint" src="/common/assets/images/calendar_icon2.jpg" style="margin: 0 0 -.125em .25em"></a></div> <span id="page-title-buttons"></span>');
      $('#page-title-date').click(function(e){
        var target = $(this);
        var monthView = false;
        while (!target.hasClass('datepickerTarget')){
          target = target.parent();
        }
        
        if (schedule.preferences.display.view == 'month'){
          monthView = true;
        }
        
        var dateTarget = dpExactDate(schedule.today),
          dateStart = dpExactDate(schedule.config.start),
          dateEnd = dpExactDate(schedule.config.end);
          
        if (
          dateTarget.between(dateStart, dateEnd)
          ||
          dateTarget.equals(dateStart)
          ||
          dateTarget.equals(dateEnd)
        ){
        }
        else {
          dateTarget = dateStart.addDays(3);
        }
        
        // fblog(dateTarget);
        
        var pickerEl = $('.datepick-inline', target);
        if (!pickerEl.length){
          target.datepick({
            //defaultDate: dpExactDate('2010-01-01'),
            //yearRange: '2008:2018',
            minDate: dpExactDate(shared.systemStart), //'2008-07-01'),
            maxDate: '+2Y',
            hideIfNoPrevNext: true,
            changeMonth: false,
            changeYear: false,
            showOtherMonths: true,
            selectOtherMonths: true,
            numberOfMonths: 1,
            monthSelect: monthView,
            //showStatus: true,
            currentText: 'Current',
            prevText: '< Prev',
            nextText: 'Next >',
            highlightWeek: true,
            onSelect: function (value, date) {
              fblog(['datepick() onSelect: ', value].join(''));
              // $('#date-picker div.datepick-inline').hide();
              datepickerHide();
              if (schedule.preferences.display.view == 'month'){
                schedule.config.period = date.set({day: 1}).toString('yyyy-MM');
              }
              else {
                schedule.config.start = dateString(date);
              }
              scheduleGet();
            }
          });
          target.datepick('setDate', dateTarget);
          pickerEl = $('.datepick-inline', target);
        }
        else {
          if (pickerEl.is(':visible')){
            //fblog('visible');
            pickerEl.hide();
          }
          else {
            //fblog('not visible');
            //fblog(dateTarget.toString());
            
            var currDate = target.datepick('getDate');
            if (Date.compare(currDate, dateTarget) != 0){
              target.datepick('setDate', dateTarget);
            }
            target.datepick('option', {monthSelect: monthView});
            pickerEl.show();
          }
        }
      });
    }
    
    $('#jq-form-inject > div.flatContainer > div.body').css({'padding-bottom': 0, 'background-color': 'transparent', 'border-width': 0});
  
    today = dateString(Date.today().clearTime());
    target = today; //'2010-09-01';
    
    schedule.config = {
      start: aa.existsIn(permissions, 'debug') ? target : today, //today, //target,
      view: schedule.preferences.display.view,
      target: formDef.addedInjectTarget
    };
    
    scheduleGet();
    
    // load Google Maps API
    //setTimeout(initMapsAPI, 4000);
    // initMapsAPI();
    
  }
  
  
  
  function datepickerHide(){
    $('#date-picker div.datepick-inline').hide();
  }
  
  



  function ticklerImport(){
    $('div.widthCap').css('max-width', 'none');
    $('#col1').removeClass('colsingle').after('<div id="col3"><div class="page_content"><div id="ticklerImport"><img src="/common/assets/images/large-loading.gif" align="absmiddle"/>&nbsp;Loading... this may take quite a while...</div></div></div>');  
    
    $.ajax({
      url: [shared.urlRoot, '/ajaxSSGet/import-tickler'].join(''),
      
      success: function (data, textStatus){
        var result = clients.tickler = shared.lastJSON.generic = eval(['(', data, ')'].join(''));
        fblog('ticklerImport(): ajax received');
        
        var output = ['<table id="ticklerImportCandidates">'];
        var i;
        var items = result.items;
        var facilities = result.facilities;
        
        for (i = 0; i < facilities.length; i++){
          // if (!facilities[i].location_id){
            output.push('<tr');
            
            if (Number(facilities[i].location_id)){
              output.push(' class="tagged"><td class="location_id">', facilities[i].location_id);
            }
            else {
              output.push('><td class="location_id">');
            }
            
            output.push('</td><td class="name" index="', i, '" candidate="', facilities[i].candidate_id, '"><span class="name">', facilities[i]['name'], '</span></td></tr>');
          // }
        }
        
        output.push('</table>');
        $('#ticklerImport').html(output.join(''))
          .find('td.name').click(function(){
            $(this).parent().parent().find('tr.current').removeClass('current');
            $(this).parent().addClass('current');
            $('#cm_client').val(($(this).children('span.name').text()).replace(/[\/@]/g, ' '));
            triggerLookup({target: $('#cm_client')});

            $(this).parent().parent().find('tr td span.details').hide();
            
            if ($(this).find('span.details').length){
              $(this).find('span.details').show();
            }
            else {
              var items = clients.tickler.facilities[$(this).attr('index')].items;
              var dates = [];
              var j;
              for (j = 0; j < items.length; j++){
                dates.push([clients.tickler.items[items[j]].date, ': ', clients.tickler.items[items[j]].description].join(''));
              }
              $(this).append(['<span class="details small"><br />', dates.join('<br />'), '</span>'].join(''));
            }
          })
          // .dblclick(function(){
            // if ($(this).find('span.details').length){
              // var details = $('span.details', $(this));
              // if (details.is(':visible')){
                // details.hide();
              // }
              // else {
                // details.show();
              // }
            // }
            // else {
              // var items = clients.tickler.facilities[$(this).attr('index')].items;
              // var dates = [];
              // var j;
              // for (j = 0; j < items.length; j++){
                // dates.push(clients.tickler.items[items[j]].date);
              // }
              // $(this).append(['<span class="details"><br />', dates.join(), '</span>'].join(''));
            // }
          // })
          .end()
          .find('td.location_id').click(function(){
            var i;
            var el = $(this);
            var location;
            
            if (el.text()){
              el.html('')
                .parent().removeClass('tagged').addClass('pending');
              location = 0;
            }
            else if (clients.active.id){
              el.text(clients.active.id)
                .parent().addClass('tagged').addClass('pending');
              location = clients.active.id;
            }
            
            $(this).next().find('span.details').hide();

            
            var toBeSaved = [];
            var facility = clients.tickler.facilities[el.next('td.name').attr('index')];
            var itemIndexes = facility.items;
            for (i = 0; i < itemIndexes.length; i++){
              toBeSaved.push($.extend(clients.tickler.items[itemIndexes[i]], {index: itemIndexes[i]}));
            }
            
            $.ajax({
              url: [shared.urlRoot, '/ajaxSSGet/ticklerUpdate'].join(''),
              data: {
                location_id: location,
                candidate_id: facility.candidate_id,
                items: JSON.stringify(toBeSaved)
              },
              success: function (data, textStatus){
                var result = shared.lastJSON.generic = eval(['(', data, ')'].join(''));
                
                if (!result.saved || result.failed.length){
                }
                else {
                  el.parent().removeClass('pending');
                }
              }
            });
          });
          
        $('<div id="ticklerTaggedToggle" style="text-align: right"><a id="ticklerTaggedToggleLink">Hide linked locations</a></div>').insertBefore($('#ticklerImport')).click(function(){
          if (!$('#ticklerImportCandidates tr.tagged:first').is(':hidden')){
            $('#ticklerImportCandidates tr.tagged').hide();
            $('#ticklerTaggedToggleLink').text('Show all locations');
          }
          else {
            $('#ticklerImportCandidates tr.tagged').show();
            $('#ticklerTaggedToggleLink').text('Hide linked locations');
          }
        });
      },
      error: function (XMLHttpRequest, textStatus, errorThrown){
        fblog(['ticklerImport(): ajax failure = ', textStatus, ' (', errorThrown, ')'].join(''));
      }
    });
  }
  
  

  function initClients(params){ //}
    if (aa.existsIn(permissions, 'debug')){
      $('#page-title').append([
        '<div id="searchControls" class="tablesorterPager">',
        '<a id="linkTicklerImport">Import tickler</a>',
        '</div>'
      ].join(''));
      
      $('#linkTicklerImport').click(function(){
        ticklerImport();
        $(this).remove();
      });
    }
  
    $('#formClients div.body:first').css('margin-bottom', '2.5em');
  
    $('#jq-formstatic-inject').addClass('backgroundWhite'); //.css('position: relative');
    
    // insert browse link
    $('#cm_client_label').parent().after('<td class="inputLabelRight"><a id="linkBrowse">List all locations</a></td>');
    
    
    
    
    // browse link click functionality
    $('#linkBrowse').click(function(){
      var searchPane = $('#searchPane');
      var browsePane = $('#browsePane');
      var i;
      
      // if either pane doesn't exist (or isn't ID'd),
      // assume this is the first time this is being called
      if (!searchPane.length || !browsePane.length){
      
        // ID search pane
        searchPane = $('#formClients > div.body:first').attr('id', 'searchPane');
        
        // insert browse page
        searchPane.after('<div id="browsePane" class="body" style="margin-bottom: 2.5em"><div class="item"><img src="/common/assets/images/large-loading.gif" align="absmiddle"/>&nbsp;Loading...</div><div class="clearfix" /></div>');
        browsePane = $('#browsePane');
        searchPane.hide();
        $('#searchControls').hide();
        
        setTimeout(function(){
        
          // create table
          var wip = $('#jq-wip');
          if (!wip.length){
            $('#hidden_container').append('<div id="jq-wip" class="hidden" />');
            wip = $('#jq-wip');
          }
          
          var tableArray = [
            '<table id="browseTableContainer" cellspacing="0" class="billingTable"><tr><td class="billingGroupRow"><table id="browseTable" class="tablesorter billingTableItemization cursorPointer" cellspacing="0">',
            '<thead><tr>',
              '<th>Location Name</th>',
              '<th class="center">Frequency</th>',
              '<th class="center">Visits in<br>past year</th>',
              '<th class="center">Most recent<br>visit</th>',
              aa.existsIn(permissions, 'accounting') ? '<th class="center">Account #</th>' : '',
              aa.existsIn(permissions, 'debug') ? '<th class="center">Location #</th>' : '',
            '</tr></thead>',
            '<tbody>'
          ];
          
          var svc = shared.clients;
          
          var frequencies = [
          ];
          
          for (i = 0; i < /*100*/ svc.length; i++){
            tableArray.push('<tr location="', svc[i].location_id, '"');
            
            if (!Number(svc[i].num_visits)){
              //tableArray.push(' class="hidden"');
            }
            
            tableArray.push('>',
                '<td>', svc[i].location_name
            );
            
            if (svc[i].unique_identifier){
              tableArray.push([' (', svc[i].unique_identifier, ')'].join(''));
            }
            else if (svc[i].num_locations > 1){
              tableArray.push([' (', svc[i].city, ')'].join(''));
            }
            
            tableArray.push(
                svc[i].aliases ? ['<br/ ><span class="light"><span class="smallCaps">AKA</span> ', svc[i].aliases, '</span>'].join('') : '',
                '</td>',
                '<td class="center">'
            );
              
            var visits = Number(svc[i].num_visits);
            var cat = '';
            var threshold = 0;
            var color = 'black';
            var colors = {
              gray: '#bbb',
              black: '#666',
              green: '#5AAD52',
              yellow: '#B5AC41',
              red: '#90363D',
              blue: '#476FA2'
            };
            
            if      (visits <= 0) {cat = 'unknown'; threshold = 0; color = 'gray'}
            else if (visits <= 1) {cat = 'annual'; threshold = 1; color = 'black'}
            else if (visits <= 3) {cat = 'semiannual'; threshold = 3; color = 'black'}
            else if (visits <= 5) {cat = 'quarterly'; threshold = 5; color = 'black'}
            else if (visits <= 8) {cat = 'bimonthly'; threshold = 8; color = 'black'}
            else if (visits <= 15) {cat = 'monthly'; threshold = 15; color = 'green'}
            else if (visits <= 40) {cat = 'biweekly'; threshold = 40; color = 'yellow'}
            else if (visits <= 75) {cat = 'weekly'; threshold = 75; color = 'red'}
            else if (visits <= 200) {cat = 'semiweekly'; threshold = 200; color = 'blue'}
            else if (visits <= 9999) {cat = 'daily'; threshold = 9999; color = 'blue'}
            
            tableArray.push('<span class="hidden">', threshold, '</span><span style="color: ', colors[color], '">', cat, '</span></td>'); //Number(svc[i].num_visits) ? svc[i].num_visits : '', '</td>',
            
            tableArray.push(
                '<td class="center">', visits > 0 ? visits : '', '</td>',
                '<td class="center">', svc[i].most_recent_date ? dpExactDateTime(svc[i].most_recent_date).toString('M/d/yy') : '', '</td>',
                aa.existsIn(permissions, 'accounting') ? ['<td class="center">', svc[i].account_id, '</td>'].join('') : '',
                aa.existsIn(permissions, 'debug') ? ['<td class="center">', svc[i].location_id, '</td>'].join('') : '',
              '</tr>'
            );
          }
          
          tableArray.push('</tbody></table></td></tr></table>');
              
          wip.append(tableArray.join(''));
          var browseTable = $('#browseTable');
          
          // add parser through the tablesorter addParser method 
          $.tablesorter.addParser({ 
            // set a unique id 
            id: 'blankLastNumber', 
            is: function(s) { 
              // return false so this parser is not auto detected 
              return false; 
            }, 
            format: function(s) { 
              // format your data for normalization 
              if (!s.length){
                return 0;
              }
              else {
                return s; 
              }
            }, 
            // set type, either numeric or text 
            type: 'numeric' 
          });
          
          $.tablesorter.addParser({
            id: "blankLastDate",
            is: function(s) {
              return false;
            },
            format: function(s,table) {
              var ret;
              if (!s.length){
                //return 1245764607220;
                ret = -9999999999999;
                //return "0";
                //ret = "0";
              }
              else {
                var c = table.config;
                s = s.replace(/\-/g,"/");
                if(c.dateFormat == "us") {
                  // reformat the string in ISO format
                  s = s.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})/, "$3/$1/$2");
                } else if(c.dateFormat == "uk") {
                  //reformat the string in ISO format
                  s = s.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})/, "$3/$2/$1");
                } else if(c.dateFormat == "dd/mm/yy" || c.dateFormat == "dd-mm-yy") {
                  s = s.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{2})/, "$1/$2/$3");  
                }
                ret = $.tablesorter.formatFloat(new Date(s).getTime());
              }
              //fblog('|' + ret + '|');
              return ret;
            },
            type: "numeric"
          });
          
          // setup sorter
          browseTable
          .tablesorter({
            //debug: true,
            widgets: ['zebra'],
            deferWidgets: true,
            widgetZebra: {css: ['evenRow', '']},
            sortList: [[0,0]],
            headers: {1: {sorter: 'blankLastNumber', order: 1}, 2: {sorter: 'blankLastNumber', order: 1}, 3: {sorter: 'blankLastDate', order: 1} },
            //widthFixed: true,
            textExtraction: 'simple'
          });
          
          // setup mouse hover highlight
          tableRowHoverSetup(browseTable, 'highlightgreen');
          
          // show table
          browsePane.empty().append($('#browseTableContainer')).css('padding-bottom', 0);
          
          // fix column widths after visible
          browseTable.trigger("fixColumnWidths");
          
          // create pager and controls
          $('#page-title').append([
            '<div id="browseControls" class="tablesorterPager">',
              'Page:&nbsp;',
              '<span class="pagedisplay"></span>&nbsp;&nbsp;&nbsp;&nbsp;',
              '<a class="first">First</a>&nbsp;&nbsp;',
              '<a class="prev">Previous</a>&nbsp;&nbsp;',
              '<a class="next">Next</a>&nbsp;&nbsp;',
              '<a class="last">Last</a>&nbsp;&nbsp;&nbsp;&nbsp;',
              '<select class="pagesize">',
                '<option value="10">10</option>',
                '<option value="20" selected="selected">20</option>',
                '<option value="30">30</option>',
                '<option value="40">40</option>',
              '</select> rows per page&nbsp;&nbsp;&nbsp;&nbsp;',
            '<a id="linkSearch">Back to Search form</a></div>'].join('')
          );
          // search link click functionality
          $('#linkSearch').click(function(){
            $('#searchPane').show();
            $('#searchControls').show();
            $('#browsePane').hide();
            $('#browseControls').hide();
          });

          browseTable.tablesorterPager({container: $('#browseControls'), positionFixed: false, size: 20});
          
          // manually apply widgets after applying pager
          browseTable.trigger("applyWidgets");
          
          // prevent selection marks from dragging and shift/ctrl-clicking table cells
          browseTable.find('th, td').disableTextSelect();
          
          // remove existing event handlers if present
          browseTable.unbind('click');
        
          // add mouse event delegators
          browseTable.click(
            function(e){
              if (e.button < 2) {
                var el = $(e.target);
                
                while (!el.is('td')){ //(el[0].tagName == 'TD')){
                  el = el.parent();
                }
                var row = el.parent();
                  
                autocompleteResultClient('client', el.parents('form'), row.attr('location'), $('#jq-formstatic-inject'));
                
              }
            }
          );
          

        }, 25);
        
      }
      // otherwise assume this isn't the first time this is being called and just swap them
      else {
        browsePane.show();
        $('#browseControls').show();
        searchPane.hide();
        $('#searchControls').hide();
      }
    });

    // load Google Maps API
    //initMapsAPI();
    //google.load("maps", "2", {"callback" : function() { fblog('google maps API loaded')} });
    
    // $.getScript(
      // // 'http://maps.google.com/maps?file=api&amp;v=2&amp;sensor=false&amp;key=ABQIAAAAB-7Uy9U7txLXsZzc4Y6c_xRd2LMtx0GHTKkh6cQT-VqyFJxAexTe7tbG1mWN6HhajCD8J2PdztyJ4A'
      // 'http://www.google.com/jsapi?key=ABQIAAAAB-7Uy9U7txLXsZzc4Y6c_xRd2LMtx0GHTKkh6cQT-VqyFJxAexTe7tbG1mWN6HhajCD8J2PdztyJ4A',
      // function() {
        // google.load("maps", "2");
      // }
    // );
      
    // LazyLoad.load(
      // [
        // //'http://maps.google.com/maps?file=api&amp;v=2&amp;sensor=false&amp;key=ABQIAAAAB-7Uy9U7txLXsZzc4Y6c_xRd2LMtx0GHTKkh6cQT-VqyFJxAexTe7tbG1mWN6HhajCD8J2PdztyJ4A',
        // '/common/jquery.googlemaps.circle.js'
      // ],
      // function(){ fblog('lazyload gmaps finished'); }
    // );
    
    //updaterStart('clientsSaveDirty');
    
    
    if (Number(params.query[0])){
      var target = $('#cm_client');
      autocompleteResult(
        {target: target[0], pageLoad: true, searchText: as.urlDecode(params.query[1])}, // event
        {location_id: Number(params.query[0])} // data
      );
    }
  }
  
  
  
  
  
  
  
  
  
  function initStaff(params){
    staff.preferences = {
      hidden: []
    };
    
    // set default preferences
    if (shared.physicistsById[user.id]){
      if (shared.physicistsById[user.id].tasktype_id == 3){
        staff.preferences.availabilityList = 2;
        staff.preferences.scheduleRecent = 2;
      }
      else if (shared.physicistsById[user.id].tasktype_id == 4){
        staff.preferences.availabilityList = 1;
        staff.preferences.scheduleRecent = 1;
      }
      else {
        staff.preferences.availabilityList = 0;
        staff.preferences.scheduleRecent = 0;
      }
    }
    
    staff.config = {
      availabilityList: {
        align: 'right',
        width: 33
      },
      scheduleRecent: {
        align: 'left',
        width: 66
      }
    };
    
    // import any loaded preferences
    // aa.objectMerge(staff.preferences, params.preferences);
    $.extend(staff.preferences, params.preferences);
    if (staff.preferences.scheduleRecent === true){
      staff.preferences.scheduleRecent === 0;
    }
    
    // if (params.preferences){
      // for (var pref in params.preferences){
        // staff.preferences[pref] = params.preferences[pref];
      // }
      
      // if (staff.preferences.scheduleRecent === true){
        // staff.preferences.scheduleRecent === 0;
      // }
      
      // // if (params.preferences.availabilityList !== undefined){
        // // staff.preferences.availabilityList = params.preferences.availabilityList;
      // // }
      // // if (params.preferences.scheduleRecent !== undefined){
        // // staff.preferences.scheduleRecent = params.preferences.scheduleRecent;
        // // if (staff.preferences.scheduleRecent === true){
          // // staff.preferences.scheduleRecent === 0;
        // // }
      // // }
    // }
  
    fbdir('staff.preferences', true);
      
    $('#jq-form-inject').empty().after('<div class="clear"></div>');
    
    if (!staff.preferences.hasOwnProperty('availabilityList')){
      staff.config.scheduleRecent.width = 100;
    }

    if (staff.preferences.hasOwnProperty('scheduleRecent')){
      staffScheduleRecent();
    }
    else {
      staff.config.availabilityList.align = 'left';
    }
    
    if (staff.preferences.hasOwnProperty('availabilityList')){
      staffAvailability();
    }
    
    // if (
      // aa.existsIn(permissions, 'hr-admin')
      // ||
      // shared.physicistsById[user.id]
    // ){
      staffTime(Number(params.physicist), (params.target == 'time'));
    // }
    
    if (aa.existsIn(permissions, 'debug')){
      staffProximity();
    }
    
    
    Acat.windowResizeChildren({
      forceResize: true,
      callback: function(){
        // staffAvailabilityCallback();
        // staffScheduleRecentCallback();
        // staffProximityCallback();
      }
    });

    updaterStart('staffGetUpdated');
  }
  
  
  
  
  
  
  // load Google Maps API after page loads
  function initMapsAPI(success){
    if (window.google !== undefined){
      google.load("maps", "2", {
        other_params: "sensor=false", // required to be set even if false
        
        callback: function() {
          // load mod after maps API is loaded
          $.getScript(
            '/common/jquery.googlemaps.circle.js',
            function(){
              setTimeout(function(){
                fblog('google maps API with mods loaded, after timeout CircleOverlay = ' + Boolean(window.CircleOverlay));
                shared.geo.loaded = true;
                if (!shared.geo.directions){
                  shared.geo.directions = {};
                }
              
                if (!shared.geo.directions.gdir){
                  shared.geo.directions.gdir = new google.maps.Directions();
                }

                if (success && typeof success == 'function'){
                  success();
                }
              }, 150);
            }
          );
        }
      });
      return true;
    }
    else {
      fblog('unable to load Google API');
      return false;
    }
  }
  
  
  
  
  
  
  
  
  
  
  











  
  
  // expose functions
  return {

    // published variables
    // activeInterface: activeInterface,
    forms: forms,
    schedule: schedule,
    shared: shared,
    staff: staff,
    billing: billing,
    clients: clients,
    //activeEdit: activeEdit,
    debugLog: debugLog,
    logStringArr: logStringArr,
  
    // published functions
    fblog: fblog,
    fbdir: fbdir,
    init: init,
    errorHandler: errorHandler,
    
    staffTimeUpdate: staffTimeUpdate
    
  };

}();


