mirror of
https://github.com/grocy/grocy.git
synced 2025-04-29 17:45:39 +00:00
Added a new "adaptive" chore period type (closes #1495)
This commit is contained in:
parent
091a93ff4e
commit
69a7ea6057
@ -47,12 +47,14 @@
|
|||||||
- Chore schedules can now be skipped
|
- Chore schedules can now be skipped
|
||||||
- New button on the chores overview and chore tracking page
|
- New button on the chores overview and chore tracking page
|
||||||
- Skipped schedules will be highlighted accordingly on the chore journal
|
- Skipped schedules will be highlighted accordingly on the chore journal
|
||||||
|
- The chorecard now also shows the average execution frequency (how often the chore was executed in the past on average)
|
||||||
- Added a new chore option "Start date" which is used as a schedule starting point when the chore was never tracked
|
- Added a new chore option "Start date" which is used as a schedule starting point when the chore was never tracked
|
||||||
- Until now, the schedule starting point was the first tracked execution
|
- Until now, the schedule starting point was the first tracked execution
|
||||||
- For all existing chores, the start date will be set to the first tracked execution time (or today, for chores which were never tracked) on migration
|
- For all existing chores, the start date will be set to the first tracked execution time (or today, for chores which were never tracked) on migration
|
||||||
- The `Yearly` period type has been changed to be schedule the chore on the _same day_ each year
|
- The `Yearly` period type has been changed to be schedule the chore on the _same day_ each year
|
||||||
- This period type scheduled chores 1 year _after the last execution_ before, which is also possible by using the `Daily` period type and a period interval of 365 days - All existing `Yearly` schedules will be converted to that on migration
|
- This period type scheduled chores 1 year _after the last execution_ before, which is also possible by using the `Daily` period type and a period interval of 365 days; all existing `Yearly` schedules will be converted to that on migration
|
||||||
- Added a new `Hourly` period type (to schedule chores every `x` hours)
|
- Added a new `Hourly` period type (to schedule chores every `x` hours)
|
||||||
|
- Added a new `Adaptive` period type (to schedule chores dynamically based on the past average execution frequency)
|
||||||
- Removed the period type `Dynamic regular`, since it's the same as `Daily`
|
- Removed the period type `Dynamic regular`, since it's the same as `Daily`
|
||||||
- All existing `Dynamic regular` schedules will be converted to that on migration
|
- All existing `Dynamic regular` schedules will be converted to that on migration
|
||||||
|
|
||||||
@ -92,4 +94,5 @@
|
|||||||
|
|
||||||
- The API endpoint `/stock/shoppinglist/clear` has now a new optional request body parameter `done_only` (to only remove done items from the given shopping list, defaults to `false`)
|
- The API endpoint `/stock/shoppinglist/clear` has now a new optional request body parameter `done_only` (to only remove done items from the given shopping list, defaults to `false`)
|
||||||
- The API endpoint `/chores/{choreId}/execute` has now a new optional request body parameter `skipped` (to skip the next chore schedule, defaults to `false`)
|
- The API endpoint `/chores/{choreId}/execute` has now a new optional request body parameter `skipped` (to skip the next chore schedule, defaults to `false`)
|
||||||
|
- The API endpoint `/chores/{choreId}` has new response field/property `average_execution_frequency_hours` (contains the average past execution frequency in hours or `null`, when the chore was never executed before)
|
||||||
- Fixed that the barcode lookup for the "Stock by-barcode" API endpoints was case sensitive
|
- Fixed that the barcode lookup for the "Stock by-barcode" API endpoints was case sensitive
|
||||||
|
@ -4806,6 +4806,10 @@
|
|||||||
},
|
},
|
||||||
"next_execution_assigned_user": {
|
"next_execution_assigned_user": {
|
||||||
"$ref": "#/components/schemas/UserDto"
|
"$ref": "#/components/schemas/UserDto"
|
||||||
|
},
|
||||||
|
"average_execution_frequency_hours": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Contains the average past execution frequency in hours or `null`, when the chore was never executed before"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"example": {
|
"example": {
|
||||||
|
@ -29,3 +29,6 @@ msgstr ""
|
|||||||
|
|
||||||
msgid "hourly"
|
msgid "hourly"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "adaptive"
|
||||||
|
msgstr ""
|
||||||
|
@ -30,3 +30,6 @@ msgstr "Yearly"
|
|||||||
|
|
||||||
msgid "hourly"
|
msgid "hourly"
|
||||||
msgstr "Hourly"
|
msgstr "Hourly"
|
||||||
|
|
||||||
|
msgid "adaptive"
|
||||||
|
msgstr "Adaptive"
|
||||||
|
@ -2302,3 +2302,9 @@ msgstr ""
|
|||||||
|
|
||||||
msgid "Show the recipe list and the recipe side by side"
|
msgid "Show the recipe list and the recipe side by side"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "This means the next execution of this chore is scheduled dynamically based on the past average execution frequency"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Average execution frequency"
|
||||||
|
msgstr ""
|
||||||
|
98
migrations/0167.sql
Normal file
98
migrations/0167.sql
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
CREATE INDEX ix_chores_log_performance1 ON chores_log (
|
||||||
|
id,
|
||||||
|
undone,
|
||||||
|
tracked_time
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE VIEW chores_execution_timeline
|
||||||
|
AS
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
cl.chore_id,
|
||||||
|
cl.tracked_time,
|
||||||
|
(SELECT tracked_time FROM chores_log WHERE chore_id = cl.chore_id AND undone = 0 AND tracked_time < cl.tracked_time ORDER BY tracked_time DESC LIMIT 1) AS tracked_time_before,
|
||||||
|
CAST((JULIANDAY(cl.tracked_time) - JULIANDAY((SELECT tracked_time FROM chores_log WHERE chore_id = cl.chore_id AND undone = 0 AND tracked_time < cl.tracked_time ORDER BY tracked_time DESC LIMIT 1))) * 24 AS INT) AS frequency_hours
|
||||||
|
FROM chores_log cl
|
||||||
|
WHERE cl.undone = 0;
|
||||||
|
|
||||||
|
CREATE VIEW chores_execution_average_frequency
|
||||||
|
AS
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
cet.chore_id,
|
||||||
|
AVG(cet.frequency_hours) AS average_frequency_hours
|
||||||
|
FROM chores_execution_timeline cet
|
||||||
|
GROUP BY cet.chore_id;
|
||||||
|
|
||||||
|
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
|
||||||
|
FROM (
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
h.id AS chore_id,
|
||||||
|
h.name AS chore_name,
|
||||||
|
MAX(l.tracked_time) AS last_tracked_time,
|
||||||
|
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(MAX(l.tracked_time), '+' || CAST(h.period_interval AS TEXT) || ' day')
|
||||||
|
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 AS next_estimated_execution_time,
|
||||||
|
h.track_date_only,
|
||||||
|
h.rollover,
|
||||||
|
h.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;
|
@ -176,6 +176,10 @@ $('.input-group-chore-period-type').on('change keyup', function(e)
|
|||||||
{
|
{
|
||||||
$('#chore-schedule-info').text(__n(periodInterval, 'This means the next execution of this chore is scheduled every year on the same day (based on the start date)', 'This means the next execution of this chore is scheduled every %s years on the same day (based on the start date)'));
|
$('#chore-schedule-info').text(__n(periodInterval, 'This means the next execution of this chore is scheduled every year on the same day (based on the start date)', 'This means the next execution of this chore is scheduled every %s years on the same day (based on the start date)'));
|
||||||
}
|
}
|
||||||
|
else if (periodType === 'adaptive')
|
||||||
|
{
|
||||||
|
$('#chore-schedule-info').text(__t('This means the next execution of this chore is scheduled dynamically based on the past average execution frequency'));
|
||||||
|
}
|
||||||
|
|
||||||
Grocy.FrontendHelpers.ValidateForm('chore-form');
|
Grocy.FrontendHelpers.ValidateForm('chore-form');
|
||||||
});
|
});
|
||||||
@ -190,23 +194,23 @@ $('.input-group-chore-assignment-type').on('change', function(e)
|
|||||||
|
|
||||||
if (assignmentType === 'no-assignment')
|
if (assignmentType === 'no-assignment')
|
||||||
{
|
{
|
||||||
$('#chore-assignment-type-info').attr("data-original-title", __t('This means the next execution of this chore will not be assigned to anyone'));
|
$('#chore-assignment-type-info').text(__t('This means the next execution of this chore will not be assigned to anyone'));
|
||||||
}
|
}
|
||||||
else if (assignmentType === 'who-least-did-first')
|
else if (assignmentType === 'who-least-did-first')
|
||||||
{
|
{
|
||||||
$('#chore-assignment-type-info').attr("data-original-title", __t('This means the next execution of this chore will be assigned to the one who executed it least'));
|
$('#chore-assignment-type-info').text(__t('This means the next execution of this chore will be assigned to the one who executed it least'));
|
||||||
$("#assignment_config").attr("required", "");
|
$("#assignment_config").attr("required", "");
|
||||||
$("#assignment_config").removeAttr("disabled");
|
$("#assignment_config").removeAttr("disabled");
|
||||||
}
|
}
|
||||||
else if (assignmentType === 'random')
|
else if (assignmentType === 'random')
|
||||||
{
|
{
|
||||||
$('#chore-assignment-type-info').attr("data-original-title", __t('This means the next execution of this chore will be assigned randomly'));
|
$('#chore-assignment-type-info').text(__t('This means the next execution of this chore will be assigned randomly'));
|
||||||
$("#assignment_config").attr("required", "");
|
$("#assignment_config").attr("required", "");
|
||||||
$("#assignment_config").removeAttr("disabled");
|
$("#assignment_config").removeAttr("disabled");
|
||||||
}
|
}
|
||||||
else if (assignmentType === 'in-alphabetical-order')
|
else if (assignmentType === 'in-alphabetical-order')
|
||||||
{
|
{
|
||||||
$('#chore-assignment-type-info').attr("data-original-title", __t('This means the next execution of this chore will be assigned to the next one in alphabetical order'));
|
$('#chore-assignment-type-info').text(__t('This means the next execution of this chore will be assigned to the next one in alphabetical order'));
|
||||||
$("#assignment_config").attr("required", "");
|
$("#assignment_config").attr("required", "");
|
||||||
$("#assignment_config").removeAttr("disabled");
|
$("#assignment_config").removeAttr("disabled");
|
||||||
}
|
}
|
||||||
|
@ -31,6 +31,15 @@ Grocy.Components.ChoreCard.Refresh = function(choreId)
|
|||||||
$("#chorecard-chore-last-tracked-timeago").removeClass("timeago-date-only");
|
$("#chorecard-chore-last-tracked-timeago").removeClass("timeago-date-only");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (choreDetails.average_execution_frequency_hours == null)
|
||||||
|
{
|
||||||
|
$('#chorecard-average-execution-frequency').text(__t("Unknown"));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$('#chorecard-average-execution-frequency').text(moment.duration(parseInt(choreDetails.average_execution_frequency_hours) / 24, "days").humanize());
|
||||||
|
}
|
||||||
|
|
||||||
RefreshContextualTimeago(".chorecard");
|
RefreshContextualTimeago(".chorecard");
|
||||||
},
|
},
|
||||||
function(xhr)
|
function(xhr)
|
||||||
|
@ -24,6 +24,8 @@ class ChoresService extends BaseService
|
|||||||
|
|
||||||
const CHORE_PERIOD_TYPE_YEARLY = 'yearly';
|
const CHORE_PERIOD_TYPE_YEARLY = 'yearly';
|
||||||
|
|
||||||
|
const CHORE_PERIOD_TYPE_ADAPTIVE = 'adaptive';
|
||||||
|
|
||||||
public function CalculateNextExecutionAssignment($choreId)
|
public function CalculateNextExecutionAssignment($choreId)
|
||||||
{
|
{
|
||||||
if (!$this->ChoreExists($choreId))
|
if (!$this->ChoreExists($choreId))
|
||||||
@ -113,6 +115,7 @@ class ChoresService extends BaseService
|
|||||||
$choreTrackedCount = $this->getDatabase()->chores_log()->where('chore_id = :1 AND undone = 0 AND skipped = 0', $choreId)->count();
|
$choreTrackedCount = $this->getDatabase()->chores_log()->where('chore_id = :1 AND undone = 0 AND skipped = 0', $choreId)->count();
|
||||||
$choreLastTrackedTime = $this->getDatabase()->chores_log()->where('chore_id = :1 AND undone = 0 AND skipped = 0', $choreId)->max('tracked_time');
|
$choreLastTrackedTime = $this->getDatabase()->chores_log()->where('chore_id = :1 AND undone = 0 AND skipped = 0', $choreId)->max('tracked_time');
|
||||||
$nextExecutionTime = $this->getDatabase()->chores_current()->where('chore_id', $choreId)->min('next_estimated_execution_time');
|
$nextExecutionTime = $this->getDatabase()->chores_current()->where('chore_id', $choreId)->min('next_estimated_execution_time');
|
||||||
|
$averageExecutionFrequency = $this->getDatabase()->chores_execution_average_frequency()->where('chore_id', $choreId)->min('average_frequency_hours');
|
||||||
|
|
||||||
$lastChoreLogRow = $this->getDatabase()->chores_log()->where('chore_id = :1 AND tracked_time = :2 AND undone = 0', $choreId, $choreLastTrackedTime)->fetch();
|
$lastChoreLogRow = $this->getDatabase()->chores_log()->where('chore_id = :1 AND tracked_time = :2 AND undone = 0', $choreId, $choreLastTrackedTime)->fetch();
|
||||||
$lastDoneByUser = null;
|
$lastDoneByUser = null;
|
||||||
@ -133,7 +136,8 @@ class ChoresService extends BaseService
|
|||||||
'tracked_count' => $choreTrackedCount,
|
'tracked_count' => $choreTrackedCount,
|
||||||
'last_done_by' => $lastDoneByUser,
|
'last_done_by' => $lastDoneByUser,
|
||||||
'next_estimated_execution_time' => $nextExecutionTime,
|
'next_estimated_execution_time' => $nextExecutionTime,
|
||||||
'next_execution_assigned_user' => $nextExecutionAssignedUser
|
'next_execution_assigned_user' => $nextExecutionAssignedUser,
|
||||||
|
'average_execution_frequency_hours' => $averageExecutionFrequency
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -188,11 +188,7 @@
|
|||||||
|
|
||||||
@if(GROCY_FEATURE_FLAG_CHORES_ASSIGNMENTS)
|
@if(GROCY_FEATURE_FLAG_CHORES_ASSIGNMENTS)
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="assignment_type">{{ $__t('Assignment type') }} <i id="chore-assignment-type-info"
|
<label for="assignment_type">{{ $__t('Assignment type') }}</label>
|
||||||
class="fas fa-question-circle text-muted"
|
|
||||||
data-toggle="tooltip"
|
|
||||||
data-trigger="hover click"
|
|
||||||
title=""></i></label>
|
|
||||||
<select required
|
<select required
|
||||||
class="custom-control custom-select input-group-chore-assignment-type"
|
class="custom-control custom-select input-group-chore-assignment-type"
|
||||||
id="assignment_type"
|
id="assignment_type"
|
||||||
@ -223,6 +219,9 @@
|
|||||||
</select>
|
</select>
|
||||||
<div class="invalid-feedback">{{ $__t('This assignment type requires that at least one is assigned') }}</div>
|
<div class="invalid-feedback">{{ $__t('This assignment type requires that at least one is assigned') }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<p id="chore-assignment-type-info"
|
||||||
|
class="form-text text-info mt-n2"></p>
|
||||||
@else
|
@else
|
||||||
<input type="hidden"
|
<input type="hidden"
|
||||||
id="assignment_type"
|
id="assignment_type"
|
||||||
|
@ -28,6 +28,7 @@
|
|||||||
|
|
||||||
<strong>{{ $__t('Tracked count') }}:</strong> <span id="chorecard-chore-tracked-count"
|
<strong>{{ $__t('Tracked count') }}:</strong> <span id="chorecard-chore-tracked-count"
|
||||||
class="locale-number locale-number-generic"></span><br>
|
class="locale-number locale-number-generic"></span><br>
|
||||||
|
<strong>{{ $__t('Average execution frequency') }}:</strong> <span id="chorecard-average-execution-frequency"></span><br>
|
||||||
<strong>{{ $__t('Last tracked') }}:</strong> <span id="chorecard-chore-last-tracked"></span> <time id="chorecard-chore-last-tracked-timeago"
|
<strong>{{ $__t('Last tracked') }}:</strong> <span id="chorecard-chore-last-tracked"></span> <time id="chorecard-chore-last-tracked-timeago"
|
||||||
class="timeago timeago-contextual"></time><br>
|
class="timeago timeago-contextual"></time><br>
|
||||||
@if(GROCY_FEATURE_FLAG_CHORES_ASSIGNMENTS)
|
@if(GROCY_FEATURE_FLAG_CHORES_ASSIGNMENTS)
|
||||||
|
@ -54,8 +54,8 @@
|
|||||||
<strong>{{ $__t('Last used') }}:</strong> <span id="productcard-product-last-used"></span> <time id="productcard-product-last-used-timeago"
|
<strong>{{ $__t('Last used') }}:</strong> <span id="productcard-product-last-used"></span> <time id="productcard-product-last-used-timeago"
|
||||||
class="timeago timeago-contextual"></time><br>
|
class="timeago timeago-contextual"></time><br>
|
||||||
@if(GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)<strong>{{ $__t('Last price') }}:</strong> <span id="productcard-product-last-price"></span><br>@endif
|
@if(GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)<strong>{{ $__t('Last price') }}:</strong> <span id="productcard-product-last-price"></span><br>@endif
|
||||||
@if (GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)<strong>{{ $__t('Average price') }}:</strong> <span id="productcard-product-average-price"></span><br>@endif
|
@if(GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)<strong>{{ $__t('Average price') }}:</strong> <span id="productcard-product-average-price"></span><br>@endif
|
||||||
@if (GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING)<strong>{{ $__t('Average shelf life') }}:</strong> <span id="productcard-product-average-shelf-life"></span><br>@endif
|
@if(GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING)<strong>{{ $__t('Average shelf life') }}:</strong> <span id="productcard-product-average-shelf-life"></span><br>@endif
|
||||||
<strong>{{ $__t('Spoil rate') }}:</strong> <span id="productcard-product-spoil-rate"></span>
|
<strong>{{ $__t('Spoil rate') }}:</strong> <span id="productcard-product-spoil-rate"></span>
|
||||||
|
|
||||||
<p class="w-75 mt-3 mx-auto"><img id="productcard-product-picture"
|
<p class="w-75 mt-3 mx-auto"><img id="productcard-product-picture"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user