Make it possible to manually re-assign chores (closes #1492, references #1830)

This commit is contained in:
Bernd Bestel
2022-04-03 13:56:14 +02:00
parent a5294262e6
commit 3efecb8bed
8 changed files with 232 additions and 63 deletions

View File

@@ -39,7 +39,7 @@
### Shopping list ### Shopping list
- Added a new shopping list setting (top right corner settings menu) to automatically add products, that are below their defined min. stock amount, to the shopping list - Added a new shopping list setting (top right corner settings menu) to automatically add products, that are below their defined min. stock amount, to the shopping list (defaults to disabled)
- Fixed that when using "Add products that are below defined min. stock amount", the calculated missing amount was wrong for products which had the new product option `Treat opened as out of stock` set and when having at least one opened stock entry - Fixed that when using "Add products that are below defined min. stock amount", the calculated missing amount was wrong for products which had the new product option `Treat opened as out of stock` set and when having at least one opened stock entry
### Recipes ### Recipes
@@ -61,7 +61,7 @@
- The `Daily` period type has been changed to schedule the chore at the _same time_ (based on the start date) each `n` days - 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 - 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 - It's now possible to manually reschedule / assign chores
- New entry "Reschedule next execution" in the context/more menu on the chores overview page - 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 - 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" - Rescheduled chores will be highlighted with an corresponding icon next to the "next estiamted tracking date"

View File

@@ -98,7 +98,7 @@ class ChoresController extends BaseController
$currentChores = $this->getChoresService()->GetCurrent(); $currentChores = $this->getChoresService()->GetCurrent();
foreach ($currentChores as $currentChore) foreach ($currentChores as $currentChore)
{ {
if (FindObjectInArrayByPropertyValue($chores, 'id', $currentChore->chore_id)->period_type !== \Grocy\Services\ChoresService::CHORE_PERIOD_TYPE_MANUALLY) if (!empty($currentChore->next_estimated_execution_time))
{ {
if ($currentChore->next_estimated_execution_time < date('Y-m-d H:i:s')) if ($currentChore->next_estimated_execution_time < date('Y-m-d H:i:s'))
{ {

View File

@@ -5246,6 +5246,9 @@
"type": "string", "type": "string",
"format": "date-time" "format": "date-time"
}, },
"rescheduled_next_execution_assigned_to_user_id": {
"type": "integer"
},
"row_created_timestamp": { "row_created_timestamp": {
"type": "string", "type": "string",
"format": "date-time" "format": "date-time"
@@ -5516,6 +5519,12 @@
"next_execution_assigned_to_user_id": { "next_execution_assigned_to_user_id": {
"type": "integer" "type": "integer"
}, },
"is_rescheduled": {
"type": "boolean"
},
"is_reassigned": {
"type": "boolean"
},
"next_execution_assigned_user": { "next_execution_assigned_user": {
"$ref": "#/components/schemas/UserDto" "$ref": "#/components/schemas/UserDto"
} }

View File

@@ -2353,3 +2353,6 @@ msgstr ""
msgid "Automatically add products that are below their defined min. stock amount to the shopping list" msgid "Automatically add products that are below their defined min. stock amount to the shopping list"
msgstr "" msgstr ""
msgid "Reassigned"
msgstr ""

83
migrations/0185.sql Normal file
View File

@@ -0,0 +1,83 @@
ALTER TABLE chores
ADD rescheduled_next_execution_assigned_to_user_id INT;
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,
CASE WHEN IFNULL(x.rescheduled_next_execution_assigned_to_user_id, '') != '' THEN 1 ELSE 0 END AS is_reassigned
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 AND h.period_type != 'manually' THEN
h.start_date
ELSE
CASE h.period_type
WHEN 'manually' THEN NULL
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,
h.rescheduled_next_execution_assigned_to_user_id
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;

View File

@@ -152,18 +152,28 @@ $(document).on('click', '.track-chore-button', function(e)
$('#chore-' + choreId + '-last-tracked-time').text(trackedTime); $('#chore-' + choreId + '-last-tracked-time').text(trackedTime);
$('#chore-' + choreId + '-last-tracked-time-timeago').attr('datetime', trackedTime); $('#chore-' + choreId + '-last-tracked-time-timeago').attr('datetime', trackedTime);
if (result.chore.period_type != "manually") if (result.next_estimated_execution_time != null && !result.next_estimated_execution_time.isEmpty())
{ {
$('#chore-' + choreId + '-next-execution-time').text(result.next_estimated_execution_time); $('#chore-' + choreId + '-next-execution-time').text(result.next_estimated_execution_time);
$('#chore-' + choreId + '-next-execution-time-timeago').attr('datetime', result.next_estimated_execution_time); $('#chore-' + choreId + '-next-execution-time-timeago').attr('datetime', result.next_estimated_execution_time);
} }
else
{
$('#chore-' + choreId + '-next-execution-time').text("-");
$('#chore-' + choreId + '-next-execution-time-timeago').removeAttr('datetime');
}
if (result.chore.next_execution_assigned_to_user_id != null) if (result.chore.next_execution_assigned_to_user_id != null)
{ {
$('#chore-' + choreId + '-next-execution-assigned-user').text(result.next_execution_assigned_user.display_name); $('#chore-' + choreId + '-next-execution-assigned-user').text(result.next_execution_assigned_user.display_name);
} }
else
{
$('#chore-' + choreId + '-next-execution-assigned-user').text("-");
}
$('#chore-' + choreId + '-reschedule-icon').remove(); $('#chore-' + choreId + '-rescheduled-icon').remove();
$('#chore-' + choreId + '-reassigned-icon').remove();
Grocy.FrontendHelpers.EndUiBusy(); Grocy.FrontendHelpers.EndUiBusy();
toastr.success(__t('Tracked execution of chore %1$s on %2$s', choreName, trackedTime)); toastr.success(__t('Tracked execution of chore %1$s on %2$s', choreName, trackedTime));
@@ -280,7 +290,7 @@ $(document).on("click", ".reschedule-chore-button", function(e)
Grocy.EditObjectId = choreId; Grocy.EditObjectId = choreId;
Grocy.Api.Get("chores/" + choreId, function(choreDetails) Grocy.Api.Get("chores/" + choreId, function(choreDetails)
{ {
var prefillDate = choreDetails.next_estimated_execution_time; var prefillDate = choreDetails.next_estimated_execution_time || moment().format("YYYY-MM-DD HH:mm:ss");
if (choreDetails.chore.rescheduled_date != null && !choreDetails.chore.rescheduled_date.isEmpty()) if (choreDetails.chore.rescheduled_date != null && !choreDetails.chore.rescheduled_date.isEmpty())
{ {
prefillDate = choreDetails.chore.rescheduled_date; prefillDate = choreDetails.chore.rescheduled_date;
@@ -297,6 +307,16 @@ $(document).on("click", ".reschedule-chore-button", function(e)
Grocy.Components.DateTimePicker.SetValue(moment(prefillDate).format("YYYY-MM-DD HH:mm:ss")); Grocy.Components.DateTimePicker.SetValue(moment(prefillDate).format("YYYY-MM-DD HH:mm:ss"));
} }
if (choreDetails.chore.next_execution_assigned_to_user_id != null && !choreDetails.chore.next_execution_assigned_to_user_id.isEmpty())
{
Grocy.Components.UserPicker.SetId(choreDetails.chore.next_execution_assigned_to_user_id)
}
else
{
Grocy.Components.UserPicker.SetValue("");
Grocy.Components.UserPicker.SetId(null);
}
$("#reschedule-chore-modal-title").text(choreDetails.chore.name); $("#reschedule-chore-modal-title").text(choreDetails.chore.name);
$("#reschedule-chore-modal").modal("show"); $("#reschedule-chore-modal").modal("show");
}); });
@@ -311,12 +331,21 @@ $("#reschedule-chore-save-button").on("click", function(e)
return; return;
} }
Grocy.Api.Put('objects/chores/' + Grocy.EditObjectId, { "rescheduled_date": Grocy.Components.DateTimePicker.GetValue() }, Grocy.Api.Put('objects/chores/' + Grocy.EditObjectId, { "rescheduled_date": Grocy.Components.DateTimePicker.GetValue(), "rescheduled_next_execution_assigned_to_user_id": Grocy.Components.UserPicker.GetValue() },
function(result)
{
Grocy.Api.Post('chores/executions/calculate-next-assignments', { "chore_id": Grocy.EditObjectId },
function(result) function(result)
{ {
window.location.reload(); window.location.reload();
}, },
function(xhr) function(xhr)
{
console.error(xhr);
}
);
},
function(xhr)
{ {
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response) Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response)
} }
@@ -327,12 +356,21 @@ $("#reschedule-chore-clear-button").on("click", function(e)
{ {
e.preventDefault(); e.preventDefault();
Grocy.Api.Put('objects/chores/' + Grocy.EditObjectId, { "rescheduled_date": null }, Grocy.Api.Put('objects/chores/' + Grocy.EditObjectId, { "rescheduled_date": null, "rescheduled_next_execution_assigned_to_user_id": null },
function(result)
{
Grocy.Api.Post('chores/executions/calculate-next-assignments', { "chore_id": Grocy.EditObjectId },
function(result) function(result)
{ {
window.location.reload(); window.location.reload();
}, },
function(xhr) function(xhr)
{
console.error(xhr);
}
);
},
function(xhr)
{ {
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response) Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response)
} }

View File

@@ -34,6 +34,13 @@ class ChoresService extends BaseService
} }
$chore = $this->getDatabase()->chores($choreId); $chore = $this->getDatabase()->chores($choreId);
if (!empty($chore->rescheduled_next_execution_assigned_to_user_id))
{
$nextExecutionUserId = $chore->rescheduled_next_execution_assigned_to_user_id;
}
else
{
$choreLastTrackedTime = $this->getDatabase()->chores_log()->where('chore_id = :1 AND undone = 0', $choreId)->max('tracked_time'); $choreLastTrackedTime = $this->getDatabase()->chores_log()->where('chore_id = :1 AND undone = 0', $choreId)->max('tracked_time');
$lastChoreLogRow = $this->getDatabase()->chores_log()->where('chore_id = :1 AND tracked_time = :2 AND undone = 0', $choreId, $choreLastTrackedTime)->orderBy('row_created_timestamp', 'DESC')->fetch(); $lastChoreLogRow = $this->getDatabase()->chores_log()->where('chore_id = :1 AND tracked_time = :2 AND undone = 0', $choreId, $choreLastTrackedTime)->orderBy('row_created_timestamp', 'DESC')->fetch();
$lastDoneByUserId = $lastChoreLogRow->done_by_user_id; $lastDoneByUserId = $lastChoreLogRow->done_by_user_id;
@@ -96,6 +103,7 @@ class ChoresService extends BaseService
$nextExecutionUserId = $row->user_id; $nextExecutionUserId = $row->user_id;
} }
} }
}
$chore->update([ $chore->update([
'next_execution_assigned_to_user_id' => $nextExecutionUserId 'next_execution_assigned_to_user_id' => $nextExecutionUserId
@@ -197,8 +205,6 @@ class ChoresService extends BaseService
$logRow->save(); $logRow->save();
$lastInsertId = $this->getDatabase()->lastInsertId(); $lastInsertId = $this->getDatabase()->lastInsertId();
$this->CalculateNextExecutionAssignment($choreId);
if ($chore->consume_product_on_execution == 1 && !empty($chore->product_id)) if ($chore->consume_product_on_execution == 1 && !empty($chore->product_id))
{ {
$transactionId = uniqid(); $transactionId = uniqid();
@@ -212,6 +218,15 @@ class ChoresService extends BaseService
]); ]);
} }
if (!empty($chore->rescheduled_next_execution_assigned_to_user_id))
{
$chore->update([
'rescheduled_next_execution_assigned_to_user_id' => null
]);
}
$this->CalculateNextExecutionAssignment($choreId);
return $lastInsertId; return $lastInsertId;
} }

View File

@@ -165,7 +165,7 @@
<i class="fas fa-ellipsis-v"></i> <i class="fas fa-ellipsis-v"></i>
</button> </button>
<div class="table-inline-menu dropdown-menu dropdown-menu-right"> <div class="table-inline-menu dropdown-menu dropdown-menu-right">
<a class="dropdown-item reschedule-chore-button @if(FindObjectInArrayByPropertyValue($chores, 'id', $curentChoreEntry->chore_id)->period_type == \Grocy\Services\ChoresService::CHORE_PERIOD_TYPE_MANUALLY) disabled @endif" <a class="dropdown-item reschedule-chore-button"
data-chore-id="{{ $curentChoreEntry->chore_id }}" data-chore-id="{{ $curentChoreEntry->chore_id }}"
type="button" type="button"
href="#"> href="#">
@@ -210,16 +210,18 @@
{{ FindObjectInArrayByPropertyValue($chores, 'id', $curentChoreEntry->chore_id)->name }} {{ FindObjectInArrayByPropertyValue($chores, 'id', $curentChoreEntry->chore_id)->name }}
</td> </td>
<td> <td>
@if(FindObjectInArrayByPropertyValue($chores, 'id', $curentChoreEntry->chore_id)->period_type !== \Grocy\Services\ChoresService::CHORE_PERIOD_TYPE_MANUALLY) @if(!empty($curentChoreEntry->next_estimated_execution_time))
<span id="chore-{{ $curentChoreEntry->chore_id }}-next-execution-time">{{ $curentChoreEntry->next_estimated_execution_time }}</span> <span id="chore-{{ $curentChoreEntry->chore_id }}-next-execution-time">{{ $curentChoreEntry->next_estimated_execution_time }}</span>
<time id="chore-{{ $curentChoreEntry->chore_id }}-next-execution-time-timeago" <time id="chore-{{ $curentChoreEntry->chore_id }}-next-execution-time-timeago"
class="timeago timeago-contextual @if($curentChoreEntry->track_date_only == 1) timeago-date-only @endif" class="timeago timeago-contextual @if($curentChoreEntry->track_date_only == 1) timeago-date-only @endif"
datetime="{{ $curentChoreEntry->next_estimated_execution_time }}"></time> datetime="{{ $curentChoreEntry->next_estimated_execution_time }}"></time>
@else @else
<span>-</span> <span id="chore-{{ $curentChoreEntry->chore_id }}-next-execution-time">-</span>
<time id="chore-{{ $curentChoreEntry->chore_id }}-next-execution-time-timeago"
class="timeago timeago-contextual @if($curentChoreEntry->track_date_only == 1) timeago-date-only @endif"></time>
@endif @endif
@if($curentChoreEntry->is_rescheduled == 1) @if($curentChoreEntry->is_rescheduled == 1)
<span id="chore-{{ $curentChoreEntry->chore_id }}-reschedule-icon" <span id="chore-{{ $curentChoreEntry->chore_id }}-rescheduled-icon"
class="text-muted" class="text-muted"
data-toggle="tooltip" data-toggle="tooltip"
title="{{ $__t('Rescheduled') }}"> title="{{ $__t('Rescheduled') }}">
@@ -228,10 +230,16 @@
@endif @endif
</td> </td>
<td> <td>
@if(!empty($curentChoreEntry->last_tracked_time))
<span id="chore-{{ $curentChoreEntry->chore_id }}-last-tracked-time">{{ $curentChoreEntry->last_tracked_time }}</span> <span id="chore-{{ $curentChoreEntry->chore_id }}-last-tracked-time">{{ $curentChoreEntry->last_tracked_time }}</span>
<time id="chore-{{ $curentChoreEntry->chore_id }}-last-tracked-time-timeago" <time id="chore-{{ $curentChoreEntry->chore_id }}-last-tracked-time-timeago"
class="timeago timeago-contextual @if($curentChoreEntry->track_date_only == 1) timeago-date-only @endif" class="timeago timeago-contextual @if($curentChoreEntry->track_date_only == 1) timeago-date-only @endif"
datetime="{{ $curentChoreEntry->last_tracked_time }}"></time> datetime="{{ $curentChoreEntry->last_tracked_time }}"></time>
@else
<span id="chore-{{ $curentChoreEntry->chore_id }}-last-tracked-time">-</span>
<time id="chore-{{ $curentChoreEntry->chore_id }}-last-tracked-time-timeago"
class="timeago timeago-contextual @if($curentChoreEntry->track_date_only == 1) timeago-date-only @endif"></time>
@endif
</td> </td>
<td class="@if(!GROCY_FEATURE_FLAG_CHORES_ASSIGNMENTS) d-none @endif"> <td class="@if(!GROCY_FEATURE_FLAG_CHORES_ASSIGNMENTS) d-none @endif">
@@ -241,6 +249,14 @@
@else @else
<span>-</span> <span>-</span>
@endif @endif
@if($curentChoreEntry->is_reassigned == 1)
<span id="chore-{{ $curentChoreEntry->chore_id }}-reassigned-icon"
class="text-muted"
data-toggle="tooltip"
title="{{ $__t('Reassigned') }}">
<i class="fas fa-exchange-alt"></i>
</span>
@endif
</span> </span>
</td> </td>
<td id="chore-{{ $curentChoreEntry->chore_id }}-due-filter-column" <td id="chore-{{ $curentChoreEntry->chore_id }}-due-filter-column"
@@ -309,6 +325,11 @@
'invalidFeedback' => $__t('This can only be in the future') 'invalidFeedback' => $__t('This can only be in the future')
)) ))
@include('components.userpicker', array(
'label' => 'Assigned to',
'users' => $users
))
</form> </form>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">