Make it possible to hide chores/batteries (closes #1069)

This commit is contained in:
Bernd Bestel 2020-12-20 10:19:44 +01:00
parent 31dbb95c58
commit 268b8e87d7
No known key found for this signature in database
GPG Key ID: 71BD34C0D4891300
17 changed files with 239 additions and 23 deletions

View File

@ -113,6 +113,7 @@
- Fixed that for products the quantity unit purchase was displayed instead of the products quantity unit stock (thanks @BenoitAnastay)
### Chores improvements/fixes
- Chores can now be disabled to keep the history/journal, but hide it everywhere, without deleting it (new chore option "Active", deleting a chore now explicitly also deletes its journal and all other references)
- Changed that not assigned chores on the chores overview page display now just a dash instead of an ellipsis in the "Assigned to" column to make this more clear (thanks @Germs2004)
- The assignment type "Random" now don't prevents anymore that the last user will be assigned next
- Fixed (again) that weekly chores, where the next execution should be in the same week, were scheduled (not) always (but sometimes) for the next week only (thanks @shadow7412)
@ -130,6 +131,9 @@
### Tasks improvements
- Tasks don't need to unique anymore (name field)
### Batteries improvements
- Batteries can now be disabled to keep the history/journal, but hide it everywhere, without deleting it (new battery option "Active", deleting a battery now explicitly also deletes its journal and all other references)
### Userfield improvements/fixes
- New Userfield type "File" to attach any file, will be rendered as a link to the file in tables (if enabled) (thanks @fipwmaqzufheoxq92ebc)
- New Userfield type "Picture" to attach a picture, the picture will be rendered (small) in tables (if enabled) (thanks @fipwmaqzufheoxq92ebc)

View File

@ -6,8 +6,17 @@ class BatteriesController extends BaseController
{
public function BatteriesList(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{
if (isset($request->getQueryParams()['include_disabled']))
{
$batteries = $this->getDatabase()->batteries()->orderBy('name', 'COLLATE NOCASE');
}
else
{
$batteries = $this->getDatabase()->batteries()->where('active = 1')->orderBy('name', 'COLLATE NOCASE');
}
return $this->renderPage($response, 'batteries', [
'batteries' => $this->getDatabase()->batteries()->orderBy('name', 'COLLATE NOCASE'),
'batteries' => $batteries,
'userfields' => $this->getUserfieldsService()->GetFields('batteries'),
'userfieldValues' => $this->getUserfieldsService()->GetAllValues('batteries')
]);
@ -41,7 +50,7 @@ class BatteriesController extends BaseController
{
return $this->renderPage($response, 'batteriesjournal', [
'chargeCycles' => $this->getDatabase()->battery_charge_cycles()->orderBy('tracked_time', 'DESC'),
'batteries' => $this->getDatabase()->batteries()->orderBy('name', 'COLLATE NOCASE')
'batteries' => $this->getDatabase()->batteries()->where('active = 1')->orderBy('name', 'COLLATE NOCASE')
]);
}
@ -51,7 +60,7 @@ class BatteriesController extends BaseController
$nextXDays = $usersService->GetUserSettings(GROCY_USER_ID)['batteries_due_soon_days'];
return $this->renderPage($response, 'batteriesoverview', [
'batteries' => $this->getDatabase()->batteries()->orderBy('name', 'COLLATE NOCASE'),
'batteries' => $this->getDatabase()->batteries()->where('active = 1')->orderBy('name', 'COLLATE NOCASE'),
'current' => $this->getBatteriesService()->GetCurrent(),
'nextXDays' => $nextXDays,
'userfields' => $this->getUserfieldsService()->GetFields('batteries'),
@ -62,7 +71,7 @@ class BatteriesController extends BaseController
public function TrackChargeCycle(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{
return $this->renderPage($response, 'batterytracking', [
'batteries' => $this->getDatabase()->batteries()->orderBy('name', 'COLLATE NOCASE')
'batteries' => $this->getDatabase()->batteries()->where('active = 1')->orderBy('name', 'COLLATE NOCASE')
]);
}

View File

@ -22,7 +22,6 @@ class ChoresApiController extends BaseApiController
if ($choreId === null)
{
$chores = $this->getDatabase()->chores();
foreach ($chores as $chore)
{
$this->getChoresService()->CalculateNextExecutionAssignment($chore->id);

View File

@ -36,8 +36,17 @@ class ChoresController extends BaseController
public function ChoresList(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{
if (isset($request->getQueryParams()['include_disabled']))
{
$chores = $this->getDatabase()->chores()->orderBy('name', 'COLLATE NOCASE');
}
else
{
$chores = $this->getDatabase()->chores()->where('active = 1')->orderBy('name', 'COLLATE NOCASE');
}
return $this->renderPage($response, 'chores', [
'chores' => $this->getDatabase()->chores()->orderBy('name', 'COLLATE NOCASE'),
'chores' => $chores,
'userfields' => $this->getUserfieldsService()->GetFields('chores'),
'userfieldValues' => $this->getUserfieldsService()->GetAllValues('chores')
]);
@ -52,7 +61,7 @@ class ChoresController extends BaseController
{
return $this->renderPage($response, 'choresjournal', [
'choresLog' => $this->getDatabase()->chores_log()->orderBy('tracked_time', 'DESC'),
'chores' => $this->getDatabase()->chores()->orderBy('name', 'COLLATE NOCASE'),
'chores' => $this->getDatabase()->chores()->where('active = 1')->orderBy('name', 'COLLATE NOCASE'),
'users' => $this->getDatabase()->users()->orderBy('username')
]);
}
@ -75,7 +84,7 @@ class ChoresController extends BaseController
public function TrackChoreExecution(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{
return $this->renderPage($response, 'choretracking', [
'chores' => $this->getDatabase()->chores()->orderBy('name', 'COLLATE NOCASE'),
'chores' => $this->getDatabase()->chores()->where('active = 1')->orderBy('name', 'COLLATE NOCASE'),
'users' => $this->getDatabase()->users()->orderBy('username')
]);
}

View File

@ -2000,7 +2000,7 @@ msgstr ""
msgid "This also removes any stock amount, the journal and all other references of this product - consider disabling it instead, if you want to keep that and just hide the product."
msgstr ""
msgid "Show disabled products"
msgid "Show disabled"
msgstr ""
msgid "Show on stock overview page"

View File

@ -50,7 +50,7 @@ CREATE TABLE products (
name TEXT NOT NULL UNIQUE,
description TEXT,
product_group_id INTEGER,
active TINYINT NOT NULL DEFAULT 1,
active TINYINT NOT NULL DEFAULT 1 CHECK(active IN (0, 1)),
location_id INTEGER NOT NULL,
shopping_location_id INTEGER,
qu_id_purchase INTEGER NOT NULL,

View File

@ -72,7 +72,6 @@ LEFT JOIN chores_log l
ON h.id = l.chore_id
AND l.undone = 0
GROUP BY h.id, h.name, h.period_days
) x;
DROP VIEW batteries_current;

111
migrations/0124.sql Normal file
View File

@ -0,0 +1,111 @@
ALTER TABLE chores
ADD active TINYINT NOT NULL DEFAULT 1 CHECK(active IN (0, 1));
ALTER TABLE batteries
ADD active TINYINT NOT NULL DEFAULT 1 CHECK(active IN (0, 1));
DELETE from chores_log
WHERE chore_id NOT IN (SELECT id from chores);
DELETE from battery_charge_cycles
WHERE battery_id NOT IN (SELECT id from batteries);
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
DATETIME(STRFTIME('%Y-%m-%d', DATETIME('now', 'localtime')) || ' ' || STRFTIME('%H:%M:%S', x.next_estimated_execution_time))
ELSE
x.next_estimated_execution_time
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 h.period_type
WHEN 'manually' THEN '2999-12-31 23:59:59'
WHEN 'dynamic-regular' THEN DATETIME(MAX(l.tracked_time), '+' || CAST(h.period_days AS TEXT) || ' day')
WHEN 'daily' THEN DATETIME(IFNULL(MAX(l.tracked_time), DATETIME('now', 'localtime')), '+' || CAST(h.period_interval AS TEXT) || ' day')
WHEN 'weekly' THEN (
SELECT next
FROM (
SELECT 'sunday' AS day, DATETIME(COALESCE((SELECT tracked_time FROM chores_log WHERE chore_id = h.id ORDER BY tracked_time DESC LIMIT 1), DATETIME('now', 'localtime')), '1 days', '+' || CAST((h.period_interval - 1) * 7 AS TEXT) || ' days', 'weekday 0') AS next
UNION
SELECT 'monday' AS day, DATETIME(COALESCE((SELECT tracked_time FROM chores_log WHERE chore_id = h.id ORDER BY tracked_time DESC LIMIT 1), DATETIME('now', 'localtime')), '1 days', '+' || CAST((h.period_interval - 1) * 7 AS TEXT) || ' days', 'weekday 1') AS next
UNION
SELECT 'tuesday' AS day, DATETIME(COALESCE((SELECT tracked_time FROM chores_log WHERE chore_id = h.id ORDER BY tracked_time DESC LIMIT 1), DATETIME('now', 'localtime')), '1 days', '+' || CAST((h.period_interval - 1) * 7 AS TEXT) || ' days', 'weekday 2') AS next
UNION
SELECT 'wednesday' AS day, DATETIME(COALESCE((SELECT tracked_time FROM chores_log WHERE chore_id = h.id ORDER BY tracked_time DESC LIMIT 1), DATETIME('now', 'localtime')), '1 days', '+' || CAST((h.period_interval - 1) * 7 AS TEXT) || ' days', 'weekday 3') AS next
UNION
SELECT 'thursday' AS day, DATETIME(COALESCE((SELECT tracked_time FROM chores_log WHERE chore_id = h.id ORDER BY tracked_time DESC LIMIT 1), DATETIME('now', 'localtime')), '1 days', '+' || CAST((h.period_interval - 1) * 7 AS TEXT) || ' days', 'weekday 4') AS next
UNION
SELECT 'friday' AS day, DATETIME(COALESCE((SELECT tracked_time FROM chores_log WHERE chore_id = h.id ORDER BY tracked_time DESC LIMIT 1), DATETIME('now', 'localtime')), '1 days', '+' || CAST((h.period_interval - 1) * 7 AS TEXT) || ' days', 'weekday 5') AS next
UNION
SELECT 'saturday' AS day, DATETIME(COALESCE((SELECT tracked_time FROM chores_log WHERE chore_id = h.id ORDER BY tracked_time DESC LIMIT 1), DATETIME('now', 'localtime')), '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(IFNULL(MAX(l.tracked_time), DATETIME('now', 'localtime')), '+' || CAST(h.period_interval AS TEXT) || ' month', 'start of month', '+' || CAST(h.period_days - 1 AS TEXT) || ' day')
WHEN 'yearly' THEN DATETIME(IFNULL(MAX(l.tracked_time), DATETIME('now', 'localtime')), '+' || CAST(h.period_interval AS TEXT) || ' years')
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;
DROP VIEW chores_assigned_users_resolved;
CREATE VIEW chores_assigned_users_resolved
AS
SELECT
c.id AS chore_id,
u.id AS user_id
FROM chores c
JOIN users u
ON ',' || c.assignment_config || ',' LIKE '%,' || CAST(u.id AS TEXT) || ',%'
WHERE c.active = 1;
DROP VIEW batteries_current;
CREATE VIEW batteries_current
AS
SELECT
b.id, -- Dummy, LessQL needs an id column
b.id AS battery_id,
MAX(l.tracked_time) AS last_tracked_time,
CASE WHEN b.charge_interval_days = 0
THEN '2999-12-31 23:59:59'
ELSE datetime(MAX(l.tracked_time), '+' || CAST(b.charge_interval_days AS TEXT) || ' day')
END AS next_estimated_charge_time
FROM batteries b
LEFT JOIN battery_charge_cycles l
ON b.id = l.battery_id
WHERE b.active = 1
GROUP BY b.id, b.charge_interval_days;
CREATE TRIGGER cascade_chore_removal AFTER DELETE ON chores
BEGIN
DELETE FROM chores_log
WHERE chore_id = OLD.id;
END;
CREATE TRIGGER cascade_battery_removal AFTER DELETE ON batteries
BEGIN
DELETE FROM battery_charge_cycles
WHERE battery_id = OLD.id;
END;

View File

@ -2,7 +2,8 @@
'order': [[1, 'asc']],
'columnDefs': [
{ 'orderable': false, 'targets': 0 },
{ 'searchable': false, "targets": 0 }
{ 'searchable': false, "targets": 0 },
{ "type": "num", "targets": 4 }
].concat($.fn.dataTable.defaults.columnDefs)
});
$('#batteries-table tbody').removeClass("d-none");
@ -23,6 +24,7 @@ $("#clear-filter-button").on("click", function()
{
$("#search").val("");
batteriesTable.search("").draw();
$("#show-disabled").prop('checked', false);
});
$(document).on('click', '.battery-delete-button', function(e)
@ -61,3 +63,20 @@ $(document).on('click', '.battery-delete-button', function(e)
}
});
});
$("#show-disabled").change(function()
{
if (this.checked)
{
window.location.href = U('/batteries?include_disabled');
}
else
{
window.location.href = U('/batteries');
}
});
if (GetUriParam('include_disabled'))
{
$("#show-disabled").prop('checked', true);
}

View File

@ -23,6 +23,7 @@ $("#clear-filter-button").on("click", function()
{
$("#search").val("");
choresTable.search("").draw();
$("#show-disabled").prop('checked', false);
});
$(document).on('click', '.chore-delete-button', function(e)
@ -61,3 +62,20 @@ $(document).on('click', '.chore-delete-button', function(e)
}
});
});
$("#show-disabled").change(function()
{
if (this.checked)
{
window.location.href = U('/chores?include_disabled');
}
else
{
window.location.href = U('/chores');
}
});
if (GetUriParam('include_disabled'))
{
$("#show-disabled").prop('checked', true);
}

View File

@ -38,7 +38,7 @@ $("#clear-filter-button").on("click", function()
$("#product-group-filter").val("all");
productsTable.column(7).search("").draw();
productsTable.search("").draw();
$("#show-disabled-products").prop('checked', false);
$("#show-disabled").prop('checked', false);
});
if (typeof GetUriParam("product-group") !== "undefined")
@ -86,7 +86,7 @@ $(document).on('click', '.product-delete-button', function(e)
});
});
$("#show-disabled-products").change(function()
$("#show-disabled").change(function()
{
if (this.checked)
{
@ -100,5 +100,5 @@ $("#show-disabled-products").change(function()
if (GetUriParam('include_disabled'))
{
$("#show-disabled-products").prop('checked', true);
$("#show-disabled").prop('checked', true);
}

View File

@ -52,7 +52,7 @@ class CalendarService extends BaseService
{
$users = $this->getUsersService()->GetUsersAsDto();
$chores = $this->getDatabase()->chores();
$chores = $this->getDatabase()->chores()->where('active = 1');
$titlePrefix = $this->getLocalizationService()->__t('Chore due') . ': ';
foreach ($this->getChoresService()->GetCurrent() as $currentChoreEntry)
@ -80,7 +80,7 @@ class CalendarService extends BaseService
if (GROCY_FEATURE_FLAG_BATTERIES)
{
$batteries = $this->getDatabase()->batteries();
$batteries = $this->getDatabase()->batteries()->where('active = 1');
$titlePrefix = $this->getLocalizationService()->__t('Battery charge cycle due') . ': ';
foreach ($this->getBatteriesService()->GetCurrent() as $currentBatteryEntry)

View File

@ -53,6 +53,17 @@
placeholder="{{ $__t('Search') }}">
</div>
</div>
<div class="col-xs-12 col-md-6 col-xl-3">
<div class="form-check custom-control custom-checkbox">
<input class="form-check-input custom-control-input"
type="checkbox"
id="show-disabled">
<label class="form-check-label custom-control-label"
for="show-disabled">
{{ $__t('Show disabled') }}
</label>
</div>
</div>
<div class="col">
<div class="float-right">
<a id="clear-filter-button"
@ -89,7 +100,7 @@
</thead>
<tbody class="d-none">
@foreach($batteries as $battery)
<tr>
<tr class="@if($battery->active == 0) text-muted @endif">
<td class="fit-content border-right">
<a class="btn btn-info btn-sm permission-MASTER_DATA_EDIT show-as-dialog-link"
href="{{ $U('/battery/') }}{{ $battery->id }}?embedded"

View File

@ -44,6 +44,19 @@
<div class="invalid-feedback">{{ $__t('A name is required') }}</div>
</div>
<div class="form-group">
<div class="custom-control custom-checkbox">
<input @if($mode=='create'
)
checked
@elseif($mode=='edit'
&&
$battery->active == 1) checked @endif class="form-check-input custom-control-input" type="checkbox" id="active" name="active" value="1">
<label class="form-check-label custom-control-label"
for="active">{{ $__t('Active') }}</label>
</div>
</div>
<div class="form-group">
<label for="description">{{ $__t('Description') }}</label>
<input type="text"

View File

@ -43,6 +43,19 @@
<div class="invalid-feedback">{{ $__t('A name is required') }}</div>
</div>
<div class="form-group">
<div class="custom-control custom-checkbox">
<input @if($mode=='create'
)
checked
@elseif($mode=='edit'
&&
$chore->active == 1) checked @endif class="form-check-input custom-control-input" type="checkbox" id="active" name="active" value="1">
<label class="form-check-label custom-control-label"
for="active">{{ $__t('Active') }}</label>
</div>
</div>
<div class="form-group">
<label for="description">{{ $__t('Description') }}</label>
<textarea class="form-control"

View File

@ -53,6 +53,17 @@
placeholder="{{ $__t('Search') }}">
</div>
</div>
<div class="col-xs-12 col-md-6 col-xl-3">
<div class="form-check custom-control custom-checkbox">
<input class="form-check-input custom-control-input"
type="checkbox"
id="show-disabled">
<label class="form-check-label custom-control-label"
for="show-disabled">
{{ $__t('Show disabled') }}
</label>
</div>
</div>
<div class="col">
<div class="float-right">
<a id="clear-filter-button"
@ -89,7 +100,7 @@
</thead>
<tbody class="d-none">
@foreach($chores as $chore)
<tr>
<tr class="@if($chore->active == 0) text-muted @endif">
<td class="fit-content border-right">
<a class="btn btn-info btn-sm"
href="{{ $U('/chore/') }}{{ $chore->id }}"

View File

@ -75,10 +75,10 @@
<div class="form-check custom-control custom-checkbox">
<input class="form-check-input custom-control-input"
type="checkbox"
id="show-disabled-products">
id="show-disabled">
<label class="form-check-label custom-control-label"
for="show-disabled-products">
{{ $__t('Show disabled products') }}
for="show-disabled">
{{ $__t('Show disabled') }}
</label>
</div>
</div>
@ -122,7 +122,7 @@
</thead>
<tbody class="d-none">
@foreach($products as $product)
<tr>
<tr class="@if($product->active == 0) text-muted @endif">
<td class="fit-content border-right">
<a class="btn btn-info btn-sm"
href="{{ $U('/product/') }}{{ $product->id }}"