Compare commits

...

32 Commits

Author SHA1 Message Date
Bernd Bestel
f4534a4bfb Update dependencies for next release 2018-10-27 17:58:05 +02:00
Bernd Bestel
89553b7fa0 Workaround for datepicker problem (fixes #100) 2018-10-27 17:56:53 +02:00
Bernd Bestel
364f6b2051 Add journal and undo UI for stock bookings, chore executions and battery charge cycles (closes #63, closes #97) 2018-10-27 17:26:00 +02:00
Bernd Bestel
fe83e2fa6f Created API endpoints to undo battery charge cycles and chore executions (references #63) 2018-10-27 10:55:30 +02:00
Bernd Bestel
1f3dd58ddf Properly display line breaks of recipe positions and shopping list item notes 2018-10-27 10:39:52 +02:00
Bernd Bestel
da98efa833 Finalize nested recipes / group recipe positions feature (closes #77) 2018-10-27 10:37:31 +02:00
Bernd Bestel
3e6cf545d7 Finalize stock booking undo API (references #63 and #97) 2018-10-27 10:19:06 +02:00
Bernd Bestel
1080c3486c Created first version of an API endpoint to undo stock bookings (references #63 and #97) 2018-10-26 22:28:58 +02:00
Bernd Bestel
cd7b6b686d Fix API keys cannot be deleted (fixes #99) 2018-10-26 20:12:48 +02:00
Bernd Bestel
b84e6da0dd Added recipe position groups (references #77) 2018-10-25 22:45:44 +02:00
Bernd Bestel
fc3a4c6899 Finish first version of nested recipes feature (references #77) 2018-10-25 20:36:29 +02:00
Bernd Bestel
12a2cb0bdf Created basis edit UI for nested recipes (references #77) 2018-10-23 19:36:39 +02:00
Bernd Bestel
57a0864465 Show productcard as a popup on stockoverview page instead of only the product picture (closes #93) 2018-10-22 19:28:59 +02:00
Bernd Bestel
b3da837ede Added link to item edit page for all item cards (product/chore/battery) (references #93) 2018-10-22 19:13:08 +02:00
Bernd Bestel
3de3e03ab3 Created database logic for nested recipes (references #77) 2018-10-21 15:02:52 +02:00
Bernd Bestel
78865a9d3c Fix product groups were empty on product presets page (references #92) 2018-10-20 15:22:32 +02:00
Bernd Bestel
5b3230d63d Properly name the quantity unit field on product presets page (references #92) 2018-10-20 15:18:58 +02:00
Bernd Bestel
04c93d937e Make presets for new products configurable (closes #92) 2018-10-20 14:55:49 +02:00
Bernd Bestel
5318e79f55 Properly pluralize quantity unit in success message on purchase/consume/inventory page 2018-10-20 14:09:19 +02:00
Bernd Bestel
7bf4421d44 Improve best before date comparison on stockoverview page (fixes #87) 2018-10-20 14:04:09 +02:00
Bernd Bestel
366152c049 Next attempt to fix the scroll issue of navigation section (references #90) 2018-10-20 11:25:22 +02:00
Bernd Bestel
70c00e81d9 Cascade changes of stock quantity unit of products to recipe positions (fixes #91) 2018-10-20 11:17:51 +02:00
Talmai Oliveira
f13abf483e Grocy docker patch fix (#94)
fixed:

Warning: Use of undefined constant GROCY_USER_ID - assumed
'GROCY_USER_ID' (this will throw an Error in a future version of PHP) in
/www/controllers/BaseController.php on line 47
2018-10-20 09:07:05 +02:00
Bernd Bestel
0e723a0a9b Fixed reported login problem ("PHP Warning: date() expects parameter 2 to be integer, float given") 2018-10-16 18:21:38 +02:00
Bernd Bestel
4a35477c35 Don't auto reload when database has changed in "fullscreen-card-mode" (fixes #88) 2018-10-13 09:18:16 +02:00
Bernd Bestel
df7d360516 Update screenshots 2018-10-11 10:33:38 +02:00
Bernd Bestel
03eaa6c79f Add possibility to filter by product group on stock overview page 2018-10-06 18:19:31 +02:00
Bernd Bestel
132999ce36 Show the little "product has an image icon" also on products page (references #58) 2018-10-06 18:08:25 +02:00
Bernd Bestel
188407e3c7 Merge branch 'master' of https://github.com/berrnd/grocy 2018-10-06 18:02:30 +02:00
Bernd Bestel
8cf68ade30 Fix missing translation (references #85) 2018-10-06 18:02:13 +02:00
Marius Boro
d62657c698 Better translation (#84)
* Update no.php

* Update no.php

minor fix
2018-10-06 17:57:21 +02:00
Bernd Bestel
3262e534dc Hotfix (will be included in v1.21.0 release): Fixed a syntax error in norwegian localization file 2018-10-06 12:08:44 +02:00
69 changed files with 1593 additions and 341 deletions

61
composer.lock generated
View File

@@ -159,20 +159,21 @@
},
{
"name": "illuminate/container",
"version": "v5.7.8",
"version": "v5.7.11",
"source": {
"type": "git",
"url": "https://github.com/illuminate/container.git",
"reference": "2582a994f2f8a153a4880de757a89ad4eeb083d7"
"reference": "4c90c3d3ba88e52da152e885d24c9f891a2ec545"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/illuminate/container/zipball/2582a994f2f8a153a4880de757a89ad4eeb083d7",
"reference": "2582a994f2f8a153a4880de757a89ad4eeb083d7",
"url": "https://api.github.com/repos/illuminate/container/zipball/4c90c3d3ba88e52da152e885d24c9f891a2ec545",
"reference": "4c90c3d3ba88e52da152e885d24c9f891a2ec545",
"shasum": ""
},
"require": {
"illuminate/contracts": "5.7.*",
"illuminate/support": "5.7.*",
"php": "^7.1.3",
"psr/container": "^1.0"
},
@@ -199,20 +200,20 @@
],
"description": "The Illuminate Container package.",
"homepage": "https://laravel.com",
"time": "2018-10-03T15:20:19+00:00"
"time": "2018-10-18T03:39:45+00:00"
},
{
"name": "illuminate/contracts",
"version": "v5.7.8",
"version": "v5.7.11",
"source": {
"type": "git",
"url": "https://github.com/illuminate/contracts.git",
"reference": "9532d673de305b0c0028c0ce60c8952b807d7bc3"
"reference": "64df81d3382d876f1c1d3d5481d89c93b61b8279"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/illuminate/contracts/zipball/9532d673de305b0c0028c0ce60c8952b807d7bc3",
"reference": "9532d673de305b0c0028c0ce60c8952b807d7bc3",
"url": "https://api.github.com/repos/illuminate/contracts/zipball/64df81d3382d876f1c1d3d5481d89c93b61b8279",
"reference": "64df81d3382d876f1c1d3d5481d89c93b61b8279",
"shasum": ""
},
"require": {
@@ -243,20 +244,20 @@
],
"description": "The Illuminate Contracts package.",
"homepage": "https://laravel.com",
"time": "2018-10-03T14:04:39+00:00"
"time": "2018-10-08T13:34:14+00:00"
},
{
"name": "illuminate/events",
"version": "v5.7.8",
"version": "v5.7.11",
"source": {
"type": "git",
"url": "https://github.com/illuminate/events.git",
"reference": "4cf622acc05592f86d4a5c77ad1a544d38e58dee"
"reference": "a8e5e3d601ad7f3571428176a578ddf03ce649d8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/illuminate/events/zipball/4cf622acc05592f86d4a5c77ad1a544d38e58dee",
"reference": "4cf622acc05592f86d4a5c77ad1a544d38e58dee",
"url": "https://api.github.com/repos/illuminate/events/zipball/a8e5e3d601ad7f3571428176a578ddf03ce649d8",
"reference": "a8e5e3d601ad7f3571428176a578ddf03ce649d8",
"shasum": ""
},
"require": {
@@ -288,20 +289,20 @@
],
"description": "The Illuminate Events package.",
"homepage": "https://laravel.com",
"time": "2018-07-26T15:27:42+00:00"
"time": "2018-10-06T18:48:42+00:00"
},
{
"name": "illuminate/filesystem",
"version": "v5.7.8",
"version": "v5.7.11",
"source": {
"type": "git",
"url": "https://github.com/illuminate/filesystem.git",
"reference": "a09fae4470494dc9867609221b46fe844f2f3b70"
"reference": "cbb5650be36d7370f7ae5f039d2143952fa58f51"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/illuminate/filesystem/zipball/a09fae4470494dc9867609221b46fe844f2f3b70",
"reference": "a09fae4470494dc9867609221b46fe844f2f3b70",
"url": "https://api.github.com/repos/illuminate/filesystem/zipball/cbb5650be36d7370f7ae5f039d2143952fa58f51",
"reference": "cbb5650be36d7370f7ae5f039d2143952fa58f51",
"shasum": ""
},
"require": {
@@ -340,20 +341,20 @@
],
"description": "The Illuminate Filesystem package.",
"homepage": "https://laravel.com",
"time": "2018-08-14T19:42:44+00:00"
"time": "2018-10-24T12:49:16+00:00"
},
{
"name": "illuminate/support",
"version": "v5.7.8",
"version": "v5.7.11",
"source": {
"type": "git",
"url": "https://github.com/illuminate/support.git",
"reference": "c7583db6703a36b7fa76254073046e0a920ed276"
"reference": "45bfc0cd080c51946f61c04e324c2b4c6df58a9d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/illuminate/support/zipball/c7583db6703a36b7fa76254073046e0a920ed276",
"reference": "c7583db6703a36b7fa76254073046e0a920ed276",
"url": "https://api.github.com/repos/illuminate/support/zipball/45bfc0cd080c51946f61c04e324c2b4c6df58a9d",
"reference": "45bfc0cd080c51946f61c04e324c2b4c6df58a9d",
"shasum": ""
},
"require": {
@@ -399,20 +400,20 @@
],
"description": "The Illuminate Support package.",
"homepage": "https://laravel.com",
"time": "2018-10-04T13:27:30+00:00"
"time": "2018-10-22T17:36:06+00:00"
},
{
"name": "illuminate/view",
"version": "v5.7.8",
"version": "v5.7.11",
"source": {
"type": "git",
"url": "https://github.com/illuminate/view.git",
"reference": "86b8c60e502286135d9c91b0836a58445c4998b5"
"reference": "97dbb6910aa5df5a7414877da89b7520f4260a58"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/illuminate/view/zipball/86b8c60e502286135d9c91b0836a58445c4998b5",
"reference": "86b8c60e502286135d9c91b0836a58445c4998b5",
"url": "https://api.github.com/repos/illuminate/view/zipball/97dbb6910aa5df5a7414877da89b7520f4260a58",
"reference": "97dbb6910aa5df5a7414877da89b7520f4260a58",
"shasum": ""
},
"require": {
@@ -447,7 +448,7 @@
],
"description": "The Illuminate View package.",
"homepage": "https://laravel.com",
"time": "2018-10-02T13:51:18+00:00"
"time": "2018-10-11T15:32:19+00:00"
},
{
"name": "morris/lessql",

View File

@@ -38,6 +38,9 @@ DefaultUserSetting('auto_night_mode_time_range_from', "20:00"); // Format HH:mm
DefaultUserSetting('auto_night_mode_time_range_to', "07:00"); // Format HH:mm
DefaultUserSetting('auto_night_mode_time_range_goes_over_midnight', true); // If the time range above goes over midnight
DefaultUserSetting('currently_inside_night_mode_range', false); // If we're currently inside of night mode time range (this is not user configurable, but stored as a user setting because it's evaluated client side to be able to use the client time instead of the maybe different server time)
DefaultUserSetting('product_presets_location_id', -1); // Default location id for new products (-1 means no location is preset)
DefaultUserSetting('product_presets_product_group_id', -1); // Default product group id for new products (-1 means no product group is preset)
DefaultUserSetting('product_presets_qu_id', -1); // Default quantity unit id for new products (-1 means no quantity unit is preset)
# If the page should be automatically reloaded when there was
# an external change

View File

@@ -44,8 +44,10 @@ class BaseController
try {
$usersService = new UsersService();
if (defined('GROCY_USER_ID')) {
$container->view->set('userSettings', $usersService->GetUserSettings(GROCY_USER_ID));
}
}
catch (\Exception $ex)
{
// Happens when database is not initialised or migrated...

View File

@@ -24,8 +24,8 @@ class BatteriesApiController extends BaseApiController
try
{
$this->BatteriesService->TrackChargeCycle($args['batteryId'], $trackedTime);
return $this->VoidApiActionResponse($response);
$chargeCycleId = $this->BatteriesService->TrackChargeCycle($args['batteryId'], $trackedTime);
return $this->ApiResponse(array('charge_cycle_id' => $chargeCycleId));
}
catch (\Exception $ex)
{
@@ -49,4 +49,17 @@ class BatteriesApiController extends BaseApiController
{
return $this->ApiResponse($this->BatteriesService->GetCurrent());
}
public function UndoChargeCycle(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
try
{
$this->ApiResponse($this->BatteriesService->UndoChargeCycle($args['chargeCycleId']));
return $this->ApiResponse(array('success' => true));
}
catch (\Exception $ex)
{
return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage());
}
}
}

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
{
$this->ChoresService->TrackChore($args['choreId'], $trackedTime, $doneBy);
return $this->VoidApiActionResponse($response);
$choreExecutionId = $this->ChoresService->TrackChore($args['choreId'], $trackedTime, $doneBy);
return $this->ApiResponse(array('chore_execution_id' => $choreExecutionId));
}
catch (\Exception $ex)
{
@@ -55,4 +55,17 @@ class ChoresApiController extends BaseApiController
{
return $this->ApiResponse($this->ChoresService->GetCurrent());
}
public function UndoChoreExecution(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
try
{
$this->ApiResponse($this->ChoresService->UndoChoreExecution($args['executionId']));
return $this->ApiResponse(array('success' => true));
}
catch (\Exception $ex)
{
return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage());
}
}
}

View File

@@ -38,9 +38,9 @@ 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()->orderBy('tracked_time', 'DESC'),
'chores' => $this->Database->chores()->orderBy('name'),
'users' => $this->Database->users()->orderBy('username')

View File

@@ -6,7 +6,7 @@ class GenericEntityApiController extends BaseApiController
{
public function GetObjects(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
if ($this->IsValidEntity($args['entity']))
if ($this->IsValidEntity($args['entity']) && !$this->IsEntityWithPreventedListing($args['entity']))
{
return $this->ApiResponse($this->Database->{$args['entity']}());
}
@@ -18,7 +18,7 @@ class GenericEntityApiController extends BaseApiController
public function GetObject(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
if ($this->IsValidEntity($args['entity']))
if ($this->IsValidEntity($args['entity']) && !$this->IsEntityWithPreventedListing($args['entity']))
{
return $this->ApiResponse($this->Database->{$args['entity']}($args['objectId']));
}
@@ -77,4 +77,9 @@ class GenericEntityApiController extends BaseApiController
{
return in_array($entity, $this->OpenApiSpec->components->internalSchemas->ExposedEntity->enum);
}
private function IsEntityWithPreventedListing($entity)
{
return in_array($entity, $this->OpenApiSpec->components->internalSchemas->ExposedEntitiesPreventListing->enum);
}
}

View File

@@ -30,7 +30,7 @@ class LoginController extends BaseController
if ($user !== null && password_verify($inputPassword, $user->password))
{
$sessionKey = $this->SessionService->CreateSession($user->id, $stayLoggedInPermanently);
setcookie($this->SessionCookieName, $sessionKey, time() + 31220640000); // Cookie expires in 999 years, but session validity is up to SessionService
setcookie($this->SessionCookieName, $sessionKey, intval(time() + 31220640000)); // Cookie expires in 999 years, but session validity is up to SessionService
if (password_needs_rehash($user->password, PASSWORD_DEFAULT))
{

View File

@@ -23,18 +23,21 @@ class RecipesController extends BaseController
if (isset($request->getQueryParams()['recipe']))
{
$selectedRecipe = $this->Database->recipes($request->getQueryParams()['recipe']);
$selectedRecipePositions = $this->Database->recipes_pos()->where('recipe_id', $request->getQueryParams()['recipe']);
$selectedRecipePositions = $this->Database->recipes_pos()->where('recipe_id', $request->getQueryParams()['recipe'])->orderBy('ingredient_group');
}
else
{
foreach ($recipes as $recipe)
{
$selectedRecipe = $recipe;
$selectedRecipePositions = $this->Database->recipes_pos()->where('recipe_id', $recipe->id);
$selectedRecipePositions = $this->Database->recipes_pos()->where('recipe_id', $recipe->id)->orderBy('ingredient_group');
break;
}
}
$selectedRecipeSubRecipes = $this->Database->recipes()->where('id IN (SELECT includes_recipe_id FROM recipes_nestings_resolved WHERE recipe_id = :1 AND includes_recipe_id != :1)', $selectedRecipe->id)->orderBy('name')->fetchAll();
$selectedRecipeSubRecipesPositions = $this->Database->recipes_pos()->where('recipe_id IN (SELECT includes_recipe_id FROM recipes_nestings_resolved WHERE recipe_id = :1 AND includes_recipe_id != :1)', $selectedRecipe->id)->orderBy('ingredient_group')->fetchAll();
return $this->AppContainer->view->render($response, 'recipes', [
'recipes' => $recipes,
'recipesFulfillment' => $this->RecipesService->GetRecipesFulfillment(),
@@ -42,7 +45,9 @@ class RecipesController extends BaseController
'selectedRecipe' => $selectedRecipe,
'selectedRecipePositions' => $selectedRecipePositions,
'products' => $this->Database->products(),
'quantityunits' => $this->Database->quantity_units()
'quantityunits' => $this->Database->quantity_units(),
'selectedRecipeSubRecipes' => $selectedRecipeSubRecipes,
'selectedRecipeSubRecipesPositions' => $selectedRecipeSubRecipesPositions
]);
}
@@ -66,7 +71,9 @@ class RecipesController extends BaseController
'products' => $this->Database->products(),
'quantityunits' => $this->Database->quantity_units(),
'recipesFulfillment' => $this->RecipesService->GetRecipesFulfillment(),
'recipesSumFulfillment' => $this->RecipesService->GetRecipesSumFulfillment()
'recipesSumFulfillment' => $this->RecipesService->GetRecipesSumFulfillment(),
'recipes' => $this->Database->recipes()->orderBy('name'),
'recipeNestings' => $this->Database->recipes_nestings()->where('recipe_id', $recipeId)
]);
}

View File

@@ -60,8 +60,8 @@ class StockApiController extends BaseApiController
try
{
$this->StockService->AddProduct($args['productId'], $args['amount'], $bestBeforeDate, $transactionType, date('Y-m-d'), $price);
return $this->VoidApiActionResponse($response);
$bookingId = $this->StockService->AddProduct($args['productId'], $args['amount'], $bestBeforeDate, $transactionType, date('Y-m-d'), $price);
return $this->ApiResponse(array('booking_id' => $bookingId));
}
catch (\Exception $ex)
{
@@ -85,8 +85,8 @@ class StockApiController extends BaseApiController
try
{
$this->StockService->ConsumeProduct($args['productId'], $args['amount'], $spoiled, $transactionType);
return $this->VoidApiActionResponse($response);
$bookingId = $this->StockService->ConsumeProduct($args['productId'], $args['amount'], $spoiled, $transactionType);
return $this->ApiResponse(array('booking_id' => $bookingId));
}
catch (\Exception $ex)
{
@@ -104,8 +104,8 @@ class StockApiController extends BaseApiController
try
{
$this->StockService->InventoryProduct($args['productId'], $args['newAmount'], $bestBeforeDate);
return $this->VoidApiActionResponse($response);
$bookingId = $this->StockService->InventoryProduct($args['productId'], $args['newAmount'], $bestBeforeDate);
return $this->ApiResponse(array('booking_id' => $bookingId));
}
catch (\Exception $ex)
{
@@ -165,4 +165,17 @@ class StockApiController extends BaseApiController
return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage());
}
}
public function UndoBooking(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
try
{
$this->ApiResponse($this->StockService->UndoBooking($args['bookingId']));
return $this->ApiResponse(array('success' => true));
}
catch (\Exception $ex)
{
return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage());
}
}
}

View File

@@ -23,7 +23,8 @@ class StockController extends BaseController
'locations' => $this->Database->locations()->orderBy('name'),
'currentStock' => $this->StockService->GetCurrentStock(),
'missingProducts' => $this->StockService->GetMissingProducts(),
'nextXDays' => 5
'nextXDays' => 5,
'productGroups' => $this->Database->product_groups()->orderBy('name')
]);
}
@@ -69,6 +70,15 @@ class StockController extends BaseController
]);
}
public function ProductDefaults(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
return $this->AppContainer->view->render($response, 'productpresets', [
'locations' => $this->Database->locations()->orderBy('name'),
'quantityunits' => $this->Database->quantity_units()->orderBy('name'),
'productGroups' => $this->Database->product_groups()->orderBy('name')
]);
}
public function LocationsList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
return $this->AppContainer->view->render($response, 'locations', [
@@ -182,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

@@ -1288,6 +1288,47 @@
}
}
},
"/stock/undo-booking/{bookingId}": {
"get": {
"description": "Undoes a booking",
"tags": [
"Stock"
],
"parameters": [
{
"in": "path",
"name": "bookingId",
"required": true,
"description": "A valid stock booking id",
"schema": {
"type": "integer"
}
}
],
"responses": {
"200": {
"description": "A VoidApiActionResponse object",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/VoidApiActionResponse"
}
}
}
},
"400": {
"description": "A VoidApiActionResponse object (possible errors are: Not existing booking)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorExampleVoidApiActionResponse"
}
}
}
}
}
}
},
"/recipes/add-not-fulfilled-products-to-shopping-list/{recipeId}": {
"get": {
"description": "Adds all missing products for the given recipe to the shopping list",
@@ -1473,6 +1514,47 @@
}
}
},
"/chores/undo-chore-execution/{executionId}": {
"get": {
"description": "Undoes a chore execution",
"tags": [
"Chores"
],
"parameters": [
{
"in": "path",
"name": "executionId",
"required": true,
"description": "A valid chore execution id",
"schema": {
"type": "integer"
}
}
],
"responses": {
"200": {
"description": "A VoidApiActionResponse object",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/VoidApiActionResponse"
}
}
}
},
"400": {
"description": "A VoidApiActionResponse object (possible errors are: Not existing booking)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorExampleVoidApiActionResponse"
}
}
}
}
}
}
},
"/batteries/track-charge-cycle/{batteryId}": {
"get": {
"description": "Tracks a charge cycle of the given battery",
@@ -1588,6 +1670,47 @@
}
}
},
"/batteries/undo-charge-cycle/{chargeCycleId}": {
"get": {
"description": "Undoes a chore execution",
"tags": [
"Batteries"
],
"parameters": [
{
"in": "path",
"name": "chargeCycleId",
"required": true,
"description": "A valid charge cycle id",
"schema": {
"type": "integer"
}
}
],
"responses": {
"200": {
"description": "A VoidApiActionResponse object",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/VoidApiActionResponse"
}
}
}
},
"400": {
"description": "A VoidApiActionResponse object (possible errors are: Not existing booking)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorExampleVoidApiActionResponse"
}
}
}
}
}
}
},
"/tasks/get-current": {
"get": {
"description": "Returns all tasks which are not done yet",
@@ -1675,10 +1798,18 @@
"shopping_list",
"recipes",
"recipes_pos",
"recipes_nestings",
"tasks",
"task_categories",
"product_groups",
"equipment"
"equipment",
"api_keys"
]
},
"ExposedEntitiesPreventListing": {
"type": "string",
"enum": [
"api_keys"
]
},
"StockTransactionType": {

View File

@@ -184,7 +184,7 @@ return array(
'Last done by' => 'Zuletzt ausgeführt von',
'Unknown' => 'Unbekannt',
'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',
'Charge cycle interval (days)' => 'Ladezyklusintervall (Tage)',
'Last price' => 'Letzter Preis',
@@ -277,11 +277,40 @@ return array(
'Current instruction manual' => 'Aktuelle Bedienungsanleitung',
'No instruction manual available' => 'Keine Bedienungsanleitung vorhanden',
'The current instruction manual will be deleted when you save the equipment' => 'Die aktuelle Bedienungsanleitung wird beim Speichern des Geräts gelöscht',
'No picture available' => 'Kein Bild vorhanden',
'Filter by product group' => 'Nach Produktgruppe filtern',
'Presets for new products' => 'Vorgaben für neue Produkte',
'Included recipes' => 'Enthaltene Rezepte',
'A recipe is required' => 'Ein Rezept ist erforderlich',
'Add included recipe' => 'Enthaltenes Rezept hinzufügen',
'Edit included recipe' => 'Enthaltenes Rezept bearbeiten',
'Group' => 'Gruppe',
'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',
'dynamic-regular' => 'Dynamisch regelmäßig',
//Constants - Stock transaction types
'purchase' => 'Einkauf',
'consume' => 'Verbrauch',
'inventory-correction' => 'Inventur-Korrektur',
//Technical component translations
'timeago_locale' => 'de',
'timeago_nan' => 'vor NaN Jahren',
@@ -361,5 +390,15 @@ return array(
'Vegetables/Fruits' => 'Obst/Gemüse',
'Refrigerated products' => 'Kühlregal',
'Coffee machine' => 'Kaffeemaschine',
'Dishwasher' => 'Spülmaschine'
'Dishwasher' => 'Spülmaschine',
'Liter' => 'Liter',
'Liters' => 'Liter',
'Bottle' => 'Flasche',
'Bottles' => 'Flaschen',
'Milk' => 'Milch',
'Chocolate sauce' => 'Schokoladensoße',
'Milliliters' => 'Milliliter',
'Milliliter' => 'Milliliter',
'Bottom' => 'Boden',
'Topping' => 'Belag'
);

View File

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

View File

@@ -9,14 +9,14 @@ return array(
'Amount' => 'Antall',
'Next best before date' => 'Kommende best før dato',
'Logout' => 'Logg ut',
'Chores overview' => 'Oversikt Husarbeid',
'Batteries overview' => 'Oversikt Batteri',
'Chores overview' => 'Oversikt husarbeid',
'Batteries overview' => 'Oversikt batteri',
'Purchase' => 'Innkjøp',
'Consume' => 'Forbruk produkt',
'Inventory' => 'Endre Husholdning',
'Inventory' => 'Endre husholdning',
'Shopping list' => 'Handleliste',
'Chore tracking' => 'Logge Husarbeid',
'Battery tracking' => 'Batteri Ladesyklus',
'Chore tracking' => 'Logge husarbeid',
'Battery tracking' => 'Batteri ladesyklus',
'Products' => 'Produkter',
'Locations' => 'Lokasjoner',
'Quantity units' => 'Forpakning',
@@ -41,9 +41,9 @@ return array(
'New amount' => 'Nytt antall',
'Note' => 'Info',
'Tracked time' => 'Tid utført/ ladet',
'Chore overview' => 'Oversikt Husarbeid',
'Chore overview' => 'Oversikt husarbeid',
'Tracked count' => 'Antall utførelser/ ladninger',
'Battery overview' => 'Batteri Oversikt',
'Battery overview' => 'Batteri oversikt',
'Charge cycles count' => 'Antall ladesykluser',
'Create shopping list item' => 'Opprett handelisteoppføring',
'Edit shopping list item' => 'Endre på handlelistoppføring',
@@ -53,9 +53,9 @@ return array(
'Name' => 'Navn',
'Location' => 'Lokasjon',
'Min. stock amount' => 'Minimums antall for husholdingen',
'QU purchase' => 'FPK innkjøp',
'QU stock' => 'FPK husholdning',
'QU factor' => 'FPK faktor',
'QU purchase' => 'Forpakingsfaktor innkjøp',
'QU stock' => 'Forpakingsfaktor husholdning',
'QU factor' => 'Forpakingsfaktor',
'Description' => 'Beskrivelse',
'Create product' => 'Opprett produkt',
'Barcode(s)' => 'Strekkode(r)',
@@ -87,16 +87,16 @@ return array(
'Username' => 'Brukernavn',
'Password' => 'Passord',
'Invalid credentials, please try again' => 'Feil brukernavn og/eller passord, prøv igjen',
'Are you sure to delete battery "#1"?' => 'Er du sikker du ønsker å slette Batteri "#1"?',
'Are you sure to delete battery "#1"?' => 'Er du sikker du ønsker å slette batteri "#1"?',
'Yes' => 'Ja',
'No' => 'Nei',
'Are you sure to delete chore "#1"?' => 'Er du sikker på du ønsker å slette husarbeid oppgave "#1"?',
'"#1" could not be resolved to a product, how do you want to proceed?' => '"#1" kunne ikke bli tildelt et produkt, hvordan ønsker du å fortsette?',
'Create or assign product' => 'Opprett eller tildel til produkt',
'Create or assign product' => 'Opprett eller tildel til et produkt',
'Cancel' => 'Avbryt',
'Add as new product' => 'Legg til som nytt produkt',
'Add as barcode to existing product' => 'Legg til strekkode til allerede eksisterende produkt',
'Add as new product and prefill barcode' => 'Legg til som nytt produkt med forhåndsutfylt strekkode',
'Add as new product and prefill barcode' => 'Legg til som nytt produkt med forhåndsfylt strekkode',
'Are you sure to delete quantity unit "#1"?' => 'Er du sikker du ønsker å slette forpakning "#1"?',
'Are you sure to delete product "#1"?' => 'Er du sikker du ønsker å slette produkt "#1"?',
'Are you sure to delete location "#1"?' => 'Er du sikker du ønsker å slette lokasjon "#1"?',
@@ -114,10 +114,10 @@ return array(
'Removed #1 #2 of #3 from stock' => 'Fjernet #1 #2 #3 fra husholdningen',
'About grocy' => 'Om Grocy',
'Close' => 'Lukk',
'#1 batteries are due to be charged within the next #2 days' => '#1 Batteri må lades innen de #2 neste dagene',
'#1 batteries are due to be charged within the next #2 days' => '#1 batteri må lades innen de #2 neste dagene',
'#1 batteries are overdue to be charged' => '#1 Batteri har gått over fristen for å bli ladet opp',
'#1 chores are due to be done within the next #2 days' => '#1 husarbeid(s) oppgave(r) skal gjøres inne de #2 neste dagene',
'#1 chores are overdue to be done' => '#1 husarbeid(s) oppgave(r) har gått over fristen for utførelse',
'#1 chores are due to be done within the next #2 days' => '#1 husarbeids oppgaver skal gjøres inne de #2 neste dagene',
'#1 chores are overdue to be done' => '#1 husarbeids oppgaver har gått over fristen for utførelse',
'Released on' => 'Utgitt',
'Consume #3 #1 of #2' => 'Forbruk #3 #1 #2',
'Added #1 #2 of #3 to stock' => '#1 #2 #3 lagt til i husholdningen',
@@ -132,7 +132,7 @@ return array(
'Search' => 'Søk',
'Not logged in' => 'Ikke logget inn',
'You have to select a product' => 'Du må velge et produkt',
'You have to select a chore' => 'Du må velge en husarbeid oppgave',
'You have to select a chore' => 'Du må velge en husarbeids oppgave',
'You have to select a battery' => 'Du må velge et batteri',
'A name is required' => 'Vennligst fyll inn et navn',
'A location is required' => 'En lokasjon kreves',
@@ -153,7 +153,7 @@ return array(
'Are you sure to delete recipe "#1"?' => 'Er du sikker du ønsker å slette oppskrift "#1"?',
'Are you sure to delete recipe ingredient "#1"?' => 'Er du sikker du ønsker å slette ingrediens "#1" fra oppskriften?',
'Are you sure to empty the shopping list?' => 'Er du sikker du ønsker å slette handlelisten?',
'Clear list' => 'Tøm liste',
'Clear list' => 'Slett handleliste',
'Requirements fulfilled' => 'Har jeg alt jeg trenger for denne oppskriften?',
'Put missing products on shopping list' => 'Legg manglende produkter til handlelisten',
'Not enough in stock, #1 ingredients missing' => 'Ikke nok i husholdningen, #1 ingredienser mangler',
@@ -184,9 +184,9 @@ return array(
'Last done by' => 'Sist utført av',
'Unknown' => 'Ukjent',
'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',
'Charge cycle interval (days)' => 'Ladesyklysintervall (Dager)',
'Charge cycle interval (days)' => 'Ladesyklysintervall (dager)',
'Last price' => 'Siste pris',
'Price history' => 'Prishistorikk',
'No price history available' => 'Ingen prishistorikk tilgjengelig',
@@ -198,8 +198,8 @@ return array(
'#1 product is below defined min. stock amount' => '#1 Produkt er under minimums husholdningsnivå',
'Unit' => 'Enhet',
'Units' => 'Enheter',
'#1 chore is due to be done within the next #2 days' => '#1 husarbeid oppgave(r) skal gjøres inne de #2 neste dagene',
'#1 chore is overdue to be done' => '#1 husarbeid(s) oppgave(r) har gått over fristen for utførelse',
'#1 chore is due to be done within the next #2 days' => '#1 husarbeid oppgave skal gjøres inne de #2 neste dagene',
'#1 chore is overdue to be done' => '#1 husarbeid oppgave har gått over fristen for utførelse',
'#1 battery is due to be charged within the next #2 days' => '#1 Batteri må lades innen #2 dager',
'#1 battery is overdue to be charged' => '#1 Batteri har gått over fristen for å lades',
'#1 unit was automatically added and will apply in addition to the amount entered here' => '#1 enhet ble automatisk lagt til i tillegg til hva som blir skrevet inn her',
@@ -209,8 +209,8 @@ return array(
'This cannot be lower than #1' => 'Dette kan ikke være lavere enn #1',
'-1 means that this product never expires' => '-1 Betyr at dette produktet aldri går ut på dato',
'Quantity unit' => 'Forpakning',
'Only check if a single unit is in stock (a different quantity can then be used above)' => 'Huk av hvis du ønsker å bruke mindre enn forpakningsstørrelse i husholdningen',
'Are you sure to consume all ingredients needed by recipe "#1" (ingredients marked with "check only if a single unit is in stock" will be ignored)?' => 'Er du sikker du ønsker å forbruke alle ingredienser for "#1" oppskriften? (Ingredienser merket med "bruke mindre enn forpakningsstørrelse i husholdningen" blir ignorert',
'Only check if a single unit is in stock (a different quantity can then be used above)' => 'Ønsker du å bruke mindre enn forpakningsstørrelse?',
'Are you sure to consume all ingredients needed by recipe "#1" (ingredients marked with "check only if a single unit is in stock" will be ignored)?' => 'Er du sikker du ønsker å forbruke alle ingredienser for "#1" oppskriften? (Ingredienser merket med "Ønsker du å bruke mindre enn forpakningsstørrelse?" blir ignorert',
'Removed all ingredients of recipe "#1" from stock' => 'Fjern alle ingredienser for "#1" oppskriften fra husholdningen.',
'Consume all ingredients needed by this recipe' => 'Forbruk alle ingredienser for denne oppskriften',
'Click to show technical details' => 'Klikk for å vise teknisk informasjon',
@@ -236,7 +236,7 @@ return array(
'Edit task category' => 'Endre oppgave kategori',
'Create task category' => 'Opprett oppgave kategori',
'Product groups' => 'Produktgrupper',
'Ungrouped' => 'Ikke i grupper',
'Ungrouped' => 'Mangler gruppe',
'Create product group' => 'Opprett produkt gruppe',
'Edit product group' => 'Endre produkt gruppe',
'Product group' => 'Produktgruppe',
@@ -277,6 +277,7 @@ return array(
'Current instruction manual' => 'Nåværende instruksjonsmanual',
'No instruction manual available' => 'Ingen instruksjonsmanual tilgjengelig',
'The current instruction manual will be deleted when you save the equipment' => 'Nåværende instruksjonsmanual vil bli slettet når du lagrer utstyret',
'No picture available' => 'Ingen bilde tilgjengelig',
//Constants
'manually' => 'Manuel',
@@ -355,11 +356,11 @@ return array(
'Fork and improve grocy' => 'Fork og forbedre grocy',
'Find a solution for what to do when I forget the door keys' => 'Finne på løsning for hva jeg skal gjøre når jeg mister dørnøklene',
'Sweets' => 'Godteri',
'Bakery products' => 'Produkt fra bakeren ',
'Bakery products' => 'Bakevarer',
'Tinned food' => 'Boksemat',
'Butchery products' => 'Produkt fra slakteren',
'Butchery products' => 'Kjøtt/ Ferskvare',
'Vegetables/Fruits' => 'Frukt/ Grønnsaker',
'Refrigerated products' => 'Kjølte produkter'
'Refrigerated products' => 'Frysedisk',
'Coffee machine' => 'Kaffetrakter',
'Dishwasher' => 'Oppvaskmaskin'
);

7
migrations/0042.sql Normal file
View File

@@ -0,0 +1,7 @@
CREATE TRIGGER cascade_change_qu_id_stock AFTER UPDATE ON products
BEGIN
UPDATE recipes_pos
SET qu_id = (SELECT qu_id_stock FROM products WHERE id = NEW.id)
WHERE product_id IN (SELECT id FROM products WHERE id = NEW.id)
AND only_check_single_unit_in_stock = 0;
END;

43
migrations/0043.sql Normal file
View File

@@ -0,0 +1,43 @@
CREATE TABLE recipes_nestings (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
recipe_id INTEGER NOT NULL,
includes_recipe_id INTEGER NOT NULL,
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime')),
UNIQUE(recipe_id, includes_recipe_id)
);
CREATE VIEW recipes_nestings_resolved
AS
WITH RECURSIVE r1(recipe_id, includes_recipe_id)
AS (
SELECT id, id
FROM recipes
UNION ALL
SELECT rn.recipe_id, r1.includes_recipe_id
FROM recipes_nestings rn, r1 r1
WHERE rn.includes_recipe_id = r1.recipe_id
LIMIT 100 -- This is just a safety limit to prevent infinite loops due to infinite nested recipes
)
SELECT *
FROM r1;
DROP VIEW recipes_fulfillment_sum;
CREATE VIEW recipes_fulfillment_sum
AS
SELECT
r.id AS recipe_id,
IFNULL(MIN(rf.need_fulfilled), 1) AS need_fulfilled,
IFNULL(MIN(rf.need_fulfilled_with_shopping_list), 1) AS need_fulfilled_with_shopping_list,
(SELECT COUNT(*) FROM recipes_fulfillment WHERE recipe_id IN (SELECT includes_recipe_id FROM recipes_nestings_resolved rnr2 WHERE rnr2.recipe_id = r.id) AND need_fulfilled = 0 AND recipe_pos_id IS NOT NULL) AS missing_products_count
FROM recipes r
LEFT JOIN recipes_nestings_resolved rnr
ON r.id = rnr.recipe_id
LEFT JOIN recipes_fulfillment rf
ON rnr.includes_recipe_id = rf.recipe_id
GROUP BY r.id;
ALTER TABLE recipes_pos
ADD ingredient_group TEXT;

26
migrations/0044.sql Normal file
View File

@@ -0,0 +1,26 @@
ALTER TABLE stock_log
ADD undone TINYINT NOT NULL DEFAULT 0 CHECK(undone IN (0, 1));
UPDATE stock_log
SET undone = 0;
ALTER TABLE stock_log
ADD undone_timestamp DATETIME;
ALTER TABLE chores_log
ADD undone TINYINT NOT NULL DEFAULT 0 CHECK(undone IN (0, 1));
UPDATE chores_log
SET undone = 0;
ALTER TABLE chores_log
ADD undone_timestamp DATETIME;
ALTER TABLE battery_charge_cycles
ADD undone TINYINT NOT NULL DEFAULT 0 CHECK(undone IN (0, 1));
UPDATE battery_charge_cycles
SET undone = 0;
ALTER TABLE battery_charge_cycles
ADD undone_timestamp DATETIME;

View File

@@ -26,7 +26,7 @@
"summernote": "^0.8.10",
"swagger-ui-dist": "^3.17.3",
"tagmanager": "https://github.com/max-favilli/tagmanager.git#3.0.2",
"tempusdominus-bootstrap-4": "^5.0.1",
"tempusdominus-bootstrap-4": "^5.1.2",
"timeago": "^1.6.3",
"toastr": "^2.1.4"
}

View File

@@ -81,12 +81,6 @@ input::-webkit-inner-spin-button {
-webkit-appearance: none;
}
.centered-dialog .modal-title,
.centered-dialog .modal-body {
margin-left: auto;
margin-right: auto;
}
/* Navigation style customizations */
#mainNav {
background-color: #e5e5e5 !important;
@@ -95,7 +89,8 @@ input::-webkit-inner-spin-button {
}
.navbar-sidenav {
overflow: hidden;
overflow-x: hidden;
overflow-y: auto;
border-top: 2px solid !important;
}

View File

@@ -283,9 +283,14 @@ $("form").on("click", "select", function()
$(".user-setting-control").on("change", function()
{
var element = $(this);
var inputType = element.attr("type").toLowerCase();
var settingKey = element.attr("data-setting-key");
var inputType = "unknown";
if (typeof element.attr("type") !== typeof undefined && element.attr("type") !== false)
{
inputType = element.attr("type").toLowerCase();
}
if (inputType === "checkbox")
{
value = element.is(":checked");
@@ -338,6 +343,5 @@ ResizeResponsiveEmbeds = function(fillEntireViewport = false)
}
$(window).on('resize', function()
{
console.log($("body").hasClass("fullscreen-responsive-embed-active"));
ResizeResponsiveEmbeds($("body").hasClass("fullscreen-responsive-embed-active"));
ResizeResponsiveEmbeds($("body").hasClass("fullscreen-card"));
});

View File

@@ -22,7 +22,7 @@ setInterval(function()
{
if (Grocy.IdleTime >= 50)
{
if (BoolVal(Grocy.UserSettings.auto_reload_on_db_change) && $("form.is-dirty").length === 0)
if (BoolVal(Grocy.UserSettings.auto_reload_on_db_change) && $("form.is-dirty").length === 0 && !$("body").hasClass("fullscreen-card"))
{
window.location.reload();
}

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(),
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_text_input').focus();
@@ -86,3 +86,16 @@ $('#tracked_time').find('input').on('keypress', function (e)
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(),
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_text_input').focus();
@@ -82,3 +82,17 @@ Grocy.Components.DateTimePicker.GetInputElement().on('keypress', function(e)
{
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

@@ -11,6 +11,9 @@ Grocy.Components.BatteryCard.Refresh = function(batteryId)
$('#batterycard-battery-last-charged-timeago').text($.timeago(batteryDetails.last_charged || ''));
$('#batterycard-battery-charge-cycles-count').text((batteryDetails.charge_cycles_count || '0'));
$('#batterycard-battery-edit-button').attr("href", U("/battery/" + batteryDetails.battery.id.toString()));
$('#batterycard-battery-edit-button').removeClass("disabled");
EmptyElementWhenMatches('#batterycard-battery-last-charged-timeago', L('timeago_nan'));
},
function(xhr)

View File

@@ -11,6 +11,9 @@ Grocy.Components.ChoreCard.Refresh = function(choreId)
$('#chorecard-chore-tracked-count').text((choreDetails.tracked_count || '0'));
$('#chorecard-chore-last-done-by').text((choreDetails.last_done_by.display_name || L('Unknown')));
$('#chorecard-chore-edit-button').attr("href", U("/chore/" + choreDetails.chore.id.toString()));
$('#chorecard-chore-edit-button').removeClass("disabled");
EmptyElementWhenMatches('#chorecard-chore-last-tracked-timeago', L('timeago_nan'));
},
function(xhr)

View File

@@ -24,6 +24,23 @@ Grocy.Components.DateTimePicker.SetValue = function(value)
}
}
Grocy.Components.DateTimePicker.Clear = function()
{
$(".datetimepicker").datetimepicker("destroy");
Grocy.Components.DateTimePicker.Init();
Grocy.Components.DateTimePicker.GetInputElement().val("");
// "Click" the shortcut checkbox when the desired value is
// not the shortcut value and it is currently set
value = "";
var shortcutValue = $("#datetimepicker-shortcut").data("datetimepicker-shortcut-value");
if (value != shortcutValue && $("#datetimepicker-shortcut").is(":checked"))
{
$("#datetimepicker-shortcut").click();
}
}
var startDate = null;
if (Grocy.Components.DateTimePicker.GetInputElement().data('init-with-now') === true)
{
@@ -40,6 +57,8 @@ if (Grocy.Components.DateTimePicker.GetInputElement().data('limit-end-to-now') =
limitDate = moment();
}
Grocy.Components.DateTimePicker.Init = function()
{
$('.datetimepicker').datetimepicker(
{
format: Grocy.Components.DateTimePicker.GetInputElement().data('format'),
@@ -80,6 +99,8 @@ $('.datetimepicker').datetimepicker(
'delete': function(widget) { }
}
});
}
Grocy.Components.DateTimePicker.Init();
Grocy.Components.DateTimePicker.GetInputElement().on('keyup', function(e)
{

View File

@@ -15,6 +15,9 @@ Grocy.Components.ProductCard.Refresh = function(productId)
$('#productcard-product-last-used').text((productDetails.last_used || L('never')).substring(0, 10));
$('#productcard-product-last-used-timeago').text($.timeago(productDetails.last_used || ''));
$('#productcard-product-edit-button').attr("href", U("/product/" + productDetails.product.id.toString()));
$('#productcard-product-edit-button').removeClass("disabled");
if (productDetails.last_price !== null)
{
$('#productcard-product-last-price').text(Number.parseFloat(productDetails.last_price).toLocaleString() + ' ' + Grocy.Currency);

View File

@@ -16,7 +16,7 @@
Grocy.Api.Get('stock/consume-product/' + jsonForm.product_id + '/' + jsonForm.amount + '?spoiled=' + spoiled,
function(result)
{
toastr.success(L('Removed #1 #2 of #3 from stock', jsonForm.amount, productDetails.quantity_unit_stock.name, 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);
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

@@ -116,7 +116,7 @@ $("#selectedEquipmentInstructionManualToggleFullscreenButton").on('click', funct
$("#selectedEquipmentInstructionManualCard").toggleClass("fullscreen");
$("#selectedEquipmentInstructionManualCard .card-header").toggleClass("fixed-top");
$("#selectedEquipmentInstructionManualCard .card-body").toggleClass("mt-5");
$("body").toggleClass("fullscreen-responsive-embed-active");
$("body").toggleClass("fullscreen-card");
ResizeResponsiveEmbeds(true);
});

View File

@@ -32,7 +32,7 @@
);
}
toastr.success(L('Stock amount of #1 is now #2 #3', productDetails.product.name, jsonForm.new_amount, productDetails.quantity_unit_stock.name));
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)
{
@@ -42,7 +42,7 @@
{
$('#inventory-change-info').addClass('d-none');
$('#new_amount').val('');
Grocy.Components.DateTimePicker.SetValue('');
Grocy.Components.DateTimePicker.Clear();
Grocy.Components.ProductPicker.SetValue('');
Grocy.Components.ProductPicker.GetInputElement().focus();
Grocy.FrontendHelpers.ValidateForm('inventory-form');
@@ -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

@@ -196,6 +196,24 @@ $('#delete-current-product-picture-button').on('click', function (e)
$("#delete-current-product-picture-button").addClass("disabled");
});
if (Grocy.EditMode === 'create')
{
if (Grocy.UserSettings.product_presets_location_id.toString() !== '-1')
{
$("#location_id").val(Grocy.UserSettings.product_presets_location_id);
}
if (Grocy.UserSettings.product_presets_product_group_id.toString() !== '-1')
{
$("#product_group_id").val(Grocy.UserSettings.product_presets_product_group_id);
}
if (Grocy.UserSettings.product_presets_qu_id.toString() !== '-1')
{
$("select.input-group-qu").val(Grocy.UserSettings.product_presets_qu_id);
}
}
$('#name').focus();
$('.input-group-qu').trigger('change');
Grocy.FrontendHelpers.ValidateForm('product-form');

View File

@@ -0,0 +1,3 @@
$("#product_presets_location_id").val(Grocy.UserSettings.product_presets_location_id);
$("#product_presets_product_group_id").val(Grocy.UserSettings.product_presets_product_group_id);
$("#product_presets_qu_id").val(Grocy.UserSettings.product_presets_qu_id);

View File

@@ -40,7 +40,7 @@
);
}
toastr.success(L('Added #1 #2 of #3 to stock', amount, productDetails.quantity_unit_stock.name, 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)
{
@@ -50,7 +50,7 @@
{
$('#amount').val(0);
$('#price').val('');
Grocy.Components.DateTimePicker.SetValue('');
Grocy.Components.DateTimePicker.Clear();
Grocy.Components.ProductPicker.SetValue('');
Grocy.Components.ProductPicker.GetInputElement().focus();
Grocy.FrontendHelpers.ValidateForm('purchase-form');
@@ -171,3 +171,17 @@ $('#amount').on('change', function (e)
{
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

@@ -15,6 +15,32 @@
});
var recipesPosTables = $('#recipes-pos-table').DataTable({
'paginate': false,
'order': [[1, 'asc']],
"orderFixed": [[4, 'asc']],
'columnDefs': [
{ 'orderable': false, 'targets': 0 },
{ 'visible': false, 'targets': 4 }
],
'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 = "";
});
},
'rowGroup': {
dataSrc: 4
}
});
var recipesIncludesTables = $('#recipes-includes-table').DataTable({
'paginate': false,
'order': [[1, 'asc']],
'columnDefs': [
@@ -35,17 +61,6 @@ var recipesPosTables = $('#recipes-pos-table').DataTable({
}
});
$("#search").on("keyup", function ()
{
var value = $(this).val();
if (value === "all")
{
value = "";
}
recipesPosTables.search(value).draw();
});
Grocy.FrontendHelpers.ValidateForm('recipe-form');
$("#name").focus();
@@ -108,6 +123,43 @@ $(document).on('click', '.recipe-pos-delete-button', function(e)
});
});
$(document).on('click', '.recipe-include-delete-button', function(e)
{
var objectName = $(e.currentTarget).attr('data-recipe-include-name');
var objectId = $(e.currentTarget).attr('data-recipe-include-id');
bootbox.confirm({
message: L('Are you sure to remove included recipe "#1"?', objectName),
buttons: {
confirm: {
label: L('Yes'),
className: 'btn-success'
},
cancel: {
label: L('No'),
className: 'btn-danger'
}
},
callback: function(result)
{
if (result === true)
{
Grocy.Api.Post('edit-object/recipes/' + Grocy.EditObjectId, $('#recipe-form').serializeJSON(), function() { }, function() { });
Grocy.Api.Get('delete-object/recipes_nestings/' + objectId,
function(result)
{
window.location.href = U('/recipe/' + Grocy.EditObjectId);
},
function(xhr)
{
console.error(xhr);
}
);
}
}
});
});
$(document).on('click', '.recipe-pos-order-missing-button', function(e)
{
var productName = $(e.currentTarget).attr('data-product-name');
@@ -156,6 +208,28 @@ $(document).on('click', '.recipe-pos-edit-button', function (e)
);
});
$(document).on('click', '.recipe-include-edit-button', function (e)
{
var id = $(e.currentTarget).attr('data-recipe-include-id');
var recipeId = $(e.currentTarget).attr('data-recipe-included-recipe-id');
console.log(recipeId);
Grocy.Api.Post('edit-object/recipes/' + Grocy.EditObjectId, $('#recipe-form').serializeJSON(),
function(result)
{
$("#recipe-include-editform-title").text(L("Edit included recipe"));
$("#recipe-include-form").data("edit-mode", "edit");
$("#recipe-include-form").data("recipe-nesting-id", id);
$("#includes_recipe_id").val(recipeId);
$("#recipe-include-editform-modal").modal("show");
Grocy.FrontendHelpers.ValidateForm("recipe-include-form");
},
function(xhr)
{
console.error(xhr);
}
);
});
$("#recipe-pos-add-button").on("click", function(e)
{
Grocy.Api.Post('edit-object/recipes/' + Grocy.EditObjectId, $('#recipe-form').serializeJSON(),
@@ -170,6 +244,62 @@ $("#recipe-pos-add-button").on("click", function(e)
);
});
$("#recipe-include-add-button").on("click", function(e)
{
Grocy.Api.Post('edit-object/recipes/' + Grocy.EditObjectId, $('#recipe-form').serializeJSON(),
function(result)
{
$("#recipe-include-editform-title").text(L("Add included recipe"));
$("#recipe-include-form").data("edit-mode", "create");
$("#includes_recipe_id").val("");
$("#recipe-include-editform-modal").modal("show");
Grocy.FrontendHelpers.ValidateForm("recipe-include-form");
},
function(xhr)
{
console.error(xhr);
}
);
});
$('#save-recipe-include-button').on('click', function(e)
{
e.preventDefault();
var nestingId = $("#recipe-include-form").data("recipe-nesting-id");
var editMode = $("#recipe-include-form").data("edit-mode");
var jsonData = $('#recipe-include-form').serializeJSON();
jsonData.recipe_id = Grocy.EditObjectId;
if (editMode === 'create')
{
Grocy.Api.Post('add-object/recipes_nestings', jsonData,
function(result)
{
window.location.href = U('/recipe/' + Grocy.EditObjectId);
},
function(xhr)
{
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response)
}
);
}
else
{
Grocy.Api.Post('edit-object/recipes_nestings/' + nestingId, jsonData,
function(result)
{
window.location.href = U('/recipe/' + Grocy.EditObjectId);
},
function(xhr)
{
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response)
}
);
}
});
$('#description').summernote({
minHeight: '300px',
lang: L('summernote_locale')

View File

@@ -158,6 +158,7 @@ recipesTables.on('select', function(e, dt, type, indexes)
$("#selectedRecipeToggleFullscreenButton").on('click', function(e)
{
$("#selectedRecipeCard").toggleClass("fullscreen");
$("body").toggleClass("fullscreen-card");
$("#selectedRecipeCard .card-header").toggleClass("fixed-top");
$("#selectedRecipeCard .card-body").toggleClass("mt-5");
});

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

@@ -31,6 +31,17 @@ $("#location-filter").on("change", function()
stockOverviewTable.column(4).search(value).draw();
});
$("#product-group-filter").on("change", function()
{
var value = $(this).val();
if (value === "all")
{
value = "";
}
stockOverviewTable.column(6).search(value).draw();
});
$("#status-filter").on("change", function()
{
var value = $(this).val();
@@ -143,41 +154,8 @@ $(document).on('click', '.product-consume-button', function(e)
$(document).on("click", ".product-name-cell", function(e)
{
var productHasPicture = BoolVal($(e.currentTarget).attr("data-product-has-picture"));
if (productHasPicture)
{
var pictureUrl = $(e.currentTarget).attr("data-picture-url");
var productName = $(e.currentTarget).attr("data-product-name");
var productId = $(e.currentTarget).attr("data-product-id");
bootbox.dialog({
title: L("Image of product #1", productName),
message: "<img src='" + pictureUrl + "' class='img-fluid img-thumbnail'>",
backdrop: false,
onEscape: true,
closeButton: false,
className: 'centered-dialog',
buttons: {
editproduct: {
label: '<i class="fas fa-edit"></i> ' + L('Edit product'),
className: 'btn-info responsive-button',
callback: function ()
{
window.location.href = U('/product/' + productId + '?returnto=' + encodeURIComponent(window.location.pathname) + '#product-picture');
}
},
close: {
label: L('Close'),
className: 'btn-default responsive-button',
callback: function()
{
bootbox.hideAll();
}
}
}
});
}
Grocy.Components.ProductCard.Refresh($(e.currentTarget).attr("data-product-id"));
$("#stockoverview-productcard-modal").modal("show");
});
function RefreshStatistics()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 166 KiB

View File

@@ -26,6 +26,7 @@ $app->group('', function()
$this->get('/inventory', '\Grocy\Controllers\StockController:Inventory');
$this->get('/products', '\Grocy\Controllers\StockController:ProductsList');
$this->get('/product/{productId}', '\Grocy\Controllers\StockController:ProductEditForm');
$this->get('/productpresets', '\Grocy\Controllers\StockController:ProductDefaults');
$this->get('/locations', '\Grocy\Controllers\StockController:LocationsList');
$this->get('/location/{locationId}', '\Grocy\Controllers\StockController:LocationEditForm');
$this->get('/quantityunits', '\Grocy\Controllers\StockController:QuantityUnitsList');
@@ -34,6 +35,7 @@ $app->group('', function()
$this->get('/productgroup/{productGroupId}', '\Grocy\Controllers\StockController:ProductGroupEditForm');
$this->get('/shoppinglist', '\Grocy\Controllers\StockController:ShoppingList');
$this->get('/shoppinglistitem/{itemId}', '\Grocy\Controllers\StockController:ShoppingListItemEditForm');
$this->get('/stockjournal', '\Grocy\Controllers\StockController:Journal');
// Recipe routes
$this->get('/recipes', '\Grocy\Controllers\RecipesController:Overview');
@@ -43,7 +45,7 @@ $app->group('', function()
// Chore routes
$this->get('/choresoverview', '\Grocy\Controllers\ChoresController:Overview');
$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('/chore/{choreId}', '\Grocy\Controllers\ChoresController:ChoreEditForm');
@@ -51,6 +53,7 @@ $app->group('', function()
// Battery routes
$this->get('/batteriesoverview', '\Grocy\Controllers\BatteriesController:Overview');
$this->get('/batterytracking', '\Grocy\Controllers\BatteriesController:TrackChargeCycle');
$this->get('/batteriesjournal', '\Grocy\Controllers\BatteriesController:Journal');
$this->get('/batteries', '\Grocy\Controllers\BatteriesController:BatteriesList');
$this->get('/battery/{batteryId}', '\Grocy\Controllers\BatteriesController:BatteryEditForm');
@@ -113,6 +116,7 @@ $app->group('/api', function()
$this->get('/stock/add-missing-products-to-shoppinglist', '\Grocy\Controllers\StockApiController:AddMissingProductsToShoppingList');
$this->get('/stock/clear-shopping-list', '\Grocy\Controllers\StockApiController:ClearShoppingList');
$this->get('/stock/external-barcode-lookup/{barcode}', '\Grocy\Controllers\StockApiController:ExternalBarcodeLookup');
$this->get('/stock/undo-booking/{bookingId}', '\Grocy\Controllers\StockApiController:UndoBooking');
// Recipes
$this->get('/recipes/add-not-fulfilled-products-to-shopping-list/{recipeId}', '\Grocy\Controllers\RecipesApiController:AddNotFulfilledProductsToShoppingList');
@@ -122,11 +126,13 @@ $app->group('/api', function()
$this->get('/chores/track-chore-execution/{choreId}', '\Grocy\Controllers\ChoresApiController:TrackChoreExecution');
$this->get('/chores/get-chore-details/{choreId}', '\Grocy\Controllers\ChoresApiController:ChoreDetails');
$this->get('/chores/get-current', '\Grocy\Controllers\ChoresApiController:Current');
$this->get('/chores/undo-chore-execution/{executionId}', '\Grocy\Controllers\ChoresApiController:UndoChoreExecution');
// Batteries
$this->get('/batteries/track-charge-cycle/{batteryId}', '\Grocy\Controllers\BatteriesApiController:TrackChargeCycle');
$this->get('/batteries/get-battery-details/{batteryId}', '\Grocy\Controllers\BatteriesApiController:BatteryDetails');
$this->get('/batteries/get-current', '\Grocy\Controllers\BatteriesApiController:Current');
$this->get('/batteries/undo-charge-cycle/{chargeCycleId}', '\Grocy\Controllers\BatteriesApiController:UndoChargeCycle');
// Tasks
$this->get('/tasks/get-current', '\Grocy\Controllers\TasksApiController:Current');

View File

@@ -18,14 +18,14 @@ class BatteriesService extends BaseService
}
$battery = $this->Database->batteries($batteryId);
$batteryChargeCylcesCount = $this->Database->battery_charge_cycles()->where('battery_id', $batteryId)->count();
$batteryLastChargedTime = $this->Database->battery_charge_cycles()->where('battery_id', $batteryId)->max('tracked_time');
$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');
$nextChargeTime = $this->Database->batteries_current()->where('battery_id', $batteryId)->min('next_estimated_charge_time');
return array(
'battery' => $battery,
'last_charged' => $batteryLastChargedTime,
'charge_cycles_count' => $batteryChargeCylcesCount,
'charge_cycles_count' => $batteryChargeCyclesCount,
'next_estimated_charge_time' => $nextChargeTime
);
}
@@ -43,7 +43,7 @@ class BatteriesService extends BaseService
));
$logRow->save();
return true;
return $this->Database->lastInsertId();
}
private function BatteryExists($batteryId)
@@ -51,4 +51,19 @@ class BatteriesService extends BaseService
$batteryRow = $this->Database->batteries()->where('id = :1', $batteryId)->fetch();
return $batteryRow !== null;
}
public function UndoChargeCycle($chargeCycleId)
{
$logRow = $this->Database->battery_charge_cycles()->where('id = :1 AND undone = 0', $chargeCycleId)->fetch();
if ($logRow == null)
{
throw new \Exception('Charge cycle does not exist or was already undone');
}
// Update log entry
$logRow->update(array(
'undone' => 1,
'undone_timestamp' => date('Y-m-d H:i:s')
));
}
}

View File

@@ -21,11 +21,11 @@ class ChoresService extends BaseService
}
$chore = $this->Database->chores($choreId);
$choreTrackedCount = $this->Database->chores_log()->where('chore_id', $choreId)->count();
$choreLastTrackedTime = $this->Database->chores_log()->where('chore_id', $choreId)->max('tracked_time');
$choreTrackedCount = $this->Database->chores_log()->where('chore_id = :1 AND undone = 0', $choreId)->count();
$choreLastTrackedTime = $this->Database->chores_log()->where('chore_id = :1 AND undone = 0', $choreId)->max('tracked_time');
$nextExeuctionTime = $this->Database->chores_current()->where('chore_id', $choreId)->min('next_estimated_execution_time');
$lastChoreLogRow = $this->Database->chores_log()->where('chore_id = :1 AND tracked_time = :2', $choreId, $choreLastTrackedTime)->fetch();
$lastChoreLogRow = $this->Database->chores_log()->where('chore_id = :1 AND tracked_time = :2 AND undone = 0', $choreId, $choreLastTrackedTime)->fetch();
$lastDoneByUser = null;
if ($lastChoreLogRow !== null && !empty($lastChoreLogRow))
{
@@ -63,7 +63,7 @@ class ChoresService extends BaseService
));
$logRow->save();
return true;
return $this->Database->lastInsertId();
}
private function ChoreExists($choreId)
@@ -71,4 +71,19 @@ class ChoresService extends BaseService
$choreRow = $this->Database->chores()->where('id = :1', $choreId)->fetch();
return $choreRow !== null;
}
public function UndoChoreExecution($executionId)
{
$logRow = $this->Database->chores_log()->where('id = :1 AND undone = 0', $executionId)->fetch();
if ($logRow == null)
{
throw new \Exception('Execution does not exist or was already undone');
}
// Update log entry
$logRow->update(array(
'undone' => 1,
'undone_timestamp' => date('Y-m-d H:i:s')
));
}
}

View File

@@ -31,6 +31,9 @@ class DemoDataGeneratorService extends BaseService
INSERT INTO quantity_units (name, name_plural) VALUES ('{$localizationService->Localize('Can')}', '{$localizationService->Localize('Cans')}'); --6
INSERT INTO quantity_units (name, name_plural) VALUES ('{$localizationService->Localize('Bunch')}', '{$localizationService->Localize('Bunches')}'); --7
INSERT INTO quantity_units (name, name_plural) VALUES ('{$localizationService->Localize('Gram')}', '{$localizationService->Localize('Grams')}'); --8
INSERT INTO quantity_units (name, name_plural) VALUES ('{$localizationService->Localize('Liter')}', '{$localizationService->Localize('Liters')}'); --9
INSERT INTO quantity_units (name, name_plural) VALUES ('{$localizationService->Localize('Bottle')}', '{$localizationService->Localize('Bottles')}'); --10
INSERT INTO quantity_units (name, name_plural) VALUES ('{$localizationService->Localize('Milliliter')}', '{$localizationService->Localize('Milliliters')}'); --11
INSERT INTO product_groups(name) VALUES ('01 {$localizationService->Localize('Sweets')}'); --1
INSERT INTO product_groups(name) VALUES ('02 {$localizationService->Localize('Bakery products')}'); --2
@@ -62,6 +65,7 @@ class DemoDataGeneratorService extends BaseService
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id) VALUES ('{$localizationService->Localize('Minced meat')}', 2, 3, 3, 1, 4); --20
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id) VALUES ('{$localizationService->Localize('Flour')}', 2, 3, 3, 1, 3); --21
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id) VALUES ('{$localizationService->Localize('Sugar')}', 3, 3, 3, 1, 3); --22
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('{$localizationService->Localize('Milk')}', 2, 10, 10, 1); --23
INSERT INTO shopping_list (note, amount) VALUES ('{$localizationService->Localize('Some good snacks')}', 1);
INSERT INTO shopping_list (product_id, amount) VALUES (20, 1);
@@ -71,11 +75,13 @@ class DemoDataGeneratorService extends BaseService
INSERT INTO recipes (name, description) VALUES ('{$localizationService->Localize('Spaghetti bolognese')}', '{$loremIpsumWithHtmlFormattings}'); --2
INSERT INTO recipes (name, description) VALUES ('{$localizationService->Localize('Sandwiches')}', '{$loremIpsumWithHtmlFormattings}'); --3
INSERT INTO recipes (name, description) VALUES ('{$localizationService->Localize('Pancakes')}', '{$loremIpsumWithHtmlFormattings}'); --4
INSERT INTO recipes (name, description) VALUES ('{$localizationService->Localize('Chocolate sauce')}', '{$loremIpsumWithHtmlFormattings}'); --5
INSERT INTO recipes (name, description) VALUES ('{$localizationService->Localize('Pancakes')} / {$localizationService->Localize('Chocolate sauce')}', '{$loremIpsumWithHtmlFormattings}'); --6
INSERT INTO recipes_pos (recipe_id, product_id, amount) VALUES (1, 16, 1);
INSERT INTO recipes_pos (recipe_id, product_id, amount) VALUES (1, 17, 1);
INSERT INTO recipes_pos (recipe_id, product_id, amount, note) VALUES (1, 18, 1, '{$localizationService->Localize('This is the note content of the recipe ingredient')}');
INSERT INTO recipes_pos (recipe_id, product_id, amount) VALUES (1, 10, 1);
INSERT INTO recipes_pos (recipe_id, product_id, amount, ingredient_group) VALUES (1, 16, 1, '{$localizationService->Localize('Bottom')}');
INSERT INTO recipes_pos (recipe_id, product_id, amount, ingredient_group) VALUES (1, 17, 1, '{$localizationService->Localize('Topping')}');
INSERT INTO recipes_pos (recipe_id, product_id, amount, note, ingredient_group) VALUES (1, 18, 1, '{$localizationService->Localize('This is the note content of the recipe ingredient')}', '{$localizationService->Localize('Topping')}');
INSERT INTO recipes_pos (recipe_id, product_id, amount, ingredient_group) VALUES (1, 10, 1, '{$localizationService->Localize('Bottom')}');
INSERT INTO recipes_pos (recipe_id, product_id, amount) VALUES (2, 6, 1);
INSERT INTO recipes_pos (recipe_id, product_id, amount) VALUES (2, 10, 1);
INSERT INTO recipes_pos (recipe_id, product_id, amount, note) VALUES (2, 17, 1, '{$localizationService->Localize('This is the note content of the recipe ingredient')}');
@@ -85,6 +91,11 @@ class DemoDataGeneratorService extends BaseService
INSERT INTO recipes_pos (recipe_id, product_id, amount) VALUES (4, 5, 4);
INSERT INTO recipes_pos (recipe_id, product_id, amount, qu_id, only_check_single_unit_in_stock) VALUES (4, 21, 200, 8, 1);
INSERT INTO recipes_pos (recipe_id, product_id, amount, qu_id, only_check_single_unit_in_stock) VALUES (4, 22, 200, 8, 1);
INSERT INTO recipes_pos (recipe_id, product_id, amount) VALUES (5, 2, 1);
INSERT INTO recipes_pos (recipe_id, product_id, amount, qu_id, only_check_single_unit_in_stock) VALUES (5, 23, 200, 11, 1);
INSERt INTO recipes_nestings(recipe_id, includes_recipe_id) VALUES (6, 4);
INSERt INTO recipes_nestings(recipe_id, includes_recipe_id) VALUES (6, 5);
INSERT INTO chores (name, period_type, period_days) VALUES ('{$localizationService->Localize('Changed towels in the bathroom')}', 'manually', 5); --1
INSERT INTO chores (name, period_type, period_days) VALUES ('{$localizationService->Localize('Cleaned the kitchen floor')}', 'dynamic-regular', 7); --2
@@ -184,6 +195,8 @@ class DemoDataGeneratorService extends BaseService
$stockService->AddProduct(21, 1, date('Y-m-d', strtotime('+200 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-20 days')), $this->RandomPrice());
$stockService->AddProduct(22, 1, date('Y-m-d', strtotime('+200 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice());
$stockService->AddProduct(22, 1, date('Y-m-d', strtotime('+200 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-20 days')), $this->RandomPrice());
$stockService->AddProduct(23, 1, date('Y-m-d', strtotime('+2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-40 days')), $this->RandomPrice());
$stockService->AddProduct(23, 1, date('Y-m-d', strtotime('+2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-50 days')), $this->RandomPrice());
$stockService->AddMissingProductsToShoppingList();
$choresService = new ChoresService();

View File

@@ -37,10 +37,10 @@ class SessionService extends BaseService
{
$newSessionKey = $this->GenerateSessionKey();
$expires = date('Y-m-d H:i:s', time() + 2592000); // Default is that sessions expire in 30 days
$expires = date('Y-m-d H:i:s', intval(time() + 2592000)); // Default is that sessions expire in 30 days
if ($stayLoggedInPermanently === true)
{
$expires = date('Y-m-d H:i:s', time() + 31220640000); // 999 years aka forever
$expires = date('Y-m-d H:i:s', intval(time() + 31220640000)); // 999 years aka forever
}
$sessionRow = $this->Database->sessions()->createRow(array(

View File

@@ -47,14 +47,14 @@ class StockService extends BaseService
$product = $this->Database->products($productId);
$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');
$productLastUsed = $this->Database->stock_log()->where('product_id', $productId)->where('transaction_type', self::TRANSACTION_TYPE_CONSUME)->max('used_date');
$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)->where('undone', 0)->max('used_date');
$nextBestBeforeDate = $this->Database->stock()->where('product_id', $productId)->min('best_before_date');
$quPurchase = $this->Database->quantity_units($product->qu_id_purchase);
$quStock = $this->Database->quantity_units($product->qu_id_stock);
$lastPrice = null;
$lastLogRow = $this->Database->stock_log()->where('product_id = :1 AND transaction_type = :2', $productId, self::TRANSACTION_TYPE_PURCHASE)->orderBy('row_created_timestamp', 'DESC')->limit(1)->fetch();
$lastLogRow = $this->Database->stock_log()->where('product_id = :1 AND transaction_type = :2 AND undone = 0', $productId, self::TRANSACTION_TYPE_PURCHASE)->orderBy('row_created_timestamp', 'DESC')->limit(1)->fetch();
if ($lastLogRow !== null && !empty($lastLogRow))
{
$lastPrice = $lastLogRow->price;
@@ -80,7 +80,7 @@ class StockService extends BaseService
}
$returnData = array();
$rows = $this->Database->stock_log()->where('product_id = :1 AND transaction_type = :2', $productId, self::TRANSACTION_TYPE_PURCHASE)->whereNOT('price', null)->orderBy('purchased_date', 'DESC');
$rows = $this->Database->stock_log()->where('product_id = :1 AND transaction_type = :2 AND undone = 0', $productId, self::TRANSACTION_TYPE_PURCHASE)->whereNOT('price', null)->orderBy('purchased_date', 'DESC');
foreach ($rows as $row)
{
$returnData[] = array(
@@ -113,6 +113,8 @@ class StockService extends BaseService
));
$logRow->save();
$returnValue = $this->Database->lastInsertId();
$stockRow = $this->Database->stock()->createRow(array(
'product_id' => $productId,
'amount' => $amount,
@@ -123,7 +125,7 @@ class StockService extends BaseService
));
$stockRow->save();
return true;
return $returnValue;
}
else
{
@@ -197,7 +199,7 @@ class StockService extends BaseService
}
}
return true;
return $this->Database->lastInsertId();
}
else
{
@@ -226,7 +228,7 @@ class StockService extends BaseService
$this->ConsumeProduct($productId, $amountToRemove, false, self::TRANSACTION_TYPE_INVENTORY_CORRECTION);
}
return true;
return $this->Database->lastInsertId();
}
public function AddMissingProductsToShoppingList()
@@ -305,4 +307,55 @@ class StockService extends BaseService
return $pluginOutput;
}
public function UndoBooking($bookingId)
{
$logRow = $this->Database->stock_log()->where('id = :1 AND undone = 0', $bookingId)->fetch();
if ($logRow == null)
{
throw new \Exception('Booking does not exist or was already undone');
}
$hasSubsequentBookings = $this->Database->stock_log()->where('stock_id = :1 AND id != :2 AND id > :2', $logRow->stock_id, $logRow->id)->count() > 0;
if ($hasSubsequentBookings)
{
throw new \Exception('Booking has subsequent dependent bookings, undo not possible');
}
if ($logRow->transaction_type === self::TRANSACTION_TYPE_PURCHASE || ($logRow->transaction_type === self::TRANSACTION_TYPE_INVENTORY_CORRECTION && $logRow->amount > 0))
{
// Remove corresponding stock entry
$stockRows = $this->Database->stock()->where('stock_id', $logRow->stock_id);
$stockRows->delete();
// Update log entry
$logRow->update(array(
'undone' => 1,
'undone_timestamp' => date('Y-m-d H:i:s')
));
}
elseif ($logRow->transaction_type === self::TRANSACTION_TYPE_CONSUME || ($logRow->transaction_type === self::TRANSACTION_TYPE_INVENTORY_CORRECTION && $logRow->amount < 0))
{
// Add corresponding amount back to stock
$stockRow = $this->Database->stock()->createRow(array(
'product_id' => $logRow->product_id,
'amount' => $logRow->amount * -1,
'best_before_date' => $logRow->best_before_date,
'purchased_date' => $logRow->purchased_date,
'stock_id' => $logRow->stock_id,
'price' => $logRow->price
));
$stockRow->save();
// Update log entry
$logRow->update(array(
'undone' => 1,
'undone_timestamp' => date('Y-m-d H:i:s')
));
}
else
{
throw new \Exception('This booking cannot be undone');
}
}
}

View File

@@ -1,4 +1,4 @@
{
"Version": "1.21.0",
"ReleaseDate": "2018-10-06"
"Version": "1.22.0",
"ReleaseDate": "2018-10-27"
}

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')
<div class="row">
<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-overdue-batteries" data-status-filter="overdue" class="btn btn-lg btn-danger status-filter-button responsive-button"></p>
</div>
@@ -53,6 +57,9 @@
data-battery-name="{{ FindObjectInArrayByPropertyValue($batteries, 'id', $curentBatteryEntry->battery_id)->name }}">
<i class="fas fa-fire"></i>
</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>
{{ FindObjectInArrayByPropertyValue($batteries, 'id', $curentBatteryEntry->battery_id)->name }}

View File

@@ -1,8 +1,8 @@
@extends('layout.default')
@section('title', $L('Chores analysis'))
@section('activeNav', 'choresanalysis')
@section('viewJsName', 'choresanalysis')
@section('title', $L('Chores journal'))
@section('activeNav', 'choresjournal')
@section('viewJsName', 'choresjournal')
@section('content')
<div class="row">
@@ -11,7 +11,7 @@
</div>
</div>
<div class="row mt-3">
<div class="row my-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>
<select class="form-control" id="chore-filter">
@@ -29,9 +29,10 @@
<div class="row">
<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>
<tr>
<th>#</th>
<th>{{ $L('Chore') }}</th>
<th>{{ $L('Tracked time') }}</th>
<th>{{ $L('Done by') }}</th>
@@ -39,9 +40,19 @@
</thead>
<tbody>
@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>
{{ 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>
{{ $choreLogEntry->tracked_time }}

View File

@@ -11,7 +11,11 @@
@section('content')
<div class="row">
<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-overdue-chores" data-status-filter="overdue" class="btn btn-lg btn-danger status-filter-button responsive-button"></p>
</div>
@@ -53,8 +57,8 @@
data-chore-name="{{ FindObjectInArrayByPropertyValue($chores, 'id', $curentChoreEntry->chore_id)->name }}">
<i class="fas fa-play"></i>
</a>
<a class="btn btn-info btn-sm" href="{{ $U('/choresanalysis?chore=') }}{{ $curentChoreEntry->chore_id }}">
<i class="fas fa-chart-line"></i>
<a class="btn btn-info btn-sm" href="{{ $U('/choresjournal?chore=') }}{{ $curentChoreEntry->chore_id }}">
<i class="fas fa-file-alt"></i>
</a>
</td>
<td>

View File

@@ -5,6 +5,9 @@
<div class="card">
<div class="card-header">
<i class="fas fa-battery-three-quarters"></i> {{ $L('Battery overview') }}
<a id="batterycard-battery-edit-button" class="btn btn-sm btn-outline-info py-0 float-right disabled" href="#" data-toggle="tooltip" title="{{ $L('Edit battery') }}">
<i class="fas fa-edit"></i>
</a>
</div>
<div class="card-body">
<h3><span id="batterycard-battery-name"></span></h3>

View File

@@ -4,7 +4,10 @@
<div class="card">
<div class="card-header">
<i class="fas fa-refresh"></i> {{ $L('Chore overview') }}
<i class="fas fa-home"></i> {{ $L('Chore overview') }}
<a id="chorecard-chore-edit-button" class="btn btn-sm btn-outline-info py-0 float-right disabled" href="#" data-toggle="tooltip" title="{{ $L('Edit chore') }}">
<i class="fas fa-edit"></i>
</a>
</div>
<div class="card-body">
<h3><span id="chorecard-chore-name"></span></h3>

View File

@@ -6,6 +6,9 @@
<div class="card">
<div class="card-header">
<i class="fab fa-product-hunt"></i> {{ $L('Product overview') }}
<a id="productcard-product-edit-button" class="btn btn-sm btn-outline-info py-0 float-right disabled" href="#" data-toggle="tooltip" title="{{ $L('Edit product') }}">
<i class="fas fa-edit"></i>
</a>
</div>
<div class="card-body">
<h3><span id="productcard-product-name"></span></h3>
@@ -17,7 +20,7 @@
<h5 class="mt-3">{{ $L('Product picture') }}</h5>
<img id="productcard-product-picture" src="" class="img-fluid img-thumbnail d-none">
<span id="productcard-no-product-picture" class="font-italic d-none">{{ $L('No picture') }}</span>
<span id="productcard-no-product-picture" class="font-italic d-none">{{ $L('No picture available') }}</span>
<h5 class="mt-3">{{ $L('Price history') }}</h5>
<canvas id="productcard-product-price-history-chart" class="w-100 d-none"></canvas>

View File

@@ -46,6 +46,7 @@
<div class="form-group">
<label for="location_id">{{ $L('Location') }}</label>
<select required class="form-control" id="location_id" name="location_id">
<option></option>
@foreach($locations as $location)
<option @if($mode == 'edit' && $location->id == $product->location_id) selected="selected" @endif value="{{ $location->id }}">{{ $location->name }}</option>
@endforeach
@@ -85,6 +86,7 @@
<div class="form-group">
<label for="qu_id_purchase">{{ $L('Quantity unit purchase') }}</label>
<select required class="form-control input-group-qu" id="qu_id_purchase" name="qu_id_purchase">
<option></option>
@foreach($quantityunits as $quantityunit)
<option @if($mode == 'edit' && $quantityunit->id == $product->qu_id_purchase) selected="selected" @endif value="{{ $quantityunit->id }}">{{ $quantityunit->name }}</option>
@endforeach
@@ -95,6 +97,7 @@
<div class="form-group">
<label for="qu_id_stock">{{ $L('Quantity unit stock') }}</label>
<select required class="form-control input-group-qu" id="qu_id_stock" name="qu_id_stock">
<option></option>
@foreach($quantityunits as $quantityunit)
<option @if($mode == 'edit' && $quantityunit->id == $product->qu_id_stock) selected="selected" @endif value="{{ $quantityunit->id }}">{{ $quantityunit->name }}</option>
@endforeach
@@ -133,7 +136,7 @@
<p><img id="current-product-picture" src="{{ $U('/api/file/productpictures?file_name=' . $product->picture_file_name) }}" class="img-fluid img-thumbnail mt-2"></p>
<p id="delete-current-product-picture-on-save-hint" class="form-text text-muted font-italic d-none">{{ $L('The current picture will be deleted when you save the product') }}</p>
@else
<p id="no-current-product-picture-hint" class="form-text text-muted font-italic">{{ $L('No picture') }}</p>
<p id="no-current-product-picture-hint" class="form-text text-muted font-italic">{{ $L('No picture available') }}</p>
@endif
</div>
</div>

View File

@@ -0,0 +1,45 @@
@extends('layout.default')
@section('title', $L('Presets for new products'))
@section('viewJsName', 'productpresets')
@section('content')
<div class="row">
<div class="col-lg-6 col-xs-12">
<h1>@yield('title')</h1>
<div class="form-group">
<label for="product_presets_location_id">{{ $L('Location') }}</label>
<select class="form-control user-setting-control" id="product_presets_location_id" data-setting-key="product_presets_location_id">
<option value="-1"></option>
@foreach($locations as $location)
<option value="{{ $location->id }}">{{ $location->name }}</option>
@endforeach
</select>
</div>
<div class="form-group">
<label for="product_presets_product_group_id">{{ $L('Product group') }}</label>
<select class="form-control user-setting-control" id="product_presets_product_group_id" data-setting-key="product_presets_product_group_id">
<option value="-1"></option>
@foreach($productGroups as $productGroup)
<option value="{{ $productGroup->id }}">{{ $productGroup->name }}</option>
@endforeach
</select>
</div>
<div class="form-group">
<label for="product_presets_qu_id">{{ $L('Quantity unit') }}</label>
<select class="form-control user-setting-control" id="product_presets_qu_id" data-setting-key="product_presets_qu_id">
<option value="-1"></option>
@foreach($quantityunits as $quantityunit)
<option value="{{ $quantityunit->id }}">{{ $quantityunit->name }}</option>
@endforeach
</select>
</div>
<a href="{{ $U('/products') }}" class="btn btn-success">{{ $L('OK') }}</a>
</div>
</div>
@stop

View File

@@ -12,6 +12,9 @@
<a class="btn btn-outline-dark" href="{{ $U('/product/new') }}">
<i class="fas fa-plus"></i>&nbsp;{{ $L('Add') }}
</a>
<a class="btn btn-outline-secondary" href="{{ $U('/productpresets') }}">
<i class="fas fa-sliders-h"></i>&nbsp;{{ $L('Presets for new products') }}
</a>
</h1>
</div>
</div>
@@ -50,7 +53,7 @@
</a>
</td>
<td>
{{ $product->name }}
{{ $product->name }}@if(!empty($product->picture_file_name)) <i class="fas fa-image text-muted"></i>@endif
</td>
<td>
{{ FindObjectInArrayByPropertyValue($locations, 'id', $product->location_id)->name }}

View File

@@ -11,10 +11,13 @@
@push('pageScripts')
<script src="{{ $U('/node_modules/summernote/dist/summernote-bs4.js?v=', true) }}{{ $version }}"></script>
@if(!empty($L('summernote_locale')))<script src="{{ $U('/node_modules', true) }}/summernote/dist/lang/summernote-{{ $L('summernote_locale') }}.js?v={{ $version }}"></script>@endif
<script src="{{ $U('/node_modules/datatables.net-rowgroup/js/dataTables.rowGroup.min.js?v=', true) }}{{ $version }}"></script>
<script src="{{ $U('/node_modules/datatables.net-rowgroup-bs4/js/rowGroup.bootstrap4.min.js?v=', true) }}{{ $version }}"></script>
@endpush
@push('pageStyles')
<link href="{{ $U('/node_modules/summernote/dist/summernote-bs4.css?v=', true) }}{{ $version }}" rel="stylesheet">
<link href="{{ $U('/node_modules/datatables.net-rowgroup-bs4/css/rowGroup.bootstrap4.min.css?v=', true) }}{{ $version }}" rel="stylesheet">
@endpush
@section('content')
@@ -51,12 +54,14 @@
</div>
<div class="col-xs-12 col-md-5 pb-3">
<div class="row">
<div class="col">
<h2>
{{ $L('Ingredients list') }}
<a id="recipe-pos-add-button" class="btn btn-outline-dark" href="#">
<i class="fas fa-plus"></i> {{ $L('Add') }}
</a>
</h1>
</h2>
<table id="recipes-pos-table" class="table table-sm table-striped dt-responsive">
<thead>
<tr>
@@ -64,6 +69,7 @@
<th>{{ $L('Product') }}</th>
<th>{{ $L('Amount') }}</th>
<th>{{ $L('Note') }}</th>
<th class="d-none">Hiden ingredient group</th>
</tr>
</thead>
<tbody>
@@ -93,6 +99,9 @@
<i class="fas fa-eye"></i>
</a>
</td>
<td>
{{ $recipePosition->ingredient_group }}
</td>
</tr>
@endforeach
@endif
@@ -100,4 +109,74 @@
</table>
</div>
</div>
<div class="row mt-5">
<div class="col">
<h2>
{{ $L('Included recipes') }}
<a id="recipe-include-add-button" class="btn btn-outline-dark" href="#">
<i class="fas fa-plus"></i> {{ $L('Add') }}
</a>
</h2>
<table id="recipes-includes-table" class="table table-sm table-striped dt-responsive">
<thead>
<tr>
<th>#</th>
<th>{{ $L('Recipe') }}</th>
</tr>
</thead>
<tbody>
@if($mode == "edit")
@foreach($recipeNestings as $recipeNesting)
<tr>
<td class="fit-content">
<a class="btn btn-sm btn-info recipe-include-edit-button" href="#" data-recipe-include-id="{{ $recipeNesting->id }}" data-recipe-included-recipe-id="{{ $recipeNesting->includes_recipe_id }}">
<i class="fas fa-edit"></i>
</a>
<a class="btn btn-sm btn-danger recipe-include-delete-button" href="#" data-recipe-include-id="{{ $recipeNesting->id }}" data-recipe-include-name="{{ FindObjectInArrayByPropertyValue($recipes, 'id', $recipeNesting->includes_recipe_id)->name }}">
<i class="fas fa-trash"></i>
</a>
</td>
<td>
{{ FindObjectInArrayByPropertyValue($recipes, 'id', $recipeNesting->includes_recipe_id)->name }}
</td>
</tr>
@endforeach
@endif
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="modal fade" id="recipe-include-editform-modal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content text-center">
<div class="modal-header">
<h4 id="recipe-include-editform-title" class="modal-title w-100"></h4>
</div>
<div class="modal-body">
<form id="recipe-include-form" novalidate>
<div class="form-group">
<label for="includes_recipe_id">{{ $L('Recipe') }}</label>
<select required class="form-control" id="includes_recipe_id" name="includes_recipe_id">
<option></option>
@foreach($recipes as $recipeForList)
@if($recipeForList->id !== $recipe->id)
<option data-already-included="{{ BoolToString(FindObjectInArrayByPropertyValue($recipeNestings, 'includes_recipe_id', $recipeForList->id) === null) }}" value="{{ $recipeForList->id }}">{{ $recipeForList->name }}</option>
@endif
@endforeach
</select>
<div class="invalid-feedback">{{ $L('A recipe is required') }}</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">{{ $L('Cancel') }}</button>
<button id="save-recipe-include-button" data-dismiss="modal" class="btn btn-success">{{ $L('Save') }}</button>
</div>
</div>
</div>
</div>
@stop

View File

@@ -68,6 +68,11 @@
</div>
</div>
<div class="form-group">
<label for="ingredient_group">{{ $L('Group') }}&nbsp;&nbsp;<span class="small text-muted">{{ $L('This will be used as a headline to group ingredients together') }}</span></label>
<input type="text" class="form-control" id="ingredient_group" name="ingredient_group" value="@if($mode == 'edit'){{ $recipePos->ingredient_group }}@endif">
</div>
<div class="form-group">
<label for="note">{{ $L('Note') }}</label>
<textarea class="form-control" rows="2" id="note" name="note">@if($mode == 'edit'){{ $recipePos->note }}@endif</textarea>

View File

@@ -62,25 +62,73 @@
<i class="fas fa-expand-arrows-alt"></i>
</a>
</div>
<!-- Subrecipes first -->
@foreach($selectedRecipeSubRecipes as $selectedRecipeSubRecipe)
<div class="card-body">
<h3 class="mb-0">{{ $selectedRecipeSubRecipe->name }}</h3>
</div>
@php $selectedRecipeSubRecipePositionsFiltered = FindAllObjectsInArrayByPropertyValue($selectedRecipeSubRecipesPositions, 'recipe_id', $selectedRecipeSubRecipe->id); @endphp
@if(count($selectedRecipeSubRecipePositionsFiltered) > 0)
<div class="card-body">
<h5 class="mb-0">{{ $L('Ingredients') }}</h5>
</div>
<ul class="list-group list-group-flush">
@foreach($selectedRecipePositions as $selectedRecipePosition)
@php $lastGroup = 'undefined'; @endphp
@foreach($selectedRecipeSubRecipePositionsFiltered as $selectedRecipePosition)
@if($lastGroup != $selectedRecipePosition->ingredient_group)
<h5 class="mb-2 mt-2 ml-4"><strong>{{ $selectedRecipePosition->ingredient_group }}</strong></h5>
@endif
<li class="list-group-item">
{{ $selectedRecipePosition->amount }} {{ Pluralize($selectedRecipePosition->amount, FindObjectInArrayByPropertyValue($quantityunits, 'id', $selectedRecipePosition->qu_id)->name, FindObjectInArrayByPropertyValue($quantityunits, 'id', $selectedRecipePosition->qu_id)->name_plural) }} {{ FindObjectInArrayByPropertyValue($products, 'id', $selectedRecipePosition->product_id)->name }}
<span class="timeago-contextual">@if(FindObjectInArrayByPropertyValue($recipesFulfillment, 'recipe_pos_id', $selectedRecipePosition->id)->need_fulfilled == 1) {{ $L('Enough in stock') }} @else {{ $L('Not enough in stock, #1 missing, #2 already on shopping list', FindObjectInArrayByPropertyValue($recipesFulfillment, 'recipe_pos_id', $selectedRecipePosition->id)->missing_amount, FindObjectInArrayByPropertyValue($recipesFulfillment, 'recipe_pos_id', $selectedRecipePosition->id)->amount_on_shopping_list) }} @endif</span>
@if(!empty($selectedRecipePosition->note))
<div class="text-muted">{{ $selectedRecipePosition->note }} </div>
<div class="text-muted">{!! nl2br($selectedRecipePosition->note) !!}</div>
@endif
</li>
@php $lastGroup = $selectedRecipePosition->ingredient_group; @endphp
@endforeach
</ul>
@endif
@if(!empty($selectedRecipeSubRecipe->description))
<div class="card-body">
<h5>{{ $L('Preparation') }}</h5>
{!! $selectedRecipeSubRecipe->description !!}
</div>
@endif
@endforeach
<!-- Selected recipe -->
@if($selectedRecipePositions->count() > 0)
<div class="card-body">
<h5 class="mb-0">{{ $L('Ingredients') }}</h5>
</div>
<ul class="list-group list-group-flush">
@php $lastGroup = 'undefined'; @endphp
@foreach($selectedRecipePositions as $selectedRecipePosition)
@if($lastGroup != $selectedRecipePosition->ingredient_group)
<h5 class="mb-2 mt-2 ml-4"><strong>{{ $selectedRecipePosition->ingredient_group }}</strong></h5>
@endif
<li class="list-group-item">
{{ $selectedRecipePosition->amount }} {{ Pluralize($selectedRecipePosition->amount, FindObjectInArrayByPropertyValue($quantityunits, 'id', $selectedRecipePosition->qu_id)->name, FindObjectInArrayByPropertyValue($quantityunits, 'id', $selectedRecipePosition->qu_id)->name_plural) }} {{ FindObjectInArrayByPropertyValue($products, 'id', $selectedRecipePosition->product_id)->name }}
<span class="timeago-contextual">@if(FindObjectInArrayByPropertyValue($recipesFulfillment, 'recipe_pos_id', $selectedRecipePosition->id)->need_fulfilled == 1) {{ $L('Enough in stock') }} @else {{ $L('Not enough in stock, #1 missing, #2 already on shopping list', FindObjectInArrayByPropertyValue($recipesFulfillment, 'recipe_pos_id', $selectedRecipePosition->id)->missing_amount, FindObjectInArrayByPropertyValue($recipesFulfillment, 'recipe_pos_id', $selectedRecipePosition->id)->amount_on_shopping_list) }} @endif</span>
@if(!empty($selectedRecipePosition->note))
<div class="text-muted">{!! nl2br($selectedRecipePosition->note) !!}</div>
@endif
</li>
@php $lastGroup = $selectedRecipePosition->ingredient_group; @endphp
@endforeach
</ul>
@endif
@if(!empty($selectedRecipe->description))
<div class="card-body">
<h5>{{ $L('Preparation') }}</h5>
{!! $selectedRecipe->description !!}
</div>
@endif
</div>
</div>
@endif

View File

@@ -71,7 +71,7 @@
</a>
</td>
<td>
@if(!empty($listItem->product_id)) {{ FindObjectInArrayByPropertyValue($products, 'id', $listItem->product_id)->name }}<br>@endif<em>{{ $listItem->note }}</em>
@if(!empty($listItem->product_id)) {{ FindObjectInArrayByPropertyValue($products, 'id', $listItem->product_id)->name }}<br>@endif<em>{!! nl2br($listItem->note) !!}</em>
</td>
<td>
{{ $listItem->amount + $listItem->amount_autoadded }} @if(!empty($listItem->product_id)){{ Pluralize($listItem->amount + $listItem->amount_autoadded, FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $listItem->product_id)->qu_id_purchase)->name, FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $listItem->product_id)->qu_id_purchase)->name_plural) }}@endif

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')
<div class="row">
<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-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>
@@ -36,6 +41,15 @@
@endforeach
</select>
</div>
<div class="col-xs-12 col-md-6 col-xl-3">
<label for="location-filter">{{ $L('Filter by product group') }}</label> <i class="fas fa-filter"></i>
<select class="form-control" id="product-group-filter">
<option value="all">{{ $L('All') }}</option>
@foreach($productGroups as $productGroup)
<option value="{{ $productGroup->name }}">{{ $productGroup->name }}</option>
@endforeach
</select>
</div>
<div class="col-xs-12 col-md-6 col-xl-3">
<label for="status-filter">{{ $L('Filter by status') }}</label> <i class="fas fa-filter"></i>
<select class="form-control" id="status-filter">
@@ -62,11 +76,12 @@
<th>{{ $L('Next best before date') }}</th>
<th class="d-none">Hidden location</th>
<th class="d-none">Hidden status</th>
<th class="d-none">Hidden product group</th>
</tr>
</thead>
<tbody>
@foreach($currentStock as $currentStockEntry)
<tr id="product-{{ $currentStockEntry->product_id }}-row" class="@if($currentStockEntry->best_before_date < date('Y-m-d', strtotime('-1 days')) && $currentStockEntry->amount > 0) table-danger @elseif($currentStockEntry->best_before_date < date('Y-m-d', strtotime("+$nextXDays days")) && $currentStockEntry->amount > 0) table-warning @elseif (FindObjectInArrayByPropertyValue($missingProducts, 'id', $currentStockEntry->product_id) !== null) table-info @endif">
<tr id="product-{{ $currentStockEntry->product_id }}-row" class="@if($currentStockEntry->best_before_date < date('Y-m-d 23:59:59', strtotime('-1 days')) && $currentStockEntry->amount > 0) table-danger @elseif($currentStockEntry->best_before_date < date('Y-m-d 23:59:59', strtotime("+$nextXDays days")) && $currentStockEntry->amount > 0) table-warning @elseif (FindObjectInArrayByPropertyValue($missingProducts, 'id', $currentStockEntry->product_id) !== null) table-info @endif">
<td class="fit-content">
<a class="btn btn-success btn-sm product-consume-button @if($currentStockEntry->amount == 0) disabled @endif" href="#" data-toggle="tooltip" data-placement="left" title="{{ $L('Consume #3 #1 of #2', FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_stock)->name, FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name, 1) }}"
data-product-id="{{ $currentStockEntry->product_id }}"
@@ -82,26 +97,29 @@
data-consume-amount="{{ $currentStockEntry->amount }}">
<i class="fas fa-utensils"></i> {{ $L('All') }}
</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 class="product-name-cell"
data-picture-url="{{ $U('/api/file/productpictures?file_name=' . FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->picture_file_name) }}"
data-product-id="{{ $currentStockEntry->product_id }}"
data-product-name="{{ FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name }}"
data-product-has-picture="{{ BoolToString(!empty(FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->picture_file_name)) }}">
{{ FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name }}@if(!empty(FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->picture_file_name)) <i class="fas fa-image text-muted"></i>@endif
<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>
</td>
<td>
<span id="product-{{ $currentStockEntry->product_id }}-amount">{{ $currentStockEntry->amount }}</span> {{ Pluralize($currentStockEntry->amount, FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_stock)->name, FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_stock)->name_plural) }}
</td>
<td>
<span id="product-{{ $currentStockEntry->product_id }}-next-best-before-date">{{ $currentStockEntry->best_before_date }}</span>
<time id="product-{{ $currentStockEntry->product_id }}-next-best-before-date-timeago" class="timeago timeago-contextual" datetime="{{ $currentStockEntry->best_before_date }}"></time>
<time id="product-{{ $currentStockEntry->product_id }}-next-best-before-date-timeago" class="timeago timeago-contextual" datetime="{{ $currentStockEntry->best_before_date }} 23:59:59"></time>
</td>
<td class="d-none">
{{ FindObjectInArrayByPropertyValue($locations, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->location_id)->name }}
</td>
<td class="d-none">
@if($currentStockEntry->best_before_date < date('Y-m-d', strtotime('-1 days')) && $currentStockEntry->amount > 0) expired @elseif($currentStockEntry->best_before_date < date('Y-m-d', strtotime("+$nextXDays days")) && $currentStockEntry->amount > 0) expiring @elseif (FindObjectInArrayByPropertyValue($missingProducts, 'id', $currentStockEntry->product_id) !== null) belowminstockamount @endif
@if($currentStockEntry->best_before_date < date('Y-m-d 23:59:59', strtotime('-1 days')) && $currentStockEntry->amount > 0) expired @elseif($currentStockEntry->best_before_date < date('Y-m-d 23:59:59', strtotime("+$nextXDays days")) && $currentStockEntry->amount > 0) expiring @elseif (FindObjectInArrayByPropertyValue($missingProducts, 'id', $currentStockEntry->product_id) !== null) belowminstockamount @endif
</td>
@php $productGroup = FindObjectInArrayByPropertyValue($productGroups, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->product_group_id) @endphp
<td class="d-none">
@if($productGroup !== null){{ $productGroup->name }}@endif
</td>
</tr>
@endforeach
@@ -109,4 +127,17 @@
</table>
</div>
</div>
<div class="modal fade" id="stockoverview-productcard-modal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content text-center">
<div class="modal-body">
@include('components.productcard')
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ $L('Close') }}</button>
</div>
</div>
</div>
</div>
@stop

View File

@@ -7,8 +7,9 @@
resolved "https://github.com/berrnd/bootstrap-combobox.git#fcf0110146f4daab94888234c57d198b4ca5f129"
"@fortawesome/fontawesome-free@^5.1.0":
version "5.3.1"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.3.1.tgz#5466b8f31c1f493a96754c1426c25796d0633dd9"
version "5.4.2"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.4.2.tgz#b2d782da69c45ea61954291e2615a8f7741c96b2"
integrity sha512-lpruVv/4AkKUujyzHCNJ8yOceBf7Na/pFaX52ZI/BFMCYkUEwiQ1P0viZg//+WelrNNEgvMOvCMDEDaOMZ/u1w==
"TagManager@https://github.com/max-favilli/tagmanager.git#3.0.2":
version "3.0.1"
@@ -23,21 +24,25 @@
bootstrap@4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.0.0.tgz#ceb03842c145fcc1b9b4e15da2a05656ba68469a"
integrity sha512-gulJE5dGFo6Q61V/whS6VM4WIyrlydXfCgkE+Gxe5hjrJ8rXLLZlALq7zq2RPhOc45PSwQpJkrTnc2KgD6cvmA==
bootstrap@>=4.1.2, bootstrap@^4.1.1:
version "4.1.3"
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.1.3.tgz#0eb371af2c8448e8c210411d0cb824a6409a12be"
integrity sha512-rDFIzgXcof0jDyjNosjv4Sno77X4KuPeFxG2XZZv1/Kc8DRVGVADdoQyyOVDwPqL36DDmtCQbrpMCqvpPLJQ0w==
chart.js@2.7.1:
version "2.7.1"
resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-2.7.1.tgz#ae90b4aa4ff1f02decd6b1a2a8dabfd73c9f9886"
integrity sha512-pX1oQAY86MiuyZ2hY593Acbl4MLHKrBBhhmZ1YqSadzQbbsBE2rnd6WISoHjIsdf0WDeC0hbePYCz2ZxkV8L+g==
dependencies:
chartjs-color "~2.2.0"
moment "~2.18.0"
chart.js@^2.7.2:
version "2.7.2"
resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-2.7.2.tgz#3c9fde4dc5b95608211bdefeda7e5d33dffa5714"
version "2.7.3"
resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-2.7.3.tgz#cdb61618830bf216dc887e2f7b1b3c228b73c57e"
integrity sha512-3+7k/DbR92m6BsMUYP6M0dMsMVZpMnwkUyNSAbqolHKsbIzH2Q4LWVEHHYq7v0fmEV8whXE0DrjANulw9j2K5g==
dependencies:
chartjs-color "^2.1.0"
moment "^2.10.2"
@@ -45,12 +50,14 @@ chart.js@^2.7.2:
chartjs-color-string@^0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/chartjs-color-string/-/chartjs-color-string-0.5.0.tgz#8d3752d8581d86687c35bfe2cb80ac5213ceb8c1"
integrity sha512-amWNvCOXlOUYxZVDSa0YOab5K/lmEhbFNKI55PWc4mlv28BDzA7zaoQTGxSBgJMHIW+hGX8YUrvw/FH4LyhwSQ==
dependencies:
color-name "^1.0.0"
chartjs-color@^2.1.0, chartjs-color@~2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/chartjs-color/-/chartjs-color-2.2.0.tgz#84a2fb755787ed85c39dd6dd8c7b1d88429baeae"
integrity sha1-hKL7dVeH7YXDndbdjHsdiEKbrq4=
dependencies:
chartjs-color-string "^0.5.0"
color-convert "^0.5.3"
@@ -58,14 +65,17 @@ chartjs-color@^2.1.0, chartjs-color@~2.2.0:
color-convert@^0.5.3:
version "0.5.3"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-0.5.3.tgz#bdb6c69ce660fadffe0b0007cc447e1b9f7282bd"
integrity sha1-vbbGnOZg+t/+CwAHzER+G59ygr0=
color-name@^1.0.0:
version "1.1.4"
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
datatables.net-bs4@1.10.16:
version "1.10.16"
resolved "https://registry.yarnpkg.com/datatables.net-bs4/-/datatables.net-bs4-1.10.16.tgz#9eee67cfa8565bd3807a603a188305f7d0e20e32"
integrity sha1-nu5nz6hWW9OAemA6GIMF99DiDjI=
dependencies:
datatables.net "1.10.16"
jquery ">=1.7"
@@ -73,6 +83,7 @@ datatables.net-bs4@1.10.16:
datatables.net-bs4@^1.10.15, datatables.net-bs4@^1.10.19:
version "1.10.19"
resolved "https://registry.yarnpkg.com/datatables.net-bs4/-/datatables.net-bs4-1.10.19.tgz#0608dff22008cf3c7b8a68b1bc702ed255b404fb"
integrity sha512-pgeP17w4aPR7HIxIwuJghfqXULjdg1K6xMUUKDyCERJRSNNK4MRToFfELtIsluLNN555YBK4Kx8nihX5/ZT1Fw==
dependencies:
datatables.net "1.10.19"
jquery ">=1.7"
@@ -80,6 +91,7 @@ datatables.net-bs4@^1.10.15, datatables.net-bs4@^1.10.19:
datatables.net-colreorder-bs4@^1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/datatables.net-colreorder-bs4/-/datatables.net-colreorder-bs4-1.5.1.tgz#7ded353a68296b85f0dbfb234b8cb5d8fc2bee3c"
integrity sha512-mi5wxkjnkI8fxGJ9e/Vu20E7MMZj7yK3xjLIzDrsHOHQupr6FF6eJnUoVH4gDo9Ynfb/yApVbnuW29v/YjnDKw==
dependencies:
datatables.net-bs4 "^1.10.15"
datatables.net-colreorder "1.5.1"
@@ -88,6 +100,7 @@ datatables.net-colreorder-bs4@^1.5.1:
datatables.net-colreorder@1.5.1, datatables.net-colreorder@^1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/datatables.net-colreorder/-/datatables.net-colreorder-1.5.1.tgz#ee5eacd7178b5fd9396aab44d4907aae35086f8c"
integrity sha512-nKV0ZBOdOG+CCrtDZZlTOvhu9NC53pr4rYR8Xhd3KIKipLZohWWdBoOFGMu+VKDvllg2Xj79oS/wicYSiNyteA==
dependencies:
datatables.net "^1.10.15"
jquery ">=1.7"
@@ -95,6 +108,7 @@ datatables.net-colreorder@1.5.1, datatables.net-colreorder@^1.5.1:
datatables.net-responsive-bs4@^2.2.3:
version "2.2.3"
resolved "https://registry.yarnpkg.com/datatables.net-responsive-bs4/-/datatables.net-responsive-bs4-2.2.3.tgz#639de17c1d31210ebf2b3c25f1c774c13f729e94"
integrity sha512-SQaWI0uLuPcaiBBin9zX+MuQfTSIkK1bYxbXqUV6NLkHCVa6PMQK7Rvftj0ywG4R7uOtjbzY8nSVqxEKvQI0Vg==
dependencies:
datatables.net-bs4 "^1.10.15"
datatables.net-responsive "2.2.3"
@@ -103,6 +117,7 @@ datatables.net-responsive-bs4@^2.2.3:
datatables.net-responsive@2.2.3, datatables.net-responsive@^2.2.3:
version "2.2.3"
resolved "https://registry.yarnpkg.com/datatables.net-responsive/-/datatables.net-responsive-2.2.3.tgz#50a2b1b4955b16b32f573a3f00f473b0bfbee913"
integrity sha512-8D6VtZcyuH3FG0Hn5A4LPZQEOX3+HrRFM7HjpmsQc/nQDBbdeBLkJX4Sh/o1nzFTSneuT1Wh/lYZHVPpjcN+Sw==
dependencies:
datatables.net "^1.10.15"
jquery ">=1.7"
@@ -110,6 +125,7 @@ datatables.net-responsive@2.2.3, datatables.net-responsive@^2.2.3:
datatables.net-rowgroup-bs4@^1.0.4:
version "1.1.0"
resolved "https://registry.yarnpkg.com/datatables.net-rowgroup-bs4/-/datatables.net-rowgroup-bs4-1.1.0.tgz#bcaa9842bc9cf70eeba19e8af6edad190c7b896e"
integrity sha512-R2C+lLbVyPuyITVJo/1va97nHOcQ06M0kReTmN4zaR4/SRkFDfOVXz48w2jT1AJW/j65DYAh+OHwUSYgcgKisA==
dependencies:
datatables.net-bs4 "^1.10.15"
datatables.net-rowgroup "1.1.0"
@@ -118,6 +134,7 @@ datatables.net-rowgroup-bs4@^1.0.4:
datatables.net-rowgroup@1.1.0, datatables.net-rowgroup@^1.0.4:
version "1.1.0"
resolved "https://registry.yarnpkg.com/datatables.net-rowgroup/-/datatables.net-rowgroup-1.1.0.tgz#638efb37a1a15f5b3402b7dbce89b3bcdc286f1a"
integrity sha512-OPIiiMTbIoh1hSnJ9lx3avqKpYtEPux5iA6jpWKRdyKbdYU8UQbi5ht3b096frYQplIuAQnQcwqpGmvrf77l6g==
dependencies:
datatables.net "^1.10.15"
jquery ">=1.7"
@@ -125,6 +142,7 @@ datatables.net-rowgroup@1.1.0, datatables.net-rowgroup@^1.0.4:
datatables.net-select-bs4@^1.2.7:
version "1.2.7"
resolved "https://registry.yarnpkg.com/datatables.net-select-bs4/-/datatables.net-select-bs4-1.2.7.tgz#5e4ddd8feb412e974b54a15e81b2bb29840ba55b"
integrity sha512-ch+PI9rcp7FAddgF7ZyUfqptoSq1Ck0FadL/HHh6X1xazT+VwZdPy+Gtt2qjMCZhJfCkzjAGxvi1Hk4aFgr3vg==
dependencies:
datatables.net-bs4 "^1.10.15"
datatables.net-select "1.2.7"
@@ -133,6 +151,7 @@ datatables.net-select-bs4@^1.2.7:
datatables.net-select@1.2.7, datatables.net-select@^1.2.7:
version "1.2.7"
resolved "https://registry.yarnpkg.com/datatables.net-select/-/datatables.net-select-1.2.7.tgz#7d5badfca49c438f8b51df04483d8d77857e917c"
integrity sha512-C3XDi7wpruGjDXV36dc9hN/FrAX9GOFvBZ7+KfKJTBNkGFbbhdzHS91SMeGiwRXPYivAyxfPTcVVndVaO83uBQ==
dependencies:
datatables.net "^1.10.15"
jquery ">=1.7"
@@ -140,60 +159,73 @@ datatables.net-select@1.2.7, datatables.net-select@^1.2.7:
datatables.net@1.10.16:
version "1.10.16"
resolved "https://registry.yarnpkg.com/datatables.net/-/datatables.net-1.10.16.tgz#4b052d1082824261b68eed9d22741b711d3d2469"
integrity sha1-SwUtEIKCQmG2ju2dInQbcR09JGk=
dependencies:
jquery ">=1.7"
datatables.net@1.10.19, datatables.net@^1.10.15, datatables.net@^1.10.19:
version "1.10.19"
resolved "https://registry.yarnpkg.com/datatables.net/-/datatables.net-1.10.19.tgz#97a1ed41c85e62d61040603481b59790a172dd1f"
integrity sha512-+ljXcI6Pj3PTGy5pesp3E5Dr3x3AV45EZe0o1r0gKENN2gafBKXodVnk2ypKwl2tTmivjxbkiqoWnipTefyBTA==
dependencies:
jquery ">=1.7"
font-awesome@4.7.0:
version "4.7.0"
resolved "https://registry.yarnpkg.com/font-awesome/-/font-awesome-4.7.0.tgz#8fa8cf0411a1a31afd07b06d2902bb9fc815a133"
integrity sha1-j6jPBBGhoxr9B7BtKQK7n8gVoTM=
jquery-serializejson@^2.8.1:
version "2.8.1"
resolved "https://registry.yarnpkg.com/jquery-serializejson/-/jquery-serializejson-2.8.1.tgz#fc40dd11e5d9a6fd2a3614fdcba89e4af794f0a8"
integrity sha1-/EDdEeXZpv0qNhT9y6ieSveU8Kg=
jquery-ui-dist@^1.12.1:
version "1.12.1"
resolved "https://registry.yarnpkg.com/jquery-ui-dist/-/jquery-ui-dist-1.12.1.tgz#5c0815d3cc6f90ff5faaf5b268a6e23b4ca904fa"
integrity sha1-XAgV08xvkP9fqvWyaKbiO0ypBPo=
jquery.easing@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/jquery.easing/-/jquery.easing-1.4.1.tgz#47982c5836bd758fd48494923c4a101ef6e93e3b"
integrity sha1-R5gsWDa9dY/UhJSSPEoQHvbpPjs=
jquery@1:
version "1.12.4"
resolved "https://registry.yarnpkg.com/jquery/-/jquery-1.12.4.tgz#01e1dfba290fe73deba77ceeacb0f9ba2fec9e0c"
integrity sha1-AeHfuikP5z3rp3zurLD5ui/sngw=
jquery@3.3.1, jquery@>=1.12.0, jquery@>=1.2.3, jquery@>=1.7, jquery@^3.0, jquery@^3.3.1:
version "3.3.1"
resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.3.1.tgz#958ce29e81c9790f31be7792df5d4d95fc57fbca"
integrity sha512-Ubldcmxp5np52/ENotGxlLe6aGMvmF4R8S6tZjsP6Knsaxd/xp3Zrh50cG93lR6nPXyUFwzN3ZSOQI0wRJNdGg==
moment-timezone@^0.5.11:
version "0.5.21"
resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.21.tgz#3cba247d84492174dbf71de2a9848fa13207b845"
integrity sha512-j96bAh4otsgj3lKydm3K7kdtA3iKf2m6MY2iSYCzCm5a1zmHo1g+aK3068dDEeocLZQIS9kU8bsdQHLqEvgW0A==
dependencies:
moment ">= 2.9.0"
"moment@>= 2.9.0", moment@^2.10.2, moment@^2.22.2:
version "2.22.2"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.22.2.tgz#3c257f9839fc0e93ff53149632239eb90783ff66"
integrity sha1-PCV/mDn8DpP/UxSWMiOeuQeD/2Y=
moment@~2.18.0:
version "2.18.1"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.18.1.tgz#c36193dd3ce1c2eed2adb7c802dbbc77a81b1c0f"
integrity sha1-w2GT3Tzhwu7SrbfIAtu8d6gbHA8=
popper.js@^1.14.3:
version "1.14.4"
resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.14.4.tgz#8eec1d8ff02a5a3a152dd43414a15c7b79fd69b6"
integrity sha1-juwdj/AqWjoVLdQ0FKFce3n9abY=
startbootstrap-sb-admin@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/startbootstrap-sb-admin/-/startbootstrap-sb-admin-4.0.0.tgz#cf141a260d031b36bdc013c68200a1c1ea6c9881"
integrity sha512-qnIu5fcjAb288+MgeaOBnZ93eq4lXb3cpUgbqGUVG2afDRFgsIX0ii3BgbWHrQ7jYZRYADEKWe580F8uUpIAHg==
dependencies:
bootstrap "4.0.0"
chart.js "2.7.1"
@@ -205,14 +237,17 @@ startbootstrap-sb-admin@^4.0.0:
summernote@^0.8.10:
version "0.8.10"
resolved "https://registry.yarnpkg.com/summernote/-/summernote-0.8.10.tgz#21a5d7f18a3b07500b58b60d5907417a54897520"
integrity sha512-1b4ESCiY9HW+12HYXCntjbThVgeYNaYKfKL7pC4Jqjo/WDS4G4mMtd2kPuCw56HxeRT67d+zlehopaE+M4o6aQ==
swagger-ui-dist@^3.17.3:
version "3.19.2"
resolved "https://registry.yarnpkg.com/swagger-ui-dist/-/swagger-ui-dist-3.19.2.tgz#3218f205e7cbc9f0c7c11fabbee07340173ae939"
version "3.19.4"
resolved "https://registry.yarnpkg.com/swagger-ui-dist/-/swagger-ui-dist-3.19.4.tgz#18053b1e7fa135c00dce0f2ebfaeb05d186cd5b5"
integrity sha512-YxC2mN53oXzF3hobm9cFhR14j9amNe2zUeKQa/tHckDJKItIxLQd+ksfeJgKw+pK04C2yppscOCFlIAvgR+jNQ==
tempusdominus-bootstrap-4@^5.0.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/tempusdominus-bootstrap-4/-/tempusdominus-bootstrap-4-5.1.1.tgz#f1c765be4f17ffd86a56f2cb7fb7d4b19ea029c7"
tempusdominus-bootstrap-4@^5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/tempusdominus-bootstrap-4/-/tempusdominus-bootstrap-4-5.1.2.tgz#3c9906ca6e5d563faa0b81b2fdc6aa79cad9c0be"
integrity sha512-ksD8qc4wOJeE19wvryXmEpRzMUSZu4wSOdG6zKSn8l4ccad16249KOX1j0CccyZpuuES/n4FLqLAUB+Dd1LTBA==
dependencies:
bootstrap ">=4.1.2"
jquery "^3.0"
@@ -223,11 +258,13 @@ tempusdominus-bootstrap-4@^5.0.1:
timeago@^1.6.3:
version "1.6.3"
resolved "https://registry.yarnpkg.com/timeago/-/timeago-1.6.3.tgz#162a1adae99356297df59339837d09f1b0f36465"
integrity sha512-x97d1X1KsNapWJTgCOOAy/59XagYu2WsDTAH/yvPsWi5bqtGbLPaVZBv3HZ3jTpakHR+JGGyrI9qC0yuvIAvnQ==
dependencies:
jquery ">=1.2.3"
toastr@^2.1.4:
version "2.1.4"
resolved "https://registry.yarnpkg.com/toastr/-/toastr-2.1.4.tgz#8b43be64fb9d0c414871446f2db8e8ca4e95f181"
integrity sha1-i0O+ZPudDEFIcURvLbjoyk6V8YE=
dependencies:
jquery ">=1.12.0"