diff --git a/shifter/notifications.py b/shifter/notifications.py index 20cde88f99dbe203909f033000872e726eb22157..bac6cbaaed7fd8c6361b1a7a11e4481d6ce80ec5 100644 --- a/shifter/notifications.py +++ b/shifter/notifications.py @@ -178,6 +178,7 @@ class EmailNotifier(NotificationService): if isinstance(event, Shift) and status in EmailNotifier.STATUS_CONFIG: config = EmailNotifier.STATUS_CONFIG[status] subject = config["subject"] + email_body = render_to_string( config["template"], { @@ -188,6 +189,7 @@ class EmailNotifier(NotificationService): "op_email": DEFAULT_EMAIL_OPS, }, ) + notify_message = config["notify_message"] send_mail(subject, email_body, self.DEFAULT_NO_REPLY, [event.member.email], fail_silently=False, html_message=email_body) diff --git a/shifts/contexts.py b/shifts/contexts.py index f83098ec9895fba608809cdac47527fb0444e7bd..32f1642755987badede1f08ad8a37cab1627128e 100644 --- a/shifts/contexts.py +++ b/shifts/contexts.py @@ -13,6 +13,15 @@ from shifter.settings import ( ) +def get_default_backup_revision(): + # TODO add some fancy handling if it does not exists + return Revision.objects.filter(name__startswith="BACKUP").first() + + +def get_latest_valid_revision(): + return Revision.objects.filter(valid=True).order_by("-number").first() + + def prepare_default_context(request, contextToAdd): """ providing any of the following will override their default values: @@ -21,7 +30,7 @@ def prepare_default_context(request, contextToAdd): @APP_NAME name of the APP displayed in the upper left corner """ date = datetime.datetime.now().date() - latest_revision = Revision.objects.filter(valid=True).order_by("-number").first() + latest_revision = get_latest_valid_revision() for oneShifterMessages in ShifterMessage.objects.filter(valid=True).order_by("-number"): messages.warning(request, oneShifterMessages.description) @@ -45,7 +54,7 @@ def prepare_default_context(request, contextToAdd): def prepare_user_context(member, revisionNext=None): currentMonth = datetime.datetime.now() nextMonth = currentMonth + datetime.timedelta(30) # banking rounding - revision = Revision.objects.filter(valid=True).order_by("-number").first() + revision = get_latest_valid_revision() newer_revisions = Revision.objects.filter(date_start__gt=revision.date_start).filter(ready_for_preview=True).filter(merged=False).order_by("-number") scheduled_shifts = Shift.objects.filter(member=member, revision=revision).order_by("-date") # scheduled_studies = StudyRequest.objects.filter(member=member,state__in=["B","D"]).order_by('slot_start', 'priority') diff --git a/shifts/hrcodes.py b/shifts/hrcodes.py index 4c6bd8cb825a6ee9195d337799cfe7cfc2debc62..15e1c34ef2251755df2471be03b8b49d0430da5c 100644 --- a/shifts/hrcodes.py +++ b/shifts/hrcodes.py @@ -12,7 +12,6 @@ HANDOVER_IN_HOURS = 0.25 NWH_DAY_DURATION_IN_HOURS = 7.8 VACATION_DAY_IN_HOURS = 8.0 - # The ones that are counted as OB3 (as weekends) # TODO think of importing it as external table (not need to re-release it) # TODO consider https://pypi.org/project/holidays/ when Sweden is included (not in Nov 2021) @@ -36,15 +35,15 @@ red_days = [ date(2024, 5, 10), date(2024, 6, 6), date(2024, 6, 7), - date(2024, 12, 31), # https://confluence.ess.eu/display/HR/Public+holidays+and+additional+days+off+%282025%29+Sweden date(2025, 1, 6), date(2025, 5, 1), date(2025, 5, 29), date(2025, 6, 6), - date(2025, 12, 31), ] +# +# These, with the new 2024-09-13 agrement will be treaded special bank/pay reduced_days = [ date(2021, 1, 5), date(2021, 4, 1), @@ -65,12 +64,16 @@ reduced_days = [ date(2024, 11, 1), # https://confluence.ess.eu/display/HR/Public+holidays+and+additional+days+off+%282025%29+Sweden # 3h - date(2024, 4, 17), - date(2024, 4, 30), - date(2024, 11, 1), + date(2025, 4, 17), + date(2025, 4, 30), + date(2025, 10, 31), +] + +bridge_days = [ # 8h: - date(2024, 5, 2), - date(2024, 5, 30), + date(2024, 12, 27), + date(2025, 5, 2), + date(2025, 5, 30), ] # The ones that are counted as OB4 @@ -114,6 +117,7 @@ public_holidays_special = [ date(2024, 12, 24), date(2024, 12, 25), date(2024, 12, 26), + date(2024, 12, 31), # https://confluence.ess.eu/display/HR/Public+holidays+and+additional+days+off+%282025%29+Sweden date(2025, 1, 1), date(2025, 4, 18), @@ -124,6 +128,7 @@ public_holidays_special = [ date(2025, 12, 24), date(2025, 12, 25), date(2025, 12, 26), + date(2025, 12, 31), ] @@ -136,7 +141,18 @@ def get_public_holidays(fmt=None): def count_total(counts): - countsReturn = {"OB1": 0, "OB2": 0, "OB3": 0, "OB4": 0, "NWH": 0} + countsReturn = { + "OB1": 0, + "OB2": 0, + "OB3": 0, + "OB4": 0, + "NWH": 0, + "ELC": 0, + "LC1": 0, + "LC2": 0, + "OT": 0, + "BTPorBTB": 0, + } for oneDay in counts.keys(): for oneCode in countsReturn.keys(): countsReturn[oneCode] += counts.get(oneDay)[oneCode] @@ -174,7 +190,18 @@ def get_code_counts(shift: Shift, granulation=900, returnFactor=4, handoverTime= MAX_HOURS = 8 if verbose: print("========================================\n =======< {} {} {} >=========".format(shift.date, shift.date.strftime("%A"), shift.slot)) - counts = {"OB1": 0, "OB2": 0, "OB3": 0, "OB4": 0, "NWH": 0} + counts = { + "OB1": 0, + "OB2": 0, + "OB3": 0, + "OB4": 0, + "NWH": 0, + "ELC": 0, + "LC1": 0, + "LC2": 0, + "OT": 0, + "BTPorBTB": 0, + } duration = shift.end - shift.start if verbose: @@ -202,6 +229,24 @@ def get_code_counts(shift: Shift, granulation=900, returnFactor=4, handoverTime= if verbose: print("Weekends OB3: ", counts) + for bd in reduced_days: + if shift.date == bd: + counts["BTPorBTB"] = 3 + handoverTime + + for bd in bridge_days: + if shift.date == bd: + counts["BTPorBTB"] = 8 + handoverTime + + if shift.is_changed: + counts["ELC"] = 1 # TODO check unit + if shift.is_changed_urgent: + counts["LC1"] = 1 + else: + counts["LC2"] = 1 + if shift.is_changed_offday: + counts["OT"] = 8.25 # TODO check + notAWEOrHoliday = False + if notAWEOrHoliday: _check(shift, "NWH", counts) _check(shift, "OB1", counts) diff --git a/shifts/migrations/0026_auto_20241017_1659.py b/shifts/migrations/0026_auto_20241017_1659.py new file mode 100644 index 0000000000000000000000000000000000000000..2aea74f94e8cae8717096b5c51e269eefb7eebdc --- /dev/null +++ b/shifts/migrations/0026_auto_20241017_1659.py @@ -0,0 +1,27 @@ +# Generated by Django 3.2.16 on 2024-10-17 14:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("shifts", "0025_shift_is_vacation"), + ] + + operations = [ + migrations.AddField( + model_name="shift", + name="is_changed", + field=models.BooleanField(default=False, help_text="Applies for change compensation?"), + ), + migrations.AddField( + model_name="shift", + name="is_changed_offday", + field=models.BooleanField(default=False, help_text="Applies for OT instead monetary?"), + ), + migrations.AddField( + model_name="shift", + name="is_changed_urgent", + field=models.BooleanField(default=False, help_text="Applies for extra change compensation?"), + ), + ] diff --git a/shifts/models.py b/shifts/models.py index 026403f90600ae463efe9b890ddbbf4dacabd34a..22eb84bd924c6ff769a3345ac900fe4b2f0e6fa8 100644 --- a/shifts/models.py +++ b/shifts/models.py @@ -218,6 +218,10 @@ class Shift(models.Model): is_cancelled = models.BooleanField(default=False, help_text="Last minute cancellation? IT IS counted for the HR " "code.") is_active = models.BooleanField(default=True, help_text="Leave or early cancellation, IT IS NOT counted for the " "HR code.") is_vacation = models.BooleanField(default=False) + # The following should be only for ROTA/ADMIN + is_changed = models.BooleanField(default=False, help_text="Applies for change compensation?") + is_changed_urgent = models.BooleanField(default=False, help_text="Applies for extra change compensation?") + is_changed_offday = models.BooleanField(default=False, help_text="Applies for OT instead monetary?") pre_comment = models.TextField(blank=True, null=True, help_text="Text to be displayed in shifters as shift constraint. [Shifters view " "ONLY]") post_comment = models.TextField(blank=True, null=True, help_text="Summary/comment for the end/post shift time. [Shifters view ONLY]") diff --git a/shifts/static/js/desiderata_space.js b/shifts/static/js/desiderata_space.js index c6e04f249a546032ba8e7fa5901ed697144a0c50..27508885a9ab9af0c20e1bd8cdc8c68ad6860e98 100644 --- a/shifts/static/js/desiderata_space.js +++ b/shifts/static/js/desiderata_space.js @@ -65,6 +65,31 @@ $(document).ready(function() { show: false } }, + { + searchPanes: { + show: false + } + }, + { + searchPanes: { + show: false + } + }, + { + searchPanes: { + show: false + } + }, + { + searchPanes: { + show: false + } + }, + { + searchPanes: { + show: false + } + }, ], "autoWidth": false, footerCallback: function(row, data, start, end, display) { @@ -76,14 +101,14 @@ $(document).ready(function() { }; // Total over all pages - ob1 = api.column(2) + ob1 = api.column(4) .data() .reduce(function(a, b) { return intVal(a) + intVal(b); }, 0); ob1Total = api - .column(2, { + .column(4, { page: 'current' }) .data() @@ -91,14 +116,14 @@ $(document).ready(function() { return intVal(a) + intVal(b); }, 0); - ob2 = api.column(3) + ob2 = api.column(5) .data() .reduce(function(a, b) { return intVal(a) + intVal(b); }, 0); ob2Total = api - .column(3, { + .column(5, { page: 'current' }) .data() @@ -106,14 +131,14 @@ $(document).ready(function() { return intVal(a) + intVal(b); }, 0); - ob3 = api.column(4) + ob3 = api.column(6) .data() .reduce(function(a, b) { return intVal(a) + intVal(b); }, 0); ob3Total = api - .column(4, { + .column(6, { page: 'current' }) .data() @@ -122,14 +147,14 @@ $(document).ready(function() { }, 0); - ob4 = api.column(5) + ob4 = api.column(7) .data() .reduce(function(a, b) { return intVal(a) + intVal(b); }, 0); ob4Total = api - .column(5, { + .column(7, { page: 'current' }) .data() @@ -137,27 +162,27 @@ $(document).ready(function() { return intVal(a) + intVal(b); }, 0); - nwh = api.column(6) + nwh = api.column(3) .data() .reduce(function(a, b) { return intVal(a) + intVal(b); }, 0); nwhTotal = api - .column(6, { + .column(3, { page: 'current' }) .data() .reduce(function(a, b) { return intVal(a) + intVal(b); }, 0); - vac = api.column(6) + vac = api.column(2) .data() .reduce(function(a, b) { return intVal(a) + intVal(b); }, 0); - vacTotal = api.column(6, { + vacTotal = api.column(2, { page: 'current' }) .data() @@ -165,12 +190,12 @@ $(document).ready(function() { return intVal(a) + intVal(b); }, 0); // Update footer - $(api.column(2).footer()).html(ob1Total + ' hours (' + ob1 + 'h total)'); - $(api.column(3).footer()).html(ob2Total + ' hours (' + ob2 + 'h total)'); - $(api.column(4).footer()).html(ob3Total + ' hours (' + ob3 + 'h total)'); - $(api.column(5).footer()).html(ob4Total + ' hours (' + ob4 + 'h total)'); - $(api.column(6).footer()).html(nwhTotal + ' hours (' + nwh + 'h total)'); - $(api.column(6).footer()).html(vacTotal + ' days (' + vac + 'd total)'); + $(api.column(2).footer()).html(vacTotal.toFixed(2) + 'd (' + vac.toFixed(2) + 'd)'); + $(api.column(3).footer()).html(nwhTotal.toFixed(2) + 'h (' + nwh.toFixed(2) + 'h)'); + $(api.column(4).footer()).html(ob1Total.toFixed(2) + 'h (' + ob1.toFixed(2) + 'h)'); + $(api.column(5).footer()).html(ob2Total.toFixed(2) + 'h (' + ob2.toFixed(2) + 'h)'); + $(api.column(6).footer()).html(ob3Total.toFixed(2) + 'h (' + ob3.toFixed(2) + 'h)'); + $(api.column(7).footer()).html(ob4Total.toFixed(2) + 'h (' + ob4.toFixed(2) + 'h)'); }, diff --git a/shifts/static/js/desiderata_space.min.js b/shifts/static/js/desiderata_space.min.js index 7a2006d650f9bdb6b35430e169f87f42b7d0e92c..f364a9c639240f2e6f8286b78aa59e22a859f995 100644 --- a/shifts/static/js/desiderata_space.min.js +++ b/shifts/static/js/desiderata_space.min.js @@ -1 +1 @@ -$(document).ready(function(){$("#date_range_picker").daterangepicker({opens:"left",locale:{firstDay:1}}),$("#date_range_picker").on("change",function(){$("#table_HR").DataTable().ajax.reload()}),$("#table_HR").DataTable({ajax:{url:$("#table_HR").data("source"),data:function(e){e.start=$("#date_range_picker").data("daterangepicker").startDate.format("YYYY-MM-DD"),e.end=$("#date_range_picker").data("daterangepicker").endDate.format("YYYY-MM-DD"),e.team=$("team_id").data("id")}},dom:"P",bPaginate:!1,paging:!1,searchPanes:{initCollapsed:!0},columns:[{searchPanes:{show:!1}},{visible:!0,searchPanes:{show:!0}},{searchPanes:{show:!1}},{searchPanes:{show:!1}},{searchPanes:{show:!1}},{searchPanes:{show:!1}},{searchPanes:{show:!1}},{searchPanes:{show:!1}},],autoWidth:!1,footerCallback:function(e,a,t,n,r){var o=this.api(),u=function(e){return"string"==typeof e?1*e.replace(/[\$,]/g,""):"number"==typeof e?e:0};ob1=o.column(2).data().reduce(function(e,a){return u(e)+u(a)},0),ob1Total=o.column(2,{page:"current"}).data().reduce(function(e,a){return u(e)+u(a)},0),ob2=o.column(3).data().reduce(function(e,a){return u(e)+u(a)},0),ob2Total=o.column(3,{page:"current"}).data().reduce(function(e,a){return u(e)+u(a)},0),ob3=o.column(4).data().reduce(function(e,a){return u(e)+u(a)},0),ob3Total=o.column(4,{page:"current"}).data().reduce(function(e,a){return u(e)+u(a)},0),ob4=o.column(5).data().reduce(function(e,a){return u(e)+u(a)},0),ob4Total=o.column(5,{page:"current"}).data().reduce(function(e,a){return u(e)+u(a)},0),nwh=o.column(6).data().reduce(function(e,a){return u(e)+u(a)},0),nwhTotal=o.column(6,{page:"current"}).data().reduce(function(e,a){return u(e)+u(a)},0),vac=o.column(6).data().reduce(function(e,a){return u(e)+u(a)},0),vacTotal=o.column(6,{page:"current"}).data().reduce(function(e,a){return u(e)+u(a)},0),$(o.column(2).footer()).html(ob1Total+" hours ("+ob1+"h total)"),$(o.column(3).footer()).html(ob2Total+" hours ("+ob2+"h total)"),$(o.column(4).footer()).html(ob3Total+" hours ("+ob3+"h total)"),$(o.column(5).footer()).html(ob4Total+" hours ("+ob4+"h total)"),$(o.column(6).footer()).html(nwhTotal+" hours ("+nwh+"h total)"),$(o.column(6).footer()).html(vacTotal+" days ("+vac+"d total)")}})}); +$(document).ready(function(){$("#date_range_picker").daterangepicker({opens:"left",locale:{firstDay:1}}),$("#date_range_picker").on("change",function(){$("#table_HR").DataTable().ajax.reload()}),$("#table_HR").DataTable({ajax:{url:$("#table_HR").data("source"),data:function(e){e.start=$("#date_range_picker").data("daterangepicker").startDate.format("YYYY-MM-DD"),e.end=$("#date_range_picker").data("daterangepicker").endDate.format("YYYY-MM-DD"),e.team=$("team_id").data("id")}},dom:"P",bPaginate:!1,paging:!1,searchPanes:{initCollapsed:!0},columns:[{searchPanes:{show:!1}},{visible:!0,searchPanes:{show:!0}},{searchPanes:{show:!1}},{searchPanes:{show:!1}},{searchPanes:{show:!1}},{searchPanes:{show:!1}},{searchPanes:{show:!1}},{searchPanes:{show:!1}},{searchPanes:{show:!1}},{searchPanes:{show:!1}},{searchPanes:{show:!1}},{searchPanes:{show:!1}},{searchPanes:{show:!1}},],autoWidth:!1,footerCallback:function(e,a,t,n,r){var o=this.api(),c=function(e){return"string"==typeof e?1*e.replace(/[\$,]/g,""):"number"==typeof e?e:0};ob1=o.column(4).data().reduce(function(e,a){return c(e)+c(a)},0),ob1Total=o.column(4,{page:"current"}).data().reduce(function(e,a){return c(e)+c(a)},0),ob2=o.column(5).data().reduce(function(e,a){return c(e)+c(a)},0),ob2Total=o.column(5,{page:"current"}).data().reduce(function(e,a){return c(e)+c(a)},0),ob3=o.column(6).data().reduce(function(e,a){return c(e)+c(a)},0),ob3Total=o.column(6,{page:"current"}).data().reduce(function(e,a){return c(e)+c(a)},0),ob4=o.column(7).data().reduce(function(e,a){return c(e)+c(a)},0),ob4Total=o.column(7,{page:"current"}).data().reduce(function(e,a){return c(e)+c(a)},0),nwh=o.column(3).data().reduce(function(e,a){return c(e)+c(a)},0),nwhTotal=o.column(3,{page:"current"}).data().reduce(function(e,a){return c(e)+c(a)},0),vac=o.column(2).data().reduce(function(e,a){return c(e)+c(a)},0),vacTotal=o.column(2,{page:"current"}).data().reduce(function(e,a){return c(e)+c(a)},0),$(o.column(2).footer()).html(vacTotal.toFixed(2)+"d ("+vac.toFixed(2)+"d)"),$(o.column(3).footer()).html(nwhTotal.toFixed(2)+"h ("+nwh.toFixed(2)+"h)"),$(o.column(4).footer()).html(ob1Total.toFixed(2)+"h ("+ob1.toFixed(2)+"h)"),$(o.column(5).footer()).html(ob2Total.toFixed(2)+"h ("+ob2.toFixed(2)+"h)"),$(o.column(6).footer()).html(ob3Total.toFixed(2)+"h ("+ob3.toFixed(2)+"h)"),$(o.column(7).footer()).html(ob4Total.toFixed(2)+"h ("+ob4.toFixed(2)+"h)")}})}); diff --git a/shifts/static/js/user_space.js b/shifts/static/js/user_space.js index 6690050e2858e9533b3d396253a24b3e45769a76..bb3b251cdbdecbea47a89c9803cfacc1be2e4bf2 100644 --- a/shifts/static/js/user_space.js +++ b/shifts/static/js/user_space.js @@ -54,14 +54,14 @@ $(document).ready(function() { }; // Total over all pages - ob1 = api.column(1) + ob1 = api.column(3) .data() .reduce(function(a, b) { return intVal(a) + intVal(b); }, 0); ob1Total = api - .column(1, { + .column(3, { page: 'current' }) .data() @@ -69,14 +69,14 @@ $(document).ready(function() { return intVal(a) + intVal(b); }, 0); - ob2 = api.column(2) + ob2 = api.column(4) .data() .reduce(function(a, b) { return intVal(a) + intVal(b); }, 0); ob2Total = api - .column(2, { + .column(4, { page: 'current' }) .data() @@ -84,14 +84,14 @@ $(document).ready(function() { return intVal(a) + intVal(b); }, 0); - ob3 = api.column(3) + ob3 = api.column(5) .data() .reduce(function(a, b) { return intVal(a) + intVal(b); }, 0); ob3Total = api - .column(3, { + .column(5, { page: 'current' }) .data() @@ -100,14 +100,14 @@ $(document).ready(function() { }, 0); - ob4 = api.column(4) + ob4 = api.column(6) .data() .reduce(function(a, b) { return intVal(a) + intVal(b); }, 0); ob4Total = api - .column(4, { + .column(6, { page: 'current' }) .data() @@ -115,14 +115,14 @@ $(document).ready(function() { return intVal(a) + intVal(b); }, 0); - nwh = api.column(5) + nwh = api.column(2) .data() .reduce(function(a, b) { return intVal(a) + intVal(b); }, 0); nwhTotal = api - .column(5, { + .column(2, { page: 'current' }) .data() @@ -130,25 +130,25 @@ $(document).ready(function() { return intVal(a) + intVal(b); }, 0); - vac = api.column(6) + vac = api.column(1) .data() .reduce(function(a, b) { return intVal(a) + intVal(b); }, 0); - vacTotal = api.column(6, { page: 'current' }) + vacTotal = api.column(1, { page: 'current' }) .data() .reduce(function(a, b) { return intVal(a) + intVal(b); }, 0); // Update footer - $(api.column(1).footer()).html(ob1Total + ' hours (' + ob1 + 'h total)'); - $(api.column(2).footer()).html(ob2Total + ' hours (' + ob2 + 'h total)'); - $(api.column(3).footer()).html(ob3Total + ' hours (' + ob3 + 'h total)'); - $(api.column(4).footer()).html(ob4Total + ' hours (' + ob4 + 'h total)'); - $(api.column(5).footer()).html(nwhTotal + ' hours (' + nwh + 'h total)'); - $(api.column(6).footer()).html(vacTotal + ' days (' + vac + 'd total)'); + $(api.column(1).footer()).html(vacTotal.toFixed(2) + 'd (' + vac.toFixed(2) + 'd)'); + $(api.column(2).footer()).html(nwhTotal.toFixed(2) + 'h (' + nwh.toFixed(2) + 'h)'); + $(api.column(3).footer()).html(ob1Total.toFixed(2) + 'h (' + ob1.toFixed(2) + 'h)'); + $(api.column(4).footer()).html(ob2Total.toFixed(2) + 'h (' + ob2.toFixed(2) + 'h)'); + $(api.column(5).footer()).html(ob3Total.toFixed(2) + 'h (' + ob3.toFixed(2) + 'h)'); + $(api.column(6).footer()).html(ob4Total.toFixed(2) + 'h (' + ob4.toFixed(2) + 'h)'); var total = (ob1/600 + ob2/450 + ob3/300 + ob4/150) * 100; $("#helpOB").html(total.toFixed(1)+'%'); }, diff --git a/shifts/static/js/user_space.min.js b/shifts/static/js/user_space.min.js index 970ebf31d5ee9ce366ea462874410df78e2e7a81..80bd53f8dac371c9bae6aa79f7227110b7d187cc 100644 --- a/shifts/static/js/user_space.min.js +++ b/shifts/static/js/user_space.min.js @@ -1 +1 @@ -getUrlParameter=function t(e){var a,n,r=window.location.search.substring(1).split("&");for(n=0;n<r.length;n++)if((a=r[n].split("="))[0]===e)return void 0===a[1]||decodeURIComponent(a[1]);return!1},$(document).ready(function(){$("#date_range_picker").daterangepicker({opens:"left",locale:{firstDay:1}}),$("#date_range_picker").on("change",function(){$("#table_HR").DataTable().ajax.reload()}),$("#table_HR").DataTable({ajax:{url:$("#table_HR").data("source"),data:function(t){t.start=$("#date_range_picker").data("daterangepicker").startDate.format("YYYY-MM-DD"),t.end=$("#date_range_picker").data("daterangepicker").endDate.format("YYYY-MM-DD")}},autoWidth:!1,buttons:[{extend:"collection",text:"Export",buttons:["copy","csv",]}],dom:"Bfrtip",pageLength:25,footerCallback:function(t,e,a,n,r){var o=this.api(),c=function(t){return"string"==typeof t?1*t.replace(/[\$,]/g,""):"number"==typeof t?t:0};ob1=o.column(1).data().reduce(function(t,e){return c(t)+c(e)},0),ob1Total=o.column(1,{page:"current"}).data().reduce(function(t,e){return c(t)+c(e)},0),ob2=o.column(2).data().reduce(function(t,e){return c(t)+c(e)},0),ob2Total=o.column(2,{page:"current"}).data().reduce(function(t,e){return c(t)+c(e)},0),ob3=o.column(3).data().reduce(function(t,e){return c(t)+c(e)},0),ob3Total=o.column(3,{page:"current"}).data().reduce(function(t,e){return c(t)+c(e)},0),ob4=o.column(4).data().reduce(function(t,e){return c(t)+c(e)},0),ob4Total=o.column(4,{page:"current"}).data().reduce(function(t,e){return c(t)+c(e)},0),nwh=o.column(5).data().reduce(function(t,e){return c(t)+c(e)},0),nwhTotal=o.column(5,{page:"current"}).data().reduce(function(t,e){return c(t)+c(e)},0),vac=o.column(6).data().reduce(function(t,e){return c(t)+c(e)},0),vacTotal=o.column(6,{page:"current"}).data().reduce(function(t,e){return c(t)+c(e)},0),$(o.column(1).footer()).html(ob1Total+" hours ("+ob1+"h total)"),$(o.column(2).footer()).html(ob2Total+" hours ("+ob2+"h total)"),$(o.column(3).footer()).html(ob3Total+" hours ("+ob3+"h total)"),$(o.column(4).footer()).html(ob4Total+" hours ("+ob4+"h total)"),$(o.column(5).footer()).html(nwhTotal+" hours ("+nwh+"h total)"),$(o.column(6).footer()).html(vacTotal+" days ("+vac+"d total)");var u=(ob1/600+ob2/450+ob3/300+ob4/150)*100;$("#helpOB").html(u.toFixed(1)+"%")}});let t=document.getElementById("live_toast");var e=getUrlParameter("ical");if(e){let a=document.querySelectorAll("#user_tabs button");a.forEach(t=>{let e=new bootstrap.Tab(t);t.addEventListener("click",t=>{t.preventDefault(),e.show()})});let n=document.querySelector('#user_tabs button[data-bs-target="#links-tab-pane"]');bootstrap.Tab.getInstance(n).show()}var e=getUrlParameter("swaps");if(e){let r=document.querySelectorAll("#user_tabs button");r.forEach(t=>{let e=new bootstrap.Tab(t);t.addEventListener("click",t=>{t.preventDefault(),e.show()})});let o=document.querySelector('#user_tabs button[data-bs-target="#shift-swap-tab-pane"]');bootstrap.Tab.getInstance(o).show()}var e=getUrlParameter("notifications");if(e){let c=document.querySelectorAll("#user_tabs button");c.forEach(t=>{let e=new bootstrap.Tab(t);t.addEventListener("click",t=>{t.preventDefault(),e.show()})});let u=document.querySelector('#user_tabs button[data-bs-target="#notify-tab-pane"]');bootstrap.Tab.getInstance(u).show()}$("#personal_link_clipboard").click(function(){navigator.clipboard.writeText($("#personal_link_clipboard_data").val()).then(function(){$("#toast_content").text("Data copied to clipboard. User Ctrl+P to paste data.")},function(){$("#toast_content").text("Failure to copy. Check permissions for clipboard")});let e=new bootstrap.Toast(t);e.show()}),$("#team_link_clipboard").click(function(){navigator.clipboard.writeText($("#team_link_clipboard_data").val()).then(function(){$("#toast_content").text("Data copied to clipboard. User Ctrl+P to paste data.")},function(){$("#toast_content").text("Failure to copy. Check permissions for clipboard")});let e=new bootstrap.Toast(t);e.show()})}); +getUrlParameter=function t(e){var a,n,o=window.location.search.substring(1).split("&");for(n=0;n<o.length;n++)if((a=o[n].split("="))[0]===e)return void 0===a[1]||decodeURIComponent(a[1]);return!1},$(document).ready(function(){$("#date_range_picker").daterangepicker({opens:"left",locale:{firstDay:1}}),$("#date_range_picker").on("change",function(){$("#table_HR").DataTable().ajax.reload()}),$("#table_HR").DataTable({ajax:{url:$("#table_HR").data("source"),data:function(t){t.start=$("#date_range_picker").data("daterangepicker").startDate.format("YYYY-MM-DD"),t.end=$("#date_range_picker").data("daterangepicker").endDate.format("YYYY-MM-DD")}},autoWidth:!1,buttons:[{extend:"collection",text:"Export",buttons:["copy","csv",]}],dom:"Bfrtip",pageLength:25,footerCallback:function(t,e,a,n,o){var r=this.api(),c=function(t){return"string"==typeof t?1*t.replace(/[\$,]/g,""):"number"==typeof t?t:0};ob1=r.column(3).data().reduce(function(t,e){return c(t)+c(e)},0),ob1Total=r.column(3,{page:"current"}).data().reduce(function(t,e){return c(t)+c(e)},0),ob2=r.column(4).data().reduce(function(t,e){return c(t)+c(e)},0),ob2Total=r.column(4,{page:"current"}).data().reduce(function(t,e){return c(t)+c(e)},0),ob3=r.column(5).data().reduce(function(t,e){return c(t)+c(e)},0),ob3Total=r.column(5,{page:"current"}).data().reduce(function(t,e){return c(t)+c(e)},0),ob4=r.column(6).data().reduce(function(t,e){return c(t)+c(e)},0),ob4Total=r.column(6,{page:"current"}).data().reduce(function(t,e){return c(t)+c(e)},0),nwh=r.column(2).data().reduce(function(t,e){return c(t)+c(e)},0),nwhTotal=r.column(2,{page:"current"}).data().reduce(function(t,e){return c(t)+c(e)},0),vac=r.column(1).data().reduce(function(t,e){return c(t)+c(e)},0),vacTotal=r.column(1,{page:"current"}).data().reduce(function(t,e){return c(t)+c(e)},0),$(r.column(1).footer()).html(vacTotal.toFixed(2)+"d ("+vac.toFixed(2)+"d)"),$(r.column(2).footer()).html(nwhTotal.toFixed(2)+"h ("+nwh.toFixed(2)+"h)"),$(r.column(3).footer()).html(ob1Total.toFixed(2)+"h ("+ob1.toFixed(2)+"h)"),$(r.column(4).footer()).html(ob2Total.toFixed(2)+"h ("+ob2.toFixed(2)+"h)"),$(r.column(5).footer()).html(ob3Total.toFixed(2)+"h ("+ob3.toFixed(2)+"h)"),$(r.column(6).footer()).html(ob4Total.toFixed(2)+"h ("+ob4.toFixed(2)+"h)");var u=(ob1/600+ob2/450+ob3/300+ob4/150)*100;$("#helpOB").html(u.toFixed(1)+"%")}});let t=document.getElementById("live_toast");var e=getUrlParameter("ical");if(e){let a=document.querySelectorAll("#user_tabs button");a.forEach(t=>{let e=new bootstrap.Tab(t);t.addEventListener("click",t=>{t.preventDefault(),e.show()})});let n=document.querySelector('#user_tabs button[data-bs-target="#links-tab-pane"]');bootstrap.Tab.getInstance(n).show()}var e=getUrlParameter("swaps");if(e){let o=document.querySelectorAll("#user_tabs button");o.forEach(t=>{let e=new bootstrap.Tab(t);t.addEventListener("click",t=>{t.preventDefault(),e.show()})});let r=document.querySelector('#user_tabs button[data-bs-target="#shift-swap-tab-pane"]');bootstrap.Tab.getInstance(r).show()}var e=getUrlParameter("notifications");if(e){let c=document.querySelectorAll("#user_tabs button");c.forEach(t=>{let e=new bootstrap.Tab(t);t.addEventListener("click",t=>{t.preventDefault(),e.show()})});let u=document.querySelector('#user_tabs button[data-bs-target="#notify-tab-pane"]');bootstrap.Tab.getInstance(u).show()}$("#personal_link_clipboard").click(function(){navigator.clipboard.writeText($("#personal_link_clipboard_data").val()).then(function(){$("#toast_content").text("Data copied to clipboard. User Ctrl+P to paste data.")},function(){$("#toast_content").text("Failure to copy. Check permissions for clipboard")});let e=new bootstrap.Toast(t);e.show()}),$("#team_link_clipboard").click(function(){navigator.clipboard.writeText($("#team_link_clipboard_data").val()).then(function(){$("#toast_content").text("Data copied to clipboard. User Ctrl+P to paste data.")},function(){$("#toast_content").text("Failure to copy. Check permissions for clipboard")});let e=new bootstrap.Toast(t);e.show()})}); diff --git a/shifts/templates/shift_edit.html b/shifts/templates/shift_edit.html index b61e0497dfca67ed99ac9399b54cca8f7317a7b7..a94cf20be86f3b66597eaaedc88536f0cead3a8a 100644 --- a/shifts/templates/shift_edit.html +++ b/shifts/templates/shift_edit.html @@ -7,15 +7,39 @@ <div class="alert alert-primary" role="alert"> <h5>Edit/Update shift of </h5> <h2 class="display-5">{{ shift }}</h2> + <p>{{shift.revision}}</p> </div> + + {% if request.user.is_staff and edit and swap != None %} + <div class="alert alert-danger" role="alert"> + <h3>Status for the changes' entitlement as part of the <br> {{swap}}</h3> + <blockquote><a class="link" href="{% url 'shifter:shift-exchange-view' swap.id%}">Visit the corresponding shift exchange transaction.</a></blockquote> + <input class="form-check-input" type="checkbox" name="is_changed" + id="is_changed" {% if shift.is_changed %}checked{% endif %} disabled> + <label class="form-check-label" for="is_changed"> + Applicable for the ListChange? + </label> + <input class="form-check-input" type="checkbox" name="is_changed_urgent" + id="is_changed_urgent" {% if shift.is_changed_urgent %}checked{% endif %} disabled> + <label class="form-check-label" for="is_changed_urgent"> + Applicable for higher compensation? + </label> + <input class="form-check-input" type="checkbox" name="is_changed_offday" + id="is_changed_offday" {% if shift.is_changed_offday %}checked{% endif %} disabled> + <label class="form-check-label" for="is_changed_offday"> + Was that originally the assignee off day? + </label> + </div> + {% endif %} + {% if request.user.is_staff and edit %} + <div class="alert alert-danger" role="alert"> <form action="{% url 'shifter:shift-single-exchange-post' shift.id %}" method="POST" enctype="multipart/form-data" class="form-horizontal"> {% csrf_token %} - <div class="alert alert-danger" role="alert"> <h4 class="alert-heading">Re-assign Shift Member</h4> - <p>Note: Updating the member will create an <strong>approved ShiftExchange</strong> with respective notifications!</p> + <div class="mb-3"> - <label for="shiftMember" class="form-label">Select New Shift Member:</label> + <label for="shiftMember" class="form-label">Select New Shift Member: <p>Note: Updating the member will create an <strong>approved ShiftExchange</strong> with respective notifications!</p></label> <select class="form-select" name="shiftMember" id="shiftMember"> {% for sr in replacement %} <option value="{{ sr.id }}" {% if shift.member == sr %} selected {% endif %}>Shifter - {{ sr.name }}</option> @@ -35,9 +59,11 @@ </select> </div> <button class="btn btn-danger"><i class="fa-solid fa-user-tie fa-1x"></i> Update Shift Member</button> - - </div> + <br><br> </form> + <h4>Apply/modify special conditions </h4> + {% include 'shift_edit_special.html' with shift=shift style="danger"%} + </div> {% endif %} {% if edit %} @@ -66,7 +92,7 @@ </label> </div> <div class="form-check mb-3"> - <input class="form-check-input" type="checkbox" name="cancelledLastMinute" id="cancelledLastMinute" {% if shift.is_cancelled %}checked{% endif %}> + <input class="form-check-input" type="checkbox" name="cancelledLastMinute" id="cancelledLastMinute"> <label class="form-check-label" for="cancelledLastMinute"> Canceled last minute? </label> diff --git a/shifts/templates/shift_edit_special.html b/shifts/templates/shift_edit_special.html new file mode 100644 index 0000000000000000000000000000000000000000..fc9959b583d3179ef9927c105eaf73ca9c660ded --- /dev/null +++ b/shifts/templates/shift_edit_special.html @@ -0,0 +1,44 @@ + <button type="button" class="btn btn-{{style}}" data-bs-toggle="modal" data-bs-target="#updateShift{{shift.id}}"> + <i class="fa-solid fa-user-tie fa-1x"></i> <i class="fa-solid fa-wrench"></i> {{shift}} + </button> + <div class="modal fade" id="updateShift{{shift.id}}" tabindex="-1" aria-labelledby="updateShift" aria-hidden="true"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title" id="updateShiftLabel"><i class="fa-solid fa-user-tie fa-1x"></i> Update {{shift}}</h5> + <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> + </div> + <div class="modal-body"> + <form action="{% url 'shifter:shift-edit-special-post' shift.id %}" method="POST"> + {% csrf_token %} + <div class="form-check"> + <input class="form-check-input" type="checkbox" name="is_changed" + id="is_changed" {% if shift.is_changed %}checked{% endif %}> + <label class="form-check-label" for="is_changed"> + Applicable for the ListChange? + </label> + </div> + <div class="form-check"> + <input class="form-check-input" type="checkbox" name="is_changed_urgent" + id="is_changed_urgent" {% if shift.is_changed_urgent %}checked{% endif %}> + <label class="form-check-label" for="is_changed_urgent"> + Applicable for higher compensation, i.e. very last minute change? + </label> + </div> + <div class="form-check"> + <input class="form-check-input" type="checkbox" name="is_changed_offday" + id="is_changed_offday" {% if shift.is_changed_offday %}checked{% endif %}> + <label class="form-check-label" for="is_changed_offday"> + Was that originally the assignee free day, i.e. is shifter called to work from non-assigned day? + </label> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-light" data-bs-dismiss="modal">Close</button> + <a class="btn btn-light" href="{% url 'shifter:shift-edit' shift.id %}"><i class="fa-solid fa-user-tie fa-1x"></i> Go to shift edit</a> + <button type="submit" class="btn btn-warning"><i class="fa-solid fa-user-tie fa-1x"></i> Submit changes</button> + </div> + </form> + </div> + </div> + </div> + </div> diff --git a/shifts/templates/shiftexchange_edit.html b/shifts/templates/shiftexchange_edit.html index 96a92b5670a201c9c683957f31100aece7f79a0b..e68a7e25cad82b44fa3c76e95a0d4797a40874b7 100644 --- a/shifts/templates/shiftexchange_edit.html +++ b/shifts/templates/shiftexchange_edit.html @@ -2,7 +2,7 @@ {% block body %} {%if request.user.is_staff %} -<div class="container mb-5 pb-5"> +<div class="container mb-4 pb-4"> <div class="col"> <div class="empty-div"></div> <div class="card text-center"> @@ -15,9 +15,18 @@ <p class="card-text">The follwoing shifts were exchanged:</p> <hr> {% for onePair in sEx.shifts.all%} - <p> <small>[WAS]</small> {{onePair.shift}} <i class="fa-solid fa-arrows-left-right"></i> - <small>[IS now]</small> {{onePair.shift_for_exchange.member}}</p> - <p>[taker old shift] {{onePair.shift_for_exchange}}</p> + <p> <small>[WAS]</small> + <a class="btn btn-outline-secondary" href="{% url 'shifter:shift-edit' onePair.shift.id %}">{{onePair.shift}}</a> <i class="fa-solid fa-arrows-left-right"></i> + <small>[IS now]</small> + {% include 'shift_edit_special.html' with shift=newShiftRequestor style="success" %} + </p> + {% if newShiftRequested != None %} + <p><small>[WAS]</small> <a class="btn btn-outline-secondary" href="{% url 'shifter:shift-edit' onePair.shift_for_exchange.id %}">{{onePair.shift_for_exchange}}</a> + <i class="fa-solid fa-arrows-left-right"></i> + <small>[IS now]</small> + {% include 'shift_edit_special.html' with shift=newShiftRequested style="success" %} + </p> + {% endif %} <hr> {%endfor%} </div> diff --git a/shifts/templates/team_desiderata.html b/shifts/templates/team_desiderata.html index 9768376ee50bcf0ffd088e29310ad3105dbb93dd..f7153430b8add521ccaece9296a356af14282389 100644 --- a/shifts/templates/team_desiderata.html +++ b/shifts/templates/team_desiderata.html @@ -112,14 +112,19 @@ <table class="table table-striped" id="table_HR" data-source="{% url 'ajax.get_team_hr_codes' %}"> <thead> <tr> - <th scope="col">#</th> - <th scope="col">Member</th> - <th scope="col">OB1 hours</th> - <th scope="col">OB2 hours</th> - <th scope="col">OB3 hours</th> - <th scope="col">OB4 hours</th> - <th scope="col">NWH hours</th> - <th scope="col">Vacation</th> + <th scope="col">#</th> + <th scope="col">Member</th> + <th scope="col">Vac [d]</th> + <th scope="col">NWH [h]</th> + <th scope="col">OB1 [h]</th> + <th scope="col">OB2 [h]</th> + <th scope="col">OB3 [h]</th> + <th scope="col">OB4 [h]</th> + <th scope="col">BD [h]</th> + <th scope="col">ELC [1]</th> + <th scope="col">LC1 [1]</th> + <th scope="col">LC2 [1]</th> + <th scope="col">OT [h]</th> </tr> </thead> <tbody> @@ -127,14 +132,19 @@ </tbody> <tfoot> <tr> - <th></th> - <th></th> - <th></th> - <th></th> - <th></th> - <th></th> - <th></th> - <th></th> + <th></th> + <th></th> + <th></th> + <th></th> + <th></th> + <th></th> + <th></th> + <th></th> + <th></th> + <th></th> + <th></th> + <th></th> + <th></th> </tr> </tfoot> </table> @@ -245,7 +255,7 @@ <div class="form-group row"> <label for="shiftswap_date_range_picker" class="col-sm-2 col-form-label">Date-range :</label> <div class="col-sm-10"> - <input class="form-control" type="text" id="shiftswap_date_range_picker" name="daterange" value="{{ default_start | date:"m/d/y" }} - {{ default_end | date:"m/d/y" }}" /> + <input class="form-control" type="text" id="shiftswap_date_range_picker" name="daterange" value="{{ year_start | date:"m/d/y" }} - {{ year_end | date:"m/d/y" }}" /> </div> </div> </form> diff --git a/shifts/templates/user.html b/shifts/templates/user.html index a5e2e15caf3ebbdabbaec6c1f1d465de7d3f7393..13af2ea8be95a3bf484d2a95fd14ec07a75b89bd 100644 --- a/shifts/templates/user.html +++ b/shifts/templates/user.html @@ -110,7 +110,7 @@ <div class="row justify-content-center m-4"> <div class="col"> <h2 class="mb-3">HR Codes to report</h2> - <div class="alert alert-primary" role="alert"> + <div class="alert alert-success" role="alert"> <h4 class="alert-heading">Nice top up!</h4> <hr> <p> For the selected period, you may find <strong id="helpOB"></strong> more on your payslip due to your OBs! Enjoy!</p> @@ -128,24 +128,48 @@ </div> </form> + <p> + <a class="btn btn-secondary" data-bs-toggle="collapse" href="#collapseExample" role="button" aria-expanded="false" aria-controls="collapseExample"> + Show HR codes explanation + </a> + </p> + <div class="collapse" id="collapseExample"> + <div class="card card-body"> + <ul> + <li><strong>VAC</strong>, your vacation days, report usig VAC (in days) or CL/BL (in hours) </li> + <li><strong>OB1/OB2/OB3/OB4</strong> - regular unsocial working hours, </li> + <li><strong>NWH</strong> - normal working hours, usually office time, conference, training etc. </li> + <li><strong>BD for: BTP or BTB</strong> - Bridging day time pay or time bank, to be reported with the personal preference,</li> + <li><strong>ELC</strong> - one time (in 14days) entitlement for the List (Schedule) change,</li> + <li><strong>LC1/LC2</strong> - extra entiltement for a changed schedule, LC1 for if less than 2 days in advance, LC2 for day 3-14</li> + <li><strong>OT</strong> - Over time to be reported if the shift was done on a innitially free day. <strong>NOTE</strong> Mind reporting with the correct code: OTT1/2 or OTW1/2 depending on the time and preference.</li> + </ul> + </div> + </div> <div class="table-responsive"> <table class="table table-striped" id="table_HR" data-source="{% url 'ajax.get_hr_codes' %}"> <thead> <tr> <th scope="col">#</th> - <th scope="col">OB1 hours</th> - <th scope="col">OB2 hours</th> - <th scope="col">OB3 hours</th> - <th scope="col">OB4 hours</th> - <th scope="col">NWH hours</th> - <th scope="col">Vacation</th> + <th scope="col">Vac [d]</th> + <th scope="col">NWH [h]</th> + <th scope="col">OB1 [h]</th> + <th scope="col">OB2 [h]</th> + <th scope="col">OB3 [h]</th> + <th scope="col">OB4 [h]</th> + <th scope="col">BD [h]</th> + <th scope="col">ELC [1]</th> + <th scope="col">LC1 [1]</th> + <th scope="col">LC2 [1]</th> + <th scope="col">OT [h]</th> </tr> </thead> <tbody> <!-- filled by AJAX --> </tbody> <tfoot> + <!-- bottom of this table is filled by js--> <tr> <th></th> <th></th> @@ -154,6 +178,11 @@ <th></th> <th></th> <th></th> + <th></th> + <th></th> + <th></th> + <th></th> + <th></th> </tr> </tfoot> </table> diff --git a/shifts/urls/main.py b/shifts/urls/main.py index 527c5009a47043b1f403a8701edce81eca0f86a3..1bd29a4ea884be38b1f51afef172422a320469b5 100644 --- a/shifts/urls/main.py +++ b/shifts/urls/main.py @@ -25,6 +25,7 @@ urlpatterns = [ path("shift/<int:sid>", views.shift_edit, name="shift-edit"), path("shift/<int:sid>/view", views.shift_view, name="shift-view"), path("shift/<int:sid>/edit", views.shift_edit_post, name="shift-edit-post"), + path("shift/<int:sid>/edit-special", views.shift_edit_special_post, name="shift-edit-special-post"), path("shift-market/", views.market_shifts, name="shifts_market"), path("shift/create", views.shift_create, name="shifter.shift_create"), path("shift-market/<int:shift_id>/view/", views.market_shift_view, name="market_shift_view"), @@ -33,7 +34,7 @@ urlpatterns = [ path("shift/<int:shift_id>/add-to-market/", views.market_shift_add, name="add_shift_to_market"), path("shift/<int:sid>/exchange", views.shift_single_exchange_post, name="shift-single-exchange-post"), path("shift-exchange", views.shiftExchangeRequestCreateOrUpdate, name="shift-exchange-request"), - path("shift-exchange/<int:ex_id>", views.shiftExchangeRequestCreateOrUpdate, name="shift-exchange-request"), + path("shift-exchange/<int:ex_id>", views.shiftExchangeRequestCreateOrUpdate, name="shift-exchange-edit"), path("shift-exchange/<int:ex_id>/view", views.shiftExchangeView, name="shift-exchange-view"), path("shift-exchange/<int:ex_id>/close", views.shiftExchangeRequestClose, name="shift-exchange-close"), path("shift-exchange/<int:ex_id>/cancel", views.shiftExchangeRequestCancel, name="shift-exchange-cancel"), diff --git a/shifts/views/ajax.py b/shifts/views/ajax.py index 5722b9d2d9214498d1fb5e81ed8b75f24eda56ff..228195c4a6ca77895b20fb0555632af9adfc644a 100644 --- a/shifts/views/ajax.py +++ b/shifts/views/ajax.py @@ -266,11 +266,37 @@ def get_hr_codes(request: HttpRequest) -> HttpResponse: data = [] for date, item in shift2codes.items(): - a_row = [date, _trim4vis(item["OB1"]), _trim4vis(item["OB2"]), _trim4vis(item["OB3"]), _trim4vis(item["OB4"]), _trim4vis(item["NWH"]), ""] + a_row = [ + date[2:], + "", # vacation comes in the other line + _trim4vis(item["NWH"]), + _trim4vis(item["OB1"]), + _trim4vis(item["OB2"]), + _trim4vis(item["OB3"]), + _trim4vis(item["OB4"]), + _trim4vis(item["BTPorBTB"]), + _trim4vis(item["ELC"]), + _trim4vis(item["LC1"]), + _trim4vis(item["LC2"]), + _trim4vis(item["OT"]), + ] data.append(a_row) for date in dates: - a_row = [date, "", "", "", "", "", "1"] + a_row = [ + date[2:], + "1", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + ] data.append(a_row) return HttpResponse(json.dumps({"data": data}), content_type="application/json") @@ -317,17 +343,40 @@ def get_team_hr_codes(request: HttpRequest) -> HttpResponse: shift2codes = get_date_code_counts(scheduled_shifts) - def _check(date): - if date in dates: - return "1" - return - for date, item in shift2codes.items(): - a_row = [date, str(m), _trim4vis(item["OB1"]), _trim4vis(item["OB2"]), _trim4vis(item["OB3"]), _trim4vis(item["OB4"]), _trim4vis(item["NWH"]), ""] + a_row = [ + date[2:], + str(m), + "", # vacation comes in the other line + _trim4vis(item["NWH"]), + _trim4vis(item["OB1"]), + _trim4vis(item["OB2"]), + _trim4vis(item["OB3"]), + _trim4vis(item["OB4"]), + _trim4vis(item["BTPorBTB"]), + _trim4vis(item["ELC"]), + _trim4vis(item["LC1"]), + _trim4vis(item["LC2"]), + _trim4vis(item["OT"]), + ] data.append(a_row) for date in dates: - a_row = [date, str(m), "", "", "", "", "", "1"] + a_row = [ + date[2:], + str(m), + "1", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + ] data.append(a_row) return HttpResponse(json.dumps({"data": data}), content_type="application/json") diff --git a/shifts/views/desiderata.py b/shifts/views/desiderata.py index 2062095bb2b101efb6450cc8ef77348945d8f019..3855ef98a8476ed760b197d3f7b41ca38fd35cd6 100644 --- a/shifts/views/desiderata.py +++ b/shifts/views/desiderata.py @@ -31,6 +31,8 @@ def team_view(request: HttpRequest, team_id: int) -> HttpResponse: default_start = datetime.date(year, month, 1) endMonth = month + 1 if month < 12 else 1 default_end = datetime.date(year, endMonth, 1) + datetime.timedelta(days=-1) + year_start = datetime.date(year, month, 1) + year_end = year_start + datetime.timedelta(days=365) the_team = get_object_or_404(Team, id=team_id) logged_user = request.user @@ -44,6 +46,8 @@ def team_view(request: HttpRequest, team_id: int) -> HttpResponse: "team": the_team, "default_start": default_start, "default_end": default_end, + "year_start": year_start, + "year_end": year_end, "latest_revision": Revision.objects.filter(valid=True).order_by("-number").first(), "revisions": Revision.objects.order_by("-number"), "teamShifters": Member.objects.filter(team=the_team, is_active=True), diff --git a/shifts/views/main.py b/shifts/views/main.py index e7cf7ade8fc89c3f0073eba6f7dbbebbd0ccdec9..d69a9e0a1af8d07f3e635d7940938888faedd74d 100644 --- a/shifts/views/main.py +++ b/shifts/views/main.py @@ -20,7 +20,7 @@ from assets.forms import AssetBookingForm, AssetBookingFormClosing import datetime import phonenumbers from shifts.activeshift import prepare_active_crew, prepare_for_JSON -from shifts.contexts import prepare_default_context, prepare_user_context +from shifts.contexts import prepare_default_context, prepare_user_context, get_default_backup_revision from shifter.settings import DEFAULT_SHIFT_SLOT from shifts.workinghours import find_working_hours from shifts.exchanges import ( @@ -273,7 +273,34 @@ def shiftExchangeRequestCancel(request, ex_id=None): @login_required def shiftExchangeView(request, ex_id=None): sEx = ShiftExchange.objects.get(id=ex_id) - context = {"sEx": sEx} + r = Revision.objects.filter(valid=True).order_by("-number")[0] + + class PlaceHolder: + id = 0 + + def __repr__(self): + return "NO SHIFT" + + s = PlaceHolder() + s2 = PlaceHolder() + + for one in sEx.shifts.all(): + try: + s = Shift.objects.get(revision=r, member=one.shift.member, slot=one.shift_for_exchange.slot, date=one.shift_for_exchange.date) + except ObjectDoesNotExist: + pass + try: + s2 = Shift.objects.get(revision=r, member=one.shift_for_exchange.member, slot=one.shift.slot, date=one.shift.date) + except ObjectDoesNotExist: + pass + # TODO Fix the display issue for MORE than one shift in the swap + context = { + "sEx": sEx, + "oldShiftRequestor": one.shift, + "oldShiftRequested": one.shift_for_exchange, + "newShiftRequestor": s2, + "newShiftRequested": None if isinstance(s, PlaceHolder) else s, + } return render(request, "shiftexchange_edit.html", prepare_default_context(request, context)) @@ -295,7 +322,8 @@ def shiftExchangeRequestCreateOrUpdate(request, ex_id=None): sEx = ShiftExchange() sEx.requestor = request.user sEx.approver = otherShift.member - sEx.backupRevision = Revision.objects.filter(number=1).first() + sEx.backupRevision = get_default_backup_revision() + # sEx.backupRevision = Revision.objects.filter(number=1).first() sEx.requested = timezone.now() sEx.save() sPair.save() @@ -672,9 +700,26 @@ def shifts_update_status_post(request): def shift_edit(request, sid=None): s = Shift.objects.get(id=sid) sfm = ShiftForMarket.objects.filter(shift=s).first() + swap = None + + sExPairAsIs = ShiftExchangePair.objects.filter(Q(shift=s) | Q(shift_for_exchange=s)) + ss = Shift.objects.filter(revision=get_default_backup_revision(), slot=s.slot, date=s.date) + sExPairExplicitBackup = ShiftExchangePair.objects.filter(Q(shift__in=ss) | Q(shift_for_exchange__in=ss)) + # print("old exPairs AS IS : ", sExPairAsIs) + # print("old exPairs BACKUP: ", sExPairExplicitBackup) + if len(sExPairAsIs) or len(sExPairExplicitBackup): + try: + swap = ShiftExchange.objects.get(shifts__in=sExPairAsIs) + print(swap) + except: + swap = ShiftExchange.objects.get(shifts__in=sExPairExplicitBackup) + print(swap) + # TODO in some complicated swaps the above may trigger 'returned 2' + data = { "shift": s, "on_market": sfm, + "swap": swap, "shiftRoles": ShiftRole.objects.all(), "replacement": Member.objects.filter(team=request.user.team, is_active=True).order_by("role"), "edit": True, @@ -718,6 +763,25 @@ def shift_edit_post(request, sid=None): return HttpResponseRedirect(reverse("shifter:index")) +@require_http_methods(["POST"]) +@csrf_protect +@login_required +def shift_edit_special_post(request, sid=None): + print(request.POST) + s = Shift.objects.get(id=sid) + s.is_changed_urgent = "is_changed_urgent" in request.POST + s.is_changed = "is_changed" in request.POST + s.is_changed_offday = "is_changed_offday" in request.POST + s.save() + print(s) + print(s.is_changed) + print(s.is_changed_offday) + print(s.is_changed_urgent) + url = reverse("shifter:shift-edit", kwargs={"sid": sid}) + messages.success(request, "<a class='href' href='{}'> Shift {} updated successfully! Edit again?</a>".format(url, s)) + return HttpResponseRedirect(url) + + @require_http_methods(["POST"]) @csrf_protect @login_required @@ -834,7 +898,7 @@ def market_shift_take(request, shift_id): request.user, requestor=shift_market.offerer, approver=request.user, - revisionBackup=Revision.objects.filter(name__startswith="BACKUP").first(), + revisionBackup=get_default_backup_revision(), exType="Market", ) notificationService.notify(sExDone) @@ -869,12 +933,13 @@ def user_page(request): def shift_single_exchange_post(request, sid=None): s = Shift.objects.get(id=sid) m = Member.objects.get(id=int(request.POST["shiftMember"])) + shift_slot = s.slot + shift_date = s.date shift_date_str = request.POST.get("shiftDate") if shift_date_str: try: - shift_date = datetime.datetime.strptime(shift_date_str, "%Y-%m-%d").date() - s.date = shift_date + shift_date = datetime.datetime.strptime(shift_date_str, DATE_FORMAT).date() except ValueError: messages.error(request, "Invalid date format for shift date.") return HttpResponseRedirect(reverse("shifter:index")) @@ -883,15 +948,19 @@ def shift_single_exchange_post(request, sid=None): if shift_slot_id: try: shift_slot = Slot.objects.get(id=int(shift_slot_id)) - s.slot = shift_slot except (ValueError, Slot.DoesNotExist): messages.error(request, "Invalid slot selected.") return HttpResponseRedirect(reverse("shifter:index")) + if s.member == m and s.date == shift_date and s.slot == shift_slot: + messages.info(request, "No change applied, no update performed!") + return HttpResponseRedirect(reverse("shifter:index")) + + s.member = m + s.slot = shift_slot + s.date = shift_date s.save() - sExDone = perform_simplified_exchange_and_save_backup( - s, m, requestor=request.user, approver=request.user, revisionBackup=Revision.objects.filter(name__startswith="BACKUP").first() - ) + sExDone = perform_simplified_exchange_and_save_backup(s, m, requestor=request.user, approver=request.user, revisionBackup=get_default_backup_revision()) messages.success(request, "Requested shift exchange (update) is now successfully implemented.") notificationService.notify(sExDone) diff --git a/tests/test_hrcodes.py b/tests/test_hrcodes.py index 28f80c604dcbec116c9e63208dfe1baec9b8f02d..57b9ee3d8d535393a671311b82113515d0361101 100644 --- a/tests/test_hrcodes.py +++ b/tests/test_hrcodes.py @@ -173,4 +173,4 @@ class HRCodes(TestCase): def compare(self, codes1, codes2): for oneCode in codes1.keys(): - self.assertEqual(codes1.get(oneCode), codes2.get(oneCode)) + self.assertEqual(codes1.get(oneCode, 0), codes2.get(oneCode, 0))