From 268b8e87d782e7bcd3e8356223d59e2638d99bb8 Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Sun, 20 Dec 2020 10:19:44 +0100 Subject: [PATCH] Make it possible to hide chores/batteries (closes #1069) --- changelog/60_3.0.0_2020-12-22.md | 4 + controllers/BatteriesController.php | 17 ++++- controllers/ChoresApiController.php | 1 - controllers/ChoresController.php | 15 +++- localization/strings.pot | 2 +- migrations/0103.sql | 2 +- migrations/0113.sql | 1 - migrations/0124.sql | 111 ++++++++++++++++++++++++++++ public/viewjs/batteries.js | 21 +++++- public/viewjs/chores.js | 18 +++++ public/viewjs/products.js | 6 +- services/CalendarService.php | 4 +- views/batteries.blade.php | 13 +++- views/batteryform.blade.php | 13 ++++ views/choreform.blade.php | 13 ++++ views/chores.blade.php | 13 +++- views/products.blade.php | 8 +- 17 files changed, 239 insertions(+), 23 deletions(-) create mode 100644 migrations/0124.sql diff --git a/changelog/60_3.0.0_2020-12-22.md b/changelog/60_3.0.0_2020-12-22.md index 28f63096..953c92e6 100644 --- a/changelog/60_3.0.0_2020-12-22.md +++ b/changelog/60_3.0.0_2020-12-22.md @@ -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) diff --git a/controllers/BatteriesController.php b/controllers/BatteriesController.php index fcdf26cf..2a7eb0d4 100644 --- a/controllers/BatteriesController.php +++ b/controllers/BatteriesController.php @@ -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') ]); } diff --git a/controllers/ChoresApiController.php b/controllers/ChoresApiController.php index c70d4d0b..fb8d51fe 100644 --- a/controllers/ChoresApiController.php +++ b/controllers/ChoresApiController.php @@ -22,7 +22,6 @@ class ChoresApiController extends BaseApiController if ($choreId === null) { $chores = $this->getDatabase()->chores(); - foreach ($chores as $chore) { $this->getChoresService()->CalculateNextExecutionAssignment($chore->id); diff --git a/controllers/ChoresController.php b/controllers/ChoresController.php index a65f4bc1..403e8e9d 100644 --- a/controllers/ChoresController.php +++ b/controllers/ChoresController.php @@ -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') ]); } diff --git a/localization/strings.pot b/localization/strings.pot index 0edf78dc..8cf43d93 100644 --- a/localization/strings.pot +++ b/localization/strings.pot @@ -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" diff --git a/migrations/0103.sql b/migrations/0103.sql index 5b41fd5b..421720b0 100644 --- a/migrations/0103.sql +++ b/migrations/0103.sql @@ -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, diff --git a/migrations/0113.sql b/migrations/0113.sql index 66493d87..c993ae87 100644 --- a/migrations/0113.sql +++ b/migrations/0113.sql @@ -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; diff --git a/migrations/0124.sql b/migrations/0124.sql new file mode 100644 index 00000000..f2905688 --- /dev/null +++ b/migrations/0124.sql @@ -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; diff --git a/public/viewjs/batteries.js b/public/viewjs/batteries.js index 210006e0..3e7eaffb 100644 --- a/public/viewjs/batteries.js +++ b/public/viewjs/batteries.js @@ -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); +} diff --git a/public/viewjs/chores.js b/public/viewjs/chores.js index 5f29a2b6..d45ebd4e 100644 --- a/public/viewjs/chores.js +++ b/public/viewjs/chores.js @@ -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); +} diff --git a/public/viewjs/products.js b/public/viewjs/products.js index 7ac0d093..c32f5493 100644 --- a/public/viewjs/products.js +++ b/public/viewjs/products.js @@ -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); } diff --git a/services/CalendarService.php b/services/CalendarService.php index 0d54fb39..91ded607 100644 --- a/services/CalendarService.php +++ b/services/CalendarService.php @@ -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) diff --git a/views/batteries.blade.php b/views/batteries.blade.php index 8f949562..551984cb 100644 --- a/views/batteries.blade.php +++ b/views/batteries.blade.php @@ -53,6 +53,17 @@ placeholder="{{ $__t('Search') }}"> +
+
+ + +
+
+
+
+ active == 1) checked @endif class="form-check-input custom-control-input" type="checkbox" id="active" name="active" value="1"> + +
+
+
{{ $__t('A name is required') }}
+
+
+ active == 1) checked @endif class="form-check-input custom-control-input" type="checkbox" id="active" name="active" value="1"> + +
+
+