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:
parent
d21c8bcaeb
commit
7ca03df04d
224
tailbone/static/js/tailbone.timesheet.edit.js
Normal file
224
tailbone/static/js/tailbone.timesheet.edit.js
Normal 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();
|
||||
});
|
||||
|
||||
});
|
|
@ -11,6 +11,11 @@
|
|||
var data_modified = false;
|
||||
var okay_to_leave = true;
|
||||
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() {
|
||||
if (! okay_to_leave) {
|
||||
|
@ -89,15 +94,6 @@
|
|||
|
||||
<%def name="edit_timetable_javascript()">
|
||||
${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 name="edit_timetable_styles()">
|
||||
|
|
|
@ -3,7 +3,59 @@
|
|||
|
||||
<%def name="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 name="extra_styles()">
|
||||
|
@ -37,18 +89,9 @@
|
|||
${h.csrf_token(request)}
|
||||
</%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')}
|
||||
|
||||
${edit_tools()}
|
||||
|
||||
<div id="day-editor" style="display: none;">
|
||||
<div class="shifts"></div>
|
||||
<button type="button" id="add-shift">Add Shift</button>
|
||||
|
|
|
@ -28,6 +28,8 @@ from __future__ import unicode_literals, absolute_import
|
|||
|
||||
import datetime
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from rattail import enum
|
||||
from rattail.db import model, api
|
||||
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)))
|
||||
shifts = Session.query(cls)\
|
||||
.filter(cls.employee_uuid.in_([e.uuid for e in employees]))\
|
||||
.filter(cls.start_time >= make_utc(min_time))\
|
||||
.filter(cls.start_time < make_utc(max_time))\
|
||||
.filter(sa.or_(
|
||||
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()
|
||||
|
||||
for employee in employees:
|
||||
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'):
|
||||
employee.weekdays = [{} for day in weekdays]
|
||||
setattr(employee, '{}_hours'.format(shift_type), datetime.timedelta(0))
|
||||
|
|
|
@ -97,10 +97,20 @@ class TimeSheetView(BaseTimeSheetView):
|
|||
shift = Session.query(model.WorkedShift).get(uuid)
|
||||
assert 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))
|
||||
end_time = datetime.datetime.strptime(data['end_time'][uuid], time_format)
|
||||
shift.end_time = make_utc(localtime(self.rattail_config, end_time))
|
||||
|
||||
start_time = data['start_time'][uuid] or None
|
||||
if start_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 {}, "
|
||||
"deleted {} Worked Shifts".format(
|
||||
|
|
Loading…
Reference in a new issue