diff --git a/changelog/67_UNRELEASED_xxxx-xx-xx.md b/changelog/67_UNRELEASED_xxxx-xx-xx.md index 36d1e9fc..78f2fa92 100644 --- a/changelog/67_UNRELEASED_xxxx-xx-xx.md +++ b/changelog/67_UNRELEASED_xxxx-xx-xx.md @@ -33,6 +33,10 @@ - The `Daily` period type has been changed to schedule the chore at the _same time_ (based on the start date) each `n` days - This period type scheduled chores `n` days _after the last execution_ before, which is also possible by using the `Hourly` period type and a corresponding period interval; all existing `Daily` schedules will be converted to that on migration +- It's now possible to manually reschedule chores + - New entry "Reschedule next execution" in the context/more menu on the chores overview page + - If you have rescheduled a chore and want to continue the normal schedule instead, use the "Clear" button in the dialog + - Rescheduled chores will be highlighted with an corresponding icon next to the "next estiamted tracking date" - Optimized that when skipping chores via the chore tracking page, the given time is used as the "skipped time", not the scheduled next estimated tracking time of the corresponding chore (essentially making it possible to skip more then one schedule at once) - Fixed that when consuming a parent product on chore execution (chore option "Consume product on chore execution"), no child products were used if the parent product itself is not in-stock - Fixed that the upgrade to v3.2.0 failed when having any former "Dynamic Regular" chore with a "Period interval" of `0` (which makes absolutely no sense in reality) diff --git a/localization/strings.pot b/localization/strings.pot index 4b17ab64..7fc0abda 100644 --- a/localization/strings.pot +++ b/localization/strings.pot @@ -2305,3 +2305,12 @@ msgstr "" msgid "Average execution frequency" msgstr "" + +msgid "Reschedule next execution" +msgstr "" + +msgid "This can only be in the future" +msgstr "" + +msgid "Rescheduled" +msgstr "" diff --git a/migrations/0176.sql b/migrations/0176.sql new file mode 100644 index 00000000..cfaac7cc --- /dev/null +++ b/migrations/0176.sql @@ -0,0 +1,81 @@ +ALTER TABLE chores +ADD rescheduled_date DATETIME; + +DROP VIEW chores_current; +CREATE VIEW chores_current +AS +SELECT + x.chore_id AS id, -- Dummy, LessQL needs an id column + x.chore_id, + x.chore_name, + x.last_tracked_time, + CASE WHEN x.rollover = 1 AND DATETIME('now', 'localtime') > x.next_estimated_execution_time THEN + CASE WHEN IFNULL(x.track_date_only, 0) = 1 THEN + DATETIME(STRFTIME('%Y-%m-%d', DATETIME('now', 'localtime')) || ' 23:59:59') + ELSE + DATETIME(STRFTIME('%Y-%m-%d', DATETIME('now', 'localtime')) || ' ' || STRFTIME('%H:%M:%S', x.next_estimated_execution_time)) + END + ELSE + CASE WHEN IFNULL(x.track_date_only, 0) = 1 THEN + DATETIME(STRFTIME('%Y-%m-%d', x.next_estimated_execution_time) || ' 23:59:59') + ELSE + x.next_estimated_execution_time + END + END AS next_estimated_execution_time, + x.track_date_only, + x.next_execution_assigned_to_user_id, + CASE WHEN IFNULL(x.rescheduled_date, '') != '' THEN 1 ELSE 0 END AS is_rescheduled +FROM ( + +SELECT + h.id AS chore_id, + h.name AS chore_name, + MAX(l.tracked_time) AS last_tracked_time, + CASE WHEN IFNULL(h.rescheduled_date, '') != '' THEN + h.rescheduled_date + ELSE + CASE WHEN MAX(l.tracked_time) IS NULL THEN + h.start_date + ELSE + CASE h.period_type + WHEN 'manually' THEN '2999-12-31 23:59:59' + WHEN 'hourly' THEN DATETIME(MAX(l.tracked_time), '+' || CAST(h.period_interval AS TEXT) || ' hour') + WHEN 'daily' THEN DATETIME(SUBSTR(CAST(DATETIME(MAX(l.tracked_time), '+' || CAST(h.period_interval AS TEXT) || ' days') AS TEXT), 1, 11) || SUBSTR(CAST(h.start_date AS TEXT), -8)) + WHEN 'weekly' THEN ( + SELECT next + FROM ( + SELECT 'sunday' AS day, DATETIME((SELECT tracked_time FROM chores_log WHERE chore_id = h.id ORDER BY tracked_time DESC LIMIT 1), '1 days', '+' || CAST((h.period_interval - 1) * 7 AS TEXT) || ' days', 'weekday 0') AS next + UNION + SELECT 'monday' AS day, DATETIME((SELECT tracked_time FROM chores_log WHERE chore_id = h.id ORDER BY tracked_time DESC LIMIT 1), '1 days', '+' || CAST((h.period_interval - 1) * 7 AS TEXT) || ' days', 'weekday 1') AS next + UNION + SELECT 'tuesday' AS day, DATETIME((SELECT tracked_time FROM chores_log WHERE chore_id = h.id ORDER BY tracked_time DESC LIMIT 1), '1 days', '+' || CAST((h.period_interval - 1) * 7 AS TEXT) || ' days', 'weekday 2') AS next + UNION + SELECT 'wednesday' AS day, DATETIME((SELECT tracked_time FROM chores_log WHERE chore_id = h.id ORDER BY tracked_time DESC LIMIT 1), '1 days', '+' || CAST((h.period_interval - 1) * 7 AS TEXT) || ' days', 'weekday 3') AS next + UNION + SELECT 'thursday' AS day, DATETIME((SELECT tracked_time FROM chores_log WHERE chore_id = h.id ORDER BY tracked_time DESC LIMIT 1), '1 days', '+' || CAST((h.period_interval - 1) * 7 AS TEXT) || ' days', 'weekday 4') AS next + UNION + SELECT 'friday' AS day, DATETIME((SELECT tracked_time FROM chores_log WHERE chore_id = h.id ORDER BY tracked_time DESC LIMIT 1), '1 days', '+' || CAST((h.period_interval - 1) * 7 AS TEXT) || ' days', 'weekday 5') AS next + UNION + SELECT 'saturday' AS day, DATETIME((SELECT tracked_time FROM chores_log WHERE chore_id = h.id ORDER BY tracked_time DESC LIMIT 1), '1 days', '+' || CAST((h.period_interval - 1) * 7 AS TEXT) || ' days', 'weekday 6') AS next + ) + WHERE INSTR(period_config, day) > 0 + ORDER BY next + LIMIT 1 + ) + WHEN 'monthly' THEN DATETIME(MAX(l.tracked_time), 'start of month', '+' || CAST(h.period_interval AS TEXT) || ' month', '+' || CAST(h.period_days - 1 AS TEXT) || ' day') + WHEN 'yearly' THEN DATETIME(SUBSTR(CAST(DATETIME(MAX(l.tracked_time), '+' || CAST(h.period_interval AS TEXT) || ' years') AS TEXT), 1, 4) || SUBSTR(CAST(h.start_date AS TEXT), 5, 6) || SUBSTR(CAST(DATETIME(MAX(l.tracked_time), '+' || CAST(h.period_interval AS TEXT) || ' years') AS TEXT), -9)) + WHEN 'adaptive' THEN DATETIME(MAX(l.tracked_time), '+' || CAST(IFNULL((SELECT average_frequency_hours FROM chores_execution_average_frequency WHERE chore_id = h.id), 0) AS TEXT) || ' hour') + END + END + END AS next_estimated_execution_time, + h.track_date_only, + h.rollover, + h.next_execution_assigned_to_user_id, + h.rescheduled_date +FROM chores h +LEFT JOIN chores_log l + ON h.id = l.chore_id + AND l.undone = 0 +WHERE h.active = 1 +GROUP BY h.id, h.name, h.period_days +) x; diff --git a/public/viewjs/choresoverview.js b/public/viewjs/choresoverview.js index d9dd783c..115ee278 100644 --- a/public/viewjs/choresoverview.js +++ b/public/viewjs/choresoverview.js @@ -263,6 +263,74 @@ function RefreshStatistics() ); } +$(document).on("click", ".reschedule-chore-button", function(e) +{ + e.preventDefault(); + + var choreId = $(e.currentTarget).attr("data-chore-id"); + Grocy.EditObjectId = choreId; + Grocy.Api.Get("chores/" + choreId, function(choreDetails) + { + var prefillDate = choreDetails.next_estimated_execution_time; + if (choreDetails.chore.rescheduled_date != null && !choreDetails.chore.rescheduled_date.isEmpty()) + { + prefillDate = choreDetails.chore.rescheduled_date; + } + + if (choreDetails.chore.track_date_only == 1) + { + Grocy.Components.DateTimePicker.ChangeFormat("YYYY-MM-DD"); + Grocy.Components.DateTimePicker.SetValue(moment(prefillDate).format("YYYY-MM-DD")); + } + else + { + Grocy.Components.DateTimePicker.ChangeFormat("YYYY-MM-DD HH:mm:ss"); + Grocy.Components.DateTimePicker.SetValue(moment(prefillDate).format("YYYY-MM-DD HH:mm:ss")); + } + + $("#reschedule-chore-modal-title").text(choreDetails.chore.name); + $("#reschedule-chore-modal").modal("show"); + }); +}); + +$("#reschedule-chore-save-button").on("click", function(e) +{ + e.preventDefault(); + + if (!Grocy.FrontendHelpers.ValidateForm("reschedule-chore-form", true)) + { + return; + } + + Grocy.Api.Put('objects/chores/' + Grocy.EditObjectId, { "rescheduled_date": Grocy.Components.DateTimePicker.GetValue() }, + function(result) + { + window.location.reload(); + }, + function(xhr) + { + Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response) + } + ); +}); + +$("#reschedule-chore-clear-button").on("click", function(e) +{ + e.preventDefault(); + + Grocy.Api.Put('objects/chores/' + Grocy.EditObjectId, { "rescheduled_date": null }, + function(result) + { + window.location.reload(); + }, + function(xhr) + { + Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response) + } + ); +}); + + if (GetUriParam("user") !== undefined) { $("#user-filter").val("xx" + GetUriParam("user") + "xx"); diff --git a/views/choresoverview.blade.php b/views/choresoverview.blade.php index 45b45ae3..3da10cac 100644 --- a/views/choresoverview.blade.php +++ b/views/choresoverview.blade.php @@ -165,6 +165,13 @@ + + @stop