diff --git a/GrocyDbMigrator.php b/GrocyDbMigrator.php index a62e4823..35e23ee1 100644 --- a/GrocyDbMigrator.php +++ b/GrocyDbMigrator.php @@ -131,6 +131,35 @@ class GrocyDbMigrator GROUP BY habit_id ORDER BY MAX(tracked_time) DESC;" ); + + self::ExecuteMigrationWhenNeeded($pdo, 13, " + CREATE TABLE batteries ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, + name TEXT NOT NULL UNIQUE, + description TEXT, + used_in TEXT, + charge_interval_days INTEGER NOT NULL DEFAULT 0, + row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime')) + )" + ); + + self::ExecuteMigrationWhenNeeded($pdo, 14, " + CREATE TABLE battery_charge_cycles ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, + battery_id TEXT NOT NULL, + tracked_time DATETIME, + row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime')) + )" + ); + + self::ExecuteMigrationWhenNeeded($pdo, 15, " + CREATE VIEW batteries_current + AS + SELECT battery_id, MAX(tracked_time) AS last_tracked_time + FROM battery_charge_cycles + GROUP BY battery_id + ORDER BY MAX(tracked_time) DESC;" + ); } private static function ExecuteMigrationWhenNeeded(PDO $pdo, int $migrationId, string $sql) diff --git a/GrocyDemoDataGenerator.php b/GrocyDemoDataGenerator.php index b015bfc2..95cee54c 100644 --- a/GrocyDemoDataGenerator.php +++ b/GrocyDemoDataGenerator.php @@ -9,11 +9,11 @@ class GrocyDemoDataGenerator { $sql = " UPDATE locations SET name = 'Vorratskammer', description = '' WHERE id = 1; - INSERT INTO locations (name) VALUES ('Süßigkeitenschrank'); --2 + INSERT INTO locations (name) VALUES ('S��igkeitenschrank'); --2 INSERT INTO locations (name) VALUES ('Konservenschrank'); --3 - INSERT INTO locations (name) VALUES ('Kühlschrank'); --4 + INSERT INTO locations (name) VALUES ('K�hlschrank'); --4 - UPDATE quantity_units SET name = 'Stück' WHERE id = 1; + UPDATE quantity_units SET name = 'St�ck' WHERE id = 1; INSERT INTO quantity_units (name) VALUES ('Packung'); --2 INSERT INTO quantity_units (name) VALUES ('Glas'); --3 INSERT INTO quantity_units (name) VALUES ('Dose'); --4 @@ -21,14 +21,14 @@ class GrocyDemoDataGenerator INSERT INTO quantity_units (name) VALUES ('Bund'); --6 DELETE FROM products WHERE id IN (1, 2); - INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, min_stock_amount) VALUES ('Gummibärchen', 2, 2, 2, 1, 8); --3 + INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, min_stock_amount) VALUES ('Gummib�rchen', 2, 2, 2, 1, 8); --3 INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, min_stock_amount) VALUES ('Chips', 2, 2, 2, 1, 10); --4 INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('Eier', 1, 2, 1, 10); --5 INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('Nudeln', 1, 2, 2, 1); --6 INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('Essiggurken', 3, 3, 3, 1); --7 INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('Gulaschsuppe', 3, 4, 4, 1); --8 INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('Joghurt', 4, 5, 5, 1); --9 - INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('Käse', 4, 2, 2, 1); --10 + INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('K�se', 4, 2, 2, 1); --10 INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('Aufschnitt', 4, 2, 2, 1); --11 INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('Paprika', 4, 1, 1, 1); --12 INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('Gurke', 4, 1, 1, 1); --13 @@ -38,6 +38,10 @@ class GrocyDemoDataGenerator INSERT INTO habits (name, period_type, period_days) VALUES ('Changed towels in the bathroom', 'manually', 5); --1 INSERT INTO habits (name, period_type, period_days) VALUES ('Cleaned the kitchen floor', 'dynamic-regular', 7); --2 + INSERT INTO batteries (name, description, used_in) VALUES ('Battery1', 'Warranty ends 2022', 'TV remote control'); --1 + INSERT INTO batteries (name, description, used_in) VALUES ('Battery2', 'Warranty ends 2022', 'Alarm clock'); --2 + INSERT INTO batteries (name, description, used_in, charge_interval_days) VALUES ('Battery3', 'Warranty ends 2022', 'Heat remote control', 60); --3 + INSERT INTO migrations (migration) VALUES (-1); "; @@ -63,6 +67,16 @@ class GrocyDemoDataGenerator GrocyLogicHabits::TrackHabit(1, date('Y-m-d H:i:s', strtotime('-15 days'))); GrocyLogicHabits::TrackHabit(2, date('Y-m-d H:i:s', strtotime('-10 days'))); GrocyLogicHabits::TrackHabit(2, date('Y-m-d H:i:s', strtotime('-20 days'))); + + GrocyLogicBatteries::TrackChargeCycle(1, date('Y-m-d H:i:s', strtotime('-200 days'))); + GrocyLogicBatteries::TrackChargeCycle(1, date('Y-m-d H:i:s', strtotime('-150 days'))); + GrocyLogicBatteries::TrackChargeCycle(1, date('Y-m-d H:i:s', strtotime('-100 days'))); + GrocyLogicBatteries::TrackChargeCycle(1, date('Y-m-d H:i:s', strtotime('-50 days'))); + GrocyLogicBatteries::TrackChargeCycle(2, date('Y-m-d H:i:s', strtotime('-200 days'))); + GrocyLogicBatteries::TrackChargeCycle(2, date('Y-m-d H:i:s', strtotime('-150 days'))); + GrocyLogicBatteries::TrackChargeCycle(2, date('Y-m-d H:i:s', strtotime('-100 days'))); + GrocyLogicBatteries::TrackChargeCycle(2, date('Y-m-d H:i:s', strtotime('-50 days'))); + GrocyLogicBatteries::TrackChargeCycle(3, date('Y-m-d H:i:s', strtotime('-65 days'))); } } diff --git a/GrocyLogicBatteries.php b/GrocyLogicBatteries.php new file mode 100644 index 00000000..08a9a677 --- /dev/null +++ b/GrocyLogicBatteries.php @@ -0,0 +1,57 @@ +fetchAll(PDO::FETCH_OBJ); + } + + public static function GetNextChargeTime(int $batteryId) + { + $db = Grocy::GetDbConnection(); + + $battery = $db->batteries($batteryId); + $batteryLastLogRow = Grocy::ExecuteDbQuery(Grocy::GetDbConnectionRaw(), "SELECT * from batteries_current WHERE battery_id = $batteryId LIMIT 1")->fetch(PDO::FETCH_OBJ); + + if ($battery->charge_interval_days > 0) + { + return date('Y-m-d H:i:s', strtotime('+' . $battery->charge_interval_days . ' day', strtotime($batteryLastLogRow->last_tracked_time))); + } + else + { + return date('Y-m-d H:i:s'); + } + + return null; + } + + public static function GetBatteryDetails(int $batteryId) + { + $db = Grocy::GetDbConnection(); + + $battery = $db->batteries($batteryId); + $batteryChargeCylcesCount = $db->battery_charge_cycles()->where('battery_id', $batteryId)->count(); + $batteryLastChargedTime = $db->battery_charge_cycles()->where('battery_id', $batteryId)->max('tracked_time'); + + return array( + 'battery' => $battery, + 'last_charged' => $batteryLastChargedTime, + 'charge_cycles_count' => $batteryChargeCylcesCount + ); + } + + public static function TrackChargeCycle(int $batteryId, string $trackedTime) + { + $db = Grocy::GetDbConnection(); + + $logRow = $db->battery_charge_cycles()->createRow(array( + 'battery_id' => $batteryId, + 'tracked_time' => $trackedTime + )); + $logRow->save(); + + return true; + } +} diff --git a/index.php b/index.php index 636c7d13..89fbd2b8 100644 --- a/index.php +++ b/index.php @@ -11,6 +11,7 @@ require_once __DIR__ . '/GrocyDbMigrator.php'; require_once __DIR__ . '/GrocyDemoDataGenerator.php'; require_once __DIR__ . '/GrocyLogicStock.php'; require_once __DIR__ . '/GrocyLogicHabits.php'; +require_once __DIR__ . '/GrocyLogicBatteries.php'; require_once __DIR__ . '/GrocyPhpHelper.php'; $app = new \Slim\App; @@ -122,6 +123,16 @@ $app->get('/habitsoverview', function(Request $request, Response $response) use( ]); }); +$app->get('/batteriesoverview', function(Request $request, Response $response) use($db) +{ + return $this->renderer->render($response, '/layout.php', [ + 'title' => 'Batteries overview', + 'contentPage' => 'batteriesoverview.php', + 'batteries' => $db->batteries(), + 'current' => GrocyLogicBatteries::GetCurrent(), + ]); +}); + $app->get('/purchase', function(Request $request, Response $response) use($db) { return $this->renderer->render($response, '/layout.php', [ @@ -170,6 +181,15 @@ $app->get('/habittracking', function(Request $request, Response $response) use($ ]); }); +$app->get('/batterytracking', function(Request $request, Response $response) use($db) +{ + return $this->renderer->render($response, '/layout.php', [ + 'title' => 'Battery tracking', + 'contentPage' => 'batterytracking.php', + 'batteries' => $db->batteries() + ]); +}); + $app->get('/products', function(Request $request, Response $response) use($db) { return $this->renderer->render($response, '/layout.php', [ @@ -208,6 +228,15 @@ $app->get('/habits', function(Request $request, Response $response) use($db) ]); }); +$app->get('/batteries', function(Request $request, Response $response) use($db) +{ + return $this->renderer->render($response, '/layout.php', [ + 'title' => 'Batteries', + 'contentPage' => 'batteries.php', + 'batteries' => $db->batteries() + ]); +}); + $app->get('/product/{productId}', function(Request $request, Response $response, $args) use($db) { @@ -299,6 +328,27 @@ $app->get('/habit/{habitId}', function(Request $request, Response $response, $ar } }); +$app->get('/battery/{batteryId}', function(Request $request, Response $response, $args) use($db) +{ + if ($args['batteryId'] == 'new') + { + return $this->renderer->render($response, '/layout.php', [ + 'title' => 'Create battery', + 'contentPage' => 'batteryform.php', + 'mode' => 'create' + ]); + } + else + { + return $this->renderer->render($response, '/layout.php', [ + 'title' => 'Edit battery', + 'contentPage' => 'batteryform.php', + 'battery' => $db->batteries($args['batteryId']), + 'mode' => 'edit' + ]); + } +}); + $app->get('/shoppinglistitem/{itemId}', function(Request $request, Response $response, $args) use($db) { if ($args['itemId'] == 'new') @@ -434,6 +484,22 @@ $app->group('/api', function() use($db) { echo json_encode(GrocyLogicHabits::GetHabitDetails($args['habitId'])); }); + + $this->get('/batteries/track-charge-cycle/{batteryId}', function(Request $request, Response $response, $args) + { + $trackedTime = date('Y-m-d H:i:s'); + if (isset($request->getQueryParams()['tracked_time']) && !empty($request->getQueryParams()['tracked_time'])) + { + $trackedTime = $request->getQueryParams()['tracked_time']; + } + + echo json_encode(array('success' => GrocyLogicBatteries::TrackChargeCycle($args['batteryId'], $trackedTime))); + }); + + $this->get('/batteries/get-battery-details/{batteryId}', function(Request $request, Response $response, $args) + { + echo json_encode(GrocyLogicBatteries::GetBatteryDetails($args['batteryId'])); + }); })->add(function($request, $response, $next) { $response = $next($request, $response); diff --git a/views/batteries.js b/views/batteries.js new file mode 100644 index 00000000..2350f98a --- /dev/null +++ b/views/batteries.js @@ -0,0 +1,43 @@ +$(document).on('click', '.battery-delete-button', function(e) +{ + bootbox.confirm({ + message: 'Delete battery ' + $(e.target).attr('data-battery-name') + '?', + buttons: { + confirm: { + label: 'Yes', + className: 'btn-success' + }, + cancel: { + label: 'No', + className: 'btn-danger' + } + }, + callback: function(result) + { + if (result === true) + { + Grocy.FetchJson('/api/delete-object/batteries/' + $(e.target).attr('data-battery-id'), + function(result) + { + window.location.href = '/batteries'; + }, + function(xhr) + { + console.error(xhr); + } + ); + } + } + }); +}); + +$(function() +{ + $('#batteries-table').DataTable({ + 'pageLength': 50, + 'order': [[1, 'asc']], + 'columnDefs': [ + { 'orderable': false, 'targets': 0 } + ] + }); +}); diff --git a/views/batteries.php b/views/batteries.php new file mode 100644 index 00000000..b46f3600 --- /dev/null +++ b/views/batteries.php @@ -0,0 +1,46 @@ +
+ +

+ Batteries + +  Add + +

+ +
+ + + + + + + + + + + + + + + + + + + +
#NameDescriptionUsed in
+ + + + + + + + name; ?> + + description; ?> + + used_in; ?> +
+
+ +
diff --git a/views/batteriesoverview.js b/views/batteriesoverview.js new file mode 100644 index 00000000..4d7fb551 --- /dev/null +++ b/views/batteriesoverview.js @@ -0,0 +1,7 @@ +$(function() +{ + $('#habits-overview-table').DataTable({ + 'pageLength': 50, + 'order': [[1, 'desc']] + }); +}); diff --git a/views/batteriesoverview.php b/views/batteriesoverview.php new file mode 100644 index 00000000..9e9fa55f --- /dev/null +++ b/views/batteriesoverview.php @@ -0,0 +1,38 @@ +
+ +

Batteries overview

+ +
+ + + + + + + + + + + + + + + + + +
BatteryLast chargedNext planned charge cycle
+ battery_id)->name; ?> + + last_tracked_time; ?> + + + battery_id)->charge_interval_days > 0): ?> + battery_id); ?> + + + Whenever you want... + +
+
+ +
diff --git a/views/batteryform.js b/views/batteryform.js new file mode 100644 index 00000000..8b3b8d8f --- /dev/null +++ b/views/batteryform.js @@ -0,0 +1,38 @@ +$('#save-battery-button').on('click', function(e) +{ + e.preventDefault(); + + if (Grocy.EditMode === 'create') + { + Grocy.PostJson('/api/add-object/batteries', $('#battery-form').serializeJSON(), + function(result) + { + window.location.href = '/batteries'; + }, + function(xhr) + { + console.error(xhr); + } + ); + } + else + { + Grocy.PostJson('/api/edit-object/batteries/' + Grocy.EditObjectId, $('#battery-form').serializeJSON(), + function(result) + { + window.location.href = '/batteries'; + }, + function(xhr) + { + console.error(xhr); + } + ); + } +}); + +$(function() +{ + $('#name').focus(); + $('#battery-form').validator(); + $('#battery-form').validator('validate'); +}); diff --git a/views/batteryform.php b/views/batteryform.php new file mode 100644 index 00000000..d39c6729 --- /dev/null +++ b/views/batteryform.php @@ -0,0 +1,33 @@ +
+ +

+ + + + + + + +
+ +
+ + +
+
+ +
+ + +
+ +
+ + +
+ + + +
+ +
diff --git a/views/batterytracking.js b/views/batterytracking.js new file mode 100644 index 00000000..a488e5c7 --- /dev/null +++ b/views/batterytracking.js @@ -0,0 +1,167 @@ +$('#save-batterytracking-button').on('click', function(e) +{ + e.preventDefault(); + + var jsonForm = $('#batterytracking-form').serializeJSON(); + + Grocy.FetchJson('/api/batteries/get-battery-details/' + jsonForm.battery_id, + function (batteryDetails) + { + Grocy.FetchJson('/api/batteries/track-charge-cycle/' + jsonForm.battery_id + '?tracked_time=' + $('#tracked_time').val(), + function(result) + { + toastr.success('Tracked charge cylce of battery ' + batteryDetails.battery.name + ' on ' + $('#tracked_time').val()); + + $('#battery_id').val(''); + $('#battery_id_text_input').focus(); + $('#battery_id_text_input').val(''); + $('#tracked_time').val(moment().format('YYYY-MM-DD HH:mm:ss')); + $('#tracked_time').trigger('change'); + $('#battery_id_text_input').trigger('change'); + $('#batterytracking-form').validator('validate'); + }, + function(xhr) + { + console.error(xhr); + } + ); + }, + function(xhr) + { + console.error(xhr); + } + ); +}); + +$('#battery_id').on('change', function(e) +{ + var batteryId = $(e.target).val(); + + if (batteryId) + { + Grocy.FetchJson('/api/batteries/get-battery-details/' + batteryId, + function(batteryDetails) + { + $('#selected-battery-name').text(batteryDetails.battery.name); + $('#selected-battery-last-charged').text((batteryDetails.last_charged || 'never')); + $('#selected-battery-last-charged-timeago').text($.timeago(batteryDetails.last_charged || '')); + $('#selected-battery-charge-cycles-count').text((batteryDetails.charge_cycles_count || '0')); + + $('#tracked_time').focus(); + + Grocy.EmptyElementWhenMatches('#selected-battery-last-charged-timeago', 'NaN years ago'); + }, + function(xhr) + { + console.error(xhr); + } + ); + } +}); + +$(function() +{ + $('.datetimepicker').datetimepicker( + { + format: 'YYYY-MM-DD HH:mm:ss', + showTodayButton: true, + calendarWeeks: true, + maxDate: moment() + }); + + $('#tracked_time').val(moment().format('YYYY-MM-DD HH:mm:ss')); + $('#tracked_time').trigger('change'); + + $('#tracked_time').on('focus', function(e) + { + if ($('#battery_id_text_input').val().length === 0) + { + $('#battery_id_text_input').focus(); + } + }); + + $('.combobox').combobox({ + appendId: '_text_input' + }); + + $('#battery_id').val(''); + $('#battery_id_text_input').focus(); + $('#battery_id_text_input').val(''); + $('#battery_id_text_input').trigger('change'); + + $('#batterytracking-form').validator(); + $('#batterytracking-form').validator('validate'); + + $('#batterytracking-form input').keydown(function(event) + { + if (event.keyCode === 13) //Enter + { + if ($('#batterytracking-form').validator('validate').has('.has-error').length !== 0) //There is at least one validation error + { + event.preventDefault(); + return false; + } + } + }); +}); + +$('#tracked_time').on('change', function(e) +{ + var value = $('#tracked_time').val(); + var now = new Date(); + var centuryStart = Number.parseInt(now.getFullYear().toString().substring(0, 2) + '00'); + var centuryEnd = Number.parseInt(now.getFullYear().toString().substring(0, 2) + '99'); + + if (value === 'x' || value === 'X') { + value = '29991231'; + } + + if (value.length === 4 && !(Number.parseInt(value) > centuryStart && Number.parseInt(value) < centuryEnd)) + { + value = (new Date()).getFullYear().toString() + value; + } + + if (value.length === 8 && $.isNumeric(value)) + { + value = value.replace(/(\d{4})(\d{2})(\d{2})/, '$1-$2-$3'); + $('#tracked_time').val(value); + $('#batterytracking-form').validator('validate'); + } +}); + +$('#tracked_time').on('keypress', function(e) +{ + var element = $(e.target); + var value = element.val(); + var dateObj = moment(element.val(), 'YYYY-MM-DD', true); + + $('.datepicker').datepicker('hide'); + + //If input is empty and any arrow key is pressed, set date to today + if (value.length === 0 && (e.keyCode === 38 || e.keyCode === 40 || e.keyCode === 37 || e.keyCode === 39)) + { + dateObj = moment(new Date(), 'YYYY-MM-DD', true); + } + + if (dateObj.isValid()) + { + if (e.keyCode === 38) //Up + { + element.val(dateObj.add(-1, 'days').format('YYYY-MM-DD')); + } + else if (e.keyCode === 40) //Down + { + element.val(dateObj.add(1, 'days').format('YYYY-MM-DD')); + } + else if (e.keyCode === 37) //Left + { + element.val(dateObj.add(-1, 'weeks').format('YYYY-MM-DD')); + } + else if (e.keyCode === 39) //Right + { + element.val(dateObj.add(1, 'weeks').format('YYYY-MM-DD')); + } + } + + $('#batterytracking-form').validator('validate'); +}); diff --git a/views/batterytracking.php b/views/batterytracking.php new file mode 100644 index 00000000..1b57ad41 --- /dev/null +++ b/views/batterytracking.php @@ -0,0 +1,42 @@ +
+ +

Battery tracking

+ +
+ +
+ + +
+
+ +
+ +
+ + + + +
+
+
+ + + +
+ +
+ +
+

Battery overview

+ +

+ Charge cycles count:
+ Last charged:
+

+
diff --git a/views/layout.php b/views/layout.php index b52d728d..a3ce8587 100644 --- a/views/layout.php +++ b/views/layout.php @@ -58,6 +58,9 @@
  •  Habits overview
  • +
  • +  Batteries overview +