phase2 / openair-reborn

A Chrome extension to improve (fix) the OpenAir time entry grid interface
11 stars 17 forks source link

Save timesheet automatically #23

Open crittermike opened 10 years ago

crittermike commented 10 years ago

This should help with losing data due to getting logged out. Can take inspiration from the code of https://chrome.google.com/webstore/detail/enhanced-openair-timeshee/cndbpehenhdahdpiodadlihdkofcakkm?hl=en which does this (or did).

crittermike commented 9 years ago

Here's the JS from the linked extension which auto-saves every 15 minutes of no activity. The good stuff is at the bottom.

var Entry = function(cell, month_year) {
  this.init(cell, month_year);
  this.enhance();
}

Entry.prototype.init = function(cell, month_year) {
  this.cell = cell;
  this.month_year = month_year;
  this.input = $('input', cell)[0];
  this.noteslink = $('a', cell)[0];
  this.name = this.input.name;

  this.notesinput = document.forms[0][this.name + '_dialog_notes'];
  this.fixHTML();
}

Entry.prototype.enhance = function() {
  this.addTimer();
  this.improveNotes();
  this.markToday();
}

Entry.prototype.fixHTML = function() {
  var nobr = this.cell;
  this.cell = $(this.cell).parent('td');
  $(nobr).wrap('<div class="entry-wrapper"></div>');
  $(nobr).children().unwrap();
  this.cell.addClass('entry-cell');
  this.cell = $('.entry-wrapper', this.cell);

  $(this.input).attr('autocomplete', 'off');
}

/**
 * Adds timer functionality to this entry.
 */
Entry.prototype.addTimer = function() {
  if (this.isToday() || this.hasSavedTimer()) {
    this.start = $('<a class="start-timer">Start Timer</a>').appendTo(this.cell)
    var input = this.input, self = this;

    this.start.toggle(function(){
      self.startTimer();
      $(this).text('Stop Timer');
    },
    function() {
      self.stopTimer();
      $(this).text('Start Timer');
    });

    this.loadTimer();
  }
}

Entry.prototype.startTimer = function() {
  var input = this.input;
  var ref = this;

  if (!input.value) { input.value = '0.00'; }
  this.timer = setInterval(function(){
    ref.input.value = Math.round((parseFloat(ref.input.value) + 0.01) * 100)/100;
    $(ref.input).trigger('change').trigger('blur');
    ref.timed += 0.01;
  }, 36000);

  $(this.input).addClass('running-timer');
}

Entry.prototype.stopTimer = function() {
  if(this.timer) {
    clearInterval(this.timer);
    this.timer = false;
    this.timed = 0;
  }
  $(this.input).removeClass('running-timer');
}

Entry.prototype.saveTimer = function(remainder) {
  var time = 0;
  if (window.is_form_save || remainder) {
    var timer = parseFloat(this.input.value);
    time = Math.round((timer * 100) % 25) / 100;
    if (time >= 0.13) {
      time = -(Math.round((0.25 - time) * 100) / 100);
    }
  } else {
    time = this.timed;
  }

  this.save('timer', time);
}

Entry.prototype.wasPersisted = function() {
  return $(this.input).parent('tr').has('font.error').size() > 0;
}

Entry.prototype.loadTimer = function() {
  var item = this.load('timer');
  if (item) {
    var val = Math.round(parseFloat(this.input.value) * 100)/100;
    if (isNaN(val)) { val = 0.0; }
    added = parseFloat(item);
    if (isNaN(added)) { added = 0.0; }
    val += added;
    this.timed = added;
    if (!this.wasPersisted()) {
      this.input.value = Math.round(val * 100) / 100;
    }
    this.start.click();
    $(this.input).change();

    this.remove('timer');
  }
}

Entry.prototype.hasSavedTimer = function() {
  return this.load('timer') ? true : false;
}

/**
 * Retrieves the notes for a given entry.
 */
Entry.prototype.getNotes = function() {
  var message = '';
  if (this.notesinput) {
    message = this.notesinput.value;
  }
  return message;
}
/**
 * Renders a Notes edit form.
 */
Entry.prototype.improveNotes = function() {
  var value = this.getNotes();
  var name = this.name + '_dialog_notes';

  $(this.noteslink).remove();
  var ref = this;

  $(this.input).focus(function() {
    Entry.closeAllNotes(ref);

    if (!ref.haspopover) {
      $(ref.input).popover({
        html : true,
        placement : 'bottom',
        trigger : 'manual',
        content : function() {
          var popover = $('<div class="tooltip-notes" id="' + ref.name + '_notes"></div>');
          var text = $('<textarea name="' + ref.name + '_notes_real_input" id="'+ ref.name +'_notes_real_input" placeholder="Note details..."></textarea>');
          text.text(ref.getNotes()).appendTo(popover);
          text.after('<div class="instructions pull-left"><sub><b>Enter</b> to Save, <b>Esc</b> to Cancel, <b>Shift+Enter</b> for newline.</sub></div>' +
            '<div class="popover-group pull-right clearfix">' +
            '<button class="save-notes btn btn-primary">Save</button>' +
            '<button class="close-notes btn btn-danger">Cancel</button>' +
          '</div>');
          return popover[0].outerHTML;
        },
        title : 'Notes',
      });

      // Show the Popover
      $(ref.input).popover('show');
      ref.haspopover = true;

      // Textarea Bindings
      $('#'+ref.name +'_notes_real_input').keyup(function(e){
        // Auto-height
        while(parseInt($(this).css('height')) <= $(this)[0].scrollHeight ){
          $(this)[0].rows++;
        }
      }).keydown(function(e){ //have to do this in keydown to prevent the enters from being accepted by the text box
        // Handle Keys
        key = (e.keyCode ? e.keyCode : e.which);
        if(key == 13 && !(e.ctrlKey || e.altKey || e.metaKey || e.shiftKey)) { // Enter
          e.stopPropagation();
          return ref.closeNotes(true);
        }else if(key == 27){ // Escape
          return ref.closeNotes();
        }
      }).keyup(); // Call on box display

      // Button Bindings
      $('.close-notes').click(function() {
        return ref.closeNotes();
      });
      $('.save-notes').click(function(){
        return ref.closeNotes(true);
      });
    }
  }).click(function(e){  //make it easier to get the popover back if you accidently close it
    $(this).trigger('focus');
  });

  this.loadNotes();
}

Entry.prototype.closeNotes = function(save, nofocus){
  if (save) {
    var notes = $('#' + this.name + '_notes_real_input');
    if (notes.size()) {
      this.notesinput.value = notes.val();
    }
  }
  if (!nofocus) { this.input.focus(); }
  $(this.input).popover('destroy');
  this.haspopover = false;
  return false;
};

Entry.closeAllNotes = function(skip) {
  if (window.entries) {
    var e = window.entries.length;
    while(e--) {
      var entry = window.entries[e];
      if (skip && entry === skip) {
        continue;
      }
      entry.closeNotes(false, true);
    }
  }
};

Entry.prototype.saveNotes = function() {
  this.save('notes', this.getNotes());
}

Entry.prototype.loadNotes = function() {
  var local = this.load('notes');
  if (!this.getNotes() && local) {
    this.notesinput.value = local;
  }
}

Entry.prototype.getTid = function() {
  if (!this.tid) {
    var search = window.location.search.split(';'),
    params = [];
    for(var i = 0; i < search.length; i++) {
      var exploded = search[i].split('=');
      params[exploded[0]] = exploded[1];
    }

    this.tid = params['timesheet_id'];
  }
  return this.tid;
}

Entry.prototype.save = function(name, value) {
  var tid = this.getTid();
  localStorage[tid + '_' + this.name + '_' + name] = value;
}

Entry.prototype.load = function(name) {
  var tid = this.getTid();
  if (localStorage[tid + '_' + this.name + '_' + name]) {
    return localStorage[tid + '_' + this.name + '_' + name];
  }
  return false;
}

Entry.prototype.remove = function(name) {
  var tid = this.getTid();
  localStorage.removeItem(tid + '_' + this.name + '_' + name);
}

Entry.prototype.isToday = function() {
  if (typeof this.is_today == 'undefined') {
    //get our date string for this column
    var col = (this.name.split('_'))[1].replace('c', '');
    col = (+col) - 3;

    var date = parseInt($('.table-header-row td > font').eq(col).text());
    var col_date = new Date(this.month_year.join('-') + '-' + date);
    var today = new Date();

    this.is_today = (today.getDate() == date) && (today.getMonth() == col_date.getMonth()) && (today.getFullYear() == this.month_year[0]);
  }
  return this.is_today;
}

Entry.prototype.markToday = function() {
  if (this.isToday()) {
    this.cell.addClass('today');
  }
}

// Helper to get the date range for a timesheet
function getTimesheetMonthYear() {
  var month_year = document.forms[0]['_date'].value;
  return month_year.split('-').slice(0, 2);
}

// ======================== SETUP FUNCTION and EVENTS ================================ //
//If this is the proper timesheet view, create entries
if (window.location.search.indexOf(';action=grid;') > -1) {
  var m_y = getTimesheetMonthYear();

  var entries = [];
  //Create an Entry object for each time entry slot.
  $('td nobr', formtable).has('input').has('.notesIcon').removeClass('disabled').each(function(ind, el){
    entries.push(new Entry(el, m_y));
  });
  //add a class to disabled cells for theming
  $('td', formtable).has('nobr input').not('.entry-cell').not('.project-cell').addClass('entry-cell disabled');

  //save the timesheet timers.
  function submitTimesheet(nosubmit) {
    //get all our timers that are running
    var e = entries.length;
    while(e--) {
      var entry = entries[e];

      if (entry.timer) {
        entry.saveTimer();
      }
      entry.saveNotes();
    }
    if (!nosubmit) {
      $('input[name="_save_grid"]').click();
    }
  }

  $(window).bind('beforeunload', function(e) {
    submitTimesheet(true);
  });

  //allow users to configure autosave
  $('.date-row td').append('<div class="autosave-wrapper"><input id="autosave" type="checkbox" /><label for="autosave">AutoSave after 15m of inactivity.</label></div>');
  if (typeof(localStorage['autosave']) == 'undefined') {
    localStorage['autosave'] = true;
  }

  //save the form every 15m
  time = setTimeout(submitTimesheet, 900000);
  var resetTimeout = function() {
    clearTimeout(time);
    if (localStorage['autosave']) {
      time = setTimeout(submitTimesheet, 900000);
    }
  }

  //reset the timeout if the page is in use
  $('body').mousemove(function(){
    resetTimeout();
  });

  var check = localStorage['autosave'] && localStorage['autosave'] != 'false';
  $('#autosave').prop('checked', check).click(function(e){
    localStorage['autosave'] = this.checked;
    resetTimeout();
  });
}