Add journal and undo UI for stock bookings, chore executions and battery charge cycles (closes #63, closes #97)

This commit is contained in:
Bernd Bestel 2018-10-27 17:26:00 +02:00
parent fe83e2fa6f
commit 364f6b2051
No known key found for this signature in database
GPG Key ID: 71BD34C0D4891300
28 changed files with 541 additions and 92 deletions

View File

@ -24,8 +24,8 @@ class BatteriesApiController extends BaseApiController
try try
{ {
$this->BatteriesService->TrackChargeCycle($args['batteryId'], $trackedTime); $chargeCycleId = $this->BatteriesService->TrackChargeCycle($args['batteryId'], $trackedTime);
return $this->VoidApiActionResponse($response); return $this->ApiResponse(array('charge_cycle_id' => $chargeCycleId));
} }
catch (\Exception $ex) catch (\Exception $ex)
{ {

View File

@ -53,4 +53,12 @@ class BatteriesController extends BaseController
]); ]);
} }
} }
public function Journal(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
return $this->AppContainer->view->render($response, 'batteriesjournal', [
'chargeCycles' => $this->Database->battery_charge_cycles()->orderBy('tracked_time', 'DESC'),
'batteries' => $this->Database->batteries()->orderBy('name')
]);
}
} }

View File

@ -30,8 +30,8 @@ class ChoresApiController extends BaseApiController
try try
{ {
$this->ChoresService->TrackChore($args['choreId'], $trackedTime, $doneBy); $choreExecutionId = $this->ChoresService->TrackChore($args['choreId'], $trackedTime, $doneBy);
return $this->VoidApiActionResponse($response); return $this->ApiResponse(array('chore_execution_id' => $choreExecutionId));
} }
catch (\Exception $ex) catch (\Exception $ex)
{ {

View File

@ -38,10 +38,10 @@ class ChoresController extends BaseController
]); ]);
} }
public function Analysis(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) public function Journal(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{ {
return $this->AppContainer->view->render($response, 'choresanalysis', [ return $this->AppContainer->view->render($response, 'choresjournal', [
'choresLog' => $this->Database->chores_log()->where('undone', 0)->orderBy('tracked_time', 'DESC'), 'choresLog' => $this->Database->chores_log()->orderBy('tracked_time', 'DESC'),
'chores' => $this->Database->chores()->orderBy('name'), 'chores' => $this->Database->chores()->orderBy('name'),
'users' => $this->Database->users()->orderBy('username') 'users' => $this->Database->users()->orderBy('username')
]); ]);

View File

@ -60,8 +60,8 @@ class StockApiController extends BaseApiController
try try
{ {
$this->StockService->AddProduct($args['productId'], $args['amount'], $bestBeforeDate, $transactionType, date('Y-m-d'), $price); $bookingId = $this->StockService->AddProduct($args['productId'], $args['amount'], $bestBeforeDate, $transactionType, date('Y-m-d'), $price);
return $this->VoidApiActionResponse($response); return $this->ApiResponse(array('booking_id' => $bookingId));
} }
catch (\Exception $ex) catch (\Exception $ex)
{ {
@ -85,8 +85,8 @@ class StockApiController extends BaseApiController
try try
{ {
$this->StockService->ConsumeProduct($args['productId'], $args['amount'], $spoiled, $transactionType); $bookingId = $this->StockService->ConsumeProduct($args['productId'], $args['amount'], $spoiled, $transactionType);
return $this->VoidApiActionResponse($response); return $this->ApiResponse(array('booking_id' => $bookingId));
} }
catch (\Exception $ex) catch (\Exception $ex)
{ {
@ -104,8 +104,8 @@ class StockApiController extends BaseApiController
try try
{ {
$this->StockService->InventoryProduct($args['productId'], $args['newAmount'], $bestBeforeDate); $bookingId = $this->StockService->InventoryProduct($args['productId'], $args['newAmount'], $bestBeforeDate);
return $this->VoidApiActionResponse($response); return $this->ApiResponse(array('booking_id' => $bookingId));
} }
catch (\Exception $ex) catch (\Exception $ex)
{ {

View File

@ -192,4 +192,13 @@ class StockController extends BaseController
]); ]);
} }
} }
public function Journal(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
return $this->AppContainer->view->render($response, 'stockjournal', [
'stockLog' => $this->Database->stock_log()->orderBy('row_created_timestamp', 'DESC'),
'products' => $this->Database->products()->orderBy('name'),
'quantityunits' => $this->Database->quantity_units()->orderBy('name')
]);
}
} }

View File

@ -184,7 +184,7 @@ return array(
'Last done by' => 'Zuletzt ausgeführt von', 'Last done by' => 'Zuletzt ausgeführt von',
'Unknown' => 'Unbekannt', 'Unknown' => 'Unbekannt',
'Filter by chore' => 'Nach Hausarbeit filtern', 'Filter by chore' => 'Nach Hausarbeit filtern',
'Chores analysis' => 'Hausarbeiten Analyse', 'Chores journal' => 'Hausarbeitenjournal',
'0 means suggestions for the next charge cycle are disabled' => '0 bedeutet dass Vorschläge für den nächsten Ladezyklus deaktiviert sind', '0 means suggestions for the next charge cycle are disabled' => '0 bedeutet dass Vorschläge für den nächsten Ladezyklus deaktiviert sind',
'Charge cycle interval (days)' => 'Ladezyklusintervall (Tage)', 'Charge cycle interval (days)' => 'Ladezyklusintervall (Tage)',
'Last price' => 'Letzter Preis', 'Last price' => 'Letzter Preis',
@ -286,11 +286,31 @@ return array(
'Edit included recipe' => 'Enthaltenes Rezept bearbeiten', 'Edit included recipe' => 'Enthaltenes Rezept bearbeiten',
'Group' => 'Gruppe', 'Group' => 'Gruppe',
'This will be used as a headline to group ingredients together' => 'Dies wird als Überschrift verwendet, um Zutaten zusammenzufassen', 'This will be used as a headline to group ingredients together' => 'Dies wird als Überschrift verwendet, um Zutaten zusammenzufassen',
'Journal' => 'Journal',
'Stock journal' => 'Bestandsjournal',
'Filter by product' => 'Nach Produkt filtern',
'Booking time' => 'Buchungszeit',
'Booking type' => 'Buchungsart',
'Undo booking' => 'Buchung rückgängig machen',
'Undone on' => 'Rückgängig gemacht am',
'Batteries journal' => 'Batteriejournal',
'Filter by battery' => 'Nach Batterie filtern',
'Undo charge cycle' => 'Ladezyklus rückgängig machen',
'Undo chore execution' => 'Ausführung rückgängig machen',
'Chore execution successfully undone' => 'Ausführung erfolgreich rückgängig gemacht',
'Undo' => 'Rückgängig machen',
'Booking successfully undone' => 'Buchung erfolgreich rückgängig gemacht',
'Charge cycle successfully undone' => 'Ladezyklus erfolgreich rückgängig gemacht',
//Constants //Constants - Chore types
'manually' => 'Manuell', 'manually' => 'Manuell',
'dynamic-regular' => 'Dynamisch regelmäßig', 'dynamic-regular' => 'Dynamisch regelmäßig',
//Constants - Stock transaction types
'purchase' => 'Einkauf',
'consume' => 'Verbrauch',
'inventory-correction' => 'Inventur-Korrektur',
//Technical component translations //Technical component translations
'timeago_locale' => 'de', 'timeago_locale' => 'de',
'timeago_nan' => 'vor NaN Jahren', 'timeago_nan' => 'vor NaN Jahren',

View File

@ -1,10 +1,15 @@
<?php <?php
return array( return array(
//Constants //Constants - Chore types
'manually' => 'Manually', 'manually' => 'Manually',
'dynamic-regular' => 'Dynamic regular', 'dynamic-regular' => 'Dynamic regular',
//Constants - Stock transaction types
'purchase' => 'Purchase',
'consume' => 'Consume',
'inventory-correction' => 'Inventory correction',
//Technical component translations //Technical component translations
'timeago_locale' => 'en', 'timeago_locale' => 'en',
'timeago_nan' => 'NaN years ago', 'timeago_nan' => 'NaN years ago',

View File

@ -184,7 +184,7 @@ return array(
'Last done by' => 'Sist utført av', 'Last done by' => 'Sist utført av',
'Unknown' => 'Ukjent', 'Unknown' => 'Ukjent',
'Filter by chore' => 'Filtrér husarbeid', 'Filter by chore' => 'Filtrér husarbeid',
'Chores analysis' => 'Statistikk husarbeid', 'Chores journal' => 'Statistikk husarbeid',
'0 means suggestions for the next charge cycle are disabled' => '0 betyr neste ladesyklus er avslått', '0 means suggestions for the next charge cycle are disabled' => '0 betyr neste ladesyklus er avslått',
'Charge cycle interval (days)' => 'Ladesyklysintervall (dager)', 'Charge cycle interval (days)' => 'Ladesyklysintervall (dager)',
'Last price' => 'Siste pris', 'Last price' => 'Siste pris',

View File

@ -0,0 +1,70 @@
var batteriesJournalTable = $('#batteries-journal-table').DataTable({
'paginate': true,
'order': [[1, 'desc']],
'columnDefs': [
{ 'orderable': false, 'targets': 0 }
],
'language': JSON.parse(L('datatables_localization')),
'scrollY': false,
'colReorder': true,
'stateSave': true,
'stateSaveParams': function(settings, data)
{
data.search.search = "";
data.columns.forEach(column =>
{
column.search.search = "";
});
}
});
$("#battery-filter").on("change", function()
{
var value = $(this).val();
var text = $("#battery-filter option:selected").text();
if (value === "all")
{
text = "";
}
batteriesJournalTable.column(1).search(text).draw();
});
$("#search").on("keyup", function()
{
var value = $(this).val();
if (value === "all")
{
value = "";
}
batteriesJournalTable.search(value).draw();
});
if (typeof GetUriParam("battery") !== "undefined")
{
$("#battery-filter").val(GetUriParam("battery"));
$("#battery-filter").trigger("change");
}
$(document).on('click', '.undo-battery-execution-button', function(e)
{
e.preventDefault();
var element = $(e.currentTarget);
var chargeCycleId = $(e.currentTarget).attr('data-charge-cycle-id');
Grocy.Api.Get('batteries/undo-charge-cycle/' + chargeCycleId.toString(),
function(result)
{
element.closest("tr").addClass("text-muted");
element.closest(".undo-battery-execution-button").addClass("disabled");
toastr.success(L("Charge cycle successfully undone"));
},
function(xhr)
{
console.error(xhr);
}
);
});

View File

@ -10,7 +10,7 @@
Grocy.Api.Get('batteries/track-charge-cycle/' + jsonForm.battery_id + '?tracked_time=' + $('#tracked_time').find('input').val(), Grocy.Api.Get('batteries/track-charge-cycle/' + jsonForm.battery_id + '?tracked_time=' + $('#tracked_time').find('input').val(),
function(result) function(result)
{ {
toastr.success(L('Tracked charge cylce of battery #1 on #2', batteryDetails.battery.name, $('#tracked_time').find('input').val())); toastr.success(L('Tracked charge cycle of battery #1 on #2', batteryDetails.battery.name, $('#tracked_time').find('input').val()) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoChargeCycle(' + result.charge_cycle_id + ')"><i class="fas fa-undo"></i> ' + L("Undo") + '</a>');
$('#battery_id').val(''); $('#battery_id').val('');
$('#battery_id_text_input').focus(); $('#battery_id_text_input').focus();
@ -86,3 +86,16 @@ $('#tracked_time').find('input').on('keypress', function (e)
Grocy.FrontendHelpers.ValidateForm('batterytracking-form'); Grocy.FrontendHelpers.ValidateForm('batterytracking-form');
}); });
function UndoChargeCycle(chargeCycleId)
{
Grocy.Api.Get('batteries/undo-charge-cycle/' + chargeCycleId.toString(),
function(result)
{
toastr.success(L("Charge cycle successfully undone"));
},
function(xhr)
{
console.error(xhr);
}
);
};

View File

@ -1,46 +0,0 @@
var choresAnalysisTable = $('#chores-analysis-table').DataTable({
'paginate': false,
'order': [[1, 'desc']],
'language': JSON.parse(L('datatables_localization')),
'scrollY': false,
'colReorder': true,
'stateSave': true,
'stateSaveParams': function(settings, data)
{
data.search.search = "";
data.columns.forEach(column =>
{
column.search.search = "";
});
}
});
$("#chore-filter").on("change", function()
{
var value = $(this).val();
var text = $("#chore-filter option:selected").text();
if (value === "all")
{
text = "";
}
choresAnalysisTable.column(0).search(text).draw();
});
$("#search").on("keyup", function()
{
var value = $(this).val();
if (value === "all")
{
value = "";
}
choresAnalysisTable.search(value).draw();
});
if (typeof GetUriParam("chore") !== "undefined")
{
$("#chore-filter").val(GetUriParam("chore"));
$("#chore-filter").trigger("change");
}

View File

@ -0,0 +1,70 @@
var choresJournalTable = $('#chores-journal-table').DataTable({
'paginate': true,
'order': [[1, 'desc']],
'columnDefs': [
{ 'orderable': false, 'targets': 0 }
],
'language': JSON.parse(L('datatables_localization')),
'scrollY': false,
'colReorder': true,
'stateSave': true,
'stateSaveParams': function(settings, data)
{
data.search.search = "";
data.columns.forEach(column =>
{
column.search.search = "";
});
}
});
$("#chore-filter").on("change", function()
{
var value = $(this).val();
var text = $("#chore-filter option:selected").text();
if (value === "all")
{
text = "";
}
choresJournalTable.column(1).search(text).draw();
});
$("#search").on("keyup", function()
{
var value = $(this).val();
if (value === "all")
{
value = "";
}
choresJournalTable.search(value).draw();
});
if (typeof GetUriParam("chore") !== "undefined")
{
$("#chore-filter").val(GetUriParam("chore"));
$("#chore-filter").trigger("change");
}
$(document).on('click', '.undo-chore-execution-button', function(e)
{
e.preventDefault();
var element = $(e.currentTarget);
var executionId = $(e.currentTarget).attr('data-execution-id');
Grocy.Api.Get('chores/undo-chore-execution/' + executionId.toString(),
function(result)
{
element.closest("tr").addClass("text-muted");
element.closest(".undo-chore-execution-button").addClass("disabled");
toastr.success(L("Chore execution successfully undone"));
},
function(xhr)
{
console.error(xhr);
}
);
});

View File

@ -10,7 +10,7 @@
Grocy.Api.Get('chores/track-chore-execution/' + jsonForm.chore_id + '?tracked_time=' + Grocy.Components.DateTimePicker.GetValue() + "&done_by=" + Grocy.Components.UserPicker.GetValue(), Grocy.Api.Get('chores/track-chore-execution/' + jsonForm.chore_id + '?tracked_time=' + Grocy.Components.DateTimePicker.GetValue() + "&done_by=" + Grocy.Components.UserPicker.GetValue(),
function(result) function(result)
{ {
toastr.success(L('Tracked execution of chore #1 on #2', choreDetails.chore.name, Grocy.Components.DateTimePicker.GetValue())); toastr.success(L('Tracked execution of chore #1 on #2', choreDetails.chore.name, Grocy.Components.DateTimePicker.GetValue()) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoChoreExecution(' + result.chore_execution_id + ')"><i class="fas fa-undo"></i> ' + L("Undo") + '</a>');
$('#chore_id').val(''); $('#chore_id').val('');
$('#chore_id_text_input').focus(); $('#chore_id_text_input').focus();
@ -82,3 +82,17 @@ Grocy.Components.DateTimePicker.GetInputElement().on('keypress', function(e)
{ {
Grocy.FrontendHelpers.ValidateForm('choretracking-form'); Grocy.FrontendHelpers.ValidateForm('choretracking-form');
}); });
function UndoChoreExecution(executionId)
{
Grocy.Api.Get('chores/undo-chore-execution/' + executionId.toString(),
function(result)
{
toastr.success(L("Chore execution successfully undone"));
},
function(xhr)
{
console.error(xhr);
}
);
};

View File

@ -16,7 +16,7 @@
Grocy.Api.Get('stock/consume-product/' + jsonForm.product_id + '/' + jsonForm.amount + '?spoiled=' + spoiled, Grocy.Api.Get('stock/consume-product/' + jsonForm.product_id + '/' + jsonForm.amount + '?spoiled=' + spoiled,
function(result) function(result)
{ {
toastr.success(L('Removed #1 #2 of #3 from stock', jsonForm.amount, Pluralize(jsonForm.amount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural), productDetails.product.name)); toastr.success(L('Removed #1 #2 of #3 from stock', jsonForm.amount, Pluralize(jsonForm.amount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural), productDetails.product.name) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoStockBooking(' + result.booking_id + ')"><i class="fas fa-undo"></i> ' + L("Undo") + '</a>');
$('#amount').val(1); $('#amount').val(1);
Grocy.Components.ProductPicker.SetValue(''); Grocy.Components.ProductPicker.SetValue('');
@ -102,3 +102,17 @@ $('#consume-form input').keydown(function(event)
} }
} }
}); });
function UndoStockBooking(bookingId)
{
Grocy.Api.Get('stock/undo-booking/' + bookingId.toString(),
function(result)
{
toastr.success(L("Booking successfully undone"));
},
function(xhr)
{
console.error(xhr);
}
);
};

View File

@ -32,7 +32,7 @@
); );
} }
toastr.success(L('Stock amount of #1 is now #2 #3', productDetails.product.name, jsonForm.new_amount, Pluralize(jsonForm.new_amount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural))); toastr.success(L('Stock amount of #1 is now #2 #3', productDetails.product.name, jsonForm.new_amount, Pluralize(jsonForm.new_amount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural)) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoStockBooking(' + result.booking_id + ')"><i class="fas fa-undo"></i> ' + L("Undo") + '</a>');
if (addBarcode !== undefined) if (addBarcode !== undefined)
{ {
@ -186,3 +186,17 @@ $('#new_amount').on('keyup', function(e)
); );
} }
}); });
function UndoStockBooking(bookingId)
{
Grocy.Api.Get('stock/undo-booking/' + bookingId.toString(),
function(result)
{
toastr.success(L("Booking successfully undone"));
},
function(xhr)
{
console.error(xhr);
}
);
};

View File

@ -5,7 +5,7 @@
var jsonForm = $('#purchase-form').serializeJSON(); var jsonForm = $('#purchase-form').serializeJSON();
Grocy.Api.Get('stock/get-product-details/' + jsonForm.product_id, Grocy.Api.Get('stock/get-product-details/' + jsonForm.product_id,
function (productDetails) function(productDetails)
{ {
var amount = jsonForm.amount * productDetails.product.qu_factor_purchase_to_stock; var amount = jsonForm.amount * productDetails.product.qu_factor_purchase_to_stock;
@ -40,7 +40,7 @@
); );
} }
toastr.success(L('Added #1 #2 of #3 to stock', amount, Pluralize(amount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural), productDetails.product.name)); toastr.success(L('Added #1 #2 of #3 to stock', amount, Pluralize(amount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural), productDetails.product.name) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoStockBooking(' + result.booking_id + ')"><i class="fas fa-undo"></i> ' + L("Undo") + '</a>');
if (addBarcode !== undefined) if (addBarcode !== undefined)
{ {
@ -171,3 +171,17 @@ $('#amount').on('change', function (e)
{ {
Grocy.FrontendHelpers.ValidateForm('purchase-form'); Grocy.FrontendHelpers.ValidateForm('purchase-form');
}); });
function UndoStockBooking(bookingId)
{
Grocy.Api.Get('stock/undo-booking/' + bookingId.toString(),
function(result)
{
toastr.success(L("Booking successfully undone"));
},
function(xhr)
{
console.error(xhr);
}
);
};

View File

@ -0,0 +1,70 @@
var stockJournalTable = $('#stock-journal-table').DataTable({
'paginate': true,
'order': [[3, 'desc']],
'columnDefs': [
{ 'orderable': false, 'targets': 0 }
],
'language': JSON.parse(L('datatables_localization')),
'scrollY': false,
'colReorder': true,
'stateSave': true,
'stateSaveParams': function(settings, data)
{
data.search.search = "";
data.columns.forEach(column =>
{
column.search.search = "";
});
}
});
$("#product-filter").on("change", function()
{
var value = $(this).val();
var text = $("#product-filter option:selected").text();
if (value === "all")
{
text = "";
}
stockJournalTable.column(1).search(text).draw();
});
$("#search").on("keyup", function()
{
var value = $(this).val();
if (value === "all")
{
value = "";
}
stockJournalTable.search(value).draw();
});
if (typeof GetUriParam("product") !== "undefined")
{
$("#product-filter").val(GetUriParam("product"));
$("#product-filter").trigger("change");
}
$(document).on('click', '.undo-stock-booking-button', function(e)
{
e.preventDefault();
var element = $(e.currentTarget);
var bookingId = $(e.currentTarget).attr('data-booking-id');
Grocy.Api.Get('stock/undo-booking/' + bookingId.toString(),
function(result)
{
element.closest("tr").addClass("text-muted");
element.closest(".undo-stock-booking-button").addClass("disabled");
toastr.success(L("Booking successfully undone"));
},
function(xhr)
{
console.error(xhr);
}
);
});

View File

@ -35,6 +35,7 @@ $app->group('', function()
$this->get('/productgroup/{productGroupId}', '\Grocy\Controllers\StockController:ProductGroupEditForm'); $this->get('/productgroup/{productGroupId}', '\Grocy\Controllers\StockController:ProductGroupEditForm');
$this->get('/shoppinglist', '\Grocy\Controllers\StockController:ShoppingList'); $this->get('/shoppinglist', '\Grocy\Controllers\StockController:ShoppingList');
$this->get('/shoppinglistitem/{itemId}', '\Grocy\Controllers\StockController:ShoppingListItemEditForm'); $this->get('/shoppinglistitem/{itemId}', '\Grocy\Controllers\StockController:ShoppingListItemEditForm');
$this->get('/stockjournal', '\Grocy\Controllers\StockController:Journal');
// Recipe routes // Recipe routes
$this->get('/recipes', '\Grocy\Controllers\RecipesController:Overview'); $this->get('/recipes', '\Grocy\Controllers\RecipesController:Overview');
@ -44,7 +45,7 @@ $app->group('', function()
// Chore routes // Chore routes
$this->get('/choresoverview', '\Grocy\Controllers\ChoresController:Overview'); $this->get('/choresoverview', '\Grocy\Controllers\ChoresController:Overview');
$this->get('/choretracking', '\Grocy\Controllers\ChoresController:TrackChoreExecution'); $this->get('/choretracking', '\Grocy\Controllers\ChoresController:TrackChoreExecution');
$this->get('/choresanalysis', '\Grocy\Controllers\ChoresController:Analysis'); $this->get('/choresjournal', '\Grocy\Controllers\ChoresController:Journal');
$this->get('/chores', '\Grocy\Controllers\ChoresController:ChoresList'); $this->get('/chores', '\Grocy\Controllers\ChoresController:ChoresList');
$this->get('/chore/{choreId}', '\Grocy\Controllers\ChoresController:ChoreEditForm'); $this->get('/chore/{choreId}', '\Grocy\Controllers\ChoresController:ChoreEditForm');
@ -52,6 +53,7 @@ $app->group('', function()
// Battery routes // Battery routes
$this->get('/batteriesoverview', '\Grocy\Controllers\BatteriesController:Overview'); $this->get('/batteriesoverview', '\Grocy\Controllers\BatteriesController:Overview');
$this->get('/batterytracking', '\Grocy\Controllers\BatteriesController:TrackChargeCycle'); $this->get('/batterytracking', '\Grocy\Controllers\BatteriesController:TrackChargeCycle');
$this->get('/batteriesjournal', '\Grocy\Controllers\BatteriesController:Journal');
$this->get('/batteries', '\Grocy\Controllers\BatteriesController:BatteriesList'); $this->get('/batteries', '\Grocy\Controllers\BatteriesController:BatteriesList');
$this->get('/battery/{batteryId}', '\Grocy\Controllers\BatteriesController:BatteryEditForm'); $this->get('/battery/{batteryId}', '\Grocy\Controllers\BatteriesController:BatteryEditForm');

View File

@ -18,14 +18,14 @@ class BatteriesService extends BaseService
} }
$battery = $this->Database->batteries($batteryId); $battery = $this->Database->batteries($batteryId);
$batteryChargeCylcesCount = $this->Database->battery_charge_cycles()->where('battery_id = :1 AND undone = 0', $batteryId)->count(); $batteryChargeCyclesCount = $this->Database->battery_charge_cycles()->where('battery_id = :1 AND undone = 0', $batteryId)->count();
$batteryLastChargedTime = $this->Database->battery_charge_cycles()->where('battery_id = :1 AND undone = 0', $batteryId)->max('tracked_time'); $batteryLastChargedTime = $this->Database->battery_charge_cycles()->where('battery_id = :1 AND undone = 0', $batteryId)->max('tracked_time');
$nextChargeTime = $this->Database->batteries_current()->where('battery_id', $batteryId)->min('next_estimated_charge_time'); $nextChargeTime = $this->Database->batteries_current()->where('battery_id', $batteryId)->min('next_estimated_charge_time');
return array( return array(
'battery' => $battery, 'battery' => $battery,
'last_charged' => $batteryLastChargedTime, 'last_charged' => $batteryLastChargedTime,
'charge_cycles_count' => $batteryChargeCylcesCount, 'charge_cycles_count' => $batteryChargeCyclesCount,
'next_estimated_charge_time' => $nextChargeTime 'next_estimated_charge_time' => $nextChargeTime
); );
} }
@ -43,7 +43,7 @@ class BatteriesService extends BaseService
)); ));
$logRow->save(); $logRow->save();
return true; return $this->Database->lastInsertId();
} }
private function BatteryExists($batteryId) private function BatteryExists($batteryId)

View File

@ -63,7 +63,7 @@ class ChoresService extends BaseService
)); ));
$logRow->save(); $logRow->save();
return true; return $this->Database->lastInsertId();
} }
private function ChoreExists($choreId) private function ChoreExists($choreId)

View File

@ -47,8 +47,8 @@ class StockService extends BaseService
$product = $this->Database->products($productId); $product = $this->Database->products($productId);
$productStockAmount = $this->Database->stock()->where('product_id', $productId)->sum('amount'); $productStockAmount = $this->Database->stock()->where('product_id', $productId)->sum('amount');
$productLastPurchased = $this->Database->stock_log()->where('product_id', $productId)->where('transaction_type', self::TRANSACTION_TYPE_PURCHASE)->max('purchased_date')->where('undone', 0); $productLastPurchased = $this->Database->stock_log()->where('product_id', $productId)->where('transaction_type', self::TRANSACTION_TYPE_PURCHASE)->where('undone', 0)->max('purchased_date');
$productLastUsed = $this->Database->stock_log()->where('product_id', $productId)->where('transaction_type', self::TRANSACTION_TYPE_CONSUME)->max('used_date')->where('undone', 0); $productLastUsed = $this->Database->stock_log()->where('product_id', $productId)->where('transaction_type', self::TRANSACTION_TYPE_CONSUME)->where('undone', 0)->max('used_date');
$nextBestBeforeDate = $this->Database->stock()->where('product_id', $productId)->min('best_before_date'); $nextBestBeforeDate = $this->Database->stock()->where('product_id', $productId)->min('best_before_date');
$quPurchase = $this->Database->quantity_units($product->qu_id_purchase); $quPurchase = $this->Database->quantity_units($product->qu_id_purchase);
$quStock = $this->Database->quantity_units($product->qu_id_stock); $quStock = $this->Database->quantity_units($product->qu_id_stock);
@ -113,6 +113,8 @@ class StockService extends BaseService
)); ));
$logRow->save(); $logRow->save();
$returnValue = $this->Database->lastInsertId();
$stockRow = $this->Database->stock()->createRow(array( $stockRow = $this->Database->stock()->createRow(array(
'product_id' => $productId, 'product_id' => $productId,
'amount' => $amount, 'amount' => $amount,
@ -123,7 +125,7 @@ class StockService extends BaseService
)); ));
$stockRow->save(); $stockRow->save();
return true; return $returnValue;
} }
else else
{ {
@ -197,7 +199,7 @@ class StockService extends BaseService
} }
} }
return true; return $this->Database->lastInsertId();
} }
else else
{ {
@ -226,7 +228,7 @@ class StockService extends BaseService
$this->ConsumeProduct($productId, $amountToRemove, false, self::TRANSACTION_TYPE_INVENTORY_CORRECTION); $this->ConsumeProduct($productId, $amountToRemove, false, self::TRANSACTION_TYPE_INVENTORY_CORRECTION);
} }
return true; return $this->Database->lastInsertId();
} }
public function AddMissingProductsToShoppingList() public function AddMissingProductsToShoppingList()

View File

@ -0,0 +1,66 @@
@extends('layout.default')
@section('title', $L('Batteries journal'))
@section('activeNav', 'batteriesjournal')
@section('viewJsName', 'batteriesjournal')
@section('content')
<div class="row">
<div class="col">
<h1>@yield('title')</h1>
</div>
</div>
<div class="row my-3">
<div class="col-xs-12 col-md-6 col-xl-3">
<label for="battery-filter">{{ $L('Filter by battery') }}</label> <i class="fas fa-filter"></i>
<select class="form-control" id="battery-filter">
<option value="all">{{ $L('All') }}</option>
@foreach($batteries as $battery)
<option value="{{ $battery->id }}">{{ $battery->name }}</option>
@endforeach
</select>
</div>
<div class="col-xs-12 col-md-6 col-xl-3">
<label for="search">{{ $L('Search') }}</label> <i class="fas fa-search"></i>
<input type="text" class="form-control" id="search">
</div>
</div>
<div class="row">
<div class="col">
<table id="batteries-journal-table" class="table table-sm table-striped dt-responsive">
<thead>
<tr>
<th>#</th>
<th>{{ $L('Battery') }}</th>
<th>{{ $L('Tracked time') }}</th>
</tr>
</thead>
<tbody>
@foreach($chargeCycles as $chargeCycleEntry)
<tr class="@if($chargeCycleEntry->undone == 1) text-muted @endif">
<td class="fit-content">
<a class="btn btn-secondary btn-sm undo-battery-execution-button @if($chargeCycleEntry->undone == 1) disabled @endif" href="#" data-charge-cycle-id="{{ $chargeCycleEntry->id }}" data-toggle="tooltip" data-placement="left" title="{{ $L('Undo charge cycle') }}">
<i class="fas fa-undo"></i>
</a>
</td>
<td>
<span class="@if($chargeCycleEntry->undone == 1) text-strike-through @endif">{{ FindObjectInArrayByPropertyValue($batteries, 'id', $chargeCycleEntry->battery_id)->name }}</span>
@if($chargeCycleEntry->undone == 1)
<br>
{{ $L('Undone on') . ' ' . $chargeCycleEntry->undone_timestamp }}
<time class="timeago timeago-contextual" datetime="{{ $chargeCycleEntry->undone_timestamp }}"></time>
@endif
</td>
<td>
{{ $chargeCycleEntry->tracked_time }}
<time class="timeago timeago-contextual" datetime="{{ $chargeCycleEntry->tracked_time }}"></time>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
@stop

View File

@ -11,7 +11,11 @@
@section('content') @section('content')
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<h1>@yield('title')</h1> <h1>@yield('title')
<a class="btn btn-outline-dark responsive-button" href="{{ $U('/batteriesjournal') }}">
<i class="fas fa-file-alt"></i> {{ $L('Journal') }}
</a>
</h1>
<p id="info-due-batteries" data-status-filter="duesoon" data-next-x-days="{{ $nextXDays }}" class="btn btn-lg btn-warning status-filter-button responsive-button mr-2"></p> <p id="info-due-batteries" data-status-filter="duesoon" data-next-x-days="{{ $nextXDays }}" class="btn btn-lg btn-warning status-filter-button responsive-button mr-2"></p>
<p id="info-overdue-batteries" data-status-filter="overdue" class="btn btn-lg btn-danger status-filter-button responsive-button"></p> <p id="info-overdue-batteries" data-status-filter="overdue" class="btn btn-lg btn-danger status-filter-button responsive-button"></p>
</div> </div>
@ -53,6 +57,9 @@
data-battery-name="{{ FindObjectInArrayByPropertyValue($batteries, 'id', $curentBatteryEntry->battery_id)->name }}"> data-battery-name="{{ FindObjectInArrayByPropertyValue($batteries, 'id', $curentBatteryEntry->battery_id)->name }}">
<i class="fas fa-fire"></i> <i class="fas fa-fire"></i>
</a> </a>
<a class="btn btn-info btn-sm" href="{{ $U('/batteriesjournal?battery=') }}{{ $curentBatteryEntry->battery_id }}">
<i class="fas fa-file-alt"></i>
</a>
</td> </td>
<td> <td>
{{ FindObjectInArrayByPropertyValue($batteries, 'id', $curentBatteryEntry->battery_id)->name }} {{ FindObjectInArrayByPropertyValue($batteries, 'id', $curentBatteryEntry->battery_id)->name }}

View File

@ -1,8 +1,8 @@
@extends('layout.default') @extends('layout.default')
@section('title', $L('Chores analysis')) @section('title', $L('Chores journal'))
@section('activeNav', 'choresanalysis') @section('activeNav', 'choresjournal')
@section('viewJsName', 'choresanalysis') @section('viewJsName', 'choresjournal')
@section('content') @section('content')
<div class="row"> <div class="row">
@ -11,7 +11,7 @@
</div> </div>
</div> </div>
<div class="row mt-3"> <div class="row my-3">
<div class="col-xs-12 col-md-6 col-xl-3"> <div class="col-xs-12 col-md-6 col-xl-3">
<label for="chore-filter">{{ $L('Filter by chore') }}</label> <i class="fas fa-filter"></i> <label for="chore-filter">{{ $L('Filter by chore') }}</label> <i class="fas fa-filter"></i>
<select class="form-control" id="chore-filter"> <select class="form-control" id="chore-filter">
@ -29,9 +29,10 @@
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<table id="chores-analysis-table" class="table table-sm table-striped dt-responsive"> <table id="chores-journal-table" class="table table-sm table-striped dt-responsive">
<thead> <thead>
<tr> <tr>
<th>#</th>
<th>{{ $L('Chore') }}</th> <th>{{ $L('Chore') }}</th>
<th>{{ $L('Tracked time') }}</th> <th>{{ $L('Tracked time') }}</th>
<th>{{ $L('Done by') }}</th> <th>{{ $L('Done by') }}</th>
@ -39,13 +40,23 @@
</thead> </thead>
<tbody> <tbody>
@foreach($choresLog as $choreLogEntry) @foreach($choresLog as $choreLogEntry)
<tr> <tr class="@if($choreLogEntry->undone == 1) text-muted @endif">
<td class="fit-content">
<a class="btn btn-secondary btn-sm undo-chore-execution-button @if($choreLogEntry->undone == 1) disabled @endif" href="#" data-execution-id="{{ $choreLogEntry->id }}" data-toggle="tooltip" data-placement="left" title="{{ $L('Undo chore execution') }}">
<i class="fas fa-undo"></i>
</a>
</td>
<td> <td>
{{ FindObjectInArrayByPropertyValue($chores, 'id', $choreLogEntry->chore_id)->name }} <span class="@if($choreLogEntry->undone == 1) text-strike-through @endif">{{ FindObjectInArrayByPropertyValue($chores, 'id', $choreLogEntry->chore_id)->name }}</span>
@if($choreLogEntry->undone == 1)
<br>
{{ $L('Undone on') . ' ' . $choreLogEntry->undone_timestamp }}
<time class="timeago timeago-contextual" datetime="{{ $choreLogEntry->undone_timestamp }}"></time>
@endif
</td> </td>
<td> <td>
{{ $choreLogEntry->tracked_time }} {{ $choreLogEntry->tracked_time }}
<time class="timeago timeago-contextual" datetime="{{ $choreLogEntry->tracked_time }}"></time> <time class="timeago timeago-contextual" datetime="{{ $choreLogEntry->tracked_time }}"></time>
</td> </td>
<td> <td>
@if ($choreLogEntry->done_by_user_id !== null && !empty($choreLogEntry->done_by_user_id)) @if ($choreLogEntry->done_by_user_id !== null && !empty($choreLogEntry->done_by_user_id))

View File

@ -11,7 +11,11 @@
@section('content') @section('content')
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<h1>@yield('title')</h1> <h1>@yield('title')
<a class="btn btn-outline-dark responsive-button" href="{{ $U('/choresjournal') }}">
<i class="fas fa-file-alt"></i> {{ $L('Journal') }}
</a>
</h1>
<p id="info-due-chores" data-status-filter="duesoon" data-next-x-days="{{ $nextXDays }}" class="btn btn-lg btn-warning status-filter-button responsive-button mr-2"></p> <p id="info-due-chores" data-status-filter="duesoon" data-next-x-days="{{ $nextXDays }}" class="btn btn-lg btn-warning status-filter-button responsive-button mr-2"></p>
<p id="info-overdue-chores" data-status-filter="overdue" class="btn btn-lg btn-danger status-filter-button responsive-button"></p> <p id="info-overdue-chores" data-status-filter="overdue" class="btn btn-lg btn-danger status-filter-button responsive-button"></p>
</div> </div>
@ -53,8 +57,8 @@
data-chore-name="{{ FindObjectInArrayByPropertyValue($chores, 'id', $curentChoreEntry->chore_id)->name }}"> data-chore-name="{{ FindObjectInArrayByPropertyValue($chores, 'id', $curentChoreEntry->chore_id)->name }}">
<i class="fas fa-play"></i> <i class="fas fa-play"></i>
</a> </a>
<a class="btn btn-info btn-sm" href="{{ $U('/choresanalysis?chore=') }}{{ $curentChoreEntry->chore_id }}"> <a class="btn btn-info btn-sm" href="{{ $U('/choresjournal?chore=') }}{{ $curentChoreEntry->chore_id }}">
<i class="fas fa-chart-line"></i> <i class="fas fa-file-alt"></i>
</a> </a>
</td> </td>
<td> <td>

View File

@ -0,0 +1,74 @@
@extends('layout.default')
@section('title', $L('Stock journal'))
@section('activeNav', 'stockjournal')
@section('viewJsName', 'stockjournal')
@section('content')
<div class="row">
<div class="col">
<h1>@yield('title')</h1>
</div>
</div>
<div class="row my-3">
<div class="col-xs-12 col-md-6 col-xl-3">
<label for="product-filter">{{ $L('Filter by product') }}</label> <i class="fas fa-filter"></i>
<select class="form-control" id="product-filter">
<option value="all">{{ $L('All') }}</option>
@foreach($products as $product)
<option value="{{ $product->id }}">{{ $product->name }}</option>
@endforeach
</select>
</div>
<div class="col-xs-12 col-md-6 col-xl-3">
<label for="search">{{ $L('Search') }}</label> <i class="fas fa-search"></i>
<input type="text" class="form-control" id="search">
</div>
</div>
<div class="row">
<div class="col">
<table id="stock-journal-table" class="table table-sm table-striped dt-responsive">
<thead>
<tr>
<th>#</th>
<th>{{ $L('Product') }}</th>
<th>{{ $L('Amount') }}</th>
<th>{{ $L('Booking time') }}</th>
<th>{{ $L('Booking type') }}</th>
</tr>
</thead>
<tbody>
@foreach($stockLog as $stockLogEntry)
<tr class="@if($stockLogEntry->undone == 1) text-muted @endif">
<td class="fit-content">
<a class="btn btn-secondary btn-sm undo-stock-booking-button @if($stockLogEntry->undone == 1) disabled @endif" href="#" data-booking-id="{{ $stockLogEntry->id }}" data-toggle="tooltip" data-placement="left" title="{{ $L('Undo booking') }}">
<i class="fas fa-undo"></i>
</a>
</td>
<td>
<span class="@if($stockLogEntry->undone == 1) text-strike-through @endif">{{ FindObjectInArrayByPropertyValue($products, 'id', $stockLogEntry->product_id)->name }}</span>
@if($stockLogEntry->undone == 1)
<br>
{{ $L('Undone on') . ' ' . $stockLogEntry->undone_timestamp }}
<time class="timeago timeago-contextual" datetime="{{ $stockLogEntry->undone_timestamp }}"></time>
@endif
</td>
<td>
{{ $stockLogEntry->amount }} {{ Pluralize($stockLogEntry->amount, FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $stockLogEntry->product_id)->qu_id_stock)->name, FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $stockLogEntry->product_id)->qu_id_stock)->name_plural) }}
</td>
<td>
{{ $stockLogEntry->row_created_timestamp }}
<time class="timeago timeago-contextual" datetime="{{ $stockLogEntry->row_created_timestamp }}"></time>
</td>
<td>
{{ $L($stockLogEntry->transaction_type) }}
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
@stop

View File

@ -19,7 +19,12 @@
@section('content') @section('content')
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<h1>@yield('title') <small id="info-current-stock" class="text-muted"></small></h1> <h1>@yield('title')
<small id="info-current-stock" class="text-muted"></small>
<a class="btn btn-outline-dark responsive-button" href="{{ $U('/stockjournal') }}">
<i class="fas fa-file-alt"></i> {{ $L('Journal') }}
</a>
</h1>
<p id="info-expiring-products" data-next-x-days="{{ $nextXDays }}" data-status-filter="expiring" class="btn btn-lg btn-warning status-filter-button responsive-button mr-2"></p> <p id="info-expiring-products" data-next-x-days="{{ $nextXDays }}" data-status-filter="expiring" class="btn btn-lg btn-warning status-filter-button responsive-button mr-2"></p>
<p id="info-expired-products" data-status-filter="expired" class="btn btn-lg btn-danger status-filter-button responsive-button mr-2"></p> <p id="info-expired-products" data-status-filter="expired" class="btn btn-lg btn-danger status-filter-button responsive-button mr-2"></p>
<p id="info-missing-products" data-status-filter="belowminstockamount" class="btn btn-lg btn-info status-filter-button responsive-button"></p> <p id="info-missing-products" data-status-filter="belowminstockamount" class="btn btn-lg btn-info status-filter-button responsive-button"></p>
@ -92,6 +97,9 @@
data-consume-amount="{{ $currentStockEntry->amount }}"> data-consume-amount="{{ $currentStockEntry->amount }}">
<i class="fas fa-utensils"></i> {{ $L('All') }} <i class="fas fa-utensils"></i> {{ $L('All') }}
</a> </a>
<a class="btn btn-info btn-sm" href="{{ $U('/stockjournal?product=') }}{{ $currentStockEntry->product_id }}">
<i class="fas fa-file-alt"></i>
</a>
</td> </td>
<td class="product-name-cell" data-product-id="{{ $currentStockEntry->product_id }}"> <td class="product-name-cell" data-product-id="{{ $currentStockEntry->product_id }}">
{{ FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name }} <i class="fas fa-info text-muted"></i> {{ FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name }} <i class="fas fa-info text-muted"></i>