Refactor the Edit Time Sheet view for "autocommit" mode

I.e. instead of letting changes queue up for "batch" mode, immediately
flush changes to server after each edit.
This commit is contained in:
Lance Edgar 2017-02-09 16:10:40 -06:00
parent d21c8bcaeb
commit 7ca03df04d
5 changed files with 309 additions and 26 deletions

View file

@ -0,0 +1,224 @@
/************************************************************
*
* tailbone.timesheet.edit.js
*
* Common logic for editing time sheet / schedule data.
*
************************************************************/
var editing_day = null;
var new_shift_id = 1;
var show_timepicker = true;
/*
* Add a new shift entry to the editor dialog.
* @param {boolean} focus - Whether to set focus to the start_time input
* element after adding the shift.
* @param {string} uuid - UUID value for the shift, if applicable.
* @param {string} start_time - Value for start_time input element.
* @param {string} end_time - Value for end_time input element.
*/
function add_shift(focus, uuid, start_time, end_time) {
var shift = $('#snippets .shift').clone();
if (! uuid) {
uuid = 'new-' + (new_shift_id++).toString();
}
shift.attr('data-uuid', uuid);
shift.children('input').each(function() {
var name = $(this).attr('name') + '-' + uuid;
$(this).attr('name', name);
$(this).attr('id', name);
});
shift.children('input[name|="edit_start_time"]').val(start_time);
shift.children('input[name|="edit_end_time"]').val(end_time);
$('#day-editor .shifts').append(shift);
// maybe trick timepicker into never showing itself
var args = {showPeriod: true};
if (! show_timepicker) {
args.showOn = 'button';
args.button = '#nevershow';
}
shift.children('input').timepicker(args);
if (focus) {
shift.children('input:first').focus();
}
}
/**
* Calculate the number of minutes between given the times.
* @param {string} start_time - Value from start_time input element.
* @param {string} end_time - Value from end_time input element.
*/
function calc_minutes(start_time, end_time) {
var start = parseTime(start_time);
var end = parseTime(end_time);
if (start && end) {
start = new Date(2000, 0, 1, start.hh, start.mm);
end = new Date(2000, 0, 1, end.hh, end.mm);
return Math.floor((end - start) / 1000 / 60);
}
}
/**
* Converts a number of minutes into string of HH:MM format.
* @param {number} minutes - Number of minutes to be converted.
*/
function format_minutes(minutes) {
var hours = Math.floor(minutes / 60);
if (hours) {
minutes -= hours * 60;
}
return hours.toString() + ':' + (minutes < 10 ? '0' : '') + minutes.toString();
}
/**
* NOTE: most of this logic was stolen from http://stackoverflow.com/a/1788084
*
* Parse a time string and convert to simple object with hh and mm keys.
* @param {string} time - Time value in 'HH:MM PP' format, or close enough.
*/
function parseTime(time) {
if (time) {
var part = time.match(/(\d+):(\d+)(?: )?(am|pm)?/i);
if (part) {
var hh = parseInt(part[1], 10);
var mm = parseInt(part[2], 10);
var ap = part[3] ? part[3].toUpperCase() : null;
if (ap == 'AM') {
if (hh == 12) {
hh = 0;
}
} else if (ap == 'PM') {
if (hh != 12) {
hh += 12;
}
}
return { hh: hh, mm: mm };
}
}
}
/**
* Return a jQuery object containing the hidden start or end time input element
* for the shift (i.e. within the *main* timesheet form). This will create the
* input if necessary.
* @param {jQuery} shift - A jQuery object for the shift itself.
* @param {string} type - Should be 'start' or 'end' only.
*/
function time_input(shift, type) {
var input = shift.children('input[name|="' + type + '_time"]');
if (! input.length) {
input = $('<input type="hidden" name="' + type + '_time-' + shift.data('uuid') + '" />');
shift.append(input);
}
return input;
}
/**
* Update the weekly hour total for a given row (employee).
* @param {jQuery} row - A jQuery object for the row to be updated.
*/
function update_row_hours(row) {
var minutes = 0;
row.find('.day .shift:not(.deleted)').each(function() {
var time_range = $.trim($(this).children('span').text()).split(' - ');
minutes += calc_minutes(time_range[0], time_range[1]);
});
row.children('.total').text(minutes ? format_minutes(minutes) : '0');
}
/**
* Clean up user input within the editor dialog, e.g. '8:30am' => '08:30 AM'.
* This also should ensure invalid input will become empty string.
*/
function cleanup_editor_input() {
// TODO: is this hacky? invoking timepicker to format the time values
// in all cases, to avoid "invalid format" from user input
$('#day-editor .shifts .shift').each(function() {
var start_time = $(this).children('input[name|="edit_start_time"]');
var end_time = $(this).children('input[name|="edit_end_time"]');
$.timepicker._setTime(start_time.data('timepicker'), start_time.val() || '??');
$.timepicker._setTime(end_time.data('timepicker'), end_time.val() || '??');
});
}
/**
* Update the main timesheet table based on editor dialog input. This updates
* both the displayed timesheet, as well as any hidden input elements on the
* main form.
*/
function update_timetable() {
var date = weekdays[editing_day.get(0).cellIndex - 1];
// add or update
$('#day-editor .shifts .shift').each(function() {
var uuid = $(this).data('uuid');
var start_time = $(this).children('input[name|="edit_start_time"]').val();
var end_time = $(this).children('input[name|="edit_end_time"]').val();
var shift = editing_day.children('.shift[data-uuid="' + uuid + '"]');
if (! shift.length) {
shift = $('<p class="shift" data-uuid="' + uuid + '"><span></span></p>');
shift.append($('<input type="hidden" name="employee_uuid-' + uuid + '" value="'
+ editing_day.parents('tr:first').data('employee-uuid') + '" />'));
editing_day.append(shift);
}
shift.children('span').text((start_time || '??') + ' - ' + (end_time || '??'));
start_time = start_time ? (date + ' ' + start_time) : '';
end_time = end_time ? (date + ' ' + end_time) : '';
time_input(shift, 'start').val(start_time);
time_input(shift, 'end').val(end_time);
});
// remove / mark for deletion
editing_day.children('.shift').each(function() {
var uuid = $(this).data('uuid');
if (! $('#day-editor .shifts .shift[data-uuid="' + uuid + '"]').length) {
if (uuid.match(/^new-/)) {
$(this).remove();
} else {
$(this).addClass('deleted');
$(this).append($('<input type="hidden" name="delete-' + uuid + '" value="delete" />'));
}
}
});
}
/*
* on document load...
*/
$(function() {
/*
* Within editor dialog, clicking Add Shift button will create a new/empty
* shift and set focus to its start_time input.
*/
$('#day-editor #add-shift').click(function() {
add_shift(true);
});
/*
* Within editor dialog, clicking a shift's "trash can" button will remove
* the shift.
*/
$('#day-editor').on('click', '.shifts button', function() {
$(this).parents('.shift:first').remove();
});
});

View file

@ -11,6 +11,11 @@
var data_modified = false; var data_modified = false;
var okay_to_leave = true; var okay_to_leave = true;
var previous_selections = {}; var previous_selections = {};
var weekdays = [
% for i, day in enumerate(weekdays, 1):
'${day.strftime('%a %d %b %Y')}'${',' if i < len(weekdays) else ''}
% endfor
];
window.onbeforeunload = function() { window.onbeforeunload = function() {
if (! okay_to_leave) { if (! okay_to_leave) {
@ -89,15 +94,6 @@
<%def name="edit_timetable_javascript()"> <%def name="edit_timetable_javascript()">
${h.javascript_link(request.static_url('tailbone:static/js/tailbone.edit-shifts.js'))} ${h.javascript_link(request.static_url('tailbone:static/js/tailbone.edit-shifts.js'))}
<script type="text/javascript">
var weekdays = [
% for i, day in enumerate(weekdays, 1):
'${day.strftime('%a %d %b %Y')}'${',' if i < len(weekdays) else ''}
% endfor
];
</script>
</%def> </%def>
<%def name="edit_timetable_styles()"> <%def name="edit_timetable_styles()">

View file

@ -3,7 +3,59 @@
<%def name="extra_javascript()"> <%def name="extra_javascript()">
${parent.extra_javascript()} ${parent.extra_javascript()}
${self.edit_timetable_javascript()} ${h.javascript_link(request.static_url('tailbone:static/js/tailbone.timesheet.edit.js'))}
<script type="text/javascript">
show_timepicker = false;
$(function() {
$('.timesheet').on('click', '.day', function() {
editing_day = $(this);
var editor = $('#day-editor');
var employee = editing_day.siblings('.employee').text();
var date = weekdays[editing_day.get(0).cellIndex - 1];
var shifts = editor.children('.shifts');
shifts.empty();
editing_day.children('.shift:not(.deleted)').each(function() {
var uuid = $(this).data('uuid');
var times = $.trim($(this).children('span').text()).split(' - ');
times[0] = times[0] == '??' ? '' : times[0];
times[1] = times[1] == '??' ? '' : times[1];
add_shift(false, uuid, times[0], times[1]);
});
if (! shifts.children('.shift').length) {
add_shift();
}
editor.dialog({
modal: true,
title: employee + ' - ' + date,
position: {my: 'center', at: 'center', of: editing_day},
width: 'auto',
autoResize: true,
buttons: [
{
text: "Save Changes",
click: function(event) {
$(event.target).button('disable').button('option', 'label', "Saving...");
cleanup_editor_input();
update_timetable();
$('#timetable-form').submit();
}
},
{
text: "Cancel",
click: function() {
editor.dialog('close');
}
}
]
});
});
});
</script>
</%def> </%def>
<%def name="extra_styles()"> <%def name="extra_styles()">
@ -37,18 +89,9 @@
${h.csrf_token(request)} ${h.csrf_token(request)}
</%def> </%def>
<%def name="edit_tools()">
<div class="buttons">
<button type="button" class="save-changes" disabled="disabled">Save Changes</button>
<button type="button" class="undo-changes" disabled="disabled">Undo Changes</button>
</div>
</%def>
${self.timesheet_wrapper(with_edit_form=True, change_employee='confirm_leave')} ${self.timesheet_wrapper(with_edit_form=True, change_employee='confirm_leave')}
${edit_tools()}
<div id="day-editor" style="display: none;"> <div id="day-editor" style="display: none;">
<div class="shifts"></div> <div class="shifts"></div>
<button type="button" id="add-shift">Add Shift</button> <button type="button" id="add-shift">Add Shift</button>

View file

@ -28,6 +28,8 @@ from __future__ import unicode_literals, absolute_import
import datetime import datetime
import sqlalchemy as sa
from rattail import enum from rattail import enum
from rattail.db import model, api from rattail.db import model, api
from rattail.time import localtime, make_utc, get_sunday from rattail.time import localtime, make_utc, get_sunday
@ -350,13 +352,21 @@ class TimeSheetView(View):
max_time = localtime(self.rattail_config, datetime.datetime.combine(weekdays[-1] + datetime.timedelta(days=1), datetime.time(0))) max_time = localtime(self.rattail_config, datetime.datetime.combine(weekdays[-1] + datetime.timedelta(days=1), datetime.time(0)))
shifts = Session.query(cls)\ shifts = Session.query(cls)\
.filter(cls.employee_uuid.in_([e.uuid for e in employees]))\ .filter(cls.employee_uuid.in_([e.uuid for e in employees]))\
.filter(cls.start_time >= make_utc(min_time))\ .filter(sa.or_(
.filter(cls.start_time < make_utc(max_time))\ sa.and_(
cls.start_time >= make_utc(min_time),
cls.start_time < make_utc(max_time),
),
sa.and_(
cls.start_time == None,
cls.end_time >= make_utc(min_time),
cls.end_time < make_utc(max_time),
)))\
.all() .all()
for employee in employees: for employee in employees:
employee_shifts = sorted([s for s in shifts if s.employee_uuid == employee.uuid], employee_shifts = sorted([s for s in shifts if s.employee_uuid == employee.uuid],
key=lambda s: s.start_time) key=lambda s: s.start_time or s.end_time)
if not hasattr(employee, 'weekdays'): if not hasattr(employee, 'weekdays'):
employee.weekdays = [{} for day in weekdays] employee.weekdays = [{} for day in weekdays]
setattr(employee, '{}_hours'.format(shift_type), datetime.timedelta(0)) setattr(employee, '{}_hours'.format(shift_type), datetime.timedelta(0))

View file

@ -97,10 +97,20 @@ class TimeSheetView(BaseTimeSheetView):
shift = Session.query(model.WorkedShift).get(uuid) shift = Session.query(model.WorkedShift).get(uuid)
assert shift assert shift
updated[uuid] = shift updated[uuid] = shift
start_time = datetime.datetime.strptime(data['start_time'][uuid], time_format)
shift.start_time = make_utc(localtime(self.rattail_config, start_time)) start_time = data['start_time'][uuid] or None
end_time = datetime.datetime.strptime(data['end_time'][uuid], time_format) if start_time:
shift.end_time = make_utc(localtime(self.rattail_config, end_time)) start_time = datetime.datetime.strptime(start_time, time_format)
shift.start_time = make_utc(localtime(self.rattail_config, start_time))
else:
shift.start_time = None
end_time = data['end_time'][uuid] or None
if end_time:
end_time = datetime.datetime.strptime(end_time, time_format)
shift.end_time = make_utc(localtime(self.rattail_config, end_time))
else:
shift.end_time = None
self.request.session.flash("Changes were applied: created {}, updated {}, " self.request.session.flash("Changes were applied: created {}, updated {}, "
"deleted {} Worked Shifts".format( "deleted {} Worked Shifts".format(