Add ability to edit employee time sheet

Also disable some unwanted autocomplete logic, plus add ability to
prevent autocomplete "change click" event
This commit is contained in:
Lance Edgar 2017-01-29 18:53:52 -06:00
parent 2e88cdde88
commit 7104e275c3
11 changed files with 467 additions and 302 deletions

View file

@ -0,0 +1,186 @@
/************************************************************
*
* tailbone.edit-shifts.js
*
* Common logic for editing time sheet / schedule data.
*
************************************************************/
var editing_day = null;
var new_shift_id = 1;
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);
shift.children('input').timepicker({showPeriod: true});
if (focus) {
shift.children('input:first').focus();
}
}
function calc_minutes(start_time, end_time) {
var start = parseTime(start_time);
start = new Date(2000, 0, 1, start.hh, start.mm);
var end = parseTime(end_time);
end = new Date(2000, 0, 1, end.hh, end.mm);
return Math.floor((end - start) / 1000 / 60);
}
function format_minutes(minutes) {
var hours = Math.floor(minutes / 60);
if (hours) {
minutes -= hours * 60;
}
return hours.toString() + ':' + (minutes < 10 ? '0' : '') + minutes.toString();
}
// stolen from http://stackoverflow.com/a/1788084
function parseTime(s) {
var part = s.match(/(\d+):(\d+)(?: )?(am|pm)?/i);
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 };
}
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;
}
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');
}
$(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 time_range = $.trim($(this).children('span').text()).split(' - ');
add_shift(false, uuid, time_range[0], time_range[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: "Update",
click: function() {
// TODO: need to validate times here...
// create / update shifts in time table, as needed
editor.find('.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);
time_input(shift, 'start').val(date + ' ' + start_time);
time_input(shift, 'end').val(date + ' ' + end_time);
});
// remove shifts from time table, as needed
editing_day.children('.shift').each(function() {
var uuid = $(this).data('uuid');
if (! editor.find('.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" />'));
}
}
});
// mark day as modified, close dialog
editing_day.addClass('modified');
$('.save-changes').button('enable');
$('.undo-changes').button('enable');
update_row_hours(editing_day.parents('tr:first'));
editor.dialog('close');
data_modified = true;
okay_to_leave = false;
}
},
{
text: "Cancel",
click: function() {
editor.dialog('close');
}
}
]
});
});
$('#day-editor #add-shift').click(function() {
add_shift(true);
});
$('#day-editor').on('click', '.shifts button', function() {
$(this).parents('.shift:first').remove();
});
$('.save-changes').click(function() {
$(this).button('disable').button('option', 'label', "Saving Changes...");
okay_to_leave = true;
$('#timetable-form').submit();
});
$('.undo-changes').click(function() {
$(this).button('disable').button('option', 'label', "Refreshing...");
okay_to_leave = true;
location.href = location.href;
});
});

View file

@ -259,24 +259,6 @@ $(function() {
});
/*
* Whenever the "change" button is clicked within the context of an
* autocomplete field, hide the static display and show the autocomplete
* textbox.
*/
$('div.autocomplete-container button.autocomplete-change').click(function() {
var container = $(this).parents('div.autocomplete-container');
var textbox = container.find('input.autocomplete-textbox');
container.find('input[type="hidden"]').val('');
container.find('div.autocomplete-display').hide();
textbox.val('');
textbox.show();
textbox.select();
textbox.focus();
});
/*
* Add "check all" functionality to tables with checkboxes.
*/

View file

@ -1,6 +1,6 @@
## -*- coding: utf-8 -*-
## TODO: This function signature is getting out of hand...
<%def name="autocomplete(field_name, service_url, field_value=None, field_display=None, width='300px', select=None, selected=None, cleared=None, options={})">
<%def name="autocomplete(field_name, service_url, field_value=None, field_display=None, width='300px', select=None, selected=None, cleared=None, change_clicked=None, options={})">
<div id="${field_name}-container" class="autocomplete-container">
${h.hidden(field_name, id=field_name, value=field_value)}
${h.text(field_name+'-textbox', id=field_name+'-textbox', value=field_display,
@ -37,6 +37,11 @@
% endif
});
$('#${field_name}-change').click(function() {
% if change_clicked:
if (! ${change_clicked}()) {
return false;
}
% endif
$('#${field_name}').val('');
$('#${field_name}-display').hide();
with ($('#${field_name}-textbox')) {

View file

@ -3,9 +3,8 @@
<%def name="title()">${page_title}</%def>
<%def name="head_tags()">
${parent.head_tags()}
${h.stylesheet_link(request.static_url('tailbone:static/css/timesheet.css'))}
<%def name="extra_javascript()">
${parent.extra_javascript()}
<script type="text/javascript">
var data_modified = false;
@ -82,6 +81,55 @@
</script>
</%def>
<%def name="extra_styles()">
${parent.extra_styles()}
${h.stylesheet_link(request.static_url('tailbone:static/css/timesheet.css'))}
</%def>
<%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()">
<style type="text/css">
.timesheet .day {
cursor: pointer;
height: 5em;
}
.timesheet tr .day.modified {
background-color: #fcc;
}
.timesheet tr:nth-child(odd) .day.modified {
background-color: #ebb;
}
.timesheet .day .shift.deleted {
display: none;
}
#day-editor .shift {
margin-bottom: 1em;
white-space: nowrap;
}
#day-editor .shift input {
width: 6em;
}
#day-editor .shift button {
margin-left: 0.5em;
}
#snippets {
display: none;
}
</style>
</%def>
<%def name="context_menu()"></%def>
<%def name="render_day(day)">

View file

@ -1,7 +1,7 @@
## -*- coding: utf-8 -*-
<%namespace file="/autocomplete.mako" import="autocomplete" />
<%def name="timesheet_wrapper(edit_form=None, edit_tools=None, context_menu=None, render_day=None)">
<%def name="timesheet_wrapper(edit_form=None, edit_tools=None, context_menu=None, render_day=None, change_employee=None)">
<div class="timesheet-wrapper">
${form.begin(id='filter-form')}
@ -21,7 +21,8 @@
${autocomplete('employee', url('employees.autocomplete'),
field_value=employee.uuid if employee else None,
field_display=unicode(employee or ''),
selected='employee_selected')}
selected='employee_selected',
change_clicked=change_employee)}
% else:
${form.hidden('employee', value=employee.uuid)}
${employee}
@ -111,7 +112,10 @@
<tbody>
% for emp in sorted(employees, key=unicode):
<tr data-employee-uuid="${emp.uuid}">
<td class="employee">${emp}</td>
<td class="employee">
## TODO: add link to single employee schedule / timesheet here...
${emp}
</td>
% for day in emp.weekdays:
<td class="day">
% if render_day:

View file

@ -2,195 +2,13 @@
<%inherit file="/shifts/base.mako" />
<%namespace file="/shifts/lib.mako" import="timesheet_wrapper" />
<%def name="head_tags()">
${parent.head_tags()}
<%def name="extra_javascript()">
${parent.extra_javascript()}
${self.edit_timetable_javascript()}
<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
];
var editing_day = null;
var new_shift_id = 1;
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);
shift.children('input').timepicker({showPeriod: true});
if (focus) {
shift.children('input:first').focus();
}
}
function calc_minutes(start_time, end_time) {
var start = parseTime(start_time);
start = new Date(2000, 0, 1, start.hh, start.mm);
var end = parseTime(end_time);
end = new Date(2000, 0, 1, end.hh, end.mm);
return Math.floor((end - start) / 1000 / 60);
}
function format_minutes(minutes) {
var hours = Math.floor(minutes / 60);
if (hours) {
minutes -= hours * 60;
}
return hours.toString() + ':' + (minutes < 10 ? '0' : '') + minutes.toString();
}
// stolen from http://stackoverflow.com/a/1788084
function parseTime(s) {
var part = s.match(/(\d+):(\d+)(?: )?(am|pm)?/i);
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 };
}
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;
}
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');
}
$(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 time_range = $.trim($(this).children('span').text()).split(' - ');
add_shift(false, uuid, time_range[0], time_range[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: "Update",
click: function() {
// TODO: need to validate times here...
// create / update shifts in schedule table, as needed
editor.find('.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') + '" />'));
## TODO: how to handle editing schedule w/ no store selected..?
% if store:
shift.append($('<input type="hidden" name="store_uuid-' + uuid + '" value="${store.uuid}" />'));
% endif
editing_day.append(shift);
}
shift.children('span').text(start_time + ' - ' + end_time);
time_input(shift, 'start').val(date + ' ' + start_time);
time_input(shift, 'end').val(date + ' ' + end_time);
});
// remove shifts from schedule table, as needed
editing_day.children('.shift').each(function() {
var uuid = $(this).data('uuid');
if (! editor.find('.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" />'));
}
}
});
// mark day as modified, close dialog
editing_day.addClass('modified');
$('.save-changes').button('enable');
$('.undo-changes').button('enable');
update_row_hours(editing_day.parents('tr:first'));
editor.dialog('close');
data_modified = true;
okay_to_leave = false;
}
},
{
text: "Cancel",
click: function() {
editor.dialog('close');
}
}
]
});
});
$('#day-editor #add-shift').click(function() {
add_shift(true);
});
$('#day-editor').on('click', '.shifts button', function() {
$(this).parents('.shift:first').remove();
});
$('.save-changes').click(function() {
$(this).button('disable').button('option', 'label', "Saving Changes...");
okay_to_leave = true;
$('#schedule-form').submit();
});
$('.undo-changes').click(function() {
$(this).button('disable').button('option', 'label', "Refreshing...");
okay_to_leave = true;
location.href = '${url('schedule.edit')}';
});
$('.clear-schedule').click(function() {
if (confirm("This will remove all shifts from the schedule you're " +
"currently viewing.\n\nAre you sure you wish to do this?")) {
@ -234,34 +52,11 @@
});
</script>
<style type="text/css">
.timesheet .day {
cursor: pointer;
height: 5em;
}
.timesheet tr .day.modified {
background-color: #fcc;
}
.timesheet tr:nth-child(odd) .day.modified {
background-color: #ebb;
}
.timesheet .day .shift.deleted {
display: none;
}
#day-editor .shift {
margin-bottom: 1em;
white-space: nowrap;
}
#day-editor .shift input {
width: 6em;
}
#day-editor .shift button {
margin-left: 0.5em;
}
#snippets {
display: none;
}
</style>
</%def>
<%def name="extra_styles()">
${parent.extra_styles()}
${self.edit_timetable_styles()}
</%def>
<%def name="context_menu()">
@ -282,7 +77,7 @@
</%def>
<%def name="edit_form()">
${h.form(url('schedule.edit'), id='schedule-form')}
${h.form(url('schedule.edit'), id='timetable-form')}
${h.csrf_token(request)}
</%def>

View file

@ -3,6 +3,9 @@
<%namespace file="/shifts/lib.mako" import="timesheet_wrapper" />
<%def name="context_menu()">
% if employee is not Undefined and request.has_perm('timesheet.edit'):
<li>${h.link_to("Edit this Time Sheet", url('timesheet.employee.edit'))}</li>
% endif
% if request.has_perm('schedule.view'):
<li>${h.link_to("View this Schedule", url('timesheet.goto.schedule'), class_='goto')}</li>
% endif

View file

@ -0,0 +1,58 @@
## -*- coding: utf-8 -*-
<%inherit file="/shifts/base.mako" />
<%namespace file="/shifts/lib.mako" import="timesheet_wrapper" />
<%def name="extra_javascript()">
${parent.extra_javascript()}
${self.edit_timetable_javascript()}
</%def>
<%def name="extra_styles()">
${parent.extra_styles()}
${self.edit_timetable_styles()}
</%def>
<%def name="context_menu()">
% if request.has_perm('timesheet.view'):
<li>${h.link_to("View this Time Sheet", url('timesheet.employee'))}</li>
% endif
% if request.has_perm('schedule.view'):
<li>${h.link_to("View this Schedule", url('schedule.employee'))}</li>
% endif
</%def>
<%def name="render_day(day)">
% for shift in day['shifts']:
<p class="shift" data-uuid="${shift.uuid}">
${render_shift(shift)}
</p>
% endfor
</%def>
<%def name="edit_form()">
${h.form(url('timesheet.employee.edit'), id='timetable-form')}
${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>
${timesheet_wrapper(edit_form=edit_form, edit_tools=edit_tools, context_menu=context_menu, render_day=render_day, 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>
</div>
<div id="snippets">
<div class="shift" data-uuid="">
${h.text('edit_start_time')} thru ${h.text('edit_end_time')}
<button type="button"><span class="ui-icon ui-icon-trash"></span></button>
</div>
</div>

View file

@ -2,7 +2,7 @@
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2016 Lance Edgar
# Copyright © 2010-2017 Lance Edgar
#
# This file is part of Rattail.
#
@ -121,6 +121,37 @@ class TimeSheetView(View):
'employees': employees.all(),
}
def get_employee_context(self):
"""
Determine employee/date context from user's session and/or defaults
"""
date = None
date_key = 'timesheet.{}.employee.date'.format(self.key)
if date_key in self.request.session:
date_value = self.request.session.get(date_key)
if date_value:
try:
date = datetime.datetime.strptime(date_value, '%m/%d/%Y').date()
except ValueError:
pass
if not date:
date = localtime(self.rattail_config).date()
employee = None
employee_key = 'timesheet.{}.employee'.format(self.key)
if employee_key in self.request.session:
employee_uuid = self.request.session[employee_key]
employee = Session.query(model.Employee).get(employee_uuid) if employee_uuid else None
if not employee:
employee = self.request.user.employee
# force current user if not allowed to view all data
if not self.request.has_perm('{}.viewall'.format(self.key)):
employee = self.request.user.employee
assert employee
return {'date': date, 'employee': employee}
def process_filter_form(self, form):
"""
Process a "shift filter" form if one was in fact POST'ed. If it was
@ -136,6 +167,19 @@ class TimeSheetView(View):
self.request.session['timesheet.{}.date'.format(self.key)] = date.strftime('%m/%d/%Y') if date else None
raise self.redirect(self.request.current_route_url())
def process_employee_filter_form(self, form):
"""
Process an "employee shift filter" form if one was in fact POST'ed. If it
was then we store new context in session and redirect to display as normal.
"""
if self.request.method == 'POST':
if form.validate():
employee = form.data['employee']
self.request.session['timesheet.{}.employee'.format(self.key)] = employee.uuid if employee else None
date = form.data['date']
self.request.session['timesheet.{}.employee.date'.format(self.key)] = date.strftime('%m/%d/%Y') if date else None
raise self.redirect(self.request.current_route_url())
def full(self):
"""
View a "full" timesheet/schedule, i.e. all employees but filterable by
@ -151,50 +195,11 @@ class TimeSheetView(View):
"""
View time sheet for single employee.
"""
date = None
employee = None
if not self.request.has_perm('{}.viewall'.format(self.key)):
# force current user if not allowed to view all data
employee = self.request.user.employee
assert employee
form = Form(self.request, schema=EmployeeShiftFilter)
if self.request.method == 'POST':
if form.validate():
if self.request.has_perm('{}.viewall'.format(self.key)):
employee = form.data['employee']
self.request.session['timesheet.{}.employee'.format(self.key)] = employee.uuid if employee else None
date = form.data['date']
self.request.session['timesheet.{}.employee.date'.format(self.key)] = date.strftime('%m/%d/%Y') if date else None
return self.redirect(self.request.current_route_url())
else:
if self.request.has_perm('{}.viewall'.format(self.key)):
employee_key = 'timesheet.{}.employee'.format(self.key)
if employee_key in self.request.session:
employee_uuid = self.request.session.get(employee_key)
if employee_uuid:
employee = Session.query(model.Employee).get(employee_uuid)
else: # no employee in session
if self.request.user:
employee = self.request.user.employee
date_key = 'timesheet.{}.employee.date'.format(self.key)
if date_key in self.request.session:
date_value = self.request.session.get(date_key)
if date_value:
try:
date = datetime.datetime.strptime(date_value, '%m/%d/%Y').date()
except ValueError:
pass
# default to current user; force unless allowed to view all data
if not employee or not self.request.has_perm('{}.viewall'.format(self.key)):
employee = self.request.user.employee
assert employee
if not date:
date = localtime(self.rattail_config).date()
return self.render_single(date, employee, form=form)
self.process_employee_filter_form(form)
context = self.get_employee_context()
context['form'] = form
return self.render_single(**context)
def crossview(self):
"""
@ -216,17 +221,6 @@ class TimeSheetView(View):
self.session_put('date', self.session_get('date'), mainkey=other_key)
return self.redirect(self.request.route_url(other_key))
# def session_has(self, key, mainkey=None):
# if mainkey is None:
# mainkey = self.key
# return 'timesheet.{}.{}'.format(mainkey, key) in self.request.session
# def session_has_any(self, *keys, **kwargs):
# for key in keys:
# if self.session_has(key, **kwargs):
# return True
# return False
def session_get(self, key, mainkey=None):
if mainkey is None:
mainkey = self.key
@ -301,7 +295,7 @@ class TimeSheetView(View):
def render_shift(self, shift):
return HTML.tag('span', c=shift.get_display(self.rattail_config))
def render_single(self, date, employee, form=None):
def render_single(self, date=None, employee=None, form=None, **kwargs):
"""
Render a time sheet for one employee, for the week which includes the
specified date.
@ -319,7 +313,7 @@ class TimeSheetView(View):
self.modify_employees([employee], weekdays)
return {
context = {
'page_title': "Employee {}".format(self.get_title()),
'form': forms.FormRenderer(form) if form else None,
'employee': employee,
@ -332,6 +326,8 @@ class TimeSheetView(View):
'permission_prefix': self.key,
'render_shift': self.render_shift,
}
context.update(kwargs)
return context
def modify_employees(self, employees, weekdays):
min_time = localtime(self.rattail_config, datetime.datetime.combine(weekdays[0], datetime.time(0)))

View file

@ -64,6 +64,8 @@ class ScheduleView(TimeSheetView):
form = Form(self.request, schema=ShiftFilter)
self.process_filter_form(form)
context = self.get_timesheet_context()
# okay then, maybe process saved shift data
if self.request.method == 'POST':
@ -97,7 +99,10 @@ class ScheduleView(TimeSheetView):
if uuid.startswith('new-'):
shift = model.ScheduledShift()
shift.employee_uuid = data['employee_uuid'][uuid]
shift.store_uuid = data['store_uuid'][uuid]
if 'store_uuid' in data and uuid in data['store_uuid']:
shift.store_uuid = data['store_uuid'][uuid]
else:
shift.store_uuid = context['store'].uuid if context['store'] else None
Session.add(shift)
created[uuid] = shift
else:
@ -114,7 +119,6 @@ class ScheduleView(TimeSheetView):
len(created), len(updated), len(deleted)))
return self.redirect(self.request.route_url('schedule.edit'))
context = self.get_timesheet_context()
context['form'] = form
context['page_title'] = "Edit Schedule"
return self.render_full(**context)

View file

@ -2,7 +2,7 @@
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2016 Lance Edgar
# Copyright © 2010-2017 Lance Edgar
#
# This file is part of Rattail.
#
@ -26,19 +26,103 @@ Views for employee time sheets
from __future__ import unicode_literals, absolute_import
from rattail.db import model
import datetime
from tailbone.views.shifts.lib import TimeSheetView as BaseTimeSheetView
from rattail.db import model
from rattail.time import make_utc, localtime
from pyramid_simpleform import Form
from tailbone.db import Session
from tailbone.views.shifts.lib import TimeSheetView as BaseTimeSheetView, EmployeeShiftFilter
class TimeSheetView(BaseTimeSheetView):
"""
Simple view for current user's time sheet.
Views for employee time sheets, i.e. worked shift data
"""
key = 'timesheet'
title = "Time Sheet"
model_class = model.WorkedShift
def edit_employee(self):
"""
View for editing single employee's timesheet
"""
# process filters; redirect if any were received
form = Form(self.request, schema=EmployeeShiftFilter)
self.process_employee_filter_form(form)
context = self.get_employee_context()
# okay then, maybe process saved shift data
if self.request.method == 'POST':
# TODO: most of this is copied from 'schedule.edit' view, should merge...
# organize form data by uuid / field
fields = ['start_time', 'end_time', 'delete']
data = dict([(f, {}) for f in fields])
for key in self.request.POST:
for field in fields:
if key.startswith('{}-'.format(field)):
uuid = key[len('{}-'.format(field)):]
if uuid:
data[field][uuid] = self.request.POST[key]
break
# apply delete operations
deleted = []
for uuid, value in list(data['delete'].items()):
assert value == 'delete'
shift = Session.query(model.WorkedShift).get(uuid)
assert shift
Session.delete(shift)
deleted.append(uuid)
# apply create / update operations
created = {}
updated = {}
time_format = '%a %d %b %Y %I:%M %p'
for uuid, time in data['start_time'].iteritems():
if uuid in deleted:
continue
if uuid.startswith('new-'):
shift = model.WorkedShift()
shift.employee_uuid = context['employee'].uuid
# TODO: add support for setting store here...
Session.add(shift)
created[uuid] = shift
else:
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))
self.request.session.flash("Changes were applied: created {}, updated {}, "
"deleted {} Worked Shifts".format(
len(created), len(updated), len(deleted)))
return self.redirect(self.request.route_url('timesheet.employee.edit'))
context['form'] = form
context['page_title'] = "Edit Employee Time Sheet"
return self.render_single(**context)
@classmethod
def defaults(cls, config):
cls._defaults(config)
# edit employee time sheet
config.add_tailbone_permission('timesheet', 'timesheet.edit',
"Edit time sheet (for *any* employee!)")
config.add_route('timesheet.employee.edit', '/timesheeet/employee/edit')
config.add_view(cls, attr='edit_employee', route_name='timesheet.employee.edit',
renderer='/shifts/timesheet_edit.mako',
permission='timesheet.edit')
def includeme(config):
TimeSheetView.defaults(config)