Implement async loading of products, add TODOs

This commit is contained in:
Alexander Sagen 2022-03-12 21:50:51 +01:00
parent 7532626123
commit 94b6bf8eef
No known key found for this signature in database
GPG Key ID: C1B6BD677756F470
78 changed files with 9748 additions and 11044 deletions

View File

@ -6,7 +6,7 @@ use LessQL\Result;
class BaseApiController extends BaseController class BaseApiController extends BaseController
{ {
const PATTERN_FIELD = '[A-Za-z_][A-Za-z0-9_]+'; const PATTERN_FIELD = '[A-Za-z_][A-Za-z0-9_\.]+';
const PATTERN_OPERATOR = '!?(=|~|<|>|(>=)|(<=)|(§))'; const PATTERN_OPERATOR = '!?(=|~|<|>|(>=)|(<=)|(§))';
@ -16,8 +16,7 @@ class BaseApiController extends BaseController
protected function ApiResponse(\Psr\Http\Message\ResponseInterface $response, $data, $cache = false) protected function ApiResponse(\Psr\Http\Message\ResponseInterface $response, $data, $cache = false)
{ {
if ($cache) if ($cache) {
{
$response = $response->withHeader('Cache-Control', 'max-age=2592000'); $response = $response->withHeader('Cache-Control', 'max-age=2592000');
} }
@ -39,38 +38,93 @@ class BaseApiController extends BaseController
public function FilteredApiResponse(\Psr\Http\Message\ResponseInterface $response, Result $data, array $query) public function FilteredApiResponse(\Psr\Http\Message\ResponseInterface $response, Result $data, array $query)
{ {
$data = $this->queryData($data, $query); // get total row count
$response = $response->withHeader('x-rowcount-total', $data->count());
// apply filter, get filtered row count
$data = $this->applyQuery($data, $query);
$response = $response->withHeader('x-rowcount-filtered', $data->count());
// apply limit/order
$data = $this->applyOrder($data, $query);
$data = $this->applyLimit($data, $query);
return $this->ApiResponse($response, $data); return $this->ApiResponse($response, $data);
} }
protected function queryData(Result $data, array $query) protected function applyQuery(Result $data, array $query): Result
{
if (isset($query['query']))
{ {
if (isset($query['query'])) {
$data = $this->filter($data, $query['query']); $data = $this->filter($data, $query['query']);
} }
if (isset($query['limit'])) if (isset($query['search'])) {
// get list of fields from table
$stmt = $this->getDatabase()->prepare('SELECT `sql` FROM sqlite_master WHERE `name` = ? LIMIT 1');
$stmt->execute([$data->getTable()]);
$sql = $stmt->fetchColumn();
$sql = substr($sql, strpos($sql, '(') + 1);
$sql = substr($sql, 0, strrpos($sql, ')'));
$sql = trim($sql);
while (preg_match('/\(.*?\)/', $sql) === 1) {
$sql = preg_replace('/\([^\(\)]*\)/', '', $sql);
}
$fields = array_map(function ($field) {
preg_match('/\s*([^\s]+)/', $field, $matches);
return $matches[1];
}, explode(',', $sql));
$join_info = match ($data->getTable()) {
'products' => [
'product_group_id' => ['field' => 'name', 'table' => 'product_groups'],
'location_id' => ['field' => 'name', 'table' => 'locations'],
'shopping_location_id' => ['field' => 'name', 'table' => 'shopping_locations'],
'qu_id_purchase' => ['field' => 'name', 'table' => 'quantity_units'],
'qu_id_stock' => ['field' => 'name', 'table' => 'quantity_units'],
'parent_product_id' => ['field' => 'name', 'table' => 'products'],
],
default => [],
};
// create search query that matches any field
$fields_query = implode(' OR ', array_map(function ($field) use ($join_info) {
$field_escaped = '`' . str_replace('`', '``', $field) . '`';
if (array_key_exists($field, $join_info)) {
$table_escaped = '`' . str_replace('`', '``', $join_info[$field]['table']) . '`';
return $field_escaped . ' IN(SELECT id FROM ' . $table_escaped . ' WHERE `' . str_replace('`', '``', $join_info[$field]['field']) . '` LIKE ? ESCAPE \'\\\')';
}
return $field_escaped . ' LIKE ? ESCAPE \'\\\'';
}, $fields));
// apply search query
$data = $data->where($fields_query, array_fill(0, count($fields), '%' . str_replace(['\\', '%', '_', '*'], ['\\\\', '\\%', '\\_', '%'], $query['search']) . '%'));
}
return $data;
}
protected function applyLimit(Result $data, array $query): Result
{ {
if (isset($query['limit'])) {
$data = $data->limit(intval($query['limit']), intval($query['offset'] ?? 0)); $data = $data->limit(intval($query['limit']), intval($query['offset'] ?? 0));
} }
if (isset($query['order'])) return $data;
{
$parts = explode(':', $query['order']);
if (count($parts) == 1)
{
$data = $data->orderBy($parts[0]);
}
else
{
if ($parts[1] != 'asc' && $parts[1] != 'desc')
{
throw new \Exception('Invalid sort order ' . $parts[1]);
} }
$data = $data->orderBy($parts[0], $parts[1]); protected function applyOrder(Result $data, array $query): Result
{
if (isset($query['order'])) {
$parts = explode(',', $query['order']);
foreach ($parts as $part) {
$col_dir = explode(':', $part, 2);
if (count($col_dir) == 1) {
$data = $data->orderBy($col_dir[0]);
} else {
if ($col_dir[1] != 'asc' && $col_dir[1] != 'desc' && $col_dir[1] != 'collate nocase') {
throw new \Exception('Invalid sort order ' . $col_dir[1]);
}
$data = $data->orderBy($col_dir[0], $col_dir[1]);
}
} }
} }
@ -79,8 +133,7 @@ class BaseApiController extends BaseController
protected function filter(Result $data, array $query): Result protected function filter(Result $data, array $query): Result
{ {
foreach ($query as $q) foreach ($query as $q) {
{
$matches = []; $matches = [];
preg_match( preg_match(
'/(?P<field>' . self::PATTERN_FIELD . ')' '/(?P<field>' . self::PATTERN_FIELD . ')'
@ -90,47 +143,63 @@ class BaseApiController extends BaseController
$matches $matches
); );
if (!array_key_exists('field', $matches) || !array_key_exists('op', $matches) || !array_key_exists('value', $matches)) if (!array_key_exists('field', $matches) || !array_key_exists('op', $matches) || !array_key_exists('value', $matches) || !in_array($matches['op'], ['=', '!=', '~', '!~', '<', '>', '>=', '<=', '§'], true)) {
{
throw new \Exception('Invalid query'); throw new \Exception('Invalid query');
} }
list('field' => $field, 'op' => $op, 'value' => $value) = $matches;
$sqlOrNull = ''; $params = match ($op) {
if (strtolower($matches['value']) == 'null') '=' => [$value],
{ '!=' => [$value],
$sqlOrNull = ' OR ' . $matches['field'] . ' IS NULL'; '~' => ['%' . $value . '%'],
'!~' => ['%' . $value . '%'],
'<' => [$value],
'>' => [$value],
'>=' => [$value],
'<=' => [$value],
'§' => [$value],
default => [],
};
$where_prefix = '';
$where_suffix = '';
if (strpos($field, '.') !== false) {
list($join, $field) = explode('.', $field, 2);
$join_info = match ($data->getTable()) {
'products' => [
'product_group' => ['id_field' => 'product_group_id', 'table' => 'product_groups'],
'location' => ['id_field' => 'location_id', 'table' => 'locations'],
'shopping_location' => ['id_field' => 'shopping_location_id', 'table' => 'shopping_locations'],
'qu_purchase' => ['id_field' => 'qu_id_purchase', 'table' => 'quantity_units'],
'qu_stock' => ['id_field' => 'qu_id_stock', 'table' => 'quantity_units'],
'parent_product' => ['id_field' => 'parent_product_id', 'table' => 'products'],
],
default => [],
};
if (!array_key_exists($join, $join_info)) {
throw new \Exception('Invalid query');
}
$field_escaped = '`' . str_replace('`', '``', $join_info[$join]['id_field']) . '`';
$table_escaped = '`' . str_replace('`', '``', $join_info[$join]['table']) . '`';
$where_prefix = $field_escaped . ' IN(SELECT id FROM ' . $table_escaped . ' WHERE ';
$where_suffix = ')';
} }
switch ($matches['op']) { $field_escaped = '`' . str_replace('`', '``', $field) . '`';
case '=': $where = match ($op) {
$data = $data->where($matches['field'] . ' = ?' . $sqlOrNull, $matches['value']); '=' => $field_escaped . ' = ?' . (strtolower($value) === 'null' ? ' OR ' . $field_escaped . ' IS NULL' : ''),
break; '!=' => $field_escaped . ' != ?' . (strtolower($value) === 'null' ? ' OR ' . $field_escaped . ' IS NULL' : ''),
case '!=': '~' => $field_escaped . ' LIKE ?',
$data = $data->where($matches['field'] . ' != ?' . $sqlOrNull, $matches['value']); '!~' => $field_escaped . ' NOT LIKE ?',
break; '<' => $field_escaped . ' < ?',
case '~': '>' => $field_escaped . ' > ?',
$data = $data->where($matches['field'] . ' LIKE ?', '%' . $matches['value'] . '%'); '>=' => $field_escaped . ' >= ?',
break; '<=' => $field_escaped . ' <= ?',
case '!~': '§' => $field_escaped . ' REGEXP ?',
$data = $data->where($matches['field'] . ' NOT LIKE ?', '%' . $matches['value'] . '%'); default => '',
break; };
case '<':
$data = $data->where($matches['field'] . ' < ?', $matches['value']);
break;
case '>':
$data = $data->where($matches['field'] . ' > ?', $matches['value']);
break;
case '>=':
$data = $data->where($matches['field'] . ' >= ?', $matches['value']);
break;
case '<=':
$data = $data->where($matches['field'] . ' <= ?', $matches['value']);
break;
case '§':
$data = $data->where($matches['field'] . ' REGEXP ?', $matches['value']);
break;
} $data = $data->where($where_prefix . $where . $where_suffix, $params);
} }
return $data; return $data;
@ -138,8 +207,7 @@ class BaseApiController extends BaseController
protected function getOpenApispec() protected function getOpenApispec()
{ {
if ($this->OpenApiSpec == null) if ($this->OpenApiSpec == null) {
{
$this->OpenApiSpec = json_decode(file_get_contents(__DIR__ . '/../grocy.openapi.json')); $this->OpenApiSpec = json_decode(file_get_contents(__DIR__ . '/../grocy.openapi.json'));
} }

View File

@ -13,19 +13,15 @@ class ChoresController extends BaseController
$usersService = $this->getUsersService(); $usersService = $this->getUsersService();
$users = $usersService->GetUsersAsDto(); $users = $usersService->GetUsersAsDto();
if ($args['choreId'] == 'new') if ($args['choreId'] == 'new') {
{
return $this->renderPage($response, 'choreform', [ return $this->renderPage($response, 'choreform', [
'periodTypes' => GetClassConstants('\Grocy\Services\ChoresService', 'CHORE_PERIOD_TYPE_'), 'periodTypes' => GetClassConstants('\Grocy\Services\ChoresService', 'CHORE_PERIOD_TYPE_'),
'mode' => 'create', 'mode' => 'create',
'userfields' => $this->getUserfieldsService()->GetFields('chores'), 'userfields' => $this->getUserfieldsService()->GetFields('chores'),
'assignmentTypes' => GetClassConstants('\Grocy\Services\ChoresService', 'CHORE_ASSIGNMENT_TYPE_'), 'assignmentTypes' => GetClassConstants('\Grocy\Services\ChoresService', 'CHORE_ASSIGNMENT_TYPE_'),
'users' => $users, 'users' => $users,
'products' => $this->getDatabase()->products()->orderBy('name', 'COLLATE NOCASE')
]); ]);
} } else {
else
{
return $this->renderPage($response, 'choreform', [ return $this->renderPage($response, 'choreform', [
'chore' => $this->getDatabase()->chores($args['choreId']), 'chore' => $this->getDatabase()->chores($args['choreId']),
'periodTypes' => GetClassConstants('\Grocy\Services\ChoresService', 'CHORE_PERIOD_TYPE_'), 'periodTypes' => GetClassConstants('\Grocy\Services\ChoresService', 'CHORE_PERIOD_TYPE_'),
@ -33,19 +29,15 @@ class ChoresController extends BaseController
'userfields' => $this->getUserfieldsService()->GetFields('chores'), 'userfields' => $this->getUserfieldsService()->GetFields('chores'),
'assignmentTypes' => GetClassConstants('\Grocy\Services\ChoresService', 'CHORE_ASSIGNMENT_TYPE_'), 'assignmentTypes' => GetClassConstants('\Grocy\Services\ChoresService', 'CHORE_ASSIGNMENT_TYPE_'),
'users' => $users, 'users' => $users,
'products' => $this->getDatabase()->products()->orderBy('name', 'COLLATE NOCASE')
]); ]);
} }
} }
public function ChoresList(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) public function ChoresList(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{ {
if (isset($request->getQueryParams()['include_disabled'])) if (isset($request->getQueryParams()['include_disabled'])) {
{
$chores = $this->getDatabase()->chores()->orderBy('name', 'COLLATE NOCASE'); $chores = $this->getDatabase()->chores()->orderBy('name', 'COLLATE NOCASE');
} } else {
else
{
$chores = $this->getDatabase()->chores()->where('active = 1')->orderBy('name', 'COLLATE NOCASE'); $chores = $this->getDatabase()->chores()->where('active = 1')->orderBy('name', 'COLLATE NOCASE');
} }
@ -63,19 +55,15 @@ class ChoresController extends BaseController
public function Journal(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) public function Journal(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{ {
if (isset($request->getQueryParams()['months']) && filter_var($request->getQueryParams()['months'], FILTER_VALIDATE_INT) !== false) if (isset($request->getQueryParams()['months']) && filter_var($request->getQueryParams()['months'], FILTER_VALIDATE_INT) !== false) {
{
$months = $request->getQueryParams()['months']; $months = $request->getQueryParams()['months'];
$where = "tracked_time > DATE(DATE('now', 'localtime'), '-$months months')"; $where = "tracked_time > DATE(DATE('now', 'localtime'), '-$months months')";
} } else {
else
{
// Default 1 year // Default 1 year
$where = "tracked_time > DATE(DATE('now', 'localtime'), '-12 months')"; $where = "tracked_time > DATE(DATE('now', 'localtime'), '-12 months')";
} }
if (isset($request->getQueryParams()['chore']) && filter_var($request->getQueryParams()['chore'], FILTER_VALIDATE_INT) !== false) if (isset($request->getQueryParams()['chore']) && filter_var($request->getQueryParams()['chore'], FILTER_VALIDATE_INT) !== false) {
{
$choreId = $request->getQueryParams()['chore']; $choreId = $request->getQueryParams()['chore'];
$where .= " AND chore_id = $choreId"; $where .= " AND chore_id = $choreId";
} }
@ -96,20 +84,13 @@ class ChoresController extends BaseController
$chores = $this->getDatabase()->chores()->orderBy('name', 'COLLATE NOCASE'); $chores = $this->getDatabase()->chores()->orderBy('name', 'COLLATE NOCASE');
$currentChores = $this->getChoresService()->GetCurrent(); $currentChores = $this->getChoresService()->GetCurrent();
foreach ($currentChores as $currentChore) foreach ($currentChores as $currentChore) {
{ if (FindObjectInArrayByPropertyValue($chores, 'id', $currentChore->chore_id)->period_type !== \Grocy\Services\ChoresService::CHORE_PERIOD_TYPE_MANUALLY) {
if (FindObjectInArrayByPropertyValue($chores, 'id', $currentChore->chore_id)->period_type !== \Grocy\Services\ChoresService::CHORE_PERIOD_TYPE_MANUALLY) if ($currentChore->next_estimated_execution_time < date('Y-m-d H:i:s')) {
{
if ($currentChore->next_estimated_execution_time < date('Y-m-d H:i:s'))
{
$currentChore->due_type = 'overdue'; $currentChore->due_type = 'overdue';
} } elseif ($currentChore->next_estimated_execution_time <= date('Y-m-d 23:59:59')) {
elseif ($currentChore->next_estimated_execution_time <= date('Y-m-d 23:59:59'))
{
$currentChore->due_type = 'duetoday'; $currentChore->due_type = 'duetoday';
} } elseif ($nextXDays > 0 && $currentChore->next_estimated_execution_time <= date('Y-m-d H:i:s', strtotime('+' . $nextXDays . ' days'))) {
elseif ($nextXDays > 0 && $currentChore->next_estimated_execution_time <= date('Y-m-d H:i:s', strtotime('+' . $nextXDays . ' days')))
{
$currentChore->due_type = 'duesoon'; $currentChore->due_type = 'duesoon';
} }
} }

View File

@ -3,7 +3,7 @@
namespace Grocy\Controllers; namespace Grocy\Controllers;
use Grocy\Controllers\Users\User; use Grocy\Controllers\Users\User;
use Slim\Exception\HttpBadRequestException; use LessQL\Row;
class GenericEntityApiController extends BaseApiController class GenericEntityApiController extends BaseApiController
{ {
@ -11,37 +11,28 @@ class GenericEntityApiController extends BaseApiController
{ {
User::checkPermission($request, User::PERMISSION_MASTER_DATA_EDIT); User::checkPermission($request, User::PERMISSION_MASTER_DATA_EDIT);
if ($this->IsValidExposedEntity($args['entity']) && !$this->IsEntityWithNoEdit($args['entity'])) if ($this->IsValidExposedEntity($args['entity']) && !$this->IsEntityWithNoEdit($args['entity'])) {
{ if ($this->IsEntityWithEditRequiresAdmin($args['entity'])) {
if ($this->IsEntityWithEditRequiresAdmin($args['entity']))
{
User::checkPermission($request, User::PERMISSION_ADMIN); User::checkPermission($request, User::PERMISSION_ADMIN);
} }
$requestBody = $this->GetParsedAndFilteredRequestBody($request); $requestBody = $this->GetParsedAndFilteredRequestBody($request);
try try {
{ if ($requestBody === null) {
if ($requestBody === null)
{
throw new \Exception('Request body could not be parsed (probably invalid JSON format or missing/wrong Content-Type header)'); throw new \Exception('Request body could not be parsed (probably invalid JSON format or missing/wrong Content-Type header)');
} }
$newRow = $this->getDatabase()->{$args['entity']}()->createRow($requestBody); $newRow = $this->getDatabase()->{$args['entity']}()->createRow($requestBody);
$newRow->save(); $newRow->save();
$success = $newRow->isClean();
return $this->ApiResponse($response, [ return $this->ApiResponse($response, [
'created_object_id' => $this->getDatabase()->lastInsertId() 'created_object_id' => $this->getDatabase()->lastInsertId()
]); ]);
} } catch (\Exception $ex) {
catch (\Exception $ex)
{
return $this->GenericErrorResponse($response, $ex->getMessage()); return $this->GenericErrorResponse($response, $ex->getMessage());
} }
} } else {
else
{
return $this->GenericErrorResponse($response, 'Entity does not exist or is not exposed'); return $this->GenericErrorResponse($response, 'Entity does not exist or is not exposed');
} }
} }
@ -50,26 +41,20 @@ class GenericEntityApiController extends BaseApiController
{ {
User::checkPermission($request, User::PERMISSION_MASTER_DATA_EDIT); User::checkPermission($request, User::PERMISSION_MASTER_DATA_EDIT);
if ($this->IsValidExposedEntity($args['entity']) && !$this->IsEntityWithNoDelete($args['entity'])) if ($this->IsValidExposedEntity($args['entity']) && !$this->IsEntityWithNoDelete($args['entity'])) {
{ if ($this->IsEntityWithEditRequiresAdmin($args['entity'])) {
if ($this->IsEntityWithEditRequiresAdmin($args['entity']))
{
User::checkPermission($request, User::PERMISSION_ADMIN); User::checkPermission($request, User::PERMISSION_ADMIN);
} }
$row = $this->getDatabase()->{$args['entity']}($args['objectId']); $row = $this->getDatabase()->{$args['entity']}($args['objectId']);
if ($row == null) if ($row == null) {
{
return $this->GenericErrorResponse($response, 'Object not found', 400); return $this->GenericErrorResponse($response, 'Object not found', 400);
} }
$row->delete(); $row->delete();
$success = $row->isClean();
return $this->EmptyApiResponse($response); return $this->EmptyApiResponse($response);
} } else {
else
{
return $this->GenericErrorResponse($response, 'Invalid entity'); return $this->GenericErrorResponse($response, 'Invalid entity');
} }
} }
@ -78,102 +63,78 @@ class GenericEntityApiController extends BaseApiController
{ {
User::checkPermission($request, User::PERMISSION_MASTER_DATA_EDIT); User::checkPermission($request, User::PERMISSION_MASTER_DATA_EDIT);
if ($this->IsValidExposedEntity($args['entity']) && !$this->IsEntityWithNoEdit($args['entity'])) if ($this->IsValidExposedEntity($args['entity']) && !$this->IsEntityWithNoEdit($args['entity'])) {
{ if ($this->IsEntityWithEditRequiresAdmin($args['entity'])) {
if ($this->IsEntityWithEditRequiresAdmin($args['entity']))
{
User::checkPermission($request, User::PERMISSION_ADMIN); User::checkPermission($request, User::PERMISSION_ADMIN);
} }
$requestBody = $this->GetParsedAndFilteredRequestBody($request); $requestBody = $this->GetParsedAndFilteredRequestBody($request);
try try {
{ if ($requestBody === null) {
if ($requestBody === null)
{
throw new \Exception('Request body could not be parsed (probably invalid JSON format or missing/wrong Content-Type header)'); throw new \Exception('Request body could not be parsed (probably invalid JSON format or missing/wrong Content-Type header)');
} }
$row = $this->getDatabase()->{$args['entity']}($args['objectId']); $row = $this->getDatabase()->{$args['entity']}($args['objectId']);
if ($row == null) if ($row == null) {
{
return $this->GenericErrorResponse($response, 'Object not found', 400); return $this->GenericErrorResponse($response, 'Object not found', 400);
} }
$row->update($requestBody); $row->update($requestBody);
$success = $row->isClean();
return $this->EmptyApiResponse($response); return $this->EmptyApiResponse($response);
} } catch (\Exception $ex) {
catch (\Exception $ex)
{
return $this->GenericErrorResponse($response, $ex->getMessage()); return $this->GenericErrorResponse($response, $ex->getMessage());
} }
} } else {
else
{
return $this->GenericErrorResponse($response, 'Entity does not exist or is not exposed'); return $this->GenericErrorResponse($response, 'Entity does not exist or is not exposed');
} }
} }
public function GetObject(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) public function GetObject(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{ {
if ($this->IsValidExposedEntity($args['entity']) && !$this->IsEntityWithNoListing($args['entity'])) if ($this->IsValidExposedEntity($args['entity']) && !$this->IsEntityWithNoListing($args['entity'])) {
{
$userfields = $this->getUserfieldsService()->GetValues($args['entity'], $args['objectId']);
if (count($userfields) === 0)
{
$userfields = null;
}
$object = $this->getDatabase()->{$args['entity']}($args['objectId']); $object = $this->getDatabase()->{$args['entity']}($args['objectId']);
if ($object == null) if ($object == null) {
{
return $this->GenericErrorResponse($response, 'Object not found', 404); return $this->GenericErrorResponse($response, 'Object not found', 404);
} }
$object['userfields'] = $userfields; $this->addUserfieldsAndJoinsToRow($object, $args);
return $this->ApiResponse($response, $object); return $this->ApiResponse($response, $object);
} } else {
else
{
return $this->GenericErrorResponse($response, 'Entity does not exist or is not exposed'); return $this->GenericErrorResponse($response, 'Entity does not exist or is not exposed');
} }
} }
public function GetObjects(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) public function GetObjects(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{ {
if (!$this->IsValidExposedEntity($args['entity']) || $this->IsEntityWithNoListing($args['entity'])) if (!$this->IsValidExposedEntity($args['entity']) || $this->IsEntityWithNoListing($args['entity'])) {
{
return $this->GenericErrorResponse($response, 'Entity does not exist or is not exposed'); return $this->GenericErrorResponse($response, 'Entity does not exist or is not exposed');
} }
$objects = $this->queryData($this->getDatabase()->{$args['entity']}(), $request->getQueryParams()); $query = $request->getQueryParams();
$userfields = $this->getUserfieldsService()->GetFields($args['entity']);
if (count($userfields) > 0) // get result and total row count
{ $objects = $this->getDatabase()->{$args['entity']}();
$allUserfieldValues = $this->getUserfieldsService()->GetAllValues($args['entity']); $response = $response->withHeader('x-rowcount-total', $objects->count());
foreach ($objects as $object) // apply filter, get filtered row count
{ $objects = $this->applyQuery($objects, $query);
$userfieldKeyValuePairs = null; $response = $response->withHeader('x-rowcount-filtered', $objects->count());
foreach ($userfields as $userfield)
{ // apply limit/order
$value = FindObjectInArrayByPropertyValue(FindAllObjectsInArrayByPropertyValue($allUserfieldValues, 'object_id', $object->id), 'name', $userfield->name); $objects = $this->applyLimit($objects, $query);
if ($value) $objects = $this->applyOrder($objects, $query);
{
$userfieldKeyValuePairs[$userfield->name] = $value->value; // add entity-specific queries
} if ($args['entity'] === 'products' && isset($query['only_in_stock'])) {
else $objects = $objects->where('id IN (SELECT product_id from stock_current WHERE amount_aggregated > 0)');
{
$userfieldKeyValuePairs[$userfield->name] = null;
}
} }
$object->userfields = $userfieldKeyValuePairs; // add userfields and joins to objects
} foreach ($objects as $object) {
$this->addUserfieldsAndJoinsToRow($object, $args);
} }
return $this->ApiResponse($response, $objects); return $this->ApiResponse($response, $objects);
@ -181,12 +142,9 @@ class GenericEntityApiController extends BaseApiController
public function GetUserfields(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) public function GetUserfields(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{ {
try try {
{
return $this->ApiResponse($response, $this->getUserfieldsService()->GetValues($args['entity'], $args['objectId'])); return $this->ApiResponse($response, $this->getUserfieldsService()->GetValues($args['entity'], $args['objectId']));
} } catch (\Exception $ex) {
catch (\Exception $ex)
{
return $this->GenericErrorResponse($response, $ex->getMessage()); return $this->GenericErrorResponse($response, $ex->getMessage());
} }
} }
@ -197,22 +155,40 @@ class GenericEntityApiController extends BaseApiController
$requestBody = $this->GetParsedAndFilteredRequestBody($request); $requestBody = $this->GetParsedAndFilteredRequestBody($request);
try try {
{ if ($requestBody === null) {
if ($requestBody === null)
{
throw new \Exception('Request body could not be parsed (probably invalid JSON format or missing/wrong Content-Type header)'); throw new \Exception('Request body could not be parsed (probably invalid JSON format or missing/wrong Content-Type header)');
} }
$this->getUserfieldsService()->SetValues($args['entity'], $args['objectId'], $requestBody); $this->getUserfieldsService()->SetValues($args['entity'], $args['objectId'], $requestBody);
return $this->EmptyApiResponse($response); return $this->EmptyApiResponse($response);
} } catch (\Exception $ex) {
catch (\Exception $ex)
{
return $this->GenericErrorResponse($response, $ex->getMessage()); return $this->GenericErrorResponse($response, $ex->getMessage());
} }
} }
private function addUserfieldsAndJoinsToRow(Row $object, array $args)
{
// add userfields
$userfields = $this->getUserfieldsService()->GetValues($args['entity'], $object->id);
if (count($userfields) === 0) {
$userfields = null;
}
$object->userfields = $userfields;
// add entity-specific joins
if ($args['entity'] === 'products') {
$object->product_group = $object->product_group_id !== null ? $this->getDatabase()->product_groups($object->product_group_id) : null;
$object->location = $object->location_id !== null ? $this->getDatabase()->locations($object->location_id) : null;
$object->shopping_location = $object->shopping_location_id !== null ? $this->getDatabase()->shopping_locations($object->shopping_location_id) : null;
$object->qu_purchase = $object->qu_id_purchase !== null ? $this->getDatabase()->quantity_units($object->qu_id_purchase) : null;
$object->qu_stock = $object->qu_id_stock !== null ? $this->getDatabase()->quantity_units($object->qu_id_stock) : null;
$object->parent_product = $object->parent_product_id !== null ? $this->getDatabase()->products($object->parent_product_id) : null;
}
return $object;
}
private function IsEntityWithEditRequiresAdmin($entity) private function IsEntityWithEditRequiresAdmin($entity)
{ {
return in_array($entity, $this->getOpenApiSpec()->components->schemas->ExposedEntityEditRequiresAdmin->enum); return in_array($entity, $this->getOpenApiSpec()->components->schemas->ExposedEntityEditRequiresAdmin->enum);

View File

@ -12,14 +12,12 @@ class RecipesController extends BaseController
public function MealPlan(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) public function MealPlan(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{ {
$start = date('Y-m-d'); $start = date('Y-m-d');
if (isset($request->getQueryParams()['start']) && IsIsoDate($request->getQueryParams()['start'])) if (isset($request->getQueryParams()['start']) && IsIsoDate($request->getQueryParams()['start'])) {
{
$start = $request->getQueryParams()['start']; $start = $request->getQueryParams()['start'];
} }
$days = 6; $days = 6;
if (isset($request->getQueryParams()['days']) && filter_var($request->getQueryParams()['days'], FILTER_VALIDATE_INT) !== false) if (isset($request->getQueryParams()['days']) && filter_var($request->getQueryParams()['days'], FILTER_VALIDATE_INT) !== false) {
{
$days = $request->getQueryParams()['days']; $days = $request->getQueryParams()['days'];
} }
@ -27,19 +25,16 @@ class RecipesController extends BaseController
$recipes = $this->getDatabase()->recipes()->where('type', RecipesService::RECIPE_TYPE_NORMAL)->fetchAll(); $recipes = $this->getDatabase()->recipes()->where('type', RecipesService::RECIPE_TYPE_NORMAL)->fetchAll();
$events = []; $events = [];
foreach ($this->getDatabase()->meal_plan()->where($mealPlanWhereTimespan) as $mealPlanEntry) foreach ($this->getDatabase()->meal_plan()->where($mealPlanWhereTimespan) as $mealPlanEntry) {
{
$recipe = FindObjectInArrayByPropertyValue($recipes, 'id', $mealPlanEntry['recipe_id']); $recipe = FindObjectInArrayByPropertyValue($recipes, 'id', $mealPlanEntry['recipe_id']);
$title = ''; $title = '';
if ($recipe !== null) if ($recipe !== null) {
{
$title = $recipe->name; $title = $recipe->name;
} }
$productDetails = null; $productDetails = null;
if ($mealPlanEntry['product_id'] !== null) if ($mealPlanEntry['product_id'] !== null) {
{
$productDetails = $this->getStockService()->GetProductDetails($mealPlanEntry['product_id']); $productDetails = $this->getStockService()->GetProductDetails($mealPlanEntry['product_id']);
} }
@ -60,7 +55,6 @@ class RecipesController extends BaseController
'recipes' => $recipes, 'recipes' => $recipes,
'internalRecipes' => $this->getDatabase()->recipes()->where("id IN (SELECT recipe_id FROM meal_plan_internal_recipe_relation WHERE $mealPlanWhereTimespan)")->fetchAll(), 'internalRecipes' => $this->getDatabase()->recipes()->where("id IN (SELECT recipe_id FROM meal_plan_internal_recipe_relation WHERE $mealPlanWhereTimespan)")->fetchAll(),
'recipesResolved' => $this->getRecipesService()->GetRecipesResolved("recipe_id IN (SELECT recipe_id FROM meal_plan_internal_recipe_relation WHERE $mealPlanWhereTimespan)"), 'recipesResolved' => $this->getRecipesService()->GetRecipesResolved("recipe_id IN (SELECT recipe_id FROM meal_plan_internal_recipe_relation WHERE $mealPlanWhereTimespan)"),
'products' => $this->getDatabase()->products()->orderBy('name', 'COLLATE NOCASE'),
'quantityUnits' => $this->getDatabase()->quantity_units()->orderBy('name', 'COLLATE NOCASE'), 'quantityUnits' => $this->getDatabase()->quantity_units()->orderBy('name', 'COLLATE NOCASE'),
'quantityUnitConversionsResolved' => $this->getDatabase()->quantity_unit_conversions_resolved(), 'quantityUnitConversionsResolved' => $this->getDatabase()->quantity_unit_conversions_resolved(),
'mealplanSections' => $this->getDatabase()->meal_plan_sections()->orderBy('sort_number'), 'mealplanSections' => $this->getDatabase()->meal_plan_sections()->orderBy('sort_number'),
@ -74,14 +68,10 @@ class RecipesController extends BaseController
$recipesResolved = $this->getRecipesService()->GetRecipesResolved('recipe_id > 0'); $recipesResolved = $this->getRecipesService()->GetRecipesResolved('recipe_id > 0');
$selectedRecipe = null; $selectedRecipe = null;
if (isset($request->getQueryParams()['recipe'])) if (isset($request->getQueryParams()['recipe'])) {
{
$selectedRecipe = $this->getDatabase()->recipes($request->getQueryParams()['recipe']); $selectedRecipe = $this->getDatabase()->recipes($request->getQueryParams()['recipe']);
} } else {
else foreach ($recipes as $recipe) {
{
foreach ($recipes as $recipe)
{
$selectedRecipe = $recipe; $selectedRecipe = $recipe;
break; break;
} }
@ -89,8 +79,7 @@ class RecipesController extends BaseController
$totalCosts = null; $totalCosts = null;
$totalCalories = null; $totalCalories = null;
if ($selectedRecipe) if ($selectedRecipe) {
{
$totalCosts = FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $selectedRecipe->id)->costs; $totalCosts = FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $selectedRecipe->id)->costs;
$totalCalories = FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $selectedRecipe->id)->calories; $totalCalories = FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $selectedRecipe->id)->calories;
} }
@ -109,27 +98,22 @@ class RecipesController extends BaseController
'selectedRecipeTotalCalories' => $totalCalories 'selectedRecipeTotalCalories' => $totalCalories
]; ];
if ($selectedRecipe) if ($selectedRecipe) {
{
$selectedRecipeSubRecipes = $this->getDatabase()->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', 'COLLATE NOCASE')->fetchAll(); $selectedRecipeSubRecipes = $this->getDatabase()->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', 'COLLATE NOCASE')->fetchAll();
$includedRecipeIdsAbsolute = []; $includedRecipeIdsAbsolute = [];
$includedRecipeIdsAbsolute[] = $selectedRecipe->id; $includedRecipeIdsAbsolute[] = $selectedRecipe->id;
foreach ($selectedRecipeSubRecipes as $subRecipe) foreach ($selectedRecipeSubRecipes as $subRecipe) {
{
$includedRecipeIdsAbsolute[] = $subRecipe->id; $includedRecipeIdsAbsolute[] = $subRecipe->id;
} }
// TODO: Why not directly use recipes_pos_resolved for all recipe positions here (parent and child)? // TODO: Why not directly use recipes_pos_resolved for all recipe positions here (parent and child)?
// This view already correctly recolves child recipe amounts... // This view already correctly recolves child recipe amounts...
$allRecipePositions = []; $allRecipePositions = [];
foreach ($includedRecipeIdsAbsolute as $id) foreach ($includedRecipeIdsAbsolute as $id) {
{
$allRecipePositions[$id] = $this->getDatabase()->recipes_pos_resolved()->where('recipe_id = :1 AND is_nested_recipe_pos = 0', $id)->orderBy('ingredient_group', 'ASC', 'product_group', 'ASC'); $allRecipePositions[$id] = $this->getDatabase()->recipes_pos_resolved()->where('recipe_id = :1 AND is_nested_recipe_pos = 0', $id)->orderBy('ingredient_group', 'ASC', 'product_group', 'ASC');
foreach ($allRecipePositions[$id] as $pos) foreach ($allRecipePositions[$id] as $pos) {
{ if ($id != $selectedRecipe->id) {
if ($id != $selectedRecipe->id)
{
$pos2 = $this->getDatabase()->recipes_pos_resolved()->where('recipe_id = :1 AND recipe_pos_id = :2 AND is_nested_recipe_pos = 1', $selectedRecipe->id, $pos->recipe_pos_id)->fetch(); $pos2 = $this->getDatabase()->recipes_pos_resolved()->where('recipe_id = :1 AND recipe_pos_id = :2 AND is_nested_recipe_pos = 1', $selectedRecipe->id, $pos->recipe_pos_id)->fetch();
$pos->recipe_amount = $pos2->recipe_amount; $pos->recipe_amount = $pos2->recipe_amount;
$pos->missing_amount = $pos2->missing_amount; $pos->missing_amount = $pos2->missing_amount;
@ -153,6 +137,7 @@ class RecipesController extends BaseController
'recipe' => $this->getDatabase()->recipes($recipeId), 'recipe' => $this->getDatabase()->recipes($recipeId),
'recipePositions' => $this->getDatabase()->recipes_pos()->where('recipe_id', $recipeId), 'recipePositions' => $this->getDatabase()->recipes_pos()->where('recipe_id', $recipeId),
'mode' => $recipeId == 'new' ? 'create' : 'edit', 'mode' => $recipeId == 'new' ? 'create' : 'edit',
// TODO: remove 'products' after converting DataTable
'products' => $this->getDatabase()->products()->orderBy('name', 'COLLATE NOCASE'), 'products' => $this->getDatabase()->products()->orderBy('name', 'COLLATE NOCASE'),
'quantityunits' => $this->getDatabase()->quantity_units(), 'quantityunits' => $this->getDatabase()->quantity_units(),
'recipes' => $this->getDatabase()->recipes()->where('type', RecipesService::RECIPE_TYPE_NORMAL)->orderBy('name', 'COLLATE NOCASE'), 'recipes' => $this->getDatabase()->recipes()->where('type', RecipesService::RECIPE_TYPE_NORMAL)->orderBy('name', 'COLLATE NOCASE'),
@ -164,24 +149,21 @@ class RecipesController extends BaseController
public function RecipePosEditForm(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) public function RecipePosEditForm(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{ {
if ($args['recipePosId'] == 'new') if ($args['recipePosId'] == 'new') {
{
return $this->renderPage($response, 'recipeposform', [ return $this->renderPage($response, 'recipeposform', [
'mode' => 'create', 'mode' => 'create',
'recipe' => $this->getDatabase()->recipes($args['recipeId']), 'recipe' => $this->getDatabase()->recipes($args['recipeId']),
'recipePos' => new \stdClass(), 'recipePos' => new \stdClass(),
'products' => $this->getDatabase()->products()->where('active = 1')->orderBy('name', 'COLLATE NOCASE'), 'recipePosId' => $args['recipePosId'],
'quantityUnits' => $this->getDatabase()->quantity_units()->orderBy('name', 'COLLATE NOCASE'), 'quantityUnits' => $this->getDatabase()->quantity_units()->orderBy('name', 'COLLATE NOCASE'),
'quantityUnitConversionsResolved' => $this->getDatabase()->quantity_unit_conversions_resolved() 'quantityUnitConversionsResolved' => $this->getDatabase()->quantity_unit_conversions_resolved()
]); ]);
} } else {
else
{
return $this->renderPage($response, 'recipeposform', [ return $this->renderPage($response, 'recipeposform', [
'mode' => 'edit', 'mode' => 'edit',
'recipe' => $this->getDatabase()->recipes($args['recipeId']), 'recipe' => $this->getDatabase()->recipes($args['recipeId']),
'recipePos' => $this->getDatabase()->recipes_pos($args['recipePosId']), 'recipePos' => $this->getDatabase()->recipes_pos($args['recipePosId']),
'products' => $this->getDatabase()->products()->orderBy('name', 'COLLATE NOCASE'), 'recipePosId' => $args['recipePosId'],
'quantityUnits' => $this->getDatabase()->quantity_units()->orderBy('name', 'COLLATE NOCASE'), 'quantityUnits' => $this->getDatabase()->quantity_units()->orderBy('name', 'COLLATE NOCASE'),
'quantityUnitConversionsResolved' => $this->getDatabase()->quantity_unit_conversions_resolved() 'quantityUnitConversionsResolved' => $this->getDatabase()->quantity_unit_conversions_resolved()
]); ]);
@ -195,14 +177,11 @@ class RecipesController extends BaseController
public function MealPlanSectionEditForm(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) public function MealPlanSectionEditForm(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{ {
if ($args['sectionId'] == 'new') if ($args['sectionId'] == 'new') {
{
return $this->renderPage($response, 'mealplansectionform', [ return $this->renderPage($response, 'mealplansectionform', [
'mode' => 'create' 'mode' => 'create'
]); ]);
} } else {
else
{
return $this->renderPage($response, 'mealplansectionform', [ return $this->renderPage($response, 'mealplansectionform', [
'mealplanSection' => $this->getDatabase()->meal_plan_sections($args['sectionId']), 'mealplanSection' => $this->getDatabase()->meal_plan_sections($args['sectionId']),
'mode' => 'edit' 'mode' => 'edit'

View File

@ -12,8 +12,6 @@ class StockController extends BaseController
public function Consume(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) public function Consume(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{ {
return $this->renderPage($response, 'consume', [ return $this->renderPage($response, 'consume', [
'products' => $this->getDatabase()->products()->where('active = 1')->where('id IN (SELECT product_id from stock_current WHERE amount_aggregated > 0)')->orderBy('name'),
'barcodes' => $this->getDatabase()->product_barcodes_comma_separated(),
'recipes' => $this->getDatabase()->recipes()->where('type', RecipesService::RECIPE_TYPE_NORMAL)->orderBy('name', 'COLLATE NOCASE'), 'recipes' => $this->getDatabase()->recipes()->where('type', RecipesService::RECIPE_TYPE_NORMAL)->orderBy('name', 'COLLATE NOCASE'),
'locations' => $this->getDatabase()->locations()->orderBy('name', 'COLLATE NOCASE'), 'locations' => $this->getDatabase()->locations()->orderBy('name', 'COLLATE NOCASE'),
'quantityUnits' => $this->getDatabase()->quantity_units()->orderBy('name', 'COLLATE NOCASE'), 'quantityUnits' => $this->getDatabase()->quantity_units()->orderBy('name', 'COLLATE NOCASE'),
@ -24,8 +22,6 @@ class StockController extends BaseController
public function Inventory(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) public function Inventory(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{ {
return $this->renderPage($response, 'inventory', [ return $this->renderPage($response, 'inventory', [
'products' => $this->getDatabase()->products()->where('active = 1')->orderBy('name', 'COLLATE NOCASE'),
'barcodes' => $this->getDatabase()->product_barcodes_comma_separated(),
'shoppinglocations' => $this->getDatabase()->shopping_locations()->orderBy('name', 'COLLATE NOCASE'), 'shoppinglocations' => $this->getDatabase()->shopping_locations()->orderBy('name', 'COLLATE NOCASE'),
'locations' => $this->getDatabase()->locations()->orderBy('name', 'COLLATE NOCASE'), 'locations' => $this->getDatabase()->locations()->orderBy('name', 'COLLATE NOCASE'),
'quantityUnits' => $this->getDatabase()->quantity_units()->orderBy('name', 'COLLATE NOCASE'), 'quantityUnits' => $this->getDatabase()->quantity_units()->orderBy('name', 'COLLATE NOCASE'),
@ -35,19 +31,15 @@ class StockController extends BaseController
public function Journal(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) public function Journal(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{ {
if (isset($request->getQueryParams()['months']) && filter_var($request->getQueryParams()['months'], FILTER_VALIDATE_INT) !== false) if (isset($request->getQueryParams()['months']) && filter_var($request->getQueryParams()['months'], FILTER_VALIDATE_INT) !== false) {
{
$months = $request->getQueryParams()['months']; $months = $request->getQueryParams()['months'];
$where = "row_created_timestamp > DATE(DATE('now', 'localtime'), '-$months months')"; $where = "row_created_timestamp > DATE(DATE('now', 'localtime'), '-$months months')";
} } else {
else
{
// Default 6 months // Default 6 months
$where = "row_created_timestamp > DATE(DATE('now', 'localtime'), '-6 months')"; $where = "row_created_timestamp > DATE(DATE('now', 'localtime'), '-6 months')";
} }
if (isset($request->getQueryParams()['product']) && filter_var($request->getQueryParams()['product'], FILTER_VALIDATE_INT) !== false) if (isset($request->getQueryParams()['product']) && filter_var($request->getQueryParams()['product'], FILTER_VALIDATE_INT) !== false) {
{
$productId = $request->getQueryParams()['product']; $productId = $request->getQueryParams()['product'];
$where .= " AND product_id = $productId"; $where .= " AND product_id = $productId";
} }
@ -56,7 +48,6 @@ class StockController extends BaseController
return $this->renderPage($response, 'stockjournal', [ return $this->renderPage($response, 'stockjournal', [
'stockLog' => $this->getDatabase()->uihelper_stock_journal()->where($where)->orderBy('row_created_timestamp', 'DESC'), 'stockLog' => $this->getDatabase()->uihelper_stock_journal()->where($where)->orderBy('row_created_timestamp', 'DESC'),
'products' => $this->getDatabase()->products()->where('active = 1')->orderBy('name', 'COLLATE NOCASE'),
'locations' => $this->getDatabase()->locations()->orderBy('name', 'COLLATE NOCASE'), 'locations' => $this->getDatabase()->locations()->orderBy('name', 'COLLATE NOCASE'),
'users' => $usersService->GetUsersAsDto(), 'users' => $usersService->GetUsersAsDto(),
'transactionTypes' => GetClassConstants('\Grocy\Services\StockService', 'TRANSACTION_TYPE_') 'transactionTypes' => GetClassConstants('\Grocy\Services\StockService', 'TRANSACTION_TYPE_')
@ -75,15 +66,12 @@ class StockController extends BaseController
public function LocationEditForm(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) public function LocationEditForm(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{ {
if ($args['locationId'] == 'new') if ($args['locationId'] == 'new') {
{
return $this->renderPage($response, 'locationform', [ return $this->renderPage($response, 'locationform', [
'mode' => 'create', 'mode' => 'create',
'userfields' => $this->getUserfieldsService()->GetFields('locations') 'userfields' => $this->getUserfieldsService()->GetFields('locations')
]); ]);
} } else {
else
{
return $this->renderPage($response, 'locationform', [ return $this->renderPage($response, 'locationform', [
'location' => $this->getDatabase()->locations($args['locationId']), 'location' => $this->getDatabase()->locations($args['locationId']),
'mode' => 'edit', 'mode' => 'edit',
@ -121,13 +109,11 @@ class StockController extends BaseController
{ {
$product = null; $product = null;
if (isset($request->getQueryParams()['product'])) if (isset($request->getQueryParams()['product'])) {
{
$product = $this->getDatabase()->products($request->getQueryParams()['product']); $product = $this->getDatabase()->products($request->getQueryParams()['product']);
} }
if ($args['productBarcodeId'] == 'new') if ($args['productBarcodeId'] == 'new') {
{
return $this->renderPage($response, 'productbarcodeform', [ return $this->renderPage($response, 'productbarcodeform', [
'mode' => 'create', 'mode' => 'create',
'barcodes' => $this->getDatabase()->product_barcodes()->orderBy('barcode'), 'barcodes' => $this->getDatabase()->product_barcodes()->orderBy('barcode'),
@ -137,9 +123,7 @@ class StockController extends BaseController
'quantityUnitConversionsResolved' => $this->getDatabase()->quantity_unit_conversions_resolved(), 'quantityUnitConversionsResolved' => $this->getDatabase()->quantity_unit_conversions_resolved(),
'userfields' => $this->getUserfieldsService()->GetFields('product_barcodes') 'userfields' => $this->getUserfieldsService()->GetFields('product_barcodes')
]); ]);
} } else {
else
{
return $this->renderPage($response, 'productbarcodeform', [ return $this->renderPage($response, 'productbarcodeform', [
'mode' => 'edit', 'mode' => 'edit',
'barcode' => $this->getDatabase()->product_barcodes($args['productBarcodeId']), 'barcode' => $this->getDatabase()->product_barcodes($args['productBarcodeId']),
@ -154,8 +138,7 @@ class StockController extends BaseController
public function ProductEditForm(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) public function ProductEditForm(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{ {
if ($args['productId'] == 'new') if ($args['productId'] == 'new') {
{
return $this->renderPage($response, 'productform', [ return $this->renderPage($response, 'productform', [
'locations' => $this->getDatabase()->locations()->orderBy('name'), 'locations' => $this->getDatabase()->locations()->orderBy('name'),
'barcodes' => $this->getDatabase()->product_barcodes()->orderBy('barcode'), 'barcodes' => $this->getDatabase()->product_barcodes()->orderBy('barcode'),
@ -163,13 +146,10 @@ class StockController extends BaseController
'shoppinglocations' => $this->getDatabase()->shopping_locations()->orderBy('name', 'COLLATE NOCASE'), 'shoppinglocations' => $this->getDatabase()->shopping_locations()->orderBy('name', 'COLLATE NOCASE'),
'productgroups' => $this->getDatabase()->product_groups()->orderBy('name', 'COLLATE NOCASE'), 'productgroups' => $this->getDatabase()->product_groups()->orderBy('name', 'COLLATE NOCASE'),
'userfields' => $this->getUserfieldsService()->GetFields('products'), 'userfields' => $this->getUserfieldsService()->GetFields('products'),
'products' => $this->getDatabase()->products()->where('parent_product_id IS NULL and active = 1')->orderBy('name', 'COLLATE NOCASE'),
'isSubProductOfOthers' => false, 'isSubProductOfOthers' => false,
'mode' => 'create' 'mode' => 'create'
]); ]);
} } else {
else
{
$product = $this->getDatabase()->products($args['productId']); $product = $this->getDatabase()->products($args['productId']);
return $this->renderPage($response, 'productform', [ return $this->renderPage($response, 'productform', [
@ -180,7 +160,6 @@ class StockController extends BaseController
'shoppinglocations' => $this->getDatabase()->shopping_locations()->orderBy('name', 'COLLATE NOCASE'), 'shoppinglocations' => $this->getDatabase()->shopping_locations()->orderBy('name', 'COLLATE NOCASE'),
'productgroups' => $this->getDatabase()->product_groups()->orderBy('name', 'COLLATE NOCASE'), 'productgroups' => $this->getDatabase()->product_groups()->orderBy('name', 'COLLATE NOCASE'),
'userfields' => $this->getUserfieldsService()->GetFields('products'), 'userfields' => $this->getUserfieldsService()->GetFields('products'),
'products' => $this->getDatabase()->products()->where('id != :1 AND parent_product_id IS NULL and active = 1', $product->id)->orderBy('name', 'COLLATE NOCASE'),
'isSubProductOfOthers' => $this->getDatabase()->products()->where('parent_product_id = :1', $product->id)->count() !== 0, 'isSubProductOfOthers' => $this->getDatabase()->products()->where('parent_product_id = :1', $product->id)->count() !== 0,
'mode' => 'edit', 'mode' => 'edit',
'quConversions' => $this->getDatabase()->quantity_unit_conversions(), 'quConversions' => $this->getDatabase()->quantity_unit_conversions(),
@ -198,15 +177,12 @@ class StockController extends BaseController
public function ProductGroupEditForm(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) public function ProductGroupEditForm(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{ {
if ($args['productGroupId'] == 'new') if ($args['productGroupId'] == 'new') {
{
return $this->renderPage($response, 'productgroupform', [ return $this->renderPage($response, 'productgroupform', [
'mode' => 'create', 'mode' => 'create',
'userfields' => $this->getUserfieldsService()->GetFields('product_groups') 'userfields' => $this->getUserfieldsService()->GetFields('product_groups')
]); ]);
} } else {
else
{
return $this->renderPage($response, 'productgroupform', [ return $this->renderPage($response, 'productgroupform', [
'group' => $this->getDatabase()->product_groups($args['productGroupId']), 'group' => $this->getDatabase()->product_groups($args['productGroupId']),
'mode' => 'edit', 'mode' => 'edit',
@ -227,21 +203,21 @@ class StockController extends BaseController
public function ProductsList(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) public function ProductsList(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{ {
$products = $this->getDatabase()->products(); // $products = $this->getDatabase()->products();
if (!isset($request->getQueryParams()['include_disabled'])) // if (!isset($request->getQueryParams()['include_disabled']))
{ // {
$products = $products->where('active = 1'); // $products = $products->where('active = 1');
} // }
if (isset($request->getQueryParams()['only_in_stock'])) // if (isset($request->getQueryParams()['only_in_stock']))
{ // {
$products = $products->where('id IN (SELECT product_id from stock_current WHERE amount_aggregated > 0)'); // $products = $products->where('id IN (SELECT product_id from stock_current WHERE amount_aggregated > 0)');
} // }
$products = $products->orderBy('name', 'COLLATE NOCASE'); // $products = $products->orderBy('name', 'COLLATE NOCASE');
return $this->renderPage($response, 'products', [ return $this->renderPage($response, 'products', [
'products' => $products, // 'products' => $products,
'locations' => $this->getDatabase()->locations()->orderBy('name', 'COLLATE NOCASE'), 'locations' => $this->getDatabase()->locations()->orderBy('name', 'COLLATE NOCASE'),
'quantityunits' => $this->getDatabase()->quantity_units()->orderBy('name', 'COLLATE NOCASE'), 'quantityunits' => $this->getDatabase()->quantity_units()->orderBy('name', 'COLLATE NOCASE'),
'productGroups' => $this->getDatabase()->product_groups()->orderBy('name', 'COLLATE NOCASE'), 'productGroups' => $this->getDatabase()->product_groups()->orderBy('name', 'COLLATE NOCASE'),
@ -254,8 +230,6 @@ class StockController extends BaseController
public function Purchase(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) public function Purchase(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{ {
return $this->renderPage($response, 'purchase', [ return $this->renderPage($response, 'purchase', [
'products' => $this->getDatabase()->products()->where('active = 1')->orderBy('name', 'COLLATE NOCASE'),
'barcodes' => $this->getDatabase()->product_barcodes_comma_separated(),
'shoppinglocations' => $this->getDatabase()->shopping_locations()->orderBy('name', 'COLLATE NOCASE'), 'shoppinglocations' => $this->getDatabase()->shopping_locations()->orderBy('name', 'COLLATE NOCASE'),
'locations' => $this->getDatabase()->locations()->orderBy('name', 'COLLATE NOCASE'), 'locations' => $this->getDatabase()->locations()->orderBy('name', 'COLLATE NOCASE'),
'quantityUnits' => $this->getDatabase()->quantity_units()->orderBy('name', 'COLLATE NOCASE'), 'quantityUnits' => $this->getDatabase()->quantity_units()->orderBy('name', 'COLLATE NOCASE'),
@ -267,20 +241,17 @@ class StockController extends BaseController
{ {
$product = null; $product = null;
if (isset($request->getQueryParams()['product'])) if (isset($request->getQueryParams()['product'])) {
{
$product = $this->getDatabase()->products($request->getQueryParams()['product']); $product = $this->getDatabase()->products($request->getQueryParams()['product']);
} }
$defaultQuUnit = null; $defaultQuUnit = null;
if (isset($request->getQueryParams()['qu-unit'])) if (isset($request->getQueryParams()['qu-unit'])) {
{
$defaultQuUnit = $this->getDatabase()->quantity_units($request->getQueryParams()['qu-unit']); $defaultQuUnit = $this->getDatabase()->quantity_units($request->getQueryParams()['qu-unit']);
} }
if ($args['quConversionId'] == 'new') if ($args['quConversionId'] == 'new') {
{
return $this->renderPage($response, 'quantityunitconversionform', [ return $this->renderPage($response, 'quantityunitconversionform', [
'mode' => 'create', 'mode' => 'create',
'userfields' => $this->getUserfieldsService()->GetFields('quantity_unit_conversions'), 'userfields' => $this->getUserfieldsService()->GetFields('quantity_unit_conversions'),
@ -288,9 +259,7 @@ class StockController extends BaseController
'product' => $product, 'product' => $product,
'defaultQuUnit' => $defaultQuUnit 'defaultQuUnit' => $defaultQuUnit
]); ]);
} } else {
else
{
return $this->renderPage($response, 'quantityunitconversionform', [ return $this->renderPage($response, 'quantityunitconversionform', [
'quConversion' => $this->getDatabase()->quantity_unit_conversions($args['quConversionId']), 'quConversion' => $this->getDatabase()->quantity_unit_conversions($args['quConversionId']),
'mode' => 'edit', 'mode' => 'edit',
@ -304,17 +273,14 @@ class StockController extends BaseController
public function QuantityUnitEditForm(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) public function QuantityUnitEditForm(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{ {
if ($args['quantityunitId'] == 'new') if ($args['quantityunitId'] == 'new') {
{
return $this->renderPage($response, 'quantityunitform', [ return $this->renderPage($response, 'quantityunitform', [
'mode' => 'create', 'mode' => 'create',
'userfields' => $this->getUserfieldsService()->GetFields('quantity_units'), 'userfields' => $this->getUserfieldsService()->GetFields('quantity_units'),
'pluralCount' => $this->getLocalizationService()->GetPluralCount(), 'pluralCount' => $this->getLocalizationService()->GetPluralCount(),
'pluralRule' => $this->getLocalizationService()->GetPluralDefinition() 'pluralRule' => $this->getLocalizationService()->GetPluralDefinition()
]); ]);
} } else {
else
{
$quantityUnit = $this->getDatabase()->quantity_units($args['quantityunitId']); $quantityUnit = $this->getDatabase()->quantity_units($args['quantityunitId']);
return $this->renderPage($response, 'quantityunitform', [ return $this->renderPage($response, 'quantityunitform', [
@ -349,8 +315,7 @@ class StockController extends BaseController
{ {
$listId = 1; $listId = 1;
if (isset($request->getQueryParams()['list'])) if (isset($request->getQueryParams()['list'])) {
{
$listId = $request->getQueryParams()['list']; $listId = $request->getQueryParams()['list'];
} }
@ -371,15 +336,12 @@ class StockController extends BaseController
public function ShoppingListEditForm(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) public function ShoppingListEditForm(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{ {
if ($args['listId'] == 'new') if ($args['listId'] == 'new') {
{
return $this->renderPage($response, 'shoppinglistform', [ return $this->renderPage($response, 'shoppinglistform', [
'mode' => 'create', 'mode' => 'create',
'userfields' => $this->getUserfieldsService()->GetFields('shopping_lists') 'userfields' => $this->getUserfieldsService()->GetFields('shopping_lists')
]); ]);
} } else {
else
{
return $this->renderPage($response, 'shoppinglistform', [ return $this->renderPage($response, 'shoppinglistform', [
'shoppingList' => $this->getDatabase()->shopping_lists($args['listId']), 'shoppingList' => $this->getDatabase()->shopping_lists($args['listId']),
'mode' => 'edit', 'mode' => 'edit',
@ -390,8 +352,7 @@ class StockController extends BaseController
public function ShoppingListItemEditForm(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) public function ShoppingListItemEditForm(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{ {
if ($args['itemId'] == 'new') if ($args['itemId'] == 'new') {
{
return $this->renderPage($response, 'shoppinglistitemform', [ return $this->renderPage($response, 'shoppinglistitemform', [
'products' => $this->getDatabase()->products()->where('active = 1')->orderBy('name', 'COLLATE NOCASE'), 'products' => $this->getDatabase()->products()->where('active = 1')->orderBy('name', 'COLLATE NOCASE'),
'shoppingLists' => $this->getDatabase()->shopping_lists()->orderBy('name', 'COLLATE NOCASE'), 'shoppingLists' => $this->getDatabase()->shopping_lists()->orderBy('name', 'COLLATE NOCASE'),
@ -400,12 +361,9 @@ class StockController extends BaseController
'quantityUnitConversionsResolved' => $this->getDatabase()->quantity_unit_conversions_resolved(), 'quantityUnitConversionsResolved' => $this->getDatabase()->quantity_unit_conversions_resolved(),
'userfields' => $this->getUserfieldsService()->GetFields('shopping_list') 'userfields' => $this->getUserfieldsService()->GetFields('shopping_list')
]); ]);
} } else {
else
{
return $this->renderPage($response, 'shoppinglistitemform', [ return $this->renderPage($response, 'shoppinglistitemform', [
'listItem' => $this->getDatabase()->shopping_list($args['itemId']), 'listItem' => $this->getDatabase()->shopping_list($args['itemId']),
'products' => $this->getDatabase()->products()->where('active = 1')->orderBy('name', 'COLLATE NOCASE'),
'shoppingLists' => $this->getDatabase()->shopping_lists()->orderBy('name', 'COLLATE NOCASE'), 'shoppingLists' => $this->getDatabase()->shopping_lists()->orderBy('name', 'COLLATE NOCASE'),
'mode' => 'edit', 'mode' => 'edit',
'quantityUnits' => $this->getDatabase()->quantity_units()->orderBy('name', 'COLLATE NOCASE'), 'quantityUnits' => $this->getDatabase()->quantity_units()->orderBy('name', 'COLLATE NOCASE'),
@ -422,15 +380,12 @@ class StockController extends BaseController
public function ShoppingLocationEditForm(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) public function ShoppingLocationEditForm(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{ {
if ($args['shoppingLocationId'] == 'new') if ($args['shoppingLocationId'] == 'new') {
{
return $this->renderPage($response, 'shoppinglocationform', [ return $this->renderPage($response, 'shoppinglocationform', [
'mode' => 'create', 'mode' => 'create',
'userfields' => $this->getUserfieldsService()->GetFields('shopping_locations') 'userfields' => $this->getUserfieldsService()->GetFields('shopping_locations')
]); ]);
} } else {
else
{
return $this->renderPage($response, 'shoppinglocationform', [ return $this->renderPage($response, 'shoppinglocationform', [
'shoppinglocation' => $this->getDatabase()->shopping_locations($args['shoppingLocationId']), 'shoppinglocation' => $this->getDatabase()->shopping_locations($args['shoppingLocationId']),
'mode' => 'edit', 'mode' => 'edit',
@ -489,6 +444,7 @@ class StockController extends BaseController
$nextXDays = $usersService->GetUserSettings(GROCY_USER_ID)['stock_due_soon_days']; $nextXDays = $usersService->GetUserSettings(GROCY_USER_ID)['stock_due_soon_days'];
return $this->renderPage($response, 'stockentries', [ return $this->renderPage($response, 'stockentries', [
// TODO: remove 'products' after converting DataTable
'products' => $this->getDatabase()->products()->where('active = 1')->orderBy('name', 'COLLATE NOCASE'), 'products' => $this->getDatabase()->products()->where('active = 1')->orderBy('name', 'COLLATE NOCASE'),
'quantityunits' => $this->getDatabase()->quantity_units()->orderBy('name', 'COLLATE NOCASE'), 'quantityunits' => $this->getDatabase()->quantity_units()->orderBy('name', 'COLLATE NOCASE'),
'locations' => $this->getDatabase()->locations()->orderBy('name', 'COLLATE NOCASE'), 'locations' => $this->getDatabase()->locations()->orderBy('name', 'COLLATE NOCASE'),
@ -504,8 +460,6 @@ class StockController extends BaseController
public function Transfer(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) public function Transfer(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{ {
return $this->renderPage($response, 'transfer', [ return $this->renderPage($response, 'transfer', [
'products' => $this->getDatabase()->products()->where('active = 1')->where('id IN (SELECT product_id from stock_current WHERE amount_aggregated > 0)')->orderBy('name', 'COLLATE NOCASE'),
'barcodes' => $this->getDatabase()->product_barcodes_comma_separated(),
'locations' => $this->getDatabase()->locations()->orderBy('name', 'COLLATE NOCASE'), 'locations' => $this->getDatabase()->locations()->orderBy('name', 'COLLATE NOCASE'),
'quantityUnits' => $this->getDatabase()->quantity_units()->orderBy('name', 'COLLATE NOCASE'), 'quantityUnits' => $this->getDatabase()->quantity_units()->orderBy('name', 'COLLATE NOCASE'),
'quantityUnitConversionsResolved' => $this->getDatabase()->quantity_unit_conversions_resolved() 'quantityUnitConversionsResolved' => $this->getDatabase()->quantity_unit_conversions_resolved()
@ -515,23 +469,19 @@ class StockController extends BaseController
public function JournalSummary(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) public function JournalSummary(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{ {
$entries = $this->getDatabase()->uihelper_stock_journal_summary(); $entries = $this->getDatabase()->uihelper_stock_journal_summary();
if (isset($request->getQueryParams()['product_id'])) if (isset($request->getQueryParams()['product_id'])) {
{
$entries = $entries->where('product_id', $request->getQueryParams()['product_id']); $entries = $entries->where('product_id', $request->getQueryParams()['product_id']);
} }
if (isset($request->getQueryParams()['user_id'])) if (isset($request->getQueryParams()['user_id'])) {
{
$entries = $entries->where('user_id', $request->getQueryParams()['user_id']); $entries = $entries->where('user_id', $request->getQueryParams()['user_id']);
} }
if (isset($request->getQueryParams()['transaction_type'])) if (isset($request->getQueryParams()['transaction_type'])) {
{
$entries = $entries->where('transaction_type', $request->getQueryParams()['transaction_type']); $entries = $entries->where('transaction_type', $request->getQueryParams()['transaction_type']);
} }
$usersService = $this->getUsersService(); $usersService = $this->getUsersService();
return $this->renderPage($response, 'stockjournalsummary', [ return $this->renderPage($response, 'stockjournalsummary', [
'entries' => $entries, 'entries' => $entries,
'products' => $this->getDatabase()->products()->where('active = 1')->orderBy('name', 'COLLATE NOCASE'),
'users' => $usersService->GetUsersAsDto(), 'users' => $usersService->GetUsersAsDto(),
'transactionTypes' => GetClassConstants('\Grocy\Services\StockService', 'TRANSACTION_TYPE_') 'transactionTypes' => GetClassConstants('\Grocy\Services\StockService', 'TRANSACTION_TYPE_')
]); ]);

View File

@ -2308,3 +2308,18 @@ msgstr ""
msgid "Average execution frequency" msgid "Average execution frequency"
msgstr "" msgstr ""
msgid "Pakke"
msgstr ""
msgid "Del"
msgstr ""
msgid "Boks"
msgstr ""
msgid "Bunt"
msgstr ""
msgid "Flaske"
msgstr ""

View File

@ -12,15 +12,15 @@
"bootstrap-select": "^1.13.18", "bootstrap-select": "^1.13.18",
"bwip-js": "^3.0.1", "bwip-js": "^3.0.1",
"chart.js": "^2.8.0", "chart.js": "^2.8.0",
"datatables.net": "^1.10.22", "datatables.net": "^1.11.5",
"datatables.net-bs4": "^1.10.22", "datatables.net-bs4": "^1.11.5",
"datatables.net-colreorder": "^1.5.2", "datatables.net-colreorder": "^1.5.5",
"datatables.net-colreorder-bs4": "^1.5.2", "datatables.net-colreorder-bs4": "^1.5.5",
"datatables.net-plugins": "^1.10.20", "datatables.net-plugins": "^1.11.5",
"datatables.net-rowgroup": "^1.1.2", "datatables.net-rowgroup": "^1.1.4",
"datatables.net-rowgroup-bs4": "^1.1.2", "datatables.net-rowgroup-bs4": "^1.1.4",
"datatables.net-select": "^1.3.1", "datatables.net-select": "^1.3.4",
"datatables.net-select-bs4": "^1.3.1", "datatables.net-select-bs4": "^1.3.4",
"fullcalendar": "^3.10.1", "fullcalendar": "^3.10.1",
"gettext-translator": "2.1.0", "gettext-translator": "2.1.0",
"jquery": "3.5.1", "jquery": "3.5.1",
@ -28,6 +28,8 @@
"jquery-serializejson": "^2.9.0", "jquery-serializejson": "^2.9.0",
"moment": "^2.27.0", "moment": "^2.27.0",
"nosleep.js": "^0.12.0", "nosleep.js": "^0.12.0",
"select2": "^4.0.13",
"select2-theme-bootstrap4": "^1.0.1",
"sprintf-js": "^1.1.2", "sprintf-js": "^1.1.2",
"startbootstrap-sb-admin": "4.0.0", "startbootstrap-sb-admin": "4.0.0",
"summernote": "^0.8.18", "summernote": "^0.8.18",

View File

@ -228,7 +228,8 @@ form.has-sticky-form-footer .form-group:nth-last-child(2) {
cursor: wait; cursor: wait;
} }
.expandable-text .collapse, .module .collapsing { .expandable-text .collapse,
.module .collapsing {
height: 2.4rem; height: 2.4rem;
} }
@ -255,7 +256,8 @@ form.has-sticky-form-footer .form-group:nth-last-child(2) {
.table-inline-menu.dropdown-menu { .table-inline-menu.dropdown-menu {
padding-left: 12px; padding-left: 12px;
padding-right: 12px; padding-right: 12px;
width: 96vw; /* Set width of popup menu to screen size */ width: 96vw;
/* Set width of popup menu to screen size */
} }
a:not([href]) { a:not([href]) {
@ -326,12 +328,39 @@ a:not([href]) {
background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right .75rem center/8px 10px; background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right .75rem center/8px 10px;
} }
.select2-hidden-accessible.custom-select.is-invalid+.select2-container--bootstrap:not(:last-child)>.selection>.select2-selection,
.was-validated .select2-hidden-accessible.custom-select:invalid+.select2-container--bootstrap:not(:last-child)>.selection>.select2-selection {
border-color: #dc3545;
}
.select2-hidden-accessible.custom-select.is-invalid+.select2-container--focus.select2-container--bootstrap:not(:last-child)>.selection>.select2-selection,
.was-validated .select2-hidden-accessible.custom-select:invalid+.select2-container--focus.select2-container--bootstrap:not(:last-child)>.selection>.select2-selection,
.select2-hidden-accessible.custom-select.is-invalid+.select2-container--open.select2-container--bootstrap:not(:last-child)>.selection>.select2-selection,
.was-validated .select2-hidden-accessible.custom-select:invalid+.select2-container--open.select2-container--bootstrap:not(:last-child)>.selection>.select2-selection {
border-color: #dc3545;
box-shadow: 0 0 0 0.2rem rgb(220 53 69 / 25%);
}
.select2-hidden-accessible.custom-select.is-valid+.select2-container--bootstrap:not(:last-child)>.selection>.select2-selection,
.was-validated .select2-hidden-accessible.custom-select:valid+.select2-container--bootstrap:not(:last-child)>.selection>.select2-selection {
border-color: #28a745;
}
.select2-hidden-accessible.custom-select.is-valid+.select2-container--focus.select2-container--bootstrap:not(:last-child)>.selection>.select2-selection,
.was-validated .select2-hidden-accessible.custom-select:valid+.select2-container--focus.select2-container--bootstrap:not(:last-child)>.selection>.select2-selection,
.select2-hidden-accessible.custom-select.is-valid+.select2-container--open.select2-container--bootstrap:not(:last-child)>.selection>.select2-selection,
.was-validated .select2-hidden-accessible.custom-select:valid+.select2-container--open.select2-container--bootstrap:not(:last-child)>.selection>.select2-selection {
border-color: #28a745;
box-shadow: 0 0 0 0.2rem rgb(40 167 69 / 25%);
}
/* There is a little too much padding on form inputs */ /* There is a little too much padding on form inputs */
.form-control { .form-control {
padding-right: 0.75rem !important; padding-right: 0.75rem !important;
} }
.btn-group-xs > .btn, .btn-xs { .btn-group-xs>.btn,
.btn-xs {
padding: 0.25rem 0.4rem; padding: 0.25rem 0.4rem;
font-size: 0.875rem; font-size: 0.875rem;
line-height: 0.5; line-height: 0.5;
@ -656,3 +685,9 @@ canvas.drawingBuffer {
pointer-events: none; pointer-events: none;
opacity: 0.5; opacity: 0.5;
} }
/* Fix Select2 in .input-group */
.input-group>.select2-hidden-accessible+.select2-container--bootstrap>.selection>.select2-selection,
.input-group>.select2-hidden-accessible+.select2-container--bootstrap>.selection>.select2-selection.form-control {
width: 100%;
}

File diff suppressed because it is too large Load Diff

View File

@ -1,72 +1,60 @@
$('#save-chore-button').on('click', function(e) $('#save-chore-button').on('click', function(e) {
{
e.preventDefault(); e.preventDefault();
if ($(".combobox-menu-visible").length) if ($(".combobox-menu-visible").length) {
{
return; return;
} }
var jsonData = $('#chore-form').serializeJSON(); var jsonData = $('#chore-form').serializeJSON();
jsonData.start_date = Grocy.Components.DateTimePicker.GetValue(); jsonData.start_date = Grocy.Components.DateTimePicker.GetValue();
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_CHORES_ASSIGNMENTS) if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_CHORES_ASSIGNMENTS) {
{
jsonData.assignment_config = $("#assignment_config").val().join(","); jsonData.assignment_config = $("#assignment_config").val().join(",");
} }
Grocy.FrontendHelpers.BeginUiBusy("chore-form"); Grocy.FrontendHelpers.BeginUiBusy("chore-form");
if (Grocy.EditMode === 'create') if (Grocy.EditMode === 'create') {
{
Grocy.Api.Post('objects/chores', jsonData, Grocy.Api.Post('objects/chores', jsonData,
function(result) function(result) {
{
Grocy.EditObjectId = result.created_object_id; Grocy.EditObjectId = result.created_object_id;
Grocy.Components.UserfieldsForm.Save(function() Grocy.Components.UserfieldsForm.Save(function() {
{ Grocy.Api.Post('chores/executions/calculate-next-assignments', {
Grocy.Api.Post('chores/executions/calculate-next-assignments', { "chore_id": Grocy.EditObjectId }, "chore_id": Grocy.EditObjectId
function(result) },
{ function(result) {
window.location.href = U('/chores'); window.location.href = U('/chores');
}, },
function(xhr) function(xhr) {
{
Grocy.FrontendHelpers.EndUiBusy(); Grocy.FrontendHelpers.EndUiBusy();
console.error(xhr); console.error(xhr);
} }
); );
}); });
}, },
function(xhr) function(xhr) {
{
Grocy.FrontendHelpers.EndUiBusy("chore-form"); Grocy.FrontendHelpers.EndUiBusy("chore-form");
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response) Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response)
} }
); );
} } else {
else
{
Grocy.Api.Put('objects/chores/' + Grocy.EditObjectId, jsonData, Grocy.Api.Put('objects/chores/' + Grocy.EditObjectId, jsonData,
function(result) function(result) {
{ Grocy.Components.UserfieldsForm.Save(function() {
Grocy.Components.UserfieldsForm.Save(function() Grocy.Api.Post('chores/executions/calculate-next-assignments', {
{ "chore_id": Grocy.EditObjectId
Grocy.Api.Post('chores/executions/calculate-next-assignments', { "chore_id": Grocy.EditObjectId }, },
function(result) function(result) {
{
window.location.href = U('/chores'); window.location.href = U('/chores');
}, },
function(xhr) function(xhr) {
{
Grocy.FrontendHelpers.EndUiBusy(); Grocy.FrontendHelpers.EndUiBusy();
console.error(xhr); console.error(xhr);
} }
); );
}); });
}, },
function(xhr) function(xhr) {
{
Grocy.FrontendHelpers.EndUiBusy("chore-form"); Grocy.FrontendHelpers.EndUiBusy("chore-form");
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response) Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response)
} }
@ -74,13 +62,11 @@
} }
}); });
$('#chore-form input').keyup(function(event) $('#chore-form input').on('keyup', function(event) {
{
Grocy.FrontendHelpers.ValidateForm('chore-form'); Grocy.FrontendHelpers.ValidateForm('chore-form');
}); });
$('#chore-form input').keydown(function(event) $('#chore-form input').on('keydown', function(event) {
{
if (event.keyCode === 13) //Enter if (event.keyCode === 13) //Enter
{ {
event.preventDefault(); event.preventDefault();
@ -88,58 +74,48 @@ $('#chore-form input').keydown(function(event)
if (document.getElementById('chore-form').checkValidity() === false) //There is at least one validation error if (document.getElementById('chore-form').checkValidity() === false) //There is at least one validation error
{ {
return false; return false;
} } else {
else
{
$('#save-chore-button').click(); $('#save-chore-button').click();
} }
} }
}); });
var checkboxValues = $("#period_config").val().split(","); var checkboxValues = $("#period_config").val().split(",");
for (var i = 0; i < checkboxValues.length; i++) for (var i = 0; i < checkboxValues.length; i++) {
{ if (!checkboxValues[i].isEmpty()) {
if (!checkboxValues[i].isEmpty())
{
$("#" + checkboxValues[i]).prop('checked', true); $("#" + checkboxValues[i]).prop('checked', true);
} }
} }
Grocy.Components.UserfieldsForm.Load(); Grocy.Components.UserfieldsForm.Load();
$('#name').focus(); $('#name').trigger('focus');
Grocy.FrontendHelpers.ValidateForm('chore-form'); Grocy.FrontendHelpers.ValidateForm('chore-form');
if (Grocy.EditMode == "edit") if (Grocy.EditMode == "edit") {
{
Grocy.Api.Get('objects/chores_log?limit=1&query[]=chore_id=' + Grocy.EditObjectId, Grocy.Api.Get('objects/chores_log?limit=1&query[]=chore_id=' + Grocy.EditObjectId,
function(journalEntries) function(journalEntries) {
{ if (journalEntries.length > 0) {
if (journalEntries.length > 0)
{
$(".datetimepicker-input").attr("disabled", ""); $(".datetimepicker-input").attr("disabled", "");
} }
}, },
function(xhr) function(xhr) {
{
console.error(xhr); console.error(xhr);
} }
); );
} }
setTimeout(function() setTimeout(function() {
{
$(".input-group-chore-period-type").trigger("change"); $(".input-group-chore-period-type").trigger("change");
$(".input-group-chore-assignment-type").trigger("change"); $(".input-group-chore-assignment-type").trigger("change");
// Click twice to trigger on-click but not change the actual checked state // Click twice to trigger on-click but not change the actual checked state
$("#consume_product_on_execution").click(); $("#consume_product_on_execution").trigger('click');
$("#consume_product_on_execution").click(); $("#consume_product_on_execution").trigger('click');
Grocy.Components.ProductPicker.GetPicker().trigger('change'); Grocy.Components.ProductPicker.Validate();
}, 100); }, 100);
$('.input-group-chore-period-type').on('change keyup', function(e) $('.input-group-chore-period-type').on('change keyup', function(e) {
{
var periodType = $('#period_type').val(); var periodType = $('#period_type').val();
var periodDays = $('#period_days').val(); var periodDays = $('#period_days').val();
var periodInterval = $('#period_interval').val(); var periodInterval = $('#period_interval').val();
@ -148,68 +124,49 @@ $('.input-group-chore-period-type').on('change keyup', function(e)
$(".period-type-" + periodType).removeClass("d-none"); $(".period-type-" + periodType).removeClass("d-none");
$("#period_config").val(""); $("#period_config").val("");
if (periodType === 'manually') if (periodType === 'manually') {
{
$('#chore-schedule-info').text(__t('This means the next execution of this chore is not scheduled')); $('#chore-schedule-info').text(__t('This means the next execution of this chore is not scheduled'));
} } else if (periodType === 'hourly') {
else if (periodType === 'hourly')
{
$('#chore-schedule-info').text(__n(periodInterval, "This means the next execution of this chore is scheduled %s hour after the last execution", "This means the next execution of this chore is scheduled %s hours after the last execution")); $('#chore-schedule-info').text(__n(periodInterval, "This means the next execution of this chore is scheduled %s hour after the last execution", "This means the next execution of this chore is scheduled %s hours after the last execution"));
} } else if (periodType === 'daily') {
else if (periodType === 'daily')
{
$('#chore-schedule-info').text(__n(periodInterval, "This means the next execution of this chore is scheduled at the same time (based on the start date) every day", "This means the next execution of this chore is scheduled at the same time (based on the start date) every %s days")); $('#chore-schedule-info').text(__n(periodInterval, "This means the next execution of this chore is scheduled at the same time (based on the start date) every day", "This means the next execution of this chore is scheduled at the same time (based on the start date) every %s days"));
} } else if (periodType === 'weekly') {
else if (periodType === 'weekly')
{
$('#chore-schedule-info').text(__n(periodInterval, "This means the next execution of this chore is scheduled every week on the selected weekdays", "This means the next execution of this chore is scheduled every %s weeks on the selected weekdays")); $('#chore-schedule-info').text(__n(periodInterval, "This means the next execution of this chore is scheduled every week on the selected weekdays", "This means the next execution of this chore is scheduled every %s weeks on the selected weekdays"));
$("#period_config").val($(".period-type-weekly input:checkbox:checked").map(function() { return this.value; }).get().join(",")); $("#period_config").val($(".period-type-weekly input:checkbox:checked").map(function() {
} return this.value;
else if (periodType === 'monthly') }).get().join(","));
{ } else if (periodType === 'monthly') {
$('#chore-schedule-info').text(__n(periodInterval, "This means the next execution of this chore is scheduled on the selected day every month", "This means the next execution of this chore is scheduled on the selected day every %s months")); $('#chore-schedule-info').text(__n(periodInterval, "This means the next execution of this chore is scheduled on the selected day every month", "This means the next execution of this chore is scheduled on the selected day every %s months"));
$("label[for='period_days']").text(__t("Day of month")); $("label[for='period_days']").text(__t("Day of month"));
$("#period_days").attr("min", "1"); $("#period_days").attr("min", "1");
$("#period_days").attr("max", "31"); $("#period_days").attr("max", "31");
} } else if (periodType === 'yearly') {
else if (periodType === 'yearly')
{
$('#chore-schedule-info').text(__n(periodInterval, 'This means the next execution of this chore is scheduled every year on the same day (based on the start date)', 'This means the next execution of this chore is scheduled every %s years on the same day (based on the start date)')); $('#chore-schedule-info').text(__n(periodInterval, 'This means the next execution of this chore is scheduled every year on the same day (based on the start date)', 'This means the next execution of this chore is scheduled every %s years on the same day (based on the start date)'));
} } else if (periodType === 'adaptive') {
else if (periodType === 'adaptive')
{
$('#chore-schedule-info').text(__t('This means the next execution of this chore is scheduled dynamically based on the past average execution frequency')); $('#chore-schedule-info').text(__t('This means the next execution of this chore is scheduled dynamically based on the past average execution frequency'));
} }
Grocy.FrontendHelpers.ValidateForm('chore-form'); Grocy.FrontendHelpers.ValidateForm('chore-form');
}); });
$('.input-group-chore-assignment-type').on('change', function(e) $('.input-group-chore-assignment-type').on('change', function(e) {
{
var assignmentType = $('#assignment_type').val(); var assignmentType = $('#assignment_type').val();
$('#chore-period-assignment-info').text(""); $('#chore-period-assignment-info').text("");
$("#assignment_config").removeAttr("required"); $("#assignment_config").removeAttr("required");
$("#assignment_config").attr("disabled", ""); $("#assignment_config").attr("disabled", "");
if (assignmentType === 'no-assignment') if (assignmentType === 'no-assignment') {
{
$('#chore-assignment-type-info').text(__t('This means the next execution of this chore will not be assigned to anyone')); $('#chore-assignment-type-info').text(__t('This means the next execution of this chore will not be assigned to anyone'));
} } else if (assignmentType === 'who-least-did-first') {
else if (assignmentType === 'who-least-did-first')
{
$('#chore-assignment-type-info').text(__t('This means the next execution of this chore will be assigned to the one who executed it least')); $('#chore-assignment-type-info').text(__t('This means the next execution of this chore will be assigned to the one who executed it least'));
$("#assignment_config").attr("required", ""); $("#assignment_config").attr("required", "");
$("#assignment_config").removeAttr("disabled"); $("#assignment_config").removeAttr("disabled");
} } else if (assignmentType === 'random') {
else if (assignmentType === 'random')
{
$('#chore-assignment-type-info').text(__t('This means the next execution of this chore will be assigned randomly')); $('#chore-assignment-type-info').text(__t('This means the next execution of this chore will be assigned randomly'));
$("#assignment_config").attr("required", ""); $("#assignment_config").attr("required", "");
$("#assignment_config").removeAttr("disabled"); $("#assignment_config").removeAttr("disabled");
} } else if (assignmentType === 'in-alphabetical-order') {
else if (assignmentType === 'in-alphabetical-order')
{
$('#chore-assignment-type-info').text(__t('This means the next execution of this chore will be assigned to the next one in alphabetical order')); $('#chore-assignment-type-info').text(__t('This means the next execution of this chore will be assigned to the next one in alphabetical order'));
$("#assignment_config").attr("required", ""); $("#assignment_config").attr("required", "");
$("#assignment_config").removeAttr("disabled"); $("#assignment_config").removeAttr("disabled");
@ -218,15 +175,11 @@ $('.input-group-chore-assignment-type').on('change', function(e)
Grocy.FrontendHelpers.ValidateForm('chore-form'); Grocy.FrontendHelpers.ValidateForm('chore-form');
}); });
$("#consume_product_on_execution").on("click", function() $("#consume_product_on_execution").on("click", function() {
{ if (this.checked) {
if (this.checked)
{
Grocy.Components.ProductPicker.Enable(); Grocy.Components.ProductPicker.Enable();
$("#product_amount").removeAttr("disabled"); $("#product_amount").removeAttr("disabled");
} } else {
else
{
Grocy.Components.ProductPicker.Disable(); Grocy.Components.ProductPicker.Disable();
$("#product_amount").attr("disabled", ""); $("#product_amount").attr("disabled", "");
} }
@ -234,35 +187,28 @@ $("#consume_product_on_execution").on("click", function()
Grocy.FrontendHelpers.ValidateForm("chore-form"); Grocy.FrontendHelpers.ValidateForm("chore-form");
}); });
Grocy.Components.ProductPicker.GetPicker().on('change', function(e) Grocy.Components.ProductPicker.OnChange(function(e) {
{
var productId = $(e.target).val(); var productId = $(e.target).val();
if (productId) if (productId) {
{
Grocy.Api.Get('stock/products/' + productId, Grocy.Api.Get('stock/products/' + productId,
function(productDetails) function(productDetails) {
{
$('#amount_qu_unit').text(productDetails.quantity_unit_stock.name); $('#amount_qu_unit').text(productDetails.quantity_unit_stock.name);
}, },
function(xhr) function(xhr) {
{
console.error(xhr); console.error(xhr);
} }
); );
} }
}); });
$(document).on('click', '.chore-grocycode-label-print', function(e) $(document).on('click', '.chore-grocycode-label-print', function(e) {
{
e.preventDefault(); e.preventDefault();
document.activeElement.blur(); document.activeElement.blur();
var choreId = $(e.currentTarget).attr('data-chore-id'); var choreId = $(e.currentTarget).attr('data-chore-id');
Grocy.Api.Get('chores/' + choreId + '/printlabel', function(labelData) Grocy.Api.Get('chores/' + choreId + '/printlabel', function(labelData) {
{ if (Grocy.Webhooks.labelprinter !== undefined) {
if (Grocy.Webhooks.labelprinter !== undefined)
{
Grocy.FrontendHelpers.RunWebhook(Grocy.Webhooks.labelprinter, labelData); Grocy.FrontendHelpers.RunWebhook(Grocy.Webhooks.labelprinter, labelData);
} }
}); });

View File

@ -1,20 +1,17 @@
Grocy.Components.BarcodeScanner = {}; Grocy.Components.BarcodeScanner = {};
Grocy.Components.BarcodeScanner.LiveVideoSizeAdjusted = false; Grocy.Components.BarcodeScanner.LiveVideoSizeAdjusted = false;
Grocy.Components.BarcodeScanner.CheckCapabilities = async function() Grocy.Components.BarcodeScanner.CheckCapabilities = async function() {
{
var track = Quagga.CameraAccess.getActiveTrack(); var track = Quagga.CameraAccess.getActiveTrack();
var capabilities = {}; var capabilities = {};
if (typeof track.getCapabilities === 'function') if (typeof track.getCapabilities === 'function') {
{
capabilities = track.getCapabilities(); capabilities = track.getCapabilities();
} }
// If there is more than 1 camera, show the camera selection // If there is more than 1 camera, show the camera selection
var cameras = await Quagga.CameraAccess.enumerateVideoDevices(); var cameras = await Quagga.CameraAccess.enumerateVideoDevices();
var cameraSelect = document.querySelector('.cameraSelect-wrapper'); var cameraSelect = document.querySelector('.cameraSelect-wrapper');
if (cameraSelect) if (cameraSelect) {
{
cameraSelect.style.display = cameras.length > 1 ? 'inline-block' : 'none'; cameraSelect.style.display = cameras.length > 1 ? 'inline-block' : 'none';
} }
@ -22,29 +19,23 @@ Grocy.Components.BarcodeScanner.CheckCapabilities = async function()
var canTorch = typeof capabilities.torch === 'boolean' && capabilities.torch var canTorch = typeof capabilities.torch === 'boolean' && capabilities.torch
// Remove the torch button, if either the device can not torch or AutoTorchOn is set. // Remove the torch button, if either the device can not torch or AutoTorchOn is set.
var node = document.querySelector('.torch'); var node = document.querySelector('.torch');
if (node) if (node) {
{
node.style.display = canTorch && !Grocy.FeatureFlags.GROCY_FEATURE_FLAG_AUTO_TORCH_ON_WITH_CAMERA ? 'inline-block' : 'none'; node.style.display = canTorch && !Grocy.FeatureFlags.GROCY_FEATURE_FLAG_AUTO_TORCH_ON_WITH_CAMERA ? 'inline-block' : 'none';
} }
// If AutoTorchOn is set, turn on the torch. // If AutoTorchOn is set, turn on the torch.
if (canTorch && Grocy.FeatureFlags.GROCY_FEATURE_FLAG_AUTO_TORCH_ON_WITH_CAMERA) if (canTorch && Grocy.FeatureFlags.GROCY_FEATURE_FLAG_AUTO_TORCH_ON_WITH_CAMERA) {
{
Grocy.Components.BarcodeScanner.TorchOn(track); Grocy.Components.BarcodeScanner.TorchOn(track);
} }
// Reduce the height of the video, if it's higher than then the viewport // Reduce the height of the video, if it's higher than then the viewport
if (!Grocy.Components.BarcodeScanner.LiveVideoSizeAdjusted) if (!Grocy.Components.BarcodeScanner.LiveVideoSizeAdjusted) {
{
var bc = document.getElementById('barcodescanner-container'); var bc = document.getElementById('barcodescanner-container');
if (bc) if (bc) {
{
var bcAspectRatio = bc.offsetWidth / bc.offsetHeight; var bcAspectRatio = bc.offsetWidth / bc.offsetHeight;
var settings = track.getSettings(); var settings = track.getSettings();
if (bcAspectRatio > settings.aspectRatio) if (bcAspectRatio > settings.aspectRatio) {
{
var v = document.querySelector('#barcodescanner-livestream video') var v = document.querySelector('#barcodescanner-livestream video')
if (v) if (v) {
{
var c = document.querySelector('#barcodescanner-livestream canvas') var c = document.querySelector('#barcodescanner-livestream canvas')
var newWidth = v.clientWidth / bcAspectRatio * settings.aspectRatio + 'px'; var newWidth = v.clientWidth / bcAspectRatio * settings.aspectRatio + 'px';
v.style.width = newWidth; v.style.width = newWidth;
@ -57,8 +48,7 @@ Grocy.Components.BarcodeScanner.CheckCapabilities = async function()
} }
} }
Grocy.Components.BarcodeScanner.StartScanning = function() Grocy.Components.BarcodeScanner.StartScanning = function() {
{
Grocy.Components.BarcodeScanner.DecodedCodesCount = 0; Grocy.Components.BarcodeScanner.DecodedCodesCount = 0;
Grocy.Components.BarcodeScanner.DecodedCodesErrorCount = 0; Grocy.Components.BarcodeScanner.DecodedCodesErrorCount = 0;
@ -69,7 +59,9 @@ Grocy.Components.BarcodeScanner.StartScanning = function()
target: document.querySelector("#barcodescanner-livestream"), target: document.querySelector("#barcodescanner-livestream"),
constraints: { constraints: {
facingMode: "environment", facingMode: "environment",
...(window.localStorage.getItem('cameraId') && { deviceId: window.localStorage.getItem('cameraId') }) // If preferred cameraId is set, request to use that specific camera ...(window.localStorage.getItem('cameraId') && {
deviceId: window.localStorage.getItem('cameraId')
}) // If preferred cameraId is set, request to use that specific camera
} }
}, },
locator: { locator: {
@ -115,15 +107,12 @@ Grocy.Components.BarcodeScanner.StartScanning = function()
} }
}, },
locate: true locate: true
}, function(error) }, function(error) {
{ if (error) {
if (error)
{
Grocy.FrontendHelpers.ShowGenericError("Error while initializing the barcode scanning library", error.message); Grocy.FrontendHelpers.ShowGenericError("Error while initializing the barcode scanning library", error.message);
toastr.info(__t("Camera access is only possible when supported and allowed by your browser and when grocy is served via a secure (https://) connection")); toastr.info(__t("Camera access is only possible when supported and allowed by your browser and when grocy is served via a secure (https://) connection"));
window.localStorage.removeItem("cameraId"); window.localStorage.removeItem("cameraId");
setTimeout(function() setTimeout(function() {
{
bootbox.hideAll(); bootbox.hideAll();
}, 500); }, 500);
return; return;
@ -135,8 +124,7 @@ Grocy.Components.BarcodeScanner.StartScanning = function()
}); });
} }
Grocy.Components.BarcodeScanner.StopScanning = function() Grocy.Components.BarcodeScanner.StopScanning = function() {
{
Quagga.stop(); Quagga.stop();
Grocy.Components.BarcodeScanner.DecodedCodesCount = 0; Grocy.Components.BarcodeScanner.DecodedCodesCount = 0;
@ -145,76 +133,77 @@ Grocy.Components.BarcodeScanner.StopScanning = function()
bootbox.hideAll(); bootbox.hideAll();
} }
Grocy.Components.BarcodeScanner.TorchOn = function(track) Grocy.Components.BarcodeScanner.TorchOn = function(track) {
{ if (track) {
if (track)
{
track.applyConstraints({ track.applyConstraints({
advanced: [ advanced: [{
{
torch: true torch: true
} }]
]
}); });
} }
} }
Quagga.onDetected(function(result) Quagga.onDetected(function(result) {
{ $.each(result.codeResult.decodedCodes, function(id, error) {
$.each(result.codeResult.decodedCodes, function(id, error) if (error.error != undefined) {
{
if (error.error != undefined)
{
Grocy.Components.BarcodeScanner.DecodedCodesCount++; Grocy.Components.BarcodeScanner.DecodedCodesCount++;
Grocy.Components.BarcodeScanner.DecodedCodesErrorCount += parseFloat(error.error); Grocy.Components.BarcodeScanner.DecodedCodesErrorCount += parseFloat(error.error);
} }
}); });
if ((Grocy.Components.BarcodeScanner.DecodedCodesErrorCount / Grocy.Components.BarcodeScanner.DecodedCodesCount < 0.15) || if ((Grocy.Components.BarcodeScanner.DecodedCodesErrorCount / Grocy.Components.BarcodeScanner.DecodedCodesCount < 0.15) ||
(Grocy.Components.BarcodeScanner.DecodedCodesErrorCount == 0 && Grocy.Components.BarcodeScanner.DecodedCodesCount == 0 && result.codeResult.code.length != 0)) (Grocy.Components.BarcodeScanner.DecodedCodesErrorCount == 0 && Grocy.Components.BarcodeScanner.DecodedCodesCount == 0 && result.codeResult.code.length != 0)) {
{
Grocy.Components.BarcodeScanner.StopScanning(); Grocy.Components.BarcodeScanner.StopScanning();
$(document).trigger("Grocy.BarcodeScanned", [result.codeResult.code, Grocy.Components.BarcodeScanner.CurrentTarget]); $(document).trigger("Grocy.BarcodeScanned", [result.codeResult.code, Grocy.Components.BarcodeScanner.CurrentTarget]);
} }
}); });
Quagga.onProcessed(function(result) Quagga.onProcessed(function(result) {
{
var drawingCtx = Quagga.canvas.ctx.overlay; var drawingCtx = Quagga.canvas.ctx.overlay;
var drawingCanvas = Quagga.canvas.dom.overlay; var drawingCanvas = Quagga.canvas.dom.overlay;
if (result) if (result) {
{ if (result.boxes) {
if (result.boxes)
{
drawingCtx.clearRect(0, 0, parseInt(drawingCanvas.getAttribute("width")), parseInt(drawingCanvas.getAttribute("height"))); drawingCtx.clearRect(0, 0, parseInt(drawingCanvas.getAttribute("width")), parseInt(drawingCanvas.getAttribute("height")));
result.boxes.filter(function(box) result.boxes.filter(function(box) {
{
return box !== result.box; return box !== result.box;
}).forEach(function(box) }).forEach(function(box) {
{ Quagga.ImageDebug.drawPath(box, {
Quagga.ImageDebug.drawPath(box, { x: 0, y: 1 }, drawingCtx, { color: "yellow", lineWidth: 4 }); x: 0,
y: 1
}, drawingCtx, {
color: "yellow",
lineWidth: 4
});
}); });
} }
if (result.box) if (result.box) {
{ Quagga.ImageDebug.drawPath(result.box, {
Quagga.ImageDebug.drawPath(result.box, { x: 0, y: 1 }, drawingCtx, { color: "green", lineWidth: 4 }); x: 0,
y: 1
}, drawingCtx, {
color: "green",
lineWidth: 4
});
} }
if (result.codeResult && result.codeResult.code) if (result.codeResult && result.codeResult.code) {
{ Quagga.ImageDebug.drawPath(result.line, {
Quagga.ImageDebug.drawPath(result.line, { x: 'x', y: 'y' }, drawingCtx, { color: "red", lineWidth: 4 }); x: 'x',
y: 'y'
}, drawingCtx, {
color: "red",
lineWidth: 4
});
} }
} }
}); });
$(document).on("click", "#barcodescanner-start-button", async function(e) $(document).on("click", "#barcodescanner-start-button", async function(e) {
{
e.preventDefault(); e.preventDefault();
var inputElement = $(e.currentTarget).prev(); var inputElement = $(e.currentTarget).prev();
if (inputElement.hasAttr("disabled")) if (inputElement.hasAttr("disabled")) {
{
// Do nothing and disable the barcode scanner start button // Do nothing and disable the barcode scanner start button
$(e.currentTarget).addClass("disabled"); $(e.currentTarget).addClass("disabled");
return; return;
@ -225,8 +214,7 @@ $(document).on("click", "#barcodescanner-start-button", async function(e)
var dialog = bootbox.dialog({ var dialog = bootbox.dialog({
message: '<div id="barcodescanner-container" class="col"><div id="barcodescanner-livestream"></div></div>', message: '<div id="barcodescanner-container" class="col"><div id="barcodescanner-livestream"></div></div>',
title: __t('Scan a barcode'), title: __t('Scan a barcode'),
onEscape: function() onEscape: function() {
{
Grocy.Components.BarcodeScanner.StopScanning(); Grocy.Components.BarcodeScanner.StopScanning();
}, },
size: 'big', size: 'big',
@ -236,8 +224,7 @@ $(document).on("click", "#barcodescanner-start-button", async function(e)
torch: { torch: {
label: '<i class="far fa-lightbulb"></i>', label: '<i class="far fa-lightbulb"></i>',
className: 'btn-warning responsive-button torch', className: 'btn-warning responsive-button torch',
callback: function() callback: function() {
{
Grocy.Components.BarcodeScanner.TorchOn(Quagga.CameraAccess.getActiveTrack()); Grocy.Components.BarcodeScanner.TorchOn(Quagga.CameraAccess.getActiveTrack());
return false; return false;
} }
@ -245,8 +232,7 @@ $(document).on("click", "#barcodescanner-start-button", async function(e)
cancel: { cancel: {
label: __t('Cancel'), label: __t('Cancel'),
className: 'btn-secondary responsive-button', className: 'btn-secondary responsive-button',
callback: function() callback: function() {
{
Grocy.Components.BarcodeScanner.StopScanning(); Grocy.Components.BarcodeScanner.StopScanning();
} }
} }
@ -258,8 +244,7 @@ $(document).on("click", "#barcodescanner-start-button", async function(e)
var cameraSelect = document.querySelector('.cameraSelect'); var cameraSelect = document.querySelector('.cameraSelect');
var cameras = await Quagga.CameraAccess.enumerateVideoDevices(); var cameras = await Quagga.CameraAccess.enumerateVideoDevices();
cameras.forEach(camera => cameras.forEach(camera => {
{
var option = document.createElement("option"); var option = document.createElement("option");
option.text = camera.label ? camera.label : camera.deviceId; // Use camera label if it exists, else show device id option.text = camera.label ? camera.label : camera.deviceId; // Use camera label if it exists, else show device id
option.value = camera.deviceId; option.value = camera.deviceId;
@ -269,8 +254,7 @@ $(document).on("click", "#barcodescanner-start-button", async function(e)
// Set initial value to preferred camera if one exists - and if not, start out empty // Set initial value to preferred camera if one exists - and if not, start out empty
cameraSelect.value = window.localStorage.getItem('cameraId'); cameraSelect.value = window.localStorage.getItem('cameraId');
cameraSelect.onchange = function() cameraSelect.onchange = function() {
{
window.localStorage.setItem('cameraId', cameraSelect.value); window.localStorage.setItem('cameraId', cameraSelect.value);
Quagga.stop(); Quagga.stop();
Grocy.Components.BarcodeScanner.StartScanning(); Grocy.Components.BarcodeScanner.StartScanning();
@ -280,29 +264,25 @@ $(document).on("click", "#barcodescanner-start-button", async function(e)
}); });
Grocy.Components.BarcodeScanner.InitDone = false; Grocy.Components.BarcodeScanner.InitDone = false;
Grocy.Components.BarcodeScanner.Init = function() Grocy.Components.BarcodeScanner.Init = function() {
{ if (Grocy.Components.BarcodeScanner.InitDone) {
if (Grocy.Components.BarcodeScanner.InitDone)
{
return; return;
} }
$(".barcodescanner-input:visible").each(function() $(".barcodescanner-input:visible").each(function() {
{ var isSelect2 = $(this).hasClass('select2') && $(this).parent().hasClass('input-group');
if ($(this).hasAttr("disabled")) var html = '<a id="barcodescanner-start-button" class="btn' + (isSelect2 ? '' : ' btn-sm') + ' btn-primary text-white' + ($(this).hasAttr("disabled") ? ' disabled' : '') + '" data-target="' + $(this).attr("data-target") + '"><i class="fas fa-camera"></i></a>';
{ if (isSelect2) {
$(this).after('<a id="barcodescanner-start-button" class="btn btn-sm btn-primary text-white disabled" data-target="' + $(this).attr("data-target") + '"><i class="fas fa-camera"></i></a>'); html = '<div class="input-group-prepend" id="barcodescanner-start-button-container">' + html + '</div>'
} $(this).parent().prepend(html);
else } else {
{ $(this).after(html);
$(this).after('<a id="barcodescanner-start-button" class="btn btn-sm btn-primary text-white" data-target="' + $(this).attr("data-target") + '"><i class="fas fa-camera"></i></a>');
} }
Grocy.Components.BarcodeScanner.InitDone = true; Grocy.Components.BarcodeScanner.InitDone = true;
}); });
} }
setTimeout(function() setTimeout(function() {
{
Grocy.Components.BarcodeScanner.Init(); Grocy.Components.BarcodeScanner.Init();
}, 50); }, 50);

View File

@ -1,195 +1,341 @@
Grocy.Components.ProductPicker = {}; Grocy.Components.ProductPicker = {};
Grocy.Components.ProductPicker.GetPicker = function() Grocy.Components.ProductPicker.GetPicker = function() {
{
return $('#product_id'); return $('#product_id');
} }
Grocy.Components.ProductPicker.GetInputElement = function() Grocy.Components.ProductPicker.GetValue = function() {
{ return this.GetPicker().val();
return $('#product_id_text_input');
} }
Grocy.Components.ProductPicker.GetValue = function() Grocy.Components.ProductPicker.GetOption = function(key) {
{ return this.GetPicker().parents('.form-group').data(key);
return $('#product_id').val();
} }
Grocy.Components.ProductPicker.SetValue = function(value) Grocy.Components.ProductPicker.GetState = function(key) {
{ return this.GetPicker().data(key);
Grocy.Components.ProductPicker.GetInputElement().val(value);
Grocy.Components.ProductPicker.GetInputElement().trigger('change');
} }
Grocy.Components.ProductPicker.SetId = function(value) Grocy.Components.ProductPicker.SetState = function(key, value) {
{ this.GetPicker().data(key, value);
Grocy.Components.ProductPicker.GetPicker().val(value); return this;
Grocy.Components.ProductPicker.GetPicker().data('combobox').refresh();
Grocy.Components.ProductPicker.GetInputElement().trigger('change');
} }
Grocy.Components.ProductPicker.Clear = function() Grocy.Components.ProductPicker.SetId = function(value, callback) {
{ if (this.GetPicker().find('option[value="' + value + '"]').length) {
Grocy.Components.ProductPicker.SetValue(''); this.GetPicker().val(value).trigger('change');
Grocy.Components.ProductPicker.SetId(null); } else {
Grocy.Api.Get('objects/products/' + encodeURIComponent(value),
function(result) {
var option = new Option(result.name, value, true, true);
if (typeof callback === 'function') {
Grocy.Components.ProductPicker.GetPicker().one('change.select2', callback);
}
Grocy.Components.ProductPicker.GetPicker().append(option).trigger('change').select2('close');
},
function(xhr) {
console.error(xhr);
}
);
}
return this;
} }
Grocy.Components.ProductPicker.InProductAddWorkflow = function() Grocy.Components.ProductPicker.Clear = function() {
{ this.GetPicker().val(null).trigger('change');
return this;
}
Grocy.Components.ProductPicker.InProductAddWorkflow = function() {
return GetUriParam('flow') == "InplaceNewProductWithName"; return GetUriParam('flow') == "InplaceNewProductWithName";
} }
Grocy.Components.ProductPicker.InProductModifyWorkflow = function() Grocy.Components.ProductPicker.InProductModifyWorkflow = function() {
{
return GetUriParam('flow') == "InplaceAddBarcodeToExistingProduct"; return GetUriParam('flow') == "InplaceAddBarcodeToExistingProduct";
} }
Grocy.Components.ProductPicker.InAnyFlow = function() Grocy.Components.ProductPicker.InAnyFlow = function() {
{ return GetUriParam('flow') !== undefined;
return Grocy.Components.ProductPicker.InProductAddWorkflow() || Grocy.Components.ProductPicker.InProductModifyWorkflow();
} }
Grocy.Components.ProductPicker.FinishFlow = function() Grocy.Components.ProductPicker.FinishFlow = function() {
{
RemoveUriParam("flow"); RemoveUriParam("flow");
RemoveUriParam("barcode"); RemoveUriParam("barcode");
RemoveUriParam("product-name"); RemoveUriParam("product-name");
return this;
} }
Grocy.Components.ProductPicker.ShowCustomError = function(text) Grocy.Components.ProductPicker.ShowCustomError = function(text) {
{
var element = $("#custom-productpicker-error"); var element = $("#custom-productpicker-error");
element.text(text); element.text(text);
element.removeClass("d-none"); element.removeClass("d-none");
return this;
} }
Grocy.Components.ProductPicker.HideCustomError = function() Grocy.Components.ProductPicker.HideCustomError = function() {
{
$("#custom-productpicker-error").addClass("d-none"); $("#custom-productpicker-error").addClass("d-none");
return this;
} }
Grocy.Components.ProductPicker.Disable = function() Grocy.Components.ProductPicker.Disable = function() {
{ this.GetPicker().prop("disabled", true);
Grocy.Components.ProductPicker.GetInputElement().attr("disabled", "");
$("#barcodescanner-start-button").attr("disabled", ""); $("#barcodescanner-start-button").attr("disabled", "");
$("#barcodescanner-start-button").addClass("disabled"); $("#barcodescanner-start-button").addClass("disabled");
return this;
} }
Grocy.Components.ProductPicker.Enable = function() Grocy.Components.ProductPicker.Enable = function() {
{ this.GetPicker().prop("disabled", false);
Grocy.Components.ProductPicker.GetInputElement().removeAttr("disabled");
$("#barcodescanner-start-button").removeAttr("disabled"); $("#barcodescanner-start-button").removeAttr("disabled");
$("#barcodescanner-start-button").removeClass("disabled"); $("#barcodescanner-start-button").removeClass("disabled");
return this;
} }
$('.product-combobox').combobox({ Grocy.Components.ProductPicker.Require = function() {
appendId: '_text_input', this.GetPicker().prop("required", true);
bsVersion: '4', return this;
clearIfNoMatch: false }
Grocy.Components.ProductPicker.Optional = function() {
this.GetPicker().prop("required", false);
return this;
}
Grocy.Components.ProductPicker.Focus = function() {
this.GetPicker().select2('open');
return this;
}
Grocy.Components.ProductPicker.Validate = function() {
this.GetPicker().trigger('change');
return this;
}
Grocy.Components.ProductPicker.OnChange = function(eventHandler) {
this.GetPicker().on('change', eventHandler);
return this;
}
Grocy.Components.ProductPicker.Search = function(term, callback) {
var $picker = this.GetPicker();
var doSearch = function() {
var $search = $picker.data('select2').dropdown.$search || $picker.data('select2').selection.$search;
$search.val(term).trigger('input');
// must wait for debounce before listening for 'results:all' event
setTimeout(function() {
$picker.one('Grocy.ResultsUpdated', function() {
if (typeof callback === 'function') {
$picker.one('select2:close', callback);
}
$picker.select2('close');
});
}, 150);
};
if ($picker.data('select2').isOpen()) {
doSearch();
} else {
$picker.one('select2:open', doSearch);
$picker.select2('open');
}
return this;
}
Grocy.Components.ProductPicker.IsOpen = function() {
return this.GetPicker().parent().find('.select2-container').hasClass('select2-container--open');
}
// initialize Select2 product picker
var lastProductSearchTerm = '';
Grocy.Components.ProductPicker.GetPicker().select2({
selectOnClose: true,
ajax: {
delay: 150,
transport: function(params, success, failure) {
var results_per_page = 10;
var page = params.data.page || 1;
var term = params.data.term || "";
var termIsGrocycode = term.startsWith("grcy");
lastProductSearchTerm = term;
// reset grocycode/barcode state
Grocy.Components.ProductPicker.SetState('barcode', 'null');
Grocy.Components.ProductPicker.SetState('grocycode', false);
// build search queries
var baseQuery = Grocy.Components.ProductPicker.GetOption('products-query').split('&');
baseQuery.push('limit=' + encodeURIComponent(results_per_page));
baseQuery.push('offset=' + encodeURIComponent((page - 1) * results_per_page));
var queries = [];
if (term.length > 0) {
queries = [
// search product fields (name, etc.)
baseQuery.concat('search=' + encodeURIComponent(term)).join('&'),
];
// grocycode handling
if (termIsGrocycode) {
var gc = term.split(":");
if (gc[1] == "p") {
queries.push(baseQuery.concat('query%5B%5D=id%3D' + encodeURIComponent(gc[2])).join('&'));
}
}
} else {
queries = [baseQuery.join('&')];
}
// execute all queries in parallel, return first non-empty response
var complete = 0;
var responded = false;
var xhrs = [];
var handleEmptyResponse = function() {
if (responded || complete < xhrs.length) return;
success({
results: [],
pagination: {
more: false
}
});
};
var handleResponse = function(results, meta, cur_xhr) {
// track complete queries
complete++;
// abort if we already responded
if (responded) return;
// abort if no results
if (results.length === 0) {
handleEmptyResponse();
return;
}
// track whether we have responded
responded = true;
// abort all other queries
xhrs.forEach(function(xhr) {
if (xhr !== cur_xhr) xhr.abort();
}); });
var prefillProduct = GetUriParam('product-name'); // update grocycode/barcode state
var prefillProduct2 = Grocy.Components.ProductPicker.GetPicker().parent().data('prefill-by-name').toString(); Grocy.Components.ProductPicker.SetState('grocycode', termIsGrocycode);
if (!prefillProduct2.isEmpty()) Grocy.Components.ProductPicker.SetState('barcode', term);
{
prefillProduct = prefillProduct2; success({
results: results.map(function(result) {
return {
id: result.id,
text: result.name
};
}),
pagination: {
more: page * results_per_page < meta.recordsFiltered
} }
if (typeof prefillProduct !== "undefined") });
{ };
var possibleOptionElement = $("#product_id option[data-additional-searchdata*=\"" + prefillProduct + "\"]").first(); var handleErrors = function(xhr) {
if (possibleOptionElement.length === 0) console.error(xhr);
{
possibleOptionElement = $("#product_id option:contains(\"" + prefillProduct + "\")").first(); // track complete queries
complete++;
// abort if we already responded
if (responded) return;
handleEmptyResponse();
};
if (term.length > 0) {
xhrs.push(Grocy.Api.Get('objects/product_barcodes?limit=1&query%5B%5D=barcode%3D' + encodeURIComponent(term),
function(results, meta) {
// track complete queries
complete++;
// abort if we already responded
if (responded) return;
// abort if no results
if (results.length === 0) {
handleEmptyResponse();
return;
} }
if (possibleOptionElement.length > 0) var cur_xhr = Grocy.Api.Get('objects/products?' + baseQuery.concat('query%5B%5D=id%3D' + encodeURIComponent(results[0].product_id)).join('&'),
{ function(results, meta) {
$('#product_id').val(possibleOptionElement.val()); handleResponse(results, meta, cur_xhr);
$('#product_id').data('combobox').refresh(); },
$('#product_id').trigger('change'); handleErrors
);
var nextInputElement = $(Grocy.Components.ProductPicker.GetPicker().parent().data('next-input-selector').toString()); xhrs.push(cur_xhr);
nextInputElement.focus(); },
handleErrors
));
}
xhrs = xhrs.concat(queries.map(function(query) {
var cur_xhr = Grocy.Api.Get('objects/products' + (query.length > 0 ? '?' + query : ''),
function(results, meta) {
handleResponse(results, meta, cur_xhr);
},
handleErrors
);
return cur_xhr;
}));
} }
} }
});
var prefillProductId = GetUriParam("product"); // forward 'results:all' event
var prefillProductId2 = Grocy.Components.ProductPicker.GetPicker().parent().data('prefill-by-id').toString(); Grocy.Components.ProductPicker.GetPicker().data('select2').on('results:all', function(data) {
if (!prefillProductId2.isEmpty()) Grocy.Components.ProductPicker.GetPicker().trigger('Grocy.ResultsUpdated', data);
{ });
prefillProductId = prefillProductId2;
}
if (typeof prefillProductId !== "undefined")
{
$('#product_id').val(prefillProductId);
$('#product_id').data('combobox').refresh();
$('#product_id').trigger('change');
var nextInputElement = $(Grocy.Components.ProductPicker.GetPicker().parent().data('next-input-selector').toString()); // handle barcode scanning
nextInputElement.focus(); $(document).on("Grocy.BarcodeScanned", function(e, barcode, target) {
} // check that the barcode scan is for the product picker
if (!(target == "@productpicker" || target == "undefined" || target == undefined)) return;
if (GetUriParam("flow") === "InplaceAddBarcodeToExistingProduct") Grocy.Components.ProductPicker.Search(barcode);
{ });
// fix placement of bootbox footer buttons
$(document).on("shown.bs.modal", function() {
$(".modal-footer").addClass("d-block").addClass("d-sm-flex");
$(".modal-footer").find("button").addClass("mt-2").addClass("mt-sm-0");
});
if (GetUriParam("flow") === "InplaceAddBarcodeToExistingProduct") {
$('#InplaceAddBarcodeToExistingProduct').text(GetUriParam("barcode")); $('#InplaceAddBarcodeToExistingProduct').text(GetUriParam("barcode"));
$('#flow-info-InplaceAddBarcodeToExistingProduct').removeClass('d-none'); $('#flow-info-InplaceAddBarcodeToExistingProduct').removeClass('d-none');
$('#barcode-lookup-disabled-hint').removeClass('d-none'); $('#barcode-lookup-disabled-hint').removeClass('d-none');
$('#barcode-lookup-hint').addClass('d-none'); $('#barcode-lookup-hint').addClass('d-none');
} }
// prefill by name
var prefillProduct = Grocy.Components.ProductPicker.GetOption('prefill-by-name') || GetUriParam('product-name');
if (typeof prefillProduct === 'string' && !prefillProduct.isEmpty()) {
Grocy.Components.ProductPicker.Search(prefillProduct, function() {
$(Grocy.Components.ProductPicker.GetOption('next-input-selector')).trigger('focus');
});
}
// prefill by ID
var prefillProduct = Grocy.Components.ProductPicker.GetOption('prefill-by-id') || GetUriParam('product');
if (typeof prefillProduct === 'string' && !prefillProduct.isEmpty()) {
Grocy.Components.ProductPicker.SetId(prefillProduct, function() {
$(Grocy.Components.ProductPicker.GetOption('next-input-selector')).trigger('focus');
});
}
// open create product/barcode dialog if no results
Grocy.Components.ProductPicker.PopupOpen = false; Grocy.Components.ProductPicker.PopupOpen = false;
$('#product_id_text_input').on('blur', function(e) Grocy.Components.ProductPicker.GetPicker().on('select2:close', function() {
{ if (Grocy.Components.ProductPicker.PopupOpen || Grocy.Components.ProductPicker.GetPicker().select2('data').length > 0) return;
if (Grocy.Components.ProductPicker.GetPicker().hasClass("combobox-menu-visible"))
{
return;
}
$('#product_id').attr("barcode", "null");
var input = $('#product_id_text_input').val().toString();
var possibleOptionElement = [];
// grocycode handling
if (input.startsWith("grcy"))
{
var gc = input.split(":");
if (gc[1] == "p")
{
possibleOptionElement = $("#product_id option[value=\"" + gc[2] + "\"]").first();
$("#product_id").data("grocycode", true);
}
}
else // Normal product barcode handling
{
possibleOptionElement = $("#product_id option[data-additional-searchdata*=\"" + input + ",\"]").first();
}
if (GetUriParam('flow') === undefined && input.length > 0 && possibleOptionElement.length > 0)
{
$('#product_id').val(possibleOptionElement.val());
$('#product_id').attr("barcode", input);
$('#product_id').data('combobox').refresh();
$('#product_id').trigger('change');
}
else
{
if (Grocy.Components.ProductPicker.PopupOpen === true)
{
return;
}
var optionElement = $("#product_id option:contains(\"" + input + "\")").first();
if (input.length > 0 && optionElement.length === 0 && GetUriParam('flow') === undefined && Grocy.Components.ProductPicker.GetPicker().parent().data('disallow-all-product-workflows').toString() === "false")
{
var addProductWorkflowsAdditionalCssClasses = ""; var addProductWorkflowsAdditionalCssClasses = "";
if (Grocy.Components.ProductPicker.GetPicker().parent().data('disallow-add-product-workflows').toString() === "true") if (Grocy.Components.ProductPicker.GetOption('disallow-add-product-workflows')) {
{
addProductWorkflowsAdditionalCssClasses = "d-none"; addProductWorkflowsAdditionalCssClasses = "d-none";
} }
var embedded = ""; var embedded = "";
if (GetUriParam("embedded") !== undefined) if (GetUriParam("embedded") !== undefined) {
{
embedded = "embedded"; embedded = "embedded";
} }
@ -197,58 +343,51 @@ $('#product_id_text_input').on('blur', function(e)
cancel: { cancel: {
label: __t('Cancel'), label: __t('Cancel'),
className: 'btn-secondary responsive-button', className: 'btn-secondary responsive-button',
callback: function() callback: function() {
{
Grocy.Components.ProductPicker.PopupOpen = false; Grocy.Components.ProductPicker.PopupOpen = false;
Grocy.Components.ProductPicker.SetValue(''); Grocy.Components.ProductPicker.Clear();
} }
}, },
addnewproduct: { addnewproduct: {
label: '<strong>P</strong> ' + __t('Add as new product'), label: '<strong>P</strong> ' + __t('Add as new product'),
className: 'btn-success add-new-product-dialog-button responsive-button ' + addProductWorkflowsAdditionalCssClasses, className: 'btn-success add-new-product-dialog-button responsive-button ' + addProductWorkflowsAdditionalCssClasses,
callback: function() callback: function() {
{
// Not the best place here - this is only relevant when this flow is started from the shopping list item form // Not the best place here - this is only relevant when this flow is started from the shopping list item form
// (to select the correct shopping list on return) // (to select the correct shopping list on return)
if (GetUriParam("list") !== undefined) if (GetUriParam("list") !== undefined) {
{
embedded += "&list=" + GetUriParam("list"); embedded += "&list=" + GetUriParam("list");
} }
Grocy.Components.ProductPicker.PopupOpen = false; Grocy.Components.ProductPicker.PopupOpen = false;
window.location.href = U('/product/new?flow=InplaceNewProductWithName&name=' + encodeURIComponent(input) + '&returnto=' + encodeURIComponent(Grocy.CurrentUrlRelative + "?flow=InplaceNewProductWithName&" + embedded) + "&" + embedded); window.location.href = U('/product/new?flow=InplaceNewProductWithName&name=' + encodeURIComponent(lastProductSearchTerm) + '&returnto=' + encodeURIComponent(Grocy.CurrentUrlRelative + "?flow=InplaceNewProductWithName&" + embedded) + "&" + embedded);
} }
}, },
addbarcode: { addbarcode: {
label: '<strong>B</strong> ' + __t('Add as barcode to existing product'), label: '<strong>B</strong> ' + __t('Add as barcode to existing product'),
className: 'btn-info add-new-barcode-dialog-button responsive-button', className: 'btn-info add-new-barcode-dialog-button responsive-button',
callback: function() callback: function() {
{
Grocy.Components.ProductPicker.PopupOpen = false; Grocy.Components.ProductPicker.PopupOpen = false;
window.location.href = U(Grocy.CurrentUrlRelative + '?flow=InplaceAddBarcodeToExistingProduct&barcode=' + encodeURIComponent(input) + "&" + embedded); window.location.href = U(Grocy.CurrentUrlRelative + '?flow=InplaceAddBarcodeToExistingProduct&barcode=' + encodeURIComponent(lastProductSearchTerm) + "&" + embedded);
} }
}, },
addnewproductwithbarcode: { addnewproductwithbarcode: {
label: '<strong>A</strong> ' + __t('Add as new product and prefill barcode'), label: '<strong>A</strong> ' + __t('Add as new product and prefill barcode'),
className: 'btn-warning add-new-product-with-barcode-dialog-button responsive-button ' + addProductWorkflowsAdditionalCssClasses, className: 'btn-warning add-new-product-with-barcode-dialog-button responsive-button ' + addProductWorkflowsAdditionalCssClasses,
callback: function() callback: function() {
{
Grocy.Components.ProductPicker.PopupOpen = false; Grocy.Components.ProductPicker.PopupOpen = false;
window.location.href = U('/product/new?flow=InplaceNewProductWithBarcode&barcode=' + encodeURIComponent(input) + '&returnto=' + encodeURIComponent(Grocy.CurrentUrlRelative + "?flow=InplaceAddBarcodeToExistingProduct&barcode=" + input + "&" + embedded) + "&" + embedded); window.location.href = U('/product/new?flow=InplaceNewProductWithBarcode&barcode=' + encodeURIComponent(lastProductSearchTerm) + '&returnto=' + encodeURIComponent(Grocy.CurrentUrlRelative + "?flow=InplaceAddBarcodeToExistingProduct&barcode=" + lastProductSearchTerm + "&" + embedded) + "&" + embedded);
} }
} }
}; };
if (!Grocy.FeatureFlags.GROCY_FEATURE_FLAG_DISABLE_BROWSER_BARCODE_CAMERA_SCANNING) if (!Grocy.FeatureFlags.GROCY_FEATURE_FLAG_DISABLE_BROWSER_BARCODE_CAMERA_SCANNING) {
{
buttons.retrycamerascanning = { buttons.retrycamerascanning = {
label: '<strong>C</strong> <i class="fas fa-camera"></i>', label: '<strong>C</strong> <i class="fas fa-camera"></i>',
className: 'btn-primary responsive-button retry-camera-scanning-button', className: 'btn-primary responsive-button retry-camera-scanning-button',
callback: function() callback: function() {
{
Grocy.Components.ProductPicker.PopupOpen = false; Grocy.Components.ProductPicker.PopupOpen = false;
Grocy.Components.ProductPicker.SetValue(''); Grocy.Components.ProductPicker.Clear();
$("#barcodescanner-start-button").click(); $("#barcodescanner-start-button").trigger('click');
} }
}; };
} }
@ -259,119 +398,60 @@ $('#product_id_text_input').on('blur', function(e)
// otherwise an error validation message that the product is not in stock // otherwise an error validation message that the product is not in stock
var existsAsProduct = false; var existsAsProduct = false;
var existsAsBarcode = false; var existsAsBarcode = false;
Grocy.Api.Get('objects/product_barcodes?query[]=barcode=' + input, Grocy.Api.Get('objects/product_barcodes?query[]=barcode=' + lastProductSearchTerm,
function(barcodeResult) function(barcodeResult) {
{ if (barcodeResult.length > 0) {
if (barcodeResult.length > 0)
{
existsAsProduct = true; existsAsProduct = true;
} }
Grocy.Api.Get('objects/products?query[]=name=' + input, Grocy.Api.Get('objects/products?query[]=name=' + lastProductSearchTerm,
function(productResult) function(productResult) {
{ if (productResult.length > 0) {
if (productResult.length > 0)
{
existsAsProduct = true; existsAsProduct = true;
} }
if (!existsAsBarcode && !existsAsProduct) if (!existsAsBarcode && !existsAsProduct) {
{
Grocy.Components.ProductPicker.PopupOpen = true; Grocy.Components.ProductPicker.PopupOpen = true;
bootbox.dialog({ bootbox.dialog({
message: __t('"%s" could not be resolved to a product, how do you want to proceed?', input), message: __t('"%s" could not be resolved to a product, how do you want to proceed?', lastProductSearchTerm),
title: __t('Create or assign product'), title: __t('Create or assign product'),
onEscape: function() onEscape: function() {
{
Grocy.Components.ProductPicker.PopupOpen = false; Grocy.Components.ProductPicker.PopupOpen = false;
Grocy.Components.ProductPicker.SetValue(''); Grocy.Components.ProductPicker.Clear();
}, },
size: 'large', size: 'large',
backdrop: true, backdrop: true,
closeButton: false, closeButton: false,
buttons: buttons buttons: buttons
}).on('keypress', function(e) }).on('keypress', function(e) {
{ if (e.key === 'B' || e.key === 'b') {
if (e.key === 'B' || e.key === 'b') $('.add-new-barcode-dialog-button').not(".d-none").trigger('click');
{
$('.add-new-barcode-dialog-button').not(".d-none").click();
} }
if (e.key === 'p' || e.key === 'P') if (e.key === 'p' || e.key === 'P') {
{ $('.add-new-product-dialog-button').not(".d-none").trigger('click');
$('.add-new-product-dialog-button').not(".d-none").click();
} }
if (e.key === 'a' || e.key === 'A') if (e.key === 'a' || e.key === 'A') {
{ $('.add-new-product-with-barcode-dialog-button').not(".d-none").trigger('click');
$('.add-new-product-with-barcode-dialog-button').not(".d-none").click();
} }
if (e.key === 'c' || e.key === 'C') if (e.key === 'c' || e.key === 'C') {
{ $('.retry-camera-scanning-button').not(".d-none").trigger('click');
$('.retry-camera-scanning-button').not(".d-none").click();
} }
}); });
} } else {
else
{
Grocy.Components.ProductAmountPicker.Reset(); Grocy.Components.ProductAmountPicker.Reset();
Grocy.Components.ProductPicker.Clear(); Grocy.Components.ProductPicker.Clear();
Grocy.FrontendHelpers.ValidateForm('consume-form'); Grocy.FrontendHelpers.ValidateForm('consume-form');
Grocy.Components.ProductPicker.ShowCustomError(__t('This product is not in stock')); Grocy.Components.ProductPicker.ShowCustomError(__t('This product is not in stock'));
Grocy.Components.ProductPicker.GetInputElement().focus(); Grocy.Components.ProductPicker.Focus();
} }
}, },
function(xhr) function(xhr) {
{
console.error(xhr); console.error(xhr);
} }
); );
}, },
function(xhr) function(xhr) {
{
console.error(xhr); console.error(xhr);
} }
); );
}
}
});
$(document).on("Grocy.BarcodeScanned", function(e, barcode, target)
{
if (!(target == "@productpicker" || target == "undefined" || target == undefined)) // Default target
{
return;
}
// Don't know why the blur event does not fire immediately ... this works...
Grocy.Components.ProductPicker.GetInputElement().focusout();
Grocy.Components.ProductPicker.GetInputElement().focus();
Grocy.Components.ProductPicker.GetInputElement().blur();
Grocy.Components.ProductPicker.GetInputElement().val(barcode);
setTimeout(function()
{
Grocy.Components.ProductPicker.GetInputElement().focusout();
Grocy.Components.ProductPicker.GetInputElement().focus();
Grocy.Components.ProductPicker.GetInputElement().blur();
}, 200);
});
$(document).on("shown.bs.modal", function(e)
{
$(".modal-footer").addClass("d-block").addClass("d-sm-flex");
$(".modal-footer").find("button").addClass("mt-2").addClass("mt-sm-0");
})
// Make that ENTER behaves the same like TAB (trigger blur to start workflows, but only when the dropdown is not opened)
$('#product_id_text_input').keydown(function(event)
{
if (event.keyCode === 13) // Enter
{
if (Grocy.Components.ProductPicker.GetPicker().hasClass("combobox-menu-visible"))
{
return;
}
$("#product_id_text_input").trigger("blur");
}
}); });

View File

@ -1,9 +1,7 @@
$('#save-consume-button').on('click', function(e) $('#save-consume-button').on('click', function(e) {
{
e.preventDefault(); e.preventDefault();
if ($(".combobox-menu-visible").length) if ($(".combobox-menu-visible").length) {
{
return; return;
} }
@ -18,51 +16,42 @@
jsonData.spoiled = $('#spoiled').is(':checked'); jsonData.spoiled = $('#spoiled').is(':checked');
jsonData.allow_subproduct_substitution = true; jsonData.allow_subproduct_substitution = true;
if ($("#use_specific_stock_entry").is(":checked")) if ($("#use_specific_stock_entry").is(":checked")) {
{
jsonData.stock_entry_id = jsonForm.specific_stock_entry; jsonData.stock_entry_id = jsonForm.specific_stock_entry;
} }
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING) if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING) {
{
jsonData.location_id = $("#location_id").val(); jsonData.location_id = $("#location_id").val();
} }
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_RECIPES && Grocy.Components.RecipePicker.GetValue().toString().length > 0) if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_RECIPES && Grocy.Components.RecipePicker.GetValue().toString().length > 0) {
{
jsonData.recipe_id = Grocy.Components.RecipePicker.GetValue(); jsonData.recipe_id = Grocy.Components.RecipePicker.GetValue();
} }
var bookingResponse = null; var bookingResponse = null;
Grocy.Api.Get('stock/products/' + jsonForm.product_id, Grocy.Api.Get('stock/products/' + jsonForm.product_id,
function(productDetails) function(productDetails) {
{
Grocy.Api.Post(apiUrl, jsonData, Grocy.Api.Post(apiUrl, jsonData,
function(result) function(result) {
{ if (BoolVal(Grocy.UserSettings.scan_mode_consume_enabled)) {
if (BoolVal(Grocy.UserSettings.scan_mode_consume_enabled))
{
Grocy.UISound.Success(); Grocy.UISound.Success();
} }
bookingResponse = result; bookingResponse = result;
if (GetUriParam("flow") === "InplaceAddBarcodeToExistingProduct") if (GetUriParam("flow") === "InplaceAddBarcodeToExistingProduct") {
{
var jsonDataBarcode = {}; var jsonDataBarcode = {};
jsonDataBarcode.barcode = GetUriParam("barcode"); jsonDataBarcode.barcode = GetUriParam("barcode");
jsonDataBarcode.product_id = jsonForm.product_id; jsonDataBarcode.product_id = jsonForm.product_id;
Grocy.Api.Post('objects/product_barcodes', jsonDataBarcode, Grocy.Api.Post('objects/product_barcodes', jsonDataBarcode,
function(result) function(result) {
{
$("#flow-info-InplaceAddBarcodeToExistingProduct").addClass("d-none"); $("#flow-info-InplaceAddBarcodeToExistingProduct").addClass("d-none");
$('#barcode-lookup-disabled-hint').addClass('d-none'); $('#barcode-lookup-disabled-hint').addClass('d-none');
$('#barcode-lookup-hint').removeClass('d-none'); $('#barcode-lookup-hint').removeClass('d-none');
window.history.replaceState({}, document.title, U("/consume")); window.history.replaceState({}, document.title, U("/consume"));
}, },
function(xhr) function(xhr) {
{
Grocy.FrontendHelpers.EndUiBusy("consume-form"); Grocy.FrontendHelpers.EndUiBusy("consume-form");
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response); Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response);
} }
@ -70,28 +59,21 @@
} }
$("#specific_stock_entry").find("option").remove().end().append("<option></option>"); $("#specific_stock_entry").find("option").remove().end().append("<option></option>");
if ($("#use_specific_stock_entry").is(":checked")) if ($("#use_specific_stock_entry").is(":checked")) {
{
$("#use_specific_stock_entry").click(); $("#use_specific_stock_entry").click();
} }
if (productDetails.product.enable_tare_weight_handling == 1 && !jsonData.exact_amount) if (productDetails.product.enable_tare_weight_handling == 1 && !jsonData.exact_amount) {
{
var successMessage = __t('Removed %1$s of %2$s from stock', Math.abs(jsonForm.amount - (parseFloat(productDetails.product.tare_weight) + parseFloat(productDetails.stock_amount))) + " " + __n(jsonForm.amount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural, true), productDetails.product.name) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoStockTransaction(\'' + bookingResponse[0].transaction_id + '\')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>'; var successMessage = __t('Removed %1$s of %2$s from stock', Math.abs(jsonForm.amount - (parseFloat(productDetails.product.tare_weight) + parseFloat(productDetails.stock_amount))) + " " + __n(jsonForm.amount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural, true), productDetails.product.name) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoStockTransaction(\'' + bookingResponse[0].transaction_id + '\')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>';
} } else {
else
{
var successMessage = __t('Removed %1$s of %2$s from stock', Math.abs(jsonForm.amount) + " " + __n(jsonForm.amount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural, true), productDetails.product.name) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoStockTransaction(\'' + bookingResponse[0].transaction_id + '\')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>'; var successMessage = __t('Removed %1$s of %2$s from stock', Math.abs(jsonForm.amount) + " " + __n(jsonForm.amount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural, true), productDetails.product.name) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoStockTransaction(\'' + bookingResponse[0].transaction_id + '\')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>';
} }
if (GetUriParam("embedded") !== undefined) if (GetUriParam("embedded") !== undefined) {
{
window.parent.postMessage(WindowMessageBag("ProductChanged", jsonForm.product_id), Grocy.BaseUrl); window.parent.postMessage(WindowMessageBag("ProductChanged", jsonForm.product_id), Grocy.BaseUrl);
window.parent.postMessage(WindowMessageBag("ShowSuccessMessage", successMessage), Grocy.BaseUrl); window.parent.postMessage(WindowMessageBag("ShowSuccessMessage", successMessage), Grocy.BaseUrl);
window.parent.postMessage(WindowMessageBag("CloseAllModals"), Grocy.BaseUrl); window.parent.postMessage(WindowMessageBag("CloseAllModals"), Grocy.BaseUrl);
} } else {
else
{
Grocy.FrontendHelpers.EndUiBusy("consume-form"); Grocy.FrontendHelpers.EndUiBusy("consume-form");
toastr.success(successMessage); toastr.success(successMessage);
Grocy.Components.ProductPicker.FinishFlow(); Grocy.Components.ProductPicker.FinishFlow();
@ -99,54 +81,45 @@
Grocy.Components.ProductAmountPicker.Reset(); Grocy.Components.ProductAmountPicker.Reset();
$("#display_amount").attr("min", Grocy.DefaultMinAmount); $("#display_amount").attr("min", Grocy.DefaultMinAmount);
$("#display_amount").removeAttr("max"); $("#display_amount").removeAttr("max");
if (BoolVal(Grocy.UserSettings.stock_default_consume_amount_use_quick_consume_amount)) if (BoolVal(Grocy.UserSettings.stock_default_consume_amount_use_quick_consume_amount)) {
{
$('#display_amount').val(productDetails.product.quick_consume_amount); $('#display_amount').val(productDetails.product.quick_consume_amount);
} } else {
else
{
$('#display_amount').val(parseFloat(Grocy.UserSettings.stock_default_consume_amount)); $('#display_amount').val(parseFloat(Grocy.UserSettings.stock_default_consume_amount));
} }
RefreshLocaleNumberInput(); RefreshLocaleNumberInput();
$(".input-group-productamountpicker").trigger("change"); $(".input-group-productamountpicker").trigger("change");
$("#tare-weight-handling-info").addClass("d-none"); $("#tare-weight-handling-info").addClass("d-none");
Grocy.Components.ProductPicker.Clear(); Grocy.Components.ProductPicker.Clear();
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_RECIPES) if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_RECIPES) {
{
Grocy.Components.RecipePicker.Clear(); Grocy.Components.RecipePicker.Clear();
} }
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING) if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING) {
{
$("#location_id").find("option").remove().end().append("<option></option>"); $("#location_id").find("option").remove().end().append("<option></option>");
} }
Grocy.Components.ProductPicker.GetInputElement().focus(); Grocy.Components.ProductPicker.Focus();
Grocy.Components.ProductCard.Refresh(jsonForm.product_id); Grocy.Components.ProductCard.Refresh(jsonForm.product_id);
Grocy.FrontendHelpers.ValidateForm('consume-form'); Grocy.FrontendHelpers.ValidateForm('consume-form');
$("#consume-exact-amount-group").addClass("d-none"); $("#consume-exact-amount-group").addClass("d-none");
} }
}, },
function(xhr) function(xhr) {
{
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response); Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response);
Grocy.FrontendHelpers.EndUiBusy("consume-form"); Grocy.FrontendHelpers.EndUiBusy("consume-form");
console.error(xhr); console.error(xhr);
} }
); );
}, },
function(xhr) function(xhr) {
{
Grocy.FrontendHelpers.EndUiBusy("consume-form"); Grocy.FrontendHelpers.EndUiBusy("consume-form");
console.error(xhr); console.error(xhr);
} }
); );
}); });
$('#save-mark-as-open-button').on('click', function(e) $('#save-mark-as-open-button').on('click', function(e) {
{
e.preventDefault(); e.preventDefault();
if ($(".combobox-menu-visible").length) if ($(".combobox-menu-visible").length) {
{
return; return;
} }
@ -159,123 +132,98 @@ $('#save-mark-as-open-button').on('click', function(e)
jsonData.amount = jsonForm.amount; jsonData.amount = jsonForm.amount;
jsonData.allow_subproduct_substitution = true; jsonData.allow_subproduct_substitution = true;
if ($("#use_specific_stock_entry").is(":checked")) if ($("#use_specific_stock_entry").is(":checked")) {
{
jsonData.stock_entry_id = jsonForm.specific_stock_entry; jsonData.stock_entry_id = jsonForm.specific_stock_entry;
} }
Grocy.Api.Get('stock/products/' + jsonForm.product_id, Grocy.Api.Get('stock/products/' + jsonForm.product_id,
function(productDetails) function(productDetails) {
{
Grocy.Api.Post(apiUrl, jsonData, Grocy.Api.Post(apiUrl, jsonData,
function(result) function(result) {
{
$("#specific_stock_entry").find("option").remove().end().append("<option></option>"); $("#specific_stock_entry").find("option").remove().end().append("<option></option>");
if ($("#use_specific_stock_entry").is(":checked")) if ($("#use_specific_stock_entry").is(":checked")) {
{
$("#use_specific_stock_entry").click(); $("#use_specific_stock_entry").click();
} }
Grocy.FrontendHelpers.EndUiBusy("consume-form"); Grocy.FrontendHelpers.EndUiBusy("consume-form");
toastr.success(__t('Marked %1$s of %2$s as opened', parseFloat(jsonForm.amount).toLocaleString({ minimumFractionDigits: 0, maximumFractionDigits: Grocy.UserSettings.stock_decimal_places_amounts }) + " " + __n(jsonForm.amount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural, true), productDetails.product.name) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoStockTransaction(\'' + result[0].transaction_id + '\')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>'); toastr.success(__t('Marked %1$s of %2$s as opened', parseFloat(jsonForm.amount).toLocaleString({
minimumFractionDigits: 0,
maximumFractionDigits: Grocy.UserSettings.stock_decimal_places_amounts
}) + " " + __n(jsonForm.amount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural, true), productDetails.product.name) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoStockTransaction(\'' + result[0].transaction_id + '\')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>');
if (BoolVal(Grocy.UserSettings.stock_default_consume_amount_use_quick_consume_amount)) if (BoolVal(Grocy.UserSettings.stock_default_consume_amount_use_quick_consume_amount)) {
{
$('#display_amount').val(productDetails.product.quick_consume_amount); $('#display_amount').val(productDetails.product.quick_consume_amount);
} } else {
else
{
$('#display_amount').val(parseFloat(Grocy.UserSettings.stock_default_consume_amount)); $('#display_amount').val(parseFloat(Grocy.UserSettings.stock_default_consume_amount));
} }
RefreshLocaleNumberInput(); RefreshLocaleNumberInput();
$(".input-group-productamountpicker").trigger("change"); $(".input-group-productamountpicker").trigger("change");
Grocy.Components.ProductPicker.Clear(); Grocy.Components.ProductPicker.Clear();
Grocy.Components.ProductPicker.GetInputElement().focus(); Grocy.Components.ProductPicker.Focus();
Grocy.FrontendHelpers.ValidateForm('consume-form'); Grocy.FrontendHelpers.ValidateForm('consume-form');
}, },
function(xhr) function(xhr) {
{
Grocy.FrontendHelpers.EndUiBusy("consume-form"); Grocy.FrontendHelpers.EndUiBusy("consume-form");
console.error(xhr); console.error(xhr);
} }
); );
}, },
function(xhr) function(xhr) {
{
Grocy.FrontendHelpers.EndUiBusy("consume-form"); Grocy.FrontendHelpers.EndUiBusy("consume-form");
console.error(xhr); console.error(xhr);
} }
); );
}); });
var sumValue = 0; var sumValue = 0;
$("#location_id").on('change', function(e) $("#location_id").on('change', function(e) {
{
var locationId = $(e.target).val(); var locationId = $(e.target).val();
$("#specific_stock_entry").find("option").remove().end().append("<option></option>"); $("#specific_stock_entry").find("option").remove().end().append("<option></option>");
if ($("#use_specific_stock_entry").is(":checked")) if ($("#use_specific_stock_entry").is(":checked")) {
{ $("#use_specific_stock_entry").trigger('click');
$("#use_specific_stock_entry").click();
} }
if (GetUriParam("embedded") !== undefined) if (GetUriParam("embedded") !== undefined) {
{
OnLocationChange(locationId, GetUriParam('stockId')); OnLocationChange(locationId, GetUriParam('stockId'));
} } else {
else
{
// try to get stock id from grocycode // try to get stock id from grocycode
if ($("#product_id").data("grocycode")) if (Grocy.Components.ProductPicker.GetState('grocycode')) {
{ var gc = Grocy.Components.ProductPicker.GetState('barcode').split(':');
var gc = $("#product_id").attr("barcode").split(":"); if (gc.length == 4) {
if (gc.length == 4) Grocy.Api.Get("stock/products/" + Grocy.Components.ProductPicker.GetValue() + '/entries?query%5B%5D=stock_id%3D' + gc[3],
{ function(stockEntries) {
Grocy.Api.Get("stock/products/" + Grocy.Components.ProductPicker.GetValue() + '/entries?query[]=stock_id=' + gc[3],
function(stockEntries)
{
OnLocationChange(stockEntries[0].location_id, gc[3]); OnLocationChange(stockEntries[0].location_id, gc[3]);
$('#display_amount').val(stockEntries[0].amount); $('#display_amount').val(stockEntries[0].amount);
}, },
function(xhr) function(xhr) {
{
console.error(xhr); console.error(xhr);
} }
); );
} }
} } else {
else
{
OnLocationChange(locationId, null); OnLocationChange(locationId, null);
} }
} }
}); });
function OnLocationChange(locationId, stockId) function OnLocationChange(locationId, stockId) {
{
sumValue = 0; sumValue = 0;
if (locationId) if (locationId) {
{ if ($("#location_id").val() != locationId) {
if ($("#location_id").val() != locationId)
{
$("#location_id").val(locationId); $("#location_id").val(locationId);
} }
Grocy.Api.Get("stock/products/" + Grocy.Components.ProductPicker.GetValue() + '/entries?include_sub_products=true', Grocy.Api.Get("stock/products/" + Grocy.Components.ProductPicker.GetValue() + '/entries?include_sub_products=true',
function(stockEntries) function(stockEntries) {
{ stockEntries.forEach(stockEntry => {
stockEntries.forEach(stockEntry =>
{
var openTxt = __t("Not opened"); var openTxt = __t("Not opened");
if (stockEntry.open == 1) if (stockEntry.open == 1) {
{
openTxt = __t("Opened"); openTxt = __t("Opened");
} }
if (stockEntry.location_id == locationId) if (stockEntry.location_id == locationId) {
{ if ($("#specific_stock_entry option[value='" + stockEntry.stock_id + "']").length == 0) {
if ($("#specific_stock_entry option[value='" + stockEntry.stock_id + "']").length == 0)
{
$("#specific_stock_entry").append($("<option>", { $("#specific_stock_entry").append($("<option>", {
value: stockEntry.stock_id, value: stockEntry.stock_id,
amount: stockEntry.amount, amount: stockEntry.amount,
@ -285,8 +233,7 @@ function OnLocationChange(locationId, stockId)
sumValue = sumValue + parseFloat(stockEntry.amount || 0); sumValue = sumValue + parseFloat(stockEntry.amount || 0);
if (stockEntry.stock_id == stockId) if (stockEntry.stock_id == stockId) {
{
$("#use_specific_stock_entry").click(); $("#use_specific_stock_entry").click();
$("#specific_stock_entry").val(stockId); $("#specific_stock_entry").val(stockId);
} }
@ -294,63 +241,51 @@ function OnLocationChange(locationId, stockId)
}); });
Grocy.Api.Get('stock/products/' + Grocy.Components.ProductPicker.GetValue(), Grocy.Api.Get('stock/products/' + Grocy.Components.ProductPicker.GetValue(),
function(productDetails) function(productDetails) {
{
current_productDetails = productDetails; current_productDetails = productDetails;
RefreshForm(); RefreshForm();
}, },
function(xhr) function(xhr) {
{
console.error(xhr); console.error(xhr);
} }
); );
if (document.getElementById("product_id").getAttribute("barcode") == "null") if (document.getElementById("product_id").getAttribute("barcode") == "null") {
{
ScanModeSubmit(); ScanModeSubmit();
} }
}, },
function(xhr) function(xhr) {
{
console.error(xhr); console.error(xhr);
} }
); );
} }
} }
Grocy.Components.ProductPicker.GetPicker().on('change', function(e) Grocy.Components.ProductPicker.OnChange(function(e) {
{ if (BoolVal(Grocy.UserSettings.scan_mode_consume_enabled)) {
if (BoolVal(Grocy.UserSettings.scan_mode_consume_enabled))
{
Grocy.UISound.BarcodeScannerBeep(); Grocy.UISound.BarcodeScannerBeep();
} }
$("#specific_stock_entry").find("option").remove().end().append("<option></option>"); $("#specific_stock_entry").find("option").remove().end().append("<option></option>");
if ($("#use_specific_stock_entry").is(":checked")) if ($("#use_specific_stock_entry").is(":checked")) {
{
$("#use_specific_stock_entry").click(); $("#use_specific_stock_entry").click();
} }
$("#location_id").val(""); $("#location_id").val("");
var productId = $(e.target).val(); var productId = $(e.target).val();
if (productId) if (productId) {
{
Grocy.Components.ProductCard.Refresh(productId); Grocy.Components.ProductCard.Refresh(productId);
Grocy.Api.Get('stock/products/' + productId, Grocy.Api.Get('stock/products/' + productId,
function(productDetails) function(productDetails) {
{
current_productDetails = productDetails; current_productDetails = productDetails;
Grocy.Components.ProductAmountPicker.Reload(productDetails.product.id, productDetails.quantity_unit_stock.id); Grocy.Components.ProductAmountPicker.Reload(productDetails.product.id, productDetails.quantity_unit_stock.id);
Grocy.Components.ProductAmountPicker.SetQuantityUnit(productDetails.quantity_unit_stock.id); Grocy.Components.ProductAmountPicker.SetQuantityUnit(productDetails.quantity_unit_stock.id);
if (BoolVal(Grocy.UserSettings.stock_default_consume_amount_use_quick_consume_amount)) if (BoolVal(Grocy.UserSettings.stock_default_consume_amount_use_quick_consume_amount)) {
{
$('#display_amount').val(productDetails.product.quick_consume_amount); $('#display_amount').val(productDetails.product.quick_consume_amount);
} } else {
else
{
$('#display_amount').val(parseFloat(Grocy.UserSettings.stock_default_consume_amount)); $('#display_amount').val(parseFloat(Grocy.UserSettings.stock_default_consume_amount));
} }
RefreshLocaleNumberInput(); RefreshLocaleNumberInput();
@ -358,13 +293,10 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
$("#location_id").find("option").remove().end().append("<option></option>"); $("#location_id").find("option").remove().end().append("<option></option>");
Grocy.Api.Get("stock/products/" + productId + '/locations?include_sub_products=true', Grocy.Api.Get("stock/products/" + productId + '/locations?include_sub_products=true',
function(stockLocations) function(stockLocations) {
{
var setDefault = 0; var setDefault = 0;
stockLocations.forEach(stockLocation => stockLocations.forEach(stockLocation => {
{ if (productDetails.location.id == stockLocation.location_id) {
if (productDetails.location.id == stockLocation.location_id)
{
$("#location_id").append($("<option>", { $("#location_id").append($("<option>", {
value: stockLocation.location_id, value: stockLocation.location_id,
text: stockLocation.location_name + " (" + __t("Default location") + ")" text: stockLocation.location_name + " (" + __t("Default location") + ")"
@ -372,41 +304,32 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
$("#location_id").val(productDetails.location.id); $("#location_id").val(productDetails.location.id);
$("#location_id").trigger('change'); $("#location_id").trigger('change');
setDefault = 1; setDefault = 1;
} } else {
else
{
$("#location_id").append($("<option>", { $("#location_id").append($("<option>", {
value: stockLocation.location_id, value: stockLocation.location_id,
text: stockLocation.location_name text: stockLocation.location_name
})); }));
} }
if (setDefault == 0) if (setDefault == 0) {
{
$("#location_id").val(stockLocation.location_id); $("#location_id").val(stockLocation.location_id);
$("#location_id").trigger('change'); $("#location_id").trigger('change');
} }
}); });
if (document.getElementById("product_id").getAttribute("barcode") != "null") if (document.getElementById("product_id").getAttribute("barcode") != "null") {
{
Grocy.Api.Get('objects/product_barcodes?query[]=barcode=' + document.getElementById("product_id").getAttribute("barcode"), Grocy.Api.Get('objects/product_barcodes?query[]=barcode=' + document.getElementById("product_id").getAttribute("barcode"),
function(barcodeResult) function(barcodeResult) {
{ if (barcodeResult != null) {
if (barcodeResult != null)
{
var barcode = barcodeResult[0]; var barcode = barcodeResult[0];
if (barcode != null) if (barcode != null) {
{ if (barcode.amount != null && !barcode.amount.isEmpty()) {
if (barcode.amount != null && !barcode.amount.isEmpty())
{
$("#display_amount").val(barcode.amount); $("#display_amount").val(barcode.amount);
$("#display_amount").select(); $("#display_amount").select();
} }
if (barcode.qu_id != null && !barcode.qu_id.isEmpty()) if (barcode.qu_id != null && !barcode.qu_id.isEmpty()) {
{
Grocy.Components.ProductAmountPicker.SetQuantityUnit(barcode.qu_id); Grocy.Components.ProductAmountPicker.SetQuantityUnit(barcode.qu_id);
} }
@ -417,27 +340,22 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
} }
} }
}, },
function(xhr) function(xhr) {
{
console.error(xhr); console.error(xhr);
} }
); );
} }
}, },
function(xhr) function(xhr) {
{
console.error(xhr); console.error(xhr);
} }
); );
if (productDetails.product.enable_tare_weight_handling == 1) if (productDetails.product.enable_tare_weight_handling == 1) {
{
$("#display_amount").attr("min", productDetails.product.tare_weight); $("#display_amount").attr("min", productDetails.product.tare_weight);
$('#display_amount').attr('max', parseFloat(productDetails.stock_amount) + parseFloat(productDetails.product.tare_weight)); $('#display_amount').attr('max', parseFloat(productDetails.stock_amount) + parseFloat(productDetails.product.tare_weight));
$("#tare-weight-handling-info").removeClass("d-none"); $("#tare-weight-handling-info").removeClass("d-none");
} } else {
else
{
$("#display_amount").attr("min", Grocy.DefaultMinAmount); $("#display_amount").attr("min", Grocy.DefaultMinAmount);
$("#tare-weight-handling-info").addClass("d-none"); $("#tare-weight-handling-info").addClass("d-none");
} }
@ -446,17 +364,13 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
Grocy.FrontendHelpers.ValidateForm('consume-form'); Grocy.FrontendHelpers.ValidateForm('consume-form');
$('#display_amount').focus(); $('#display_amount').focus();
if (productDetails.stock_amount == productDetails.stock_amount_opened || productDetails.product.enable_tare_weight_handling == 1) if (productDetails.stock_amount == productDetails.stock_amount_opened || productDetails.product.enable_tare_weight_handling == 1) {
{
$("#save-mark-as-open-button").addClass("disabled"); $("#save-mark-as-open-button").addClass("disabled");
} } else {
else
{
$("#save-mark-as-open-button").removeClass("disabled"); $("#save-mark-as-open-button").removeClass("disabled");
} }
}, },
function(xhr) function(xhr) {
{
console.error(xhr); console.error(xhr);
} }
); );
@ -467,29 +381,24 @@ $('#display_amount').val(parseFloat(Grocy.UserSettings.stock_default_consume_amo
$(".input-group-productamountpicker").trigger("change"); $(".input-group-productamountpicker").trigger("change");
Grocy.FrontendHelpers.ValidateForm('consume-form'); Grocy.FrontendHelpers.ValidateForm('consume-form');
$('#display_amount').on('focus', function(e) $('#display_amount').on('focus', function(e) {
{
$(this).select(); $(this).select();
}); });
$('#price').on('focus', function(e) $('#price').on('focus', function(e) {
{
$(this).select(); $(this).select();
}); });
$('#consume-form input').keyup(function(event) $('#consume-form input').keyup(function(event) {
{
Grocy.FrontendHelpers.ValidateForm('consume-form'); Grocy.FrontendHelpers.ValidateForm('consume-form');
}); });
$('#consume-form select').change(function(event) $('#consume-form select').change(function(event) {
{
Grocy.FrontendHelpers.ValidateForm('consume-form'); Grocy.FrontendHelpers.ValidateForm('consume-form');
}); });
$('#consume-form input').keydown(function(event) $('#consume-form input').keydown(function(event) {
{
if (event.keyCode === 13) //Enter if (event.keyCode === 13) //Enter
{ {
event.preventDefault(); event.preventDefault();
@ -497,58 +406,43 @@ $('#consume-form input').keydown(function(event)
if (document.getElementById('consume-form').checkValidity() === false) //There is at least one validation error if (document.getElementById('consume-form').checkValidity() === false) //There is at least one validation error
{ {
return false; return false;
} } else {
else
{
$('#save-consume-button').click(); $('#save-consume-button').click();
} }
} }
}); });
$("#specific_stock_entry").on("change", function(e) $("#specific_stock_entry").on("change", function(e) {
{ if ($(e.target).val() == "") {
if ($(e.target).val() == "")
{
sumValue = 0; sumValue = 0;
Grocy.Api.Get("stock/products/" + Grocy.Components.ProductPicker.GetValue() + '/entries?include_sub_products=true', Grocy.Api.Get("stock/products/" + Grocy.Components.ProductPicker.GetValue() + '/entries?include_sub_products=true',
function(stockEntries) function(stockEntries) {
{ stockEntries.forEach(stockEntry => {
stockEntries.forEach(stockEntry => if (stockEntry.location_id == $("#location_id").val() || stockEntry.location_id == "") {
{
if (stockEntry.location_id == $("#location_id").val() || stockEntry.location_id == "")
{
sumValue = sumValue + parseFloat(stockEntry.amount_aggregated); sumValue = sumValue + parseFloat(stockEntry.amount_aggregated);
} }
}); });
$("#display_amount").attr("max", sumValue); $("#display_amount").attr("max", sumValue);
if (sumValue == 0) if (sumValue == 0) {
{
$("#display_amount").parent().find(".invalid-feedback").text(__t('There are no units available at this location')); $("#display_amount").parent().find(".invalid-feedback").text(__t('There are no units available at this location'));
} }
}, },
function(xhr) function(xhr) {
{
console.error(xhr); console.error(xhr);
} }
); );
} } else {
else
{
$("#display_amount").attr("max", $('option:selected', this).attr('amount')); $("#display_amount").attr("max", $('option:selected', this).attr('amount'));
} }
}); });
$("#use_specific_stock_entry").on("change", function() $("#use_specific_stock_entry").on("change", function() {
{
var value = $(this).is(":checked"); var value = $(this).is(":checked");
if (value) if (value) {
{
$("#specific_stock_entry").removeAttr("disabled"); $("#specific_stock_entry").removeAttr("disabled");
$("#specific_stock_entry").attr("required", ""); $("#specific_stock_entry").attr("required", "");
} } else {
else
{
$("#specific_stock_entry").attr("disabled", ""); $("#specific_stock_entry").attr("disabled", "");
$("#specific_stock_entry").removeAttr("required"); $("#specific_stock_entry").removeAttr("required");
$("#specific_stock_entry").val(""); $("#specific_stock_entry").val("");
@ -558,146 +452,114 @@ $("#use_specific_stock_entry").on("change", function()
Grocy.FrontendHelpers.ValidateForm("consume-form"); Grocy.FrontendHelpers.ValidateForm("consume-form");
}); });
$("#qu_id").on("change", function() $("#qu_id").on("change", function() {
{
RefreshForm(); RefreshForm();
}); });
function UndoStockBooking(bookingId) function UndoStockBooking(bookingId) {
{
Grocy.Api.Post('stock/bookings/' + bookingId.toString() + '/undo', {}, Grocy.Api.Post('stock/bookings/' + bookingId.toString() + '/undo', {},
function(result) function(result) {
{
toastr.success(__t("Booking successfully undone")); toastr.success(__t("Booking successfully undone"));
}, },
function(xhr) function(xhr) {
{
console.error(xhr); console.error(xhr);
} }
); );
}; };
function UndoStockTransaction(transactionId) function UndoStockTransaction(transactionId) {
{
Grocy.Api.Post('stock/transactions/' + transactionId.toString() + '/undo', {}, Grocy.Api.Post('stock/transactions/' + transactionId.toString() + '/undo', {},
function(result) function(result) {
{
toastr.success(__t("Transaction successfully undone")); toastr.success(__t("Transaction successfully undone"));
}, },
function(xhr) function(xhr) {
{
console.error(xhr); console.error(xhr);
} }
); );
}; };
if (GetUriParam("embedded") !== undefined) if (GetUriParam("embedded") !== undefined) {
{
var locationId = GetUriParam('locationId'); var locationId = GetUriParam('locationId');
if (typeof locationId === 'undefined') if (typeof locationId === 'undefined') {
{ Grocy.Components.ProductPicker.Validate();
Grocy.Components.ProductPicker.GetPicker().trigger('change'); } else {
}
else
{
$("#location_id").val(locationId); $("#location_id").val(locationId);
$("#location_id").trigger('change'); $("#location_id").trigger('change');
$("#use_specific_stock_entry").click(); $("#use_specific_stock_entry").trigger('click');
$("#use_specific_stock_entry").trigger('change'); $("#use_specific_stock_entry").trigger('change');
Grocy.Components.ProductPicker.GetPicker().trigger('change'); Grocy.Components.ProductPicker.Validate();
} }
} }
// Default input field // Default input field
Grocy.Components.ProductPicker.GetInputElement().focus(); Grocy.Components.ProductPicker.Focus();
$(document).on("change", "#scan-mode", function(e) $(document).on("change", "#scan-mode", function(e) {
{ if ($(this).prop("checked")) {
if ($(this).prop("checked"))
{
Grocy.UISound.AskForPermission(); Grocy.UISound.AskForPermission();
} }
}); });
$("#scan-mode-button").on("click", function(e) $("#scan-mode-button").on("click", function(e) {
{
document.activeElement.blur(); document.activeElement.blur();
$("#scan-mode").click(); $("#scan-mode").trigger('click');
$("#scan-mode-button").toggleClass("btn-success").toggleClass("btn-danger"); $("#scan-mode-button").toggleClass("btn-success").toggleClass("btn-danger");
if ($("#scan-mode").prop("checked")) if ($("#scan-mode").prop("checked")) {
{
$("#scan-mode-status").text(__t("on")); $("#scan-mode-status").text(__t("on"));
} } else {
else
{
$("#scan-mode-status").text(__t("off")); $("#scan-mode-status").text(__t("off"));
} }
}); });
$('#consume-exact-amount').on('change', RefreshForm); $('#consume-exact-amount').on('change', RefreshForm);
var current_productDetails; var current_productDetails;
function RefreshForm()
{ function RefreshForm() {
var productDetails = current_productDetails; var productDetails = current_productDetails;
if (!productDetails) if (!productDetails) {
{
return; return;
} }
if (productDetails.product.enable_tare_weight_handling == 1) if (productDetails.product.enable_tare_weight_handling == 1) {
{
$("#consume-exact-amount-group").removeClass("d-none"); $("#consume-exact-amount-group").removeClass("d-none");
} } else {
else
{
$("#consume-exact-amount-group").addClass("d-none"); $("#consume-exact-amount-group").addClass("d-none");
} }
if (productDetails.product.enable_tare_weight_handling == 1 && !$('#consume-exact-amount').is(':checked')) if (productDetails.product.enable_tare_weight_handling == 1 && !$('#consume-exact-amount').is(':checked')) {
{
$("#display_amount").attr("min", productDetails.product.tare_weight); $("#display_amount").attr("min", productDetails.product.tare_weight);
$('#display_amount').attr('max', sumValue + parseFloat(productDetails.product.tare_weight)); $('#display_amount').attr('max', sumValue + parseFloat(productDetails.product.tare_weight));
$("#tare-weight-handling-info").removeClass("d-none"); $("#tare-weight-handling-info").removeClass("d-none");
} } else {
else
{
$("#tare-weight-handling-info").addClass("d-none"); $("#tare-weight-handling-info").addClass("d-none");
$("#display_amount").attr("min", Grocy.DefaultMinAmount); $("#display_amount").attr("min", Grocy.DefaultMinAmount);
$('#display_amount').attr('max', sumValue * $("#qu_id option:selected").attr("data-qu-factor")); $('#display_amount').attr('max', sumValue * $("#qu_id option:selected").attr("data-qu-factor"));
if (sumValue == 0) if (sumValue == 0) {
{
$("#display_amount").parent().find(".invalid-feedback").text(__t('There are no units available at this location')); $("#display_amount").parent().find(".invalid-feedback").text(__t('There are no units available at this location'));
} }
} }
if (productDetails.has_childs) if (productDetails.has_childs) {
{
$("#display_amount").removeAttr("max"); $("#display_amount").removeAttr("max");
} }
Grocy.FrontendHelpers.ValidateForm("consume-form"); Grocy.FrontendHelpers.ValidateForm("consume-form");
} }
function ScanModeSubmit(singleUnit = true) function ScanModeSubmit(singleUnit = true) {
{ if (BoolVal(Grocy.UserSettings.scan_mode_consume_enabled)) {
if (BoolVal(Grocy.UserSettings.scan_mode_consume_enabled)) if (singleUnit) {
{
if (singleUnit)
{
$("#display_amount").val(1); $("#display_amount").val(1);
$(".input-group-productamountpicker").trigger("change"); $(".input-group-productamountpicker").trigger("change");
} }
Grocy.FrontendHelpers.ValidateForm("consume-form"); Grocy.FrontendHelpers.ValidateForm("consume-form");
if (document.getElementById("consume-form").checkValidity() === true) if (document.getElementById("consume-form").checkValidity() === true) {
{
$('#save-consume-button').click(); $('#save-consume-button').click();
} } else {
else
{
toastr.warning(__t("Scan mode is on but not all required fields could be populated automatically")); toastr.warning(__t("Scan mode is on but not all required fields could be populated automatically"));
Grocy.UISound.Error(); Grocy.UISound.Error();
} }

View File

@ -1,9 +1,7 @@
$('#save-inventory-button').on('click', function(e) $('#save-inventory-button').on('click', function(e) {
{
e.preventDefault(); e.preventDefault();
if ($(".combobox-menu-visible").length) if ($(".combobox-menu-visible").length) {
{
return; return;
} }
@ -11,11 +9,9 @@
Grocy.FrontendHelpers.BeginUiBusy("inventory-form"); Grocy.FrontendHelpers.BeginUiBusy("inventory-form");
Grocy.Api.Get('stock/products/' + jsonForm.product_id, Grocy.Api.Get('stock/products/' + jsonForm.product_id,
function(productDetails) function(productDetails) {
{
var price = ""; var price = "";
if (!jsonForm.price.toString().isEmpty()) if (!jsonForm.price.toString().isEmpty()) {
{
price = parseFloat(jsonForm.price).toFixed(Grocy.UserSettings.stock_decimal_places_prices); price = parseFloat(jsonForm.price).toFixed(Grocy.UserSettings.stock_decimal_places_prices);
} }
@ -23,16 +19,13 @@
jsonData.new_amount = jsonForm.amount; jsonData.new_amount = jsonForm.amount;
jsonData.best_before_date = Grocy.Components.DateTimePicker.GetValue(); jsonData.best_before_date = Grocy.Components.DateTimePicker.GetValue();
jsonData.stock_label_type = jsonForm.stock_label_type; jsonData.stock_label_type = jsonForm.stock_label_type;
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) {
{
jsonData.shopping_location_id = Grocy.Components.ShoppingLocationPicker.GetValue(); jsonData.shopping_location_id = Grocy.Components.ShoppingLocationPicker.GetValue();
} }
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING) if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING) {
{
jsonData.location_id = Grocy.Components.LocationPicker.GetValue(); jsonData.location_id = Grocy.Components.LocationPicker.GetValue();
} }
if (Grocy.UserSettings.show_purchased_date_on_purchase) if (Grocy.UserSettings.show_purchased_date_on_purchase) {
{
jsonData.purchased_date = Grocy.Components.DateTimePicker2.GetValue(); jsonData.purchased_date = Grocy.Components.DateTimePicker2.GetValue();
} }
@ -41,69 +34,57 @@
var bookingResponse = null; var bookingResponse = null;
Grocy.Api.Post('stock/products/' + jsonForm.product_id + '/inventory', jsonData, Grocy.Api.Post('stock/products/' + jsonForm.product_id + '/inventory', jsonData,
function(result) function(result) {
{
bookingResponse = result; bookingResponse = result;
if (GetUriParam("flow") === "InplaceAddBarcodeToExistingProduct") if (GetUriParam("flow") === "InplaceAddBarcodeToExistingProduct") {
{
var jsonDataBarcode = {}; var jsonDataBarcode = {};
jsonDataBarcode.barcode = GetUriParam("barcode"); jsonDataBarcode.barcode = GetUriParam("barcode");
jsonDataBarcode.product_id = jsonForm.product_id; jsonDataBarcode.product_id = jsonForm.product_id;
jsonDataBarcode.shopping_location_id = jsonForm.shopping_location_id; jsonDataBarcode.shopping_location_id = jsonForm.shopping_location_id;
Grocy.Api.Post('objects/product_barcodes', jsonDataBarcode, Grocy.Api.Post('objects/product_barcodes', jsonDataBarcode,
function(result) function(result) {
{
$("#flow-info-InplaceAddBarcodeToExistingProduct").addClass("d-none"); $("#flow-info-InplaceAddBarcodeToExistingProduct").addClass("d-none");
$('#barcode-lookup-disabled-hint').addClass('d-none'); $('#barcode-lookup-disabled-hint').addClass('d-none');
$('#barcode-lookup-hint').removeClass('d-none'); $('#barcode-lookup-hint').removeClass('d-none');
window.history.replaceState({}, document.title, U("/inventory")); window.history.replaceState({}, document.title, U("/inventory"));
}, },
function(xhr) function(xhr) {
{
Grocy.FrontendHelpers.EndUiBusy("inventory-form"); Grocy.FrontendHelpers.EndUiBusy("inventory-form");
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response); Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response);
} }
); );
} }
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_LABEL_PRINTER && parseFloat($("#display_amount").attr("data-estimated-booking-amount")) > 0) if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_LABEL_PRINTER && parseFloat($("#display_amount").attr("data-estimated-booking-amount")) > 0) {
{ if (Grocy.Webhooks.labelprinter !== undefined) {
if (Grocy.Webhooks.labelprinter !== undefined)
{
if (jsonForm.stock_label_type == 1) // Single label if (jsonForm.stock_label_type == 1) // Single label
{ {
var webhookData = {}; var webhookData = {};
webhookData.product = productDetails.product.name; webhookData.product = productDetails.product.name;
webhookData.grocycode = 'grcy:p:' + jsonForm.product_id + ":" + result[0].stock_id; webhookData.grocycode = 'grcy:p:' + jsonForm.product_id + ":" + result[0].stock_id;
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING) if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING) {
{
webhookData.due_date = __t('DD') + ': ' + result[0].best_before_date; webhookData.due_date = __t('DD') + ': ' + result[0].best_before_date;
} }
Grocy.FrontendHelpers.RunWebhook(Grocy.Webhooks.labelprinter, webhookData); Grocy.FrontendHelpers.RunWebhook(Grocy.Webhooks.labelprinter, webhookData);
} } else if (jsonForm.stock_label_type == 2) // Label per unit
else if (jsonForm.stock_label_type == 2) // Label per unit
{ {
Grocy.Api.Get('stock/transactions/' + result[0].transaction_id, Grocy.Api.Get('stock/transactions/' + result[0].transaction_id,
function(stockEntries) function(stockEntries) {
{ stockEntries.forEach(stockEntry => {
stockEntries.forEach(stockEntry =>
{
var webhookData = {}; var webhookData = {};
webhookData.product = productDetails.product.name; webhookData.product = productDetails.product.name;
webhookData.grocycode = 'grcy:p:' + jsonForm.product_id + ":" + stockEntry.stock_id; webhookData.grocycode = 'grcy:p:' + jsonForm.product_id + ":" + stockEntry.stock_id;
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING) if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING) {
{
webhookData.due_date = __t('DD') + ': ' + result[0].best_before_date; webhookData.due_date = __t('DD') + ': ' + result[0].best_before_date;
} }
Grocy.FrontendHelpers.RunWebhook(Grocy.Webhooks.labelprinter, webhookData); Grocy.FrontendHelpers.RunWebhook(Grocy.Webhooks.labelprinter, webhookData);
}); });
}, },
function(xhr) function(xhr) {
{
console.error(xhr); console.error(xhr);
} }
); );
@ -112,18 +93,14 @@
} }
Grocy.Api.Get('stock/products/' + jsonForm.product_id, Grocy.Api.Get('stock/products/' + jsonForm.product_id,
function(result) function(result) {
{
var successMessage = __t('Stock amount of %1$s is now %2$s', result.product.name, result.stock_amount + " " + __n(result.stock_amount, result.quantity_unit_stock.name, result.quantity_unit_stock.name_plural, true)) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoStockTransaction(\'' + bookingResponse[0].transaction_id + '\')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>'; var successMessage = __t('Stock amount of %1$s is now %2$s', result.product.name, result.stock_amount + " " + __n(result.stock_amount, result.quantity_unit_stock.name, result.quantity_unit_stock.name_plural, true)) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoStockTransaction(\'' + bookingResponse[0].transaction_id + '\')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>';
if (GetUriParam("embedded") !== undefined) if (GetUriParam("embedded") !== undefined) {
{
window.parent.postMessage(WindowMessageBag("ProductChanged", jsonForm.product_id), Grocy.BaseUrl); window.parent.postMessage(WindowMessageBag("ProductChanged", jsonForm.product_id), Grocy.BaseUrl);
window.parent.postMessage(WindowMessageBag("ShowSuccessMessage", successMessage), Grocy.BaseUrl); window.parent.postMessage(WindowMessageBag("ShowSuccessMessage", successMessage), Grocy.BaseUrl);
window.parent.postMessage(WindowMessageBag("CloseAllModals"), Grocy.BaseUrl); window.parent.postMessage(WindowMessageBag("CloseAllModals"), Grocy.BaseUrl);
} } else {
else
{
Grocy.FrontendHelpers.EndUiBusy("inventory-form"); Grocy.FrontendHelpers.EndUiBusy("inventory-form");
toastr.success(successMessage); toastr.success(successMessage);
Grocy.Components.ProductPicker.FinishFlow(); Grocy.Components.ProductPicker.FinishFlow();
@ -137,125 +114,99 @@
$(".input-group-productamountpicker").trigger("change"); $(".input-group-productamountpicker").trigger("change");
$('#price').val(''); $('#price').val('');
Grocy.Components.DateTimePicker.Clear(); Grocy.Components.DateTimePicker.Clear();
Grocy.Components.ProductPicker.SetValue(''); Grocy.Components.ProductPicker.Clear();
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) {
{
Grocy.Components.ShoppingLocationPicker.SetValue(''); Grocy.Components.ShoppingLocationPicker.SetValue('');
} }
Grocy.Components.ProductPicker.GetInputElement().focus(); Grocy.Components.ProductPicker.Focus();
Grocy.Components.ProductCard.Refresh(jsonForm.product_id); Grocy.Components.ProductCard.Refresh(jsonForm.product_id);
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_LABEL_PRINTER) if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_LABEL_PRINTER) {
{
$("#stock_label_type").val(0); $("#stock_label_type").val(0);
} }
Grocy.FrontendHelpers.ValidateForm('inventory-form'); Grocy.FrontendHelpers.ValidateForm('inventory-form');
} }
}, },
function(xhr) function(xhr) {
{
Grocy.FrontendHelpers.EndUiBusy(); Grocy.FrontendHelpers.EndUiBusy();
console.error(xhr); console.error(xhr);
} }
); );
}, },
function(xhr) function(xhr) {
{
Grocy.FrontendHelpers.EndUiBusy("inventory-form"); Grocy.FrontendHelpers.EndUiBusy("inventory-form");
console.error(xhr); console.error(xhr);
} }
); );
}, },
function(xhr) function(xhr) {
{
Grocy.FrontendHelpers.EndUiBusy("inventory-form"); Grocy.FrontendHelpers.EndUiBusy("inventory-form");
console.error(xhr); console.error(xhr);
} }
); );
}); });
Grocy.Components.ProductPicker.GetPicker().on('change', function(e) Grocy.Components.ProductPicker.OnChange(function(e) {
{
var productId = $(e.target).val(); var productId = $(e.target).val();
if (productId) if (productId) {
{
Grocy.Components.ProductCard.Refresh(productId); Grocy.Components.ProductCard.Refresh(productId);
Grocy.Api.Get('stock/products/' + productId, Grocy.Api.Get('stock/products/' + productId,
function(productDetails) function(productDetails) {
{
Grocy.Components.ProductAmountPicker.Reload(productDetails.product.id, productDetails.quantity_unit_stock.id); Grocy.Components.ProductAmountPicker.Reload(productDetails.product.id, productDetails.quantity_unit_stock.id);
Grocy.Components.ProductAmountPicker.SetQuantityUnit(productDetails.quantity_unit_stock.id); Grocy.Components.ProductAmountPicker.SetQuantityUnit(productDetails.quantity_unit_stock.id);
$('#display_amount').attr("data-stock-amount", productDetails.stock_amount) $('#display_amount').attr("data-stock-amount", productDetails.stock_amount)
$('#display_amount').attr('data-not-equal', productDetails.stock_amount * $("#qu_id option:selected").attr("data-qu-factor")); $('#display_amount').attr('data-not-equal', productDetails.stock_amount * $("#qu_id option:selected").attr("data-qu-factor"));
if (productDetails.product.enable_tare_weight_handling == 1) if (productDetails.product.enable_tare_weight_handling == 1) {
{
$("#display_amount").attr("min", productDetails.product.tare_weight); $("#display_amount").attr("min", productDetails.product.tare_weight);
$("#tare-weight-handling-info").removeClass("d-none"); $("#tare-weight-handling-info").removeClass("d-none");
} } else {
else
{
$("#display_amount").attr("min", "0"); $("#display_amount").attr("min", "0");
$("#tare-weight-handling-info").addClass("d-none"); $("#tare-weight-handling-info").addClass("d-none");
} }
$('#price').val(parseFloat(productDetails.last_price)); $('#price').val(parseFloat(productDetails.last_price));
RefreshLocaleNumberInput(); RefreshLocaleNumberInput();
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) {
{
Grocy.Components.ShoppingLocationPicker.SetId(productDetails.last_shopping_location_id); Grocy.Components.ShoppingLocationPicker.SetId(productDetails.last_shopping_location_id);
} }
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING) if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING) {
{
Grocy.Components.LocationPicker.SetId(productDetails.location.id); Grocy.Components.LocationPicker.SetId(productDetails.location.id);
} }
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING) if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING) {
{ if (productDetails.product.default_best_before_days.toString() !== '0') {
if (productDetails.product.default_best_before_days.toString() !== '0') if (productDetails.product.default_best_before_days == -1) {
{ if (!$("#datetimepicker-shortcut").is(":checked")) {
if (productDetails.product.default_best_before_days == -1)
{
if (!$("#datetimepicker-shortcut").is(":checked"))
{
$("#datetimepicker-shortcut").click(); $("#datetimepicker-shortcut").click();
} }
} } else {
else
{
Grocy.Components.DateTimePicker.SetValue(moment().add(productDetails.product.default_best_before_days, 'days').format('YYYY-MM-DD')); Grocy.Components.DateTimePicker.SetValue(moment().add(productDetails.product.default_best_before_days, 'days').format('YYYY-MM-DD'));
} }
} }
} }
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_LABEL_PRINTER) if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_LABEL_PRINTER) {
{
$("#stock_label_type").val(productDetails.product.default_stock_label_type); $("#stock_label_type").val(productDetails.product.default_stock_label_type);
} }
if (document.getElementById("product_id").getAttribute("barcode") != "null") if (document.getElementById("product_id").getAttribute("barcode") != "null") {
{
Grocy.Api.Get('objects/product_barcodes?query[]=barcode=' + document.getElementById("product_id").getAttribute("barcode"), Grocy.Api.Get('objects/product_barcodes?query[]=barcode=' + document.getElementById("product_id").getAttribute("barcode"),
function(barcodeResult) function(barcodeResult) {
{ if (barcodeResult != null) {
if (barcodeResult != null)
{
var barcode = barcodeResult[0]; var barcode = barcodeResult[0];
if (barcode != null) if (barcode != null) {
{ if (barcode.amount != null && !barcode.amount.isEmpty()) {
if (barcode.amount != null && !barcode.amount.isEmpty())
{
$("#display_amount").val(barcode.amount); $("#display_amount").val(barcode.amount);
$("#display_amount").select(); $("#display_amount").select();
} }
if (barcode.qu_id != null && !barcode.qu_id.isEmpty()) if (barcode.qu_id != null && !barcode.qu_id.isEmpty()) {
{
Grocy.Components.ProductAmountPicker.SetQuantityUnit(barcode.qu_id); Grocy.Components.ProductAmountPicker.SetQuantityUnit(barcode.qu_id);
} }
@ -265,8 +216,7 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
} }
} }
}, },
function(xhr) function(xhr) {
{
console.error(xhr); console.error(xhr);
} }
); );
@ -278,8 +228,7 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
$('#display_amount').focus(); $('#display_amount').focus();
$('#display_amount').trigger('keyup'); $('#display_amount').trigger('keyup');
}, },
function(xhr) function(xhr) {
{
console.error(xhr); console.error(xhr);
} }
); );
@ -290,39 +239,29 @@ $('#display_amount').val('');
$(".input-group-productamountpicker").trigger("change"); $(".input-group-productamountpicker").trigger("change");
Grocy.FrontendHelpers.ValidateForm('inventory-form'); Grocy.FrontendHelpers.ValidateForm('inventory-form');
if (Grocy.Components.ProductPicker.InAnyFlow() === false && GetUriParam("embedded") === undefined) if (Grocy.Components.ProductPicker.InAnyFlow() === false && GetUriParam("embedded") === undefined) {
{ Grocy.Components.ProductPicker.Focus();
Grocy.Components.ProductPicker.GetInputElement().focus(); } else {
} Grocy.Components.ProductPicker.Validate();
else
{
Grocy.Components.ProductPicker.GetPicker().trigger('change');
if (Grocy.Components.ProductPicker.InProductModifyWorkflow()) if (Grocy.Components.ProductPicker.InProductModifyWorkflow()) {
{ Grocy.Components.ProductPicker.Focus();
Grocy.Components.ProductPicker.GetInputElement().focus();
} }
} }
$('#display_amount').on('focus', function(e) $('#display_amount').on('focus', function(e) {
{ if (Grocy.Components.ProductPicker.GetValue().length === 0) {
if (Grocy.Components.ProductPicker.GetValue().length === 0) Grocy.Components.ProductPicker.Focus();
{ } else {
Grocy.Components.ProductPicker.GetInputElement().focus();
}
else
{
$(this).select(); $(this).select();
} }
}); });
$('#inventory-form input').keyup(function(event) $('#inventory-form input').keyup(function(event) {
{
Grocy.FrontendHelpers.ValidateForm('inventory-form'); Grocy.FrontendHelpers.ValidateForm('inventory-form');
}); });
$('#inventory-form input').keydown(function(event) $('#inventory-form input').keydown(function(event) {
{
if (event.keyCode === 13) //Enter if (event.keyCode === 13) //Enter
{ {
event.preventDefault(); event.preventDefault();
@ -330,46 +269,37 @@ $('#inventory-form input').keydown(function(event)
if (document.getElementById('inventory-form').checkValidity() === false) //There is at least one validation error if (document.getElementById('inventory-form').checkValidity() === false) //There is at least one validation error
{ {
return false; return false;
} } else {
else
{
$('#save-inventory-button').click(); $('#save-inventory-button').click();
} }
} }
}); });
$('#qu_id').on('change', function(e) $('#qu_id').on('change', function(e) {
{
$('#display_amount').attr('data-not-equal', parseFloat($('#display_amount').attr('data-stock-amount')) * parseFloat($("#qu_id option:selected").attr("data-qu-factor"))); $('#display_amount').attr('data-not-equal', parseFloat($('#display_amount').attr('data-stock-amount')) * parseFloat($("#qu_id option:selected").attr("data-qu-factor")));
Grocy.FrontendHelpers.ValidateForm('inventory-form'); Grocy.FrontendHelpers.ValidateForm('inventory-form');
}); });
Grocy.Components.DateTimePicker.GetInputElement().on('change', function(e) Grocy.Components.DateTimePicker.GetInputElement().on('change', function(e) {
{
Grocy.FrontendHelpers.ValidateForm('inventory-form'); Grocy.FrontendHelpers.ValidateForm('inventory-form');
}); });
Grocy.Components.DateTimePicker.GetInputElement().on('keypress', function(e) Grocy.Components.DateTimePicker.GetInputElement().on('keypress', function(e) {
{
Grocy.FrontendHelpers.ValidateForm('inventory-form'); Grocy.FrontendHelpers.ValidateForm('inventory-form');
}); });
$('#display_amount').on('keyup', function(e) $('#display_amount').on('keyup', function(e) {
{
var productId = Grocy.Components.ProductPicker.GetValue(); var productId = Grocy.Components.ProductPicker.GetValue();
var newAmount = parseFloat($('#amount').val()); var newAmount = parseFloat($('#amount').val());
if (productId) if (productId) {
{
Grocy.Api.Get('stock/products/' + productId, Grocy.Api.Get('stock/products/' + productId,
function(productDetails) function(productDetails) {
{
var productStockAmount = parseFloat(productDetails.stock_amount || parseFloat('0')); var productStockAmount = parseFloat(productDetails.stock_amount || parseFloat('0'));
var containerWeight = parseFloat("0"); var containerWeight = parseFloat("0");
if (productDetails.product.enable_tare_weight_handling == 1) if (productDetails.product.enable_tare_weight_handling == 1) {
{
containerWeight = parseFloat(productDetails.product.tare_weight); containerWeight = parseFloat(productDetails.product.tare_weight);
} }
@ -378,71 +308,54 @@ $('#display_amount').on('keyup', function(e)
estimatedBookingAmount = Math.abs(estimatedBookingAmount); estimatedBookingAmount = Math.abs(estimatedBookingAmount);
$('#inventory-change-info').removeClass('d-none'); $('#inventory-change-info').removeClass('d-none');
if (productDetails.product.enable_tare_weight_handling == 1 && newAmount < containerWeight) if (productDetails.product.enable_tare_weight_handling == 1 && newAmount < containerWeight) {
{
$('#inventory-change-info').addClass('d-none'); $('#inventory-change-info').addClass('d-none');
} } else if (newAmount > productStockAmount + containerWeight) {
else if (newAmount > productStockAmount + containerWeight)
{
$('#inventory-change-info').text(__t('This means %s will be added to stock', estimatedBookingAmount.toLocaleString() + ' ' + __n(estimatedBookingAmount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural, true))); $('#inventory-change-info').text(__t('This means %s will be added to stock', estimatedBookingAmount.toLocaleString() + ' ' + __n(estimatedBookingAmount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural, true)));
Grocy.Components.DateTimePicker.GetInputElement().attr('required', ''); Grocy.Components.DateTimePicker.GetInputElement().attr('required', '');
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING) if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING) {
{
Grocy.Components.LocationPicker.GetInputElement().attr('required', ''); Grocy.Components.LocationPicker.GetInputElement().attr('required', '');
} }
} } else if (newAmount < productStockAmount + containerWeight) {
else if (newAmount < productStockAmount + containerWeight)
{
$('#inventory-change-info').text(__t('This means %s will be removed from stock', estimatedBookingAmount.toLocaleString() + ' ' + __n(estimatedBookingAmount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural, true))); $('#inventory-change-info').text(__t('This means %s will be removed from stock', estimatedBookingAmount.toLocaleString() + ' ' + __n(estimatedBookingAmount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural, true)));
Grocy.Components.DateTimePicker.GetInputElement().removeAttr('required'); Grocy.Components.DateTimePicker.GetInputElement().removeAttr('required');
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING) if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING) {
{
Grocy.Components.LocationPicker.GetInputElement().removeAttr('required'); Grocy.Components.LocationPicker.GetInputElement().removeAttr('required');
} }
} } else if (newAmount == productStockAmount) {
else if (newAmount == productStockAmount)
{
$('#inventory-change-info').addClass('d-none'); $('#inventory-change-info').addClass('d-none');
} }
if (!Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING) if (!Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING) {
{
Grocy.Components.DateTimePicker.GetInputElement().removeAttr('required'); Grocy.Components.DateTimePicker.GetInputElement().removeAttr('required');
} }
Grocy.FrontendHelpers.ValidateForm('inventory-form'); Grocy.FrontendHelpers.ValidateForm('inventory-form');
}, },
function(xhr) function(xhr) {
{
console.error(xhr); console.error(xhr);
} }
); );
} }
}); });
function UndoStockBooking(bookingId) function UndoStockBooking(bookingId) {
{
Grocy.Api.Post('stock/bookings/' + bookingId.toString() + '/undo', {}, Grocy.Api.Post('stock/bookings/' + bookingId.toString() + '/undo', {},
function(result) function(result) {
{
toastr.success(__t("Booking successfully undone")); toastr.success(__t("Booking successfully undone"));
}, },
function(xhr) function(xhr) {
{
console.error(xhr); console.error(xhr);
} }
); );
}; };
function UndoStockTransaction(transactionId) function UndoStockTransaction(transactionId) {
{
Grocy.Api.Post('stock/transactions/' + transactionId.toString() + '/undo', {}, Grocy.Api.Post('stock/transactions/' + transactionId.toString() + '/undo', {},
function(result) function(result) {
{
toastr.success(__t("Transaction successfully undone")); toastr.success(__t("Transaction successfully undone"));
}, },
function(xhr) function(xhr) {
{
console.error(xhr); console.error(xhr);
} }
); );

File diff suppressed because it is too large Load Diff

View File

@ -1,85 +1,59 @@
function saveProductPicture(result, location, jsonData) function saveProductPicture(result, location, jsonData) {
{
var productId = Grocy.EditObjectId || result.created_object_id; var productId = Grocy.EditObjectId || result.created_object_id;
Grocy.EditObjectId = productId; // Grocy.EditObjectId is not yet set when adding a product Grocy.EditObjectId = productId; // Grocy.EditObjectId is not yet set when adding a product
Grocy.Components.UserfieldsForm.Save(() => Grocy.Components.UserfieldsForm.Save(() => {
{ if (jsonData.hasOwnProperty("picture_file_name") && !Grocy.DeleteProductPictureOnSave) {
if (jsonData.hasOwnProperty("picture_file_name") && !Grocy.DeleteProductPictureOnSave)
{
Grocy.Api.UploadFile($("#product-picture")[0].files[0], 'productpictures', jsonData.picture_file_name, Grocy.Api.UploadFile($("#product-picture")[0].files[0], 'productpictures', jsonData.picture_file_name,
(result) => (result) => {
{ if (Grocy.ProductEditFormRedirectUri == "reload") {
if (Grocy.ProductEditFormRedirectUri == "reload")
{
window.location.reload(); window.location.reload();
return return
} }
var returnTo = GetUriParam('returnto'); var returnTo = GetUriParam('returnto');
if (GetUriParam("closeAfterCreation") !== undefined) if (GetUriParam("closeAfterCreation") !== undefined) {
{
window.close(); window.close();
} } else if (returnTo !== undefined) {
else if (returnTo !== undefined) if (GetUriParam("flow") !== undefined) {
{
if (GetUriParam("flow") !== undefined)
{
window.location.href = U(returnTo) + '&product-name=' + encodeURIComponent($('#name').val()); window.location.href = U(returnTo) + '&product-name=' + encodeURIComponent($('#name').val());
} } else {
else
{
window.location.href = U(returnTo); window.location.href = U(returnTo);
} }
} } else {
else
{
window.location.href = U(location + productId); window.location.href = U(location + productId);
} }
}, },
(xhr) => (xhr) => {
{
Grocy.FrontendHelpers.EndUiBusy("product-form"); Grocy.FrontendHelpers.EndUiBusy("product-form");
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response) Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response)
} }
); );
} } else {
else if (Grocy.ProductEditFormRedirectUri == "reload") {
{
if (Grocy.ProductEditFormRedirectUri == "reload")
{
window.location.reload(); window.location.reload();
return return
} }
var returnTo = GetUriParam('returnto'); var returnTo = GetUriParam('returnto');
if (GetUriParam("closeAfterCreation") !== undefined) if (GetUriParam("closeAfterCreation") !== undefined) {
{
window.close(); window.close();
} } else if (returnTo !== undefined) {
else if (returnTo !== undefined) if (GetUriParam("flow") !== undefined) {
{
if (GetUriParam("flow") !== undefined)
{
window.location.href = U(returnTo) + '&product-name=' + encodeURIComponent($('#name').val()); window.location.href = U(returnTo) + '&product-name=' + encodeURIComponent($('#name').val());
} } else {
else
{
window.location.href = U(returnTo); window.location.href = U(returnTo);
} }
} } else {
else
{
window.location.href = U(location + productId); window.location.href = U(location + productId);
} }
} }
}); });
} }
$('.save-product-button').on('click', function(e) $('.save-product-button').on('click', function(e) {
{
e.preventDefault(); e.preventDefault();
var jsonData = $('#product-form').serializeJSON(); var jsonData = $('#product-form').serializeJSON();
@ -88,42 +62,35 @@ $('.save-product-button').on('click', function(e)
jsonData.parent_product_id = parentProductId; jsonData.parent_product_id = parentProductId;
Grocy.FrontendHelpers.BeginUiBusy("product-form"); Grocy.FrontendHelpers.BeginUiBusy("product-form");
if (jsonData.parent_product_id.toString().isEmpty()) if (jsonData.parent_product_id.toString().isEmpty()) {
{
jsonData.parent_product_id = null; jsonData.parent_product_id = null;
} }
if ($("#product-picture")[0].files.length > 0) if ($("#product-picture")[0].files.length > 0) {
{
var someRandomStuff = Math.random().toString(36).substring(2, 100) + Math.random().toString(36).substring(2, 100); var someRandomStuff = Math.random().toString(36).substring(2, 100) + Math.random().toString(36).substring(2, 100);
jsonData.picture_file_name = someRandomStuff + CleanFileName($("#product-picture")[0].files[0].name); jsonData.picture_file_name = someRandomStuff + CleanFileName($("#product-picture")[0].files[0].name);
} }
const location = $(e.currentTarget).attr('data-location') == 'return' ? '/products?product=' : '/product/'; const location = $(e.currentTarget).attr('data-location') == 'return' ? '/products?product=' : '/product/';
if (Grocy.EditMode == 'create') if (Grocy.EditMode == 'create') {
{
Grocy.Api.Post('objects/products', jsonData, Grocy.Api.Post('objects/products', jsonData,
(result) => saveProductPicture(result, location, jsonData), (result) => saveProductPicture(result, location, jsonData),
(xhr) => (xhr) => {
{
Grocy.FrontendHelpers.EndUiBusy("product-form"); Grocy.FrontendHelpers.EndUiBusy("product-form");
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response) Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response)
}); });
return; return;
} }
if (Grocy.DeleteProductPictureOnSave) if (Grocy.DeleteProductPictureOnSave) {
{
jsonData.picture_file_name = null; jsonData.picture_file_name = null;
Grocy.Api.DeleteFile(Grocy.ProductPictureFileName, 'productpictures', {}, Grocy.Api.DeleteFile(Grocy.ProductPictureFileName, 'productpictures', {},
function(result) function(result) {
{
// Nothing to do // Nothing to do
}, },
function(xhr) function(xhr) {
{
Grocy.FrontendHelpers.EndUiBusy("product-form"); Grocy.FrontendHelpers.EndUiBusy("product-form");
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response) Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response)
} }
@ -132,55 +99,44 @@ $('.save-product-button').on('click', function(e)
Grocy.Api.Put('objects/products/' + Grocy.EditObjectId, jsonData, Grocy.Api.Put('objects/products/' + Grocy.EditObjectId, jsonData,
(result) => saveProductPicture(result, location, jsonData), (result) => saveProductPicture(result, location, jsonData),
function(xhr) function(xhr) {
{
Grocy.FrontendHelpers.EndUiBusy("product-form"); Grocy.FrontendHelpers.EndUiBusy("product-form");
console.error(xhr); console.error(xhr);
} }
); );
}); });
if (Grocy.EditMode == "edit") if (Grocy.EditMode == "edit") {
{
Grocy.Api.Get('objects/stock_log?limit=1&query[]=product_id=' + Grocy.EditObjectId, Grocy.Api.Get('objects/stock_log?limit=1&query[]=product_id=' + Grocy.EditObjectId,
function(productJournalEntries) function(productJournalEntries) {
{ if (productJournalEntries.length == 0) {
if (productJournalEntries.length == 0)
{
$('#qu_id_stock').removeAttr("disabled"); $('#qu_id_stock').removeAttr("disabled");
} }
}, },
function(xhr) function(xhr) {
{
console.error(xhr); console.error(xhr);
} }
); );
} }
if (GetUriParam("flow") == "InplaceNewProductWithName") if (GetUriParam("flow") == "InplaceNewProductWithName") {
{
$('#name').val(GetUriParam("name")); $('#name').val(GetUriParam("name"));
$('#name').focus(); $('#name').focus();
} }
if (GetUriParam("flow") !== undefined || GetUriParam("returnto") !== undefined) if (GetUriParam("flow") !== undefined || GetUriParam("returnto") !== undefined) {
{
$("#save-hint").addClass("d-none"); $("#save-hint").addClass("d-none");
$(".save-product-button[data-location='return']").addClass("d-none"); $(".save-product-button[data-location='return']").addClass("d-none");
} }
$('.input-group-qu').on('change', function(e) $('.input-group-qu').on('change', function(e) {
{
var quIdPurchase = $("#qu_id_purchase").val(); var quIdPurchase = $("#qu_id_purchase").val();
var quIdStock = $("#qu_id_stock").val(); var quIdStock = $("#qu_id_stock").val();
if (Grocy.EditMode == "create" && !quIdPurchase.toString().isEmpty() && !quIdStock.toString().isEmpty() && quIdPurchase != quIdStock) if (Grocy.EditMode == "create" && !quIdPurchase.toString().isEmpty() && !quIdStock.toString().isEmpty() && quIdPurchase != quIdStock) {
{
Grocy.Api.Get("objects/quantity_unit_conversions?query[]=product_id=null&query[]=from_qu_id=" + quIdPurchase + "&query[]=to_qu_id=" + quIdStock, Grocy.Api.Get("objects/quantity_unit_conversions?query[]=product_id=null&query[]=from_qu_id=" + quIdPurchase + "&query[]=to_qu_id=" + quIdStock,
function(response) function(response) {
{ if (response != null && response.length > 0) {
if (response != null && response.length > 0)
{
var conversion = response[0]; var conversion = response[0];
$("#qu_factor_purchase_to_stock").val(conversion.factor); $("#qu_factor_purchase_to_stock").val(conversion.factor);
@ -188,14 +144,11 @@ $('.input-group-qu').on('change', function(e)
RefreshQuConversionInfo(); RefreshQuConversionInfo();
} }
}, },
function(xhr) function(xhr) {
{
console.error(xhr); console.error(xhr);
} }
); );
} } else {
else
{
RefreshQuConversionInfo(); RefreshQuConversionInfo();
} }
@ -206,25 +159,20 @@ $('.input-group-qu').on('change', function(e)
Grocy.FrontendHelpers.ValidateForm('product-form'); Grocy.FrontendHelpers.ValidateForm('product-form');
}); });
function RefreshQuConversionInfo() function RefreshQuConversionInfo() {
{
var quIdPurchase = $("#qu_id_purchase").val(); var quIdPurchase = $("#qu_id_purchase").val();
var quIdStock = $("#qu_id_stock").val(); var quIdStock = $("#qu_id_stock").val();
var factor = $('#qu_factor_purchase_to_stock').val(); var factor = $('#qu_factor_purchase_to_stock').val();
if (factor > 1 || quIdPurchase != quIdStock) if (factor > 1 || quIdPurchase != quIdStock) {
{
$('#qu-conversion-info').text(__t('This means 1 %1$s purchased will be converted into %2$s %3$s in stock', $("#qu_id_purchase option:selected").text(), (1 * factor).toString(), __n((1 * factor).toString(), $("#qu_id_stock option:selected").text(), $("#qu_id_stock option:selected").data("plural-form"), true))); $('#qu-conversion-info').text(__t('This means 1 %1$s purchased will be converted into %2$s %3$s in stock', $("#qu_id_purchase option:selected").text(), (1 * factor).toString(), __n((1 * factor).toString(), $("#qu_id_stock option:selected").text(), $("#qu_id_stock option:selected").data("plural-form"), true)));
$('#qu-conversion-info').removeClass('d-none'); $('#qu-conversion-info').removeClass('d-none');
} } else {
else
{
$('#qu-conversion-info').addClass('d-none'); $('#qu-conversion-info').addClass('d-none');
} }
} }
$('#product-form input').keyup(function(event) $('#product-form input').on('keyup', function(event) {
{
Grocy.FrontendHelpers.ValidateForm('product-form'); Grocy.FrontendHelpers.ValidateForm('product-form');
$(".input-group-qu").trigger("change"); $(".input-group-qu").trigger("change");
$("#product-form select").trigger("select"); $("#product-form select").trigger("select");
@ -232,9 +180,7 @@ $('#product-form input').keyup(function(event)
if (document.getElementById('product-form').checkValidity() === false) //There is at least one validation error if (document.getElementById('product-form').checkValidity() === false) //There is at least one validation error
{ {
$("#qu-conversion-add-button").addClass("disabled"); $("#qu-conversion-add-button").addClass("disabled");
} } else {
else
{
$("#qu-conversion-add-button").removeClass("disabled"); $("#qu-conversion-add-button").removeClass("disabled");
} }
@ -244,13 +190,11 @@ $('#product-form input').keyup(function(event)
} }
}); });
$('#location_id').change(function(event) $('#location_id').on('change', function(event) {
{
Grocy.FrontendHelpers.ValidateForm('product-form'); Grocy.FrontendHelpers.ValidateForm('product-form');
}); });
$('#product-form input').keydown(function(event) $('#product-form input').on('keydown', function(event) {
{
if (event.keyCode === 13) //Enter if (event.keyCode === 13) //Enter
{ {
event.preventDefault(); event.preventDefault();
@ -258,30 +202,23 @@ $('#product-form input').keydown(function(event)
if (document.getElementById('product-form').checkValidity() === false) //There is at least one validation error if (document.getElementById('product-form').checkValidity() === false) //There is at least one validation error
{ {
return false; return false;
} } else {
else
{
$('.default-submit-button').click(); $('.default-submit-button').click();
} }
} }
}); });
$("#enable_tare_weight_handling").on("click", function() $("#enable_tare_weight_handling").on("click", function() {
{ if (this.checked) {
if (this.checked)
{
$("#tare_weight").removeAttr("disabled"); $("#tare_weight").removeAttr("disabled");
} } else {
else
{
$("#tare_weight").attr("disabled", ""); $("#tare_weight").attr("disabled", "");
} }
Grocy.FrontendHelpers.ValidateForm("product-form"); Grocy.FrontendHelpers.ValidateForm("product-form");
}); });
$("#product-picture").on("change", function(e) $("#product-picture").on("change", function(e) {
{
$("#product-picture-label").removeClass("d-none"); $("#product-picture-label").removeClass("d-none");
$("#product-picture-label-none").addClass("d-none"); $("#product-picture-label-none").addClass("d-none");
$("#delete-current-product-picture-on-save-hint").addClass("d-none"); $("#delete-current-product-picture-on-save-hint").addClass("d-none");
@ -290,8 +227,7 @@ $("#product-picture").on("change", function(e)
}); });
Grocy.DeleteProductPictureOnSave = false; Grocy.DeleteProductPictureOnSave = false;
$("#delete-current-product-picture-button").on("click", function(e) $("#delete-current-product-picture-button").on("click", function(e) {
{
Grocy.DeleteProductPictureOnSave = true; Grocy.DeleteProductPictureOnSave = true;
$("#current-product-picture").addClass("d-none"); $("#current-product-picture").addClass("d-none");
$("#delete-current-product-picture-on-save-hint").removeClass("d-none"); $("#delete-current-product-picture-on-save-hint").removeClass("d-none");
@ -300,12 +236,24 @@ $("#delete-current-product-picture-button").on("click", function(e)
}); });
var quConversionsTable = $('#qu-conversions-table-products').DataTable({ var quConversionsTable = $('#qu-conversions-table-products').DataTable({
'order': [[1, 'asc']], 'order': [
"orderFixed": [[4, 'asc']], [1, 'asc']
'columnDefs': [ ],
{ 'orderable': false, 'targets': 0 }, "orderFixed": [
{ 'searchable': false, "targets": 0 }, [4, 'asc']
{ 'visible': false, 'targets': 4 } ],
'columnDefs': [{
'orderable': false,
'targets': 0
},
{
'searchable': false,
"targets": 0
},
{
'visible': false,
'targets': 4
}
].concat($.fn.dataTable.defaults.columnDefs), ].concat($.fn.dataTable.defaults.columnDefs),
'rowGroup': { 'rowGroup': {
enable: true, enable: true,
@ -316,13 +264,28 @@ $('#qu-conversions-table-products tbody').removeClass("d-none");
quConversionsTable.columns.adjust().draw(); quConversionsTable.columns.adjust().draw();
var barcodeTable = $('#barcode-table').DataTable({ var barcodeTable = $('#barcode-table').DataTable({
'order': [[1, 'asc']], 'order': [
"orderFixed": [[1, 'asc']], [1, 'asc']
'columnDefs': [ ],
{ 'orderable': false, 'targets': 0 }, "orderFixed": [
{ 'searchable': false, "targets": 0 }, [1, 'asc']
{ 'visible': false, 'targets': 5 }, ],
{ 'visible': false, 'targets': 6 } 'columnDefs': [{
'orderable': false,
'targets': 0
},
{
'searchable': false,
"targets": 0
},
{
'visible': false,
'targets': 5
},
{
'visible': false,
'targets': 6
}
].concat($.fn.dataTable.defaults.columnDefs) ].concat($.fn.dataTable.defaults.columnDefs)
}); });
$('#barcode-table tbody').removeClass("d-none"); $('#barcode-table tbody').removeClass("d-none");
@ -330,27 +293,23 @@ barcodeTable.columns.adjust().draw();
Grocy.Components.UserfieldsForm.Load(); Grocy.Components.UserfieldsForm.Load();
$("#name").trigger("keyup"); $("#name").trigger("keyup");
$('#name').focus(); $('#name').trigger('focus');
$('.input-group-qu').trigger('change'); $('.input-group-qu').trigger('change');
Grocy.FrontendHelpers.ValidateForm('product-form'); Grocy.FrontendHelpers.ValidateForm('product-form');
$(document).on('click', '.product-grocycode-label-print', function(e) $(document).on('click', '.product-grocycode-label-print', function(e) {
{
e.preventDefault(); e.preventDefault();
document.activeElement.blur(); document.activeElement.blur();
var productId = $(e.currentTarget).attr('data-product-id'); var productId = $(e.currentTarget).attr('data-product-id');
Grocy.Api.Get('stock/products/' + productId + '/printlabel', function(labelData) Grocy.Api.Get('stock/products/' + productId + '/printlabel', function(labelData) {
{ if (Grocy.Webhooks.labelprinter !== undefined) {
if (Grocy.Webhooks.labelprinter !== undefined)
{
Grocy.FrontendHelpers.RunWebhook(Grocy.Webhooks.labelprinter, labelData); Grocy.FrontendHelpers.RunWebhook(Grocy.Webhooks.labelprinter, labelData);
} }
}); });
}); });
$(document).on('click', '.qu-conversion-delete-button', function(e) $(document).on('click', '.qu-conversion-delete-button', function(e) {
{
var objectId = $(e.currentTarget).attr('data-qu-conversion-id'); var objectId = $(e.currentTarget).attr('data-qu-conversion-id');
bootbox.confirm({ bootbox.confirm({
@ -366,18 +325,14 @@ $(document).on('click', '.qu-conversion-delete-button', function(e)
className: 'btn-danger' className: 'btn-danger'
} }
}, },
callback: function(result) callback: function(result) {
{ if (result === true) {
if (result === true)
{
Grocy.Api.Delete('objects/quantity_unit_conversions/' + objectId, {}, Grocy.Api.Delete('objects/quantity_unit_conversions/' + objectId, {},
function(result) function(result) {
{
Grocy.ProductEditFormRedirectUri = "reload"; Grocy.ProductEditFormRedirectUri = "reload";
$('#save-product-button').click(); $('#save-product-button').click();
}, },
function(xhr) function(xhr) {
{
console.error(xhr); console.error(xhr);
} }
); );
@ -386,8 +341,7 @@ $(document).on('click', '.qu-conversion-delete-button', function(e)
}); });
}); });
$(document).on('click', '.barcode-delete-button', function(e) $(document).on('click', '.barcode-delete-button', function(e) {
{
var objectId = $(e.currentTarget).attr('data-barcode-id'); var objectId = $(e.currentTarget).attr('data-barcode-id');
bootbox.confirm({ bootbox.confirm({
@ -403,18 +357,14 @@ $(document).on('click', '.barcode-delete-button', function(e)
className: 'btn-danger' className: 'btn-danger'
} }
}, },
callback: function(result) callback: function(result) {
{ if (result === true) {
if (result === true)
{
Grocy.Api.Delete('objects/product_barcodes/' + objectId, {}, Grocy.Api.Delete('objects/product_barcodes/' + objectId, {},
function(result) function(result) {
{
Grocy.ProductEditFormRedirectUri = "reload"; Grocy.ProductEditFormRedirectUri = "reload";
$('#save-product-button').click(); $('#save-product-button').click();
}, },
function(xhr) function(xhr) {
{
console.error(xhr); console.error(xhr);
} }
); );
@ -423,14 +373,12 @@ $(document).on('click', '.barcode-delete-button', function(e)
}); });
}); });
$('#qu_id_stock').change(function(e) $('#qu_id_stock').on('change', function(e) {
{
// Preset QU purchase with stock QU if unset // Preset QU purchase with stock QU if unset
var quIdStock = $('#qu_id_stock'); var quIdStock = $('#qu_id_stock');
var quIdPurchase = $('#qu_id_purchase'); var quIdPurchase = $('#qu_id_purchase');
if (quIdPurchase[0].selectedIndex === 0 && quIdStock[0].selectedIndex !== 0) if (quIdPurchase[0].selectedIndex === 0 && quIdStock[0].selectedIndex !== 0) {
{
quIdPurchase[0].selectedIndex = quIdStock[0].selectedIndex; quIdPurchase[0].selectedIndex = quIdStock[0].selectedIndex;
Grocy.FrontendHelpers.ValidateForm('product-form'); Grocy.FrontendHelpers.ValidateForm('product-form');
} }
@ -438,59 +386,47 @@ $('#qu_id_stock').change(function(e)
RefreshQuConversionInfo(); RefreshQuConversionInfo();
}); });
$(window).on("message", function(e) $(window).on("message", function(e) {
{
var data = e.originalEvent.data; var data = e.originalEvent.data;
if (data.Message === "ProductBarcodesChanged" || data.Message === "ProductQUConversionChanged") if (data.Message === "ProductBarcodesChanged" || data.Message === "ProductQUConversionChanged") {
{
window.location.reload(); window.location.reload();
} }
}); });
if (Grocy.EditMode == "create" && GetUriParam("copy-of") != undefined) if (Grocy.EditMode == "create" && GetUriParam("copy-of") != undefined) {
{
Grocy.Api.Get('objects/products/' + GetUriParam("copy-of"), Grocy.Api.Get('objects/products/' + GetUriParam("copy-of"),
function(sourceProduct) function(sourceProduct) {
{ if (sourceProduct.parent_product_id != null) {
if (sourceProduct.parent_product_id != null)
{
Grocy.Components.ProductPicker.SetId(sourceProduct.parent_product_id); Grocy.Components.ProductPicker.SetId(sourceProduct.parent_product_id);
} }
if (sourceProduct.description != null) if (sourceProduct.description != null) {
{
$("#description").summernote("pasteHTML", sourceProduct.description); $("#description").summernote("pasteHTML", sourceProduct.description);
} }
$("#location_id").val(sourceProduct.location_id); $("#location_id").val(sourceProduct.location_id);
if (sourceProduct.shopping_location_id != null) if (sourceProduct.shopping_location_id != null) {
{
Grocy.Components.ShoppingLocationPicker.SetId(sourceProduct.shopping_location_id); Grocy.Components.ShoppingLocationPicker.SetId(sourceProduct.shopping_location_id);
} }
$("#min_stock_amount").val(sourceProduct.min_stock_amount); $("#min_stock_amount").val(sourceProduct.min_stock_amount);
if (BoolVal(sourceProduct.cumulate_min_stock_amount_of_sub_products)) if (BoolVal(sourceProduct.cumulate_min_stock_amount_of_sub_products)) {
{
$("#cumulate_min_stock_amount_of_sub_products").prop("checked", true); $("#cumulate_min_stock_amount_of_sub_products").prop("checked", true);
} }
$("#default_best_before_days").val(sourceProduct.default_best_before_days); $("#default_best_before_days").val(sourceProduct.default_best_before_days);
$("#default_best_before_days_after_open").val(sourceProduct.default_best_before_days_after_open); $("#default_best_before_days_after_open").val(sourceProduct.default_best_before_days_after_open);
if (sourceProduct.product_group_id != null) if (sourceProduct.product_group_id != null) {
{
$("#product_group_id").val(sourceProduct.product_group_id); $("#product_group_id").val(sourceProduct.product_group_id);
} }
$("#qu_id_stock").val(sourceProduct.qu_id_stock); $("#qu_id_stock").val(sourceProduct.qu_id_stock);
$("#qu_id_purchase").val(sourceProduct.qu_id_purchase); $("#qu_id_purchase").val(sourceProduct.qu_id_purchase);
$("#qu_factor_purchase_to_stock").val(sourceProduct.qu_factor_purchase_to_stock); $("#qu_factor_purchase_to_stock").val(sourceProduct.qu_factor_purchase_to_stock);
if (BoolVal(sourceProduct.enable_tare_weight_handling)) if (BoolVal(sourceProduct.enable_tare_weight_handling)) {
{
$("#enable_tare_weight_handling").prop("checked", true); $("#enable_tare_weight_handling").prop("checked", true);
} }
$("#tare_weight").val(sourceProduct.tare_weight); $("#tare_weight").val(sourceProduct.tare_weight);
if (BoolVal(sourceProduct.not_check_stock_fulfillment_for_recipes)) if (BoolVal(sourceProduct.not_check_stock_fulfillment_for_recipes)) {
{
$("#not_check_stock_fulfillment_for_recipes").prop("checked", true); $("#not_check_stock_fulfillment_for_recipes").prop("checked", true);
} }
if (sourceProduct.calories != null) if (sourceProduct.calories != null) {
{
$("#calories").val(sourceProduct.calories); $("#calories").val(sourceProduct.calories);
} }
$("#default_best_before_days_after_freezing").val(sourceProduct.default_best_before_days_after_freezing); $("#default_best_before_days_after_freezing").val(sourceProduct.default_best_before_days_after_freezing);
@ -499,75 +435,57 @@ if (Grocy.EditMode == "create" && GetUriParam("copy-of") != undefined)
Grocy.FrontendHelpers.ValidateForm('product-form'); Grocy.FrontendHelpers.ValidateForm('product-form');
}, },
function(xhr) function(xhr) {
{
console.error(xhr); console.error(xhr);
} }
); );
} } else if (Grocy.EditMode === 'create') {
else if (Grocy.EditMode === 'create') if (Grocy.UserSettings.product_presets_location_id.toString() !== '-1') {
{
if (Grocy.UserSettings.product_presets_location_id.toString() !== '-1')
{
$("#location_id").val(Grocy.UserSettings.product_presets_location_id); $("#location_id").val(Grocy.UserSettings.product_presets_location_id);
} }
if (Grocy.UserSettings.product_presets_product_group_id.toString() !== '-1') if (Grocy.UserSettings.product_presets_product_group_id.toString() !== '-1') {
{
$("#product_group_id").val(Grocy.UserSettings.product_presets_product_group_id); $("#product_group_id").val(Grocy.UserSettings.product_presets_product_group_id);
} }
if (Grocy.UserSettings.product_presets_qu_id.toString() !== '-1') if (Grocy.UserSettings.product_presets_qu_id.toString() !== '-1') {
{
$("select.input-group-qu").val(Grocy.UserSettings.product_presets_qu_id); $("select.input-group-qu").val(Grocy.UserSettings.product_presets_qu_id);
} }
if (Grocy.UserSettings.product_presets_default_due_days.toString() !== '0') if (Grocy.UserSettings.product_presets_default_due_days.toString() !== '0') {
{
$("#default_best_before_days").val(Grocy.UserSettings.product_presets_default_due_days); $("#default_best_before_days").val(Grocy.UserSettings.product_presets_default_due_days);
} }
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRODUCT_OPENED_TRACKING) if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRODUCT_OPENED_TRACKING) {
{
$("#treat_opened_as_out_of_stock").prop("checked", BoolVal(Grocy.UserSettings.product_presets_treat_opened_as_out_of_stock)); $("#treat_opened_as_out_of_stock").prop("checked", BoolVal(Grocy.UserSettings.product_presets_treat_opened_as_out_of_stock));
} }
} }
Grocy.Components.ProductPicker.GetPicker().on('change', function(e) Grocy.Components.ProductPicker.OnChange(function(e) {
{
var parentProductId = $(e.target).val(); var parentProductId = $(e.target).val();
if (parentProductId) if (parentProductId) {
{
Grocy.Api.Get('objects/products/' + parentProductId, Grocy.Api.Get('objects/products/' + parentProductId,
function(parentProduct) function(parentProduct) {
{ if (BoolVal(parentProduct.cumulate_min_stock_amount_of_sub_products)) {
if (BoolVal(parentProduct.cumulate_min_stock_amount_of_sub_products))
{
$("#min_stock_amount").attr("disabled", ""); $("#min_stock_amount").attr("disabled", "");
} } else {
else
{
$('#min_stock_amount').removeAttr("disabled"); $('#min_stock_amount').removeAttr("disabled");
} }
}, },
function(xhr) function(xhr) {
{
console.error(xhr); console.error(xhr);
} }
); );
} } else {
else
{
$('#min_stock_amount').removeAttr("disabled"); $('#min_stock_amount').removeAttr("disabled");
} }
}); });
Grocy.FrontendHelpers.ValidateForm("product-form"); Grocy.FrontendHelpers.ValidateForm("product-form");
Grocy.Components.ProductPicker.GetPicker().trigger("change"); Grocy.Components.ProductPicker.Validate();
if (Grocy.EditMode == "edit") if (Grocy.EditMode == "edit") {
{
$(".save-product-button").toggleClass("default-submit-button"); $(".save-product-button").toggleClass("default-submit-button");
} }

View File

@ -1,61 +1,180 @@
var productsTable = $('#products-table').DataTable({ var userfieldsColumns = userfields.map(function(userfield) {
'order': [[1, 'asc']], if (userfield.show_as_column_in_tables != 1) return null;
'columnDefs': [ return {
{ 'orderable': false, 'targets': 0 }, data: 'userfields.' + userfield.name,
{ 'searchable': false, "targets": 0 }, defaultContent: ''
{ 'visible': false, 'targets': 7 }, };
{ "type": "html-num-fmt", "targets": 3 } }).filter(function(userfield) {
].concat($.fn.dataTable.defaults.columnDefs) return userfield !== null;
}); });
$('#products-table tbody').removeClass("d-none");
productsTable.columns.adjust().draw();
$("#search").on("keyup", Delay(function() var productsTable = $('#products-table').DataTable({
ajax: function(data, callback, settings) {
Grocy.FrontendHelpers.BeginUiBusy();
var query = [];
if (GetUriParam('only_in_stock')) {
query.push('only_in_stock=true');
}
if (!GetUriParam('include_disabled')) {
query.push('query%5B%5D=' + encodeURIComponent('active=1'));
}
data.columns.forEach(function(column) {
var search = column.search.value.trim();
if (search.length > 0) {
query.push('query%5B%5D=' + encodeURIComponent(column.data + '=' + search));
}
});
var search = data.search.value.trim();
if (search.length > 0) {
query.push('search=' + encodeURIComponent(search));
}
query.push('limit=' + encodeURIComponent(data.length));
query.push('offset=' + encodeURIComponent(data.start));
query.push('order=' + encodeURIComponent(data.order.map(function(order) {
return data.columns[order.column].data + ':' + order.dir;
}).join(',')));
Grocy.Api.Get('objects/products' + (query.length > 0 ? '?' + query.join('&') : ''),
function(result, meta) {
callback({
data: result,
recordsTotal: meta.recordsTotal,
recordsFiltered: meta.recordsFiltered,
});
Grocy.FrontendHelpers.EndUiBusy();
},
function(xhr) {
Grocy.FrontendHelpers.EndUiBusy();
Grocy.FrontendHelpers.ShowGenericError('Server error', xhr.response);
}
);
},
paging: true,
serverSide: true,
deferRender: true,
autoWidth: true,
order: [
[1, 'asc']
],
columns: [{
searchable: false,
orderable: false,
render: function(data, type, row, meta) {
return '<a class="btn btn-info btn-sm" href="/product/' + encodeURIComponent(row.id) + '"' +
' data-toggle="tooltip" title="' + __t('Edit this item') + '">' +
' <i class="fas fa-edit"></i>' +
'</a>' +
'<a class="btn btn-danger btn-sm product-delete-button" href="#"' +
' data-product-id="' + row.id + '" data-product-name="' + row.name + '"' +
' data-toggle="tooltip" title="' + __t('Delete this item') + '">' +
' <i class="fas fa-trash"></i>' +
'</a>' +
'<div class="dropdown d-inline-block">' +
' <button class="btn btn-sm btn-light text-secondary" type="button"' +
' data-toggle="dropdown">' +
' <i class="fas fa-ellipsis-v"></i>' +
' </button>' +
' <div class="table-inline-menu dropdown-menu dropdown-menu-right">' +
' <a class="dropdown-item" type="button"' +
' href="/product/new?copy-of=' + encodeURIComponent(row.id) + '">' +
' <span class="dropdown-item-text">' + __t('Copy') + '</span>' +
' </a>' +
' <a class="dropdown-item merge-products-button"' +
' data-product-id="' + row.id + '" data-product-name="' + row.name + '"' +
' type="button" href="#" >' +
' <span class="dropdown-item-text">' + __t('Merge') + '</span>' +
' </a>' +
' </div>' +
'</div>';
}
},
{ {
data: 'name',
searchable: true,
render: function(data, type, row, meta) {
return data + (row.picture_file_name ? (
' <i class="fas fa-image text-muted" data-toggle="tooltip" ' +
'title="' + __t('This product has a picture') + '"></i>'
) : '');
}
},
{
data: 'location.name',
defaultContent: '',
visible: Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING
},
{
data: 'min_stock_amount',
type: 'html-num-fmt',
render: function(data, type, row, meta) {
return '<span class="locale-number locale-number-quantity-amount">' + data + '</span>';
}
},
{
data: 'qu_purchase.name',
defaultContent: ''
},
{
data: 'qu_stock.name',
defaultContent: ''
},
{
data: 'product_group.name',
defaultContent: ''
},
{
data: 'shopping_location.name',
defaultContent: '',
visible: Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING
}
].concat(userfieldsColumns),
});
productsTable.on('draw', function() {
$('[data-toggle=tooltip]').tooltip();
$('[data-toggle=dropdown]').dropdown();
});
$('#search').on('keyup', Delay(function() {
var value = $(this).val(); var value = $(this).val();
if (value === "all") if (value === 'all') {
{ value = '';
value = "";
} }
productsTable.search(value).draw(); productsTable.search(value).draw();
}, 200)); }, 500));
$("#product-group-filter").on("change", function() $('#product-group-filter').on('change', function() {
{ var value = $('#product-group-filter option:selected').text();
var value = $("#product-group-filter option:selected").text(); if (value === __t('All')) {
if (value === __t("All")) value = '';
{
value = "";
} }
productsTable.column(productsTable.colReorder.transpose(6)).search(value).draw(); productsTable.column(productsTable.colReorder.transpose(6)).search(value).draw();
}); });
$("#clear-filter-button").on("click", function() $('#clear-filter-button').on('click', function() {
{ $('#search').val('');
$("#search").val(""); $('#product-group-filter').val('all');
$("#product-group-filter").val("all"); productsTable.column(productsTable.colReorder.transpose(6)).search('');
productsTable.column(productsTable.colReorder.transpose(6)).search("").draw(); productsTable.search('');
productsTable.search("").draw(); if ($('#show-disabled').is(':checked') || $('#show-only-in-stock').is(':checked')) {
if ($("#show-disabled").is(":checked") || $("#show-only-in-stock").is(":checked")) $('#show-disabled').prop('checked', false);
{ $('#show-only-in-stock').prop('checked', false);
$("#show-disabled").prop("checked", false); RemoveUriParam('include_disabled');
$("#show-only-in-stock").prop("checked", false); RemoveUriParam('only_in_stock');
RemoveUriParam("include_disabled");
RemoveUriParam("only_in_stock");
window.location.reload();
} }
productsTable.draw();
}); });
if (typeof GetUriParam("product-group") !== "undefined") if (typeof GetUriParam('product-group') !== 'undefined') {
{ $('#product-group-filter').val(GetUriParam('product-group'));
$("#product-group-filter").val(GetUriParam("product-group")); $('#product-group-filter').trigger('change');
$("#product-group-filter").trigger("change");
} }
$(document).on('click', '.product-delete-button', function(e) $(document).on('click', '.product-delete-button', function(e) {
{
var objectName = $(e.currentTarget).attr('data-product-name'); var objectName = $(e.currentTarget).attr('data-product-name');
var objectId = $(e.currentTarget).attr('data-product-id'); var objectId = $(e.currentTarget).attr('data-product-id');
@ -72,19 +191,15 @@ $(document).on('click', '.product-delete-button', function(e)
className: 'btn-danger' className: 'btn-danger'
} }
}, },
callback: function(result) callback: function(result) {
{ if (result === true) {
if (result === true)
{
jsonData = {}; jsonData = {};
jsonData.active = 0; jsonData.active = 0;
Grocy.Api.Delete('objects/products/' + objectId, {}, Grocy.Api.Delete('objects/products/' + objectId, {},
function(result) function(result) {
{
window.location.href = U('/products'); window.location.href = U('/products');
}, },
function(xhr) function(xhr) {
{
console.error(xhr); console.error(xhr);
} }
); );
@ -93,61 +208,96 @@ $(document).on('click', '.product-delete-button', function(e)
}); });
}); });
$("#show-disabled").change(function() $('#show-disabled').on('change', function() {
{ if (this.checked) {
if (this.checked) UpdateUriParam('include_disabled', 'true');
{ } else {
UpdateUriParam("include_disabled", "true"); RemoveUriParam('include_disabled');
} }
else productsTable.draw();
{
RemoveUriParam("include_disabled");
}
window.location.reload();
}); });
$("#show-only-in-stock").change(function() $('#show-only-in-stock').on('change', function() {
{ if (this.checked) {
if (this.checked) UpdateUriParam('only_in_stock', 'true');
{ } else {
UpdateUriParam("only_in_stock", "true"); RemoveUriParam('only_in_stock');
} }
else productsTable.draw();
{
RemoveUriParam("only_in_stock");
}
window.location.reload();
}); });
if (GetUriParam('include_disabled')) if (GetUriParam('include_disabled')) {
{ $('#show-disabled').prop('checked', true);
$("#show-disabled").prop('checked', true);
} }
$(document).on('click', '.merge-products-button', function(e) {
var $button = $(e.currentTarget);
var $mergeKeep = $('#merge-products-keep');
$(".merge-products-button").on("click", function(e) var optionId = $button.attr('data-product-id');
{ var optionText = $button.attr('data-product-name');
var productId = $(e.currentTarget).attr("data-product-id");
$("#merge-products-keep").val(productId); if ($mergeKeep.find('option[value="' + optionId + '"]').length) {
$("#merge-products-remove").val(""); $mergeKeep.val(optionId).trigger('change');
$("#merge-products-modal").modal("show"); } else {
var option = new Option(optionText, optionId, true, true);
$mergeKeep.append(option).trigger('change');
}
$('#merge-products-remove').val(null).trigger('change');
$('#merge-products-modal').modal('show');
}); });
$("#merge-products-save-button").on("click", function() $('#merge-products-save-button').on('click', function() {
{ var productIdToKeep = $('#merge-products-keep').val();
var productIdToKeep = $("#merge-products-keep").val(); var productIdToRemove = $('#merge-products-remove').val();
var productIdToRemove = $("#merge-products-remove").val();
Grocy.Api.Post("stock/products/" + productIdToKeep.toString() + "/merge/" + productIdToRemove.toString(), {}, Grocy.Api.Post('stock/products/' + productIdToKeep.toString() + '/merge/' + productIdToRemove.toString(), {},
function(result) function(result) {
{
window.location.href = U('/products'); window.location.href = U('/products');
}, },
function(xhr) function(xhr) {
{
Grocy.FrontendHelpers.ShowGenericError('Error while merging', xhr.response); Grocy.FrontendHelpers.ShowGenericError('Error while merging', xhr.response);
} }
); );
}); });
$('#merge-products-keep, #merge-products-remove').select2({
dropdownParent: $('#merge-products-modal'),
ajax: {
delay: 150,
transport: function(params, success, failure) {
var results_per_page = 10;
var page = params.data.page || 1;
var term = params.data.term || "";
var query = [];
query.push('query%5B%5D=active%3D1');
query.push('limit=' + encodeURIComponent(results_per_page));
query.push('offset=' + encodeURIComponent((page - 1) * results_per_page));
query.push('order=name%3Acollate%20nocase');
if (term.length > 0) {
query.push('search=' + encodeURIComponent(term));
}
Grocy.Api.Get('objects/products' + (query.length > 0 ? '?' + query.join('&') : ''),
function(results, meta) {
success({
results: results.map(function(result) {
return {
id: result.id,
text: result.name
};
}),
pagination: {
more: page * results_per_page < meta.recordsFiltered
}
});
},
function(xhr) {
failure();
}
);
}
}
});

View File

@ -1,16 +1,13 @@
var CurrentProductDetails; var CurrentProductDetails;
$('#save-purchase-button').on('click', function(e) $('#save-purchase-button').on('click', function(e) {
{
e.preventDefault(); e.preventDefault();
if ($(".combobox-menu-visible").length) if ($(".combobox-menu-visible").length) {
{
return; return;
} }
if ($(".combobox-menu-visible").length) if ($(".combobox-menu-visible").length) {
{
return; return;
} }
@ -19,140 +16,116 @@ $('#save-purchase-button').on('click', function(e)
Grocy.FrontendHelpers.BeginUiBusy("purchase-form"); Grocy.FrontendHelpers.BeginUiBusy("purchase-form");
Grocy.Api.Get('stock/products/' + jsonForm.product_id, Grocy.Api.Get('stock/products/' + jsonForm.product_id,
function(productDetails) function(productDetails) {
{
var jsonData = {}; var jsonData = {};
jsonData.amount = jsonForm.amount; jsonData.amount = jsonForm.amount;
jsonData.stock_label_type = jsonForm.stock_label_type; jsonData.stock_label_type = jsonForm.stock_label_type;
if (!Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) if (!Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) {
{
jsonData.price = 0; jsonData.price = 0;
} } else {
else
{
var amount = jsonForm.display_amount; var amount = jsonForm.display_amount;
if (BoolVal(productDetails.product.enable_tare_weight_handling)) if (BoolVal(productDetails.product.enable_tare_weight_handling)) {
{
amount -= parseFloat(productDetails.product.tare_weight); amount -= parseFloat(productDetails.product.tare_weight);
} }
var price = parseFloat(jsonForm.price * $("#qu_id option:selected").attr("data-qu-factor")).toFixed(Grocy.UserSettings.stock_decimal_places_prices); var price = parseFloat(jsonForm.price * $("#qu_id option:selected").attr("data-qu-factor")).toFixed(Grocy.UserSettings.stock_decimal_places_prices);
if ($("input[name='price-type']:checked").val() == "total-price") if ($("input[name='price-type']:checked").val() == "total-price") {
{
price = parseFloat(price / amount).toFixed(Grocy.UserSettings.stock_decimal_places_prices); price = parseFloat(price / amount).toFixed(Grocy.UserSettings.stock_decimal_places_prices);
} }
jsonData.price = price; jsonData.price = price;
} }
if (BoolVal(Grocy.UserSettings.show_purchased_date_on_purchase)) if (BoolVal(Grocy.UserSettings.show_purchased_date_on_purchase)) {
{
jsonData.purchased_date = Grocy.Components.DateTimePicker2.GetValue(); jsonData.purchased_date = Grocy.Components.DateTimePicker2.GetValue();
} }
if (Grocy.Components.DateTimePicker) if (Grocy.Components.DateTimePicker) {
{
jsonData.best_before_date = Grocy.Components.DateTimePicker.GetValue(); jsonData.best_before_date = Grocy.Components.DateTimePicker.GetValue();
} } else {
else
{
jsonData.best_before_date = null; jsonData.best_before_date = null;
} }
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) {
{
jsonData.shopping_location_id = Grocy.Components.ShoppingLocationPicker.GetValue(); jsonData.shopping_location_id = Grocy.Components.ShoppingLocationPicker.GetValue();
} }
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING) if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING) {
{
jsonData.location_id = Grocy.Components.LocationPicker.GetValue(); jsonData.location_id = Grocy.Components.LocationPicker.GetValue();
} }
Grocy.Api.Post('stock/products/' + jsonForm.product_id + '/add', jsonData, Grocy.Api.Post('stock/products/' + jsonForm.product_id + '/add', jsonData,
function(result) function(result) {
{ if ($("#purchase-form").hasAttr("data-used-barcode")) {
if ($("#purchase-form").hasAttr("data-used-barcode")) Grocy.Api.Put('objects/product_barcodes/' + $("#purchase-form").attr("data-used-barcode"), {
{ last_price: $("#price").val()
Grocy.Api.Put('objects/product_barcodes/' + $("#purchase-form").attr("data-used-barcode"), { last_price: $("#price").val() }, },
function(result) function(result) {},
{ }, function(xhr) {}
function(xhr)
{ }
); );
} }
if (BoolVal(Grocy.UserSettings.scan_mode_purchase_enabled)) if (BoolVal(Grocy.UserSettings.scan_mode_purchase_enabled)) {
{
Grocy.UISound.Success(); Grocy.UISound.Success();
} }
if (GetUriParam("flow") == "InplaceAddBarcodeToExistingProduct") if (GetUriParam("flow") == "InplaceAddBarcodeToExistingProduct") {
{
var jsonDataBarcode = {}; var jsonDataBarcode = {};
jsonDataBarcode.barcode = GetUriParam("barcode"); jsonDataBarcode.barcode = GetUriParam("barcode");
jsonDataBarcode.product_id = jsonForm.product_id; jsonDataBarcode.product_id = jsonForm.product_id;
jsonDataBarcode.shopping_location_id = jsonForm.shopping_location_id; jsonDataBarcode.shopping_location_id = jsonForm.shopping_location_id;
Grocy.Api.Post('objects/product_barcodes', jsonDataBarcode, Grocy.Api.Post('objects/product_barcodes', jsonDataBarcode,
function(result) function(result) {
{
$("#flow-info-InplaceAddBarcodeToExistingProduct").addClass("d-none"); $("#flow-info-InplaceAddBarcodeToExistingProduct").addClass("d-none");
$('#barcode-lookup-disabled-hint').addClass('d-none'); $('#barcode-lookup-disabled-hint').addClass('d-none');
$('#barcode-lookup-hint').removeClass('d-none'); $('#barcode-lookup-hint').removeClass('d-none');
window.history.replaceState({}, document.title, U("/purchase")); window.history.replaceState({}, document.title, U("/purchase"));
}, },
function(xhr) function(xhr) {
{
Grocy.FrontendHelpers.EndUiBusy("purchase-form"); Grocy.FrontendHelpers.EndUiBusy("purchase-form");
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response); Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response);
} }
); );
} }
var amountMessage = parseFloat(jsonForm.amount).toLocaleString({ minimumFractionDigits: 0, maximumFractionDigits: Grocy.UserSettings.stock_decimal_places_amounts }); var amountMessage = parseFloat(jsonForm.amount).toLocaleString({
if (BoolVal(productDetails.product.enable_tare_weight_handling)) minimumFractionDigits: 0,
{ maximumFractionDigits: Grocy.UserSettings.stock_decimal_places_amounts
});
if (BoolVal(productDetails.product.enable_tare_weight_handling)) {
amountMessage = parseFloat(jsonForm.amount) - parseFloat(productDetails.stock_amount) - parseFloat(productDetails.product.tare_weight); amountMessage = parseFloat(jsonForm.amount) - parseFloat(productDetails.stock_amount) - parseFloat(productDetails.product.tare_weight);
} }
var successMessage = __t('Added %1$s of %2$s to stock', amountMessage + " " + __n(amountMessage, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural, true), productDetails.product.name) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoStockTransaction(\'' + result[0].transaction_id + '\')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>'; var successMessage = __t('Added %1$s of %2$s to stock', amountMessage + " " + __n(amountMessage, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural, true), productDetails.product.name) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoStockTransaction(\'' + result[0].transaction_id + '\')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>';
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_LABEL_PRINTER) if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_LABEL_PRINTER) {
{ if (Grocy.Webhooks.labelprinter !== undefined) {
if (Grocy.Webhooks.labelprinter !== undefined)
{
if (jsonForm.stock_label_type == 1) // Single label if (jsonForm.stock_label_type == 1) // Single label
{ {
var webhookData = {}; var webhookData = {};
webhookData.product = productDetails.product.name; webhookData.product = productDetails.product.name;
webhookData.grocycode = 'grcy:p:' + jsonForm.product_id + ":" + result[0].stock_id; webhookData.grocycode = 'grcy:p:' + jsonForm.product_id + ":" + result[0].stock_id;
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING) if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING) {
{
webhookData.due_date = __t('DD') + ': ' + result[0].best_before_date; webhookData.due_date = __t('DD') + ': ' + result[0].best_before_date;
} }
Grocy.FrontendHelpers.RunWebhook(Grocy.Webhooks.labelprinter, webhookData); Grocy.FrontendHelpers.RunWebhook(Grocy.Webhooks.labelprinter, webhookData);
} } else if (jsonForm.stock_label_type == 2) // Label per unit
else if (jsonForm.stock_label_type == 2) // Label per unit
{ {
Grocy.Api.Get('stock/transactions/' + result[0].transaction_id, Grocy.Api.Get('stock/transactions/' + result[0].transaction_id,
function(stockEntries) function(stockEntries) {
{ stockEntries.forEach(stockEntry => {
stockEntries.forEach(stockEntry =>
{
var webhookData = {}; var webhookData = {};
webhookData.product = productDetails.product.name; webhookData.product = productDetails.product.name;
webhookData.grocycode = 'grcy:p:' + jsonForm.product_id + ":" + stockEntry.stock_id; webhookData.grocycode = 'grcy:p:' + jsonForm.product_id + ":" + stockEntry.stock_id;
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING) if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING) {
{
webhookData.due_date = __t('DD') + ': ' + result[0].best_before_date; webhookData.due_date = __t('DD') + ': ' + result[0].best_before_date;
} }
Grocy.FrontendHelpers.RunWebhook(Grocy.Webhooks.labelprinter, webhookData); Grocy.FrontendHelpers.RunWebhook(Grocy.Webhooks.labelprinter, webhookData);
}); });
}, },
function(xhr) function(xhr) {
{
console.error(xhr); console.error(xhr);
} }
); );
@ -160,24 +133,19 @@ $('#save-purchase-button').on('click', function(e)
} }
} }
if (GetUriParam("embedded") !== undefined) if (GetUriParam("embedded") !== undefined) {
{
window.parent.postMessage(WindowMessageBag("ProductChanged", jsonForm.product_id), Grocy.BaseUrl); window.parent.postMessage(WindowMessageBag("ProductChanged", jsonForm.product_id), Grocy.BaseUrl);
window.parent.postMessage(WindowMessageBag("AfterItemAdded", GetUriParam("listitemid")), Grocy.BaseUrl); window.parent.postMessage(WindowMessageBag("AfterItemAdded", GetUriParam("listitemid")), Grocy.BaseUrl);
window.parent.postMessage(WindowMessageBag("ShowSuccessMessage", successMessage), Grocy.BaseUrl); window.parent.postMessage(WindowMessageBag("ShowSuccessMessage", successMessage), Grocy.BaseUrl);
window.parent.postMessage(WindowMessageBag("Ready"), Grocy.BaseUrl); window.parent.postMessage(WindowMessageBag("Ready"), Grocy.BaseUrl);
window.parent.postMessage(WindowMessageBag("CloseAllModals"), Grocy.BaseUrl); window.parent.postMessage(WindowMessageBag("CloseAllModals"), Grocy.BaseUrl);
} } else {
else
{
Grocy.FrontendHelpers.EndUiBusy("purchase-form"); Grocy.FrontendHelpers.EndUiBusy("purchase-form");
toastr.success(successMessage); toastr.success(successMessage);
Grocy.Components.ProductPicker.FinishFlow(); Grocy.Components.ProductPicker.FinishFlow();
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING && BoolVal(Grocy.UserSettings.show_warning_on_purchase_when_due_date_is_earlier_than_next)) if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING && BoolVal(Grocy.UserSettings.show_warning_on_purchase_when_due_date_is_earlier_than_next)) {
{ if (moment(jsonData.best_before_date).isBefore(CurrentProductDetails.next_due_date)) {
if (moment(jsonData.best_before_date).isBefore(CurrentProductDetails.next_due_date))
{
toastr.warning(__t("This is due earlier than already in-stock items")); toastr.warning(__t("This is due earlier than already in-stock items"));
} }
} }
@ -189,23 +157,19 @@ $('#save-purchase-button').on('click', function(e)
$(".input-group-productamountpicker").trigger("change"); $(".input-group-productamountpicker").trigger("change");
$('#price').val(''); $('#price').val('');
$("#tare-weight-handling-info").addClass("d-none"); $("#tare-weight-handling-info").addClass("d-none");
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING) if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING) {
{
Grocy.Components.LocationPicker.Clear(); Grocy.Components.LocationPicker.Clear();
} }
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING) if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING) {
{
Grocy.Components.DateTimePicker.Clear(); Grocy.Components.DateTimePicker.Clear();
} }
Grocy.Components.ProductPicker.SetValue(''); Grocy.Components.ProductPicker.Clear();
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) {
{
Grocy.Components.ShoppingLocationPicker.SetValue(''); Grocy.Components.ShoppingLocationPicker.SetValue('');
} }
Grocy.Components.ProductPicker.GetInputElement().focus(); Grocy.Components.ProductPicker.Focus();
Grocy.Components.ProductCard.Refresh(jsonForm.product_id); Grocy.Components.ProductCard.Refresh(jsonForm.product_id);
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_LABEL_PRINTER) if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_LABEL_PRINTER) {
{
$("#stock_label_type").val(0); $("#stock_label_type").val(0);
} }
@ -217,85 +181,66 @@ $('#save-purchase-button').on('click', function(e)
Grocy.FrontendHelpers.ValidateForm('purchase-form'); Grocy.FrontendHelpers.ValidateForm('purchase-form');
} }
}, },
function(xhr) function(xhr) {
{
Grocy.FrontendHelpers.EndUiBusy("purchase-form"); Grocy.FrontendHelpers.EndUiBusy("purchase-form");
console.error(xhr); console.error(xhr);
} }
); );
}, },
function(xhr) function(xhr) {
{
Grocy.FrontendHelpers.EndUiBusy("purchase-form"); Grocy.FrontendHelpers.EndUiBusy("purchase-form");
console.error(xhr); console.error(xhr);
} }
); );
}); });
if (Grocy.Components.ProductPicker !== undefined) if (Grocy.Components.ProductPicker !== undefined) {
{ Grocy.Components.ProductPicker.OnChange(function(e) {
Grocy.Components.ProductPicker.GetPicker().on('change', function(e) if (BoolVal(Grocy.UserSettings.scan_mode_purchase_enabled)) {
{
if (BoolVal(Grocy.UserSettings.scan_mode_purchase_enabled))
{
Grocy.UISound.BarcodeScannerBeep(); Grocy.UISound.BarcodeScannerBeep();
} }
var productId = $(e.target).val(); var productId = $(e.target).val();
if (productId) if (productId) {
{
Grocy.Components.ProductCard.Refresh(productId); Grocy.Components.ProductCard.Refresh(productId);
Grocy.Api.Get('stock/products/' + productId, Grocy.Api.Get('stock/products/' + productId,
function(productDetails) function(productDetails) {
{
CurrentProductDetails = productDetails; CurrentProductDetails = productDetails;
Grocy.Components.ProductAmountPicker.Reload(productDetails.product.id, productDetails.quantity_unit_stock.id); Grocy.Components.ProductAmountPicker.Reload(productDetails.product.id, productDetails.quantity_unit_stock.id);
if (productDetails.product.enable_tare_weight_handling == 1) if (productDetails.product.enable_tare_weight_handling == 1) {
{
Grocy.Components.ProductAmountPicker.SetQuantityUnit(productDetails.quantity_unit_stock.id); Grocy.Components.ProductAmountPicker.SetQuantityUnit(productDetails.quantity_unit_stock.id);
$("#qu_id").attr("disabled", ""); $("#qu_id").attr("disabled", "");
} } else {
else
{
Grocy.Components.ProductAmountPicker.SetQuantityUnit(productDetails.default_quantity_unit_purchase.id); Grocy.Components.ProductAmountPicker.SetQuantityUnit(productDetails.default_quantity_unit_purchase.id);
} }
$('#display_amount').val(parseFloat(Grocy.UserSettings.stock_default_purchase_amount)); $('#display_amount').val(parseFloat(Grocy.UserSettings.stock_default_purchase_amount));
$(".input-group-productamountpicker").trigger("change"); $(".input-group-productamountpicker").trigger("change");
if (GetUriParam("flow") === "shoppinglistitemtostock") if (GetUriParam("flow") === "shoppinglistitemtostock") {
{
Grocy.Components.ProductAmountPicker.SetQuantityUnit(GetUriParam("quId")); Grocy.Components.ProductAmountPicker.SetQuantityUnit(GetUriParam("quId"));
$('#display_amount').val(parseFloat(GetUriParam("amount") * $("#qu_id option:selected").attr("data-qu-factor"))); $('#display_amount').val(parseFloat(GetUriParam("amount") * $("#qu_id option:selected").attr("data-qu-factor")));
} }
$(".input-group-productamountpicker").trigger("change"); $(".input-group-productamountpicker").trigger("change");
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) {
{ if (productDetails.last_shopping_location_id != null) {
if (productDetails.last_shopping_location_id != null)
{
Grocy.Components.ShoppingLocationPicker.SetId(productDetails.last_shopping_location_id); Grocy.Components.ShoppingLocationPicker.SetId(productDetails.last_shopping_location_id);
} } else {
else
{
Grocy.Components.ShoppingLocationPicker.SetId(productDetails.default_shopping_location_id); Grocy.Components.ShoppingLocationPicker.SetId(productDetails.default_shopping_location_id);
} }
} }
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING) if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING) {
{
Grocy.Components.LocationPicker.SetId(productDetails.location.id); Grocy.Components.LocationPicker.SetId(productDetails.location.id);
} }
if (productDetails.last_price == null || productDetails.last_price == 0) if (productDetails.last_price == null || productDetails.last_price == 0) {
{
$("#price").val("") $("#price").val("")
} } else {
else
{
$('#price').val(parseFloat(productDetails.last_price / $("#qu_id option:selected").attr("data-qu-factor"))); $('#price').val(parseFloat(productDetails.last_price / $("#qu_id option:selected").attr("data-qu-factor")));
} }
@ -305,65 +250,52 @@ if (Grocy.Components.ProductPicker !== undefined)
refreshPriceHint(); refreshPriceHint();
if (productDetails.product.enable_tare_weight_handling == 1) if (productDetails.product.enable_tare_weight_handling == 1) {
{
var minAmount = parseFloat(productDetails.product.tare_weight) / $("#qu_id option:selected").attr("data-qu-factor") + parseFloat(productDetails.stock_amount); var minAmount = parseFloat(productDetails.product.tare_weight) / $("#qu_id option:selected").attr("data-qu-factor") + parseFloat(productDetails.stock_amount);
$("#display_amount").attr("min", minAmount); $("#display_amount").attr("min", minAmount);
$("#tare-weight-handling-info").removeClass("d-none"); $("#tare-weight-handling-info").removeClass("d-none");
} } else {
else
{
$("#display_amount").attr("min", Grocy.DefaultMinAmount); $("#display_amount").attr("min", Grocy.DefaultMinAmount);
$("#tare-weight-handling-info").addClass("d-none"); $("#tare-weight-handling-info").addClass("d-none");
} }
PrefillBestBeforeDate(productDetails.product, productDetails.location); PrefillBestBeforeDate(productDetails.product, productDetails.location);
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_LABEL_PRINTER) if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_LABEL_PRINTER) {
{
$("#stock_label_type").val(productDetails.product.default_stock_label_type); $("#stock_label_type").val(productDetails.product.default_stock_label_type);
} }
$("#display_amount").focus(); $("#display_amount").focus();
Grocy.FrontendHelpers.ValidateForm('purchase-form'); Grocy.FrontendHelpers.ValidateForm('purchase-form');
if (GetUriParam("flow") === "shoppinglistitemtostock" && BoolVal(Grocy.UserSettings.shopping_list_to_stock_workflow_auto_submit_when_prefilled) && document.getElementById("purchase-form").checkValidity() === true) if (GetUriParam("flow") === "shoppinglistitemtostock" && BoolVal(Grocy.UserSettings.shopping_list_to_stock_workflow_auto_submit_when_prefilled) && document.getElementById("purchase-form").checkValidity() === true) {
{
$("#save-purchase-button").click(); $("#save-purchase-button").click();
} }
RefreshLocaleNumberInput(); RefreshLocaleNumberInput();
if (document.getElementById("product_id").getAttribute("barcode") != "null") if (document.getElementById("product_id").getAttribute("barcode") != "null") {
{
Grocy.Api.Get('objects/product_barcodes?query[]=barcode=' + document.getElementById("product_id").getAttribute("barcode"), Grocy.Api.Get('objects/product_barcodes?query[]=barcode=' + document.getElementById("product_id").getAttribute("barcode"),
function(barcodeResult) function(barcodeResult) {
{ if (barcodeResult != null) {
if (barcodeResult != null)
{
var barcode = barcodeResult[0]; var barcode = barcodeResult[0];
$("#purchase-form").attr("data-used-barcode", barcode.id); $("#purchase-form").attr("data-used-barcode", barcode.id);
if (barcode != null) if (barcode != null) {
{ if (barcode.amount != null && !barcode.amount.isEmpty()) {
if (barcode.amount != null && !barcode.amount.isEmpty())
{
$("#display_amount").val(barcode.amount); $("#display_amount").val(barcode.amount);
$("#display_amount").select(); $("#display_amount").select();
} }
if (barcode.qu_id != null && !barcode.qu_id.isEmpty()) if (barcode.qu_id != null && !barcode.qu_id.isEmpty()) {
{
Grocy.Components.ProductAmountPicker.SetQuantityUnit(barcode.qu_id); Grocy.Components.ProductAmountPicker.SetQuantityUnit(barcode.qu_id);
} }
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING && barcode.shopping_location_id != null) if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING && barcode.shopping_location_id != null) {
{
Grocy.Components.ShoppingLocationPicker.SetId(barcode.shopping_location_id); Grocy.Components.ShoppingLocationPicker.SetId(barcode.shopping_location_id);
} }
if (barcode.last_price != null && !barcode.last_price.isEmpty()) if (barcode.last_price != null && !barcode.last_price.isEmpty()) {
{
$("#price").val(barcode.last_price); $("#price").val(barcode.last_price);
$("#price-type-total-price").click(); $("#price-type-total-price").click();
} }
@ -376,22 +308,18 @@ if (Grocy.Components.ProductPicker !== undefined)
ScanModeSubmit(false); ScanModeSubmit(false);
}, },
function(xhr) function(xhr) {
{
console.error(xhr); console.error(xhr);
} }
); );
} } else {
else
{
$("#purchase-form").removeAttr("data-used-barcode"); $("#purchase-form").removeAttr("data-used-barcode");
ScanModeSubmit(); ScanModeSubmit();
} }
$('#display_amount').trigger("keyup"); $('#display_amount').trigger("keyup");
}, },
function(xhr) function(xhr) {
{
console.error(xhr); console.error(xhr);
} }
); );
@ -399,51 +327,36 @@ if (Grocy.Components.ProductPicker !== undefined)
}); });
} }
function PrefillBestBeforeDate(product, location) function PrefillBestBeforeDate(product, location) {
{ if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING) {
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING)
{
var dueDays; var dueDays;
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRODUCT_FREEZING && BoolVal(location.is_freezer)) if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRODUCT_FREEZING && BoolVal(location.is_freezer)) {
{
dueDays = product.default_best_before_days_after_freezing; dueDays = product.default_best_before_days_after_freezing;
} } else {
else
{
dueDays = product.default_best_before_days; dueDays = product.default_best_before_days;
} }
dueDays = parseFloat(dueDays); dueDays = parseFloat(dueDays);
if (dueDays != 0) if (dueDays != 0) {
{ if (dueDays == -1) {
if (dueDays == -1) if (!$("#datetimepicker-shortcut").is(":checked")) {
{
if (!$("#datetimepicker-shortcut").is(":checked"))
{
$("#datetimepicker-shortcut").click(); $("#datetimepicker-shortcut").click();
} }
} } else {
else
{
Grocy.Components.DateTimePicker.SetValue(moment().add(dueDays, 'days').format('YYYY-MM-DD')); Grocy.Components.DateTimePicker.SetValue(moment().add(dueDays, 'days').format('YYYY-MM-DD'));
} }
} }
} }
} }
if (Grocy.Components.LocationPicker !== undefined) if (Grocy.Components.LocationPicker !== undefined) {
{ Grocy.Components.LocationPicker.GetPicker().on('change', function(e) {
Grocy.Components.LocationPicker.GetPicker().on('change', function(e) if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRODUCT_FREEZING) {
{
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRODUCT_FREEZING)
{
Grocy.Api.Get('objects/locations/' + Grocy.Components.LocationPicker.GetValue(), Grocy.Api.Get('objects/locations/' + Grocy.Components.LocationPicker.GetValue(),
function(location) function(location) {
{
PrefillBestBeforeDate(CurrentProductDetails.product, location); PrefillBestBeforeDate(CurrentProductDetails.product, location);
}, },
function(xhr) function(xhr) {}
{ }
); );
} }
}); });
@ -454,47 +367,35 @@ RefreshLocaleNumberInput();
$(".input-group-productamountpicker").trigger("change"); $(".input-group-productamountpicker").trigger("change");
Grocy.FrontendHelpers.ValidateForm('purchase-form'); Grocy.FrontendHelpers.ValidateForm('purchase-form');
if (Grocy.Components.ProductPicker) if (Grocy.Components.ProductPicker) {
{ if (Grocy.Components.ProductPicker.InAnyFlow() === false && GetUriParam("embedded") === undefined) {
if (Grocy.Components.ProductPicker.InAnyFlow() === false && GetUriParam("embedded") === undefined) Grocy.Components.ProductPicker.Focus();
{ } else {
Grocy.Components.ProductPicker.GetInputElement().focus(); Grocy.Components.ProductPicker.Validate();
}
else
{
Grocy.Components.ProductPicker.GetPicker().trigger('change');
if (Grocy.Components.ProductPicker.InProductModifyWorkflow()) if (Grocy.Components.ProductPicker.InProductModifyWorkflow()) {
{ Grocy.Components.ProductPicker.Focus();
Grocy.Components.ProductPicker.GetInputElement().focus();
} }
} }
} }
$('#display_amount').on('focus', function(e) $('#display_amount').on('focus', function(e) {
{ if (Grocy.Components.ProductPicker.GetValue().length === 0) {
if (Grocy.Components.ProductPicker.GetValue().length === 0) Grocy.Components.ProductPicker.Focus();
{ } else {
Grocy.Components.ProductPicker.GetInputElement().focus();
}
else
{
$(this).select(); $(this).select();
} }
}); });
$('#price').on('focus', function(e) $('#price').on('focus', function(e) {
{
$(this).select(); $(this).select();
}); });
$('#purchase-form input').keyup(function(event) $('#purchase-form input').keyup(function(event) {
{
Grocy.FrontendHelpers.ValidateForm('purchase-form'); Grocy.FrontendHelpers.ValidateForm('purchase-form');
}); });
$('#purchase-form input').keydown(function(event) $('#purchase-form input').keydown(function(event) {
{
if (event.keyCode === 13) //Enter if (event.keyCode === 13) //Enter
{ {
event.preventDefault(); event.preventDefault();
@ -502,191 +403,154 @@ $('#purchase-form input').keydown(function(event)
if (document.getElementById('purchase-form').checkValidity() === false) //There is at least one validation error if (document.getElementById('purchase-form').checkValidity() === false) //There is at least one validation error
{ {
return false; return false;
} } else {
else
{
$('#save-purchase-button').click(); $('#save-purchase-button').click();
} }
} }
}); });
if (Grocy.Components.DateTimePicker) if (Grocy.Components.DateTimePicker) {
{ Grocy.Components.DateTimePicker.GetInputElement().on('change', function(e) {
Grocy.Components.DateTimePicker.GetInputElement().on('change', function(e)
{
Grocy.FrontendHelpers.ValidateForm('purchase-form'); Grocy.FrontendHelpers.ValidateForm('purchase-form');
}); });
Grocy.Components.DateTimePicker.GetInputElement().on('keypress', function(e) Grocy.Components.DateTimePicker.GetInputElement().on('keypress', function(e) {
{
Grocy.FrontendHelpers.ValidateForm('purchase-form'); Grocy.FrontendHelpers.ValidateForm('purchase-form');
}); });
} }
if (Grocy.Components.DateTimePicker2) if (Grocy.Components.DateTimePicker2) {
{ Grocy.Components.DateTimePicker2.GetInputElement().on('change', function(e) {
Grocy.Components.DateTimePicker2.GetInputElement().on('change', function(e)
{
Grocy.FrontendHelpers.ValidateForm('purchase-form'); Grocy.FrontendHelpers.ValidateForm('purchase-form');
}); });
Grocy.Components.DateTimePicker2.GetInputElement().on('keypress', function(e) Grocy.Components.DateTimePicker2.GetInputElement().on('keypress', function(e) {
{
Grocy.FrontendHelpers.ValidateForm('purchase-form'); Grocy.FrontendHelpers.ValidateForm('purchase-form');
}); });
Grocy.Components.DateTimePicker2.GetInputElement().trigger("input"); Grocy.Components.DateTimePicker2.GetInputElement().trigger("input");
} }
$('#price').on('keyup', function(e) $('#price').on('keyup', function(e) {
{
refreshPriceHint(); refreshPriceHint();
}); });
$('#price-type-unit-price').on('change', function(e) $('#price-type-unit-price').on('change', function(e) {
{
refreshPriceHint(); refreshPriceHint();
}); });
$('#price-type-total-price').on('change', function(e) $('#price-type-total-price').on('change', function(e) {
{
refreshPriceHint(); refreshPriceHint();
}); });
$('#display_amount').on('change', function(e) $('#display_amount').on('change', function(e) {
{
refreshPriceHint(); refreshPriceHint();
Grocy.FrontendHelpers.ValidateForm('purchase-form'); Grocy.FrontendHelpers.ValidateForm('purchase-form');
}); });
function refreshPriceHint() function refreshPriceHint() {
{ if ($('#amount').val() == 0 || $('#price').val() == 0) {
if ($('#amount').val() == 0 || $('#price').val() == 0)
{
$('#price-hint').text(""); $('#price-hint').text("");
return; return;
} }
if ($("input[name='price-type']:checked").val() == "total-price" || $("#qu_id").attr("data-destination-qu-name") != $("#qu_id option:selected").text()) if ($("input[name='price-type']:checked").val() == "total-price" || $("#qu_id").attr("data-destination-qu-name") != $("#qu_id option:selected").text()) {
{
var amount = $('#display_amount').val(); var amount = $('#display_amount').val();
if (BoolVal(CurrentProductDetails.product.enable_tare_weight_handling)) if (BoolVal(CurrentProductDetails.product.enable_tare_weight_handling)) {
{
amount -= parseFloat(CurrentProductDetails.product.tare_weight); amount -= parseFloat(CurrentProductDetails.product.tare_weight);
} }
var price = parseFloat($('#price').val() * $("#qu_id option:selected").attr("data-qu-factor")).toFixed(Grocy.UserSettings.stock_decimal_places_prices); var price = parseFloat($('#price').val() * $("#qu_id option:selected").attr("data-qu-factor")).toFixed(Grocy.UserSettings.stock_decimal_places_prices);
if ($("input[name='price-type']:checked").val() == "total-price") if ($("input[name='price-type']:checked").val() == "total-price") {
{
price = parseFloat(price / amount).toFixed(Grocy.UserSettings.stock_decimal_places_prices); price = parseFloat(price / amount).toFixed(Grocy.UserSettings.stock_decimal_places_prices);
} }
$('#price-hint').text(__t('means %1$s per %2$s', price.toLocaleString(undefined, { style: "currency", currency: Grocy.Currency, minimumFractionDigits: Grocy.UserSettings.stock_decimal_places_prices, maximumFractionDigits: Grocy.UserSettings.stock_decimal_places_prices }), $("#qu_id").attr("data-destination-qu-name"))); $('#price-hint').text(__t('means %1$s per %2$s', price.toLocaleString(undefined, {
} style: "currency",
else currency: Grocy.Currency,
{ minimumFractionDigits: Grocy.UserSettings.stock_decimal_places_prices,
maximumFractionDigits: Grocy.UserSettings.stock_decimal_places_prices
}), $("#qu_id").attr("data-destination-qu-name")));
} else {
$('#price-hint').text(""); $('#price-hint').text("");
} }
}; };
function UndoStockBooking(bookingId) function UndoStockBooking(bookingId) {
{
Grocy.Api.Post('stock/bookings/' + bookingId.toString() + '/undo', {}, Grocy.Api.Post('stock/bookings/' + bookingId.toString() + '/undo', {},
function(result) function(result) {
{
toastr.success(__t("Booking successfully undone")); toastr.success(__t("Booking successfully undone"));
Grocy.Api.Get('stock/bookings/' + bookingId.toString(), Grocy.Api.Get('stock/bookings/' + bookingId.toString(),
function(result) function(result) {
{
window.postMessage(WindowMessageBag("ProductChanged", result.product_id), Grocy.BaseUrl); window.postMessage(WindowMessageBag("ProductChanged", result.product_id), Grocy.BaseUrl);
}, },
function(xhr) function(xhr) {
{
console.error(xhr); console.error(xhr);
} }
); );
}, },
function(xhr) function(xhr) {
{
console.error(xhr); console.error(xhr);
} }
); );
}; };
function UndoStockTransaction(transactionId) function UndoStockTransaction(transactionId) {
{
Grocy.Api.Post('stock/transactions/' + transactionId.toString() + '/undo', {}, Grocy.Api.Post('stock/transactions/' + transactionId.toString() + '/undo', {},
function(result) function(result) {
{
toastr.success(__t("Transaction successfully undone")); toastr.success(__t("Transaction successfully undone"));
Grocy.Api.Get('stock/transactions/' + transactionId.toString(), Grocy.Api.Get('stock/transactions/' + transactionId.toString(),
function(result) function(result) {
{
window.postMessage(WindowMessageBag("ProductChanged", result[0].product_id), Grocy.BaseUrl); window.postMessage(WindowMessageBag("ProductChanged", result[0].product_id), Grocy.BaseUrl);
}, },
function(xhr) function(xhr) {
{
console.error(xhr); console.error(xhr);
} }
); );
}, },
function(xhr) function(xhr) {
{
console.error(xhr); console.error(xhr);
} }
); );
}; };
$("#scan-mode").on("change", function(e) $("#scan-mode").on("change", function(e) {
{ if ($(this).prop("checked")) {
if ($(this).prop("checked"))
{
Grocy.UISound.AskForPermission(); Grocy.UISound.AskForPermission();
} }
}); });
$("#scan-mode-button").on("click", function(e) $("#scan-mode-button").on("click", function(e) {
{
document.activeElement.blur(); document.activeElement.blur();
$("#scan-mode").click(); $("#scan-mode").click();
$("#scan-mode-button").toggleClass("btn-success").toggleClass("btn-danger"); $("#scan-mode-button").toggleClass("btn-success").toggleClass("btn-danger");
if ($("#scan-mode").prop("checked")) if ($("#scan-mode").prop("checked")) {
{
$("#scan-mode-status").text(__t("on")); $("#scan-mode-status").text(__t("on"));
} } else {
else
{
$("#scan-mode-status").text(__t("off")); $("#scan-mode-status").text(__t("off"));
} }
}); });
$('#qu_id').on('change', function(e) $('#qu_id').on('change', function(e) {
{
var priceTypeUnitPrice = $("#price-type-unit-price"); var priceTypeUnitPrice = $("#price-type-unit-price");
var priceTypeUnitPriceLabel = $("[for=" + priceTypeUnitPrice.attr("id") + "]"); var priceTypeUnitPriceLabel = $("[for=" + priceTypeUnitPrice.attr("id") + "]");
priceTypeUnitPriceLabel.text($("#qu_id option:selected").text() + " " + __t("price")); priceTypeUnitPriceLabel.text($("#qu_id option:selected").text() + " " + __t("price"));
refreshPriceHint(); refreshPriceHint();
}); });
function ScanModeSubmit(singleUnit = true) function ScanModeSubmit(singleUnit = true) {
{ if (BoolVal(Grocy.UserSettings.scan_mode_purchase_enabled)) {
if (BoolVal(Grocy.UserSettings.scan_mode_purchase_enabled)) if (singleUnit) {
{
if (singleUnit)
{
$("#display_amount").val(1); $("#display_amount").val(1);
$(".input-group-productamountpicker").trigger("change"); $(".input-group-productamountpicker").trigger("change");
} }
Grocy.FrontendHelpers.ValidateForm("purchase-form"); Grocy.FrontendHelpers.ValidateForm("purchase-form");
if (document.getElementById("purchase-form").checkValidity() === true) if (document.getElementById("purchase-form").checkValidity() === true) {
{
$('#save-purchase-button').click(); $('#save-purchase-button').click();
} } else {
else
{
toastr.warning(__t("Scan mode is on but not all required fields could be populated automatically")); toastr.warning(__t("Scan mode is on but not all required fields could be populated automatically"));
Grocy.UISound.Error(); Grocy.UISound.Error();
} }

View File

@ -1,11 +1,9 @@
Grocy.RecipePosFormInitialLoadDone = false; Grocy.RecipePosFormInitialLoadDone = false;
$('#save-recipe-pos-button').on('click', function(e) $('#save-recipe-pos-button').on('click', function(e) {
{
e.preventDefault(); e.preventDefault();
if ($(".combobox-menu-visible").length) if ($(".combobox-menu-visible").length) {
{
return; return;
} }
@ -15,31 +13,24 @@ $('#save-recipe-pos-button').on('click', function(e)
Grocy.FrontendHelpers.BeginUiBusy("recipe-pos-form"); Grocy.FrontendHelpers.BeginUiBusy("recipe-pos-form");
if (Grocy.EditMode === 'create') if (Grocy.EditMode === 'create') {
{
Grocy.Api.Post('objects/recipes_pos', jsonData, Grocy.Api.Post('objects/recipes_pos', jsonData,
function(result) function(result) {
{
window.parent.postMessage(WindowMessageBag("IngredientsChanged"), Grocy.BaseUrl); window.parent.postMessage(WindowMessageBag("IngredientsChanged"), Grocy.BaseUrl);
window.parent.postMessage(WindowMessageBag("CloseAllModals"), Grocy.BaseUrl); window.parent.postMessage(WindowMessageBag("CloseAllModals"), Grocy.BaseUrl);
}, },
function(xhr) function(xhr) {
{
Grocy.FrontendHelpers.EndUiBusy("recipe-pos-form"); Grocy.FrontendHelpers.EndUiBusy("recipe-pos-form");
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response) Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response)
} }
); );
} } else {
else
{
Grocy.Api.Put('objects/recipes_pos/' + Grocy.EditObjectId, jsonData, Grocy.Api.Put('objects/recipes_pos/' + Grocy.EditObjectId, jsonData,
function(result) function(result) {
{
window.parent.postMessage(WindowMessageBag("IngredientsChanged"), Grocy.BaseUrl); window.parent.postMessage(WindowMessageBag("IngredientsChanged"), Grocy.BaseUrl);
window.parent.postMessage(WindowMessageBag("CloseAllModals"), Grocy.BaseUrl); window.parent.postMessage(WindowMessageBag("CloseAllModals"), Grocy.BaseUrl);
}, },
function(xhr) function(xhr) {
{
Grocy.FrontendHelpers.EndUiBusy("recipe-pos-form"); Grocy.FrontendHelpers.EndUiBusy("recipe-pos-form");
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response) Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response)
} }
@ -47,33 +38,25 @@ $('#save-recipe-pos-button').on('click', function(e)
} }
}); });
Grocy.Components.ProductPicker.GetPicker().on('change', function(e) Grocy.Components.ProductPicker.OnChange(function(e) {
{
var productId = $(e.target).val(); var productId = $(e.target).val();
if (productId) if (productId) {
{
Grocy.Components.ProductCard.Refresh(productId); Grocy.Components.ProductCard.Refresh(productId);
Grocy.Api.Get('stock/products/' + productId, Grocy.Api.Get('stock/products/' + productId,
function(productDetails) function(productDetails) {
{ if (!Grocy.RecipePosFormInitialLoadDone) {
if (!Grocy.RecipePosFormInitialLoadDone)
{
Grocy.Components.ProductAmountPicker.Reload(productDetails.product.id, productDetails.quantity_unit_stock.id, true); Grocy.Components.ProductAmountPicker.Reload(productDetails.product.id, productDetails.quantity_unit_stock.id, true);
} } else {
else
{
Grocy.Components.ProductAmountPicker.Reload(productDetails.product.id, productDetails.quantity_unit_stock.id); Grocy.Components.ProductAmountPicker.Reload(productDetails.product.id, productDetails.quantity_unit_stock.id);
} }
if (Grocy.Mode == "create") if (Grocy.Mode == "create") {
{
$("#not_check_stock_fulfillment").prop("checked", productDetails.product.not_check_stock_fulfillment_for_recipes == 1); $("#not_check_stock_fulfillment").prop("checked", productDetails.product.not_check_stock_fulfillment_for_recipes == 1);
} }
if (!$("#only_check_single_unit_in_stock").prop("checked") && Grocy.RecipePosFormInitialLoadDone) if (!$("#only_check_single_unit_in_stock").prop("checked") && Grocy.RecipePosFormInitialLoadDone) {
{
Grocy.Components.ProductAmountPicker.SetQuantityUnit(productDetails.quantity_unit_stock.id); Grocy.Components.ProductAmountPicker.SetQuantityUnit(productDetails.quantity_unit_stock.id);
} }
@ -81,8 +64,7 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
Grocy.FrontendHelpers.ValidateForm('recipe-pos-form'); Grocy.FrontendHelpers.ValidateForm('recipe-pos-form');
Grocy.RecipePosFormInitialLoadDone = true; Grocy.RecipePosFormInitialLoadDone = true;
}, },
function(xhr) function(xhr) {
{
console.error(xhr); console.error(xhr);
} }
); );
@ -91,41 +73,32 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
Grocy.FrontendHelpers.ValidateForm('recipe-pos-form'); Grocy.FrontendHelpers.ValidateForm('recipe-pos-form');
if (Grocy.Components.ProductPicker.InProductAddWorkflow() === false) if (Grocy.Components.ProductPicker.InProductAddWorkflow() === false) {
{ Grocy.Components.ProductPicker.Focus();
Grocy.Components.ProductPicker.GetInputElement().focus();
} }
Grocy.Components.ProductPicker.GetPicker().trigger('change'); Grocy.Components.ProductPicker.Validate();
if (Grocy.EditMode == "create") if (Grocy.EditMode == "create") {
{
Grocy.RecipePosFormInitialLoadDone = true; Grocy.RecipePosFormInitialLoadDone = true;
} }
$('#display_amount').on('focus', function(e) $('#display_amount').on('focus', function(e) {
{ if (Grocy.Components.ProductPicker.GetValue().length === 0) {
if (Grocy.Components.ProductPicker.GetValue().length === 0) Grocy.Components.ProductPicker.Focus();
{ } else {
Grocy.Components.ProductPicker.GetInputElement().focus();
}
else
{
$(this).select(); $(this).select();
} }
}); });
$('#recipe-pos-form input').keyup(function(event) $('#recipe-pos-form input').keyup(function(event) {
{
Grocy.FrontendHelpers.ValidateForm('recipe-pos-form'); Grocy.FrontendHelpers.ValidateForm('recipe-pos-form');
}); });
$('#qu_id').change(function(event) $('#qu_id').change(function(event) {
{
Grocy.FrontendHelpers.ValidateForm('recipe-pos-form'); Grocy.FrontendHelpers.ValidateForm('recipe-pos-form');
}); });
$('#recipe-pos-form input').keydown(function(event) $('#recipe-pos-form input').keydown(function(event) {
{
if (event.keyCode === 13) //Enter if (event.keyCode === 13) //Enter
{ {
event.preventDefault(); event.preventDefault();
@ -133,32 +106,25 @@ $('#recipe-pos-form input').keydown(function(event)
if (document.getElementById('recipe-pos-form').checkValidity() === false) //There is at least one validation error if (document.getElementById('recipe-pos-form').checkValidity() === false) //There is at least one validation error
{ {
return false; return false;
} } else {
else
{
$('#save-recipe-pos-button').click(); $('#save-recipe-pos-button').click();
} }
} }
}); });
$("#only_check_single_unit_in_stock").on("change", function() $("#only_check_single_unit_in_stock").on("change", function() {
{ if (this.checked) {
if (this.checked)
{
$("#display_amount").attr("min", Grocy.DefaultMinAmount); $("#display_amount").attr("min", Grocy.DefaultMinAmount);
Grocy.Components.ProductAmountPicker.AllowAnyQu(true); Grocy.Components.ProductAmountPicker.AllowAnyQu(true);
Grocy.FrontendHelpers.ValidateForm("recipe-pos-form"); Grocy.FrontendHelpers.ValidateForm("recipe-pos-form");
} } else {
else
{
$("#display_amount").attr("min", "0"); $("#display_amount").attr("min", "0");
Grocy.Components.ProductPicker.GetPicker().trigger("change"); // Selects the default quantity unit of the selected product Grocy.Components.ProductPicker.Validate(); // Selects the default quantity unit of the selected product
Grocy.Components.ProductAmountPicker.AllowAnyQuEnabled = false; Grocy.Components.ProductAmountPicker.AllowAnyQuEnabled = false;
Grocy.FrontendHelpers.ValidateForm("recipe-pos-form"); Grocy.FrontendHelpers.ValidateForm("recipe-pos-form");
} }
}); });
if ($("#only_check_single_unit_in_stock").prop("checked")) if ($("#only_check_single_unit_in_stock").prop("checked")) {
{
Grocy.Components.ProductAmountPicker.AllowAnyQu(true); Grocy.Components.ProductAmountPicker.AllowAnyQu(true);
} }

View File

@ -1,47 +1,40 @@
Grocy.ShoppingListItemFormInitialLoadDone = false; Grocy.ShoppingListItemFormInitialLoadDone = false;
$('#save-shoppinglist-button').on('click', function(e) $('#save-shoppinglist-button').on('click', function(e) {
{
e.preventDefault(); e.preventDefault();
if ($(".combobox-menu-visible").length) if ($(".combobox-menu-visible").length) {
{
return; return;
} }
var jsonData = $('#shoppinglist-form').serializeJSON(); var jsonData = $('#shoppinglist-form').serializeJSON();
var displayAmount = parseFloat(jsonData.display_amount); var displayAmount = parseFloat(jsonData.display_amount);
if (!jsonData.product_id) if (!jsonData.product_id) {
{
jsonData.amount = jsonData.display_amount; jsonData.amount = jsonData.display_amount;
} }
delete jsonData.display_amount; delete jsonData.display_amount;
Grocy.FrontendHelpers.BeginUiBusy("shoppinglist-form"); Grocy.FrontendHelpers.BeginUiBusy("shoppinglist-form");
if (GetUriParam("flow") === "InplaceAddBarcodeToExistingProduct") if (GetUriParam("flow") === "InplaceAddBarcodeToExistingProduct") {
{
var jsonDataBarcode = {}; var jsonDataBarcode = {};
jsonDataBarcode.barcode = GetUriParam("barcode"); jsonDataBarcode.barcode = GetUriParam("barcode");
jsonDataBarcode.product_id = jsonData.product_id; jsonDataBarcode.product_id = jsonData.product_id;
Grocy.Api.Post('objects/product_barcodes', jsonDataBarcode, Grocy.Api.Post('objects/product_barcodes', jsonDataBarcode,
function(result) function(result) {
{
$("#flow-info-InplaceAddBarcodeToExistingProduct").addClass("d-none"); $("#flow-info-InplaceAddBarcodeToExistingProduct").addClass("d-none");
$('#barcode-lookup-disabled-hint').addClass('d-none'); $('#barcode-lookup-disabled-hint').addClass('d-none');
$('#barcode-lookup-hint').removeClass('d-none'); $('#barcode-lookup-hint').removeClass('d-none');
}, },
function(xhr) function(xhr) {
{
Grocy.FrontendHelpers.EndUiBusy("shoppinglist-form"); Grocy.FrontendHelpers.EndUiBusy("shoppinglist-form");
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response); Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response);
} }
); );
} }
if (GetUriParam("updateexistingproduct") !== undefined) if (GetUriParam("updateexistingproduct") !== undefined) {
{
jsonData.product_amount = jsonData.amount; jsonData.product_amount = jsonData.amount;
delete jsonData.amount; delete jsonData.amount;
@ -49,118 +42,96 @@ $('#save-shoppinglist-button').on('click', function(e)
delete jsonData.shopping_list_id; delete jsonData.shopping_list_id;
Grocy.Api.Post('stock/shoppinglist/add-product', jsonData, Grocy.Api.Post('stock/shoppinglist/add-product', jsonData,
function(result) function(result) {
{
Grocy.EditObjectId = result.created_object_id; Grocy.EditObjectId = result.created_object_id;
Grocy.Components.UserfieldsForm.Save(); Grocy.Components.UserfieldsForm.Save();
if (GetUriParam("embedded") !== undefined) if (GetUriParam("embedded") !== undefined) {
{
Grocy.Api.Get('stock/products/' + jsonData.product_id, Grocy.Api.Get('stock/products/' + jsonData.product_id,
function(productDetails) function(productDetails) {
{ window.parent.postMessage(WindowMessageBag("ShowSuccessMessage", __t("Added %1$s of %2$s to the shopping list \"%3$s\"", displayAmount.toLocaleString({
window.parent.postMessage(WindowMessageBag("ShowSuccessMessage", __t("Added %1$s of %2$s to the shopping list \"%3$s\"", displayAmount.toLocaleString({ minimumFractionDigits: 0, maximumFractionDigits: Grocy.UserSettings.stock_decimal_places_amounts }) + " " + __n(displayAmount, $("#qu_id option:selected").text(), $("#qu_id option:selected").attr("data-qu-name-plural"), true), productDetails.product.name, $("#shopping_list_id option:selected").text())), Grocy.BaseUrl); minimumFractionDigits: 0,
maximumFractionDigits: Grocy.UserSettings.stock_decimal_places_amounts
}) + " " + __n(displayAmount, $("#qu_id option:selected").text(), $("#qu_id option:selected").attr("data-qu-name-plural"), true), productDetails.product.name, $("#shopping_list_id option:selected").text())), Grocy.BaseUrl);
window.parent.postMessage(WindowMessageBag("ShoppingListChanged", $("#shopping_list_id").val().toString()), Grocy.BaseUrl); window.parent.postMessage(WindowMessageBag("ShoppingListChanged", $("#shopping_list_id").val().toString()), Grocy.BaseUrl);
window.parent.postMessage(WindowMessageBag("CloseAllModals"), Grocy.BaseUrl); window.parent.postMessage(WindowMessageBag("CloseAllModals"), Grocy.BaseUrl);
}, },
function(xhr) function(xhr) {
{
console.error(xhr); console.error(xhr);
} }
); );
} } else {
else
{
window.location.href = U('/shoppinglist?list=' + $("#shopping_list_id").val().toString()); window.location.href = U('/shoppinglist?list=' + $("#shopping_list_id").val().toString());
} }
}, },
function(xhr) function(xhr) {
{
Grocy.FrontendHelpers.EndUiBusy("shoppinglist-form"); Grocy.FrontendHelpers.EndUiBusy("shoppinglist-form");
console.error(xhr); console.error(xhr);
} }
); );
} } else if (Grocy.EditMode === 'create') {
else if (Grocy.EditMode === 'create')
{
Grocy.Api.Post('objects/shopping_list', jsonData, Grocy.Api.Post('objects/shopping_list', jsonData,
function(result) function(result) {
{
Grocy.EditObjectId = result.created_object_id; Grocy.EditObjectId = result.created_object_id;
Grocy.Components.UserfieldsForm.Save(); Grocy.Components.UserfieldsForm.Save();
if (GetUriParam("embedded") !== undefined) if (GetUriParam("embedded") !== undefined) {
{ if (jsonData.product_id) {
if (jsonData.product_id)
{
Grocy.Api.Get('stock/products/' + jsonData.product_id, Grocy.Api.Get('stock/products/' + jsonData.product_id,
function(productDetails) function(productDetails) {
{ window.parent.postMessage(WindowMessageBag("ShowSuccessMessage", __t("Added %1$s of %2$s to the shopping list \"%3$s\"", displayAmount.toLocaleString({
window.parent.postMessage(WindowMessageBag("ShowSuccessMessage", __t("Added %1$s of %2$s to the shopping list \"%3$s\"", displayAmount.toLocaleString({ minimumFractionDigits: 0, maximumFractionDigits: Grocy.UserSettings.stock_decimal_places_amounts }) + " " + __n(displayAmount, $("#qu_id option:selected").text(), $("#qu_id option:selected").attr("data-qu-name-plural"), true), productDetails.product.name, $("#shopping_list_id option:selected").text())), Grocy.BaseUrl); minimumFractionDigits: 0,
maximumFractionDigits: Grocy.UserSettings.stock_decimal_places_amounts
}) + " " + __n(displayAmount, $("#qu_id option:selected").text(), $("#qu_id option:selected").attr("data-qu-name-plural"), true), productDetails.product.name, $("#shopping_list_id option:selected").text())), Grocy.BaseUrl);
window.parent.postMessage(WindowMessageBag("ShoppingListChanged", $("#shopping_list_id").val().toString()), Grocy.BaseUrl); window.parent.postMessage(WindowMessageBag("ShoppingListChanged", $("#shopping_list_id").val().toString()), Grocy.BaseUrl);
window.parent.postMessage(WindowMessageBag("CloseAllModals"), Grocy.BaseUrl); window.parent.postMessage(WindowMessageBag("CloseAllModals"), Grocy.BaseUrl);
}, },
function(xhr) function(xhr) {
{
console.error(xhr); console.error(xhr);
} }
); );
} } else {
else
{
window.parent.postMessage(WindowMessageBag("ShoppingListChanged", $("#shopping_list_id").val().toString()), Grocy.BaseUrl); window.parent.postMessage(WindowMessageBag("ShoppingListChanged", $("#shopping_list_id").val().toString()), Grocy.BaseUrl);
window.parent.postMessage(WindowMessageBag("CloseAllModals"), Grocy.BaseUrl); window.parent.postMessage(WindowMessageBag("CloseAllModals"), Grocy.BaseUrl);
} }
} } else {
else
{
window.location.href = U('/shoppinglist?list=' + $("#shopping_list_id").val().toString()); window.location.href = U('/shoppinglist?list=' + $("#shopping_list_id").val().toString());
} }
}, },
function(xhr) function(xhr) {
{
Grocy.FrontendHelpers.EndUiBusy("shoppinglist-form"); Grocy.FrontendHelpers.EndUiBusy("shoppinglist-form");
console.error(xhr); console.error(xhr);
} }
); );
} } else {
else
{
Grocy.Api.Put('objects/shopping_list/' + Grocy.EditObjectId, jsonData, Grocy.Api.Put('objects/shopping_list/' + Grocy.EditObjectId, jsonData,
function(result) function(result) {
{
Grocy.Components.UserfieldsForm.Save(); Grocy.Components.UserfieldsForm.Save();
if (GetUriParam("embedded") !== undefined) if (GetUriParam("embedded") !== undefined) {
{ if (jsonData.product_id) {
if (jsonData.product_id)
{
Grocy.Api.Get('stock/products/' + jsonData.product_id, Grocy.Api.Get('stock/products/' + jsonData.product_id,
function(productDetails) function(productDetails) {
{ window.parent.postMessage(WindowMessageBag("ShowSuccessMessage", __t("Added %1$s of %2$s to the shopping list \"%3$s\"", displayAmount.toLocaleString({
window.parent.postMessage(WindowMessageBag("ShowSuccessMessage", __t("Added %1$s of %2$s to the shopping list \"%3$s\"", displayAmount.toLocaleString({ minimumFractionDigits: 0, maximumFractionDigits: Grocy.UserSettings.stock_decimal_places_amounts }) + " " + __n(displayAmount, $("#qu_id option:selected").text(), $("#qu_id option:selected").attr("data-qu-name-plural"), true), productDetails.product.name, $("#shopping_list_id option:selected").text())), Grocy.BaseUrl); minimumFractionDigits: 0,
maximumFractionDigits: Grocy.UserSettings.stock_decimal_places_amounts
}) + " " + __n(displayAmount, $("#qu_id option:selected").text(), $("#qu_id option:selected").attr("data-qu-name-plural"), true), productDetails.product.name, $("#shopping_list_id option:selected").text())), Grocy.BaseUrl);
window.parent.postMessage(WindowMessageBag("ShoppingListChanged", $("#shopping_list_id").val().toString()), Grocy.BaseUrl); window.parent.postMessage(WindowMessageBag("ShoppingListChanged", $("#shopping_list_id").val().toString()), Grocy.BaseUrl);
window.parent.postMessage(WindowMessageBag("CloseAllModals"), Grocy.BaseUrl); window.parent.postMessage(WindowMessageBag("CloseAllModals"), Grocy.BaseUrl);
}, },
function(xhr) function(xhr) {
{
console.error(xhr); console.error(xhr);
} }
); );
} } else {
else
{
window.parent.postMessage(WindowMessageBag("ShoppingListChanged", $("#shopping_list_id").val().toString()), Grocy.BaseUrl); window.parent.postMessage(WindowMessageBag("ShoppingListChanged", $("#shopping_list_id").val().toString()), Grocy.BaseUrl);
window.parent.postMessage(WindowMessageBag("CloseAllModals"), Grocy.BaseUrl); window.parent.postMessage(WindowMessageBag("CloseAllModals"), Grocy.BaseUrl);
} }
} } else {
else
{
window.location.href = U('/shoppinglist?list=' + $("#shopping_list_id").val().toString()); window.location.href = U('/shoppinglist?list=' + $("#shopping_list_id").val().toString());
} }
}, },
function(xhr) function(xhr) {
{
Grocy.FrontendHelpers.EndUiBusy("shoppinglist-form"); Grocy.FrontendHelpers.EndUiBusy("shoppinglist-form");
console.error(xhr); console.error(xhr);
} }
@ -168,74 +139,57 @@ $('#save-shoppinglist-button').on('click', function(e)
} }
}); });
Grocy.Components.ProductPicker.GetPicker().on('change', function(e) Grocy.Components.ProductPicker.OnChange(function(e) {
{
var productId = $(e.target).val(); var productId = $(e.target).val();
if (productId) if (productId) {
{
Grocy.Api.Get('stock/products/' + productId, Grocy.Api.Get('stock/products/' + productId,
function(productDetails) function(productDetails) {
{ if (!Grocy.ShoppingListItemFormInitialLoadDone) {
if (!Grocy.ShoppingListItemFormInitialLoadDone)
{
Grocy.Components.ProductAmountPicker.Reload(productDetails.product.id, productDetails.quantity_unit_stock.id, true); Grocy.Components.ProductAmountPicker.Reload(productDetails.product.id, productDetails.quantity_unit_stock.id, true);
} } else {
else
{
Grocy.Components.ProductAmountPicker.Reload(productDetails.product.id, productDetails.quantity_unit_stock.id); Grocy.Components.ProductAmountPicker.Reload(productDetails.product.id, productDetails.quantity_unit_stock.id);
Grocy.Components.ProductAmountPicker.SetQuantityUnit(productDetails.default_quantity_unit_purchase.id); Grocy.Components.ProductAmountPicker.SetQuantityUnit(productDetails.default_quantity_unit_purchase.id);
} }
if ($("#display_amount").val().toString().isEmpty()) if ($("#display_amount").val().toString().isEmpty()) {
{
$("#display_amount").val(1); $("#display_amount").val(1);
$("#display_amount").trigger("change"); $("#display_amount").trigger("change");
} }
$('#display_amount').focus(); $('#display_amount').trigger('focus');
Grocy.FrontendHelpers.ValidateForm('shoppinglist-form'); Grocy.FrontendHelpers.ValidateForm('shoppinglist-form');
Grocy.ShoppingListItemFormInitialLoadDone = true; Grocy.ShoppingListItemFormInitialLoadDone = true;
}, },
function(xhr) function(xhr) {
{
console.error(xhr); console.error(xhr);
} }
); );
} }
// TODO: what is the point of this?
$("#note").trigger("input"); $("#note").trigger("input");
$("#product_id").trigger("input"); // Grocy.Components.ProductPicker.GetPicker().trigger("input");
}); });
Grocy.FrontendHelpers.ValidateForm('shoppinglist-form'); Grocy.FrontendHelpers.ValidateForm('shoppinglist-form');
setTimeout(function() setTimeout(function() {
{ Grocy.Components.ProductPicker.Focus();
Grocy.Components.ProductPicker.GetInputElement().focus();
}, 250); }, 250);
if (Grocy.EditMode === "edit") if (Grocy.EditMode == "create") {
{
Grocy.Components.ProductPicker.GetPicker().trigger('change');
}
if (Grocy.EditMode == "create")
{
Grocy.ShoppingListItemFormInitialLoadDone = true; Grocy.ShoppingListItemFormInitialLoadDone = true;
} }
$('#display_amount').on('focus', function(e) $('#display_amount').on('focus', function(e) {
{ $(this).trigger('select');
$(this).select();
}); });
$('#shoppinglist-form input').keyup(function(event) $('#shoppinglist-form input').on('keyup', function(event) {
{
Grocy.FrontendHelpers.ValidateForm('shoppinglist-form'); Grocy.FrontendHelpers.ValidateForm('shoppinglist-form');
}); });
$('#shoppinglist-form input').keydown(function(event) $('#shoppinglist-form input').on('keydown', function(event) {
{
if (event.keyCode === 13) //Enter if (event.keyCode === 13) //Enter
{ {
event.preventDefault(); event.preventDefault();
@ -243,52 +197,48 @@ $('#shoppinglist-form input').keydown(function(event)
if (document.getElementById('shoppinglist-form').checkValidity() === false) //There is at least one validation error if (document.getElementById('shoppinglist-form').checkValidity() === false) //There is at least one validation error
{ {
return false; return false;
} } else {
else
{
$('#save-shoppinglist-button').click(); $('#save-shoppinglist-button').click();
} }
} }
}); });
if (GetUriParam("list") !== undefined) if (GetUriParam("list") !== undefined) {
{
$("#shopping_list_id").val(GetUriParam("list")); $("#shopping_list_id").val(GetUriParam("list"));
} }
if (GetUriParam("amount") !== undefined) if (GetUriParam("amount") !== undefined) {
{
$("#display_amount").val(parseFloat(GetUriParam("amount"))); $("#display_amount").val(parseFloat(GetUriParam("amount")));
RefreshLocaleNumberInput(); RefreshLocaleNumberInput();
$(".input-group-productamountpicker").trigger("change"); $(".input-group-productamountpicker").trigger("change");
Grocy.FrontendHelpers.ValidateForm('shoppinglist-form'); Grocy.FrontendHelpers.ValidateForm('shoppinglist-form');
} }
if (GetUriParam("embedded") !== undefined) if (GetUriParam("embedded") !== undefined) {
{ if (GetUriParam("product") !== undefined) {
if (GetUriParam("product") !== undefined) $("#display_amount").trigger('focus');
{ } else {
Grocy.Components.ProductPicker.GetPicker().trigger('change'); Grocy.Components.ProductPicker.Focus();
$("#display_amount").focus();
}
else
{
Grocy.Components.ProductPicker.GetInputElement().focus();
} }
} }
var eitherRequiredFields = $("#product_id,#product_id_text_input,#note"); $("#note").prop('required', "");
eitherRequiredFields.prop('required', ""); $("#note").on('input', function() {
eitherRequiredFields.on('input', function() if (!$(this).val().length) {
{ Grocy.Components.ProductPicker.Require();
eitherRequiredFields.not(this).prop('required', !$(this).val().length); } else {
Grocy.Components.ProductPicker.Optional();
}
Grocy.FrontendHelpers.ValidateForm('shoppinglist-form'); Grocy.FrontendHelpers.ValidateForm('shoppinglist-form');
}); });
Grocy.Components.ProductPicker.OnChange(function() {
$("#note").prop('required', !$(this).val().length);
Grocy.FrontendHelpers.ValidateForm('shoppinglist-form');
});
if (GetUriParam("product-name") != null) if (GetUriParam("product-name") != null) {
{ Grocy.Components.ProductPicker.Validate();
Grocy.Components.ProductPicker.GetPicker().trigger('change');
} }
Grocy.Components.UserfieldsForm.Load(); Grocy.Components.UserfieldsForm.Load();

View File

@ -1,50 +1,68 @@
var stockEntriesTable = $('#stockentries-table').DataTable({ var stockEntriesTable = $('#stockentries-table').DataTable({
'order': [[2, 'asc']], 'order': [
'columnDefs': [ [2, 'asc']
{ 'orderable': false, 'targets': 0 }, ],
{ 'searchable': false, "targets": 0 }, 'columnDefs': [{
{ 'visible': false, 'targets': 10 }, 'orderable': false,
{ "type": "num", "targets": 1 }, 'targets': 0
{ "type": "num", "targets": 3 }, },
{ "type": "html", "targets": 4 }, {
{ "type": "html-num-fmt", "targets": 7 }, 'searchable': false,
{ "type": "html", "targets": 8 }, "targets": 0
{ "type": "html", "targets": 9 } },
{
'visible': false,
'targets': 10
},
{
"type": "num",
"targets": 1
},
{
"type": "num",
"targets": 3
},
{
"type": "html",
"targets": 4
},
{
"type": "html-num-fmt",
"targets": 7
},
{
"type": "html",
"targets": 8
},
{
"type": "html",
"targets": 9
}
].concat($.fn.dataTable.defaults.columnDefs) ].concat($.fn.dataTable.defaults.columnDefs)
}); });
$('#stockentries-table tbody').removeClass("d-none"); $('#stockentries-table tbody').removeClass("d-none");
stockEntriesTable.columns.adjust().draw(); stockEntriesTable.columns.adjust().draw();
$.fn.dataTable.ext.search.push(function(settings, data, dataIndex) $.fn.dataTable.ext.search.push(function(settings, data, dataIndex) {
{
var productId = Grocy.Components.ProductPicker.GetValue(); var productId = Grocy.Components.ProductPicker.GetValue();
if ((isNaN(productId) || productId == "" || productId == data[1])) if ((isNaN(productId) || productId == "" || productId == data[1])) {
{
return true; return true;
} }
return false; return false;
}); });
$("#clear-filter-button").on("click", function() $("#clear-filter-button").on("click", function() {
{
Grocy.Components.ProductPicker.Clear(); Grocy.Components.ProductPicker.Clear();
stockEntriesTable.draw(); stockEntriesTable.draw();
}); });
Grocy.Components.ProductPicker.GetPicker().on('change', function(e) Grocy.Components.ProductPicker.OnChange(function(e) {
{
stockEntriesTable.draw(); stockEntriesTable.draw();
}); });
Grocy.Components.ProductPicker.GetInputElement().on('keyup', function(e) $(document).on('click', '.stock-consume-button', function(e) {
{
stockEntriesTable.draw();
});
$(document).on('click', '.stock-consume-button', function(e)
{
e.preventDefault(); e.preventDefault();
// Remove the focus from the current button // Remove the focus from the current button
@ -61,15 +79,21 @@ $(document).on('click', '.stock-consume-button', function(e)
var wasSpoiled = $(e.currentTarget).hasClass("stock-consume-button-spoiled"); var wasSpoiled = $(e.currentTarget).hasClass("stock-consume-button-spoiled");
Grocy.Api.Post('stock/products/' + productId + '/consume', { 'amount': consumeAmount, 'spoiled': wasSpoiled, 'location_id': locationId, 'stock_entry_id': specificStockEntryId, 'exact_amount': true }, Grocy.Api.Post('stock/products/' + productId + '/consume', {
function(bookingResponse) 'amount': consumeAmount,
{ 'spoiled': wasSpoiled,
'location_id': locationId,
'stock_entry_id': specificStockEntryId,
'exact_amount': true
},
function(bookingResponse) {
Grocy.Api.Get('stock/products/' + productId, Grocy.Api.Get('stock/products/' + productId,
function(result) function(result) {
{ var toastMessage = __t('Removed %1$s of %2$s from stock', parseFloat(consumeAmount).toLocaleString({
var toastMessage = __t('Removed %1$s of %2$s from stock', parseFloat(consumeAmount).toLocaleString({ minimumFractionDigits: 0, maximumFractionDigits: Grocy.UserSettings.stock_decimal_places_amounts }) + " " + __n(consumeAmount, result.quantity_unit_stock.name, result.quantity_unit_stock.name_plural, true), result.product.name) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoStockBookingEntry(' + bookingResponse[0].id + ',' + stockRowId + ')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>'; minimumFractionDigits: 0,
if (wasSpoiled) maximumFractionDigits: Grocy.UserSettings.stock_decimal_places_amounts
{ }) + " " + __n(consumeAmount, result.quantity_unit_stock.name, result.quantity_unit_stock.name_plural, true), result.product.name) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoStockBookingEntry(' + bookingResponse[0].id + ',' + stockRowId + ')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>';
if (wasSpoiled) {
toastMessage += " (" + __t("Spoiled") + ")"; toastMessage += " (" + __t("Spoiled") + ")";
} }
@ -77,23 +101,20 @@ $(document).on('click', '.stock-consume-button', function(e)
RefreshStockEntryRow(stockRowId); RefreshStockEntryRow(stockRowId);
toastr.success(toastMessage); toastr.success(toastMessage);
}, },
function(xhr) function(xhr) {
{
Grocy.FrontendHelpers.EndUiBusy(); Grocy.FrontendHelpers.EndUiBusy();
console.error(xhr); console.error(xhr);
} }
); );
}, },
function(xhr) function(xhr) {
{
Grocy.FrontendHelpers.EndUiBusy(); Grocy.FrontendHelpers.EndUiBusy();
console.error(xhr); console.error(xhr);
} }
); );
}); });
$(document).on('click', '.product-open-button', function(e) $(document).on('click', '.product-open-button', function(e) {
{
e.preventDefault(); e.preventDefault();
// Remove the focus from the current button // Remove the focus from the current button
@ -109,65 +130,55 @@ $(document).on('click', '.product-open-button', function(e)
var stockRowId = $(e.currentTarget).attr('data-stockrow-id'); var stockRowId = $(e.currentTarget).attr('data-stockrow-id');
var button = $(e.currentTarget); var button = $(e.currentTarget);
Grocy.Api.Post('stock/products/' + productId + '/open', { 'amount': 1, 'stock_entry_id': specificStockEntryId }, Grocy.Api.Post('stock/products/' + productId + '/open', {
function(bookingResponse) 'amount': 1,
{ 'stock_entry_id': specificStockEntryId
},
function(bookingResponse) {
button.addClass("disabled"); button.addClass("disabled");
Grocy.FrontendHelpers.EndUiBusy(); Grocy.FrontendHelpers.EndUiBusy();
toastr.success(__t('Marked %1$s of %2$s as opened', 1 + " " + productQuName, productName) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoStockBookingEntry(' + bookingResponse[0].id + ',' + stockRowId + ')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>'); toastr.success(__t('Marked %1$s of %2$s as opened', 1 + " " + productQuName, productName) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoStockBookingEntry(' + bookingResponse[0].id + ',' + stockRowId + ')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>');
RefreshStockEntryRow(stockRowId); RefreshStockEntryRow(stockRowId);
}, },
function(xhr) function(xhr) {
{
Grocy.FrontendHelpers.EndUiBusy(); Grocy.FrontendHelpers.EndUiBusy();
console.error(xhr); console.error(xhr);
} }
); );
}); });
$(document).on("click", ".stock-name-cell", function(e) $(document).on("click", ".stock-name-cell", function(e) {
{
Grocy.Components.ProductCard.Refresh($(e.currentTarget).attr("data-stock-id")); Grocy.Components.ProductCard.Refresh($(e.currentTarget).attr("data-stock-id"));
$("#stockentry-productcard-modal").modal("show"); $("#stockentry-productcard-modal").modal("show");
}); });
$(document).on('click', '.stockentry-grocycode-label-print', function(e) $(document).on('click', '.stockentry-grocycode-label-print', function(e) {
{
e.preventDefault(); e.preventDefault();
document.activeElement.blur(); document.activeElement.blur();
var stockId = $(e.currentTarget).attr('data-stock-id'); var stockId = $(e.currentTarget).attr('data-stock-id');
Grocy.Api.Get('stock/entry/' + stockId + '/printlabel', function(labelData) Grocy.Api.Get('stock/entry/' + stockId + '/printlabel', function(labelData) {
{ if (Grocy.Webhooks.labelprinter !== undefined) {
if (Grocy.Webhooks.labelprinter !== undefined)
{
Grocy.FrontendHelpers.RunWebhook(Grocy.Webhooks.labelprinter, labelData); Grocy.FrontendHelpers.RunWebhook(Grocy.Webhooks.labelprinter, labelData);
} }
}); });
}); });
function RefreshStockEntryRow(stockRowId) function RefreshStockEntryRow(stockRowId) {
{
Grocy.Api.Get("stock/entry/" + stockRowId, Grocy.Api.Get("stock/entry/" + stockRowId,
function(result) function(result) {
{
var stockRow = $('#stock-' + stockRowId + '-row'); var stockRow = $('#stock-' + stockRowId + '-row');
// If the stock row not exists / is invisible (happens after consume/undo because the undone new stock row has different id), just reload the page for now // If the stock row not exists / is invisible (happens after consume/undo because the undone new stock row has different id), just reload the page for now
if (!stockRow.length || stockRow.hasClass("d-none")) if (!stockRow.length || stockRow.hasClass("d-none")) {
{
window.location.reload(); window.location.reload();
} }
if (result == null || result.amount == 0) if (result == null || result.amount == 0) {
{ animateCSS("#stock-" + stockRowId + "-row", "fadeOut", function() {
animateCSS("#stock-" + stockRowId + "-row", "fadeOut", function()
{
$("#stock-" + stockRowId + "-row").addClass("d-none"); $("#stock-" + stockRowId + "-row").addClass("d-none");
}); });
} } else {
else
{
var dueThreshold = moment().add(Grocy.UserSettings.stock_due_soon_days, "days"); var dueThreshold = moment().add(Grocy.UserSettings.stock_due_soon_days, "days");
var now = moment(); var now = moment();
var bestBeforeDate = moment(result.best_before_date); var bestBeforeDate = moment(result.best_before_date);
@ -177,19 +188,13 @@ function RefreshStockEntryRow(stockRowId)
stockRow.removeClass("table-info"); stockRow.removeClass("table-info");
stockRow.removeClass("d-none"); stockRow.removeClass("d-none");
stockRow.removeAttr("style"); stockRow.removeAttr("style");
if (now.isAfter(bestBeforeDate)) if (now.isAfter(bestBeforeDate)) {
{ if (stockRow.attr("data-due-type") == 1) {
if (stockRow.attr("data-due-type") == 1)
{
stockRow.addClass("table-secondary"); stockRow.addClass("table-secondary");
} } else {
else
{
stockRow.addClass("table-danger"); stockRow.addClass("table-danger");
} }
} } else if (bestBeforeDate.isBefore(dueThreshold)) {
else if (bestBeforeDate.isBefore(dueThreshold))
{
stockRow.addClass("table-warning"); stockRow.addClass("table-warning");
} }
@ -203,15 +208,13 @@ function RefreshStockEntryRow(stockRowId)
var locationName = ""; var locationName = "";
Grocy.Api.Get("objects/locations/" + result.location_id, Grocy.Api.Get("objects/locations/" + result.location_id,
function(locationResult) function(locationResult) {
{
locationName = locationResult.name; locationName = locationResult.name;
$('#stock-' + stockRowId + '-location').attr('data-location-id', result.location_id); $('#stock-' + stockRowId + '-location').attr('data-location-id', result.location_id);
$('#stock-' + stockRowId + '-location').text(locationName); $('#stock-' + stockRowId + '-location').text(locationName);
}, },
function(xhr) function(xhr) {
{
console.error(xhr); console.error(xhr);
} }
); );
@ -222,74 +225,61 @@ function RefreshStockEntryRow(stockRowId)
var shoppingLocationName = ""; var shoppingLocationName = "";
Grocy.Api.Get("objects/shopping_locations/" + result.shopping_location_id, Grocy.Api.Get("objects/shopping_locations/" + result.shopping_location_id,
function(shoppingLocationResult) function(shoppingLocationResult) {
{
shoppingLocationName = shoppingLocationResult.name; shoppingLocationName = shoppingLocationResult.name;
$('#stock-' + stockRowId + '-shopping-location').attr('data-shopping-location-id', result.location_id); $('#stock-' + stockRowId + '-shopping-location').attr('data-shopping-location-id', result.location_id);
$('#stock-' + stockRowId + '-shopping-location').text(shoppingLocationName); $('#stock-' + stockRowId + '-shopping-location').text(shoppingLocationName);
}, },
function(xhr) function(xhr) {
{
console.error(xhr); console.error(xhr);
} }
); );
if (result.open == 1) if (result.open == 1) {
{
$('#stock-' + stockRowId + '-opened-amount').text(__t('Opened')); $('#stock-' + stockRowId + '-opened-amount').text(__t('Opened'));
} } else {
else
{
$('#stock-' + stockRowId + '-opened-amount').text(""); $('#stock-' + stockRowId + '-opened-amount').text("");
$(".product-open-button[data-stockrow-id='" + stockRowId + "']").removeClass("disabled"); $(".product-open-button[data-stockrow-id='" + stockRowId + "']").removeClass("disabled");
} }
} }
// Needs to be delayed because of the animation above the date-text would be wrong if fired immediately... // Needs to be delayed because of the animation above the date-text would be wrong if fired immediately...
setTimeout(function() setTimeout(function() {
{
RefreshContextualTimeago("#stock-" + stockRowId + "-row"); RefreshContextualTimeago("#stock-" + stockRowId + "-row");
RefreshLocaleNumberDisplay("#stock-" + stockRowId + "-row"); RefreshLocaleNumberDisplay("#stock-" + stockRowId + "-row");
}, 600); }, 600);
}, },
function(xhr) function(xhr) {
{
Grocy.FrontendHelpers.EndUiBusy(); Grocy.FrontendHelpers.EndUiBusy();
console.error(xhr); console.error(xhr);
} }
); );
} }
$(window).on("message", function(e) $(window).on("message", function(e) {
{
var data = e.originalEvent.data; var data = e.originalEvent.data;
if (data.Message === "StockEntryChanged") if (data.Message === "StockEntryChanged") {
{
RefreshStockEntryRow(data.Payload); RefreshStockEntryRow(data.Payload);
} }
}); });
Grocy.Components.ProductPicker.GetPicker().trigger('change'); Grocy.Components.ProductPicker.Validate();
function UndoStockBookingEntry(bookingId, stockRowId) function UndoStockBookingEntry(bookingId, stockRowId) {
{
Grocy.Api.Post('stock/bookings/' + bookingId.toString() + '/undo', {}, Grocy.Api.Post('stock/bookings/' + bookingId.toString() + '/undo', {},
function(result) function(result) {
{
window.postMessage(WindowMessageBag("StockEntryChanged", stockRowId), Grocy.BaseUrl); window.postMessage(WindowMessageBag("StockEntryChanged", stockRowId), Grocy.BaseUrl);
toastr.success(__t("Booking successfully undone")); toastr.success(__t("Booking successfully undone"));
}, },
function(xhr) function(xhr) {
{
console.error(xhr); console.error(xhr);
} }
); );
}; };
$(document).on("click", ".product-name-cell", function(e) $(document).on("click", ".product-name-cell", function(e) {
{
Grocy.Components.ProductCard.Refresh($(e.currentTarget).attr("data-product-id")); Grocy.Components.ProductCard.Refresh($(e.currentTarget).attr("data-product-id"));
$("#productcard-modal").modal("show"); $("#productcard-modal").modal("show");
}); });

View File

@ -1,83 +1,118 @@
var stockJournalTable = $('#stock-journal-table').DataTable({ var stockJournalTable = $('#stock-journal-table').DataTable({
'order': [[3, 'desc']], 'order': [
'columnDefs': [ [3, 'desc']
{ 'orderable': false, 'targets': 0 }, ],
{ 'searchable': false, "targets": 0 } 'columnDefs': [{
'orderable': false,
'targets': 0
},
{
'searchable': false,
"targets": 0
}
].concat($.fn.dataTable.defaults.columnDefs) ].concat($.fn.dataTable.defaults.columnDefs)
}); });
$('#stock-journal-table tbody').removeClass("d-none"); $('#stock-journal-table tbody').removeClass("d-none");
stockJournalTable.columns.adjust().draw(); stockJournalTable.columns.adjust().draw();
$("#product-filter").on("change", function() $("#product-filter").select2({
{ ajax: {
var value = $(this).val(); delay: 150,
if (value === "all") transport: function(params, success, failure) {
{ var results_per_page = 10;
RemoveUriParam("product"); var page = params.data.page || 1;
} var term = params.data.term || "";
else
{ var query = [];
UpdateUriParam("product", value); query.push('query%5B%5D=active%3D1');
query.push('limit=' + encodeURIComponent(results_per_page));
query.push('offset=' + encodeURIComponent((page - 1) * results_per_page));
query.push('order=name%3Acollate%20nocase');
if (term.length > 0) {
query.push('search=' + encodeURIComponent(term));
} }
window.location.reload(); Grocy.Api.Get('objects/products' + (query.length > 0 ? '?' + query.join('&') : ''),
function(results, meta) {
success({
results: [{
id: 'all',
text: __t('All')
}].concat(results.map(function(result) {
return {
id: result.id,
text: result.name
};
})),
pagination: {
more: page * results_per_page < meta.recordsFiltered
}
});
},
function(xhr) {
failure();
}
);
}
}
}); });
$("#transaction-type-filter").on("change", function() $("#product-filter").on("change", function() {
{ var value = $(this).val();
if (value === "all" && GetUriParam("product") !== undefined) {
RemoveUriParam("product");
window.location.reload();
} else if (GetUriParam("product") !== value) {
UpdateUriParam("product", value);
window.location.reload();
}
});
$("#transaction-type-filter").on("change", function() {
var value = $(this).val(); var value = $(this).val();
var text = $("#transaction-type-filter option:selected").text(); var text = $("#transaction-type-filter option:selected").text();
if (value === "all") if (value === "all") {
{
text = ""; text = "";
} }
stockJournalTable.column(stockJournalTable.colReorder.transpose(4)).search(text).draw(); stockJournalTable.column(stockJournalTable.colReorder.transpose(4)).search(text).draw();
}); });
$("#location-filter").on("change", function() $("#location-filter").on("change", function() {
{
var value = $(this).val(); var value = $(this).val();
var text = $("#location-filter option:selected").text(); var text = $("#location-filter option:selected").text();
if (value === "all") if (value === "all") {
{
text = ""; text = "";
} }
stockJournalTable.column(stockJournalTable.colReorder.transpose(5)).search(text).draw(); stockJournalTable.column(stockJournalTable.colReorder.transpose(5)).search(text).draw();
}); });
$("#user-filter").on("change", function() $("#user-filter").on("change", function() {
{
var value = $(this).val(); var value = $(this).val();
var text = $("#user-filter option:selected").text(); var text = $("#user-filter option:selected").text();
if (value === "all") if (value === "all") {
{
text = ""; text = "";
} }
stockJournalTable.column(stockJournalTable.colReorder.transpose(6)).search(text).draw(); stockJournalTable.column(stockJournalTable.colReorder.transpose(6)).search(text).draw();
}); });
$("#daterange-filter").on("change", function() $("#daterange-filter").on("change", function() {
{
UpdateUriParam("months", $(this).val()); UpdateUriParam("months", $(this).val());
window.location.reload(); window.location.reload();
}); });
$("#search").on("keyup", Delay(function() $("#search").on("keyup", Delay(function() {
{
var value = $(this).val(); var value = $(this).val();
if (value === "all") if (value === "all") {
{
value = ""; value = "";
} }
stockJournalTable.search(value).draw(); stockJournalTable.search(value).draw();
}, 200)); }, 200));
$("#clear-filter-button").on("click", function() $("#clear-filter-button").on("click", function() {
{
$("#search").val(""); $("#search").val("");
$("#transaction-type-filter").val("all"); $("#transaction-type-filter").val("all");
$("#location-filter").val("all"); $("#location-filter").val("all");
@ -90,40 +125,47 @@ $("#clear-filter-button").on("click", function()
window.location.reload(); window.location.reload();
}); });
if (typeof GetUriParam("product") !== "undefined") var prefillProductId = GetUriParam("product");
{ if (typeof prefillProductId !== "undefined") {
$("#product-filter").val(GetUriParam("product")); if ($("#product-filter").find('option[value="' + prefillProductId + '"]').length) {
$("#product-filter").val(prefillProductId).trigger('change');
} else {
Grocy.Api.Get('objects/products/' + encodeURIComponent(prefillProductId),
function(result) {
var option = new Option(result.name, prefillProductId, true, true);
$("#product-filter").append(option).trigger('change');
},
function(xhr) {
console.error(xhr);
}
);
}
} }
if (typeof GetUriParam("months") !== "undefined") if (typeof GetUriParam("months") !== "undefined") {
{
$("#daterange-filter").val(GetUriParam("months")); $("#daterange-filter").val(GetUriParam("months"));
} }
$(document).on('click', '.undo-stock-booking-button', function(e) $(document).on('click', '.undo-stock-booking-button', function(e) {
{
e.preventDefault(); e.preventDefault();
var bookingId = $(e.currentTarget).attr('data-booking-id'); var bookingId = $(e.currentTarget).attr('data-booking-id');
var correlationId = $("#stock-booking-" + bookingId + "-row").attr("data-correlation-id"); var correlationId = $("#stock-booking-" + bookingId + "-row").attr("data-correlation-id");
var correspondingBookingsRoot = $("#stock-booking-" + bookingId + "-row"); var correspondingBookingsRoot = $("#stock-booking-" + bookingId + "-row");
if (!correlationId.isEmpty()) if (!correlationId.isEmpty()) {
{
correspondingBookingsRoot = $(".stock-booking-correlation-" + correlationId); correspondingBookingsRoot = $(".stock-booking-correlation-" + correlationId);
} }
Grocy.Api.Post('stock/bookings/' + bookingId.toString() + '/undo', {}, Grocy.Api.Post('stock/bookings/' + bookingId.toString() + '/undo', {},
function(result) function(result) {
{
correspondingBookingsRoot.addClass("text-muted"); correspondingBookingsRoot.addClass("text-muted");
correspondingBookingsRoot.find("span.name-anchor").addClass("text-strike-through").after("<br>" + __t("Undone on") + " " + moment().format("YYYY-MM-DD HH:mm:ss") + " <time class='timeago timeago-contextual' datetime='" + moment().format("YYYY-MM-DD HH:mm:ss") + "'></time>"); correspondingBookingsRoot.find("span.name-anchor").addClass("text-strike-through").after("<br>" + __t("Undone on") + " " + moment().format("YYYY-MM-DD HH:mm:ss") + " <time class='timeago timeago-contextual' datetime='" + moment().format("YYYY-MM-DD HH:mm:ss") + "'></time>");
correspondingBookingsRoot.find(".undo-stock-booking-button").addClass("disabled"); correspondingBookingsRoot.find(".undo-stock-booking-button").addClass("disabled");
RefreshContextualTimeago("#stock-booking-" + bookingId + "-row"); RefreshContextualTimeago("#stock-booking-" + bookingId + "-row");
toastr.success(__t("Booking successfully undone")); toastr.success(__t("Booking successfully undone"));
}, },
function(xhr) function(xhr) {
{
console.error(xhr); console.error(xhr);
toastr.error(__t(JSON.parse(xhr.response).error_message)); toastr.error(__t(JSON.parse(xhr.response).error_message));
} }

View File

@ -1,64 +1,102 @@
var journalSummaryTable = $('#stock-journal-summary-table').DataTable({ var journalSummaryTable = $('#stock-journal-summary-table').DataTable({
'order': [[1, 'asc']], 'order': [
'columnDefs': [ [1, 'asc']
{ 'orderable': false, 'targets': 0 }, ],
{ 'searchable': false, "targets": 0 } 'columnDefs': [{
'orderable': false,
'targets': 0
},
{
'searchable': false,
"targets": 0
}
].concat($.fn.dataTable.defaults.columnDefs) ].concat($.fn.dataTable.defaults.columnDefs)
}); });
$('#stock-journal-summary-table tbody').removeClass("d-none"); $('#stock-journal-summary-table tbody').removeClass("d-none");
journalSummaryTable.columns.adjust().draw(); journalSummaryTable.columns.adjust().draw();
$("#product-filter").on("change", function() $("#product-filter").select2({
{ ajax: {
var value = $(this).val(); delay: 150,
var text = $("#product-filter option:selected").text(); transport: function(params, success, failure) {
if (value === "all") var results_per_page = 10;
{ var page = params.data.page || 1;
journalSummaryTable.column(journalSummaryTable.colReorder.transpose(1)).search("").draw(); var term = params.data.term || "";
var query = [];
query.push('query%5B%5D=active%3D1');
query.push('limit=' + encodeURIComponent(results_per_page));
query.push('offset=' + encodeURIComponent((page - 1) * results_per_page));
query.push('order=name%3Acollate%20nocase');
if (term.length > 0) {
query.push('search=' + encodeURIComponent(term));
} }
else
{ Grocy.Api.Get('objects/products' + (query.length > 0 ? '?' + query.join('&') : ''),
function(results, meta) {
success({
results: [{
id: 'all',
text: __t('All')
}].concat(results.map(function(result) {
return {
id: result.id,
text: result.name
};
})),
pagination: {
more: page * results_per_page < meta.recordsFiltered
}
});
},
function(xhr) {
failure();
}
);
}
}
});
$("#product-filter").on("change", function() {
var value = $(this).val();
var text = $("#product-filter option:selected").text().trim();
if (value === "all") {
journalSummaryTable.column(journalSummaryTable.colReorder.transpose(1)).search("").draw();
} else {
journalSummaryTable.column(journalSummaryTable.colReorder.transpose(1)).search("^" + text + "$", true, false).draw(); journalSummaryTable.column(journalSummaryTable.colReorder.transpose(1)).search("^" + text + "$", true, false).draw();
} }
}); });
$("#transaction-type-filter").on("change", function() $("#transaction-type-filter").on("change", function() {
{
var value = $(this).val(); var value = $(this).val();
var text = $("#transaction-type-filter option:selected").text(); var text = $("#transaction-type-filter option:selected").text();
if (value === "all") if (value === "all") {
{
text = ""; text = "";
} }
journalSummaryTable.column(journalSummaryTable.colReorder.transpose(2)).search(text).draw(); journalSummaryTable.column(journalSummaryTable.colReorder.transpose(2)).search(text).draw();
}); });
$("#user-filter").on("change", function() $("#user-filter").on("change", function() {
{
var value = $(this).val(); var value = $(this).val();
var text = $("#user-filter option:selected").text(); var text = $("#user-filter option:selected").text();
if (value === "all") if (value === "all") {
{
text = ""; text = "";
} }
journalSummaryTable.column(journalSummaryTable.colReorder.transpose(3)).search(text).draw(); journalSummaryTable.column(journalSummaryTable.colReorder.transpose(3)).search(text).draw();
}); });
$("#search").on("keyup", Delay(function() $("#search").on("keyup", Delay(function() {
{
var value = $(this).val(); var value = $(this).val();
if (value === "all") if (value === "all") {
{
value = ""; value = "";
} }
journalSummaryTable.search(value).draw(); journalSummaryTable.search(value).draw();
}, 200)); }, 200));
$("#clear-filter-button").on("click", function() $("#clear-filter-button").on("click", function() {
{
$("#search").val(""); $("#search").val("");
$("#transaction-type-filter").val("all"); $("#transaction-type-filter").val("all");
$("#location-filter").val("all"); $("#location-filter").val("all");

View File

@ -1,9 +1,7 @@
$('#save-transfer-button').on('click', function(e) $('#save-transfer-button').on('click', function(e) {
{
e.preventDefault(); e.preventDefault();
if ($(".combobox-menu-visible").length) if ($(".combobox-menu-visible").length) {
{
return; return;
} }
@ -17,60 +15,48 @@
jsonData.location_id_to = $("#location_id_to").val(); jsonData.location_id_to = $("#location_id_to").val();
jsonData.location_id_from = $("#location_id_from").val(); jsonData.location_id_from = $("#location_id_from").val();
if ($("#use_specific_stock_entry").is(":checked")) if ($("#use_specific_stock_entry").is(":checked")) {
{
jsonData.stock_entry_id = jsonForm.specific_stock_entry; jsonData.stock_entry_id = jsonForm.specific_stock_entry;
} }
var bookingResponse = null; var bookingResponse = null;
Grocy.Api.Get('stock/products/' + jsonForm.product_id, Grocy.Api.Get('stock/products/' + jsonForm.product_id,
function(productDetails) function(productDetails) {
{
Grocy.Api.Post(apiUrl, jsonData, Grocy.Api.Post(apiUrl, jsonData,
function(result) function(result) {
{
bookingResponse = result; bookingResponse = result;
if (GetUriParam("flow") === "InplaceAddBarcodeToExistingProduct") if (GetUriParam("flow") === "InplaceAddBarcodeToExistingProduct") {
{
var jsonDataBarcode = {}; var jsonDataBarcode = {};
jsonDataBarcode.barcode = GetUriParam("barcode"); jsonDataBarcode.barcode = GetUriParam("barcode");
jsonDataBarcode.product_id = jsonForm.product_id; jsonDataBarcode.product_id = jsonForm.product_id;
Grocy.Api.Post('objects/product_barcodes', jsonDataBarcode, Grocy.Api.Post('objects/product_barcodes', jsonDataBarcode,
function(result) function(result) {
{
$("#flow-info-InplaceAddBarcodeToExistingProduct").addClass("d-none"); $("#flow-info-InplaceAddBarcodeToExistingProduct").addClass("d-none");
$('#barcode-lookup-disabled-hint').addClass('d-none'); $('#barcode-lookup-disabled-hint').addClass('d-none');
$('#barcode-lookup-hint').removeClass('d-none'); $('#barcode-lookup-hint').removeClass('d-none');
window.history.replaceState({}, document.title, U("/transfer")); window.history.replaceState({}, document.title, U("/transfer"));
}, },
function(xhr) function(xhr) {
{
Grocy.FrontendHelpers.EndUiBusy("transfer-form"); Grocy.FrontendHelpers.EndUiBusy("transfer-form");
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response); Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response);
} }
); );
} }
if (productDetails.product.enable_tare_weight_handling == 1) if (productDetails.product.enable_tare_weight_handling == 1) {
{
var successMessage = __t('Transfered %1$s of %2$s from %3$s to %4$s', Math.abs(jsonForm.amount - parseFloat(productDetails.product.tare_weight)) + " " + __n(jsonForm.amount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural, true), productDetails.product.name, $('option:selected', "#location_id_from").text(), $('option:selected', "#location_id_to").text()) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoStockTransaction(\'' + bookingResponse[0].transaction_id + '\')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>'; var successMessage = __t('Transfered %1$s of %2$s from %3$s to %4$s', Math.abs(jsonForm.amount - parseFloat(productDetails.product.tare_weight)) + " " + __n(jsonForm.amount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural, true), productDetails.product.name, $('option:selected', "#location_id_from").text(), $('option:selected', "#location_id_to").text()) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoStockTransaction(\'' + bookingResponse[0].transaction_id + '\')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>';
} } else {
else
{
var successMessage = __t('Transfered %1$s of %2$s from %3$s to %4$s', Math.abs(jsonForm.amount) + " " + __n(jsonForm.amount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural, true), productDetails.product.name, $('option:selected', "#location_id_from").text(), $('option:selected', "#location_id_to").text()) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoStockTransaction(\'' + bookingResponse[0].transaction_id + '\')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>'; var successMessage = __t('Transfered %1$s of %2$s from %3$s to %4$s', Math.abs(jsonForm.amount) + " " + __n(jsonForm.amount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural, true), productDetails.product.name, $('option:selected', "#location_id_from").text(), $('option:selected', "#location_id_to").text()) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoStockTransaction(\'' + bookingResponse[0].transaction_id + '\')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>';
} }
if (GetUriParam("embedded") !== undefined) if (GetUriParam("embedded") !== undefined) {
{
window.parent.postMessage(WindowMessageBag("ProductChanged", jsonForm.product_id), Grocy.BaseUrl); window.parent.postMessage(WindowMessageBag("ProductChanged", jsonForm.product_id), Grocy.BaseUrl);
window.parent.postMessage(WindowMessageBag("ShowSuccessMessage", successMessage), Grocy.BaseUrl); window.parent.postMessage(WindowMessageBag("ShowSuccessMessage", successMessage), Grocy.BaseUrl);
window.parent.postMessage(WindowMessageBag("CloseAllModals"), Grocy.BaseUrl); window.parent.postMessage(WindowMessageBag("CloseAllModals"), Grocy.BaseUrl);
} } else {
else
{
Grocy.FrontendHelpers.EndUiBusy("transfer-form"); Grocy.FrontendHelpers.EndUiBusy("transfer-form");
toastr.success(successMessage); toastr.success(successMessage);
Grocy.Components.ProductPicker.FinishFlow(); Grocy.Components.ProductPicker.FinishFlow();
@ -79,8 +65,7 @@
{ {
toastr.info('<span>' + __t("Frozen") + "</span> <i class='fas fa-snowflake'></i>"); toastr.info('<span>' + __t("Frozen") + "</span> <i class='fas fa-snowflake'></i>");
if (BoolVal(productDetails.product.should_not_be_frozen)) if (BoolVal(productDetails.product.should_not_be_frozen)) {
{
toastr.warning(__t("This product shouldn't be frozen")); toastr.warning(__t("This product shouldn't be frozen"));
} }
} }
@ -92,8 +77,7 @@
$("#specific_stock_entry").find("option").remove().end().append("<option></option>"); $("#specific_stock_entry").find("option").remove().end().append("<option></option>");
$("#specific_stock_entry").attr("disabled", ""); $("#specific_stock_entry").attr("disabled", "");
$("#specific_stock_entry").removeAttr("required"); $("#specific_stock_entry").removeAttr("required");
if ($("#use_specific_stock_entry").is(":checked")) if ($("#use_specific_stock_entry").is(":checked")) {
{
$("#use_specific_stock_entry").click(); $("#use_specific_stock_entry").click();
} }
@ -108,67 +92,56 @@
Grocy.Components.ProductPicker.Clear(); Grocy.Components.ProductPicker.Clear();
$("#location_id_to").val(""); $("#location_id_to").val("");
$("#location_id_from").val(""); $("#location_id_from").val("");
Grocy.Components.ProductPicker.GetInputElement().focus(); Grocy.Components.ProductPicker.Focus();
Grocy.Components.ProductCard.Refresh(jsonForm.product_id); Grocy.Components.ProductCard.Refresh(jsonForm.product_id);
Grocy.FrontendHelpers.ValidateForm('transfer-form'); Grocy.FrontendHelpers.ValidateForm('transfer-form');
} }
}, },
function(xhr) function(xhr) {
{
Grocy.FrontendHelpers.EndUiBusy("transfer-form"); Grocy.FrontendHelpers.EndUiBusy("transfer-form");
console.error(xhr); console.error(xhr);
} }
); );
}, },
function(xhr) function(xhr) {
{
Grocy.FrontendHelpers.EndUiBusy("transfer-form"); Grocy.FrontendHelpers.EndUiBusy("transfer-form");
console.error(xhr); console.error(xhr);
} }
); );
}); });
Grocy.Components.ProductPicker.GetPicker().on('change', function(e) Grocy.Components.ProductPicker.OnChange(function(e) {
{
$("#specific_stock_entry").find("option").remove().end().append("<option></option>"); $("#specific_stock_entry").find("option").remove().end().append("<option></option>");
if ($("#use_specific_stock_entry").is(":checked") && GetUriParam("stockId") == null) if ($("#use_specific_stock_entry").is(":checked") && GetUriParam("stockId") == null) {
{ $("#use_specific_stock_entry").trigger('click');
$("#use_specific_stock_entry").click();
} }
$("#location_id_to").val(""); $("#location_id_to").val("");
if (GetUriParam("stockId") == null) if (GetUriParam("stockId") == null) {
{
$("#location_id_from").val(""); $("#location_id_from").val("");
} }
var productId = $(e.target).val(); var productId = $(e.target).val();
if (productId) if (productId) {
{
Grocy.Components.ProductCard.Refresh(productId); Grocy.Components.ProductCard.Refresh(productId);
Grocy.Api.Get('stock/products/' + productId, Grocy.Api.Get('stock/products/' + productId,
function(productDetails) function(productDetails) {
{
Grocy.Components.ProductAmountPicker.Reload(productDetails.product.id, productDetails.quantity_unit_stock.id); Grocy.Components.ProductAmountPicker.Reload(productDetails.product.id, productDetails.quantity_unit_stock.id);
Grocy.Components.ProductAmountPicker.SetQuantityUnit(productDetails.quantity_unit_stock.id); Grocy.Components.ProductAmountPicker.SetQuantityUnit(productDetails.quantity_unit_stock.id);
if (productDetails.product.enable_tare_weight_handling == 1) if (productDetails.product.enable_tare_weight_handling == 1) {
{ Grocy.Components.ProductPicker.ShowCustomError(__t('Products with tare weight enabled are currently not supported for transfer'));
Grocy.Components.ProductPicker.GetPicker().parent().find(".invalid-feedback").text(__t('Products with tare weight enabled are currently not supported for transfer'));
Grocy.Components.ProductPicker.Clear(); Grocy.Components.ProductPicker.Clear();
return; return;
} }
$("#location_id_from").find("option").remove().end().append("<option></option>"); $("#location_id_from").find("option").remove().end().append("<option></option>");
Grocy.Api.Get("stock/products/" + productId + '/locations', Grocy.Api.Get("stock/products/" + productId + '/locations',
function(stockLocations) function(stockLocations) {
{
var setDefault = 0; var setDefault = 0;
stockLocations.forEach(stockLocation => stockLocations.forEach(stockLocation => {
{ if (productDetails.location.id == stockLocation.location_id) {
if (productDetails.location.id == stockLocation.location_id)
{
$("#location_id_from").append($("<option>", { $("#location_id_from").append($("<option>", {
value: stockLocation.location_id, value: stockLocation.location_id,
text: stockLocation.location_name + " (" + __t("Default location") + ")", text: stockLocation.location_name + " (" + __t("Default location") + ")",
@ -177,9 +150,7 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
$("#location_id_from").val(productDetails.location.id); $("#location_id_from").val(productDetails.location.id);
$("#location_id_from").trigger('change'); $("#location_id_from").trigger('change');
setDefault = 1; setDefault = 1;
} } else {
else
{
$("#location_id_from").append($("<option>", { $("#location_id_from").append($("<option>", {
value: stockLocation.location_id, value: stockLocation.location_id,
text: stockLocation.location_name, text: stockLocation.location_name,
@ -187,44 +158,35 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
})); }));
} }
if (setDefault == 0) if (setDefault == 0) {
{
$("#location_id_from").val(stockLocation.location_id); $("#location_id_from").val(stockLocation.location_id);
$("#location_id_from").trigger('change'); $("#location_id_from").trigger('change');
} }
}); });
if (GetUriParam("locationId") != null) if (GetUriParam("locationId") != null) {
{
$("#location_id_from").val(GetUriParam("locationId")); $("#location_id_from").val(GetUriParam("locationId"));
$("#location_id_from").trigger("change"); $("#location_id_from").trigger("change");
} }
}, },
function(xhr) function(xhr) {
{
console.error(xhr); console.error(xhr);
} }
); );
if (document.getElementById("product_id").getAttribute("barcode") != "null") if (document.getElementById("product_id").getAttribute("barcode") != "null") {
{
Grocy.Api.Get('objects/product_barcodes?query[]=barcode=' + document.getElementById("product_id").getAttribute("barcode"), Grocy.Api.Get('objects/product_barcodes?query[]=barcode=' + document.getElementById("product_id").getAttribute("barcode"),
function(barcodeResult) function(barcodeResult) {
{ if (barcodeResult != null) {
if (barcodeResult != null)
{
var barcode = barcodeResult[0]; var barcode = barcodeResult[0];
if (barcode != null) if (barcode != null) {
{ if (barcode.amount != null && !barcode.amount.isEmpty()) {
if (barcode.amount != null && !barcode.amount.isEmpty())
{
$("#display_amount").val(barcode.amount); $("#display_amount").val(barcode.amount);
$("#display_amount").select(); $("#display_amount").trigger('select');
} }
if (barcode.qu_id != null && !barcode.qu_id.isEmpty()) if (barcode.qu_id != null && !barcode.qu_id.isEmpty()) {
{
Grocy.Components.ProductAmountPicker.SetQuantityUnit(barcode.qu_id); Grocy.Components.ProductAmountPicker.SetQuantityUnit(barcode.qu_id);
} }
@ -234,20 +196,16 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
} }
} }
}, },
function(xhr) function(xhr) {
{
console.error(xhr); console.error(xhr);
} }
); );
} }
if (productDetails.product.enable_tare_weight_handling == 1) if (productDetails.product.enable_tare_weight_handling == 1) {
{
$("#display_amount").attr("min", productDetails.product.tare_weight); $("#display_amount").attr("min", productDetails.product.tare_weight);
$("#tare-weight-handling-info").removeClass("d-none"); $("#tare-weight-handling-info").removeClass("d-none");
} } else {
else
{
$("#display_amount").attr("min", Grocy.DefaultMinAmount); $("#display_amount").attr("min", Grocy.DefaultMinAmount);
$("#tare-weight-handling-info").addClass("d-none"); $("#tare-weight-handling-info").addClass("d-none");
} }
@ -256,11 +214,11 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
Grocy.Components.ProductPicker.HideCustomError(); Grocy.Components.ProductPicker.HideCustomError();
Grocy.FrontendHelpers.ValidateForm('transfer-form'); Grocy.FrontendHelpers.ValidateForm('transfer-form');
$('#display_amount').focus(); $('#display_amount').trigger('focus');
}, },
function(xhr) function(xhr) {
{
console.error(xhr); console.error(xhr);
Grocy.Components.ProductPicker.HideCustomError();
} }
); );
} }
@ -271,45 +229,35 @@ $(".input-group-productamountpicker").trigger("change");
Grocy.FrontendHelpers.ValidateForm('transfer-form'); Grocy.FrontendHelpers.ValidateForm('transfer-form');
RefreshLocaleNumberInput(); RefreshLocaleNumberInput();
$("#location_id_from").on('change', function(e) $("#location_id_from").on('change', function(e) {
{
var locationId = $(e.target).val(); var locationId = $(e.target).val();
var sumValue = 0; var sumValue = 0;
var stockId = null; var stockId = null;
if (locationId == $("#location_id_to").val()) if (locationId == $("#location_id_to").val()) {
{
$("#location_id_to").val(""); $("#location_id_to").val("");
} }
if (GetUriParam("embedded") !== undefined) if (GetUriParam("embedded") !== undefined) {
{
stockId = GetUriParam('stockId'); stockId = GetUriParam('stockId');
} }
$("#specific_stock_entry").find("option").remove().end().append("<option></option>"); $("#specific_stock_entry").find("option").remove().end().append("<option></option>");
if ($("#use_specific_stock_entry").is(":checked") && GetUriParam("stockId") == null) if ($("#use_specific_stock_entry").is(":checked") && GetUriParam("stockId") == null) {
{
$("#use_specific_stock_entry").click(); $("#use_specific_stock_entry").click();
} }
if (locationId) if (locationId) {
{
Grocy.Api.Get("stock/products/" + Grocy.Components.ProductPicker.GetValue() + '/entries', Grocy.Api.Get("stock/products/" + Grocy.Components.ProductPicker.GetValue() + '/entries',
function(stockEntries) function(stockEntries) {
{ stockEntries.forEach(stockEntry => {
stockEntries.forEach(stockEntry =>
{
var openTxt = __t("Not opened"); var openTxt = __t("Not opened");
if (stockEntry.open == 1) if (stockEntry.open == 1) {
{
openTxt = __t("Opened"); openTxt = __t("Opened");
} }
if (stockEntry.location_id == locationId) if (stockEntry.location_id == locationId) {
{ if ($("#specific_stock_entry option[value='" + stockEntry.stock_id + "']").length == 0) {
if ($("#specific_stock_entry option[value='" + stockEntry.stock_id + "']").length == 0)
{
$("#specific_stock_entry").append($("<option>", { $("#specific_stock_entry").append($("<option>", {
value: stockEntry.stock_id, value: stockEntry.stock_id,
amount: stockEntry.amount, amount: stockEntry.amount,
@ -317,8 +265,7 @@ $("#location_id_from").on('change', function(e)
})); }));
} }
if (stockEntry.stock_id == stockId) if (stockEntry.stock_id == stockId) {
{
$("#specific_stock_entry").val(stockId); $("#specific_stock_entry").val(stockId);
} }
@ -326,52 +273,43 @@ $("#location_id_from").on('change', function(e)
} }
}); });
$("#display_amount").attr("max", sumValue * $("#qu_id option:selected").attr("data-qu-factor")); $("#display_amount").attr("max", sumValue * $("#qu_id option:selected").attr("data-qu-factor"));
if (sumValue == 0) if (sumValue == 0) {
{
$("#display_amount").parent().find(".invalid-feedback").text(__t('There are no units available at this location')); $("#display_amount").parent().find(".invalid-feedback").text(__t('There are no units available at this location'));
} }
}, },
function(xhr) function(xhr) {
{
console.error(xhr); console.error(xhr);
} }
); );
} }
}); });
$("#location_id_to").on('change', function(e) $("#location_id_to").on('change', function(e) {
{
var locationId = $(e.target).val(); var locationId = $(e.target).val();
if (locationId == $("#location_id_from").val()) if (locationId == $("#location_id_from").val()) {
{
$("#location_id_to").parent().find(".invalid-feedback").text(__t('This cannot be the same as the "From" location')); $("#location_id_to").parent().find(".invalid-feedback").text(__t('This cannot be the same as the "From" location'));
$("#location_id_to").val(""); $("#location_id_to").val("");
} }
}); });
$("#qu_id").on('change', function(e) $("#qu_id").on('change', function(e) {
{
$("#display_amount").attr("max", parseFloat($('#display_amount').attr("data-stock-amount")) * $("#qu_id option:selected").attr("data-qu-factor")); $("#display_amount").attr("max", parseFloat($('#display_amount').attr("data-stock-amount")) * $("#qu_id option:selected").attr("data-qu-factor"));
}); });
$('#display_amount').on('focus', function(e) $('#display_amount').on('focus', function(e) {
{
$(this).select(); $(this).select();
}); });
$('#transfer-form input').keyup(function(event) $('#transfer-form input').keyup(function(event) {
{
Grocy.FrontendHelpers.ValidateForm('transfer-form'); Grocy.FrontendHelpers.ValidateForm('transfer-form');
}); });
$('#transfer-form select').change(function(event) $('#transfer-form select').change(function(event) {
{
Grocy.FrontendHelpers.ValidateForm('transfer-form'); Grocy.FrontendHelpers.ValidateForm('transfer-form');
}); });
$('#transfer-form input').keydown(function(event) $('#transfer-form input').keydown(function(event) {
{
if (event.keyCode === 13) //Enter if (event.keyCode === 13) //Enter
{ {
event.preventDefault(); event.preventDefault();
@ -379,58 +317,43 @@ $('#transfer-form input').keydown(function(event)
if (document.getElementById('transfer-form').checkValidity() === false) //There is at least one validation error if (document.getElementById('transfer-form').checkValidity() === false) //There is at least one validation error
{ {
return false; return false;
} } else {
else
{
$('#save-transfer-button').click(); $('#save-transfer-button').click();
} }
} }
}); });
$("#specific_stock_entry").on("change", function(e) $("#specific_stock_entry").on("change", function(e) {
{ if ($(e.target).val() == "") {
if ($(e.target).val() == "")
{
var sumValue = 0; var sumValue = 0;
Grocy.Api.Get("stock/products/" + Grocy.Components.ProductPicker.GetValue() + '/entries', Grocy.Api.Get("stock/products/" + Grocy.Components.ProductPicker.GetValue() + '/entries',
function(stockEntries) function(stockEntries) {
{ stockEntries.forEach(stockEntry => {
stockEntries.forEach(stockEntry => if (stockEntry.location_id == $("#location_id_from").val() || stockEntry.location_id == "") {
{
if (stockEntry.location_id == $("#location_id_from").val() || stockEntry.location_id == "")
{
sumValue = sumValue + parseFloat(stockEntry.amount); sumValue = sumValue + parseFloat(stockEntry.amount);
} }
}); });
$("#display_amount").attr("max", sumValue * $("#qu_id option:selected").attr("data-qu-factor")); $("#display_amount").attr("max", sumValue * $("#qu_id option:selected").attr("data-qu-factor"));
if (sumValue == 0) if (sumValue == 0) {
{
$("#display_amount").parent().find(".invalid-feedback").text(__t('There are no units available at this location')); $("#display_amount").parent().find(".invalid-feedback").text(__t('There are no units available at this location'));
} }
}, },
function(xhr) function(xhr) {
{
console.error(xhr); console.error(xhr);
} }
); );
} } else {
else
{
$("#display_amount").attr("max", $('option:selected', this).attr('amount')); $("#display_amount").attr("max", $('option:selected', this).attr('amount'));
} }
}); });
$("#use_specific_stock_entry").on("change", function() $("#use_specific_stock_entry").on("change", function() {
{
var value = $(this).is(":checked"); var value = $(this).is(":checked");
if (value) if (value) {
{
$("#specific_stock_entry").removeAttr("disabled"); $("#specific_stock_entry").removeAttr("disabled");
$("#specific_stock_entry").attr("required", ""); $("#specific_stock_entry").attr("required", "");
} } else {
else
{
$("#specific_stock_entry").attr("disabled", ""); $("#specific_stock_entry").attr("disabled", "");
$("#specific_stock_entry").removeAttr("required"); $("#specific_stock_entry").removeAttr("required");
$("#specific_stock_entry").val(""); $("#specific_stock_entry").val("");
@ -440,53 +363,43 @@ $("#use_specific_stock_entry").on("change", function()
Grocy.FrontendHelpers.ValidateForm("transfer-form"); Grocy.FrontendHelpers.ValidateForm("transfer-form");
}); });
function UndoStockBooking(bookingId) function UndoStockBooking(bookingId) {
{
Grocy.Api.Post('stock/bookings/' + bookingId.toString() + '/undo', {}, Grocy.Api.Post('stock/bookings/' + bookingId.toString() + '/undo', {},
function(result) function(result) {
{
toastr.success(__t("Booking successfully undone")); toastr.success(__t("Booking successfully undone"));
}, },
function(xhr) function(xhr) {
{
console.error(xhr); console.error(xhr);
} }
); );
}; };
function UndoStockTransaction(transactionId) function UndoStockTransaction(transactionId) {
{
Grocy.Api.Post('stock/transactions/' + transactionId.toString() + '/undo', {}, Grocy.Api.Post('stock/transactions/' + transactionId.toString() + '/undo', {},
function(result) function(result) {
{
toastr.success(__t("Transaction successfully undone")); toastr.success(__t("Transaction successfully undone"));
}, },
function(xhr) function(xhr) {
{
console.error(xhr); console.error(xhr);
} }
); );
}; };
if (GetUriParam("embedded") !== undefined) if (GetUriParam("embedded") !== undefined) {
{
var locationId = GetUriParam('locationId'); var locationId = GetUriParam('locationId');
if (typeof locationId === 'undefined') if (typeof locationId === 'undefined') {
{ Grocy.Components.ProductPicker.Validate();
Grocy.Components.ProductPicker.GetPicker().trigger('change'); Grocy.Components.ProductPicker.Focus();
Grocy.Components.ProductPicker.GetInputElement().focus(); } else {
}
else
{
$("#location_id_from").val(locationId); $("#location_id_from").val(locationId);
$("#location_id_from").trigger('change'); $("#location_id_from").trigger('change');
$("#use_specific_stock_entry").click(); $("#use_specific_stock_entry").trigger('click');
$("#use_specific_stock_entry").trigger('change'); $("#use_specific_stock_entry").trigger('change');
Grocy.Components.ProductPicker.GetPicker().trigger('change'); Grocy.Components.ProductPicker.Validate();
} }
} }
// Default input field // Default input field
Grocy.Components.ProductPicker.GetInputElement().focus(); Grocy.Components.ProductPicker.Focus();

View File

@ -10,21 +10,16 @@
<div class="title-related-links"> <div class="title-related-links">
<h2 class="title">@yield('title')</h2> <h2 class="title">@yield('title')</h2>
<div class="float-right"> <div class="float-right">
<button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" <button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" type="button"
type="button" data-toggle="collapse" data-target="#table-filter-row">
data-toggle="collapse"
data-target="#table-filter-row">
<i class="fas fa-filter"></i> <i class="fas fa-filter"></i>
</button> </button>
<button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" <button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" type="button"
type="button" data-toggle="collapse" data-target="#related-links">
data-toggle="collapse"
data-target="#related-links">
<i class="fas fa-ellipsis-v"></i> <i class="fas fa-ellipsis-v"></i>
</button> </button>
</div> </div>
<div class="related-links collapse d-md-flex order-2 width-xs-sm-100" <div class="related-links collapse d-md-flex order-2 width-xs-sm-100" id="related-links">
id="related-links">
<a class="btn btn-primary responsive-button permission-MASTER_DATA_EDIT m-1 mt-md-0 mb-md-0 float-right show-as-dialog-link" <a class="btn btn-primary responsive-button permission-MASTER_DATA_EDIT m-1 mt-md-0 mb-md-0 float-right show-as-dialog-link"
href="{{ $U('/battery/new?embedded') }}"> href="{{ $U('/battery/new?embedded') }}">
{{ $__t('Add') }} {{ $__t('Add') }}
@ -40,35 +35,26 @@
<hr class="my-2"> <hr class="my-2">
<div class="row collapse d-md-flex" <div class="row collapse d-md-flex" id="table-filter-row">
id="table-filter-row">
<div class="col-12 col-md-6 col-xl-3"> <div class="col-12 col-md-6 col-xl-3">
<div class="input-group"> <div class="input-group">
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-search"></i></span> <span class="input-group-text"><i class="fas fa-search"></i></span>
</div> </div>
<input type="text" <input type="text" id="search" class="form-control" placeholder="{{ $__t('Search') }}">
id="search"
class="form-control"
placeholder="{{ $__t('Search') }}">
</div> </div>
</div> </div>
<div class="col-12 col-md-6 col-xl-3"> <div class="col-12 col-md-6 col-xl-3">
<div class="form-check custom-control custom-checkbox"> <div class="form-check custom-control custom-checkbox">
<input class="form-check-input custom-control-input" <input class="form-check-input custom-control-input" type="checkbox" id="show-disabled">
type="checkbox" <label class="form-check-label custom-control-label" for="show-disabled">
id="show-disabled">
<label class="form-check-label custom-control-label"
for="show-disabled">
{{ $__t('Show disabled') }} {{ $__t('Show disabled') }}
</label> </label>
</div> </div>
</div> </div>
<div class="col"> <div class="col">
<div class="float-right"> <div class="float-right">
<a id="clear-filter-button" <a id="clear-filter-button" class="btn btn-sm btn-outline-info" href="#">
class="btn btn-sm btn-outline-info"
href="#">
{{ $__t('Clear filter') }} {{ $__t('Clear filter') }}
</a> </a>
</div> </div>
@ -77,24 +63,22 @@
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<table id="batteries-table" {{-- TODO: DataTables: dynamic data: batteries --}}
class="table table-sm table-striped nowrap w-100"> <table id="batteries-table" class="table table-sm table-striped nowrap w-100">
<thead> <thead>
<tr> <tr>
<th class="border-right"><a class="text-muted change-table-columns-visibility-button" <th class="border-right"><a class="text-muted change-table-columns-visibility-button"
data-toggle="tooltip" data-toggle="tooltip" title="{{ $__t('Table options') }}"
title="{{ $__t('Table options') }}" data-table-selector="#batteries-table" href="#"><i class="fas fa-eye"></i></a>
data-table-selector="#batteries-table"
href="#"><i class="fas fa-eye"></i></a>
</th> </th>
<th>{{ $__t('Name') }}</th> <th>{{ $__t('Name') }}</th>
<th>{{ $__t('Description') }}</th> <th>{{ $__t('Description') }}</th>
<th class="allow-grouping">{{ $__t('Used in') }}</th> <th class="allow-grouping">{{ $__t('Used in') }}</th>
<th class="allow-grouping">{{ $__t('Charge cycle interval (days)') }}</th> <th class="allow-grouping">{{ $__t('Charge cycle interval (days)') }}</th>
@include('components.userfields_thead', array( @include('components.userfields_thead', [
'userfields' => $userfields 'userfields' => $userfields,
)) ])
</tr> </tr>
</thead> </thead>
@ -103,17 +87,13 @@
<tr class="@if ($battery->active == 0) text-muted @endif"> <tr class="@if ($battery->active == 0) text-muted @endif">
<td class="fit-content border-right"> <td class="fit-content border-right">
<a class="btn btn-info btn-sm permission-MASTER_DATA_EDIT show-as-dialog-link" <a class="btn btn-info btn-sm permission-MASTER_DATA_EDIT show-as-dialog-link"
href="{{ $U('/battery/') }}{{ $battery->id }}?embedded" href="{{ $U('/battery/') }}{{ $battery->id }}?embedded" data-toggle="tooltip"
data-toggle="tooltip"
title="{{ $__t('Edit this item') }}"> title="{{ $__t('Edit this item') }}">
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
</a> </a>
<a class="btn btn-danger btn-sm battery-delete-button permission-MASTER_DATA_EDIT" <a class="btn btn-danger btn-sm battery-delete-button permission-MASTER_DATA_EDIT" href="#"
href="#" data-battery-id="{{ $battery->id }}" data-battery-name="{{ $battery->name }}"
data-battery-id="{{ $battery->id }}" data-toggle="tooltip" title="{{ $__t('Delete this item') }}">
data-battery-name="{{ $battery->name }}"
data-toggle="tooltip"
title="{{ $__t('Delete this item') }}">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
</a> </a>
</td> </td>
@ -130,10 +110,14 @@
{{ $battery->charge_interval_days }} {{ $battery->charge_interval_days }}
</td> </td>
@include('components.userfields_tbody', array( @include('components.userfields_tbody', [
'userfields' => $userfields, 'userfields' => $userfields,
'userfieldValues' => FindAllObjectsInArrayByPropertyValue($userfieldValues, 'object_id', $battery->id) 'userfieldValues' => FindAllObjectsInArrayByPropertyValue(
)) $userfieldValues,
'object_id',
$battery->id
),
])
</tr> </tr>
@endforeach @endforeach

View File

@ -9,9 +9,7 @@
<div class="col"> <div class="col">
<h2 class="title">@yield('title')</h2> <h2 class="title">@yield('title')</h2>
<div class="float-right"> <div class="float-right">
<button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" <button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" type="button" data-toggle="collapse"
type="button"
data-toggle="collapse"
data-target="#table-filter-row"> data-target="#table-filter-row">
<i class="fas fa-filter"></i> <i class="fas fa-filter"></i>
</button> </button>
@ -21,17 +19,13 @@
<hr class="my-2"> <hr class="my-2">
<div class="row collapse d-md-flex" <div class="row collapse d-md-flex" id="table-filter-row">
id="table-filter-row">
<div class="col-12 col-md-6 col-xl-3"> <div class="col-12 col-md-6 col-xl-3">
<div class="input-group"> <div class="input-group">
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-search"></i></span> <span class="input-group-text"><i class="fas fa-search"></i></span>
</div> </div>
<input type="text" <input type="text" id="search" class="form-control" placeholder="{{ $__t('Search') }}">
id="search"
class="form-control"
placeholder="{{ $__t('Search') }}">
</div> </div>
</div> </div>
<div class="col-12 col-md-6 col-xl-3"> <div class="col-12 col-md-6 col-xl-3">
@ -39,8 +33,8 @@
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-filter"></i>&nbsp;{{ $__t('Battery') }}</span> <span class="input-group-text"><i class="fas fa-filter"></i>&nbsp;{{ $__t('Battery') }}</span>
</div> </div>
<select class="custom-control custom-select" {{-- TODO: Select2: dynamic data: batteries --}}
id="battery-filter"> <select class="custom-control custom-select" id="battery-filter">
<option value="all">{{ $__t('All') }}</option> <option value="all">{{ $__t('All') }}</option>
@foreach ($batteries as $battery) @foreach ($batteries as $battery)
<option value="{{ $battery->id }}">{{ $battery->name }}</option> <option value="{{ $battery->id }}">{{ $battery->name }}</option>
@ -53,22 +47,18 @@
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-clock"></i>&nbsp;{{ $__t('Date range') }}</span> <span class="input-group-text"><i class="fas fa-clock"></i>&nbsp;{{ $__t('Date range') }}</span>
</div> </div>
<select class="custom-control custom-select" <select class="custom-control custom-select" id="daterange-filter">
id="daterange-filter">
<option value="1">{{ $__n(1, '%s month', '%s months') }}</option> <option value="1">{{ $__n(1, '%s month', '%s months') }}</option>
<option value="6">{{ $__n(6, '%s month', '%s months') }}</option> <option value="6">{{ $__n(6, '%s month', '%s months') }}</option>
<option value="12">{{ $__n(1, '%s year', '%s years') }}</option> <option value="12">{{ $__n(1, '%s year', '%s years') }}</option>
<option value="24" <option value="24" selected>{{ $__n(2, '%s month', '%s years') }}</option>
selected>{{ $__n(2, '%s month', '%s years') }}</option>
<option value="9999">{{ $__t('All') }}</option> <option value="9999">{{ $__t('All') }}</option>
</select> </select>
</div> </div>
</div> </div>
<div class="col"> <div class="col">
<div class="float-right"> <div class="float-right">
<a id="clear-filter-button" <a id="clear-filter-button" class="btn btn-sm btn-outline-info" href="#">
class="btn btn-sm btn-outline-info"
href="#">
{{ $__t('Clear filter') }} {{ $__t('Clear filter') }}
</a> </a>
</div> </div>
@ -77,16 +67,13 @@
<div class="row mt-2"> <div class="row mt-2">
<div class="col"> <div class="col">
<table id="batteries-journal-table" {{-- TODO: DataTables: dynamic data: battery_charge_cycles --}}
class="table table-sm table-striped nowrap w-100"> <table id="batteries-journal-table" class="table table-sm table-striped nowrap w-100">
<thead> <thead>
<tr> <tr>
<th class="border-right"><a class="text-muted change-table-columns-visibility-button" <th class="border-right"><a class="text-muted change-table-columns-visibility-button"
data-toggle="tooltip" data-toggle="tooltip" data-toggle="tooltip" title="{{ $__t('Table options') }}"
data-toggle="tooltip" data-table-selector="#batteries-journal-table" href="#"><i class="fas fa-eye"></i></a>
title="{{ $__t('Table options') }}"
data-table-selector="#batteries-journal-table"
href="#"><i class="fas fa-eye"></i></a>
</th> </th>
<th class="allow-grouping">{{ $__t('Battery') }}</th> <th class="allow-grouping">{{ $__t('Battery') }}</th>
<th>{{ $__t('Tracked time') }}</th> <th>{{ $__t('Tracked time') }}</th>
@ -98,16 +85,14 @@
class="@if ($chargeCycleEntry->undone == 1) text-muted @endif"> class="@if ($chargeCycleEntry->undone == 1) text-muted @endif">
<td class="fit-content border-right"> <td class="fit-content border-right">
<a class="btn btn-secondary btn-xs undo-battery-execution-button @if ($chargeCycleEntry->undone == 1) disabled @endif permission-BATTERIES_UNDO_CHARGE_CYCLE" <a class="btn btn-secondary btn-xs undo-battery-execution-button @if ($chargeCycleEntry->undone == 1) disabled @endif permission-BATTERIES_UNDO_CHARGE_CYCLE"
href="#" href="#" data-charge-cycle-id="{{ $chargeCycleEntry->id }}" data-toggle="tooltip"
data-charge-cycle-id="{{ $chargeCycleEntry->id }}" data-placement="left" title="{{ $__t('Undo charge cycle') }}">
data-toggle="tooltip"
data-placement="left"
title="{{ $__t('Undo charge cycle') }}">
<i class="fas fa-undo"></i> <i class="fas fa-undo"></i>
</a> </a>
</td> </td>
<td> <td>
<span class="name-anchor @if($chargeCycleEntry->undone == 1) text-strike-through @endif">{{ FindObjectInArrayByPropertyValue($batteries, 'id', $chargeCycleEntry->battery_id)->name }}</span> <span
class="name-anchor @if ($chargeCycleEntry->undone == 1) text-strike-through @endif">{{ FindObjectInArrayByPropertyValue($batteries, 'id', $chargeCycleEntry->battery_id)->name }}</span>
@if ($chargeCycleEntry->undone == 1) @if ($chargeCycleEntry->undone == 1)
<br> <br>
{{ $__t('Undone on') . ' ' . $chargeCycleEntry->undone_timestamp }} {{ $__t('Undone on') . ' ' . $chargeCycleEntry->undone_timestamp }}

View File

@ -5,8 +5,7 @@
@section('viewJsName', 'batteriesoverview') @section('viewJsName', 'batteriesoverview')
@push('pageStyles') @push('pageStyles')
<link href="{{ $U('/node_modules/animate.css/animate.min.css?v=', true) }}{{ $version }}" <link href="{{ $U('/node_modules/animate.css/animate.min.css?v=', true) }}{{ $version }}" rel="stylesheet">
rel="stylesheet">
@endpush @endpush
@section('content') @section('content')
@ -14,14 +13,11 @@
<div class="col"> <div class="col">
<div class="title-related-links"> <div class="title-related-links">
<h2 class="title">@yield('title')</h2> <h2 class="title">@yield('title')</h2>
<button class="btn btn-outline-dark d-md-none mt-2 float-right order-1 order-md-3" <button class="btn btn-outline-dark d-md-none mt-2 float-right order-1 order-md-3" type="button"
type="button" data-toggle="collapse" data-target="#related-links">
data-toggle="collapse"
data-target="#related-links">
<i class="fas fa-ellipsis-v"></i> <i class="fas fa-ellipsis-v"></i>
</button> </button>
<div class="related-links collapse d-md-flex order-2 width-xs-sm-100" <div class="related-links collapse d-md-flex order-2 width-xs-sm-100" id="related-links">
id="related-links">
<a class="btn btn-outline-dark responsive-button m-1 mt-md-0 mb-md-0 float-right" <a class="btn btn-outline-dark responsive-button m-1 mt-md-0 mb-md-0 float-right"
href="{{ $U('/batteriesjournal') }}"> href="{{ $U('/batteriesjournal') }}">
{{ $__t('Journal') }} {{ $__t('Journal') }}
@ -29,26 +25,19 @@
</div> </div>
</div> </div>
<div class="border-top border-bottom my-2 py-1"> <div class="border-top border-bottom my-2 py-1">
<div id="info-overdue-batteries" <div id="info-overdue-batteries" data-status-filter="overdue"
data-status-filter="overdue"
class="error-message status-filter-message responsive-button mr-2"></div> class="error-message status-filter-message responsive-button mr-2"></div>
<div id="info-due-today-batteries" <div id="info-due-today-batteries" data-status-filter="duetoday"
data-status-filter="duetoday"
class="normal-message status-filter-message responsive-button mr-2"></div> class="normal-message status-filter-message responsive-button mr-2"></div>
<div id="info-due-soon-batteries" <div id="info-due-soon-batteries" data-status-filter="duesoon" data-next-x-days="{{ $nextXDays }}"
data-status-filter="duesoon" class="warning-message status-filter-message responsive-button @if ($nextXDays == 0) d-none @endif">
data-next-x-days="{{ $nextXDays }}" </div>
class="warning-message status-filter-message responsive-button @if($nextXDays == 0) d-none @endif"></div>
<div class="float-right"> <div class="float-right">
<a class="btn btn-sm btn-outline-info d-md-none mt-1" <a class="btn btn-sm btn-outline-info d-md-none mt-1" data-toggle="collapse" href="#table-filter-row"
data-toggle="collapse"
href="#table-filter-row"
role="button"> role="button">
<i class="fas fa-filter"></i> <i class="fas fa-filter"></i>
</a> </a>
<a id="clear-filter-button" <a id="clear-filter-button" class="btn btn-sm btn-outline-info mt-1" href="#">
class="btn btn-sm btn-outline-info mt-1"
href="#">
{{ $__t('Clear filter') }} {{ $__t('Clear filter') }}
</a> </a>
</div> </div>
@ -56,17 +45,13 @@
</div> </div>
</div> </div>
<div class="row collapse d-md-flex" <div class="row collapse d-md-flex" id="table-filter-row">
id="table-filter-row">
<div class="col-12 col-md-6 col-xl-3"> <div class="col-12 col-md-6 col-xl-3">
<div class="input-group"> <div class="input-group">
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-search"></i></span> <span class="input-group-text"><i class="fas fa-search"></i></span>
</div> </div>
<input type="text" <input type="text" id="search" class="form-control" placeholder="{{ $__t('Search') }}">
id="search"
class="form-control"
placeholder="{{ $__t('Search') }}">
</div> </div>
</div> </div>
<div class="col-12 col-md-6 col-xl-3"> <div class="col-12 col-md-6 col-xl-3">
@ -74,8 +59,7 @@
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-filter"></i>&nbsp;{{ $__t('Status') }}</span> <span class="input-group-text"><i class="fas fa-filter"></i>&nbsp;{{ $__t('Status') }}</span>
</div> </div>
<select class="custom-control custom-select" <select class="custom-control custom-select" id="status-filter">
id="status-filter">
<option value="all">{{ $__t('All') }}</option> <option value="all">{{ $__t('All') }}</option>
<option value="overdue">{{ $__t('Overdue') }}</option> <option value="overdue">{{ $__t('Overdue') }}</option>
<option value="duetoday">{{ $__t('Due today') }}</option> <option value="duetoday">{{ $__t('Due today') }}</option>
@ -89,16 +73,13 @@
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<table id="batteries-overview-table" {{-- TODO: DataTables: dynamic data: batteries --}}
class="table table-sm table-striped nowrap w-100"> <table id="batteries-overview-table" class="table table-sm table-striped nowrap w-100">
<thead> <thead>
<tr> <tr>
<th class="border-right"><a class="text-muted change-table-columns-visibility-button" <th class="border-right"><a class="text-muted change-table-columns-visibility-button"
data-toggle="tooltip" data-toggle="tooltip" data-toggle="tooltip" title="{{ $__t('Table options') }}"
data-toggle="tooltip" data-table-selector="#batteries-overview-table" href="#"><i class="fas fa-eye"></i></a>
title="{{ $__t('Table options') }}"
data-table-selector="#batteries-overview-table"
href="#"><i class="fas fa-eye"></i></a>
</th> </th>
<th>{{ $__t('Battery') }}</th> <th>{{ $__t('Battery') }}</th>
<th class="allow-grouping">{{ $__t('Used in') }}</th> <th class="allow-grouping">{{ $__t('Used in') }}</th>
@ -106,9 +87,9 @@
<th>{{ $__t('Next planned charge cycle') }}</th> <th>{{ $__t('Next planned charge cycle') }}</th>
<th class="d-none">Hidden status</th> <th class="d-none">Hidden status</th>
@include('components.userfields_thead', array( @include('components.userfields_thead', [
'userfields' => $userfields 'userfields' => $userfields,
)) ])
</tr> </tr>
</thead> </thead>
@ -118,29 +99,24 @@
class="@if ($currentBatteryEntry->due_type == 'overdue') table-danger @elseif($currentBatteryEntry->due_type == 'duetoday') table-info @elseif($currentBatteryEntry->due_type == 'duesoon') table-warning @endif"> class="@if ($currentBatteryEntry->due_type == 'overdue') table-danger @elseif($currentBatteryEntry->due_type == 'duetoday') table-info @elseif($currentBatteryEntry->due_type == 'duesoon') table-warning @endif">
<td class="fit-content border-right"> <td class="fit-content border-right">
<a class="btn btn-success btn-sm track-charge-cycle-button permission-BATTERIES_TRACK_CHARGE_CYCLE" <a class="btn btn-success btn-sm track-charge-cycle-button permission-BATTERIES_TRACK_CHARGE_CYCLE"
href="#" href="#" data-toggle="tooltip" data-placement="left"
data-toggle="tooltip"
data-placement="left"
title="{{ $__t('Track charge cycle') }}" title="{{ $__t('Track charge cycle') }}"
data-battery-id="{{ $currentBatteryEntry->battery_id }}" data-battery-id="{{ $currentBatteryEntry->battery_id }}"
data-battery-name="{{ FindObjectInArrayByPropertyValue($batteries, 'id', $currentBatteryEntry->battery_id)->name }}"> data-battery-name="{{ FindObjectInArrayByPropertyValue($batteries, 'id', $currentBatteryEntry->battery_id)->name }}">
<i class="fas fa-car-battery"></i> <i class="fas fa-car-battery"></i>
</a> </a>
<div class="dropdown d-inline-block"> <div class="dropdown d-inline-block">
<button class="btn btn-sm btn-light text-secondary" <button class="btn btn-sm btn-light text-secondary" type="button"
type="button"
data-toggle="dropdown"> data-toggle="dropdown">
<i class="fas fa-ellipsis-v"></i> <i class="fas fa-ellipsis-v"></i>
</button> </button>
<div class="table-inline-menu dropdown-menu dropdown-menu-right"> <div class="table-inline-menu dropdown-menu dropdown-menu-right">
<a class="dropdown-item battery-name-cell" <a class="dropdown-item battery-name-cell"
data-battery-id="{{ $currentBatteryEntry->battery_id }}" data-battery-id="{{ $currentBatteryEntry->battery_id }}" type="button"
type="button"
href="#"> href="#">
<span class="dropdown-item-text">{{ $__t('Battery overview') }}</span> <span class="dropdown-item-text">{{ $__t('Battery overview') }}</span>
</a> </a>
<a class="dropdown-item show-as-dialog-link" <a class="dropdown-item show-as-dialog-link" type="button"
type="button"
href="{{ $U('/batteriesjournal?embedded&battery=') }}{{ $currentBatteryEntry->battery_id }}"> href="{{ $U('/batteriesjournal?embedded&battery=') }}{{ $currentBatteryEntry->battery_id }}">
<span class="dropdown-item-text">{{ $__t('Battery journal') }}</span> <span class="dropdown-item-text">{{ $__t('Battery journal') }}</span>
</a> </a>
@ -150,15 +126,13 @@
<span class="dropdown-item-text">{{ $__t('Edit battery') }}</span> <span class="dropdown-item-text">{{ $__t('Edit battery') }}</span>
</a> </a>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a class="dropdown-item" <a class="dropdown-item" type="button"
type="button"
href="{{ $U('/battery/' . $currentBatteryEntry->battery_id . '/grocycode?download=true') }}"> href="{{ $U('/battery/' . $currentBatteryEntry->battery_id . '/grocycode?download=true') }}">
{!! str_replace('grocycode', '<span class="ls-n1">grocycode</span>', $__t('Download %s grocycode', $__t('Battery'))) !!} {!! str_replace('grocycode', '<span class="ls-n1">grocycode</span>', $__t('Download %s grocycode', $__t('Battery'))) !!}
</a> </a>
@if (GROCY_FEATURE_FLAG_LABEL_PRINTER) @if (GROCY_FEATURE_FLAG_LABEL_PRINTER)
<a class="dropdown-item battery-grocycode-label-print" <a class="dropdown-item battery-grocycode-label-print"
data-battery-id="{{ $currentBatteryEntry->battery_id }}" data-battery-id="{{ $currentBatteryEntry->battery_id }}" type="button"
type="button"
href="#"> href="#">
{!! str_replace('grocycode', '<span class="ls-n1">grocycode</span>', $__t('Print %s grocycode on label printer', $__t('Battery'))) !!} {!! str_replace('grocycode', '<span class="ls-n1">grocycode</span>', $__t('Print %s grocycode on label printer', $__t('Battery'))) !!}
</a> </a>
@ -174,14 +148,16 @@
{{ FindObjectInArrayByPropertyValue($batteries, 'id', $currentBatteryEntry->battery_id)->used_in }} {{ FindObjectInArrayByPropertyValue($batteries, 'id', $currentBatteryEntry->battery_id)->used_in }}
</td> </td>
<td> <td>
<span id="battery-{{ $currentBatteryEntry->battery_id }}-last-tracked-time">{{ $currentBatteryEntry->last_tracked_time }}</span> <span
id="battery-{{ $currentBatteryEntry->battery_id }}-last-tracked-time">{{ $currentBatteryEntry->last_tracked_time }}</span>
<time id="battery-{{ $currentBatteryEntry->battery_id }}-last-tracked-time-timeago" <time id="battery-{{ $currentBatteryEntry->battery_id }}-last-tracked-time-timeago"
class="timeago timeago-contextual" class="timeago timeago-contextual"
datetime="{{ $currentBatteryEntry->last_tracked_time }}"></time> datetime="{{ $currentBatteryEntry->last_tracked_time }}"></time>
</td> </td>
<td> <td>
@if (FindObjectInArrayByPropertyValue($batteries, 'id', $currentBatteryEntry->battery_id)->charge_interval_days > 0) @if (FindObjectInArrayByPropertyValue($batteries, 'id', $currentBatteryEntry->battery_id)->charge_interval_days > 0)
<span id="battery-{{ $currentBatteryEntry->battery_id }}-next-charge-time">{{ $currentBatteryEntry->next_estimated_charge_time }}</span> <span
id="battery-{{ $currentBatteryEntry->battery_id }}-next-charge-time">{{ $currentBatteryEntry->next_estimated_charge_time }}</span>
<time id="battery-{{ $currentBatteryEntry->battery_id }}-next-charge-time-timeago" <time id="battery-{{ $currentBatteryEntry->battery_id }}-next-charge-time-timeago"
class="timeago timeago-contextual" class="timeago timeago-contextual"
datetime="{{ $currentBatteryEntry->next_estimated_charge_time }}"></time> datetime="{{ $currentBatteryEntry->next_estimated_charge_time }}"></time>
@ -196,10 +172,14 @@
@endif @endif
</td> </td>
@include('components.userfields_tbody', @include('components.userfields_tbody', [
array( 'userfields'=> $userfields, 'userfields' => $userfields,
'userfieldValues' => FindAllObjectsInArrayByPropertyValue($userfieldValues, 'object_id', $currentBatteryEntry->battery_id) 'userfieldValues' => FindAllObjectsInArrayByPropertyValue(
)) $userfieldValues,
'object_id',
$currentBatteryEntry->battery_id
),
])
</tr> </tr>
@endforeach @endforeach
@ -208,18 +188,14 @@
</div> </div>
</div> </div>
<div class="modal fade" <div class="modal fade" id="batteriesoverview-batterycard-modal" tabindex="-1">
id="batteriesoverview-batterycard-modal"
tabindex="-1">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content text-center"> <div class="modal-content text-center">
<div class="modal-body"> <div class="modal-body">
@include('components.batterycard') @include('components.batterycard')
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" <button type="button" class="btn btn-secondary" data-dismiss="modal">{{ $__t('Close') }}</button>
class="btn btn-secondary"
data-dismiss="modal">{{ $__t('Close') }}</button>
</div> </div>
</div> </div>
</div> </div>

View File

@ -11,21 +11,15 @@
<hr class="my-2"> <hr class="my-2">
<form id="batterytracking-form" <form id="batterytracking-form" novalidate>
novalidate>
<div class="form-group"> <div class="form-group">
<label class="w-100" <label class="w-100" for="battery_id">
for="battery_id">
{{ $__t('Battery') }} {{ $__t('Battery') }}
<i id="barcode-lookup-hint" <i id="barcode-lookup-hint" class="fas fa-barcode float-right mt-1"></i>
class="fas fa-barcode float-right mt-1"></i>
</label> </label>
<select class="form-control combobox barcodescanner-input" {{-- TODO: Select2: dynamic data: batteries --}}
id="battery_id" <select class="form-control combobox barcodescanner-input" id="battery_id" name="battery_id" required data-target="@batterypicker">
name="battery_id"
required
data-target="@batterypicker">
<option value=""></option> <option value=""></option>
@foreach($batteries as $battery) @foreach($batteries as $battery)
<option value="{{ $battery->id }}">{{ $battery->name }}</option> <option value="{{ $battery->id }}">{{ $battery->name }}</option>
@ -44,8 +38,7 @@
'invalidFeedback' => $__t('This can only be before now') 'invalidFeedback' => $__t('This can only be before now')
)) ))
<button id="save-batterytracking-button" <button id="save-batterytracking-button" class="btn btn-success">{{ $__t('OK') }}</button>
class="btn btn-success">{{ $__t('OK') }}</button>
</form> </form>
</div> </div>

View File

@ -276,7 +276,7 @@
@php $prefillById = ''; if($mode=='edit' && !empty($chore->product_id)) { $prefillById = $chore->product_id; } @endphp @php $prefillById = ''; if($mode=='edit' && !empty($chore->product_id)) { $prefillById = $chore->product_id; } @endphp
@include('components.productpicker', array( @include('components.productpicker', array(
'products' => $products, 'productsQuery' => 'order=name%3Acollate%20nocase',
'nextInputSelector' => '#product_amount', 'nextInputSelector' => '#product_amount',
'isRequired' => false, 'isRequired' => false,
'disallowAllProductWorkflows' => true, 'disallowAllProductWorkflows' => true,

View File

@ -10,21 +10,16 @@
<div class="title-related-links"> <div class="title-related-links">
<h2 class="title">@yield('title')</h2> <h2 class="title">@yield('title')</h2>
<div class="float-right"> <div class="float-right">
<button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" <button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" type="button"
type="button" data-toggle="collapse" data-target="#table-filter-row">
data-toggle="collapse"
data-target="#table-filter-row">
<i class="fas fa-filter"></i> <i class="fas fa-filter"></i>
</button> </button>
<button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" <button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" type="button"
type="button" data-toggle="collapse" data-target="#related-links">
data-toggle="collapse"
data-target="#related-links">
<i class="fas fa-ellipsis-v"></i> <i class="fas fa-ellipsis-v"></i>
</button> </button>
</div> </div>
<div class="related-links collapse d-md-flex order-2 width-xs-sm-100" <div class="related-links collapse d-md-flex order-2 width-xs-sm-100" id="related-links">
id="related-links">
<a class="btn btn-primary responsive-button m-1 mt-md-0 mb-md-0 float-right" <a class="btn btn-primary responsive-button m-1 mt-md-0 mb-md-0 float-right"
href="{{ $U('/chore/new') }}"> href="{{ $U('/chore/new') }}">
{{ $__t('Add') }} {{ $__t('Add') }}
@ -40,35 +35,26 @@
<hr class="my-2"> <hr class="my-2">
<div class="row collapse d-md-flex" <div class="row collapse d-md-flex" id="table-filter-row">
id="table-filter-row">
<div class="col-12 col-md-6 col-xl-3"> <div class="col-12 col-md-6 col-xl-3">
<div class="input-group"> <div class="input-group">
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-search"></i></span> <span class="input-group-text"><i class="fas fa-search"></i></span>
</div> </div>
<input type="text" <input type="text" id="search" class="form-control" placeholder="{{ $__t('Search') }}">
id="search"
class="form-control"
placeholder="{{ $__t('Search') }}">
</div> </div>
</div> </div>
<div class="col-12 col-md-6 col-xl-3"> <div class="col-12 col-md-6 col-xl-3">
<div class="form-check custom-control custom-checkbox"> <div class="form-check custom-control custom-checkbox">
<input class="form-check-input custom-control-input" <input class="form-check-input custom-control-input" type="checkbox" id="show-disabled">
type="checkbox" <label class="form-check-label custom-control-label" for="show-disabled">
id="show-disabled">
<label class="form-check-label custom-control-label"
for="show-disabled">
{{ $__t('Show disabled') }} {{ $__t('Show disabled') }}
</label> </label>
</div> </div>
</div> </div>
<div class="col"> <div class="col">
<div class="float-right"> <div class="float-right">
<a id="clear-filter-button" <a id="clear-filter-button" class="btn btn-sm btn-outline-info" href="#">
class="btn btn-sm btn-outline-info"
href="#">
{{ $__t('Clear filter') }} {{ $__t('Clear filter') }}
</a> </a>
</div> </div>
@ -77,24 +63,21 @@
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<table id="chores-table" {{-- TODO: DataTables: dynamic data: chores --}}
class="table table-sm table-striped nowrap w-100"> <table id="chores-table" class="table table-sm table-striped nowrap w-100">
<thead> <thead>
<tr> <tr>
<th class="border-right"><a class="text-muted change-table-columns-visibility-button" <th class="border-right"><a class="text-muted change-table-columns-visibility-button"
data-toggle="tooltip" data-toggle="tooltip" data-toggle="tooltip" title="{{ $__t('Table options') }}"
data-toggle="tooltip" data-table-selector="#chores-table" href="#"><i class="fas fa-eye"></i></a>
title="{{ $__t('Table options') }}"
data-table-selector="#chores-table"
href="#"><i class="fas fa-eye"></i></a>
</th> </th>
<th>{{ $__t('Name') }}</th> <th>{{ $__t('Name') }}</th>
<th class="allow-grouping">{{ $__t('Period type') }}</th> <th class="allow-grouping">{{ $__t('Period type') }}</th>
<th>{{ $__t('Description') }}</th> <th>{{ $__t('Description') }}</th>
@include('components.userfields_thead', array( @include('components.userfields_thead', [
'userfields' => $userfields 'userfields' => $userfields,
)) ])
</tr> </tr>
</thead> </thead>
@ -102,31 +85,23 @@
@foreach ($chores as $chore) @foreach ($chores as $chore)
<tr class="@if ($chore->active == 0) text-muted @endif"> <tr class="@if ($chore->active == 0) text-muted @endif">
<td class="fit-content border-right"> <td class="fit-content border-right">
<a class="btn btn-info btn-sm" <a class="btn btn-info btn-sm" href="{{ $U('/chore/') }}{{ $chore->id }}"
href="{{ $U('/chore/') }}{{ $chore->id }}" data-toggle="tooltip" title="{{ $__t('Edit this item') }}">
data-toggle="tooltip"
title="{{ $__t('Edit this item') }}">
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
</a> </a>
<a class="btn btn-danger btn-sm chore-delete-button" <a class="btn btn-danger btn-sm chore-delete-button" href="#"
href="#" data-chore-id="{{ $chore->id }}" data-chore-name="{{ $chore->name }}"
data-chore-id="{{ $chore->id }}" data-toggle="tooltip" title="{{ $__t('Delete this item') }}">
data-chore-name="{{ $chore->name }}"
data-toggle="tooltip"
title="{{ $__t('Delete this item') }}">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
</a> </a>
<div class="dropdown d-inline-block"> <div class="dropdown d-inline-block">
<button class="btn btn-sm btn-light text-secondary" <button class="btn btn-sm btn-light text-secondary" type="button"
type="button"
data-toggle="dropdown"> data-toggle="dropdown">
<i class="fas fa-ellipsis-v"></i> <i class="fas fa-ellipsis-v"></i>
</button> </button>
<div class="table-inline-menu dropdown-menu dropdown-menu-right"> <div class="table-inline-menu dropdown-menu dropdown-menu-right">
<a class="dropdown-item merge-chores-button" <a class="dropdown-item merge-chores-button" data-chore-id="{{ $chore->id }}"
data-chore-id="{{ $chore->id }}" type="button" href="#">
type="button"
href="#">
<span class="dropdown-item-text">{{ $__t('Merge') }}</span> <span class="dropdown-item-text">{{ $__t('Merge') }}</span>
</a> </a>
</div> </div>
@ -142,10 +117,14 @@
{{ $chore->description }} {{ $chore->description }}
</td> </td>
@include('components.userfields_tbody', array( @include('components.userfields_tbody', [
'userfields' => $userfields, 'userfields' => $userfields,
'userfieldValues' => FindAllObjectsInArrayByPropertyValue($userfieldValues, 'object_id', $chore->id) 'userfieldValues' => FindAllObjectsInArrayByPropertyValue(
)) $userfieldValues,
'object_id',
$chore->id
),
])
</tr> </tr>
@endforeach @endforeach
@ -154,9 +133,7 @@
</div> </div>
</div> </div>
<div class="modal fade" <div class="modal fade" id="merge-chores-modal" tabindex="-1">
id="merge-chores-modal"
tabindex="-1">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content text-center"> <div class="modal-content text-center">
<div class="modal-header"> <div class="modal-header">
@ -164,13 +141,12 @@
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="form-group"> <div class="form-group">
<label for="merge-chores-keep">{{ $__t('Chore to keep') }}&nbsp;<i class="fas fa-question-circle text-muted" <label for="merge-chores-keep">{{ $__t('Chore to keep') }}&nbsp;<i
data-toggle="tooltip" class="fas fa-question-circle text-muted" data-toggle="tooltip" data-trigger="hover click"
data-trigger="hover click"
title="{{ $__t('After merging, this chore will be kept') }}"></i> title="{{ $__t('After merging, this chore will be kept') }}"></i>
</label> </label>
<select class="custom-control custom-select" {{-- TODO: Select2: dynamic data: chores --}}
id="merge-chores-keep"> <select class="custom-control custom-select" id="merge-chores-keep">
<option></option> <option></option>
@foreach ($chores as $chore) @foreach ($chores as $chore)
<option value="{{ $chore->id }}">{{ $chore->name }}</option> <option value="{{ $chore->id }}">{{ $chore->name }}</option>
@ -178,13 +154,12 @@
</select> </select>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="merge-chores-remove">{{ $__t('Chore to remove') }}&nbsp;<i class="fas fa-question-circle text-muted" <label for="merge-chores-remove">{{ $__t('Chore to remove') }}&nbsp;<i
data-toggle="tooltip" class="fas fa-question-circle text-muted" data-toggle="tooltip" data-trigger="hover click"
data-trigger="hover click"
title="{{ $__t('After merging, all occurences of this chore will be replaced by the kept chore (means this chore will not exist anymore)') }}"></i> title="{{ $__t('After merging, all occurences of this chore will be replaced by the kept chore (means this chore will not exist anymore)') }}"></i>
</label> </label>
<select class="custom-control custom-select" {{-- TODO: Select2: dynamic data: chores --}}
id="merge-chores-remove"> <select class="custom-control custom-select" id="merge-chores-remove">
<option></option> <option></option>
@foreach ($chores as $chore) @foreach ($chores as $chore)
<option value="{{ $chore->id }}">{{ $chore->name }}</option> <option value="{{ $chore->id }}">{{ $chore->name }}</option>
@ -193,12 +168,8 @@
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" <button type="button" class="btn btn-secondary" data-dismiss="modal">{{ $__t('Cancel') }}</button>
class="btn btn-secondary" <button id="merge-chores-save-button" type="button" class="btn btn-primary"
data-dismiss="modal">{{ $__t('Cancel') }}</button>
<button id="merge-chores-save-button"
type="button"
class="btn btn-primary"
data-dismiss="modal">{{ $__t('OK') }}</button> data-dismiss="modal">{{ $__t('OK') }}</button>
</div> </div>
</div> </div>

View File

@ -9,9 +9,7 @@
<div class="col"> <div class="col">
<h2 class="title">@yield('title')</h2> <h2 class="title">@yield('title')</h2>
<div class="float-right"> <div class="float-right">
<button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" <button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" type="button" data-toggle="collapse"
type="button"
data-toggle="collapse"
data-target="#table-filter-row"> data-target="#table-filter-row">
<i class="fas fa-filter"></i> <i class="fas fa-filter"></i>
</button> </button>
@ -21,17 +19,13 @@
<hr class="my-2"> <hr class="my-2">
<div class="row collapse d-md-flex" <div class="row collapse d-md-flex" id="table-filter-row">
id="table-filter-row">
<div class="col-12 col-md-6 col-xl-3"> <div class="col-12 col-md-6 col-xl-3">
<div class="input-group"> <div class="input-group">
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-search"></i></span> <span class="input-group-text"><i class="fas fa-search"></i></span>
</div> </div>
<input type="text" <input type="text" id="search" class="form-control" placeholder="{{ $__t('Search') }}">
id="search"
class="form-control"
placeholder="{{ $__t('Search') }}">
</div> </div>
</div> </div>
<div class="col-12 col-md-6 col-xl-3"> <div class="col-12 col-md-6 col-xl-3">
@ -39,8 +33,8 @@
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-filter"></i>&nbsp;{{ $__t('Chore') }}</span> <span class="input-group-text"><i class="fas fa-filter"></i>&nbsp;{{ $__t('Chore') }}</span>
</div> </div>
<select class="custom-control custom-select" {{-- TODO: Select2: dynamic data: chores --}}
id="chore-filter"> <select class="custom-control custom-select" id="chore-filter">
<option value="all">{{ $__t('All') }}</option> <option value="all">{{ $__t('All') }}</option>
@foreach ($chores as $chore) @foreach ($chores as $chore)
<option value="{{ $chore->id }}">{{ $chore->name }}</option> <option value="{{ $chore->id }}">{{ $chore->name }}</option>
@ -53,12 +47,10 @@
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-clock"></i>&nbsp;{{ $__t('Date range') }}</span> <span class="input-group-text"><i class="fas fa-clock"></i>&nbsp;{{ $__t('Date range') }}</span>
</div> </div>
<select class="custom-control custom-select" <select class="custom-control custom-select" id="daterange-filter">
id="daterange-filter">
<option value="1">{{ $__n(1, '%s month', '%s months') }}</option> <option value="1">{{ $__n(1, '%s month', '%s months') }}</option>
<option value="6">{{ $__n(6, '%s month', '%s months') }}</option> <option value="6">{{ $__n(6, '%s month', '%s months') }}</option>
<option value="12" <option value="12" selected>{{ $__n(1, '%s year', '%s years') }}</option>
selected>{{ $__n(1, '%s year', '%s years') }}</option>
<option value="24">{{ $__n(2, '%s month', '%s years') }}</option> <option value="24">{{ $__n(2, '%s month', '%s years') }}</option>
<option value="9999">{{ $__t('All') }}</option> <option value="9999">{{ $__t('All') }}</option>
</select> </select>
@ -66,9 +58,7 @@
</div> </div>
<div class="col"> <div class="col">
<div class="float-right"> <div class="float-right">
<a id="clear-filter-button" <a id="clear-filter-button" class="btn btn-sm btn-outline-info" href="#">
class="btn btn-sm btn-outline-info"
href="#">
{{ $__t('Clear filter') }} {{ $__t('Clear filter') }}
</a> </a>
</div> </div>
@ -77,16 +67,13 @@
<div class="row mt-2"> <div class="row mt-2">
<div class="col"> <div class="col">
<table id="chores-journal-table" {{-- TODO: DataTables: dynamic data: chores_log --}}
class="table table-sm table-striped nowrap w-100"> <table id="chores-journal-table" class="table table-sm table-striped nowrap w-100">
<thead> <thead>
<tr> <tr>
<th class="border-right"><a class="text-muted change-table-columns-visibility-button" <th class="border-right"><a class="text-muted change-table-columns-visibility-button"
data-toggle="tooltip" data-toggle="tooltip" data-toggle="tooltip" title="{{ $__t('Table options') }}"
data-toggle="tooltip" data-table-selector="#chores-journal-table" href="#"><i class="fas fa-eye"></i></a>
title="{{ $__t('Table options') }}"
data-table-selector="#chores-journal-table"
href="#"><i class="fas fa-eye"></i></a>
</th> </th>
<th class="allow-grouping">{{ $__t('Chore') }}</th> <th class="allow-grouping">{{ $__t('Chore') }}</th>
<th>{{ $__t('Tracked time') }}</th> <th>{{ $__t('Tracked time') }}</th>
@ -94,9 +81,9 @@
<th class="allow-grouping">{{ $__t('Done by') }}</th> <th class="allow-grouping">{{ $__t('Done by') }}</th>
@endif @endif
@include('components.userfields_thead', array( @include('components.userfields_thead', [
'userfields' => $userfields 'userfields' => $userfields,
)) ])
</tr> </tr>
</thead> </thead>
<tbody class="d-none"> <tbody class="d-none">
@ -105,16 +92,14 @@
class="@if ($choreLogEntry->undone == 1) text-muted @endif @if ($choreLogEntry->skipped == 1) font-italic @endif"> class="@if ($choreLogEntry->undone == 1) text-muted @endif @if ($choreLogEntry->skipped == 1) font-italic @endif">
<td class="fit-content border-right"> <td class="fit-content border-right">
<a class="btn btn-secondary btn-xs undo-chore-execution-button permission-CHORE_UNDO_EXECUTION @if ($choreLogEntry->undone == 1) disabled @endif" <a class="btn btn-secondary btn-xs undo-chore-execution-button permission-CHORE_UNDO_EXECUTION @if ($choreLogEntry->undone == 1) disabled @endif"
href="#" href="#" data-execution-id="{{ $choreLogEntry->id }}" data-toggle="tooltip"
data-execution-id="{{ $choreLogEntry->id }}" data-placement="left" title="{{ $__t('Undo chore execution') }}">
data-toggle="tooltip"
data-placement="left"
title="{{ $__t('Undo chore execution') }}">
<i class="fas fa-undo"></i> <i class="fas fa-undo"></i>
</a> </a>
</td> </td>
<td> <td>
<span class="name-anchor @if($choreLogEntry->undone == 1) text-strike-through @endif">{{ FindObjectInArrayByPropertyValue($chores, 'id', $choreLogEntry->chore_id)->name }}</span> <span
class="name-anchor @if ($choreLogEntry->undone == 1) text-strike-through @endif">{{ FindObjectInArrayByPropertyValue($chores, 'id', $choreLogEntry->chore_id)->name }}</span>
@if ($choreLogEntry->undone == 1) @if ($choreLogEntry->undone == 1)
<br> <br>
{{ $__t('Undone on') . ' ' . $choreLogEntry->undone_timestamp }} {{ $__t('Undone on') . ' ' . $choreLogEntry->undone_timestamp }}
@ -124,7 +109,8 @@
</td> </td>
<td> <td>
<span>{{ $choreLogEntry->tracked_time }}</span> <span>{{ $choreLogEntry->tracked_time }}</span>
<time class="timeago timeago-contextual @if(FindObjectInArrayByPropertyValue($chores, 'id', $choreLogEntry->chore_id)->track_date_only == 1) timeago-date-only @endif" <time
class="timeago timeago-contextual @if (FindObjectInArrayByPropertyValue($chores, 'id', $choreLogEntry->chore_id)->track_date_only == 1) timeago-date-only @endif"
datetime="{{ $choreLogEntry->tracked_time }}"></time> datetime="{{ $choreLogEntry->tracked_time }}"></time>
@if ($choreLogEntry->skipped == 1) @if ($choreLogEntry->skipped == 1)
<span class="text-muted">{{ $__t('Skipped') }}</span> <span class="text-muted">{{ $__t('Skipped') }}</span>
@ -140,10 +126,14 @@
</td> </td>
@endif @endif
@include('components.userfields_tbody', array( @include('components.userfields_tbody', [
'userfields' => $userfields, 'userfields' => $userfields,
'userfieldValues' => FindAllObjectsInArrayByPropertyValue($userfieldValues, 'object_id', $choreLogEntry->id) 'userfieldValues' => FindAllObjectsInArrayByPropertyValue(
)) $userfieldValues,
'object_id',
$choreLogEntry->id
),
])
</tr> </tr>
@endforeach @endforeach
</tbody> </tbody>

View File

@ -5,8 +5,7 @@
@section('viewJsName', 'choresoverview') @section('viewJsName', 'choresoverview')
@push('pageStyles') @push('pageStyles')
<link href="{{ $U('/node_modules/animate.css/animate.min.css?v=', true) }}{{ $version }}" <link href="{{ $U('/node_modules/animate.css/animate.min.css?v=', true) }}{{ $version }}" rel="stylesheet">
rel="stylesheet">
@endpush @endpush
@section('content') @section('content')
@ -14,14 +13,11 @@
<div class="col"> <div class="col">
<div class="title-related-links"> <div class="title-related-links">
<h2 class="title">@yield('title')</h2> <h2 class="title">@yield('title')</h2>
<button class="btn btn-outline-dark d-md-none mt-2 float-right order-1 order-md-3" <button class="btn btn-outline-dark d-md-none mt-2 float-right order-1 order-md-3" type="button"
type="button" data-toggle="collapse" data-target="#related-links">
data-toggle="collapse"
data-target="#related-links">
<i class="fas fa-ellipsis-v"></i> <i class="fas fa-ellipsis-v"></i>
</button> </button>
<div class="related-links collapse d-md-flex order-2 width-xs-sm-100" <div class="related-links collapse d-md-flex order-2 width-xs-sm-100" id="related-links">
id="related-links">
<a class="btn btn-outline-dark responsive-button m-1 mt-md-0 mb-md-0 float-right" <a class="btn btn-outline-dark responsive-button m-1 mt-md-0 mb-md-0 float-right"
href="{{ $U('/choresjournal') }}"> href="{{ $U('/choresjournal') }}">
{{ $__t('Journal') }} {{ $__t('Journal') }}
@ -29,31 +25,23 @@
</div> </div>
</div> </div>
<div class="border-top border-bottom my-2 py-1"> <div class="border-top border-bottom my-2 py-1">
<div id="info-overdue-chores" <div id="info-overdue-chores" data-status-filter="overdue"
data-status-filter="overdue"
class="error-message status-filter-message responsive-button mr-2"></div> class="error-message status-filter-message responsive-button mr-2"></div>
<div id="info-due-today-chores" <div id="info-due-today-chores" data-status-filter="duetoday"
data-status-filter="duetoday"
class="normal-message status-filter-message responsive-button mr-2"></div> class="normal-message status-filter-message responsive-button mr-2"></div>
<div id="info-due-soon-chores" <div id="info-due-soon-chores" data-status-filter="duesoon" data-next-x-days="{{ $nextXDays }}"
data-status-filter="duesoon" class="warning-message status-filter-message responsive-message mr-2 @if ($nextXDays == 0) d-none @endif">
data-next-x-days="{{ $nextXDays }}" </div>
class="warning-message status-filter-message responsive-message mr-2 @if($nextXDays == 0) d-none @endif"></div>
@if (GROCY_FEATURE_FLAG_CHORES_ASSIGNMENTS) @if (GROCY_FEATURE_FLAG_CHORES_ASSIGNMENTS)
<div id="info-assigned-to-me-chores" <div id="info-assigned-to-me-chores" data-user-filter="xx{{ GROCY_USER_ID }}xx"
data-user-filter="xx{{ GROCY_USER_ID }}xx"
class="secondary-message user-filter-message responsive-button"></div> class="secondary-message user-filter-message responsive-button"></div>
@endif @endif
<div class="float-right"> <div class="float-right">
<a class="btn btn-sm btn-outline-info d-md-none mt-1" <a class="btn btn-sm btn-outline-info d-md-none mt-1" data-toggle="collapse" href="#table-filter-row"
data-toggle="collapse"
href="#table-filter-row"
role="button"> role="button">
<i class="fas fa-filter"></i> <i class="fas fa-filter"></i>
</a> </a>
<a id="clear-filter-button" <a id="clear-filter-button" class="btn btn-sm btn-outline-info mt-1" href="#">
class="btn btn-sm btn-outline-info mt-1"
href="#">
{{ $__t('Clear filter') }} {{ $__t('Clear filter') }}
</a> </a>
</div> </div>
@ -61,17 +49,13 @@
</div> </div>
</div> </div>
<div class="row collapse d-md-flex" <div class="row collapse d-md-flex" id="table-filter-row">
id="table-filter-row">
<div class="col-12 col-md-6 col-xl-3"> <div class="col-12 col-md-6 col-xl-3">
<div class="input-group"> <div class="input-group">
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-search"></i></span> <span class="input-group-text"><i class="fas fa-search"></i></span>
</div> </div>
<input type="text" <input type="text" id="search" class="form-control" placeholder="{{ $__t('Search') }}">
id="search"
class="form-control"
placeholder="{{ $__t('Search') }}">
</div> </div>
</div> </div>
<div class="col-12 col-md-6 col-xl-3"> <div class="col-12 col-md-6 col-xl-3">
@ -79,8 +63,7 @@
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-filter"></i>&nbsp;{{ $__t('Status') }}</span> <span class="input-group-text"><i class="fas fa-filter"></i>&nbsp;{{ $__t('Status') }}</span>
</div> </div>
<select class="custom-control custom-select" <select class="custom-control custom-select" id="status-filter">
id="status-filter">
<option value="all">{{ $__t('All') }}</option> <option value="all">{{ $__t('All') }}</option>
<option value="overdue">{{ $__t('Overdue') }}</option> <option value="overdue">{{ $__t('Overdue') }}</option>
<option value="duetoday">{{ $__t('Due today') }}</option> <option value="duetoday">{{ $__t('Due today') }}</option>
@ -94,14 +77,15 @@
<div class="col-12 col-md-6 col-xl-3"> <div class="col-12 col-md-6 col-xl-3">
<div class="input-group"> <div class="input-group">
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-filter"></i>&nbsp;{{ $__t('Assignment') }}</span> <span class="input-group-text"><i
class="fas fa-filter"></i>&nbsp;{{ $__t('Assignment') }}</span>
</div> </div>
<select class="custom-control custom-select" {{-- TODO: Select2: dynamic data: users --}}
id="user-filter"> <select class="custom-control custom-select" id="user-filter">
<option></option> <option></option>
@foreach ($users as $user) @foreach ($users as $user)
<option data-user-id="{{ $user->id }}" <option data-user-id="{{ $user->id }}" value="xx{{ $user->id }}xx">
value="xx{{ $user->id }}xx">{{ $user->display_name }}</option> {{ $user->display_name }}</option>
@endforeach @endforeach
</select> </select>
</div> </div>
@ -111,27 +95,25 @@
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<table id="chores-overview-table" {{-- TODO: DataTables: dynamic data: chores --}}
class="table table-sm table-striped nowrap w-100"> <table id="chores-overview-table" class="table table-sm table-striped nowrap w-100">
<thead> <thead>
<tr> <tr>
<th class="border-right"><a class="text-muted change-table-columns-visibility-button" <th class="border-right"><a class="text-muted change-table-columns-visibility-button"
data-toggle="tooltip" data-toggle="tooltip" data-toggle="tooltip" title="{{ $__t('Table options') }}"
data-toggle="tooltip" data-table-selector="#chores-overview-table" href="#"><i class="fas fa-eye"></i></a>
title="{{ $__t('Table options') }}"
data-table-selector="#chores-overview-table"
href="#"><i class="fas fa-eye"></i></a>
</th> </th>
<th>{{ $__t('Chore') }}</th> <th>{{ $__t('Chore') }}</th>
<th>{{ $__t('Next estimated tracking') }}</th> <th>{{ $__t('Next estimated tracking') }}</th>
<th>{{ $__t('Last tracked') }}</th> <th>{{ $__t('Last tracked') }}</th>
<th class="@if(!GROCY_FEATURE_FLAG_CHORES_ASSIGNMENTS) d-none @endif allow-grouping">{{ $__t('Assigned to') }}</th> <th class="@if (!GROCY_FEATURE_FLAG_CHORES_ASSIGNMENTS) d-none @endif allow-grouping">
{{ $__t('Assigned to') }}</th>
<th class="d-none">Hidden status</th> <th class="d-none">Hidden status</th>
<th class="d-none">Hidden assigned to user id</th> <th class="d-none">Hidden assigned to user id</th>
@include('components.userfields_thead', array( @include('components.userfields_thead', [
'userfields' => $userfields 'userfields' => $userfields,
)) ])
</tr> </tr>
</thead> </thead>
@ -141,70 +123,58 @@
class="@if ($curentChoreEntry->due_type == 'overdue') table-danger @elseif($curentChoreEntry->due_type == 'duetoday') table-info @elseif($curentChoreEntry->due_type == 'duesoon') table-warning @endif"> class="@if ($curentChoreEntry->due_type == 'overdue') table-danger @elseif($curentChoreEntry->due_type == 'duetoday') table-info @elseif($curentChoreEntry->due_type == 'duesoon') table-warning @endif">
<td class="fit-content border-right"> <td class="fit-content border-right">
<a class="btn btn-success btn-sm track-chore-button permission-CHORE_TRACK_EXECUTION" <a class="btn btn-success btn-sm track-chore-button permission-CHORE_TRACK_EXECUTION"
href="#" href="#" data-toggle="tooltip" data-placement="left"
data-toggle="tooltip"
data-placement="left"
title="{{ $__t('Track chore execution') }}" title="{{ $__t('Track chore execution') }}"
data-chore-id="{{ $curentChoreEntry->chore_id }}" data-chore-id="{{ $curentChoreEntry->chore_id }}"
data-chore-name="{{ FindObjectInArrayByPropertyValue($chores, 'id', $curentChoreEntry->chore_id)->name }}"> data-chore-name="{{ FindObjectInArrayByPropertyValue($chores, 'id', $curentChoreEntry->chore_id)->name }}">
<i class="fas fa-play"></i> <i class="fas fa-play"></i>
</a> </a>
<a class="btn btn-secondary btn-sm track-chore-button skip permission-CHORE_TRACK_EXECUTION @if (FindObjectInArrayByPropertyValue($chores, 'id', $curentChoreEntry->chore_id)->period_type == \Grocy\Services\ChoresService::CHORE_PERIOD_TYPE_MANUALLY) disabled @endif" <a class="btn btn-secondary btn-sm track-chore-button skip permission-CHORE_TRACK_EXECUTION @if (FindObjectInArrayByPropertyValue($chores, 'id', $curentChoreEntry->chore_id)->period_type == \Grocy\Services\ChoresService::CHORE_PERIOD_TYPE_MANUALLY) disabled @endif"
href="#" href="#" data-toggle="tooltip" data-placement="left"
data-toggle="tooltip"
data-placement="left"
title="{{ $__t('Skip next chore schedule') }}" title="{{ $__t('Skip next chore schedule') }}"
data-chore-id="{{ $curentChoreEntry->chore_id }}" data-chore-id="{{ $curentChoreEntry->chore_id }}"
data-chore-name="{{ FindObjectInArrayByPropertyValue($chores, 'id', $curentChoreEntry->chore_id)->name }}"> data-chore-name="{{ FindObjectInArrayByPropertyValue($chores, 'id', $curentChoreEntry->chore_id)->name }}">
<i class="fas fa-forward"></i> <i class="fas fa-forward"></i>
</a> </a>
<div class="dropdown d-inline-block"> <div class="dropdown d-inline-block">
<button class="btn btn-sm btn-light text-secondary" <button class="btn btn-sm btn-light text-secondary" type="button"
type="button"
data-toggle="dropdown"> data-toggle="dropdown">
<i class="fas fa-ellipsis-v"></i> <i class="fas fa-ellipsis-v"></i>
</button> </button>
<div class="table-inline-menu dropdown-menu dropdown-menu-right"> <div class="table-inline-menu dropdown-menu dropdown-menu-right">
<a class="dropdown-item chore-name-cell" <a class="dropdown-item chore-name-cell"
data-chore-id="{{ $curentChoreEntry->chore_id }}" data-chore-id="{{ $curentChoreEntry->chore_id }}" type="button" href="#">
type="button"
href="#">
<span class="dropdown-item-text">{{ $__t('Chore overview') }}</span> <span class="dropdown-item-text">{{ $__t('Chore overview') }}</span>
</a> </a>
<a class="dropdown-item show-as-dialog-link" <a class="dropdown-item show-as-dialog-link" type="button"
type="button"
href="{{ $U('/choresjournal?embedded&chore=') }}{{ $curentChoreEntry->chore_id }}"> href="{{ $U('/choresjournal?embedded&chore=') }}{{ $curentChoreEntry->chore_id }}">
<span class="dropdown-item-text">{{ $__t('Chore journal') }}</span> <span class="dropdown-item-text">{{ $__t('Chore journal') }}</span>
</a> </a>
<a class="dropdown-item permission-MASTER_DATA_EDIT" <a class="dropdown-item permission-MASTER_DATA_EDIT" type="button"
type="button"
href="{{ $U('/chore/') }}{{ $curentChoreEntry->chore_id }}"> href="{{ $U('/chore/') }}{{ $curentChoreEntry->chore_id }}">
<span class="dropdown-item-text">{{ $__t('Edit chore') }}</span> <span class="dropdown-item-text">{{ $__t('Edit chore') }}</span>
</a> </a>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a class="dropdown-item" <a class="dropdown-item" type="button"
type="button"
href="{{ $U('/chore/' . $curentChoreEntry->chore_id . '/grocycode?download=true') }}"> href="{{ $U('/chore/' . $curentChoreEntry->chore_id . '/grocycode?download=true') }}">
{!! str_replace('grocycode', '<span class="ls-n1">grocycode</span>', $__t('Download %s grocycode', $__t('Chore'))) !!} {!! str_replace('grocycode', '<span class="ls-n1">grocycode</span>', $__t('Download %s grocycode', $__t('Chore'))) !!}
</a> </a>
@if (GROCY_FEATURE_FLAG_LABEL_PRINTER) @if (GROCY_FEATURE_FLAG_LABEL_PRINTER)
<a class="dropdown-item chore-grocycode-label-print" <a class="dropdown-item chore-grocycode-label-print"
data-chore-id="{{ $curentChoreEntry->chore_id }}" data-chore-id="{{ $curentChoreEntry->chore_id }}" type="button" href="#">
type="button"
href="#">
{!! str_replace('grocycode', '<span class="ls-n1">grocycode</span>', $__t('Print %s grocycode on label printer', $__t('Chore'))) !!} {!! str_replace('grocycode', '<span class="ls-n1">grocycode</span>', $__t('Print %s grocycode on label printer', $__t('Chore'))) !!}
</a> </a>
@endif @endif
</div> </div>
</div> </div>
</td> </td>
<td class="chore-name-cell cursor-link" <td class="chore-name-cell cursor-link" data-chore-id="{{ $curentChoreEntry->chore_id }}">
data-chore-id="{{ $curentChoreEntry->chore_id }}">
{{ FindObjectInArrayByPropertyValue($chores, 'id', $curentChoreEntry->chore_id)->name }} {{ FindObjectInArrayByPropertyValue($chores, 'id', $curentChoreEntry->chore_id)->name }}
</td> </td>
<td> <td>
@if (FindObjectInArrayByPropertyValue($chores, 'id', $curentChoreEntry->chore_id)->period_type !== \Grocy\Services\ChoresService::CHORE_PERIOD_TYPE_MANUALLY) @if (FindObjectInArrayByPropertyValue($chores, 'id', $curentChoreEntry->chore_id)->period_type !== \Grocy\Services\ChoresService::CHORE_PERIOD_TYPE_MANUALLY)
<span id="chore-{{ $curentChoreEntry->chore_id }}-next-execution-time">{{ $curentChoreEntry->next_estimated_execution_time }}</span> <span
id="chore-{{ $curentChoreEntry->chore_id }}-next-execution-time">{{ $curentChoreEntry->next_estimated_execution_time }}</span>
<time id="chore-{{ $curentChoreEntry->chore_id }}-next-execution-time-timeago" <time id="chore-{{ $curentChoreEntry->chore_id }}-next-execution-time-timeago"
class="timeago timeago-contextual @if ($curentChoreEntry->track_date_only == 1) timeago-date-only @endif" class="timeago timeago-contextual @if ($curentChoreEntry->track_date_only == 1) timeago-date-only @endif"
datetime="{{ $curentChoreEntry->next_estimated_execution_time }}"></time> datetime="{{ $curentChoreEntry->next_estimated_execution_time }}"></time>
@ -213,7 +183,8 @@
@endif @endif
</td> </td>
<td> <td>
<span id="chore-{{ $curentChoreEntry->chore_id }}-last-tracked-time">{{ $curentChoreEntry->last_tracked_time }}</span> <span
id="chore-{{ $curentChoreEntry->chore_id }}-last-tracked-time">{{ $curentChoreEntry->last_tracked_time }}</span>
<time id="chore-{{ $curentChoreEntry->chore_id }}-last-tracked-time-timeago" <time id="chore-{{ $curentChoreEntry->chore_id }}-last-tracked-time-timeago"
class="timeago timeago-contextual @if ($curentChoreEntry->track_date_only == 1) timeago-date-only @endif" class="timeago timeago-contextual @if ($curentChoreEntry->track_date_only == 1) timeago-date-only @endif"
datetime="{{ $curentChoreEntry->last_tracked_time }}"></time> datetime="{{ $curentChoreEntry->last_tracked_time }}"></time>
@ -228,8 +199,7 @@
@endif @endif
</span> </span>
</td> </td>
<td id="chore-{{ $curentChoreEntry->chore_id }}-due-filter-column" <td id="chore-{{ $curentChoreEntry->chore_id }}-due-filter-column" class="d-none">
class="d-none">
{{ $curentChoreEntry->due_type }} {{ $curentChoreEntry->due_type }}
@if ($curentChoreEntry->due_type == 'duetoday') @if ($curentChoreEntry->due_type == 'duetoday')
duesoon duesoon
@ -241,10 +211,14 @@
</td> </td>
@endif @endif
@include('components.userfields_tbody', array( @include('components.userfields_tbody', [
'userfields' => $userfields, 'userfields' => $userfields,
'userfieldValues' => FindAllObjectsInArrayByPropertyValue($userfieldValues, 'object_id', $curentChoreEntry->chore_id) 'userfieldValues' => FindAllObjectsInArrayByPropertyValue(
)) $userfieldValues,
'object_id',
$curentChoreEntry->chore_id
),
])
</tr> </tr>
@endforeach @endforeach
@ -253,18 +227,14 @@
</div> </div>
</div> </div>
<div class="modal fade" <div class="modal fade" id="choresoverview-chorecard-modal" tabindex="-1">
id="choresoverview-chorecard-modal"
tabindex="-1">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content text-center"> <div class="modal-content text-center">
<div class="modal-body"> <div class="modal-body">
@include('components.chorecard') @include('components.chorecard')
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" <button type="button" class="btn btn-secondary" data-dismiss="modal">{{ $__t('Close') }}</button>
class="btn btn-secondary"
data-dismiss="modal">{{ $__t('Close') }}</button>
</div> </div>
</div> </div>
</div> </div>

View File

@ -11,21 +11,15 @@
<hr class="my-2"> <hr class="my-2">
<form id="choretracking-form" <form id="choretracking-form" novalidate>
novalidate>
<div class="form-group"> <div class="form-group">
<label class="w-100" <label class="w-100" for="chore_id">
for="chore_id">
{{ $__t('Chore') }} {{ $__t('Chore') }}
<i id="barcode-lookup-hint" <i id="barcode-lookup-hint" class="fas fa-barcode float-right mt-1"></i>
class="fas fa-barcode float-right mt-1"></i>
</label> </label>
<select class="form-control combobox barcodescanner-input" {{-- TODO: Select2: dynamic data: chores --}}
id="chore_id" <select class="form-control combobox barcodescanner-input" id="chore_id" name="chore_id" required data-target="@chorepicker">
name="chore_id"
required
data-target="@chorepicker">
<option value=""></option> <option value=""></option>
@foreach($chores as $chore) @foreach($chores as $chore)
<option value="{{ $chore->id }}">{{ $chore->name }}</option> <option value="{{ $chore->id }}">{{ $chore->name }}</option>
@ -52,10 +46,7 @@
'prefillByUserId' => GROCY_USER_ID 'prefillByUserId' => GROCY_USER_ID
)) ))
@else @else
<input type="hidden" <input type="hidden" id="user_id" name="user_id" value="{{ GROCY_USER_ID }}">
id="user_id"
name="user_id"
value="{{ GROCY_USER_ID }}">
@endif @endif
@include('components.userfieldsform', array( @include('components.userfieldsform', array(

View File

@ -26,6 +26,17 @@
margin-right: 36px !important; margin-right: 36px !important;
} }
.input-group-prepend #barcodescanner-start-button {
position: static;
right: unset;
margin: unset;
}
.input-group>#barcodescanner-start-button-container+.select2-hidden-accessible+.select2-container--bootstrap>.selection>.select2-selection, .input-group>#barcodescanner-start-button-container+.select2-hidden-accessible+.select2-container--bootstrap>.selection>.select2-selection.form-control {
border-top-right-radius: .25rem;
border-bottom-right-radius: .25rem;
}
</style> </style>
@endpush @endpush

View File

@ -4,30 +4,43 @@
@endpush @endpush
@endonce @endonce
@php if(empty($prefillByName)) { $prefillByName = ''; } @endphp @php
@php if(empty($prefillById)) { $prefillById = ''; } @endphp if (empty($prefillByName)) {
@php if(!isset($isRequired)) { $isRequired = true; } @endphp $prefillByName = '';
@php if(empty($hint)) { $hint = ''; } @endphp }
@php if(empty($nextInputSelector)) { $nextInputSelector = ''; } @endphp @endphp
@php
if (empty($prefillById)) {
$prefillById = '';
}
@endphp
@php
if (!isset($isRequired)) {
$isRequired = true;
}
@endphp
@php
if (empty($hint)) {
$hint = '';
}
@endphp
@php
if (empty($nextInputSelector)) {
$nextInputSelector = '';
}
@endphp
<div class="form-group" <div class="form-group" data-next-input-selector="{{ $nextInputSelector }}"
data-next-input-selector="{{ $nextInputSelector }}" data-prefill-by-name="{{ $prefillByName }}" data-prefill-by-id="{{ $prefillById }}">
data-prefill-by-name="{{ $prefillByName }}"
data-prefill-by-id="{{ $prefillById }}">
<label for="location_id">{{ $__t('Location') }} <label for="location_id">{{ $__t('Location') }}
@if (!empty($hint)) @if (!empty($hint))
<i class="fas fa-question-circle text-muted" <i class="fas fa-question-circle text-muted" data-toggle="tooltip" data-trigger="hover click"
data-toggle="tooltip"
data-trigger="hover click"
title="{{ $hint }}"></i> title="{{ $hint }}"></i>
@endif @endif
</label> </label>
<select class="form-control location-combobox" {{-- TODO: Select2: dynamic data: locations --}}
id="location_id" <select class="form-control location-combobox" id="location_id" name="location_id"
name="location_id" @if ($isRequired) required @endif>
@if($isRequired)
required
@endif>
<option value=""></option> <option value=""></option>
@foreach ($locations as $location) @foreach ($locations as $location)
<option value="{{ $location->id }}">{{ $location->name }}</option> <option value="{{ $location->id }}">{{ $location->name }}</option>

View File

@ -4,69 +4,71 @@
@endpush @endpush
@endonce @endonce
@php if(empty($disallowAddProductWorkflows)) { $disallowAddProductWorkflows = false; } @endphp @php
@php if(empty($disallowAllProductWorkflows)) { $disallowAllProductWorkflows = false; } @endphp if (empty($disallowAddProductWorkflows)) {
@php if(empty($prefillByName)) { $prefillByName = ''; } @endphp $disallowAddProductWorkflows = false;
@php if(empty($prefillById)) { $prefillById = ''; } @endphp }
@php if(!isset($isRequired)) { $isRequired = true; } @endphp if (empty($disallowAllProductWorkflows)) {
@php if(!isset($label)) { $label = 'Product'; } @endphp $disallowAllProductWorkflows = false;
@php if(!isset($disabled)) { $disabled = false; } @endphp }
@php if(empty($hint)) { $hint = ''; } @endphp if (empty($prefillByName)) {
@php if(empty($nextInputSelector)) { $nextInputSelector = ''; } @endphp $prefillByName = '';
@php if(empty($validationMessage)) { $validationMessage = 'You have to select a product'; } @endphp }
if (empty($prefillById)) {
$prefillById = '';
}
if (!isset($isRequired)) {
$isRequired = true;
}
if (!isset($label)) {
$label = 'Product';
}
if (!isset($disabled)) {
$disabled = false;
}
if (empty($hint)) {
$hint = '';
}
if (empty($nextInputSelector)) {
$nextInputSelector = '';
}
if (empty($validationMessage)) {
$validationMessage = 'You have to select a product';
}
if (empty($productsQuery)) {
$productsQuery = '';
}
@endphp
<div class="form-group" <div class="form-group" data-next-input-selector="{{ $nextInputSelector }}"
data-next-input-selector="{{ $nextInputSelector }}"
data-disallow-add-product-workflows="{{ BoolToString($disallowAddProductWorkflows) }}" data-disallow-add-product-workflows="{{ BoolToString($disallowAddProductWorkflows) }}"
data-disallow-all-product-workflows="{{ BoolToString($disallowAllProductWorkflows) }}" data-disallow-all-product-workflows="{{ BoolToString($disallowAllProductWorkflows) }}"
data-prefill-by-name="{{ $prefillByName }}" data-prefill-by-name="{{ $prefillByName }}" data-prefill-by-id="{{ $prefillById }}"
data-prefill-by-id="{{ $prefillById }}"> data-products-query="{{ $productsQuery }}">
<label class="w-100" <label class="w-100" for="product_id">
for="product_id">
{{ $__t($label) }} {{ $__t($label) }}
@if (!$disallowAllProductWorkflows) @if (!$disallowAllProductWorkflows)
<i class="fas fa-question-circle text-muted" <i class="fas fa-question-circle text-muted" data-toggle="tooltip" data-trigger="hover click"
data-toggle="tooltip"
data-trigger="hover click"
title="{{ $__t('Type a new product name or barcode and hit TAB or ENTER to start a workflow') }}"></i> title="{{ $__t('Type a new product name or barcode and hit TAB or ENTER to start a workflow') }}"></i>
@endif @endif
@if (!empty($hint)) @if (!empty($hint))
<i class="fas fa-question-circle text-muted" <i class="fas fa-question-circle text-muted" data-toggle="tooltip" data-trigger="hover click"
data-toggle="tooltip"
data-trigger="hover click"
title="{{ $hint }}"></i> title="{{ $hint }}"></i>
@endif @endif
<span id="barcode-lookup-disabled-hint" <span id="barcode-lookup-disabled-hint" class="small text-muted d-none float-right">
class="small text-muted d-none float-right"> {{ $__t('Barcode lookup is disabled') }}</span> {{ $__t('Barcode lookup is disabled') }}</span>
<i id="barcode-lookup-hint" <i id="barcode-lookup-hint" class="fas fa-barcode float-right mt-1"></i>
class="fas fa-barcode float-right mt-1"></i>
</label> </label>
<select class="form-control product-combobox barcodescanner-input" <div class="input-group">
id="product_id" <select class="select2 custom-control custom-select barcodescanner-input" id="product_id" name="product_id"
name="product_id" @if ($isRequired) required @endif @if ($disabled) disabled @endif
@if($isRequired) data-target="@productpicker"></select>
required
@endif
@if($disabled)
disabled
@endif
data-target="@productpicker">
<option value=""></option>
@foreach($products as $product)
@php $bc = null;
if(isset($barcodes)) {
$bc = FindObjectInArrayByPropertyValue($barcodes, 'product_id', $product->id);
}
@endphp
<option data-additional-searchdata="@if(isset($bc)){{ $bc->barcodes }}@endif,"
value="{{ $product->id }}">{{ $product->name }}</option>
@endforeach
</select>
<div class="invalid-feedback">{{ $__t($validationMessage) }}</div> <div class="invalid-feedback">{{ $__t($validationMessage) }}</div>
<div id="custom-productpicker-error" </div>
class="form-text text-danger d-none"></div> <div id="custom-productpicker-error" class="form-text text-danger d-none"></div>
<div id="flow-info-InplaceAddBarcodeToExistingProduct" <div id="flow-info-InplaceAddBarcodeToExistingProduct" class="form-text text-info small d-none"><strong><span
class="form-text text-info small d-none"><strong><span id="InplaceAddBarcodeToExistingProduct"></span></strong> {{ $__t('will be added to the list of barcodes for the selected product on submit') }}</div> id="InplaceAddBarcodeToExistingProduct"></span></strong>
{{ $__t('will be added to the list of barcodes for the selected product on submit') }}</div>
</div> </div>
@include('components.barcodescanner') @include('components.barcodescanner')

View File

@ -4,33 +4,44 @@
@endpush @endpush
@endonce @endonce
@php if(empty($prefillByName)) { $prefillByName = ''; } @endphp @php
@php if(empty($prefillById)) { $prefillById = ''; } @endphp if (empty($prefillByName)) {
@php if(!isset($isRequired)) { $isRequired = true; } @endphp $prefillByName = '';
@php if(empty($hint)) { $hint = ''; } @endphp }
@php if(empty($nextInputSelector)) { $nextInputSelector = ''; } @endphp @endphp
@php
if (empty($prefillById)) {
$prefillById = '';
}
@endphp
@php
if (!isset($isRequired)) {
$isRequired = true;
}
@endphp
@php
if (empty($hint)) {
$hint = '';
}
@endphp
@php
if (empty($nextInputSelector)) {
$nextInputSelector = '';
}
@endphp
<div class="form-group" <div class="form-group" data-next-input-selector="{{ $nextInputSelector }}"
data-next-input-selector="{{ $nextInputSelector }}" data-prefill-by-name="{{ $prefillByName }}" data-prefill-by-id="{{ $prefillById }}">
data-prefill-by-name="{{ $prefillByName }}" <label class="w-100" for="recipe_id">{{ $__t('Recipe') }}
data-prefill-by-id="{{ $prefillById }}">
<label class="w-100"
for="recipe_id">{{ $__t('Recipe') }}
@if (!empty($hint)) @if (!empty($hint))
<i class="fas fa-question-circle text-muted" <i class="fas fa-question-circle text-muted" data-toggle="tooltip" data-trigger="hover click"
data-toggle="tooltip"
data-trigger="hover click"
title="{{ $hint }}"></i> title="{{ $hint }}"></i>
@endif @endif
<i class="fas fa-barcode float-right mt-1"></i> <i class="fas fa-barcode float-right mt-1"></i>
</label> </label>
<select class="form-control recipe-combobox barcodescanner-input" {{-- TODO: Select2: dynamic data: recipes --}}
id="recipe_id" <select class="form-control recipe-combobox barcodescanner-input" id="recipe_id" name="recipe_id"
name="recipe_id" data-target="@recipepicker" @if ($isRequired) required @endif>
data-target="@recipepicker"
@if($isRequired)
required
@endif>
<option value=""></option> <option value=""></option>
@foreach ($recipes as $recipe) @foreach ($recipes as $recipe)
<option value="{{ $recipe->id }}">{{ $recipe->name }}</option> <option value="{{ $recipe->id }}">{{ $recipe->name }}</option>

View File

@ -4,25 +4,40 @@
@endpush @endpush
@endonce @endonce
@php if(empty($prefillByName)) { $prefillByName = ''; } @endphp @php
@php if(empty($prefillById)) { $prefillById = ''; } @endphp if (empty($prefillByName)) {
@php if(!isset($isRequired)) { $isRequired = false; } @endphp $prefillByName = '';
@php if(empty($hint)) { $hint = ''; } @endphp }
@php if(empty($nextInputSelector)) { $nextInputSelector = ''; } @endphp @endphp
@php
if (empty($prefillById)) {
$prefillById = '';
}
@endphp
@php
if (!isset($isRequired)) {
$isRequired = false;
}
@endphp
@php
if (empty($hint)) {
$hint = '';
}
@endphp
@php
if (empty($nextInputSelector)) {
$nextInputSelector = '';
}
@endphp
<div class="form-group" <div class="form-group" data-next-input-selector="{{ $nextInputSelector }}"
data-next-input-selector="{{ $nextInputSelector }}" data-prefill-by-name="{{ $prefillByName }}" data-prefill-by-id="{{ $prefillById }}">
data-prefill-by-name="{{ $prefillByName }}" <label for="shopping_location_id">{{ $__t($label) }}&nbsp;&nbsp;<span
data-prefill-by-id="{{ $prefillById }}"> @if (!empty($hintId)) id="{{ $hintId }}" @endif
<label for="shopping_location_id">{{ $__t($label) }}&nbsp;&nbsp;<span @if(!empty($hintId))id="{{ $hintId }}"
@endif
class="small text-muted">{{ $hint }}</span></label> class="small text-muted">{{ $hint }}</span></label>
<select class="form-control shopping-location-combobox" {{-- TODO: Select2: dynamic data: shopping_locations --}}
id="shopping_location_id" <select class="form-control shopping-location-combobox" id="shopping_location_id" name="shopping_location_id"
name="shopping_location_id" @if ($isRequired) required @endif>
@if($isRequired)
required
@endif>
<option value=""></option> <option value=""></option>
@foreach ($shoppinglocations as $shoppinglocation) @foreach ($shoppinglocations as $shoppinglocation)
<option value="{{ $shoppinglocation->id }}">{{ $shoppinglocation->name }}</option> <option value="{{ $shoppinglocation->id }}">{{ $shoppinglocation->name }}</option>

View File

@ -4,22 +4,31 @@
@endpush @endpush
@endonce @endonce
@php if(empty($prefillByUsername)) { $prefillByUsername = ''; } @endphp @php
@php if(empty($prefillByUserId)) { $prefillByUserId = ''; } @endphp if (empty($prefillByUsername)) {
@php if(!isset($nextInputSelector)) { $nextInputSelector = ''; } @endphp $prefillByUsername = '';
}
@endphp
@php
if (empty($prefillByUserId)) {
$prefillByUserId = '';
}
@endphp
@php
if (!isset($nextInputSelector)) {
$nextInputSelector = '';
}
@endphp
<div class="form-group" <div class="form-group" data-next-input-selector="{{ $nextInputSelector }}"
data-next-input-selector="{{ $nextInputSelector }}" data-prefill-by-username="{{ $prefillByUsername }}" data-prefill-by-user-id="{{ $prefillByUserId }}">
data-prefill-by-username="{{ $prefillByUsername }}"
data-prefill-by-user-id="{{ $prefillByUserId }}">
<label for="user_id">{{ $__t($label) }}</label> <label for="user_id">{{ $__t($label) }}</label>
<select class="form-control user-combobox" {{-- TODO: Select2: dynamic data: users --}}
id="user_id" <select class="form-control user-combobox" id="user_id" name="user_id">
name="user_id">
<option value=""></option> <option value=""></option>
@foreach ($users as $user) @foreach ($users as $user)
<option data-additional-searchdata="{{ $user->username }}" <option data-additional-searchdata="{{ $user->username }}" value="{{ $user->id }}">
value="{{ $user->id }}">{{ GetUserDisplayName($user) }}</option> {{ GetUserDisplayName($user) }}</option>
@endforeach @endforeach
</select> </select>
</div> </div>

View File

@ -20,25 +20,24 @@
<div class="title-related-links"> <div class="title-related-links">
<h2 class="title">@yield('title')</h2> <h2 class="title">@yield('title')</h2>
<button class="btn btn-outline-dark d-md-none mt-2 float-right order-1 order-md-3 hide-when-embedded" <button class="btn btn-outline-dark d-md-none mt-2 float-right order-1 order-md-3 hide-when-embedded"
type="button" type="button" data-toggle="collapse" data-target="#related-links">
data-toggle="collapse"
data-target="#related-links">
<i class="fas fa-ellipsis-v"></i> <i class="fas fa-ellipsis-v"></i>
</button> </button>
<div class="related-links collapse d-md-flex order-2 width-xs-sm-100" <div class="related-links collapse d-md-flex order-2 width-xs-sm-100" id="related-links">
id="related-links">
@if (!$embedded) @if (!$embedded)
<button id="scan-mode-button" <button id="scan-mode-button"
class="btn @if (boolval($userSettings['scan_mode_consume_enabled'])) btn-success @else btn-danger @endif m-1 mt-md-0 mb-md-0 float-right" class="btn @if (boolval($userSettings['scan_mode_consume_enabled'])) btn-success @else btn-danger @endif m-1 mt-md-0 mb-md-0 float-right"
data-toggle="tooltip" data-toggle="tooltip"
title="{{ $__t('When enabled, after changing/scanning a product and if all fields could be automatically populated (by product and/or barcode defaults), the transaction is automatically submitted') }}">{{ $__t('Scan mode') }} <span id="scan-mode-status">@if(boolval($userSettings['scan_mode_consume_enabled'])) {{ $__t('on') }} @else {{ $__t('off') }} @endif</span></button> title="{{ $__t('When enabled, after changing/scanning a product and if all fields could be automatically populated (by product and/or barcode defaults), the transaction is automatically submitted') }}">{{ $__t('Scan mode') }}
<input id="scan-mode" <span id="scan-mode-status">
type="checkbox"
class="d-none user-setting-control"
data-setting-key="scan_mode_consume_enabled"
@if (boolval($userSettings['scan_mode_consume_enabled'])) @if (boolval($userSettings['scan_mode_consume_enabled']))
checked {{ $__t('on') }}
@endif> @else
{{ $__t('off') }}
@endif
</span></button>
<input id="scan-mode" type="checkbox" class="d-none user-setting-control"
data-setting-key="scan_mode_consume_enabled" @if (boolval($userSettings['scan_mode_consume_enabled'])) checked @endif>
@else @else
<script> <script>
Grocy.UserSettings.scan_mode_consume_enabled = false; Grocy.UserSettings.scan_mode_consume_enabled = false;
@ -49,41 +48,38 @@
<hr class="my-2"> <hr class="my-2">
<form id="consume-form" <form id="consume-form" novalidate>
novalidate>
@include('components.productpicker', array( @include('components.productpicker', [
'products' => $products, 'productsQuery' => 'query%5B%5D=active%3D1&only_in_stock=1&order=name',
'barcodes' => $barcodes,
'nextInputSelector' => '#amount', 'nextInputSelector' => '#amount',
'disallowAddProductWorkflows' => true 'disallowAddProductWorkflows' => true,
)) ])
<div id="consume-exact-amount-group" <div id="consume-exact-amount-group" class="form-group d-none">
class="form-group d-none">
<div class="custom-control custom-checkbox"> <div class="custom-control custom-checkbox">
<input class="form-check-input custom-control-input" <input class="form-check-input custom-control-input" type="checkbox" id="consume-exact-amount"
type="checkbox" name="consume-exact-amount" value="1">
id="consume-exact-amount"
name="consume-exact-amount"
value="1">
<label class="form-check-label custom-control-label" <label class="form-check-label custom-control-label"
for="consume-exact-amount">{{ $__t('Consume exact amount') }} for="consume-exact-amount">{{ $__t('Consume exact amount') }}
</label> </label>
</div> </div>
</div> </div>
@include('components.productamountpicker', array( @include('components.productamountpicker', [
'value' => 1, 'value' => 1,
'additionalHtmlContextHelp' => '<div id="tare-weight-handling-info" 'additionalHtmlContextHelp' =>
class="text-info font-italic d-none">' . $__t('Tare weight handling enabled - please weigh the whole container, the amount to be posted will be automatically calculcated') . '</div>' '<div id="tare-weight-handling-info" class="text-info font-italic d-none">' .
)) $__t(
'Tare weight handling enabled - please weigh the whole container, the amount to be posted will be automatically calculcated'
) .
'</div>',
])
<div class="form-group @if (!GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING) d-none @endif"> <div class="form-group @if (!GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING) d-none @endif">
<label for="location_id">{{ $__t('Location') }}</label> <label for="location_id">{{ $__t('Location') }}</label>
<select required {{-- TODO: Select2: dynamic data: locations --}}
class="custom-control custom-select location-combobox" <select required class="custom-control custom-select location-combobox" id="location_id"
id="location_id"
name="location_id"> name="location_id">
<option></option> <option></option>
@foreach ($locations as $location) @foreach ($locations as $location)
@ -95,50 +91,39 @@
<div class="form-group"> <div class="form-group">
<div class="custom-control custom-checkbox"> <div class="custom-control custom-checkbox">
<input class="form-check-input custom-control-input" <input class="form-check-input custom-control-input" type="checkbox" id="spoiled" name="spoiled"
type="checkbox"
id="spoiled"
name="spoiled"
value="1"> value="1">
<label class="form-check-label custom-control-label" <label class="form-check-label custom-control-label" for="spoiled">{{ $__t('Spoiled') }}
for="spoiled">{{ $__t('Spoiled') }}
</label> </label>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="custom-control custom-checkbox"> <div class="custom-control custom-checkbox">
<input class="form-check-input custom-control-input" <input class="form-check-input custom-control-input" type="checkbox" id="use_specific_stock_entry"
type="checkbox" name="use_specific_stock_entry" value="1">
id="use_specific_stock_entry"
name="use_specific_stock_entry"
value="1">
<label class="form-check-label custom-control-label" <label class="form-check-label custom-control-label"
for="use_specific_stock_entry">{{ $__t('Use a specific stock item') }} for="use_specific_stock_entry">{{ $__t('Use a specific stock item') }}
&nbsp;<i class="fas fa-question-circle text-muted" &nbsp;<i class="fas fa-question-circle text-muted" data-toggle="tooltip"
data-toggle="tooltip"
data-trigger="hover click" data-trigger="hover click"
title="{{ $__t('The first item in this list would be picked by the default rule which is "Opened first, then first due first, then first in first out"') }}"></i> title="{{ $__t('The first item in this list would be picked by the default rule which is "Opened first, then first due first, then first in first out"') }}"></i>
</label> </label>
</div> </div>
<select disabled <select disabled class="custom-control custom-select mt-2" id="specific_stock_entry"
class="custom-control custom-select mt-2"
id="specific_stock_entry"
name="specific_stock_entry"> name="specific_stock_entry">
<option></option> <option></option>
</select> </select>
</div> </div>
@if (GROCY_FEATURE_FLAG_RECIPES) @if (GROCY_FEATURE_FLAG_RECIPES)
@include('components.recipepicker', array( @include('components.recipepicker', [
'recipes' => $recipes, 'recipes' => $recipes,
'isRequired' => false, 'isRequired' => false,
'hint' => $__t('This is for statistical purposes only') 'hint' => $__t('This is for statistical purposes only'),
)) ])
@endif @endif
<button id="save-consume-button" <button id="save-consume-button" class="btn btn-success">{{ $__t('OK') }}</button>
class="btn btn-success">{{ $__t('OK') }}</button>
@if (GROCY_FEATURE_FLAG_STOCK_PRODUCT_OPENED_TRACKING) @if (GROCY_FEATURE_FLAG_STOCK_PRODUCT_OPENED_TRACKING)
<button id="save-mark-as-open-button" <button id="save-mark-as-open-button"

View File

@ -10,21 +10,16 @@
<div class="title-related-links border-bottom mb-2 py-1"> <div class="title-related-links border-bottom mb-2 py-1">
<h2 class="title">@yield('title')</h2> <h2 class="title">@yield('title')</h2>
<div class="float-right"> <div class="float-right">
<button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" <button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" type="button"
type="button" data-toggle="collapse" data-target="#table-filter-row">
data-toggle="collapse"
data-target="#table-filter-row">
<i class="fas fa-filter"></i> <i class="fas fa-filter"></i>
</button> </button>
<button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" <button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" type="button"
type="button" data-toggle="collapse" data-target="#related-links">
data-toggle="collapse"
data-target="#related-links">
<i class="fas fa-ellipsis-v"></i> <i class="fas fa-ellipsis-v"></i>
</button> </button>
</div> </div>
<div class="related-links collapse d-md-flex order-2 width-xs-sm-100" <div class="related-links collapse d-md-flex order-2 width-xs-sm-100" id="related-links">
id="related-links">
<a class="btn btn-primary responsive-button m-1 mt-md-0 mb-md-0 float-right" <a class="btn btn-primary responsive-button m-1 mt-md-0 mb-md-0 float-right"
href="{{ $U('/equipment/new') }}"> href="{{ $U('/equipment/new') }}">
{{ $__t('Add') }} {{ $__t('Add') }}
@ -32,46 +27,37 @@
</div> </div>
</div> </div>
<div class="row collapse d-md-flex" <div class="row collapse d-md-flex" id="table-filter-row">
id="table-filter-row">
<div class="col"> <div class="col">
<div class="input-group"> <div class="input-group">
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-search"></i></span> <span class="input-group-text"><i class="fas fa-search"></i></span>
</div> </div>
<input type="text" <input type="text" id="search" class="form-control" placeholder="{{ $__t('Search') }}">
id="search"
class="form-control"
placeholder="{{ $__t('Search') }}">
</div> </div>
</div> </div>
<div class="col"> <div class="col">
<div class="float-right"> <div class="float-right">
<a id="clear-filter-button" <a id="clear-filter-button" class="btn btn-sm btn-outline-info" href="#">
class="btn btn-sm btn-outline-info"
href="#">
{{ $__t('Clear filter') }} {{ $__t('Clear filter') }}
</a> </a>
</div> </div>
</div> </div>
</div> </div>
<table id="equipment-table" {{-- TODO: DataTables: dynamic data: equipment --}}
class="table table-sm table-striped nowrap w-100"> <table id="equipment-table" class="table table-sm table-striped nowrap w-100">
<thead> <thead>
<tr> <tr>
<th class="border-right"><a class="text-muted change-table-columns-visibility-button" <th class="border-right"><a class="text-muted change-table-columns-visibility-button"
data-toggle="tooltip" data-toggle="tooltip" data-toggle="tooltip" title="{{ $__t('Table options') }}"
data-toggle="tooltip" data-table-selector="#equipment-table" href="#"><i class="fas fa-eye"></i></a>
title="{{ $__t('Table options') }}"
data-table-selector="#equipment-table"
href="#"><i class="fas fa-eye"></i></a>
</th> </th>
<th>{{ $__t('Name') }}</th> <th>{{ $__t('Name') }}</th>
@include('components.userfields_thead', array( @include('components.userfields_thead', [
'userfields' => $userfields 'userfields' => $userfields,
)) ])
</tr> </tr>
</thead> </thead>
@ -80,21 +66,18 @@
<tr data-equipment-id="{{ $equipmentItem->id }}"> <tr data-equipment-id="{{ $equipmentItem->id }}">
<td class="fit-content border-right"> <td class="fit-content border-right">
<a class="btn btn-info btn-sm hide-when-embedded hide-on-fullscreen-card" <a class="btn btn-info btn-sm hide-when-embedded hide-on-fullscreen-card"
href="{{ $U('/equipment/') }}{{ $equipmentItem->id }}" href="{{ $U('/equipment/') }}{{ $equipmentItem->id }}" data-toggle="tooltip"
data-toggle="tooltip"
title="{{ $__t('Edit this item') }}"> title="{{ $__t('Edit this item') }}">
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
</a> </a>
<div class="dropdown d-inline-block"> <div class="dropdown d-inline-block">
<button class="btn btn-sm btn-light text-secondary" <button class="btn btn-sm btn-light text-secondary" type="button"
type="button"
data-toggle="dropdown"> data-toggle="dropdown">
<i class="fas fa-ellipsis-v"></i> <i class="fas fa-ellipsis-v"></i>
</button> </button>
<div class="table-inline-menu dropdown-menu dropdown-menu-right hide-on-fullscreen-card hide-when-embedded"> <div
<a class="dropdown-item equipment-delete-button" class="table-inline-menu dropdown-menu dropdown-menu-right hide-on-fullscreen-card hide-when-embedded">
type="button" <a class="dropdown-item equipment-delete-button" type="button" href="#"
href="#"
data-equipment-id="{{ $equipmentItem->id }}" data-equipment-id="{{ $equipmentItem->id }}"
data-equipment-name="{{ $equipmentItem->name }}"> data-equipment-name="{{ $equipmentItem->name }}">
<span class="dropdown-item-text">{{ $__t('Delete this item') }}</span> <span class="dropdown-item-text">{{ $__t('Delete this item') }}</span>
@ -106,10 +89,14 @@
{{ $equipmentItem->name }} {{ $equipmentItem->name }}
</td> </td>
@include('components.userfields_tbody', array( @include('components.userfields_tbody', [
'userfields' => $userfields, 'userfields' => $userfields,
'userfieldValues' => FindAllObjectsInArrayByPropertyValue($userfieldValues, 'object_id', $equipmentItem->id) 'userfieldValues' => FindAllObjectsInArrayByPropertyValue(
)) $userfieldValues,
'object_id',
$equipmentItem->id
),
])
</tr> </tr>
@endforeach @endforeach
@ -120,66 +107,50 @@
<div class="col-12 col-md-8"> <div class="col-12 col-md-8">
<ul class="nav nav-tabs grocy-tabs"> <ul class="nav nav-tabs grocy-tabs">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link active" <a class="nav-link active" data-toggle="tab"
data-toggle="tab"
href="#instruction-manual-tab">{{ $__t('Instruction manual') }}</a> href="#instruction-manual-tab">{{ $__t('Instruction manual') }}</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" <a class="nav-link" data-toggle="tab" href="#description-tab">{{ $__t('Notes') }}</a>
data-toggle="tab"
href="#description-tab">{{ $__t('Notes') }}</a>
</li> </li>
</ul> </ul>
<div class="tab-content grocy-tabs"> <div class="tab-content grocy-tabs">
<div class="tab-pane fade show active" <div class="tab-pane fade show active" id="instruction-manual-tab">
id="instruction-manual-tab"> <div id="selectedEquipmentInstructionManualCard" class="card">
<div id="selectedEquipmentInstructionManualCard"
class="card">
<div class="card-header card-header-fullscreen"> <div class="card-header card-header-fullscreen">
<span class="selected-equipment-name"></span> <span class="selected-equipment-name"></span>
<a id="selectedEquipmentInstructionManualToggleFullscreenButton" <a id="selectedEquipmentInstructionManualToggleFullscreenButton"
class="btn btn-sm btn-outline-secondary py-0 float-right mr-1" class="btn btn-sm btn-outline-secondary py-0 float-right mr-1" href="#"
href="#" data-toggle="tooltip" title="{{ $__t('Expand to fullscreen') }}">
data-toggle="tooltip"
title="{{ $__t('Expand to fullscreen') }}">
<i class="fas fa-expand-arrows-alt"></i> <i class="fas fa-expand-arrows-alt"></i>
</a> </a>
<a id="selectedEquipmentInstructionManualDownloadButton" <a id="selectedEquipmentInstructionManualDownloadButton"
class="btn btn-sm btn-outline-secondary py-0 float-right mr-1" class="btn btn-sm btn-outline-secondary py-0 float-right mr-1" href="#" target="_blank"
href="#" data-toggle="tooltip" title="{{ $__t('Download file') }}">
target="_blank"
data-toggle="tooltip"
title="{{ $__t('Download file') }}">
<i class="fas fa-file-download"></i> <i class="fas fa-file-download"></i>
</a> </a>
</div> </div>
<div class="card-body py-0 px-0"> <div class="card-body py-0 px-0">
<p id="selected-equipment-has-no-instruction-manual-hint" <p id="selected-equipment-has-no-instruction-manual-hint"
class="text-muted font-italic d-none pt-3 pl-3">{{ $__t('The selected equipment has no instruction manual') }}</p> class="text-muted font-italic d-none pt-3 pl-3">
<embed id="selected-equipment-instruction-manual" {{ $__t('The selected equipment has no instruction manual') }}</p>
class="embed-responsive embed-responsive-4by3" <embed id="selected-equipment-instruction-manual" class="embed-responsive embed-responsive-4by3"
src="" src="" type="application/pdf">
type="application/pdf">
</div> </div>
</div> </div>
</div> </div>
<div class="tab-pane fade" <div class="tab-pane fade" id="description-tab">
id="description-tab"> <div id="selectedEquipmentDescriptionCard" class="card">
<div id="selectedEquipmentDescriptionCard"
class="card">
<div class="card-header card-header-fullscreen"> <div class="card-header card-header-fullscreen">
<span class="selected-equipment-name"></span> <span class="selected-equipment-name"></span>
<a id="selectedEquipmentDescriptionToggleFullscreenButton" <a id="selectedEquipmentDescriptionToggleFullscreenButton"
class="btn btn-sm btn-outline-secondary py-0 float-right" class="btn btn-sm btn-outline-secondary py-0 float-right" href="#" data-toggle="tooltip"
href="#"
data-toggle="tooltip"
title="{{ $__t('Expand to fullscreen') }}"> title="{{ $__t('Expand to fullscreen') }}">
<i class="fas fa-expand-arrows-alt"></i> <i class="fas fa-expand-arrows-alt"></i>
</a> </a>
</div> </div>
<div class="card-body"> <div class="card-body">
<div id="description-tab-content" <div id="description-tab-content" class="mb-0"></div>
class="mb-0"></div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -21,8 +21,7 @@
novalidate> novalidate>
@include('components.productpicker', array( @include('components.productpicker', array(
'products' => $products, 'productsQuery' => 'query%5B%5D=active%3D1&order=name%3Acollate%20nocase',
'barcodes' => $barcodes,
'nextInputSelector' => '#new_amount' 'nextInputSelector' => '#new_amount'
)) ))

View File

@ -73,6 +73,10 @@
rel="stylesheet"> rel="stylesheet">
<link href="{{ $U('/node_modules/@fontsource/noto-sans/latin.css?v=', true) }}{{ $version }}" <link href="{{ $U('/node_modules/@fontsource/noto-sans/latin.css?v=', true) }}{{ $version }}"
rel="stylesheet"> rel="stylesheet">
<link href="{{ $U('/node_modules/select2/dist/css/select2.min.css?v=', true) }}{{ $version }}"
rel="stylesheet">
<link href="{{ $U('/node_modules/select2-theme-bootstrap4/dist/select2-bootstrap.min.css?v=', true) }}{{ $version }}"
rel="stylesheet">
<link href="{{ $U('/css/grocy.css?v=', true) }}{{ $version }}" <link href="{{ $U('/css/grocy.css?v=', true) }}{{ $version }}"
rel="stylesheet"> rel="stylesheet">
<link href="{{ $U('/css/grocy_night_mode.css?v=', true) }}{{ $version }}" <link href="{{ $U('/css/grocy_night_mode.css?v=', true) }}{{ $version }}"
@ -706,6 +710,7 @@
@if(!empty($__t('bootstrap-select_locale') && $__t('bootstrap-select_locale') != 'x'))<script src="{{ $U('/node_modules', true) }}/bootstrap-select/dist/js/i18n/defaults-{{ $__t('bootstrap-select_locale') }}.js?v={{ $version }}"></script>@endif @if(!empty($__t('bootstrap-select_locale') && $__t('bootstrap-select_locale') != 'x'))<script src="{{ $U('/node_modules', true) }}/bootstrap-select/dist/js/i18n/defaults-{{ $__t('bootstrap-select_locale') }}.js?v={{ $version }}"></script>@endif
<script src="{{ $U('/node_modules/jquery-lazy/jquery.lazy.min.js?v=', true) }}{{ $version }}"></script> <script src="{{ $U('/node_modules/jquery-lazy/jquery.lazy.min.js?v=', true) }}{{ $version }}"></script>
<script src="{{ $U('/node_modules/nosleep.js/dist/NoSleep.min.js?v=', true) }}{{ $version }}"></script> <script src="{{ $U('/node_modules/nosleep.js/dist/NoSleep.min.js?v=', true) }}{{ $version }}"></script>
<script src="{{ $U('/node_modules/select2/dist/js/select2.min.js?v=', true) }}{{ $version }}"></script>
<script src="{{ $U('/js/extensions.js?v=', true) }}{{ $version }}"></script> <script src="{{ $U('/js/extensions.js?v=', true) }}{{ $version }}"></script>
<script src="{{ $U('/js/grocy.js?v=', true) }}{{ $version }}"></script> <script src="{{ $U('/js/grocy.js?v=', true) }}{{ $version }}"></script>

View File

@ -31,21 +31,16 @@
<div class="title-related-links d-print-none"> <div class="title-related-links d-print-none">
<h2 class="title"> <h2 class="title">
@yield('title') @yield('title')
<i class="fas fa-question-circle text-muted small" <i class="fas fa-question-circle text-muted small" data-toggle="tooltip" data-trigger="hover click"
data-toggle="tooltip"
data-trigger="hover click"
title="{{ $__t('Here you can print a page per location with the current stock, maybe to hang it there and note the consumed things on it') }}"></i> title="{{ $__t('Here you can print a page per location with the current stock, maybe to hang it there and note the consumed things on it') }}"></i>
</h2> </h2>
<div class="float-right"> <div class="float-right">
<button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" <button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" type="button" data-toggle="collapse"
type="button"
data-toggle="collapse"
data-target="#related-links"> data-target="#related-links">
<i class="fas fa-ellipsis-v"></i> <i class="fas fa-ellipsis-v"></i>
</button> </button>
</div> </div>
<div class="related-links collapse d-md-flex order-2 width-xs-sm-100" <div class="related-links collapse d-md-flex order-2 width-xs-sm-100" id="related-links">
id="related-links">
<a class="btn btn-outline-dark responsive-button m-1 mt-md-0 mb-md-0 float-right print-all-locations-button" <a class="btn btn-outline-dark responsive-button m-1 mt-md-0 mb-md-0 float-right print-all-locations-button"
href="#"> href="#">
{{ $__t('Print') . ' (' . $__t('all locations') . ')' }} {{ $__t('Print') . ' (' . $__t('all locations') . ')' }}
@ -58,12 +53,10 @@
@foreach ($locations as $location) @foreach ($locations as $location)
<div class="page"> <div class="page">
<h1 class="pt-4 text-center"> <h1 class="pt-4 text-center">
<img src="{{ $U('/img/grocy_logo.svg?v=', true) }}{{ $version }}" <img src="{{ $U('/img/grocy_logo.svg?v=', true) }}{{ $version }}" height="30"
height="30"
class="d-none d-print-flex mx-auto"> class="d-none d-print-flex mx-auto">
{{ $location->name }} {{ $location->name }}
<a class="btn btn-outline-dark btn-sm responsive-button print-single-location-button d-print-none" <a class="btn btn-outline-dark btn-sm responsive-button print-single-location-button d-print-none" href="#">
href="#">
{{ $__t('Print') . ' (' . $__t('this location') . ')' }} {{ $__t('Print') . ' (' . $__t('this location') . ')' }}
</a> </a>
</h1> </h1>
@ -73,6 +66,7 @@
</h6> </h6>
<div class="row w-75"> <div class="row w-75">
<div class="col"> <div class="col">
{{-- TODO: DataTables: dynamic data: stock --}}
<table class="table"> <table class="table">
<thead> <thead>
<tr> <tr>
@ -89,8 +83,15 @@
{{ FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name }} {{ FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name }}
</td> </td>
<td> <td>
<span class="locale-number locale-number-quantity-amount">{{ $currentStockEntry->amount }}</span> <span id="product-{{ $currentStockEntry->product_id }}-qu-name">{{ $__n($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, true) }}</span> <span
<span class="small font-italic">@if($currentStockEntry->amount_opened > 0){{ $__t('%s opened', $currentStockEntry->amount_opened) }}@endif</span> class="locale-number locale-number-quantity-amount">{{ $currentStockEntry->amount }}</span>
<span
id="product-{{ $currentStockEntry->product_id }}-qu-name">{{ $__n($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,true) }}</span>
<span class="small font-italic">
@if ($currentStockEntry->amount_opened > 0)
{{ $__t('%s opened', $currentStockEntry->amount_opened) }}
@endif
</span>
</td> </td>
<td class=""></td> <td class=""></td>
</tr> </tr>

View File

@ -10,21 +10,16 @@
<div class="title-related-links"> <div class="title-related-links">
<h2 class="title">@yield('title')</h2> <h2 class="title">@yield('title')</h2>
<div class="float-right"> <div class="float-right">
<button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" <button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" type="button"
type="button" data-toggle="collapse" data-target="#table-filter-row">
data-toggle="collapse"
data-target="#table-filter-row">
<i class="fas fa-filter"></i> <i class="fas fa-filter"></i>
</button> </button>
<button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" <button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" type="button"
type="button" data-toggle="collapse" data-target="#related-links">
data-toggle="collapse"
data-target="#related-links">
<i class="fas fa-ellipsis-v"></i> <i class="fas fa-ellipsis-v"></i>
</button> </button>
</div> </div>
<div class="related-links collapse d-md-flex order-2 width-xs-sm-100" <div class="related-links collapse d-md-flex order-2 width-xs-sm-100" id="related-links">
id="related-links">
<a class="btn btn-primary responsive-button m-1 mt-md-0 mb-md-0 float-right show-as-dialog-link" <a class="btn btn-primary responsive-button m-1 mt-md-0 mb-md-0 float-right show-as-dialog-link"
href="{{ $U('/location/new?embedded') }}"> href="{{ $U('/location/new?embedded') }}">
{{ $__t('Add') }} {{ $__t('Add') }}
@ -40,24 +35,18 @@
<hr class="my-2"> <hr class="my-2">
<div class="row collapse d-md-flex" <div class="row collapse d-md-flex" id="table-filter-row">
id="table-filter-row">
<div class="col-12 col-md-6 col-xl-3"> <div class="col-12 col-md-6 col-xl-3">
<div class="input-group"> <div class="input-group">
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-search"></i></span> <span class="input-group-text"><i class="fas fa-search"></i></span>
</div> </div>
<input type="text" <input type="text" id="search" class="form-control" placeholder="{{ $__t('Search') }}">
id="search"
class="form-control"
placeholder="{{ $__t('Search') }}">
</div> </div>
</div> </div>
<div class="col"> <div class="col">
<div class="float-right"> <div class="float-right">
<a id="clear-filter-button" <a id="clear-filter-button" class="btn btn-sm btn-outline-info" href="#">
class="btn btn-sm btn-outline-info"
href="#">
{{ $__t('Clear filter') }} {{ $__t('Clear filter') }}
</a> </a>
</div> </div>
@ -66,23 +55,20 @@
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<table id="locations-table" {{-- TODO: DataTables: dynamic data: locations --}}
class="table table-sm table-striped nowrap w-100"> <table id="locations-table" class="table table-sm table-striped nowrap w-100">
<thead> <thead>
<tr> <tr>
<th class="border-right"><a class="text-muted change-table-columns-visibility-button" <th class="border-right"><a class="text-muted change-table-columns-visibility-button"
data-toggle="tooltip" data-toggle="tooltip" data-toggle="tooltip" title="{{ $__t('Table options') }}"
data-toggle="tooltip" data-table-selector="#locations-table" href="#"><i class="fas fa-eye"></i></a>
title="{{ $__t('Table options') }}"
data-table-selector="#locations-table"
href="#"><i class="fas fa-eye"></i></a>
</th> </th>
<th>{{ $__t('Name') }}</th> <th>{{ $__t('Name') }}</th>
<th>{{ $__t('Description') }}</th> <th>{{ $__t('Description') }}</th>
@include('components.userfields_thead', array( @include('components.userfields_thead', [
'userfields' => $userfields 'userfields' => $userfields,
)) ])
</tr> </tr>
</thead> </thead>
@ -91,17 +77,13 @@
<tr> <tr>
<td class="fit-content border-right"> <td class="fit-content border-right">
<a class="btn btn-info btn-sm show-as-dialog-link" <a class="btn btn-info btn-sm show-as-dialog-link"
href="{{ $U('/location/') }}{{ $location->id }}?embedded" href="{{ $U('/location/') }}{{ $location->id }}?embedded" data-toggle="tooltip"
data-toggle="tooltip"
title="{{ $__t('Edit this item') }}"> title="{{ $__t('Edit this item') }}">
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
</a> </a>
<a class="btn btn-danger btn-sm location-delete-button" <a class="btn btn-danger btn-sm location-delete-button" href="#"
href="#" data-location-id="{{ $location->id }}" data-location-name="{{ $location->name }}"
data-location-id="{{ $location->id }}" data-toggle="tooltip" title="{{ $__t('Delete this item') }}">
data-location-name="{{ $location->name }}"
data-toggle="tooltip"
title="{{ $__t('Delete this item') }}">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
</a> </a>
</td> </td>
@ -112,10 +94,14 @@
{{ $location->description }} {{ $location->description }}
</td> </td>
@include('components.userfields_tbody', array( @include('components.userfields_tbody', [
'userfields' => $userfields, 'userfields' => $userfields,
'userfieldValues' => FindAllObjectsInArrayByPropertyValue($userfieldValues, 'object_id', $location->id) 'userfieldValues' => FindAllObjectsInArrayByPropertyValue(
)) $userfieldValues,
'object_id',
$location->id
),
])
</tr> </tr>
@endforeach @endforeach

View File

@ -9,8 +9,7 @@
@endpush @endpush
@push('pageStyles') @push('pageStyles')
<link href="{{ $U('/node_modules/animate.css/animate.min.css?v=', true) }}{{ $version }}" <link href="{{ $U('/node_modules/animate.css/animate.min.css?v=', true) }}{{ $version }}" rel="stylesheet">
rel="stylesheet">
@endpush @endpush
@section('content') @section('content')
@ -19,21 +18,16 @@
<div class="title-related-links"> <div class="title-related-links">
<h2 class="title">@yield('title')</h2> <h2 class="title">@yield('title')</h2>
<div class="float-right"> <div class="float-right">
<button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" <button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" type="button"
type="button" data-toggle="collapse" data-target="#table-filter-row">
data-toggle="collapse"
data-target="#table-filter-row">
<i class="fas fa-filter"></i> <i class="fas fa-filter"></i>
</button> </button>
<button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" <button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" type="button"
type="button" data-toggle="collapse" data-target="#related-links">
data-toggle="collapse"
data-target="#related-links">
<i class="fas fa-ellipsis-v"></i> <i class="fas fa-ellipsis-v"></i>
</button> </button>
</div> </div>
<div class="related-links collapse d-md-flex order-2 width-xs-sm-100" <div class="related-links collapse d-md-flex order-2 width-xs-sm-100" id="related-links">
id="related-links">
<a class="btn btn-primary responsive-button m-1 mt-md-0 mb-md-0 float-right" <a class="btn btn-primary responsive-button m-1 mt-md-0 mb-md-0 float-right"
href="{{ $U('/manageapikeys/new') }}"> href="{{ $U('/manageapikeys/new') }}">
{{ $__t('Add') }} {{ $__t('Add') }}
@ -45,24 +39,18 @@
<hr class="my-2"> <hr class="my-2">
<div class="row collapse d-md-flex" <div class="row collapse d-md-flex" id="table-filter-row">
id="table-filter-row">
<div class="col-12 col-md-6 col-xl-3"> <div class="col-12 col-md-6 col-xl-3">
<div class="input-group"> <div class="input-group">
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-search"></i></span> <span class="input-group-text"><i class="fas fa-search"></i></span>
</div> </div>
<input type="text" <input type="text" id="search" class="form-control" placeholder="{{ $__t('Search') }}">
id="search"
class="form-control"
placeholder="{{ $__t('Search') }}">
</div> </div>
</div> </div>
<div class="col"> <div class="col">
<div class="float-right"> <div class="float-right">
<a id="clear-filter-button" <a id="clear-filter-button" class="btn btn-sm btn-outline-info" href="#">
class="btn btn-sm btn-outline-info"
href="#">
{{ $__t('Clear filter') }} {{ $__t('Clear filter') }}
</a> </a>
</div> </div>
@ -71,16 +59,13 @@
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<table id="apikeys-table" {{-- TODO: DataTables: dynamic data: API keys --}}
class="table table-sm table-striped nowrap w-100"> <table id="apikeys-table" class="table table-sm table-striped nowrap w-100">
<thead> <thead>
<tr> <tr>
<th class="border-right"><a class="text-muted change-table-columns-visibility-button" <th class="border-right"><a class="text-muted change-table-columns-visibility-button"
data-toggle="tooltip" data-toggle="tooltip" data-toggle="tooltip" title="{{ $__t('Table options') }}"
data-toggle="tooltip" data-table-selector="#apikeys-table" href="#"><i class="fas fa-eye"></i></a>
title="{{ $__t('Table options') }}"
data-table-selector="#apikeys-table"
href="#"><i class="fas fa-eye"></i></a>
</th> </th>
<th>{{ $__t('API key') }}</th> <th>{{ $__t('API key') }}</th>
<th class="allow-grouping">{{ $__t('User') }}</th> <th class="allow-grouping">{{ $__t('User') }}</th>
@ -94,19 +79,14 @@
@foreach ($apiKeys as $apiKey) @foreach ($apiKeys as $apiKey)
<tr id="apiKeyRow_{{ $apiKey->id }}"> <tr id="apiKeyRow_{{ $apiKey->id }}">
<td class="fit-content border-right"> <td class="fit-content border-right">
<a class="btn btn-danger btn-sm apikey-delete-button" <a class="btn btn-danger btn-sm apikey-delete-button" href="#"
href="#" data-apikey-id="{{ $apiKey->id }}" data-apikey-apikey="{{ $apiKey->api_key }}"
data-apikey-id="{{ $apiKey->id }}" data-toggle="tooltip" title="{{ $__t('Delete this item') }}">
data-apikey-apikey="{{ $apiKey->api_key }}"
data-toggle="tooltip"
title="{{ $__t('Delete this item') }}">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
</a> </a>
<a class="btn btn-info btn-sm apikey-show-qr-button" <a class="btn btn-info btn-sm apikey-show-qr-button" href="#"
href="#"
data-apikey-key="{{ $apiKey->api_key }}" data-apikey-key="{{ $apiKey->api_key }}"
data-apikey-type="{{ $apiKey->key_type }}" data-apikey-type="{{ $apiKey->key_type }}" data-toggle="tooltip"
data-toggle="tooltip"
title="{{ $__t('Show a QR-Code for this API key') }}"> title="{{ $__t('Show a QR-Code for this API key') }}">
<i class="fas fa-qrcode"></i> <i class="fas fa-qrcode"></i>
</a> </a>
@ -119,13 +99,13 @@
</td> </td>
<td> <td>
{{ $apiKey->expires }} {{ $apiKey->expires }}
<time class="timeago timeago-contextual" <time class="timeago timeago-contextual" datetime="{{ $apiKey->expires }}"></time>
datetime="{{ $apiKey->expires }}"></time>
</td> </td>
<td> <td>
@if(empty($apiKey->last_used)){{ $__t('never') }}@else{{ $apiKey->last_used }}@endif @if (empty($apiKey->last_used))
<time class="timeago timeago-contextual" {{ $__t('never') }}@else{{ $apiKey->last_used }}
datetime="{{ $apiKey->last_used }}"></time> @endif
<time class="timeago timeago-contextual" datetime="{{ $apiKey->last_used }}"></time>
</td> </td>
<td> <td>
{{ $apiKey->row_created_timestamp }} {{ $apiKey->row_created_timestamp }}

View File

@ -6,7 +6,11 @@
@push('pageScripts') @push('pageScripts')
<script src="{{ $U('/node_modules/fullcalendar/dist/fullcalendar.min.js?v=', true) }}{{ $version }}"></script> <script src="{{ $U('/node_modules/fullcalendar/dist/fullcalendar.min.js?v=', true) }}{{ $version }}"></script>
@if(!empty($__t('fullcalendar_locale') && $__t('fullcalendar_locale') != 'x'))<script src="{{ $U('/node_modules', true) }}/fullcalendar/dist/locale/{{ $__t('fullcalendar_locale') }}.js?v={{ $version }}"></script>@endif @if (!empty($__t('fullcalendar_locale') && $__t('fullcalendar_locale') != 'x'))
<script
src="{{ $U('/node_modules', true) }}/fullcalendar/dist/locale/{{ $__t('fullcalendar_locale') }}.js?v={{ $version }}">
</script>
@endif
@endpush @endpush
@push('pageStyles') @push('pageStyles')
@ -76,7 +80,7 @@
@section('content') @section('content')
<script> <script>
var fullcalendarEventSources = {!! json_encode(array($fullcalendarEventSources)) !!} var fullcalendarEventSources = {!! json_encode([$fullcalendarEventSources]) !!}
var internalRecipes = {!! json_encode($internalRecipes) !!} var internalRecipes = {!! json_encode($internalRecipes) !!}
var recipesResolved = {!! json_encode($recipesResolved) !!} var recipesResolved = {!! json_encode($recipesResolved) !!}
@ -91,17 +95,13 @@
<div class="title-related-links"> <div class="title-related-links">
<h2 class="title">@yield('title')</h2> <h2 class="title">@yield('title')</h2>
<div class="float-right d-print-none"> <div class="float-right d-print-none">
<button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" <button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" type="button"
type="button" data-toggle="collapse" data-target="#related-links">
data-toggle="collapse"
data-target="#related-links">
<i class="fas fa-ellipsis-v"></i> <i class="fas fa-ellipsis-v"></i>
</button> </button>
</div> </div>
<div class="related-links collapse d-md-flex order-2 width-xs-sm-100 d-print-none" <div class="related-links collapse d-md-flex order-2 width-xs-sm-100 d-print-none" id="related-links">
id="related-links"> <a id="print-meal-plan-button" class="btn btn-outline-dark m-1 mt-md-0 mb-md-0 float-right">
<a id="print-meal-plan-button"
class="btn btn-outline-dark m-1 mt-md-0 mb-md-0 float-right">
{{ $__t('Print') }} {{ $__t('Print') }}
</a> </a>
<a class="btn btn-outline-secondary m-1 mt-md-0 mb-md-0 float-right" <a class="btn btn-outline-secondary m-1 mt-md-0 mb-md-0 float-right"
@ -118,13 +118,9 @@
@foreach ($usedMealplanSections as $mealplanSection) @foreach ($usedMealplanSections as $mealplanSection)
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<div class="calendar" <div class="calendar" data-section-id="{{ $mealplanSection->id }}"
data-section-id="{{ $mealplanSection->id }}"
data-section-name="{{ $mealplanSection->name }}<br><span class='small text-muted'>{{ $mealplanSection->time_info }}</span>" data-section-name="{{ $mealplanSection->name }}<br><span class='small text-muted'>{{ $mealplanSection->time_info }}</span>"
data-primary-section="{{ BoolToString($loop->first) }}" data-primary-section="{{ BoolToString($loop->first) }}" {{-- $loop->last doesn't work however, is always null... --}}
{{--
$loop->last doesn't work however, is always null...
--}}
data-last-section="{{ BoolToString(array_values(array_slice($usedMealplanSections->fetchAll(), -1))[0]->id == $mealplanSection->id) }}"> data-last-section="{{ BoolToString(array_values(array_slice($usedMealplanSections->fetchAll(), -1))[0]->id == $mealplanSection->id) }}">
</div> </div>
</div> </div>
@ -135,49 +131,41 @@
@if ($usedMealplanSections->count() === 0) @if ($usedMealplanSections->count() === 0)
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<div class="calendar" <div class="calendar" data-section-id="-1" data-section-name="" data-primary-section="true"
data-section-id="-1"
data-section-name=""
data-primary-section="true"
data-last-section="true"> data-last-section="true">
</div> </div>
</div> </div>
</div> </div>
@endif @endif
<div class="modal fade" <div class="modal fade" id="add-recipe-modal" tabindex="-1">
id="add-recipe-modal"
tabindex="-1">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h4 id="add-recipe-modal-title" <h4 id="add-recipe-modal-title" class="modal-title w-100"></h4>
class="modal-title w-100"></h4>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<form id="add-recipe-form" <form id="add-recipe-form" novalidate>
novalidate>
@include('components.recipepicker', array( @include('components.recipepicker', [
'recipes' => $recipes, 'recipes' => $recipes,
'isRequired' => true, 'isRequired' => true,
'nextInputSelector' => '#recipe_servings' 'nextInputSelector' => '#recipe_servings',
)) ])
@include('components.numberpicker', array( @include('components.numberpicker', [
'id' => 'recipe_servings', 'id' => 'recipe_servings',
'label' => 'Servings', 'label' => 'Servings',
'min' => $DEFAULT_MIN_AMOUNT, 'min' => $DEFAULT_MIN_AMOUNT,
'decimals' => $userSettings['stock_decimal_places_amounts'], 'decimals' => $userSettings['stock_decimal_places_amounts'],
'value' => '1', 'value' => '1',
'additionalCssClasses' => 'locale-number-input locale-number-quantity-amount' 'additionalCssClasses' => 'locale-number-input locale-number-quantity-amount',
)) ])
<div class="form-group"> <div class="form-group">
<label for="period_type">{{ $__t('Section') }}</label> <label for="period_type">{{ $__t('Section') }}</label>
<select class="custom-control custom-select" {{-- TODO: Select2: dynamic data: meal_plan_sections --}}
id="section_id_recipe" <select class="custom-control custom-select" id="section_id_recipe" name="section_id_recipe"
name="section_id_recipe"
required> required>
@foreach ($mealplanSections as $mealplanSection) @foreach ($mealplanSections as $mealplanSection)
<option value="{{ $mealplanSection->id }}">{{ $mealplanSection->name }}</option> <option value="{{ $mealplanSection->id }}">{{ $mealplanSection->name }}</option>
@ -185,54 +173,38 @@
</select> </select>
</div> </div>
<input type="hidden" <input type="hidden" id="day" name="day" value="">
id="day" <input type="hidden" name="type" value="recipe">
name="day"
value="">
<input type="hidden"
name="type"
value="recipe">
</form> </form>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" <button type="button" class="btn btn-secondary" data-dismiss="modal">{{ $__t('Cancel') }}</button>
class="btn btn-secondary" <button id="save-add-recipe-button" data-dismiss="modal"
data-dismiss="modal">{{ $__t('Cancel') }}</button>
<button id="save-add-recipe-button"
data-dismiss="modal"
class="btn btn-success">{{ $__t('Save') }}</button> class="btn btn-success">{{ $__t('Save') }}</button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="modal fade" <div class="modal fade" id="add-note-modal" tabindex="-1">
id="add-note-modal"
tabindex="-1">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h4 id="add-note-modal-title" <h4 id="add-note-modal-title" class="modal-title w-100"></h4>
class="modal-title w-100"></h4>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<form id="add-note-form" <form id="add-note-form" novalidate>
novalidate>
<div class="form-group"> <div class="form-group">
<label for="note">{{ $__t('Note') }}</label> <label for="note">{{ $__t('Note') }}</label>
<textarea class="form-control" <textarea class="form-control" rows="2" id="note" name="note"></textarea>
rows="2"
id="note"
name="note"></textarea>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="period_type">{{ $__t('Section') }}</label> <label for="period_type">{{ $__t('Section') }}</label>
<select class="custom-control custom-select" {{-- TODO: Select2: dynamic data: meal_plan_sections --}}
id="section_id_note" <select class="custom-control custom-select" id="section_id_note" name="section_id_note"
name="section_id_note"
required> required>
@foreach ($mealplanSections as $mealplanSection) @foreach ($mealplanSections as $mealplanSection)
<option value="{{ $mealplanSection->id }}">{{ $mealplanSection->name }}</option> <option value="{{ $mealplanSection->id }}">{{ $mealplanSection->name }}</option>
@ -240,52 +212,42 @@
</select> </select>
</div> </div>
<input type="hidden" <input type="hidden" name="type" value="note">
name="type"
value="note">
</form> </form>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" <button type="button" class="btn btn-secondary" data-dismiss="modal">{{ $__t('Cancel') }}</button>
class="btn btn-secondary" <button id="save-add-note-button" data-dismiss="modal"
data-dismiss="modal">{{ $__t('Cancel') }}</button>
<button id="save-add-note-button"
data-dismiss="modal"
class="btn btn-success">{{ $__t('Save') }}</button> class="btn btn-success">{{ $__t('Save') }}</button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="modal fade" <div class="modal fade" id="add-product-modal" tabindex="-1">
id="add-product-modal"
tabindex="-1">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h4 id="add-product-modal-title" <h4 id="add-product-modal-title" class="modal-title w-100"></h4>
class="modal-title w-100"></h4>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<form id="add-product-form" <form id="add-product-form" novalidate>
novalidate>
@include('components.productpicker', array( @include('components.productpicker', [
'products' => $products, 'productsQuery' => 'order=name%3Acollate%20nocase',
'nextInputSelector' => '#amount' 'nextInputSelector' => '#amount',
)) ])
@include('components.productamountpicker', array( @include('components.productamountpicker', [
'value' => 1, 'value' => 1,
'additionalGroupCssClasses' => 'mb-0' 'additionalGroupCssClasses' => 'mb-0',
)) ])
<div class="form-group"> <div class="form-group">
<label for="period_type">{{ $__t('Section') }}</label> <label for="period_type">{{ $__t('Section') }}</label>
<select class="custom-control custom-select" {{-- TODO: Select2: dynamic data: meal_plan_sections --}}
id="section_id_product" <select class="custom-control custom-select" id="section_id_product" name="section_id_product"
name="section_id_product"
required> required>
@foreach ($mealplanSections as $mealplanSection) @foreach ($mealplanSections as $mealplanSection)
<option value="{{ $mealplanSection->id }}">{{ $mealplanSection->name }}</option> <option value="{{ $mealplanSection->id }}">{{ $mealplanSection->name }}</option>
@ -293,38 +255,29 @@
</select> </select>
</div> </div>
<input type="hidden" <input type="hidden" name="type" value="product">
name="type"
value="product">
</form> </form>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" <button type="button" class="btn btn-secondary" data-dismiss="modal">{{ $__t('Cancel') }}</button>
class="btn btn-secondary" <button id="save-add-product-button" data-dismiss="modal"
data-dismiss="modal">{{ $__t('Cancel') }}</button>
<button id="save-add-product-button"
data-dismiss="modal"
class="btn btn-success">{{ $__t('Save') }}</button> class="btn btn-success">{{ $__t('Save') }}</button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="modal fade" <div class="modal fade" id="copy-day-modal" tabindex="-1">
id="copy-day-modal"
tabindex="-1">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h4 id="copy-day-modal-title" <h4 id="copy-day-modal-title" class="modal-title w-100"></h4>
class="modal-title w-100"></h4>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<form id="copy-day-form" <form id="copy-day-form" novalidate>
novalidate>
@include('components.datetimepicker', array( @include('components.datetimepicker', [
'id' => 'copy_to_date', 'id' => 'copy_to_date',
'label' => 'Day', 'label' => 'Day',
'format' => 'YYYY-MM-DD', 'format' => 'YYYY-MM-DD',
@ -333,35 +286,28 @@
'limitStartToNow' => false, 'limitStartToNow' => false,
'isRequired' => true, 'isRequired' => true,
'additionalCssClasses' => 'date-only-datetimepicker', 'additionalCssClasses' => 'date-only-datetimepicker',
'invalidFeedback' => $__t('A date is required') 'invalidFeedback' => $__t('A date is required'),
)) ])
</form> </form>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" <button type="button" class="btn btn-secondary" data-dismiss="modal">{{ $__t('Cancel') }}</button>
class="btn btn-secondary" <button id="save-copy-day-button" data-dismiss="modal"
data-dismiss="modal">{{ $__t('Cancel') }}</button>
<button id="save-copy-day-button"
data-dismiss="modal"
class="btn btn-primary">{{ $__t('Copy') }}</button> class="btn btn-primary">{{ $__t('Copy') }}</button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="modal fade" <div class="modal fade" id="mealplan-productcard-modal" tabindex="-1">
id="mealplan-productcard-modal"
tabindex="-1">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content text-center"> <div class="modal-content text-center">
<div class="modal-body"> <div class="modal-body">
@include('components.productcard') @include('components.productcard')
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" <button type="button" class="btn btn-secondary" data-dismiss="modal">{{ $__t('Close') }}</button>
class="btn btn-secondary"
data-dismiss="modal">{{ $__t('Close') }}</button>
</div> </div>
</div> </div>
</div> </div>

View File

@ -10,21 +10,16 @@
<div class="title-related-links"> <div class="title-related-links">
<h2 class="title">@yield('title')</h2> <h2 class="title">@yield('title')</h2>
<div class="float-right"> <div class="float-right">
<button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" <button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" type="button"
type="button" data-toggle="collapse" data-target="#table-filter-row">
data-toggle="collapse"
data-target="#table-filter-row">
<i class="fas fa-filter"></i> <i class="fas fa-filter"></i>
</button> </button>
<button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" <button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" type="button"
type="button" data-toggle="collapse" data-target="#related-links">
data-toggle="collapse"
data-target="#related-links">
<i class="fas fa-ellipsis-v"></i> <i class="fas fa-ellipsis-v"></i>
</button> </button>
</div> </div>
<div class="related-links collapse d-md-flex order-2 width-xs-sm-100" <div class="related-links collapse d-md-flex order-2 width-xs-sm-100" id="related-links">
id="related-links">
<a class="btn btn-primary responsive-button m-1 mt-md-0 mb-md-0 float-right show-as-dialog-link" <a class="btn btn-primary responsive-button m-1 mt-md-0 mb-md-0 float-right show-as-dialog-link"
href="{{ $U('/mealplansection/new?embedded') }}"> href="{{ $U('/mealplansection/new?embedded') }}">
{{ $__t('Add') }} {{ $__t('Add') }}
@ -36,24 +31,18 @@
<hr class="my-2"> <hr class="my-2">
<div class="row collapse d-md-flex" <div class="row collapse d-md-flex" id="table-filter-row">
id="table-filter-row">
<div class="col-12 col-md-6 col-xl-3"> <div class="col-12 col-md-6 col-xl-3">
<div class="input-group"> <div class="input-group">
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-search"></i></span> <span class="input-group-text"><i class="fas fa-search"></i></span>
</div> </div>
<input type="text" <input type="text" id="search" class="form-control" placeholder="{{ $__t('Search') }}">
id="search"
class="form-control"
placeholder="{{ $__t('Search') }}">
</div> </div>
</div> </div>
<div class="col"> <div class="col">
<div class="float-right"> <div class="float-right">
<a id="clear-filter-button" <a id="clear-filter-button" class="btn btn-sm btn-outline-info" href="#">
class="btn btn-sm btn-outline-info"
href="#">
{{ $__t('Clear filter') }} {{ $__t('Clear filter') }}
</a> </a>
</div> </div>
@ -62,16 +51,13 @@
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<table id="mealplansections-table" {{-- TODO: DataTables: dynamic data: meal_plan_sections --}}
class="table table-sm table-striped nowrap w-100"> <table id="mealplansections-table" class="table table-sm table-striped nowrap w-100">
<thead> <thead>
<tr> <tr>
<th class="border-right"><a class="text-muted change-table-columns-visibility-button" <th class="border-right"><a class="text-muted change-table-columns-visibility-button"
data-toggle="tooltip" data-toggle="tooltip" data-toggle="tooltip" title="{{ $__t('Table options') }}"
data-toggle="tooltip" data-table-selector="#mealplansections-table" href="#"><i class="fas fa-eye"></i></a>
title="{{ $__t('Table options') }}"
data-table-selector="#mealplansections-table"
href="#"><i class="fas fa-eye"></i></a>
</th> </th>
<th>{{ $__t('Name') }}</th> <th>{{ $__t('Name') }}</th>
<th>{{ $__t('Sort number') }}</th> <th>{{ $__t('Sort number') }}</th>
@ -84,15 +70,12 @@
<td class="fit-content border-right"> <td class="fit-content border-right">
<a class="btn btn-info btn-sm show-as-dialog-link" <a class="btn btn-info btn-sm show-as-dialog-link"
href="{{ $U('/mealplansection/') }}{{ $mealplanSection->id }}?embedded" href="{{ $U('/mealplansection/') }}{{ $mealplanSection->id }}?embedded"
data-toggle="tooltip" data-toggle="tooltip" title="{{ $__t('Edit this item') }}">
title="{{ $__t('Edit this item') }}">
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
</a> </a>
<a class="btn btn-danger btn-sm mealplansection-delete-button" <a class="btn btn-danger btn-sm mealplansection-delete-button" href="#"
href="#"
data-mealplansection-id="{{ $mealplanSection->id }}" data-mealplansection-id="{{ $mealplanSection->id }}"
data-mealplansection-name="{{ $mealplanSection->name }}" data-mealplansection-name="{{ $mealplanSection->name }}" data-toggle="tooltip"
data-toggle="tooltip"
title="{{ $__t('Delete this item') }}"> title="{{ $__t('Delete this item') }}">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
</a> </a>

View File

@ -19,7 +19,8 @@
<div class="title-related-links"> <div class="title-related-links">
<h2 class="title">@yield('title')</h2> <h2 class="title">@yield('title')</h2>
<h2> <h2>
<span class="text-muted small">{{ $__t('Barcode for product') }} <strong>{{ $product->name }}</strong></span> <span class="text-muted small">{{ $__t('Barcode for product') }}
<strong>{{ $product->name }}</strong></span>
</h2> </h2>
</div> </div>
</div> </div>
@ -42,70 +43,60 @@
</script> </script>
@endif @endif
<form id="barcode-form" <form id="barcode-form" novalidate>
novalidate>
<input type="hidden" <input type="hidden" name="product_id" value="{{ $product->id }}">
name="product_id"
value="{{ $product->id }}">
<div class="form-group"> <div class="form-group">
<label for="name">{{ $__t('Barcode') }}&nbsp;<i class="fas fa-barcode"></i></label> <label for="name">{{ $__t('Barcode') }}&nbsp;<i class="fas fa-barcode"></i></label>
<div class="input-group"> <div class="input-group">
<input type="text" <input type="text" class="form-control barcodescanner-input" required id="barcode" name="barcode"
class="form-control barcodescanner-input"
required
id="barcode"
name="barcode"
value="@if ($mode == 'edit') {{ $barcode->barcode }} @endif" value="@if ($mode == 'edit') {{ $barcode->barcode }} @endif"
data-target="#barcode"> data-target="#barcode">
@include('components.barcodescanner') @include('components.barcodescanner')
</div> </div>
</div> </div>
@php if($mode == 'edit') { $value = $barcode->amount; } else { $value = ''; } @endphp @php
@include('components.productamountpicker', array( if ($mode == 'edit') {
$value = $barcode->amount;
} else {
$value = '';
}
@endphp
@include('components.productamountpicker', [
'value' => $value, 'value' => $value,
'isRequired' => false 'isRequired' => false,
)) ])
@if (GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) @if (GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)
<div class="form-group"> <div class="form-group">
<label for="shopping_location_id_id">{{ $__t('Store') }}</label> <label for="shopping_location_id_id">{{ $__t('Store') }}</label>
<select class="custom-control custom-select" {{-- TODO: Select2: dynamic data: shopping_locations --}}
id="shopping_location_id" <select class="custom-control custom-select" id="shopping_location_id" name="shopping_location_id">
name="shopping_location_id">
<option></option> <option></option>
@foreach ($shoppinglocations as $store) @foreach ($shoppinglocations as $store)
<option @if($mode=='edit' <option @if ($mode == 'edit' && $store->id == $barcode->shopping_location_id) selected="selected" @endif
&& value="{{ $store->id }}">{{ $store->name }}</option>
$store->id == $barcode->shopping_location_id) selected="selected" @endif value="{{ $store->id }}">{{ $store->name }}</option>
@endforeach @endforeach
</select> </select>
</div> </div>
@else @else
<input type="hidden" <input type="hidden" name="shopping_location_id" id="shopping_location_id" value="1">
name="shopping_location_id"
id="shopping_location_id"
value="1">
@endif @endif
<div class="form-group"> <div class="form-group">
<label for="note">{{ $__t('Note') }}</label> <label for="note">{{ $__t('Note') }}</label>
<input type="text" <input type="text" class="form-control" id="note" name="note"
class="form-control"
id="note"
name="note"
value="@if ($mode == 'edit') {{ $barcode->note }} @endif"> value="@if ($mode == 'edit') {{ $barcode->note }} @endif">
</div> </div>
@include('components.userfieldsform', array( @include('components.userfieldsform', [
'userfields' => $userfields, 'userfields' => $userfields,
'entity' => 'product_barcodes' 'entity' => 'product_barcodes',
)) ])
<button id="save-barcode-button" <button id="save-barcode-button" class="btn btn-success">{{ $__t('Save') }}</button>
class="btn btn-success">{{ $__t('Save') }}</button>
</form> </form>
</div> </div>

View File

@ -15,15 +15,12 @@
<h2 class="title">@yield('title')</h2> <h2 class="title">@yield('title')</h2>
@if ($mode == 'edit') @if ($mode == 'edit')
<div class="float-right"> <div class="float-right">
<button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" <button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" type="button"
type="button" data-toggle="collapse" data-target="#related-links">
data-toggle="collapse"
data-target="#related-links">
<i class="fas fa-ellipsis-v"></i> <i class="fas fa-ellipsis-v"></i>
</button> </button>
</div> </div>
<div class="related-links collapse d-md-flex order-2 width-xs-sm-100" <div class="related-links collapse d-md-flex order-2 width-xs-sm-100" id="related-links">
id="related-links">
<a class="btn btn-outline-secondary m-1 mt-md-0 mb-md-0 float-right show-as-dialog-link" <a class="btn btn-outline-secondary m-1 mt-md-0 mb-md-0 float-right show-as-dialog-link"
href="{{ $U('/stockentries?embedded&product=') }}{{ $product->id }}"> href="{{ $U('/stockentries?embedded&product=') }}{{ $product->id }}">
{{ $__t('Stock entries') }} {{ $__t('Stock entries') }}
@ -58,117 +55,117 @@
@endif @endif
@endif @endif
<form id="product-form" <form id="product-form" class="has-sticky-form-footer" novalidate>
class="has-sticky-form-footer"
novalidate>
<div class="form-group"> <div class="form-group">
<label for="name">{{ $__t('Name') }}</label> <label for="name">{{ $__t('Name') }}</label>
<input type="text" <input type="text" class="form-control" required id="name" name="name"
class="form-control"
required
id="name"
name="name"
value="@if ($mode == 'edit') {{ $product->name }} @endif"> value="@if ($mode == 'edit') {{ $product->name }} @endif">
<div class="invalid-feedback">{{ $__t('A name is required') }}</div> <div class="invalid-feedback">{{ $__t('A name is required') }}</div>
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="custom-control custom-checkbox"> <div class="custom-control custom-checkbox">
<input @if($mode=='create' <input
) @if ($mode == 'create') checked
checked @elseif($mode == 'edit' && $product->active == 1) checked @endif
@elseif($mode=='edit' class="form-check-input custom-control-input" type="checkbox" id="active" name="active"
&& value="1">
$product->active == 1) checked @endif class="form-check-input custom-control-input" type="checkbox" id="active" name="active" value="1"> <label class="form-check-label custom-control-label" for="active">{{ $__t('Active') }}</label>
<label class="form-check-label custom-control-label"
for="active">{{ $__t('Active') }}</label>
</div> </div>
</div> </div>
@php $prefillById = ''; if($mode=='edit') { $prefillById = $product->parent_product_id; } @endphp @php
$prefillById = '';
if ($mode == 'edit') {
$prefillById = $product->parent_product_id;
}
@endphp
@php @php
$hint = ''; $hint = '';
if ($isSubProductOfOthers) if ($isSubProductOfOthers) {
{
$hint = $__t('Not possible because this product is already used as a parent product in another product'); $hint = $__t('Not possible because this product is already used as a parent product in another product');
} }
@endphp @endphp
@include('components.productpicker', array( @include('components.productpicker', [
'products' => $products, 'productsQuery' => (isset($product) ? 'query%5B%5D=id!%3D' . rawurlencode($product->id) . '&' : '') . 'query%5B%5D=parent_product_id%3Dnull&query%5B%5D=active%3D1&order=name%3Acollate%20nocase',
'prefillById' => $prefillById, 'prefillById' => $prefillById,
'disallowAllProductWorkflows' => true, 'disallowAllProductWorkflows' => true,
'isRequired' => false, 'isRequired' => false,
'label' => 'Parent product', 'label' => 'Parent product',
'disabled' => $isSubProductOfOthers, 'disabled' => $isSubProductOfOthers,
'hint' => $hint 'hint' => $hint,
)) ])
@php $hint = ''; @endphp @php $hint = ''; @endphp
<div class="form-group"> <div class="form-group">
<label for="description">{{ $__t('Description') }}</label> <label for="description">{{ $__t('Description') }}</label>
<textarea class="form-control wysiwyg-editor" <textarea class="form-control wysiwyg-editor" id="description" name="description">
id="description" @if ($mode == 'edit')
name="description">@if($mode == 'edit'){{ $product->description }}@endif</textarea> {{ $product->description }}
@endif
</textarea>
</div> </div>
@if (GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING) @if (GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING)
<div class="form-group"> <div class="form-group">
<label for="location_id">{{ $__t('Default location') }}</label> <label for="location_id">{{ $__t('Default location') }}</label>
<select required {{-- TODO: Select2: dynamic data: locations --}}
class="custom-control custom-select" <select required class="custom-control custom-select" id="location_id" name="location_id">
id="location_id"
name="location_id">
<option></option> <option></option>
@foreach ($locations as $location) @foreach ($locations as $location)
<option @if($mode=='edit' <option @if ($mode == 'edit' && $location->id == $product->location_id) selected="selected" @endif
&& value="{{ $location->id }}">{{ $location->name }}</option>
$location->id == $product->location_id) selected="selected" @endif value="{{ $location->id }}">{{ $location->name }}</option>
@endforeach @endforeach
</select> </select>
<div class="invalid-feedback">{{ $__t('A location is required') }}</div> <div class="invalid-feedback">{{ $__t('A location is required') }}</div>
</div> </div>
@else @else
<input type="hidden" <input type="hidden" name="location_id" id="location_id" value="1">
name="location_id"
id="location_id"
value="1">
@endif @endif
@php $prefillById = ''; if($mode=='edit') { $prefillById = $product->shopping_location_id; } @endphp @php
$prefillById = '';
if ($mode == 'edit') {
$prefillById = $product->shopping_location_id;
}
@endphp
@if (GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) @if (GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)
@include('components.shoppinglocationpicker', array( @include('components.shoppinglocationpicker', [
'label' => 'Default store', 'label' => 'Default store',
'prefillById' => $prefillById, 'prefillById' => $prefillById,
'shoppinglocations' => $shoppinglocations 'shoppinglocations' => $shoppinglocations,
)) ])
@else @else
<input type="hidden" <input type="hidden" name="shopping_location_id" id="shopping_location_id" value="1">
name="shopping_location_id"
id="shopping_location_id"
value="1">
@endif @endif
@php if($mode == 'edit') { $value = $product->min_stock_amount; } else { $value = 0; } @endphp @php
@include('components.numberpicker', array( if ($mode == 'edit') {
$value = $product->min_stock_amount;
} else {
$value = 0;
}
@endphp
@include('components.numberpicker', [
'id' => 'min_stock_amount', 'id' => 'min_stock_amount',
'label' => 'Minimum stock amount', 'label' => 'Minimum stock amount',
'min' => '0.', 'min' => '0.',
'decimals' => $userSettings['stock_decimal_places_amounts'], 'decimals' => $userSettings['stock_decimal_places_amounts'],
'value' => $value, 'value' => $value,
'additionalGroupCssClasses' => 'mb-1', 'additionalGroupCssClasses' => 'mb-1',
'additionalCssClasses' => 'locale-number-input locale-number-quantity-amount' 'additionalCssClasses' => 'locale-number-input locale-number-quantity-amount',
)) ])
<div class="form-group @if (GROCY_FEATURE_FLAG_STOCK_PRODUCT_OPENED_TRACKING) mb-1 @endif"> <div class="form-group @if (GROCY_FEATURE_FLAG_STOCK_PRODUCT_OPENED_TRACKING) mb-1 @endif">
<div class="custom-control custom-checkbox"> <div class="custom-control custom-checkbox">
<input @if($mode=='edit' <input @if ($mode == 'edit' && $product->cumulate_min_stock_amount_of_sub_products == 1) checked @endif
&& class="form-check-input custom-control-input" type="checkbox"
$product->cumulate_min_stock_amount_of_sub_products == 1) checked @endif class="form-check-input custom-control-input" type="checkbox" id="cumulate_min_stock_amount_of_sub_products" name="cumulate_min_stock_amount_of_sub_products" value="1"> id="cumulate_min_stock_amount_of_sub_products" name="cumulate_min_stock_amount_of_sub_products"
value="1">
<label class="form-check-label custom-control-label" <label class="form-check-label custom-control-label"
for="cumulate_min_stock_amount_of_sub_products">{{ $__t('Accumulate sub products min. stock amount') }} for="cumulate_min_stock_amount_of_sub_products">{{ $__t('Accumulate sub products min. stock amount') }}
&nbsp;<i class="fas fa-question-circle text-muted" &nbsp;<i class="fas fa-question-circle text-muted" data-toggle="tooltip"
data-toggle="tooltip"
data-trigger="hover click" data-trigger="hover click"
title="{{ $__t('If enabled, the min. stock amount of sub products will be accumulated into this product, means the sub product will never be missing, only this product') }}"></i> title="{{ $__t('If enabled, the min. stock amount of sub products will be accumulated into this product, means the sub product will never be missing, only this product') }}"></i>
</label> </label>
@ -178,12 +175,12 @@
@if (GROCY_FEATURE_FLAG_STOCK_PRODUCT_OPENED_TRACKING) @if (GROCY_FEATURE_FLAG_STOCK_PRODUCT_OPENED_TRACKING)
<div class="form-group"> <div class="form-group">
<div class="custom-control custom-checkbox"> <div class="custom-control custom-checkbox">
<input @if($mode=='edit' <input @if ($mode == 'edit' && $product->treat_opened_as_out_of_stock == 1) checked @endif
&& class="form-check-input custom-control-input" type="checkbox"
$product->treat_opened_as_out_of_stock == 1) checked @endif class="form-check-input custom-control-input" type="checkbox" id="treat_opened_as_out_of_stock" name="treat_opened_as_out_of_stock" value="1"> id="treat_opened_as_out_of_stock" name="treat_opened_as_out_of_stock" value="1">
<label class="form-check-label custom-control-label" <label class="form-check-label custom-control-label"
for="treat_opened_as_out_of_stock">{{ $__t('Treat opened as out of stock') }}&nbsp;<i class="fas fa-question-circle text-muted" for="treat_opened_as_out_of_stock">{{ $__t('Treat opened as out of stock') }}&nbsp;<i
data-toggle="tooltip" class="fas fa-question-circle text-muted" data-toggle="tooltip"
data-trigger="hover click" data-trigger="hover click"
title="{{ $__t('When enabled, opened items will be counted as missing for calculating if this product is below its minimum stock amount') }}"></i> title="{{ $__t('When enabled, opened items will be counted as missing for calculating if this product is below its minimum stock amount') }}"></i>
</label> </label>
@ -193,161 +190,159 @@
@if (GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING) @if (GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING)
<div class="form-group"> <div class="form-group">
<label class="d-block my-0" <label class="d-block my-0" for="location_id">{{ $__t('Due date type') }}
for="location_id">{{ $__t('Due date type') }} <i class="fas fa-question-circle text-muted" data-toggle="tooltip" data-trigger="hover click"
<i class="fas fa-question-circle text-muted"
data-toggle="tooltip"
data-trigger="hover click"
title="{{ $__t('Based on the selected type, the highlighting on the stock overview page will be different') }}"></i> title="{{ $__t('Based on the selected type, the highlighting on the stock overview page will be different') }}"></i>
</label> </label>
<div class="custom-control custom-radio mt-n2"> <div class="custom-control custom-radio mt-n2">
<input class="custom-control-input" <input class="custom-control-input" type="radio" name="due_type" id="due-type-bestbefore"
type="radio" value="1" @if ($mode == 'edit' && $product->due_type == 1) checked @else checked @endif>
name="due_type" <label class="custom-control-label" for="due-type-bestbefore">{{ $__t('Best before date') }}
id="due-type-bestbefore" <i class="fas fa-question-circle text-muted" data-toggle="tooltip"
value="1"
@if($mode=='edit'
&&
$product->due_type == 1) checked @else checked @endif>
<label class="custom-control-label"
for="due-type-bestbefore">{{ $__t('Best before date') }}
<i class="fas fa-question-circle text-muted"
data-toggle="tooltip"
data-trigger="hover click" data-trigger="hover click"
title="{{ $__t('Means that the product is maybe still safe to be consumed after its due date is reached') }}"></i> title="{{ $__t('Means that the product is maybe still safe to be consumed after its due date is reached') }}"></i>
</label> </label>
</div> </div>
<div class="custom-control custom-radio mt-n2"> <div class="custom-control custom-radio mt-n2">
<input class="custom-control-input" <input class="custom-control-input" type="radio" name="due_type" id="due-type-expiration"
type="radio" value="2" @if ($mode == 'edit' && $product->due_type == 2) checked @endif>
name="due_type" <label class="custom-control-label" for="due-type-expiration">{{ $__t('Expiration date') }}
id="due-type-expiration" <i class="fas fa-question-circle text-muted" data-toggle="tooltip"
value="2"
@if($mode=='edit'
&&
$product->due_type == 2) checked @endif>
<label class="custom-control-label"
for="due-type-expiration">{{ $__t('Expiration date') }}
<i class="fas fa-question-circle text-muted"
data-toggle="tooltip"
data-trigger="hover click" data-trigger="hover click"
title="{{ $__t('Means that the product is not safe to be consumed after its due date is reached') }}"></i> title="{{ $__t('Means that the product is not safe to be consumed after its due date is reached') }}"></i>
</label> </label>
</div> </div>
</div> </div>
@php if($mode == 'edit') { $value = $product->default_best_before_days; } else { $value = 0; } @endphp @php
@include('components.numberpicker', array( if ($mode == 'edit') {
$value = $product->default_best_before_days;
} else {
$value = 0;
}
@endphp
@include('components.numberpicker', [
'id' => 'default_best_before_days', 'id' => 'default_best_before_days',
'label' => 'Default due days', 'label' => 'Default due days',
'min' => -1, 'min' => -1,
'value' => $value, 'value' => $value,
'hint' => $__t('For purchases this amount of days will be added to today for the due date suggestion') . ' (' . $__t('-1 means that this product will be never overdue') . ')' 'hint' =>
)) $__t(
'For purchases this amount of days will be added to today for the due date suggestion'
) .
' (' .
$__t('-1 means that this product will be never overdue') .
')',
])
@if (GROCY_FEATURE_FLAG_STOCK_PRODUCT_OPENED_TRACKING) @if (GROCY_FEATURE_FLAG_STOCK_PRODUCT_OPENED_TRACKING)
@php if($mode == 'edit') { $value = $product->default_best_before_days_after_open; } else { $value = 0; } @endphp @php
@include('components.numberpicker', array( if ($mode == 'edit') {
$value = $product->default_best_before_days_after_open;
} else {
$value = 0;
}
@endphp
@include('components.numberpicker', [
'id' => 'default_best_before_days_after_open', 'id' => 'default_best_before_days_after_open',
'label' => 'Default due days after opened', 'label' => 'Default due days after opened',
'min' => 0, 'min' => 0,
'value' => $value, 'value' => $value,
'hint' => $__t('When this product was marked as opened, the due date will be replaced by today + this amount of days, but only if the resulting date is not after the original due date (a value of 0 disables this)') 'hint' => $__t(
)) 'When this product was marked as opened, the due date will be replaced by today + this amount of days, but only if the resulting date is not after the original due date (a value of 0 disables this)'
),
])
@else @else
<input type="hidden" <input type="hidden" name="default_best_before_days_after_open"
name="default_best_before_days_after_open" id="default_best_before_days_after_open" value="1">
id="default_best_before_days_after_open"
value="1">
@endif @endif
@else @else
<input type="hidden" <input type="hidden" name="default_best_before_days" id="default_best_before_days" value="1">
name="default_best_before_days" <input type="hidden" name="due_type" id="due_type" value="1">
id="default_best_before_days"
value="1">
<input type="hidden"
name="due_type"
id="due_type"
value="1">
@endif @endif
@if (GROCY_FEATURE_FLAG_STOCK_PRODUCT_FREEZING) @if (GROCY_FEATURE_FLAG_STOCK_PRODUCT_FREEZING)
@php if($mode == 'edit') { $value = $product->default_best_before_days_after_freezing; } else { $value = 0; } @endphp @php
@include('components.numberpicker', array( if ($mode == 'edit') {
$value = $product->default_best_before_days_after_freezing;
} else {
$value = 0;
}
@endphp
@include('components.numberpicker', [
'id' => 'default_best_before_days_after_freezing', 'id' => 'default_best_before_days_after_freezing',
'label' => 'Default due days after freezing', 'label' => 'Default due days after freezing',
'min' => -1, 'min' => -1,
'value' => $value, 'value' => $value,
'hint' => $__t('On moving this product to a freezer location (so when freezing it), the due date will be replaced by today + this amount of days') . ' (' . $__t('-1 means that this product will be never overdue') . ')' 'hint' =>
)) $__t(
'On moving this product to a freezer location (so when freezing it), the due date will be replaced by today + this amount of days'
) .
' (' .
$__t('-1 means that this product will be never overdue') .
')',
])
@php if($mode == 'edit') { $value = $product->default_best_before_days_after_thawing; } else { $value = 0; } @endphp @php
@include('components.numberpicker', array( if ($mode == 'edit') {
$value = $product->default_best_before_days_after_thawing;
} else {
$value = 0;
}
@endphp
@include('components.numberpicker', [
'id' => 'default_best_before_days_after_thawing', 'id' => 'default_best_before_days_after_thawing',
'label' => 'Default due days after thawing', 'label' => 'Default due days after thawing',
'min' => 0, 'min' => 0,
'value' => $value, 'value' => $value,
'hint' => $__t('On moving this product from a freezer location (so when thawing it), the due date will be replaced by today + this amount of days') 'hint' => $__t(
)) 'On moving this product from a freezer location (so when thawing it), the due date will be replaced by today + this amount of days'
),
])
<div class="form-group"> <div class="form-group">
<div class="custom-control custom-checkbox"> <div class="custom-control custom-checkbox">
<input @if($mode=='edit' <input @if ($mode == 'edit' && $product->should_not_be_frozen == 1) checked @endif
&& class="form-check-input custom-control-input" type="checkbox" id="should_not_be_frozen"
$product->should_not_be_frozen == 1) checked @endif class="form-check-input custom-control-input" type="checkbox" id="should_not_be_frozen" name="should_not_be_frozen" value="1"> name="should_not_be_frozen" value="1">
<label class="form-check-label custom-control-label" <label class="form-check-label custom-control-label"
for="should_not_be_frozen">{{ $__t('Should not be frozen') }}&nbsp;<i class="fas fa-question-circle text-muted" for="should_not_be_frozen">{{ $__t('Should not be frozen') }}&nbsp;<i
data-toggle="tooltip" class="fas fa-question-circle text-muted" data-toggle="tooltip"
data-trigger="hover click" data-trigger="hover click"
title="{{ $__t('When enabled, on moving this product to a freezer location (so when freezing it), a warning will be shown') }}"></i> title="{{ $__t('When enabled, on moving this product to a freezer location (so when freezing it), a warning will be shown') }}"></i>
</label> </label>
</div> </div>
</div> </div>
@else @else
<input type="hidden" <input type="hidden" name="default_best_before_days_after_freezing" value="0">
name="default_best_before_days_after_freezing" <input type="hidden" name="default_best_before_days_after_thawing" value="0">
value="0"> <input type="hidden" name="should_not_be_frozen" value="0">
<input type="hidden"
name="default_best_before_days_after_thawing"
value="0">
<input type="hidden"
name="should_not_be_frozen"
value="0">
@endif @endif
<div class="form-group"> <div class="form-group">
<label for="product_group_id">{{ $__t('Product group') }}</label> <label for="product_group_id">{{ $__t('Product group') }}</label>
<select class="custom-control custom-select" {{-- TODO: Select2: dynamic data: product_groups --}}
id="product_group_id" <select class="custom-control custom-select" id="product_group_id" name="product_group_id">
name="product_group_id">
<option></option> <option></option>
@foreach ($productgroups as $productgroup) @foreach ($productgroups as $productgroup)
<option @if($mode=='edit' <option @if ($mode == 'edit' && $productgroup->id == $product->product_group_id) selected="selected" @endif
&& value="{{ $productgroup->id }}">{{ $productgroup->name }}</option>
$productgroup->id == $product->product_group_id) selected="selected" @endif value="{{ $productgroup->id }}">{{ $productgroup->name }}</option>
@endforeach @endforeach
</select> </select>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="qu_id_stock">{{ $__t('Quantity unit stock') }}</label> <label for="qu_id_stock">{{ $__t('Quantity unit stock') }}</label>
<i class="fas fa-question-circle text-muted" <i class="fas fa-question-circle text-muted" data-toggle="tooltip" data-trigger="hover click"
data-toggle="tooltip"
data-trigger="hover click"
title="{{ $__t('Quantity unit stock cannot be changed after first purchase') }}"></i> title="{{ $__t('Quantity unit stock cannot be changed after first purchase') }}"></i>
<select required {{-- TODO: Select2: dynamic data: quantity_units --}}
class="custom-control custom-select input-group-qu" <select required class="custom-control custom-select input-group-qu" id="qu_id_stock" name="qu_id_stock"
id="qu_id_stock" @if ($mode == 'edit') disabled @endif>
name="qu_id_stock"
@if($mode=='edit'
)
disabled
@endif>
<option></option> <option></option>
@foreach ($quantityunits as $quantityunit) @foreach ($quantityunits as $quantityunit)
<option @if($mode=='edit' <option @if ($mode == 'edit' && $quantityunit->id == $product->qu_id_stock) selected="selected" @endif
&& value="{{ $quantityunit->id }}" data-plural-form="{{ $quantityunit->name_plural }}">
$quantityunit->id == $product->qu_id_stock) selected="selected" @endif value="{{ $quantityunit->id }}" data-plural-form="{{ $quantityunit->name_plural }}">{{ $quantityunit->name }}</option> {{ $quantityunit->name }}</option>
@endforeach @endforeach
</select> </select>
<div class="invalid-feedback">{{ $__t('A quantity unit is required') }}</div> <div class="invalid-feedback">{{ $__t('A quantity unit is required') }}</div>
@ -355,26 +350,28 @@
<div class="form-group"> <div class="form-group">
<label for="qu_id_purchase">{{ $__t('Default quantity unit purchase') }}</label> <label for="qu_id_purchase">{{ $__t('Default quantity unit purchase') }}</label>
<i class="fas fa-question-circle text-muted" <i class="fas fa-question-circle text-muted" data-toggle="tooltip" data-trigger="hover click"
data-toggle="tooltip"
data-trigger="hover click"
title="{{ $__t('This is the default quantity unit used when adding this product to the shopping list') }}"></i> title="{{ $__t('This is the default quantity unit used when adding this product to the shopping list') }}"></i>
<select required {{-- TODO: Select2: dynamic data: quantity_units --}}
class="custom-control custom-select input-group-qu" <select required class="custom-control custom-select input-group-qu" id="qu_id_purchase"
id="qu_id_purchase"
name="qu_id_purchase"> name="qu_id_purchase">
<option></option> <option></option>
@foreach ($quantityunits as $quantityunit) @foreach ($quantityunits as $quantityunit)
<option @if($mode=='edit' <option @if ($mode == 'edit' && $quantityunit->id == $product->qu_id_purchase) selected="selected" @endif
&& value="{{ $quantityunit->id }}">{{ $quantityunit->name }}</option>
$quantityunit->id == $product->qu_id_purchase) selected="selected" @endif value="{{ $quantityunit->id }}">{{ $quantityunit->name }}</option>
@endforeach @endforeach
</select> </select>
<div class="invalid-feedback">{{ $__t('A quantity unit is required') }}</div> <div class="invalid-feedback">{{ $__t('A quantity unit is required') }}</div>
</div> </div>
@php if($mode == 'edit') { $value = $product->qu_factor_purchase_to_stock; } else { $value = 1; } @endphp @php
@include('components.numberpicker', array( if ($mode == 'edit') {
$value = $product->qu_factor_purchase_to_stock;
} else {
$value = 1;
}
@endphp
@include('components.numberpicker', [
'id' => 'qu_factor_purchase_to_stock', 'id' => 'qu_factor_purchase_to_stock',
'label' => 'Factor purchase to stock quantity unit', 'label' => 'Factor purchase to stock quantity unit',
'min' => $DEFAULT_MIN_AMOUNT, 'min' => $DEFAULT_MIN_AMOUNT,
@ -382,27 +379,38 @@
'value' => $value, 'value' => $value,
'additionalCssClasses' => 'input-group-qu locale-number-input locale-number-quantity-amount', 'additionalCssClasses' => 'input-group-qu locale-number-input locale-number-quantity-amount',
'additionalHtmlElements' => '<p id="qu-conversion-info" 'additionalHtmlElements' => '<p id="qu-conversion-info"
class="form-text text-info d-none"></p>' class="form-text text-info d-none"></p>',
)) ])
<div class="form-group mb-1"> <div class="form-group mb-1">
<div class="custom-control custom-checkbox"> <div class="custom-control custom-checkbox">
<input @if($mode=='edit' <input @if ($mode == 'edit' && $product->enable_tare_weight_handling == 1) checked @endif
&& class="form-check-input custom-control-input" type="checkbox" id="enable_tare_weight_handling"
$product->enable_tare_weight_handling == 1) checked @endif class="form-check-input custom-control-input" type="checkbox" id="enable_tare_weight_handling" name="enable_tare_weight_handling" value="1"> name="enable_tare_weight_handling" value="1">
<label class="form-check-label custom-control-label" <label class="form-check-label custom-control-label"
for="enable_tare_weight_handling">{{ $__t('Enable tare weight handling') }} for="enable_tare_weight_handling">{{ $__t('Enable tare weight handling') }}
&nbsp;<i class="fas fa-question-circle text-muted" &nbsp;<i class="fas fa-question-circle text-muted" data-toggle="tooltip"
data-toggle="tooltip"
data-trigger="hover click" data-trigger="hover click"
title="{{ $__t('This is useful e.g. for flour in jars - on purchase/consume/inventory you always weigh the whole jar, the amount to be posted is then automatically calculated based on what is in stock and the tare weight defined below') }}"></i> title="{{ $__t('This is useful e.g. for flour in jars - on purchase/consume/inventory you always weigh the whole jar, the amount to be posted is then automatically calculated based on what is in stock and the tare weight defined below') }}"></i>
</label> </label>
</div> </div>
</div> </div>
@php if($mode == 'edit') { $value = $product->tare_weight; } else { $value = 0; } @endphp @php
@php if(($mode == 'edit' && $product->enable_tare_weight_handling == 0) || $mode == 'create') { $additionalAttributes = 'disabled'; } else { $additionalAttributes = ''; } @endphp if ($mode == 'edit') {
@include('components.numberpicker', array( $value = $product->tare_weight;
} else {
$value = 0;
}
@endphp
@php
if (($mode == 'edit' && $product->enable_tare_weight_handling == 0) || $mode == 'create') {
$additionalAttributes = 'disabled';
} else {
$additionalAttributes = '';
}
@endphp
@include('components.numberpicker', [
'id' => 'tare_weight', 'id' => 'tare_weight',
'label' => 'Tare weight', 'label' => 'Tare weight',
'min' => 0, 'min' => 0,
@ -410,34 +418,38 @@
'value' => $value, 'value' => $value,
'additionalAttributes' => $additionalAttributes, 'additionalAttributes' => $additionalAttributes,
'contextInfoId' => 'tare_weight_qu_info', 'contextInfoId' => 'tare_weight_qu_info',
'additionalCssClasses' => 'locale-number-input locale-number-quantity-amount' 'additionalCssClasses' => 'locale-number-input locale-number-quantity-amount',
)) ])
@php $additionalAttributes = '' @endphp @php $additionalAttributes = '' @endphp
@if (GROCY_FEATURE_FLAG_RECIPES) @if (GROCY_FEATURE_FLAG_RECIPES)
<div class="form-group"> <div class="form-group">
<div class="custom-control custom-checkbox"> <div class="custom-control custom-checkbox">
<input @if($mode=='edit' <input @if ($mode == 'edit' && $product->not_check_stock_fulfillment_for_recipes == 1) checked @endif
&& class="form-check-input custom-control-input" type="checkbox"
$product->not_check_stock_fulfillment_for_recipes == 1) checked @endif class="form-check-input custom-control-input" type="checkbox" id="not_check_stock_fulfillment_for_recipes" name="not_check_stock_fulfillment_for_recipes" value="1"> id="not_check_stock_fulfillment_for_recipes" name="not_check_stock_fulfillment_for_recipes"
value="1">
<label class="form-check-label custom-control-label" <label class="form-check-label custom-control-label"
for="not_check_stock_fulfillment_for_recipes">{{ $__t('Disable stock fulfillment checking for this ingredient') }} for="not_check_stock_fulfillment_for_recipes">{{ $__t('Disable stock fulfillment checking for this ingredient') }}
&nbsp;<i class="fas fa-question-circle text-muted" &nbsp;<i class="fas fa-question-circle text-muted" data-toggle="tooltip"
data-toggle="tooltip"
data-trigger="hover click" data-trigger="hover click"
title="{{ $__t('This will be used as the default setting when adding this product as a recipe ingredient') }}"></i> title="{{ $__t('This will be used as the default setting when adding this product as a recipe ingredient') }}"></i>
</label> </label>
</div> </div>
</div> </div>
@else @else
<input type="hidden" <input type="hidden" name="not_check_stock_fulfillment_for_recipes"
name="not_check_stock_fulfillment_for_recipes" id="not_check_stock_fulfillment_for_recipes" value="0">
id="not_check_stock_fulfillment_for_recipes"
value="0">
@endif @endif
@php if($mode == 'edit') { $value = $product->calories; } else { $value = 0; } @endphp @php
@include('components.numberpicker', array( if ($mode == 'edit') {
$value = $product->calories;
} else {
$value = 0;
}
@endphp
@include('components.numberpicker', [
'id' => 'calories', 'id' => 'calories',
'label' => 'Energy (kcal)', 'label' => 'Energy (kcal)',
'min' => '0.' . str_repeat('0', $userSettings['stock_decimal_places_amounts']), 'min' => '0.' . str_repeat('0', $userSettings['stock_decimal_places_amounts']),
@ -446,58 +458,58 @@
'hint' => $__t('Per stock quantity unit'), 'hint' => $__t('Per stock quantity unit'),
'contextInfoId' => 'energy_qu_info', 'contextInfoId' => 'energy_qu_info',
'isRequired' => false, 'isRequired' => false,
'additionalCssClasses' => 'locale-number-input locale-number-quantity-amount' 'additionalCssClasses' => 'locale-number-input locale-number-quantity-amount',
)) ])
@php if($mode == 'edit') { $value = $product->quick_consume_amount; } else { $value = 1; } @endphp @php
@include('components.numberpicker', array( if ($mode == 'edit') {
$value = $product->quick_consume_amount;
} else {
$value = 1;
}
@endphp
@include('components.numberpicker', [
'id' => 'quick_consume_amount', 'id' => 'quick_consume_amount',
'label' => 'Quick consume amount', 'label' => 'Quick consume amount',
'min' => $DEFAULT_MIN_AMOUNT, 'min' => $DEFAULT_MIN_AMOUNT,
'decimals' => $userSettings['stock_decimal_places_amounts'], 'decimals' => $userSettings['stock_decimal_places_amounts'],
'value' => $value, 'value' => $value,
'hint' => $__t('This amount is used for the "quick consume/open buttons" on the stock overview page (related to quantity unit stock)'), 'hint' => $__t(
'This amount is used for the "quick consume/open buttons" on the stock overview page (related to quantity unit stock)'
),
'contextInfoId' => 'quick_consume_qu_info', 'contextInfoId' => 'quick_consume_qu_info',
'additionalCssClasses' => 'locale-number-input locale-number-quantity-amount' 'additionalCssClasses' => 'locale-number-input locale-number-quantity-amount',
)) ])
@if (GROCY_FEATURE_FLAG_LABEL_PRINTER) @if (GROCY_FEATURE_FLAG_LABEL_PRINTER)
<div class="form-group"> <div class="form-group">
<label for="default_stock_label_type">{{ $__t('Default stock entry label') }}</label> <label for="default_stock_label_type">{{ $__t('Default stock entry label') }}</label>
<i class="fas fa-question-circle text-muted" <i class="fas fa-question-circle text-muted" data-toggle="tooltip" data-trigger="hover click"
data-toggle="tooltip"
data-trigger="hover click"
title="{{ $__t('This is the default which will be prefilled on purchase') }}"></i> title="{{ $__t('This is the default which will be prefilled on purchase') }}"></i>
<select class="form-control" <select class="form-control" id="default_stock_label_type" name="default_stock_label_type">
id="default_stock_label_type" <option @if ($mode == 'edit' && intval($product->default_stock_label_type) == 0) selected="selected" @endif value="0">
name="default_stock_label_type"> {{ $__t('No label') }}</option>
<option @if($mode=='edit' <option @if ($mode == 'edit' && intval($product->default_stock_label_type) == 1) selected="selected" @endif value="1">
&& {{ $__t('Single label') }}</option>
intval($product->default_stock_label_type) == 0 ) selected="selected" @endif value="0">{{ $__t('No label') }}</option> <option @if ($mode == 'edit' && intval($product->default_stock_label_type) == 2) selected="selected" @endif value="2">
<option @if($mode=='edit' {{ $__t('Label per unit') }}</option>
&&
intval($product->default_stock_label_type) == 1 ) selected="selected" @endif value="1">{{ $__t('Single label') }}</option>
<option @if($mode=='edit'
&&
intval($product->default_stock_label_type) == 2 ) selected="selected" @endif value="2">{{ $__t('Label per unit') }}</option>
</select> </select>
</div> </div>
@endif @endif
@include('components.userfieldsform', array( @include('components.userfieldsform', [
'userfields' => $userfields, 'userfields' => $userfields,
'entity' => 'products' 'entity' => 'products',
)) ])
<div class="form-group"> <div class="form-group">
<div class="custom-control custom-checkbox"> <div class="custom-control custom-checkbox">
<input @if($mode=='edit' <input @if ($mode == 'edit' && $product->hide_on_stock_overview == 1) checked @endif
&& class="form-check-input custom-control-input" type="checkbox" id="hide_on_stock_overview"
$product->hide_on_stock_overview == 1) checked @endif class="form-check-input custom-control-input" type="checkbox" id="hide_on_stock_overview" name="hide_on_stock_overview" value="1"> name="hide_on_stock_overview" value="1">
<label class="form-check-label custom-control-label" <label class="form-check-label custom-control-label"
for="hide_on_stock_overview">{{ $__t('Never show on stock overview') }}&nbsp;<i class="fas fa-question-circle text-muted" for="hide_on_stock_overview">{{ $__t('Never show on stock overview') }}&nbsp;<i
data-toggle="tooltip" class="fas fa-question-circle text-muted" data-toggle="tooltip" data-trigger="hover click"
data-trigger="hover click"
title="{{ $__t('The stock overview page lists all products which are currently in-stock or below their min. stock amount - enable this to hide this product there always') }}"></i> title="{{ $__t('The stock overview page lists all products which are currently in-stock or below their min. stock amount - enable this to hide this product there always') }}"></i>
</label> </label>
</div> </div>
@ -507,8 +519,7 @@
<small id="save-hint" <small id="save-hint"
class="my-1 form-text text-muted @if ($mode == 'edit') d-none @endif">{{ $__t('Save & continue to add quantity unit conversions & barcodes') }}</small> class="my-1 form-text text-muted @if ($mode == 'edit') d-none @endif">{{ $__t('Save & continue to add quantity unit conversions & barcodes') }}</small>
<button id="save-product-button" <button id="save-product-button" class="save-product-button btn btn-success mb-2 default-submit-button"
class="save-product-button btn btn-success mb-2 default-submit-button"
data-location="continue">{{ $__t('Save & continue') }}</button> data-location="continue">{{ $__t('Save & continue') }}</button>
<button class="save-product-button btn btn-info mb-2" <button class="save-product-button btn btn-info mb-2"
data-location="return">{{ $__t('Save & return to products') }}</button> data-location="return">{{ $__t('Save & return to products') }}</button>
@ -525,15 +536,12 @@
<h4> <h4>
{{ $__t('Barcodes') }} {{ $__t('Barcodes') }}
</h4> </h4>
<button class="btn btn-outline-dark d-md-none mt-2 float-right order-1 order-md-3" <button class="btn btn-outline-dark d-md-none mt-2 float-right order-1 order-md-3" type="button"
type="button" data-toggle="collapse" data-target="#related-links">
data-toggle="collapse"
data-target="#related-links">
<i class="fas fa-ellipsis-v"></i> <i class="fas fa-ellipsis-v"></i>
</button> </button>
@if($mode == "edit") @if ($mode == 'edit')
<div class="related-links collapse d-md-flex order-2 width-xs-sm-100" <div class="related-links collapse d-md-flex order-2 width-xs-sm-100" id="related-links">
id="related-links">
<a class="btn btn-outline-primary btn-sm m-1 mt-md-0 mb-md-0 float-right show-as-dialog-link" <a class="btn btn-outline-primary btn-sm m-1 mt-md-0 mb-md-0 float-right show-as-dialog-link"
href="{{ $U('/productbarcodes/new?embedded&product=' . $product->id) }}"> href="{{ $U('/productbarcodes/new?embedded&product=' . $product->id) }}">
{{ $__t('Add') }} {{ $__t('Add') }}
@ -542,34 +550,32 @@
@endif @endif
</div> </div>
<h5 id="barcode-headline-info" <h5 id="barcode-headline-info" class="text-muted font-italic"></h5>
class="text-muted font-italic"></h5>
<table id="barcode-table" {{-- TODO: DataTables: dynamic data: barcodes --}}
class="table table-sm table-striped nowrap w-100"> <table id="barcode-table" class="table table-sm table-striped nowrap w-100">
<thead> <thead>
<tr> <tr>
<th class="border-right"><a class="text-muted change-table-columns-visibility-button" <th class="border-right"><a class="text-muted change-table-columns-visibility-button"
data-toggle="tooltip" data-toggle="tooltip" data-toggle="tooltip" title="{{ $__t('Table options') }}"
data-toggle="tooltip" data-table-selector="#barcode-table" href="#"><i class="fas fa-eye"></i></a>
title="{{ $__t('Table options') }}"
data-table-selector="#barcode-table"
href="#"><i class="fas fa-eye"></i></a>
</th> </th>
<th>{{ $__t('Barcode') }}</th> <th>{{ $__t('Barcode') }}</th>
<th class="@if(!GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) d-none @endif allow-grouping">{{ $__t('Store') }}</th> <th class="@if (!GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) d-none @endif allow-grouping">
{{ $__t('Store') }}</th>
<th class="allow-grouping">{{ $__t('Quantity unit') }}</th> <th class="allow-grouping">{{ $__t('Quantity unit') }}</th>
<th>{{ $__t('Amount') }}</th> <th>{{ $__t('Amount') }}</th>
<th class="@if(!GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) d-none @endif">{{ $__t('Last price') }}</th> <th class="@if (!GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) d-none @endif">{{ $__t('Last price') }}
</th>
<th>{{ $__t('Note') }}</th> <th>{{ $__t('Note') }}</th>
@include('components.userfields_thead', array( @include('components.userfields_thead', [
'userfields' => $productBarcodeUserfields 'userfields' => $productBarcodeUserfields,
)) ])
</tr> </tr>
</thead> </thead>
<tbody class="d-none"> <tbody class="d-none">
@if($mode == "edit") @if ($mode == 'edit')
@foreach ($barcodes as $barcode) @foreach ($barcodes as $barcode)
@if ($barcode->product_id == $product->id || $barcode->product_id == null) @if ($barcode->product_id == $product->id || $barcode->product_id == null)
<tr> <tr>
@ -579,8 +585,7 @@
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
</a> </a>
<a class="btn btn-sm btn-danger barcode-delete-button @if ($barcode->product_id == null) disabled @endif" <a class="btn btn-sm btn-danger barcode-delete-button @if ($barcode->product_id == null) disabled @endif"
href="#" href="#" data-barcode-id="{{ $barcode->id }}"
data-barcode-id="{{ $barcode->id }}"
data-barcode="{{ $barcode->barcode }}" data-barcode="{{ $barcode->barcode }}"
data-product-barcode="{{ $product->barcode }}" data-product-barcode="{{ $product->barcode }}"
data-product-id="{{ $product->id }}"> data-product-id="{{ $product->id }}">
@ -603,20 +608,26 @@
</td> </td>
<td> <td>
@if (!empty($barcode->amount)) @if (!empty($barcode->amount))
<span class="locale-number locale-number-quantity-amount">{{ $barcode->amount }}</span> <span
class="locale-number locale-number-quantity-amount">{{ $barcode->amount }}</span>
@endif @endif
</td> </td>
<td class="@if (!GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) d-none @endif"> <td class="@if (!GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) d-none @endif">
<span class="locale-number locale-number-currency">{{ $barcode->last_price }}</span> <span
class="locale-number locale-number-currency">{{ $barcode->last_price }}</span>
</td> </td>
<td> <td>
{{ $barcode->note }} {{ $barcode->note }}
</td> </td>
@include('components.userfields_tbody', array( @include('components.userfields_tbody', [
'userfields' => $productBarcodeUserfields, 'userfields' => $productBarcodeUserfields,
'userfieldValues' => FindAllObjectsInArrayByPropertyValue($productBarcodeUserfieldValues, 'object_id', $barcode->id) 'userfieldValues' => FindAllObjectsInArrayByPropertyValue(
)) $productBarcodeUserfieldValues,
'object_id',
$barcode->id
),
])
</tr> </tr>
@endif @endif
@endforeach @endforeach
@ -631,9 +642,7 @@
<div class="title-related-links"> <div class="title-related-links">
<h4> <h4>
<span class="ls-n1">{{ $__t('grocycode') }}</span> <span class="ls-n1">{{ $__t('grocycode') }}</span>
<i class="fas fa-question-circle text-muted" <i class="fas fa-question-circle text-muted" data-toggle="tooltip" data-trigger="hover click"
data-toggle="tooltip"
data-trigger="hover click"
title="{{ $__t('grocycode is a unique referer to this %s in your grocy instance - print it onto a label and scan it like any other barcode',$__t('Product')) }}"></i> title="{{ $__t('grocycode is a unique referer to this %s in your grocy instance - print it onto a label and scan it like any other barcode',$__t('Product')) }}"></i>
</h4> </h4>
<p> <p>
@ -647,8 +656,7 @@
href="{{ $U('/product/' . $product->id . '/grocycode?download=true') }}">{{ $__t('Download') }}</a> href="{{ $U('/product/' . $product->id . '/grocycode?download=true') }}">{{ $__t('Download') }}</a>
@if (GROCY_FEATURE_FLAG_LABEL_PRINTER) @if (GROCY_FEATURE_FLAG_LABEL_PRINTER)
<a class="btn btn-outline-primary btn-sm product-grocycode-label-print" <a class="btn btn-outline-primary btn-sm product-grocycode-label-print"
data-product-id="{{ $product->id }}" data-product-id="{{ $product->id }}" href="#">
href="#">
{{ $__t('Print on label printer') }} {{ $__t('Print on label printer') }}
</a> </a>
@endif @endif
@ -657,21 +665,19 @@
</div> </div>
</div> </div>
<div class="row @if(GROCY_FEATURE_FLAG_STOCK) mt-5 @endif @if($mode == 'create') d-none @endif"> <div
class="row @if (GROCY_FEATURE_FLAG_STOCK) mt-5 @endif @if ($mode == 'create') d-none @endif">
<div class="col"> <div class="col">
<div class="title-related-links"> <div class="title-related-links">
<h4> <h4>
{{ $__t('QU conversions') }} {{ $__t('QU conversions') }}
</h4> </h4>
<button class="btn btn-outline-dark d-md-none mt-2 float-right order-1 order-md-3" <button class="btn btn-outline-dark d-md-none mt-2 float-right order-1 order-md-3" type="button"
type="button" data-toggle="collapse" data-target="#related-links">
data-toggle="collapse"
data-target="#related-links">
<i class="fas fa-ellipsis-v"></i> <i class="fas fa-ellipsis-v"></i>
</button> </button>
@if($mode == "edit") @if ($mode == 'edit')
<div class="related-links collapse d-md-flex order-2 width-xs-sm-100" <div class="related-links collapse d-md-flex order-2 width-xs-sm-100" id="related-links">
id="related-links">
<a class="btn btn-outline-primary btn-sm m-1 mt-md-0 mb-md-0 float-right show-as-dialog-link" <a class="btn btn-outline-primary btn-sm m-1 mt-md-0 mb-md-0 float-right show-as-dialog-link"
href="{{ $U('/quantityunitconversion/new?embedded&product=' . $product->id) }}"> href="{{ $U('/quantityunitconversion/new?embedded&product=' . $product->id) }}">
{{ $__t('Add') }} {{ $__t('Add') }}
@ -680,18 +686,16 @@
@endif @endif
</div> </div>
<h5 id="qu-conversion-headline-info" <h5 id="qu-conversion-headline-info" class="text-muted font-italic"></h5>
class="text-muted font-italic"></h5>
<table id="qu-conversions-table-products" {{-- TODO: DataTables: dynamic data: quantity_unit_conversions --}}
class="table table-sm table-striped nowrap w-100"> <table id="qu-conversions-table-products" class="table table-sm table-striped nowrap w-100">
<thead> <thead>
<tr> <tr>
<th class="border-right"><a class="text-muted change-table-columns-visibility-button" <th class="border-right"><a class="text-muted change-table-columns-visibility-button"
data-toggle="tooltip" data-toggle="tooltip" title="{{ $__t('Table options') }}"
title="{{ $__t('Table options') }}" data-table-selector="#qu-conversions-table-products" href="#"><i
data-table-selector="#qu-conversions-table-products" class="fas fa-eye"></i></a>
href="#"><i class="fas fa-eye"></i></a>
</th> </th>
<th class="allow-grouping">{{ $__t('Quantity unit from') }}</th> <th class="allow-grouping">{{ $__t('Quantity unit from') }}</th>
<th class="allow-grouping">{{ $__t('Quantity unit to') }}</th> <th class="allow-grouping">{{ $__t('Quantity unit to') }}</th>
@ -701,9 +705,9 @@
</tr> </tr>
</thead> </thead>
<tbody class="d-none"> <tbody class="d-none">
@if($mode == "edit") @if ($mode == 'edit')
@foreach ($quConversions as $quConversion) @foreach ($quConversions as $quConversion)
@if($quConversion->product_id == $product->id || $quConversion->product_id == null && ($quConversion->product_id != null || $quConversion->from_qu_id == $product->qu_id_purchase || $quConversion->from_qu_id == $product->qu_id_stock || $quConversion->to_qu_id == $product->qu_id_purchase || $quConversion->to_qu_id == $product->qu_id_stock)) @if ($quConversion->product_id == $product->id || ($quConversion->product_id == null && ($quConversion->product_id != null || $quConversion->from_qu_id == $product->qu_id_purchase || $quConversion->from_qu_id == $product->qu_id_stock || $quConversion->to_qu_id == $product->qu_id_purchase || $quConversion->to_qu_id == $product->qu_id_stock)))
<tr> <tr>
<td class="fit-content border-right"> <td class="fit-content border-right">
<a class="btn btn-sm btn-info show-as-dialog-link @if ($quConversion->product_id == null) disabled @endif" <a class="btn btn-sm btn-info show-as-dialog-link @if ($quConversion->product_id == null) disabled @endif"
@ -711,8 +715,7 @@
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
</a> </a>
<a class="btn btn-sm btn-danger qu-conversion-delete-button @if ($quConversion->product_id == null) disabled @endif" <a class="btn btn-sm btn-danger qu-conversion-delete-button @if ($quConversion->product_id == null) disabled @endif"
href="#" href="#" data-qu-conversion-id="{{ $quConversion->id }}">
data-qu-conversion-id="{{ $quConversion->id }}">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
</a> </a>
</td> </td>
@ -723,7 +726,8 @@
{{ FindObjectInArrayByPropertyValue($quantityunits, 'id', $quConversion->to_qu_id)->name }} {{ FindObjectInArrayByPropertyValue($quantityunits, 'id', $quConversion->to_qu_id)->name }}
</td> </td>
<td> <td>
<span class="locale-number locale-number-quantity-amount">{{ $quConversion->factor }}</span> <span
class="locale-number locale-number-quantity-amount">{{ $quConversion->factor }}</span>
</td> </td>
<td class="d-none"> <td class="d-none">
@if ($quConversion->product_id != null) @if ($quConversion->product_id != null)
@ -753,10 +757,7 @@
<div class="form-group w-75 m-0"> <div class="form-group w-75 m-0">
<div class="input-group"> <div class="input-group">
<div class="custom-file"> <div class="custom-file">
<input type="file" <input type="file" class="custom-file-input" id="product-picture" accept="image/*">
class="custom-file-input"
id="product-picture"
accept="image/*">
<label id="product-picture-label" <label id="product-picture-label"
class="custom-file-label @if (empty($product->picture_file_name)) d-none @endif" class="custom-file-label @if (empty($product->picture_file_name)) d-none @endif"
for="product-picture"> for="product-picture">
@ -775,15 +776,16 @@
</div> </div>
</div> </div>
</div> </div>
@if($mode == "edit" && !empty($product->picture_file_name)) @if ($mode == 'edit' && !empty($product->picture_file_name))
<img id="current-product-picture" <img id="current-product-picture"
data-src="{{ $U('/api/files/productpictures/' .base64_encode($product->picture_file_name) .'?force_serve_as=picture&best_fit_width=400') }}" data-src="{{ $U('/api/files/productpictures/' .base64_encode($product->picture_file_name) .'?force_serve_as=picture&best_fit_width=400') }}"
class="img-fluid img-thumbnail mt-2 lazy mb-5"> class="img-fluid img-thumbnail mt-2 lazy mb-5">
<p id="delete-current-product-picture-on-save-hint" <p id="delete-current-product-picture-on-save-hint"
class="form-text text-muted font-italic d-none mb-5">{{ $__t('The current picture will be deleted on save') }}</p> class="form-text text-muted font-italic d-none mb-5">
{{ $__t('The current picture will be deleted on save') }}</p>
@else @else
<p id="no-current-product-picture-hint" <p id="no-current-product-picture-hint" class="form-text text-muted font-italic mb-5">
class="form-text text-muted font-italic mb-5">{{ $__t('No picture available') }}</p> {{ $__t('No picture available') }}</p>
@endif @endif
</div> </div>
</div> </div>

View File

@ -10,21 +10,16 @@
<div class="title-related-links"> <div class="title-related-links">
<h2 class="title">@yield('title')</h2> <h2 class="title">@yield('title')</h2>
<div class="float-right"> <div class="float-right">
<button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" <button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" type="button"
type="button" data-toggle="collapse" data-target="#table-filter-row">
data-toggle="collapse"
data-target="#table-filter-row">
<i class="fas fa-filter"></i> <i class="fas fa-filter"></i>
</button> </button>
<button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" <button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" type="button"
type="button" data-toggle="collapse" data-target="#related-links">
data-toggle="collapse"
data-target="#related-links">
<i class="fas fa-ellipsis-v"></i> <i class="fas fa-ellipsis-v"></i>
</button> </button>
</div> </div>
<div class="related-links collapse d-md-flex order-2 width-xs-sm-100" <div class="related-links collapse d-md-flex order-2 width-xs-sm-100" id="related-links">
id="related-links">
<a class="btn btn-primary responsive-button show-as-dialog-link m-1 mt-md-0 mb-md-0 float-right" <a class="btn btn-primary responsive-button show-as-dialog-link m-1 mt-md-0 mb-md-0 float-right"
href="{{ $U('/productgroup/new?embedded') }}"> href="{{ $U('/productgroup/new?embedded') }}">
{{ $__t('Add') }} {{ $__t('Add') }}
@ -40,24 +35,18 @@
<hr class="my-2"> <hr class="my-2">
<div class="row collapse d-md-flex" <div class="row collapse d-md-flex" id="table-filter-row">
id="table-filter-row">
<div class="col-12 col-md-6 col-xl-3"> <div class="col-12 col-md-6 col-xl-3">
<div class="input-group mb-3"> <div class="input-group mb-3">
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-search"></i></span> <span class="input-group-text"><i class="fas fa-search"></i></span>
</div> </div>
<input type="text" <input type="text" id="search" class="form-control" placeholder="{{ $__t('Search') }}">
id="search"
class="form-control"
placeholder="{{ $__t('Search') }}">
</div> </div>
</div> </div>
<div class="col"> <div class="col">
<div class="float-right"> <div class="float-right">
<a id="clear-filter-button" <a id="clear-filter-button" class="btn btn-sm btn-outline-info" href="#">
class="btn btn-sm btn-outline-info"
href="#">
{{ $__t('Clear filter') }} {{ $__t('Clear filter') }}
</a> </a>
</div> </div>
@ -66,24 +55,21 @@
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<table id="productgroups-table" {{-- TODO: DataTables: dynamic data: product_groups --}}
class="table table-sm table-striped nowrap w-100"> <table id="productgroups-table" class="table table-sm table-striped nowrap w-100">
<thead> <thead>
<tr> <tr>
<th class="border-right"><a class="text-muted change-table-columns-visibility-button" <th class="border-right"><a class="text-muted change-table-columns-visibility-button"
data-toggle="tooltip" data-toggle="tooltip" data-toggle="tooltip" title="{{ $__t('Table options') }}"
data-toggle="tooltip" data-table-selector="#productgroups-table" href="#"><i class="fas fa-eye"></i></a>
title="{{ $__t('Table options') }}"
data-table-selector="#productgroups-table"
href="#"><i class="fas fa-eye"></i></a>
</th> </th>
<th>{{ $__t('Name') }}</th> <th>{{ $__t('Name') }}</th>
<th>{{ $__t('Description') }}</th> <th>{{ $__t('Description') }}</th>
<th>{{ $__t('Product count') }}</th> <th>{{ $__t('Product count') }}</th>
@include('components.userfields_thead', array( @include('components.userfields_thead', [
'userfields' => $userfields 'userfields' => $userfields,
)) ])
</tr> </tr>
</thead> </thead>
<tbody class="d-none"> <tbody class="d-none">
@ -92,15 +78,12 @@
<td class="fit-content border-right"> <td class="fit-content border-right">
<a class="btn btn-info btn-sm show-as-dialog-link" <a class="btn btn-info btn-sm show-as-dialog-link"
href="{{ $U('/productgroup/') }}{{ $productGroup->id }}?embedded" href="{{ $U('/productgroup/') }}{{ $productGroup->id }}?embedded"
data-toggle="tooltip" data-toggle="tooltip" title="{{ $__t('Edit this item') }}">
title="{{ $__t('Edit this item') }}">
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
</a> </a>
<a class="btn btn-danger btn-sm product-group-delete-button" <a class="btn btn-danger btn-sm product-group-delete-button" href="#"
href="#"
data-group-id="{{ $productGroup->id }}" data-group-id="{{ $productGroup->id }}"
data-group-name="{{ $productGroup->name }}" data-group-name="{{ $productGroup->name }}" data-toggle="tooltip"
data-toggle="tooltip"
title="{{ $__t('Delete this item') }}"> title="{{ $__t('Delete this item') }}">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
</a> </a>
@ -119,10 +102,14 @@
</a> </a>
</td> </td>
@include('components.userfields_tbody', array( @include('components.userfields_tbody', [
'userfields' => $userfields, 'userfields' => $userfields,
'userfieldValues' => FindAllObjectsInArrayByPropertyValue($userfieldValues, 'object_id', $productGroup->id) 'userfieldValues' => FindAllObjectsInArrayByPropertyValue(
)) $userfieldValues,
'object_id',
$productGroup->id
),
])
</tr> </tr>
@endforeach @endforeach

View File

@ -10,21 +10,16 @@
<div class="title-related-links"> <div class="title-related-links">
<h2 class="title">@yield('title')</h2> <h2 class="title">@yield('title')</h2>
<div class="float-right"> <div class="float-right">
<button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" <button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" type="button"
type="button" data-toggle="collapse" data-target="#table-filter-row">
data-toggle="collapse"
data-target="#table-filter-row">
<i class="fas fa-filter"></i> <i class="fas fa-filter"></i>
</button> </button>
<button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" <button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" type="button"
type="button" data-toggle="collapse" data-target="#related-links">
data-toggle="collapse"
data-target="#related-links">
<i class="fas fa-ellipsis-v"></i> <i class="fas fa-ellipsis-v"></i>
</button> </button>
</div> </div>
<div class="related-links collapse d-md-flex order-2 width-xs-sm-100" <div class="related-links collapse d-md-flex order-2 width-xs-sm-100" id="related-links">
id="related-links">
<a class="btn btn-primary responsive-button m-1 mt-md-0 mb-md-0 float-right" <a class="btn btn-primary responsive-button m-1 mt-md-0 mb-md-0 float-right"
href="{{ $U('/product/new') }}"> href="{{ $U('/product/new') }}">
{{ $__t('Add') }} {{ $__t('Add') }}
@ -44,17 +39,13 @@
<hr class="my-2"> <hr class="my-2">
<div class="row collapse d-md-flex" <div class="row collapse d-md-flex mb-3" id="table-filter-row">
id="table-filter-row">
<div class="col-12 col-md-6 col-xl-3"> <div class="col-12 col-md-6 col-xl-3">
<div class="input-group"> <div class="input-group">
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-search"></i></span> <span class="input-group-text"><i class="fas fa-search"></i></span>
</div> </div>
<input type="text" <input type="text" id="search" class="form-control" placeholder="{{ $__t('Search') }}">
id="search"
class="form-control"
placeholder="{{ $__t('Search') }}">
</div> </div>
</div> </div>
<div class="col-12 col-md-6 col-xl-3"> <div class="col-12 col-md-6 col-xl-3">
@ -62,8 +53,8 @@
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-filter"></i>&nbsp;{{ $__t('Product group') }}</span> <span class="input-group-text"><i class="fas fa-filter"></i>&nbsp;{{ $__t('Product group') }}</span>
</div> </div>
<select class="custom-control custom-select" {{-- TODO: Select2: dynamic data: product_groups --}}
id="product-group-filter"> <select class="custom-control custom-select" id="product-group-filter">
<option value="all">{{ $__t('All') }}</option> <option value="all">{{ $__t('All') }}</option>
@foreach ($productGroups as $productGroup) @foreach ($productGroups as $productGroup)
<option value="{{ $productGroup->id }}">{{ $productGroup->name }}</option> <option value="{{ $productGroup->id }}">{{ $productGroup->name }}</option>
@ -73,31 +64,23 @@
</div> </div>
<div class="col-12 col-md-6 col-xl-2"> <div class="col-12 col-md-6 col-xl-2">
<div class="form-check custom-control custom-checkbox"> <div class="form-check custom-control custom-checkbox">
<input class="form-check-input custom-control-input" <input class="form-check-input custom-control-input" type="checkbox" id="show-disabled">
type="checkbox" <label class="form-check-label custom-control-label" for="show-disabled">
id="show-disabled">
<label class="form-check-label custom-control-label"
for="show-disabled">
{{ $__t('Show disabled') }} {{ $__t('Show disabled') }}
</label> </label>
</div> </div>
</div> </div>
<div class="col-12 col-md-6 col-xl-2"> <div class="col-12 col-md-6 col-xl-2">
<div class="form-check custom-control custom-checkbox"> <div class="form-check custom-control custom-checkbox">
<input class="form-check-input custom-control-input" <input class="form-check-input custom-control-input" type="checkbox" id="show-only-in-stock">
type="checkbox" <label class="form-check-label custom-control-label" for="show-only-in-stock">
id="show-only-in-stock">
<label class="form-check-label custom-control-label"
for="show-only-in-stock">
{{ $__t('Show only in-stock products') }} {{ $__t('Show only in-stock products') }}
</label> </label>
</div> </div>
</div> </div>
<div class="col"> <div class="col">
<div class="float-right"> <div class="float-right">
<a id="clear-filter-button" <a id="clear-filter-button" class="btn btn-sm btn-outline-info" href="#">
class="btn btn-sm btn-outline-info"
href="#">
{{ $__t('Clear filter') }} {{ $__t('Clear filter') }}
</a> </a>
</div> </div>
@ -106,122 +89,33 @@
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<table id="products-table" <table id="products-table" class="table table-sm table-striped nowrap w-100">
class="table table-sm table-striped nowrap w-100">
<thead> <thead>
<tr> <tr>
<th class="border-right"><a class="text-muted change-table-columns-visibility-button" <th class="border-right"><a class="text-muted change-table-columns-visibility-button"
data-toggle="tooltip" data-toggle="tooltip" data-toggle="tooltip" title="{{ $__t('Table options') }}"
data-toggle="tooltip" data-table-selector="#products-table" href="#"><i class="fas fa-eye"></i></a>
title="{{ $__t('Table options') }}"
data-table-selector="#products-table"
href="#"><i class="fas fa-eye"></i></a>
</th> </th>
<th>{{ $__t('Name') }}</th> <th>{{ $__t('Name') }}</th>
<th class="@if(!GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING) d-none @endif allow-grouping">{{ $__t('Location') }}</th> <th class="@if (!GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING) d-none @endif allow-grouping">{{ $__t('Location') }}
</th>
<th class="allow-grouping">{{ $__t('Min. stock amount') }}</th> <th class="allow-grouping">{{ $__t('Min. stock amount') }}</th>
<th class="">{{ $__t('Default quantity unit purchase') }}</th> <th class="">{{ $__t('Default quantity unit purchase') }}</th>
<th class="allow-grouping">{{ $__t('Quantity unit stock') }}</th> <th class="allow-grouping">{{ $__t('Quantity unit stock') }}</th>
<th class="">{{ $__t('Product group') }}</th> <th class="">{{ $__t('Product group') }}</th>
<th class="@if(!GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) d-none @endif allow-grouping">{{ $__t('Default store') }}</th> <th class="@if (!GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) d-none @endif allow-grouping">
{{ $__t('Default store') }}</th>
@include('components.userfields_thead', array( @include('components.userfields_thead', [
'userfields' => $userfields 'userfields' => $userfields,
)) ])
</tr> </tr>
</thead> </thead>
<tbody class="d-none"> <tbody></tbody>
@foreach($products as $product)
<tr class="@if($product->active == 0) text-muted @endif">
<td class="fit-content border-right">
<a class="btn btn-info btn-sm"
href="{{ $U('/product/') }}{{ $product->id }}"
data-toggle="tooltip"
title="{{ $__t('Edit this item') }}">
<i class="fas fa-edit"></i>
</a>
<a class="btn btn-danger btn-sm product-delete-button"
href="#"
data-product-id="{{ $product->id }}"
data-product-name="{{ $product->name }}"
data-toggle="tooltip"
title="{{ $__t('Delete this item') }}">
<i class="fas fa-trash"></i>
</a>
<div class="dropdown d-inline-block">
<button class="btn btn-sm btn-light text-secondary"
type="button"
data-toggle="dropdown">
<i class="fas fa-ellipsis-v"></i>
</button>
<div class="table-inline-menu dropdown-menu dropdown-menu-right">
<a class="dropdown-item"
type="button"
href="{{ $U('/product/new?copy-of=') }}{{ $product->id }}">
<span class="dropdown-item-text">{{ $__t('Copy') }}</span>
</a>
<a class="dropdown-item merge-products-button"
data-product-id="{{ $product->id }}"
type="button"
href="#">
<span class="dropdown-item-text">{{ $__t('Merge') }}</span>
</a>
</div>
</div>
</td>
<td>
{{ $product->name }}
@if(!empty($product->picture_file_name))
<i class="fas fa-image text-muted"
data-toggle="tooltip"
title="{{ $__t('This product has a picture') }}"></i>
@endif
</td>
<td class="@if(!GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING) d-none @endif">
@php
$location = FindObjectInArrayByPropertyValue($locations, 'id', $product->location_id);
@endphp
@if($location != null)
{{ $location->name }}
@endif
</td>
<td>
<span class="locale-number locale-number-quantity-amount">{{ $product->min_stock_amount }}</span>
</td>
<td>
{{ FindObjectInArrayByPropertyValue($quantityunits, 'id', $product->qu_id_purchase)->name }}
</td>
<td>
{{ FindObjectInArrayByPropertyValue($quantityunits, 'id', $product->qu_id_stock)->name }}
</td>
<td>
@if(!empty($product->product_group_id)) {{ FindObjectInArrayByPropertyValue($productGroups, 'id', $product->product_group_id)->name }} @endif
</td>
<td class="@if(!GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) d-none @endif">
@php
$store = FindObjectInArrayByPropertyValue($shoppingLocations, 'id', $product->shopping_location_id);
@endphp
@if($store != null)
{{ $store->name }}
@endif
</td>
@include('components.userfields_tbody', array(
'userfields' => $userfields,
'userfieldValues' => FindAllObjectsInArrayByPropertyValue($userfieldValues, 'object_id', $product->id)
))
</tr>
@endforeach
</tbody>
</table> </table>
</div> </div>
</div> </div>
<div class="modal fade" <div class="modal fade" id="merge-products-modal" tabindex="-1">
id="merge-products-modal"
tabindex="-1">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content text-center"> <div class="modal-content text-center">
<div class="modal-header"> <div class="modal-header">
@ -229,44 +123,30 @@
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="form-group"> <div class="form-group">
<label for="merge-products-keep">{{ $__t('Product to keep') }}&nbsp;<i class="fas fa-question-circle text-muted" <label for="merge-products-keep">{{ $__t('Product to keep') }}&nbsp;<i
data-toggle="tooltip" class="fas fa-question-circle text-muted" data-toggle="tooltip" data-trigger="hover click"
data-trigger="hover click"
title="{{ $__t('After merging, this product will be kept') }}"></i> title="{{ $__t('After merging, this product will be kept') }}"></i>
</label> </label>
<select class="custom-control custom-select" <select class="select2 custom-control custom-select" id="merge-products-keep"></select>
id="merge-products-keep">
<option></option>
@foreach($products as $product)
<option value="{{ $product->id }}">{{ $product->name }}</option>
@endforeach
</select>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="merge-products-remove">{{ $__t('Product to remove') }}&nbsp;<i class="fas fa-question-circle text-muted" <label for="merge-products-remove">{{ $__t('Product to remove') }}&nbsp;<i
data-toggle="tooltip" class="fas fa-question-circle text-muted" data-toggle="tooltip" data-trigger="hover click"
data-trigger="hover click"
title="{{ $__t('After merging, all occurences of this product will be replaced by "Product to keep" (means this product will not exist anymore)') }}"></i> title="{{ $__t('After merging, all occurences of this product will be replaced by "Product to keep" (means this product will not exist anymore)') }}"></i>
</label> </label>
<select class="custom-control custom-select" <select class="select2 custom-control custom-select" id="merge-products-remove"></select>
id="merge-products-remove">
<option></option>
@foreach($products as $product)
<option value="{{ $product->id }}">{{ $product->name }}</option>
@endforeach
</select>
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" <button type="button" class="btn btn-secondary" data-dismiss="modal">{{ $__t('Cancel') }}</button>
class="btn btn-secondary" <button id="merge-products-save-button" type="button" class="btn btn-primary"
data-dismiss="modal">{{ $__t('Cancel') }}</button>
<button id="merge-products-save-button"
type="button"
class="btn btn-primary"
data-dismiss="modal">{{ $__t('OK') }}</button> data-dismiss="modal">{{ $__t('OK') }}</button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<script>
var userfields = {!! json_encode($userfields) !!};
</script>
@stop @stop

View File

@ -53,8 +53,7 @@
novalidate> novalidate>
@include('components.productpicker', array( @include('components.productpicker', array(
'products' => $products, 'productsQuery' => 'query%5B%5D=active%3D1&order=name%3Acollate%20nocase',
'barcodes' => $barcodes,
'nextInputSelector' => '#display_amount' 'nextInputSelector' => '#display_amount'
)) ))

View File

@ -15,9 +15,11 @@
<h2 class="title">@yield('title')</h2> <h2 class="title">@yield('title')</h2>
<h2> <h2>
@if ($product != null) @if ($product != null)
<span class="text-muted small">{{ $__t('Override for product') }} <strong>{{ $product->name }}</strong></span> <span class="text-muted small">{{ $__t('Override for product') }}
<strong>{{ $product->name }}</strong></span>
@else @else
<span class="text-muted small">{{ $__t('Default for QU') }} <strong>{{ $defaultQuUnit->name }}</strong></span> <span class="text-muted small">{{ $__t('Default for QU') }}
<strong>{{ $defaultQuUnit->name }}</strong></span>
@endif @endif
</h2> </h2>
</div> </div>
@ -39,27 +41,21 @@
</script> </script>
@endif @endif
<form id="quconversion-form" <form id="quconversion-form" novalidate>
novalidate>
@if ($product != null) @if ($product != null)
<input type="hidden" <input type="hidden" name="product_id" value="{{ $product->id }}">
name="product_id"
value="{{ $product->id }}">
@endif @endif
<div class="form-group"> <div class="form-group">
<label for="from_qu_id">{{ $__t('Quantity unit from') }}</label> <label for="from_qu_id">{{ $__t('Quantity unit from') }}</label>
<select required {{-- TODO: Select2: dynamic data: quantity_units --}}
class="custom-control custom-select input-group-qu" <select required class="custom-control custom-select input-group-qu" id="from_qu_id" name="from_qu_id">
id="from_qu_id"
name="from_qu_id">
<option></option> <option></option>
@foreach ($quantityunits as $quantityunit) @foreach ($quantityunits as $quantityunit)
<option @if(($product <option @if (($product != null && $quantityunit->id == $product->qu_id_stock) || ($defaultQuUnit != null && $quantityunit->id == $defaultQuUnit->id)) ) selected="selected" @endif
!=null value="{{ $quantityunit->id }}" data-plural-form="{{ $quantityunit->name_plural }}">
&& {{ $quantityunit->name }}</option>
$quantityunit->id == $product->qu_id_stock) || ($defaultQuUnit != null && $quantityunit->id == $defaultQuUnit->id))) selected="selected" @endif value="{{ $quantityunit->id }}" data-plural-form="{{ $quantityunit->name_plural }}">{{ $quantityunit->name }}</option>
@endforeach @endforeach
</select> </select>
<div class="invalid-feedback">{{ $__t('A quantity unit is required') }}</div> <div class="invalid-feedback">{{ $__t('A quantity unit is required') }}</div>
@ -67,22 +63,26 @@
<div class="form-group"> <div class="form-group">
<label for="to_qu_id">{{ $__t('Quantity unit to') }}</label> <label for="to_qu_id">{{ $__t('Quantity unit to') }}</label>
<select required {{-- TODO: Select2: dynamic data: quantity_units --}}
class="custom-control custom-select input-group-qu" <select required class="custom-control custom-select input-group-qu" id="to_qu_id" name="to_qu_id">
id="to_qu_id"
name="to_qu_id">
<option></option> <option></option>
@foreach ($quantityunits as $quantityunit) @foreach ($quantityunits as $quantityunit)
<option @if($mode=='edit' <option @if ($mode == 'edit' && $quantityunit->id == $quConversion->to_qu_id) selected="selected" @endif
&& value="{{ $quantityunit->id }}" data-plural-form="{{ $quantityunit->name_plural }}">
$quantityunit->id == $quConversion->to_qu_id) selected="selected" @endif value="{{ $quantityunit->id }}" data-plural-form="{{ $quantityunit->name_plural }}">{{ $quantityunit->name }}</option> {{ $quantityunit->name }}</option>
@endforeach @endforeach
</select> </select>
<div class="invalid-feedback">{{ $__t('A quantity unit is required') }}</div> <div class="invalid-feedback">{{ $__t('A quantity unit is required') }}</div>
</div> </div>
@php if($mode == 'edit') { $value = $quConversion->factor; } else { $value = 1; } @endphp @php
@include('components.numberpicker', array( if ($mode == 'edit') {
$value = $quConversion->factor;
} else {
$value = 1;
}
@endphp
@include('components.numberpicker', [
'id' => 'factor', 'id' => 'factor',
'label' => 'Factor', 'label' => 'Factor',
'min' => $DEFAULT_MIN_AMOUNT, 'min' => $DEFAULT_MIN_AMOUNT,
@ -90,31 +90,25 @@
'value' => $value, 'value' => $value,
'additionalHtmlElements' => '<p id="qu-conversion-info" 'additionalHtmlElements' => '<p id="qu-conversion-info"
class="form-text text-info d-none"></p>', class="form-text text-info d-none"></p>',
'additionalCssClasses' => 'input-group-qu locale-number-input locale-number-quantity-amount' 'additionalCssClasses' => 'input-group-qu locale-number-input locale-number-quantity-amount',
)) ])
<div class="form-group @if ($mode == 'edit') d-none @endif"> <div class="form-group @if ($mode == 'edit') d-none @endif">
<div class="custom-control custom-checkbox"> <div class="custom-control custom-checkbox">
<input checked <input checked class="form-check-input custom-control-input" type="checkbox" id="create_inverse"
class="form-check-input custom-control-input" name="create_inverse:skip" value="1">
type="checkbox"
id="create_inverse"
name="create_inverse:skip"
value="1">
<label class="form-check-label custom-control-label" <label class="form-check-label custom-control-label"
for="create_inverse">{{ $__t('Create inverse QU conversion') }}</label> for="create_inverse">{{ $__t('Create inverse QU conversion') }}</label>
</div> </div>
<span id="qu-conversion-inverse-info" <span id="qu-conversion-inverse-info" class="form-text text-info d-none"></span>
class="form-text text-info d-none"></span>
</div> </div>
@include('components.userfieldsform', array( @include('components.userfieldsform', [
'userfields' => $userfields, 'userfields' => $userfields,
'entity' => 'quantity_unit_conversions' 'entity' => 'quantity_unit_conversions',
)) ])
<button id="save-quconversion-button" <button id="save-quconversion-button" class="btn btn-success">{{ $__t('Save') }}</button>
class="btn btn-success">{{ $__t('Save') }}</button>
</form> </form>
</div> </div>

View File

@ -29,26 +29,20 @@
</script> </script>
@endif @endif
<form id="quantityunit-form" <form id="quantityunit-form" novalidate>
novalidate>
<div class="form-group"> <div class="form-group">
<label for="name">{{ $__t('Name') }} <span class="small text-muted">{{ $__t('in singular form') }}</span></label> <label for="name">{{ $__t('Name') }} <span
<input type="text" class="small text-muted">{{ $__t('in singular form') }}</span></label>
class="form-control" <input type="text" class="form-control" required id="name" name="name"
required
id="name"
name="name"
value="@if ($mode == 'edit') {{ $quantityUnit->name }} @endif"> value="@if ($mode == 'edit') {{ $quantityUnit->name }} @endif">
<div class="invalid-feedback">{{ $__t('A name is required') }}</div> <div class="invalid-feedback">{{ $__t('A name is required') }}</div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="name_plural">{{ $__t('Name') }} <span class="small text-muted">{{ $__t('in plural form') }}</span></label> <label for="name_plural">{{ $__t('Name') }} <span
<input type="text" class="small text-muted">{{ $__t('in plural form') }}</span></label>
class="form-control" <input type="text" class="form-control" id="name_plural" name="name_plural"
id="name_plural"
name="name_plural"
value="@if ($mode == 'edit') {{ $quantityUnit->name_plural }} @endif"> value="@if ($mode == 'edit') {{ $quantityUnit->name_plural }} @endif">
</div> </div>
@ -62,27 +56,30 @@
{{ $__t('Plural rule') }}: {{ $pluralRule }} {{ $__t('Plural rule') }}: {{ $pluralRule }}
</span> </span>
</label> </label>
<textarea class="form-control" <textarea class="form-control" rows="3" id="plural_forms" name="plural_forms">
rows="3" @if ($mode == 'edit')
id="plural_forms" {{ $quantityUnit->plural_forms }}
name="plural_forms">@if($mode == 'edit'){{ $quantityUnit->plural_forms }}@endif</textarea> @endif
</textarea>
</div> </div>
@endif @endif
<div class="form-group"> <div class="form-group">
<label for="description">{{ $__t('Description') }}</label> <label for="description">{{ $__t('Description') }}</label>
<textarea class="form-control" <textarea class="form-control" rows="2" id="description" name="description">
rows="2" @if ($mode == 'edit')
id="description" {{ $quantityUnit->description }}
name="description">@if($mode == 'edit'){{ $quantityUnit->description }}@endif</textarea> @endif
</textarea>
</div> </div>
@include('components.userfieldsform', array( @include('components.userfieldsform', [
'userfields' => $userfields, 'userfields' => $userfields,
'entity' => 'quantity_units' 'entity' => 'quantity_units',
)) ])
<small class="my-2 form-text text-muted @if($mode == 'edit') d-none @endif">{{ $__t('Save & continue to add conversions') }}</small> <small
class="my-2 form-text text-muted @if ($mode == 'edit') d-none @endif">{{ $__t('Save & continue to add conversions') }}</small>
<button class="save-quantityunit-button btn btn-success mb-2" <button class="save-quantityunit-button btn btn-success mb-2"
data-location="continue">{{ $__t('Save & continue') }}</button> data-location="continue">{{ $__t('Save & continue') }}</button>
@ -103,17 +100,13 @@
<div class="title-related-links"> <div class="title-related-links">
<h4> <h4>
{{ $__t('Default conversions') }} {{ $__t('Default conversions') }}
<small id="qu-conversion-headline-info" <small id="qu-conversion-headline-info" class="text-muted font-italic"></small>
class="text-muted font-italic"></small>
</h4> </h4>
<button class="btn btn-outline-dark d-md-none mt-2 float-right order-1 order-md-3" <button class="btn btn-outline-dark d-md-none mt-2 float-right order-1 order-md-3" type="button"
type="button" data-toggle="collapse" data-target="#related-links">
data-toggle="collapse"
data-target="#related-links">
<i class="fas fa-ellipsis-v"></i> <i class="fas fa-ellipsis-v"></i>
</button> </button>
<div class="related-links collapse d-md-flex order-2 width-xs-sm-100" <div class="related-links collapse d-md-flex order-2 width-xs-sm-100" id="related-links">
id="related-links">
<a class="btn btn-outline-primary btn-sm m-1 mt-md-0 mb-md-0 float-right show-as-dialog-link" <a class="btn btn-outline-primary btn-sm m-1 mt-md-0 mb-md-0 float-right show-as-dialog-link"
href="{{ $U('/quantityunitconversion/new?embedded&qu-unit=' . $quantityUnit->id) }}"> href="{{ $U('/quantityunitconversion/new?embedded&qu-unit=' . $quantityUnit->id) }}">
{{ $__t('Add') }} {{ $__t('Add') }}
@ -121,23 +114,21 @@
</div> </div>
</div> </div>
<table id="qu-conversions-table" {{-- TODO: DataTables: dynamic data: quantity_unit_conversions --}}
class="table table-sm table-striped nowrap w-100"> <table id="qu-conversions-table" class="table table-sm table-striped nowrap w-100">
<thead> <thead>
<tr> <tr>
<th class="border-right"><a class="text-muted change-table-columns-visibility-button" <th class="border-right"><a class="text-muted change-table-columns-visibility-button"
data-toggle="tooltip" data-toggle="tooltip" data-toggle="tooltip" title="{{ $__t('Table options') }}"
data-toggle="tooltip" data-table-selector="#qu-conversions-table" href="#"><i
title="{{ $__t('Table options') }}" class="fas fa-eye"></i></a>
data-table-selector="#qu-conversions-table"
href="#"><i class="fas fa-eye"></i></a>
</th> </th>
<th>{{ $__t('Factor') }}</th> <th>{{ $__t('Factor') }}</th>
<th>{{ $__t('Unit') }}</th> <th>{{ $__t('Unit') }}</th>
</tr> </tr>
</thead> </thead>
<tbody class="d-none"> <tbody class="d-none">
@if($mode == "edit") @if ($mode == 'edit')
@foreach ($defaultQuConversions as $defaultQuConversion) @foreach ($defaultQuConversions as $defaultQuConversion)
<tr> <tr>
<td class="fit-content border-right"> <td class="fit-content border-right">
@ -146,14 +137,14 @@
data-qu-conversion-id="{{ $defaultQuConversion->id }}"> data-qu-conversion-id="{{ $defaultQuConversion->id }}">
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
</a> </a>
<a class="btn btn-sm btn-danger qu-conversion-delete-button" <a class="btn btn-sm btn-danger qu-conversion-delete-button" href="#"
href="#"
data-qu-conversion-id="{{ $defaultQuConversion->id }}"> data-qu-conversion-id="{{ $defaultQuConversion->id }}">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
</a> </a>
</td> </td>
<td> <td>
<span class="locale-number locale-number-quantity-amount">{{ $defaultQuConversion->factor }}</span> <span
class="locale-number locale-number-quantity-amount">{{ $defaultQuConversion->factor }}</span>
</td> </td>
<td> <td>
{{ FindObjectInArrayByPropertyValue($quantityUnits, 'id', $defaultQuConversion->to_qu_id)->name }} {{ FindObjectInArrayByPropertyValue($quantityUnits, 'id', $defaultQuConversion->to_qu_id)->name }}

View File

@ -5,8 +5,7 @@
@section('viewJsName', 'quantityunitpluraltesting') @section('viewJsName', 'quantityunitpluraltesting')
@push('pageStyles') @push('pageStyles')
<link href="{{ $U('/node_modules/animate.css/animate.min.css?v=', true) }}{{ $version }}" <link href="{{ $U('/node_modules/animate.css/animate.min.css?v=', true) }}{{ $version }}" rel="stylesheet">
rel="stylesheet">
@endpush @endpush
@section('content') @section('content')
@ -20,32 +19,29 @@
<div class="row"> <div class="row">
<div class="col-lg-6 col-12"> <div class="col-lg-6 col-12">
<form id="quantityunitpluraltesting-form" <form id="quantityunitpluraltesting-form" novalidate>
novalidate>
<div class="form-group"> <div class="form-group">
<label for="qu_id">{{ $__t('Quantity unit') }}</label> <label for="qu_id">{{ $__t('Quantity unit') }}</label>
<select class="custom-control custom-select" {{-- TODO: Select2: dynamic data: quantity_units --}}
id="qu_id" <select class="custom-control custom-select" id="qu_id" name="qu_id">
name="qu_id">
<option></option> <option></option>
@foreach ($quantityUnits as $quantityUnit) @foreach ($quantityUnits as $quantityUnit)
<option value="{{ $quantityUnit->id }}" <option value="{{ $quantityUnit->id }}" data-singular-form="{{ $quantityUnit->name }}"
data-singular-form="{{ $quantityUnit->name }}"
data-plural-form="{{ $quantityUnit->name_plural }}">{{ $quantityUnit->name }}</option> data-plural-form="{{ $quantityUnit->name_plural }}">{{ $quantityUnit->name }}</option>
@endforeach @endforeach
</select> </select>
</div> </div>
@include('components.numberpicker', array( @include('components.numberpicker', [
'id' => 'amount', 'id' => 'amount',
'label' => 'Amount', 'label' => 'Amount',
'min' => 0, 'min' => 0,
'decimals' => $userSettings['stock_decimal_places_amounts'], 'decimals' => $userSettings['stock_decimal_places_amounts'],
'isRequired' => false, 'isRequired' => false,
'value' => 1, 'value' => 1,
'additionalCssClasses' => 'locale-number-input locale-number-quantity-amount' 'additionalCssClasses' => 'locale-number-input locale-number-quantity-amount',
)) ])
</form> </form>

View File

@ -10,21 +10,16 @@
<div class="title-related-links"> <div class="title-related-links">
<h2 class="title">@yield('title')</h2> <h2 class="title">@yield('title')</h2>
<div class="float-right"> <div class="float-right">
<button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" <button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" type="button"
type="button" data-toggle="collapse" data-target="#table-filter-row">
data-toggle="collapse"
data-target="#table-filter-row">
<i class="fas fa-filter"></i> <i class="fas fa-filter"></i>
</button> </button>
<button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" <button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" type="button"
type="button" data-toggle="collapse" data-target="#related-links">
data-toggle="collapse"
data-target="#related-links">
<i class="fas fa-ellipsis-v"></i> <i class="fas fa-ellipsis-v"></i>
</button> </button>
</div> </div>
<div class="related-links collapse d-md-flex order-2 width-xs-sm-100" <div class="related-links collapse d-md-flex order-2 width-xs-sm-100" id="related-links">
id="related-links">
<a class="btn btn-primary responsive-button m-1 mt-md-0 mb-md-0 float-right" <a class="btn btn-primary responsive-button m-1 mt-md-0 mb-md-0 float-right"
href="{{ $U('/quantityunit/new') }}"> href="{{ $U('/quantityunit/new') }}">
{{ $__t('Add') }} {{ $__t('Add') }}
@ -40,24 +35,18 @@
<hr class="my-2"> <hr class="my-2">
<div class="row collapse d-md-flex" <div class="row collapse d-md-flex" id="table-filter-row">
id="table-filter-row">
<div class="col-12 col-md-6 col-xl-3"> <div class="col-12 col-md-6 col-xl-3">
<div class="input-group"> <div class="input-group">
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-search"></i></span> <span class="input-group-text"><i class="fas fa-search"></i></span>
</div> </div>
<input type="text" <input type="text" id="search" class="form-control" placeholder="{{ $__t('Search') }}">
id="search"
class="form-control"
placeholder="{{ $__t('Search') }}">
</div> </div>
</div> </div>
<div class="col"> <div class="col">
<div class="float-right"> <div class="float-right">
<a id="clear-filter-button" <a id="clear-filter-button" class="btn btn-sm btn-outline-info" href="#">
class="btn btn-sm btn-outline-info"
href="#">
{{ $__t('Clear filter') }} {{ $__t('Clear filter') }}
</a> </a>
</div> </div>
@ -66,40 +55,33 @@
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<table id="quantityunits-table" {{-- TODO: DataTables: dynamic data: quantity_units --}}
class="table table-sm table-striped nowrap w-100"> <table id="quantityunits-table" class="table table-sm table-striped nowrap w-100">
<thead> <thead>
<tr> <tr>
<th class="border-right"><a class="text-muted change-table-columns-visibility-button" <th class="border-right"><a class="text-muted change-table-columns-visibility-button"
data-toggle="tooltip" data-toggle="tooltip" data-toggle="tooltip" title="{{ $__t('Table options') }}"
data-toggle="tooltip" data-table-selector="#quantityunits-table" href="#"><i class="fas fa-eye"></i></a>
title="{{ $__t('Table options') }}"
data-table-selector="#quantityunits-table"
href="#"><i class="fas fa-eye"></i></a>
</th> </th>
<th>{{ $__t('Name') }}</th> <th>{{ $__t('Name') }}</th>
<th>{{ $__t('Description') }}</th> <th>{{ $__t('Description') }}</th>
@include('components.userfields_thead', array( @include('components.userfields_thead', [
'userfields' => $userfields 'userfields' => $userfields,
)) ])
</tr> </tr>
</thead> </thead>
<tbody class="d-none"> <tbody class="d-none">
@foreach ($quantityunits as $quantityunit) @foreach ($quantityunits as $quantityunit)
<tr> <tr>
<td class="fit-content border-right"> <td class="fit-content border-right">
<a class="btn btn-info btn-sm" <a class="btn btn-info btn-sm" href="{{ $U('/quantityunit/') }}{{ $quantityunit->id }}"
href="{{ $U('/quantityunit/') }}{{ $quantityunit->id }}" data-toggle="tooltip" title="{{ $__t('Edit this item') }}">
data-toggle="tooltip"
title="{{ $__t('Edit this item') }}">
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
</a> </a>
<a class="btn btn-danger btn-sm quantityunit-delete-button" <a class="btn btn-danger btn-sm quantityunit-delete-button" href="#"
href="#"
data-quantityunit-id="{{ $quantityunit->id }}" data-quantityunit-id="{{ $quantityunit->id }}"
data-quantityunit-name="{{ $quantityunit->name }}" data-quantityunit-name="{{ $quantityunit->name }}" data-toggle="tooltip"
data-toggle="tooltip"
title="{{ $__t('Delete this item') }}"> title="{{ $__t('Delete this item') }}">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
</a> </a>
@ -111,10 +93,14 @@
{{ $quantityunit->description }} {{ $quantityunit->description }}
</td> </td>
@include('components.userfields_tbody', array( @include('components.userfields_tbody', [
'userfields' => $userfields, 'userfields' => $userfields,
'userfieldValues' => FindAllObjectsInArrayByPropertyValue($userfieldValues, 'object_id', $quantityunit->id) 'userfieldValues' => FindAllObjectsInArrayByPropertyValue(
)) $userfieldValues,
'object_id',
$quantityunit->id
),
])
</tr> </tr>
@endforeach @endforeach

View File

@ -37,69 +37,72 @@
<div class="row"> <div class="row">
<div class="col-12 col-md-7 pb-3"> <div class="col-12 col-md-7 pb-3">
<form id="recipe-form" <form id="recipe-form" novalidate>
novalidate>
<div class="form-group"> <div class="form-group">
<label for="name">{{ $__t('Name') }}</label> <label for="name">{{ $__t('Name') }}</label>
<input type="text" <input type="text" class="form-control" required id="name" name="name"
class="form-control"
required
id="name"
name="name"
value="@if ($mode == 'edit') {{ $recipe->name }} @endif"> value="@if ($mode == 'edit') {{ $recipe->name }} @endif">
<div class="invalid-feedback">{{ $__t('A name is required') }}</div> <div class="invalid-feedback">{{ $__t('A name is required') }}</div>
</div> </div>
@php if($mode == 'edit') { $value = $recipe->base_servings; } else { $value = 1; } @endphp @php
@include('components.numberpicker', array( if ($mode == 'edit') {
$value = $recipe->base_servings;
} else {
$value = 1;
}
@endphp
@include('components.numberpicker', [
'id' => 'base_servings', 'id' => 'base_servings',
'label' => 'Servings', 'label' => 'Servings',
'min' => $DEFAULT_MIN_AMOUNT, 'min' => $DEFAULT_MIN_AMOUNT,
'decimals' => $userSettings['stock_decimal_places_amounts'], 'decimals' => $userSettings['stock_decimal_places_amounts'],
'value' => $value, 'value' => $value,
'hint' => $__t('The ingredients listed here result in this amount of servings'), 'hint' => $__t('The ingredients listed here result in this amount of servings'),
'additionalCssClasses' => 'locale-number-input locale-number-quantity-amount' 'additionalCssClasses' => 'locale-number-input locale-number-quantity-amount',
)) ])
<div class="form-group"> <div class="form-group">
<div class="custom-control custom-checkbox"> <div class="custom-control custom-checkbox">
<input @if($mode=='edit' <input @if ($mode == 'edit' && $recipe->not_check_shoppinglist == 1) checked @endif
&& class="form-check-input custom-control-input" type="checkbox" id="not_check_shoppinglist"
$recipe->not_check_shoppinglist == 1) checked @endif class="form-check-input custom-control-input" type="checkbox" id="not_check_shoppinglist" name="not_check_shoppinglist" value="1"> name="not_check_shoppinglist" value="1">
<label class="form-check-label custom-control-label" <label class="form-check-label custom-control-label" for="not_check_shoppinglist">
for="not_check_shoppinglist">
{{ $__t('Do not check against the shopping list when adding missing items to it') }}&nbsp; {{ $__t('Do not check against the shopping list when adding missing items to it') }}&nbsp;
<i class="fas fa-question-circle text-muted" <i class="fas fa-question-circle text-muted" data-toggle="tooltip" data-trigger="hover click"
data-toggle="tooltip"
data-trigger="hover click"
title="{{ $__t('By default the amount to be added to the shopping list is "needed amount - stock amount - shopping list amount" - when this is enabled, it is only checked against the stock amount, not against what is already on the shopping list') }}"></i> title="{{ $__t('By default the amount to be added to the shopping list is "needed amount - stock amount - shopping list amount" - when this is enabled, it is only checked against the stock amount, not against what is already on the shopping list') }}"></i>
</label> </label>
</div> </div>
</div> </div>
@include('components.productpicker', array( @include('components.productpicker', [
'products' => $products, 'productsQuery' => 'order=name%3Acollate%20nocase',
'isRequired' => false, 'isRequired' => false,
'label' => 'Produces product', 'label' => 'Produces product',
'prefillById' => $mode == 'edit' ? $recipe->product_id : '', 'prefillById' => $mode == 'edit' ? $recipe->product_id : '',
'hint' => $__t('When a product is selected, one unit (per serving in stock quantity unit) will be added to stock on consuming this recipe'), 'hint' => $__t(
'When a product is selected, one unit (per serving in stock quantity unit) will be added to stock on consuming this recipe'
),
'disallowAllProductWorkflows' => true, 'disallowAllProductWorkflows' => true,
)) ])
@include('components.userfieldsform', array( @include('components.userfieldsform', [
'userfields' => $userfields, 'userfields' => $userfields,
'entity' => 'recipes' 'entity' => 'recipes',
)) ])
<div class="form-group"> <div class="form-group">
<label for="description">{{ $__t('Preparation') }}</label> <label for="description">{{ $__t('Preparation') }}</label>
<textarea id="description" <textarea id="description" class="form-control wysiwyg-editor" name="description">
class="form-control wysiwyg-editor" @if ($mode == 'edit')
name="description">@if($mode == 'edit'){{ $recipe->description }}@endif</textarea> {{ $recipe->description }}
@endif
</textarea>
</div> </div>
<small class="my-2 form-text text-muted @if($mode == 'edit') d-none @endif">{{ $__t('Save & continue to add ingredients and included recipes') }}</small> <small
class="my-2 form-text text-muted @if ($mode == 'edit') d-none @endif">{{ $__t('Save & continue to add ingredients and included recipes') }}</small>
<button class="save-recipe btn btn-success mb-2" <button class="save-recipe btn btn-success mb-2"
data-location="continue">{{ $__t('Save & continue') }}</button> data-location="continue">{{ $__t('Save & continue') }}</button>
@ -116,33 +119,26 @@
<h4> <h4>
{{ $__t('Ingredients list') }} {{ $__t('Ingredients list') }}
</h4> </h4>
<button class="btn btn-outline-dark d-md-none mt-2 float-right order-1 order-md-3" <button class="btn btn-outline-dark d-md-none mt-2 float-right order-1 order-md-3" type="button"
type="button" data-toggle="collapse" data-target="#related-links">
data-toggle="collapse"
data-target="#related-links">
<i class="fas fa-ellipsis-v"></i> <i class="fas fa-ellipsis-v"></i>
</button> </button>
<div class="related-links collapse d-md-flex order-2 width-xs-sm-100" <div class="related-links collapse d-md-flex order-2 width-xs-sm-100" id="related-links">
id="related-links">
<a id="recipe-pos-add-button" <a id="recipe-pos-add-button"
class="btn btn-outline-primary btn-sm recipe-pos-add-button m-1 mt-md-0 mb-md-0 float-right" class="btn btn-outline-primary btn-sm recipe-pos-add-button m-1 mt-md-0 mb-md-0 float-right"
type="button" type="button" href="#">
href="#">
{{ $__t('Add') }} {{ $__t('Add') }}
</a> </a>
</div> </div>
</div> </div>
<table id="recipes-pos-table" {{-- TODO: DataTables: dynamic data: recipes_pos --}}
class="table table-sm table-striped nowrap w-100"> <table id="recipes-pos-table" class="table table-sm table-striped nowrap w-100">
<thead> <thead>
<tr> <tr>
<th class="border-right"><a class="text-muted change-table-columns-visibility-button" <th class="border-right"><a class="text-muted change-table-columns-visibility-button"
data-toggle="tooltip" data-toggle="tooltip" data-toggle="tooltip" title="{{ $__t('Table options') }}"
data-toggle="tooltip" data-table-selector="#recipes-pos-table" href="#"><i class="fas fa-eye"></i></a>
title="{{ $__t('Table options') }}"
data-table-selector="#recipes-pos-table"
href="#"><i class="fas fa-eye"></i></a>
</th> </th>
<th>{{ $__t('Product') }}</th> <th>{{ $__t('Product') }}</th>
<th>{{ $__t('Amount') }}</th> <th>{{ $__t('Amount') }}</th>
@ -151,19 +147,16 @@
</tr> </tr>
</thead> </thead>
<tbody class="d-none"> <tbody class="d-none">
@if($mode == "edit") @if ($mode == 'edit')
@foreach ($recipePositions as $recipePosition) @foreach ($recipePositions as $recipePosition)
<tr> <tr>
<td class="fit-content border-right"> <td class="fit-content border-right">
<a class="btn btn-sm btn-info recipe-pos-edit-button" <a class="btn btn-sm btn-info recipe-pos-edit-button" type="button" href="#"
type="button"
href="#"
data-recipe-pos-id="{{ $recipePosition->id }}" data-recipe-pos-id="{{ $recipePosition->id }}"
data-product-id="{{ $recipePosition->product_id }}"> data-product-id="{{ $recipePosition->product_id }}">
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
</a> </a>
<a class="btn btn-sm btn-danger recipe-pos-delete-button" <a class="btn btn-sm btn-danger recipe-pos-delete-button" href="#"
href="#"
data-recipe-pos-id="{{ $recipePosition->id }}" data-recipe-pos-id="{{ $recipePosition->id }}"
data-recipe-pos-name="{{ FindObjectInArrayByPropertyValue($products, 'id', $recipePosition->product_id)->name }}"> data-recipe-pos-name="{{ FindObjectInArrayByPropertyValue($products, 'id', $recipePosition->product_id)->name }}">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
@ -178,8 +171,7 @@
// but some users decide to edit the database manually and // but some users decide to edit the database manually and
// enter something like "4 or 5" in the amount column (brilliant) // enter something like "4 or 5" in the amount column (brilliant)
// => So at least don't crash this view by just assuming 0 if that's the case // => So at least don't crash this view by just assuming 0 if that's the case
if (!is_numeric($recipePosition->amount)) if (!is_numeric($recipePosition->amount)) {
{
$recipePosition->amount = 0; $recipePosition->amount = 0;
} }
@ -187,27 +179,29 @@
$productQuConversions = FindAllObjectsInArrayByPropertyValue($quantityUnitConversionsResolved, 'product_id', $product->id); $productQuConversions = FindAllObjectsInArrayByPropertyValue($quantityUnitConversionsResolved, 'product_id', $product->id);
$productQuConversions = FindAllObjectsInArrayByPropertyValue($productQuConversions, 'from_qu_id', $product->qu_id_stock); $productQuConversions = FindAllObjectsInArrayByPropertyValue($productQuConversions, 'from_qu_id', $product->qu_id_stock);
$productQuConversion = FindObjectInArrayByPropertyValue($productQuConversions, 'to_qu_id', $recipePosition->qu_id); $productQuConversion = FindObjectInArrayByPropertyValue($productQuConversions, 'to_qu_id', $recipePosition->qu_id);
if ($productQuConversion && $recipePosition->only_check_single_unit_in_stock == 0) if ($productQuConversion && $recipePosition->only_check_single_unit_in_stock == 0) {
{
$recipePosition->amount = $recipePosition->amount * $productQuConversion->factor; $recipePosition->amount = $recipePosition->amount * $productQuConversion->factor;
} }
@endphp @endphp
@if (!empty($recipePosition->variable_amount)) @if (!empty($recipePosition->variable_amount))
{{ $recipePosition->variable_amount }} {{ $recipePosition->variable_amount }}
@else @else
<span class="locale-number locale-number-quantity-amount">@if($recipePosition->amount == round($recipePosition->amount)){{ round($recipePosition->amount) }}@else{{ $recipePosition->amount }}@endif</span> <span class="locale-number locale-number-quantity-amount">
@if ($recipePosition->amount == round($recipePosition->amount))
{{ round($recipePosition->amount) }}@else{{ $recipePosition->amount }}
@endif
</span>
@endif @endif
{{ $__n($recipePosition->amount,FindObjectInArrayByPropertyValue($quantityunits, 'id', $recipePosition->qu_id)->name,FindObjectInArrayByPropertyValue($quantityunits, 'id', $recipePosition->qu_id)->name_plural,true) }} {{ $__n($recipePosition->amount,FindObjectInArrayByPropertyValue($quantityunits, 'id', $recipePosition->qu_id)->name,FindObjectInArrayByPropertyValue($quantityunits, 'id', $recipePosition->qu_id)->name_plural,true) }}
@if (!empty($recipePosition->variable_amount)) @if (!empty($recipePosition->variable_amount))
<div class="small text-muted font-italic">{{ $__t('Variable amount') }}</div> <div class="small text-muted font-italic">{{ $__t('Variable amount') }}
</div>
@endif @endif
</td> </td>
<td class="fit-content"> <td class="fit-content">
<a class="btn btn-sm btn-info recipe-pos-show-note-button @if (empty($recipePosition->note)) disabled @endif" <a class="btn btn-sm btn-info recipe-pos-show-note-button @if (empty($recipePosition->note)) disabled @endif"
href="#" href="#" data-toggle="tooltip" data-placement="top"
data-toggle="tooltip"
data-placement="top"
title="{{ $__t('Show notes') }}" title="{{ $__t('Show notes') }}"
data-recipe-pos-note="{{ $recipePosition->note }}"> data-recipe-pos-note="{{ $recipePosition->note }}">
<i class="fas fa-eye"></i> <i class="fas fa-eye"></i>
@ -230,50 +224,42 @@
<h4> <h4>
{{ $__t('Included recipes') }} {{ $__t('Included recipes') }}
</h4> </h4>
<button class="btn btn-outline-dark d-md-none mt-2 float-right order-1 order-md-3" <button class="btn btn-outline-dark d-md-none mt-2 float-right order-1 order-md-3" type="button"
type="button" data-toggle="collapse" data-target="#related-links">
data-toggle="collapse"
data-target="#related-links">
<i class="fas fa-ellipsis-v"></i> <i class="fas fa-ellipsis-v"></i>
</button> </button>
<div class="related-links collapse d-md-flex order-2 width-xs-sm-100" <div class="related-links collapse d-md-flex order-2 width-xs-sm-100" id="related-links">
id="related-links">
<a id="recipe-include-add-button" <a id="recipe-include-add-button"
class="btn btn-outline-primary btn-sm m-1 mt-md-0 mb-md-0 float-right" class="btn btn-outline-primary btn-sm m-1 mt-md-0 mb-md-0 float-right" href="#">
href="#">
{{ $__t('Add') }} {{ $__t('Add') }}
</a> </a>
</div> </div>
</div> </div>
<table id="recipes-includes-table" {{-- TODO: DataTables: dynamic data: recipes_nestings_resolved --}}
class="table table-sm table-striped nowrap w-100"> <table id="recipes-includes-table" class="table table-sm table-striped nowrap w-100">
<thead> <thead>
<tr> <tr>
<th class="border-right"><a class="text-muted change-table-columns-visibility-button" <th class="border-right"><a class="text-muted change-table-columns-visibility-button"
data-toggle="tooltip" data-toggle="tooltip" data-toggle="tooltip" title="{{ $__t('Table options') }}"
data-toggle="tooltip" data-table-selector="#recipes-includes-table" href="#"><i
title="{{ $__t('Table options') }}" class="fas fa-eye"></i></a>
data-table-selector="#recipes-includes-table"
href="#"><i class="fas fa-eye"></i></a>
</th> </th>
<th>{{ $__t('Recipe') }}</th> <th>{{ $__t('Recipe') }}</th>
<th>{{ $__t('Servings') }}</th> <th>{{ $__t('Servings') }}</th>
</tr> </tr>
</thead> </thead>
<tbody class="d-none"> <tbody class="d-none">
@if($mode == "edit") @if ($mode == 'edit')
@foreach ($recipeNestings as $recipeNesting) @foreach ($recipeNestings as $recipeNesting)
<tr> <tr>
<td class="fit-content border-right"> <td class="fit-content border-right">
<a class="btn btn-sm btn-info recipe-include-edit-button" <a class="btn btn-sm btn-info recipe-include-edit-button" href="#"
href="#"
data-recipe-include-id="{{ $recipeNesting->id }}" data-recipe-include-id="{{ $recipeNesting->id }}"
data-recipe-included-recipe-id="{{ $recipeNesting->includes_recipe_id }}" data-recipe-included-recipe-id="{{ $recipeNesting->includes_recipe_id }}"
data-recipe-included-recipe-servings="{{ $recipeNesting->servings }}"> data-recipe-included-recipe-servings="{{ $recipeNesting->servings }}">
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
</a> </a>
<a class="btn btn-sm btn-danger recipe-include-delete-button" <a class="btn btn-sm btn-danger recipe-include-delete-button" href="#"
href="#"
data-recipe-include-id="{{ $recipeNesting->id }}" data-recipe-include-id="{{ $recipeNesting->id }}"
data-recipe-include-name="{{ FindObjectInArrayByPropertyValue($recipes, 'id', $recipeNesting->includes_recipe_id)->name }}"> data-recipe-include-name="{{ FindObjectInArrayByPropertyValue($recipes, 'id', $recipeNesting->includes_recipe_id)->name }}">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
@ -302,10 +288,7 @@
<div class="form-group w-75 m-0"> <div class="form-group w-75 m-0">
<div class="input-group"> <div class="input-group">
<div class="custom-file"> <div class="custom-file">
<input type="file" <input type="file" class="custom-file-input" id="recipe-picture" accept="image/*">
class="custom-file-input"
id="recipe-picture"
accept="image/*">
<label id="recipe-picture-label" <label id="recipe-picture-label"
class="custom-file-label @if (empty($recipe->picture_file_name)) d-none @endif" class="custom-file-label @if (empty($recipe->picture_file_name)) d-none @endif"
for="recipe-picture"> for="recipe-picture">
@ -329,10 +312,11 @@
data-src="{{ $U('/api/files/recipepictures/' .base64_encode($recipe->picture_file_name) .'?force_serve_as=picture&best_fit_width=400') }}" data-src="{{ $U('/api/files/recipepictures/' .base64_encode($recipe->picture_file_name) .'?force_serve_as=picture&best_fit_width=400') }}"
class="img-fluid img-thumbnail mt-2 lazy mb-5"> class="img-fluid img-thumbnail mt-2 lazy mb-5">
<p id="delete-current-recipe-picture-on-save-hint" <p id="delete-current-recipe-picture-on-save-hint"
class="form-text text-muted font-italic d-none mb-5">{{ $__t('The current picture will be deleted on save') }}</p> class="form-text text-muted font-italic d-none mb-5">
{{ $__t('The current picture will be deleted on save') }}</p>
@else @else
<p id="no-current-recipe-picture-hint" <p id="no-current-recipe-picture-hint" class="form-text text-muted font-italic mb-5">
class="form-text text-muted font-italic mb-5">{{ $__t('No picture available') }}</p> {{ $__t('No picture available') }}</p>
@endif @endif
</div> </div>
</div> </div>
@ -342,9 +326,7 @@
<div class="title-related-links"> <div class="title-related-links">
<h4> <h4>
<span class="ls-n1">{{ $__t('grocycode') }}</span> <span class="ls-n1">{{ $__t('grocycode') }}</span>
<i class="fas fa-question-circle text-muted" <i class="fas fa-question-circle text-muted" data-toggle="tooltip" data-trigger="hover click"
data-toggle="tooltip"
data-trigger="hover click"
title="{{ $__t('grocycode is a unique referer to this %s in your grocy instance - print it onto a label and scan it like any other barcode',$__t('Recipe')) }}"></i> title="{{ $__t('grocycode is a unique referer to this %s in your grocy instance - print it onto a label and scan it like any other barcode',$__t('Recipe')) }}"></i>
</h4> </h4>
<p> <p>
@ -358,8 +340,7 @@
href="{{ $U('/recipe/' . $recipe->id . '/grocycode?download=true') }}">{{ $__t('Download') }}</a> href="{{ $U('/recipe/' . $recipe->id . '/grocycode?download=true') }}">{{ $__t('Download') }}</a>
@if (GROCY_FEATURE_FLAG_LABEL_PRINTER) @if (GROCY_FEATURE_FLAG_LABEL_PRINTER)
<a class="btn btn-outline-primary btn-sm recipe-grocycode-label-print" <a class="btn btn-outline-primary btn-sm recipe-grocycode-label-print"
data-recipe-id="{{ $recipe->id }}" data-recipe-id="{{ $recipe->id }}" href="#">
href="#">
{{ $__t('Print on label printer') }} {{ $__t('Print on label printer') }}
</a> </a>
@endif @endif
@ -371,41 +352,34 @@
</div> </div>
<div class="modal fade" <div class="modal fade" id="recipe-include-editform-modal" tabindex="-1">
id="recipe-include-editform-modal"
tabindex="-1">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content text-center"> <div class="modal-content text-center">
<div class="modal-header"> <div class="modal-header">
<h4 id="recipe-include-editform-title" <h4 id="recipe-include-editform-title" class="modal-title w-100"></h4>
class="modal-title w-100"></h4>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<form id="recipe-include-form" <form id="recipe-include-form" novalidate>
novalidate>
@include('components.recipepicker', array( @include('components.recipepicker', [
'recipes' => $recipes, 'recipes' => $recipes,
'isRequired' => true 'isRequired' => true,
)) ])
@include('components.numberpicker', array( @include('components.numberpicker', [
'id' => 'includes_servings', 'id' => 'includes_servings',
'label' => 'Servings', 'label' => 'Servings',
'min' => $DEFAULT_MIN_AMOUNT, 'min' => $DEFAULT_MIN_AMOUNT,
'decimals' => $userSettings['stock_decimal_places_amounts'], 'decimals' => $userSettings['stock_decimal_places_amounts'],
'value' => '1', 'value' => '1',
'additionalCssClasses' => 'locale-number-input locale-number-quantity-amount' 'additionalCssClasses' => 'locale-number-input locale-number-quantity-amount',
)) ])
</form> </form>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" <button type="button" class="btn btn-secondary" data-dismiss="modal">{{ $__t('Cancel') }}</button>
class="btn btn-secondary" <button id="save-recipe-include-button" data-dismiss="modal"
data-dismiss="modal">{{ $__t('Cancel') }}</button>
<button id="save-recipe-include-button"
data-dismiss="modal"
class="btn btn-success">{{ $__t('Save') }}</button> class="btn btn-success">{{ $__t('Save') }}</button>
</div> </div>
</div> </div>

View File

@ -46,7 +46,7 @@
novalidate> novalidate>
@include('components.productpicker', array( @include('components.productpicker', array(
'products' => $products, 'productsQuery' => ($recipePosId === 'new' ? 'query%5B%5D=active%3D1&' : '') . 'order=name%3Acollate%20nocase',
'nextInputSelector' => '#amount' 'nextInputSelector' => '#amount'
)) ))

View File

@ -25,21 +25,16 @@
<div class="title-related-links border-bottom mb-2 py-1"> <div class="title-related-links border-bottom mb-2 py-1">
<h2 class="title">@yield('title')</h2> <h2 class="title">@yield('title')</h2>
<div class="float-right"> <div class="float-right">
<button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" <button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" type="button"
type="button" data-toggle="collapse" data-target="#table-filter-row">
data-toggle="collapse"
data-target="#table-filter-row">
<i class="fas fa-filter"></i> <i class="fas fa-filter"></i>
</button> </button>
<button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" <button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" type="button"
type="button" data-toggle="collapse" data-target="#related-links">
data-toggle="collapse"
data-target="#related-links">
<i class="fas fa-ellipsis-v"></i> <i class="fas fa-ellipsis-v"></i>
</button> </button>
</div> </div>
<div class="related-links collapse d-md-flex order-2 width-xs-sm-100" <div class="related-links collapse d-md-flex order-2 width-xs-sm-100" id="related-links">
id="related-links">
<a class="btn btn-primary responsive-button m-1 mt-md-0 mb-md-0 float-right" <a class="btn btn-primary responsive-button m-1 mt-md-0 mb-md-0 float-right"
href="{{ $U('/recipe/new') }}"> href="{{ $U('/recipe/new') }}">
{{ $__t('Add') }} {{ $__t('Add') }}
@ -47,17 +42,13 @@
</div> </div>
</div> </div>
<div class="row collapse d-md-flex" <div class="row collapse d-md-flex" id="table-filter-row">
id="table-filter-row">
<div class="col-12 col-md-5 col-xl-5"> <div class="col-12 col-md-5 col-xl-5">
<div class="input-group"> <div class="input-group">
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-search"></i></span> <span class="input-group-text"><i class="fas fa-search"></i></span>
</div> </div>
<input type="text" <input type="text" id="search" class="form-control" placeholder="{{ $__t('Search') }}">
id="search"
class="form-control"
placeholder="{{ $__t('Search') }}">
</div> </div>
</div> </div>
@ -66,11 +57,11 @@
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-filter"></i>&nbsp;{{ $__t('Status') }}</span> <span class="input-group-text"><i class="fas fa-filter"></i>&nbsp;{{ $__t('Status') }}</span>
</div> </div>
<select class="custom-control custom-select" <select class="custom-control custom-select" id="status-filter">
id="status-filter">
<option value="all">{{ $__t('All') }}</option> <option value="all">{{ $__t('All') }}</option>
<option value="Xenoughinstock">{{ $__t('Enough in stock') }}</option> <option value="Xenoughinstock">{{ $__t('Enough in stock') }}</option>
<option value="enoughinstockwithshoppinglist">{{ $__t('Not enough in stock, but already on the shopping list') }}</option> <option value="enoughinstockwithshoppinglist">
{{ $__t('Not enough in stock, but already on the shopping list') }}</option>
<option value="notenoughinstock">{{ $__t('Not enough in stock') }}</option> <option value="notenoughinstock">{{ $__t('Not enough in stock') }}</option>
</select> </select>
</div> </div>
@ -78,9 +69,7 @@
<div class="col"> <div class="col">
<div class="float-right mt-1"> <div class="float-right mt-1">
<a id="clear-filter-button" <a id="clear-filter-button" class="btn btn-sm btn-outline-info" href="#">
class="btn btn-sm btn-outline-info"
href="#">
{{ $__t('Clear filter') }} {{ $__t('Clear filter') }}
</a> </a>
</div> </div>
@ -89,90 +78,74 @@
<ul class="nav nav-tabs grocy-tabs"> <ul class="nav nav-tabs grocy-tabs">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link active" <a class="nav-link active" id="list-tab" data-toggle="tab" href="#list">{{ $__t('List') }}</a>
id="list-tab"
data-toggle="tab"
href="#list">{{ $__t('List') }}</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" <a class="nav-link" id="gallery-tab" data-toggle="tab"
id="gallery-tab"
data-toggle="tab"
href="#gallery">{{ $__t('Gallery') }}</a> href="#gallery">{{ $__t('Gallery') }}</a>
</li> </li>
</ul> </ul>
<div class="tab-content grocy-tabs"> <div class="tab-content grocy-tabs">
<div class="tab-pane show active" <div class="tab-pane show active" id="list">
id="list"> {{-- TODO: DataTables: dynamic data: recipes --}}
<table id="recipes-table" <table id="recipes-table" class="table table-sm table-striped nowrap w-100">
class="table table-sm table-striped nowrap w-100">
<thead> <thead>
<tr> <tr>
<th class="border-right"><a class="text-muted change-table-columns-visibility-button" <th class="border-right"><a class="text-muted change-table-columns-visibility-button"
data-toggle="tooltip" data-toggle="tooltip" data-toggle="tooltip" title="{{ $__t('Table options') }}"
data-toggle="tooltip" data-table-selector="#recipes-table" href="#"><i class="fas fa-eye"></i></a>
title="{{ $__t('Table options') }}"
data-table-selector="#recipes-table"
href="#"><i class="fas fa-eye"></i></a>
</th> </th>
<th>{{ $__t('Name') }}</th> <th>{{ $__t('Name') }}</th>
<th class="allow-grouping">{{ $__t('Desired servings') }}</th> <th class="allow-grouping">{{ $__t('Desired servings') }}</th>
<th data-shadow-rowgroup-column="7" <th data-shadow-rowgroup-column="7"
class="@if(!GROCY_FEATURE_FLAG_STOCK) d-none @endif allow-grouping">{{ $__t('Requirements fulfilled') }}</th> class="@if (!GROCY_FEATURE_FLAG_STOCK) d-none @endif allow-grouping">
{{ $__t('Requirements fulfilled') }}</th>
<th class="d-none">Hidden status for sorting of "Requirements fulfilled" column</th> <th class="d-none">Hidden status for sorting of "Requirements fulfilled" column</th>
<th class="d-none">Hidden status for filtering by status</th> <th class="d-none">Hidden status for filtering by status</th>
<th class="d-none">Hidden recipe ingredient product names</th> <th class="d-none">Hidden recipe ingredient product names</th>
<th class="d-none">Hidden status for grouping by status</th> <th class="d-none">Hidden status for grouping by status</th>
@include('components.userfields_thead', array( @include('components.userfields_thead', [
'userfields' => $userfields 'userfields' => $userfields,
)) ])
</tr> </tr>
</thead> </thead>
<tbody class="d-none"> <tbody class="d-none">
@foreach ($recipes as $recipe) @foreach ($recipes as $recipe)
<tr id="recipe-row-{{ $recipe->id }}" <tr id="recipe-row-{{ $recipe->id }}" data-recipe-id="{{ $recipe->id }}">
data-recipe-id="{{ $recipe->id }}">
<td class="fit-content border-right"> <td class="fit-content border-right">
<a class="btn btn-info btn-sm hide-when-embedded hide-on-fullscreen-card" <a class="btn btn-info btn-sm hide-when-embedded hide-on-fullscreen-card"
href="{{ $U('/recipe/') }}{{ $recipe->id }}" href="{{ $U('/recipe/') }}{{ $recipe->id }}" data-toggle="tooltip"
data-toggle="tooltip"
title="{{ $__t('Edit this item') }}"> title="{{ $__t('Edit this item') }}">
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
</a> </a>
<div class="dropdown d-inline-block"> <div class="dropdown d-inline-block">
<button class="btn btn-sm btn-light text-secondary" <button class="btn btn-sm btn-light text-secondary" type="button"
type="button"
data-toggle="dropdown"> data-toggle="dropdown">
<i class="fas fa-ellipsis-v"></i> <i class="fas fa-ellipsis-v"></i>
</button> </button>
<div class="table-inline-menu dropdown-menu dropdown-menu-right hide-on-fullscreen-card hide-when-embedded"> <div
<a class="dropdown-item recipe-delete" class="table-inline-menu dropdown-menu dropdown-menu-right hide-on-fullscreen-card hide-when-embedded">
type="button" <a class="dropdown-item recipe-delete" type="button" href="#"
href="#"
data-recipe-id="{{ $recipe->id }}" data-recipe-id="{{ $recipe->id }}"
data-recipe-name="{{ $recipe->name }}"> data-recipe-name="{{ $recipe->name }}">
<span class="dropdown-item-text">{{ $__t('Delete this item') }}</span> <span
class="dropdown-item-text">{{ $__t('Delete this item') }}</span>
</a> </a>
<a class="dropdown-item recipe-copy" <a class="dropdown-item recipe-copy" type="button" href="#"
type="button"
href="#"
data-recipe-id="{{ $recipe->id }}"> data-recipe-id="{{ $recipe->id }}">
<span class="dropdown-item-text">{{ $__t('Copy recipe') }}</span> <span class="dropdown-item-text">{{ $__t('Copy recipe') }}</span>
</a> </a>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a class="dropdown-item" <a class="dropdown-item" type="button"
type="button"
href="{{ $U('/recipe/' . $recipe->id . '/grocycode?download=true') }}"> href="{{ $U('/recipe/' . $recipe->id . '/grocycode?download=true') }}">
<span class="dropdown-item-text">{!! str_replace('grocycode', '<span class="ls-n1">grocycode</span>', $__t('Download %s grocycode', $__t('Recipe'))) !!}</span> <span class="dropdown-item-text">{!! str_replace('grocycode', '<span class="ls-n1">grocycode</span>', $__t('Download %s grocycode', $__t('Recipe'))) !!}</span>
</a> </a>
@if (GROCY_FEATURE_FLAG_LABEL_PRINTER) @if (GROCY_FEATURE_FLAG_LABEL_PRINTER)
<a class="dropdown-item recipe-grocycode-label-print" <a class="dropdown-item recipe-grocycode-label-print"
data-recipe-id="{{ $recipe->id }}" data-recipe-id="{{ $recipe->id }}" type="button" href="#">
type="button"
href="#">
<span class="dropdown-item-text">{!! str_replace('grocycode', '<span class="ls-n1">grocycode</span>', $__t('Print %s grocycode on label printer', $__t('Recipe'))) !!}</span> <span class="dropdown-item-text">{!! str_replace('grocycode', '<span class="ls-n1">grocycode</span>', $__t('Print %s grocycode on label printer', $__t('Recipe'))) !!}</span>
</a> </a>
@endif @endif
@ -186,14 +159,31 @@
{{ $recipe->desired_servings }} {{ $recipe->desired_servings }}
</td> </td>
<td class="@if (!GROCY_FEATURE_FLAG_STOCK) d-none @endif"> <td class="@if (!GROCY_FEATURE_FLAG_STOCK) d-none @endif">
@if(FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->need_fulfilled == 1)<i class="fas fa-check text-success"></i>@elseif(FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->need_fulfilled_with_shopping_list == 1)<i class="fas fa-exclamation text-warning"></i>@else<i class="fas fa-times text-danger"></i>@endif @if (FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->need_fulfilled == 1)
<span class="timeago-contextual">@if(FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->need_fulfilled == 1){{ $__t('Enough in stock') }}@elseif(FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->need_fulfilled_with_shopping_list == 1){{ $__n(FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->missing_products_count, 'Not enough in stock, %s ingredient missing but already on the shopping list', 'Not enough in stock, %s ingredients missing but already on the shopping list') }}@else{{ $__n(FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->missing_products_count, 'Not enough in stock, %s ingredient missing', 'Not enough in stock, %s ingredients missing') }}@endif</span> <i class="fas fa-check text-success"></i>
@elseif(FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->need_fulfilled_with_shopping_list == 1)
<i class="fas fa-exclamation text-warning"></i>@else<i
class="fas fa-times text-danger"></i>
@endif
<span class="timeago-contextual">
@if (FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->need_fulfilled == 1)
{{ $__t('Enough in stock') }}
@elseif(FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->need_fulfilled_with_shopping_list == 1)
{{ $__n(FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->missing_products_count,'Not enough in stock, %s ingredient missing but already on the shopping list','Not enough in stock, %s ingredients missing but already on the shopping list') }}@else{{ $__n(FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->missing_products_count,'Not enough in stock, %s ingredient missing','Not enough in stock, %s ingredients missing') }}
@endif
</span>
</td> </td>
<td class="d-none"> <td class="d-none">
{{ FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->missing_products_count }} {{ FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->missing_products_count }}
</td> </td>
<td class="d-none"> <td class="d-none">
@if(FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->need_fulfilled == 1) Xenoughinstock @elseif(FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->need_fulfilled_with_shopping_list == 1) enoughinstockwithshoppinglist @else notenoughinstock @endif @if (FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->need_fulfilled == 1)
Xenoughinstock
@elseif(FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->need_fulfilled_with_shopping_list == 1)
enoughinstockwithshoppinglist
@else
notenoughinstock
@endif
</td> </td>
<td class="d-none"> <td class="d-none">
@foreach (FindAllObjectsInArrayByPropertyValue($recipePositionsResolved, 'recipe_id', $recipe->id) as $recipePos) @foreach (FindAllObjectsInArrayByPropertyValue($recipePositionsResolved, 'recipe_id', $recipe->id) as $recipePos)
@ -201,13 +191,23 @@
@endforeach @endforeach
</td> </td>
<td class="d-none"> <td class="d-none">
@if(FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->need_fulfilled == 1) {{ $__t('Enough in stock') }} @elseif(FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->need_fulfilled_with_shopping_list == 1) {{ $__t('Not enough in stock, but already on the shopping list') }} @else {{ $__t('Not enough in stock') }} @endif @if (FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->need_fulfilled == 1)
{{ $__t('Enough in stock') }}
@elseif(FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->need_fulfilled_with_shopping_list == 1)
{{ $__t('Not enough in stock, but already on the shopping list') }}
@else
{{ $__t('Not enough in stock') }}
@endif
</td> </td>
@include('components.userfields_tbody', array( @include('components.userfields_tbody', [
'userfields' => $userfields, 'userfields' => $userfields,
'userfieldValues' => FindAllObjectsInArrayByPropertyValue($userfieldValues, 'object_id', $recipe->id) 'userfieldValues' => FindAllObjectsInArrayByPropertyValue(
)) $userfieldValues,
'object_id',
$recipe->id
),
])
</tr> </tr>
@endforeach @endforeach
@ -215,15 +215,12 @@
</table> </table>
</div> </div>
<div class="tab-pane show" <div class="tab-pane show" id="gallery">
id="gallery">
<div class="card-columns no-gutters"> <div class="card-columns no-gutters">
@foreach ($recipes as $recipe) @foreach ($recipes as $recipe)
<a class="discrete-link recipe-gallery-item @if (FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->need_fulfilled == 1) recipe-enoughinstock @elseif(FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->need_fulfilled_with_shopping_list == 1) recipe-enoughinstockwithshoppinglist @else recipe-notenoughinstock @endif" <a class="discrete-link recipe-gallery-item @if (FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->need_fulfilled == 1) recipe-enoughinstock @elseif(FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->need_fulfilled_with_shopping_list == 1) recipe-enoughinstockwithshoppinglist @else recipe-notenoughinstock @endif"
data-recipe-id="{{ $recipe->id }}" data-recipe-id="{{ $recipe->id }}" href="#">
href="#"> <div id="RecipeGalleryCard-{{ $recipe->id }}" class="card recipe-card">
<div id="RecipeGalleryCard-{{ $recipe->id }}"
class="card recipe-card">
@if (!empty($recipe->picture_file_name)) @if (!empty($recipe->picture_file_name))
<img data-src="{{ $U('/api/files/recipepictures/' .base64_encode($recipe->picture_file_name) .'?force_serve_as=picture&best_fit_width=400') }}" <img data-src="{{ $U('/api/files/recipepictures/' .base64_encode($recipe->picture_file_name) .'?force_serve_as=picture&best_fit_width=400') }}"
class="card-img-top lazy"> class="card-img-top lazy">
@ -231,21 +228,29 @@
<div class="card-body text-center"> <div class="card-body text-center">
<h5 class="card-title mb-1">{{ $recipe->name }}</h5> <h5 class="card-title mb-1">{{ $recipe->name }}</h5>
<p class="card-text"> <p class="card-text">
@if(FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->need_fulfilled == 1)<i class="fas fa-check text-success"></i>@elseif(FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->need_fulfilled_with_shopping_list == 1)<i class="fas fa-exclamation text-warning"></i>@else<i class="fas fa-times text-danger"></i>@endif @if (FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->need_fulfilled == 1)
<span class="timeago-contextual">@if(FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->need_fulfilled == 1){{ $__t('Enough in stock') }}@elseif(FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->need_fulfilled_with_shopping_list == 1){{ $__n(FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->missing_products_count, 'Not enough in stock, %s ingredient missing but already on the shopping list', 'Not enough in stock, %s ingredients missing but already on the shopping list') }}@else{{ $__n(FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->missing_products_count, 'Not enough in stock, %s ingredient missing', 'Not enough in stock, %s ingredients missing') }}@endif</span> <i class="fas fa-check text-success"></i>
@elseif(FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->need_fulfilled_with_shopping_list == 1)
<i class="fas fa-exclamation text-warning"></i>@else<i
class="fas fa-times text-danger"></i>
@endif
<span class="timeago-contextual">
@if (FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->need_fulfilled == 1)
{{ $__t('Enough in stock') }}
@elseif(FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->need_fulfilled_with_shopping_list == 1)
{{ $__n(FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->missing_products_count,'Not enough in stock, %s ingredient missing but already on the shopping list','Not enough in stock, %s ingredients missing but already on the shopping list') }}@else{{ $__n(FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->missing_products_count,'Not enough in stock, %s ingredient missing','Not enough in stock, %s ingredients missing') }}
@endif
</span>
</p> </p>
<p class="card-text mt-2"> <p class="card-text mt-2">
<a class="btn btn-xs btn-outline-danger hide-when-embedded hide-on-fullscreen-card recipe-delete" <a class="btn btn-xs btn-outline-danger hide-when-embedded hide-on-fullscreen-card recipe-delete"
href="#" href="#" data-recipe-id="{{ $recipe->id }}"
data-recipe-id="{{ $recipe->id }}" data-recipe-name="{{ $recipe->name }}" data-toggle="tooltip"
data-recipe-name="{{ $recipe->name }}"
data-toggle="tooltip"
title="{{ $__t('Delete this item') }}"> title="{{ $__t('Delete this item') }}">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
</a> </a>
<a class="btn btn-outline-info btn-xs hide-when-embedded hide-on-fullscreen-card" <a class="btn btn-outline-info btn-xs hide-when-embedded hide-on-fullscreen-card"
href="{{ $U('/recipe/') }}{{ $recipe->id }}" href="{{ $U('/recipe/') }}{{ $recipe->id }}" data-toggle="tooltip"
data-toggle="tooltip"
title="{{ $__t('Edit this item') }}"> title="{{ $__t('Edit this item') }}">
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
</a> </a>
@ -265,16 +270,14 @@
array_unshift($allRecipes, $selectedRecipe); array_unshift($allRecipes, $selectedRecipe);
@endphp @endphp
<div class="col-12 col-md-6 print-view"> <div class="col-12 col-md-6 print-view">
<div id="selectedRecipeCard" <div id="selectedRecipeCard" class="card grocy-card">
class="card grocy-card">
@if (count($allRecipes) > 1) @if (count($allRecipes) > 1)
<div class="card-header card-header-fullscreen d-print-none"> <div class="card-header card-header-fullscreen d-print-none">
<ul class="nav nav-tabs grocy-tabs card-header-tabs"> <ul class="nav nav-tabs grocy-tabs card-header-tabs">
@foreach ($allRecipes as $index => $recipe) @foreach ($allRecipes as $index => $recipe)
<li class="nav-item"> <li class="nav-item">
<a class="nav-link @if ($index == 0) active @endif" <a class="nav-link @if ($index == 0) active @endif"
data-toggle="tab" data-toggle="tab" href="#recipe-{{ $index + 1 }}">{{ $recipe->name }}</a>
href="#recipe-{{ $index + 1 }}">{{ $recipe->name }}</a>
</li> </li>
@endforeach @endforeach
</ul> </ul>
@ -284,43 +287,37 @@
<div class="tab-content grocy-tabs print break"> <div class="tab-content grocy-tabs print break">
@foreach ($allRecipes as $index => $recipe) @foreach ($allRecipes as $index => $recipe)
<div class="tab-pane @if ($index == 0) active @endif" <div class="tab-pane @if ($index == 0) active @endif"
id="recipe-{{ $index + 1 }}" id="recipe-{{ $index + 1 }}" role="tabpanel">
role="tabpanel">
@if (!empty($recipe->picture_file_name)) @if (!empty($recipe->picture_file_name))
<img class="card-img-top lazy" <img class="card-img-top lazy"
src="{{ $U('/api/files/recipepictures/' . base64_encode($recipe->picture_file_name) . '?force_serve_as=picture') }}"> src="{{ $U('/api/files/recipepictures/' . base64_encode($recipe->picture_file_name) . '?force_serve_as=picture') }}">
@endif @endif
<div class="card-body"> <div class="card-body">
<div class="shadow p-4 mb-5 bg-white rounded mt-n5 d-print-none @if(empty($recipe->picture_file_name)) d-none @endif"> <div
class="shadow p-4 mb-5 bg-white rounded mt-n5 d-print-none @if (empty($recipe->picture_file_name)) d-none @endif">
<div class="d-flex justify-content-between align-items-center"> <div class="d-flex justify-content-between align-items-center">
<h3 class="card-title mb-0">{{ $recipe->name }}</h3> <h3 class="card-title mb-0">{{ $recipe->name }}</h3>
<div class="card-icons d-flex flex-wrap justify-content-end flex-shrink-1"> <div class="card-icons d-flex flex-wrap justify-content-end flex-shrink-1">
<a class="btn @if (!GROCY_FEATURE_FLAG_STOCK) d-none @endif recipe-consume @if (FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->need_fulfilled == 0) disabled @endif" <a class="btn @if (!GROCY_FEATURE_FLAG_STOCK) d-none @endif recipe-consume @if (FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->need_fulfilled == 0) disabled @endif"
href="#" href="#" data-toggle="tooltip"
data-toggle="tooltip"
title="{{ $__t('Consume all ingredients needed by this recipe') }}" title="{{ $__t('Consume all ingredients needed by this recipe') }}"
data-recipe-id="{{ $recipe->id }}" data-recipe-id="{{ $recipe->id }}"
data-recipe-name="{{ $recipe->name }}"> data-recipe-name="{{ $recipe->name }}">
<i class="fas fa-utensils"></i> <i class="fas fa-utensils"></i>
</a> </a>
<a class="btn @if (!GROCY_FEATURE_FLAG_STOCK) d-none @endif recipe-shopping-list @if (FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->need_fulfilled_with_shopping_list == 1) disabled @endif" <a class="btn @if (!GROCY_FEATURE_FLAG_STOCK) d-none @endif recipe-shopping-list @if (FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->need_fulfilled_with_shopping_list == 1) disabled @endif"
href="#" href="#" data-toggle="tooltip"
data-toggle="tooltip"
title="{{ $__t('Put missing products on shopping list') }}" title="{{ $__t('Put missing products on shopping list') }}"
data-recipe-id="{{ $recipe->id }}" data-recipe-id="{{ $recipe->id }}"
data-recipe-name="{{ $recipe->name }}"> data-recipe-name="{{ $recipe->name }}">
<i class="fas fa-cart-plus"></i> <i class="fas fa-cart-plus"></i>
</a> </a>
<a class="btn recipe-fullscreen hide-when-embedded" <a class="btn recipe-fullscreen hide-when-embedded"
id="selectedRecipeToggleFullscreenButton" id="selectedRecipeToggleFullscreenButton" href="#" data-toggle="tooltip"
href="#"
data-toggle="tooltip"
title="{{ $__t('Expand to fullscreen') }}"> title="{{ $__t('Expand to fullscreen') }}">
<i class="fas fa-expand-arrows-alt"></i> <i class="fas fa-expand-arrows-alt"></i>
</a> </a>
<a class="btn recipe-print" <a class="btn recipe-print" href="#" data-toggle="tooltip"
href="#"
data-toggle="tooltip"
title="{{ $__t('Print') }}"> title="{{ $__t('Print') }}">
<i class="fas fa-print"></i> <i class="fas fa-print"></i>
</a> </a>
@ -328,34 +325,30 @@
</div> </div>
</div> </div>
<div class="mb-4 @if(!empty($recipe->picture_file_name)) d-none @else d-flex @endif d-print-block justify-content-between align-items-center"> <div
class="mb-4 @if (!empty($recipe->picture_file_name)) d-none @else d-flex @endif d-print-block justify-content-between align-items-center">
<h1 class="card-title mb-0">{{ $recipe->name }}</h1> <h1 class="card-title mb-0">{{ $recipe->name }}</h1>
<div class="card-icons d-flex flex-wrap justify-content-end flex-shrink-1 d-print-none"> <div
class="card-icons d-flex flex-wrap justify-content-end flex-shrink-1 d-print-none">
<a class="btn recipe-consume @if (FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->need_fulfilled == 0) disabled @endif" <a class="btn recipe-consume @if (FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->need_fulfilled == 0) disabled @endif"
href="#" href="#" data-toggle="tooltip"
data-toggle="tooltip"
title="{{ $__t('Consume all ingredients needed by this recipe') }}" title="{{ $__t('Consume all ingredients needed by this recipe') }}"
data-recipe-id="{{ $recipe->id }}" data-recipe-id="{{ $recipe->id }}"
data-recipe-name="{{ $recipe->name }}"> data-recipe-name="{{ $recipe->name }}">
<i class="fas fa-utensils"></i> <i class="fas fa-utensils"></i>
</a> </a>
<a class="btn recipe-shopping-list @if (FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->need_fulfilled_with_shopping_list == 1) disabled @endif" <a class="btn recipe-shopping-list @if (FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->need_fulfilled_with_shopping_list == 1) disabled @endif"
href="#" href="#" data-toggle="tooltip"
data-toggle="tooltip"
title="{{ $__t('Put missing products on shopping list') }}" title="{{ $__t('Put missing products on shopping list') }}"
data-recipe-id="{{ $recipe->id }}" data-recipe-id="{{ $recipe->id }}"
data-recipe-name="{{ $recipe->name }}"> data-recipe-name="{{ $recipe->name }}">
<i class="fas fa-cart-plus"></i> <i class="fas fa-cart-plus"></i>
</a> </a>
<a class=" btnrecipe-fullscreen hide-when-embedded" <a class=" btnrecipe-fullscreen hide-when-embedded" href="#"
href="#" data-toggle="tooltip" title="{{ $__t('Expand to fullscreen') }}">
data-toggle="tooltip"
title="{{ $__t('Expand to fullscreen') }}">
<i class="fas fa-expand-arrows-alt"></i> <i class="fas fa-expand-arrows-alt"></i>
</a> </a>
<a class="btn recipe-print PrintRecipe" <a class="btn recipe-print PrintRecipe" href="#" data-toggle="tooltip"
href="#"
data-toggle="tooltip"
title="{{ $__t('Print') }}"> title="{{ $__t('Print') }}">
<i class="fas fa-print"></i> <i class="fas fa-print"></i>
</a> </a>
@ -372,27 +365,27 @@
<div class="col-4"> <div class="col-4">
<label>{{ $__t('Energy (kcal)') }}</label>&nbsp; <label>{{ $__t('Energy (kcal)') }}</label>&nbsp;
<i class="fas fa-question-circle text-muted d-print-none" <i class="fas fa-question-circle text-muted d-print-none"
data-toggle="tooltip" data-toggle="tooltip" data-trigger="hover click"
data-trigger="hover click"
title="{{ $__t('per serving') }}"></i> title="{{ $__t('per serving') }}"></i>
<h3 class="locale-number locale-number-generic pt-0">{{ $calories }}</h3> <h3 class="locale-number locale-number-generic pt-0">{{ $calories }}
</h3>
</div> </div>
@endif @endif
@if (GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) @if (GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)
<div class="col-4"> <div class="col-4">
<label>{{ $__t('Costs') }}&nbsp; <label>{{ $__t('Costs') }}&nbsp;
<i class="fas fa-question-circle text-muted d-print-none" <i class="fas fa-question-circle text-muted d-print-none"
data-toggle="tooltip" data-toggle="tooltip" data-trigger="hover click"
data-trigger="hover click"
title="{{ $__t('Based on the prices of the default consume rule (Opened first, then first due first, then first in first out) for in-stock ingredients and on the last price for missing ones') }}"></i> title="{{ $__t('Based on the prices of the default consume rule (Opened first, then first due first, then first in first out) for in-stock ingredients and on the last price for missing ones') }}"></i>
</label> </label>
<h3 class="locale-number locale-number-currency pt-0">{{ $costs }}</h3> <h3 class="locale-number locale-number-currency pt-0">{{ $costs }}
</h3>
</div> </div>
@endif @endif
@if ($index == 0) @if ($index == 0)
<div class="col-4 d-print-none"> <div class="col-4 d-print-none">
@include('components.numberpicker', array( @include('components.numberpicker', [
'id' => 'servings-scale', 'id' => 'servings-scale',
'label' => 'Desired servings', 'label' => 'Desired servings',
'min' => $DEFAULT_MIN_AMOUNT, 'min' => $DEFAULT_MIN_AMOUNT,
@ -400,8 +393,9 @@
'value' => $recipe->desired_servings, 'value' => $recipe->desired_servings,
'additionalAttributes' => 'data-recipe-id="' . $recipe->id . '"', 'additionalAttributes' => 'data-recipe-id="' . $recipe->id . '"',
'hint' => $__t('Base: %s', $recipe->base_servings), 'hint' => $__t('Base: %s', $recipe->base_servings),
'additionalCssClasses' => 'locale-number-input locale-number-quantity-amount' 'additionalCssClasses' =>
)) 'locale-number-input locale-number-quantity-amount',
])
</div> </div>
@endif @endif
</div> </div>
@ -410,12 +404,10 @@
$recipePositionsFiltered = FindAllObjectsInArrayByPropertyValue($allRecipePositions[$recipe->id], 'recipe_id', $recipe->id); $recipePositionsFiltered = FindAllObjectsInArrayByPropertyValue($allRecipePositions[$recipe->id], 'recipe_id', $recipe->id);
@endphp @endphp
<ul class="nav nav-tabs grocy-tabs mb-3 d-print-none" <ul class="nav nav-tabs grocy-tabs mb-3 d-print-none" role="tablist">
role="tablist">
@if (count($recipePositionsFiltered) > 0) @if (count($recipePositionsFiltered) > 0)
<li class="nav-item"> <li class="nav-item">
<a class="nav-link active" <a class="nav-link active" data-toggle="tab"
data-toggle="tab"
href="#ingredients-{{ $index }}" href="#ingredients-{{ $index }}"
role="tab">{{ $__t('Ingredients') }}</a> role="tab">{{ $__t('Ingredients') }}</a>
</li> </li>
@ -423,8 +415,7 @@
@if (!empty($recipe->description)) @if (!empty($recipe->description))
<li class="nav-item"> <li class="nav-item">
<a class="nav-link @if (count($recipePositionsFiltered) == 0) active @endif" <a class="nav-link @if (count($recipePositionsFiltered) == 0) active @endif"
data-toggle="tab" data-toggle="tab" href="#prep-{{ $index }}"
href="#prep-{{ $index }}"
role="tab">{{ $__t('Preparation') }}</a> role="tab">{{ $__t('Preparation') }}</a>
</li> </li>
@endif @endif
@ -432,8 +423,7 @@
<div class="tab-content grocy-tabs p-2 print"> <div class="tab-content grocy-tabs p-2 print">
@if (count($recipePositionsFiltered) > 0) @if (count($recipePositionsFiltered) > 0)
<div class="tab-pane active" <div class="tab-pane active" id="ingredients-{{ $index }}"
id="ingredients-{{ $index }}"
role="tabpanel"> role="tabpanel">
<div class="mb-2 mt-3 d-none d-print-block"> <div class="mb-2 mt-3 d-none d-print-block">
<h3 class="mb-0">{{ $__t('Ingredients') }}</h3> <h3 class="mb-0">{{ $__t('Ingredients') }}</h3>
@ -448,42 +438,71 @@
@foreach ($recipePositionsFiltered as $selectedRecipePosition) @foreach ($recipePositionsFiltered as $selectedRecipePosition)
@if ($lastIngredientGroup != $selectedRecipePosition->ingredient_group && !empty($selectedRecipePosition->ingredient_group)) @if ($lastIngredientGroup != $selectedRecipePosition->ingredient_group && !empty($selectedRecipePosition->ingredient_group))
@php $hasIngredientGroups = true; @endphp @php $hasIngredientGroups = true; @endphp
<h5 class="mb-2 mt-2 ml-1"><strong>{{ $selectedRecipePosition->ingredient_group }}</strong></h5> <h5 class="mb-2 mt-2 ml-1">
<strong>{{ $selectedRecipePosition->ingredient_group }}</strong>
</h5>
@endif @endif
@if (boolval($userSettings['recipe_ingredients_group_by_product_group']) && $lastProductGroup != $selectedRecipePosition->product_group && !empty($selectedRecipePosition->product_group)) @if (boolval($userSettings['recipe_ingredients_group_by_product_group']) && $lastProductGroup != $selectedRecipePosition->product_group && !empty($selectedRecipePosition->product_group))
@php $hasProductGroups = true; @endphp @php $hasProductGroups = true; @endphp
<h6 class="mb-2 mt-2 @if($hasIngredientGroups) ml-3 @else ml-1 @endif"><strong>{{ $selectedRecipePosition->product_group }}</strong></h6> <h6
class="mb-2 mt-2 @if ($hasIngredientGroups) ml-3 @else ml-1 @endif">
<strong>{{ $selectedRecipePosition->product_group }}</strong>
</h6>
@endif @endif
<li class="list-group-item px-0 @if($hasIngredientGroups && $hasProductGroups) ml-4 @elseif($hasIngredientGroups || $hasProductGroups) ml-2 @else ml-0 @endif"> <li
class="list-group-item px-0 @if ($hasIngredientGroups && $hasProductGroups) ml-4 @elseif($hasIngredientGroups || $hasProductGroups) ml-2 @else ml-0 @endif">
@if ($selectedRecipePosition->product_active == 0) @if ($selectedRecipePosition->product_active == 0)
<div class="small text-muted font-italic">{{ $__t('Disabled') }}</div> <div class="small text-muted font-italic">
{{ $__t('Disabled') }}</div>
@endif @endif
@php @php
$product = FindObjectInArrayByPropertyValue($products, 'id', $selectedRecipePosition->product_id); $product = FindObjectInArrayByPropertyValue($products, 'id', $selectedRecipePosition->product_id);
$productQuConversions = FindAllObjectsInArrayByPropertyValue($quantityUnitConversionsResolved, 'product_id', $product->id); $productQuConversions = FindAllObjectsInArrayByPropertyValue($quantityUnitConversionsResolved, 'product_id', $product->id);
$productQuConversions = FindAllObjectsInArrayByPropertyValue($productQuConversions, 'from_qu_id', $product->qu_id_stock); $productQuConversions = FindAllObjectsInArrayByPropertyValue($productQuConversions, 'from_qu_id', $product->qu_id_stock);
$productQuConversion = FindObjectInArrayByPropertyValue($productQuConversions, 'to_qu_id', $selectedRecipePosition->qu_id); $productQuConversion = FindObjectInArrayByPropertyValue($productQuConversions, 'to_qu_id', $selectedRecipePosition->qu_id);
if ($productQuConversion && $selectedRecipePosition->only_check_single_unit_in_stock == 0) if ($productQuConversion && $selectedRecipePosition->only_check_single_unit_in_stock == 0) {
{
$selectedRecipePosition->recipe_amount = $selectedRecipePosition->recipe_amount * $productQuConversion->factor; $selectedRecipePosition->recipe_amount = $selectedRecipePosition->recipe_amount * $productQuConversion->factor;
} }
@endphp @endphp
@if (!empty($selectedRecipePosition->recipe_variable_amount)) @if (!empty($selectedRecipePosition->recipe_variable_amount))
{{ $selectedRecipePosition->recipe_variable_amount }} {{ $selectedRecipePosition->recipe_variable_amount }}
@else @else
<span class="locale-number locale-number-quantity-amount">@if($selectedRecipePosition->recipe_amount == round($selectedRecipePosition->recipe_amount, 2)){{ round($selectedRecipePosition->recipe_amount, 2) }}@else{{ $selectedRecipePosition->recipe_amount }}@endif</span> <span class="locale-number locale-number-quantity-amount">
@if ($selectedRecipePosition->recipe_amount == round($selectedRecipePosition->recipe_amount, 2))
{{ round($selectedRecipePosition->recipe_amount, 2) }}@else{{ $selectedRecipePosition->recipe_amount }}
@endif @endif
{{ $__n($selectedRecipePosition->recipe_amount, FindObjectInArrayByPropertyValue($quantityUnits, 'id', $selectedRecipePosition->qu_id)->name, FindObjectInArrayByPropertyValue($quantityUnits, 'id', $selectedRecipePosition->qu_id)->name_plural) }} {{ FindObjectInArrayByPropertyValue($products, 'id', $selectedRecipePosition->product_id)->name }}
@if(GROCY_FEATURE_FLAG_STOCK)
<span class="d-print-none">
@if($selectedRecipePosition->need_fulfilled == 1)<i class="fas fa-check text-success"></i>@elseif($selectedRecipePosition->need_fulfilled_with_shopping_list == 1)<i class="fas fa-exclamation text-warning"></i>@else<i class="fas fa-times text-danger"></i>@endif
<span class="timeago-contextual">@if(FindObjectInArrayByPropertyValue($recipePositionsResolved, 'recipe_pos_id', $selectedRecipePosition->id)->need_fulfilled == 1) {{ $__t('Enough in stock') }} @else {{ $__t('Not enough in stock, %1$s missing, %2$s already on shopping list', round($selectedRecipePosition->missing_amount, 2), round($selectedRecipePosition->amount_on_shopping_list, 2)) }} @endif</span>
</span> </span>
@endif @endif
@if($selectedRecipePosition->need_fulfilled == 1 && GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) <span class="float-right font-italic ml-2 locale-number locale-number-currency">{{ $selectedRecipePosition->costs }}</span> @endif {{ $__n($selectedRecipePosition->recipe_amount,FindObjectInArrayByPropertyValue($quantityUnits, 'id', $selectedRecipePosition->qu_id)->name,FindObjectInArrayByPropertyValue($quantityUnits, 'id', $selectedRecipePosition->qu_id)->name_plural) }}
<span class="float-right font-italic"><span class="locale-number locale-number-generic">{{ $selectedRecipePosition->calories }}</span> {{ $__t('Calories') }}</span> {{ FindObjectInArrayByPropertyValue($products, 'id', $selectedRecipePosition->product_id)->name }}
@if (GROCY_FEATURE_FLAG_STOCK)
<span class="d-print-none">
@if ($selectedRecipePosition->need_fulfilled == 1)
<i class="fas fa-check text-success"></i>
@elseif($selectedRecipePosition->need_fulfilled_with_shopping_list == 1)
<i
class="fas fa-exclamation text-warning"></i>@else<i
class="fas fa-times text-danger"></i>
@endif
<span class="timeago-contextual">
@if (FindObjectInArrayByPropertyValue($recipePositionsResolved, 'recipe_pos_id', $selectedRecipePosition->id)->need_fulfilled == 1)
{{ $__t('Enough in stock') }}
@else
{{ $__t('Not enough in stock, %1$s missing, %2$s already on shopping list',round($selectedRecipePosition->missing_amount, 2),round($selectedRecipePosition->amount_on_shopping_list, 2)) }}
@endif
</span>
</span>
@endif
@if ($selectedRecipePosition->need_fulfilled == 1 && GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)
<span
class="float-right font-italic ml-2 locale-number locale-number-currency">{{ $selectedRecipePosition->costs }}</span>
@endif
<span class="float-right font-italic"><span
class="locale-number locale-number-generic">{{ $selectedRecipePosition->calories }}</span>
{{ $__t('Calories') }}</span>
@if (!empty($selectedRecipePosition->recipe_variable_amount)) @if (!empty($selectedRecipePosition->recipe_variable_amount))
<div class="small text-muted font-italic">{{ $__t('Variable amount') }}</div> <div class="small text-muted font-italic">
{{ $__t('Variable amount') }}</div>
@endif @endif
@if (!empty($selectedRecipePosition->note)) @if (!empty($selectedRecipePosition->note))
@ -497,8 +516,7 @@
</div> </div>
@endif @endif
<div class="tab-pane @if (count($recipePositionsFiltered) == 0) active @endif" <div class="tab-pane @if (count($recipePositionsFiltered) == 0) active @endif"
id="prep-{{ $index }}" id="prep-{{ $index }}" role="tabpanel">
role="tabpanel">
<div class="mb-2 d-none d-print-block"> <div class="mb-2 d-none d-print-block">
<h3 class="mb-0">{{ $__t('Preparation') }}</h3> <h3 class="mb-0">{{ $__t('Preparation') }}</h3>
</div> </div>
@ -510,16 +528,14 @@
</div> </div>
</div> </div>
<div id="missing-recipe-pos-list" <div id="missing-recipe-pos-list" class="list-group d-none mt-3">
class="list-group d-none mt-3">
@foreach ($recipePositionsResolved as $recipePos) @foreach ($recipePositionsResolved as $recipePos)
@if (in_array($recipePos->recipe_id, $includedRecipeIdsAbsolute) && $recipePos->missing_amount > 0) @if (in_array($recipePos->recipe_id, $includedRecipeIdsAbsolute) && $recipePos->missing_amount > 0)
<a href="#" <a href="#"
class="list-group-item list-group-item-action list-group-item-primary missing-recipe-pos-select-button"> class="list-group-item list-group-item-action list-group-item-primary missing-recipe-pos-select-button">
<div class="form-check form-check-inline"> <div class="form-check form-check-inline">
<input class="form-check-input missing-recipe-pos-product-checkbox" <input class="form-check-input missing-recipe-pos-product-checkbox"
type="checkbox" type="checkbox" data-product-id="{{ $recipePos->product_id }}"
data-product-id="{{ $recipePos->product_id }}"
checked> checked>
</div> </div>
{{ FindObjectInArrayByPropertyValue($products, 'id', $recipePos->product_id)->name }} {{ FindObjectInArrayByPropertyValue($products, 'id', $recipePos->product_id)->name }}

View File

@ -5,13 +5,13 @@
@section('viewJsName', 'shoppinglist') @section('viewJsName', 'shoppinglist')
@push('pageScripts') @push('pageScripts')
<script src="{{ $U('/node_modules/bwip-js/dist/bwip-js-min.js?v=', true) }}{{ $version }}"></script> <script src="{{ $U('/node_modules/bwip-js/dist/bwip-js-min.js?v=', true) }}{{ $version }}">
</script>
<script src="{{ $U('/viewjs/purchase.js?v=', true) }}{{ $version }}"></script> <script src="{{ $U('/viewjs/purchase.js?v=', true) }}{{ $version }}"></script>
@endpush @endpush
@push('pageStyles') @push('pageStyles')
<link href="{{ $U('/node_modules/animate.css/animate.min.css?v=', true) }}{{ $version }}" <link href="{{ $U('/node_modules/animate.css/animate.min.css?v=', true) }}{{ $version }}" rel="stylesheet">
rel="stylesheet">
@endpush @endpush
@section('content') @section('content')
@ -31,27 +31,23 @@
href="{{ $U('/shoppinglistitem/new?embedded&list=' . $selectedShoppingListId) }}"> href="{{ $U('/shoppinglistitem/new?embedded&list=' . $selectedShoppingListId) }}">
{{ $__t('Add item') }} {{ $__t('Add item') }}
</button> </button>
<button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" <button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" type="button"
type="button" data-toggle="collapse" data-target="#table-filter-row">
data-toggle="collapse"
data-target="#table-filter-row">
<i class="fas fa-filter"></i> <i class="fas fa-filter"></i>
</button> </button>
<button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" <button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" type="button"
type="button" data-toggle="collapse" data-target="#related-links">
data-toggle="collapse"
data-target="#related-links">
<i class="fas fa-ellipsis-v"></i> <i class="fas fa-ellipsis-v"></i>
</button> </button>
</div> </div>
<div class="related-links collapse d-md-flex order-2 width-xs-sm-100" <div class="related-links collapse d-md-flex order-2 width-xs-sm-100" id="related-links">
id="related-links">
@if (GROCY_FEATURE_FLAG_SHOPPINGLIST_MULTIPLE_LISTS) @if (GROCY_FEATURE_FLAG_SHOPPINGLIST_MULTIPLE_LISTS)
<div class="my-auto float-right"> <div class="my-auto float-right">
<select class="custom-control custom-select custom-select-sm" {{-- TODO: Select2: dynamic data: shopping_lists --}}
id="selected-shopping-list"> <select class="custom-control custom-select custom-select-sm" id="selected-shopping-list">
@foreach ($shoppingLists as $shoppingList) @foreach ($shoppingLists as $shoppingList)
<option @if($shoppingList->id == $selectedShoppingListId) selected="selected" @endif value="{{ $shoppingList->id }}">{{ $shoppingList->name }}</option> <option @if ($shoppingList->id == $selectedShoppingListId) selected="selected" @endif
value="{{ $shoppingList->id }}">{{ $shoppingList->name }}</option>
@endforeach @endforeach
</select> </select>
</div> </div>
@ -69,25 +65,22 @@
{{ $__t('Delete shopping list') }} {{ $__t('Delete shopping list') }}
</a> </a>
@else @else
<input type="hidden" <input type="hidden" name="selected-shopping-list" id="selected-shopping-list" value="1">
name="selected-shopping-list"
id="selected-shopping-list"
value="1">
@endif @endif
<a id="print-shopping-list-button" <a id="print-shopping-list-button"
class="btn btn-outline-dark responsive-button m-1 mt-md-0 mb-md-0 float-right" class="btn btn-outline-dark responsive-button m-1 mt-md-0 mb-md-0 float-right" href="#">
href="#">
{{ $__t('Print') }} {{ $__t('Print') }}
</a> </a>
</div> </div>
</div> </div>
<div id="filter-container" <div id="filter-container" class="border-top border-bottom my-2 py-1">
class="border-top border-bottom my-2 py-1"> <div id="table-filter-row" data-status-filter="belowminstockamount"
<div id="table-filter-row" class="collapse normal-message status-filter-message responsive-button @if (!GROCY_FEATURE_FLAG_STOCK) d-none @else d-md-inline-block @endif">
data-status-filter="belowminstockamount" <span class="d-block d-md-none">{{ count($missingProducts) }} <i
class="collapse normal-message status-filter-message responsive-button @if(!GROCY_FEATURE_FLAG_STOCK) d-none @else d-md-inline-block @endif"><span class="d-block d-md-none">{{count($missingProducts)}} <i class="fas fa-exclamation-circle"></i></span><span class="d-none d-md-block">{{ $__n(count($missingProducts), '%s product is below defined min. stock amount', '%s products are below defined min. stock amount') }}</span></div> class="fas fa-exclamation-circle"></i></span><span
<div id="related-links" class="d-none d-md-block">{{ $__n(count($missingProducts),'%s product is below defined min. stock amount','%s products are below defined min. stock amount') }}</span>
class="float-right mt-1 collapse d-md-block"> </div>
<div id="related-links" class="float-right mt-1 collapse d-md-block">
<a class="btn btn-primary responsive-button btn-sm mb-1 show-as-dialog-link d-none d-md-inline-block" <a class="btn btn-primary responsive-button btn-sm mb-1 show-as-dialog-link d-none d-md-inline-block"
href="{{ $U('/shoppinglistitem/new?embedded&list=' . $selectedShoppingListId) }}"> href="{{ $U('/shoppinglistitem/new?embedded&list=' . $selectedShoppingListId) }}">
{{ $__t('Add item') }} {{ $__t('Add item') }}
@ -117,17 +110,13 @@
</div> </div>
</div> </div>
<div class="row collapse d-md-flex d-print-none hide-on-fullscreen-card" <div class="row collapse d-md-flex d-print-none hide-on-fullscreen-card" id="table-filter-row">
id="table-filter-row">
<div class="col-12 col-md-5"> <div class="col-12 col-md-5">
<div class="input-group"> <div class="input-group">
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-search"></i></span> <span class="input-group-text"><i class="fas fa-search"></i></span>
</div> </div>
<input type="text" <input type="text" id="search" class="form-control" placeholder="{{ $__t('Search') }}">
id="search"
class="form-control"
placeholder="{{ $__t('Search') }}">
</div> </div>
</div> </div>
<div class="col-12 col-md-4 col-lg-5"> <div class="col-12 col-md-4 col-lg-5">
@ -135,11 +124,10 @@
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-filter"></i>&nbsp;{{ $__t('Status') }}</span> <span class="input-group-text"><i class="fas fa-filter"></i>&nbsp;{{ $__t('Status') }}</span>
</div> </div>
<select class="custom-control custom-select" <select class="custom-control custom-select" id="status-filter">
id="status-filter">
<option value="all">{{ $__t('All') }}</option> <option value="all">{{ $__t('All') }}</option>
<option class="@if(!GROCY_FEATURE_FLAG_STOCK) d-none @endif" <option class="@if (!GROCY_FEATURE_FLAG_STOCK) d-none @endif" value="belowminstockamount">
value="belowminstockamount">{{ $__t('Below min. stock amount') }}</option> {{ $__t('Below min. stock amount') }}</option>
<option value="xxDONExx">{{ $__t('Only done items') }}</option> <option value="xxDONExx">{{ $__t('Only done items') }}</option>
<option value="xxUNDONExx">{{ $__t('Only undone items') }}</option> <option value="xxUNDONExx">{{ $__t('Only undone items') }}</option>
</select> </select>
@ -147,44 +135,40 @@
</div> </div>
<div class="col"> <div class="col">
<div class="float-right"> <div class="float-right">
<a id="clear-filter-button" <a id="clear-filter-button" class="btn btn-sm btn-outline-info" href="#">
class="btn btn-sm btn-outline-info"
href="#">
{{ $__t('Clear filter') }} {{ $__t('Clear filter') }}
</a> </a>
</div> </div>
</div> </div>
</div> </div>
<div id="shoppinglist-main" <div id="shoppinglist-main" class="row d-print-none">
class="row d-print-none">
<div class="@if (boolval($userSettings['shopping_list_show_calendar'])) col-12 col-md-8 @else col-12 @endif pb-3"> <div class="@if (boolval($userSettings['shopping_list_show_calendar'])) col-12 col-md-8 @else col-12 @endif pb-3">
<table id="shoppinglist-table" {{-- TODO: DataTables: dynamic data: shopping_lists --}}
class="table table-sm table-striped nowrap w-100"> <table id="shoppinglist-table" class="table table-sm table-striped nowrap w-100">
<thead> <thead>
<tr> <tr>
<th class="border-right"><a class="text-muted change-table-columns-visibility-button" <th class="border-right"><a class="text-muted change-table-columns-visibility-button"
data-toggle="tooltip" data-toggle="tooltip" data-toggle="tooltip" title="{{ $__t('Table options') }}"
data-toggle="tooltip" data-table-selector="#shoppinglist-table" href="#"><i class="fas fa-eye"></i></a>
title="{{ $__t('Table options') }}"
data-table-selector="#shoppinglist-table"
href="#"><i class="fas fa-eye"></i></a>
</th> </th>
<th class="allow-grouping">{{ $__t('Product') }} / <em>{{ $__t('Note') }}</em></th> <th class="allow-grouping">{{ $__t('Product') }} / <em>{{ $__t('Note') }}</em></th>
<th>{{ $__t('Amount') }}</th> <th>{{ $__t('Amount') }}</th>
<th class="allow-grouping">{{ $__t('Product group') }}</th> <th class="allow-grouping">{{ $__t('Product group') }}</th>
<th class="d-none">Hidden status</th> <th class="d-none">Hidden status</th>
<th class="@if (!GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) d-none @endif">{{ $__t('Last price (Unit)') }}</th> <th class="@if (!GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) d-none @endif">{{ $__t('Last price (Unit)') }}</th>
<th class="@if(!GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) d-none @endif">{{ $__t('Last price (Total)') }}</th> <th class="@if (!GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) d-none @endif">{{ $__t('Last price (Total)') }}
<th class="@if(!GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) d-none @endif allow-grouping">{{ $__t('Default store') }}</th> </th>
<th class="@if (!GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) d-none @endif allow-grouping">
{{ $__t('Default store') }}</th>
<th>{{ $__t('Barcodes') }}</th> <th>{{ $__t('Barcodes') }}</th>
@include('components.userfields_thead', array( @include('components.userfields_thead', [
'userfields' => $userfields 'userfields' => $userfields,
)) ])
@include('components.userfields_thead', array( @include('components.userfields_thead', [
'userfields' => $productUserfields 'userfields' => $productUserfields,
)) ])
</tr> </tr>
</thead> </thead>
@ -193,28 +177,20 @@
<tr id="shoppinglistitem-{{ $listItem->id }}-row" <tr id="shoppinglistitem-{{ $listItem->id }}-row"
class="@if (FindObjectInArrayByPropertyValue($missingProducts, 'id', $listItem->product_id) !== null) table-info @endif @if ($listItem->done == 1) text-muted text-strike-through @endif"> class="@if (FindObjectInArrayByPropertyValue($missingProducts, 'id', $listItem->product_id) !== null) table-info @endif @if ($listItem->done == 1) text-muted text-strike-through @endif">
<td class="fit-content border-right"> <td class="fit-content border-right">
<a class="btn btn-success btn-sm order-listitem-button" <a class="btn btn-success btn-sm order-listitem-button" href="#"
href="#" data-item-id="{{ $listItem->id }}" data-item-done="{{ $listItem->done }}"
data-item-id="{{ $listItem->id }}" data-toggle="tooltip" data-placement="right"
data-item-done="{{ $listItem->done }}"
data-toggle="tooltip"
data-placement="right"
title="{{ $__t('Mark this item as done') }}"> title="{{ $__t('Mark this item as done') }}">
<i class="fas fa-check"></i> <i class="fas fa-check"></i>
</a> </a>
<a class="btn btn-sm btn-info show-as-dialog-link" <a class="btn btn-sm btn-info show-as-dialog-link"
href="{{ $U('/shoppinglistitem/' . $listItem->id . '?embedded&list=' . $selectedShoppingListId) }}" href="{{ $U('/shoppinglistitem/' . $listItem->id . '?embedded&list=' . $selectedShoppingListId) }}"
data-toggle="tooltip" data-toggle="tooltip" data-placement="right" title="{{ $__t('Edit this item') }}">
data-placement="right"
title="{{ $__t('Edit this item') }}">
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
</a> </a>
<a class="btn btn-sm btn-danger shoppinglist-delete-button" <a class="btn btn-sm btn-danger shoppinglist-delete-button" href="#"
href="#" data-shoppinglist-id="{{ $listItem->id }}" data-toggle="tooltip"
data-shoppinglist-id="{{ $listItem->id }}" data-placement="right" title="{{ $__t('Delete this item') }}">
data-toggle="tooltip"
data-placement="right"
title="{{ $__t('Delete this item') }}">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
</a> </a>
<a class="btn btn-sm btn-primary @if (!GROCY_FEATURE_FLAG_STOCK) d-none @endif @if (empty($listItem->product_id)) disabled @else shopping-list-stock-add-workflow-list-item-button @endif" <a class="btn btn-sm btn-primary @if (!GROCY_FEATURE_FLAG_STOCK) d-none @endif @if (empty($listItem->product_id)) disabled @else shopping-list-stock-add-workflow-list-item-button @endif"
@ -223,9 +199,12 @@
<i class="fas fa-box"></i> <i class="fas fa-box"></i>
</a> </a>
</td> </td>
<td class="product-name-cell cursor-link" <td class="product-name-cell cursor-link" data-product-id="{{ $listItem->product_id }}">
data-product-id="{{ $listItem->product_id }}"> @if (!empty($listItem->product_id))
@if(!empty($listItem->product_id)) {{ $listItem->product_name }}<br>@endif<em>{!! nl2br($listItem->note) !!}</em> {{ $listItem->product_name }}<br>
@endif
<em>
{!! nl2br($listItem->note) !!}</em>
</td> </td>
@if (!empty($listItem->product_id)) @if (!empty($listItem->product_id))
@php @php
@ -234,29 +213,41 @@
$productQuConversions = FindAllObjectsInArrayByPropertyValue($quantityUnitConversionsResolved, 'product_id', $product->id); $productQuConversions = FindAllObjectsInArrayByPropertyValue($quantityUnitConversionsResolved, 'product_id', $product->id);
$productQuConversions = FindAllObjectsInArrayByPropertyValue($productQuConversions, 'from_qu_id', $product->qu_id_stock); $productQuConversions = FindAllObjectsInArrayByPropertyValue($productQuConversions, 'from_qu_id', $product->qu_id_stock);
$productQuConversion = FindObjectInArrayByPropertyValue($productQuConversions, 'to_qu_id', $listItem->qu_id); $productQuConversion = FindObjectInArrayByPropertyValue($productQuConversions, 'to_qu_id', $listItem->qu_id);
if ($productQuConversion) if ($productQuConversion) {
{
$listItem->amount = $listItem->amount * $productQuConversion->factor; $listItem->amount = $listItem->amount * $productQuConversion->factor;
} }
@endphp @endphp
@endif @endif
<td data-order={{ <td data-order={{ <td data-order=$listItem->amount }}>
$listItem->amount }}> <span class="locale-number locale-number-quantity-amount">{{ $listItem->amount }}</span>
<span class="locale-number locale-number-quantity-amount">{{ $listItem->amount }}</span> @if(!empty($listItem->product_id)){{ $__n($listItem->amount, $listItem->qu_name, $listItem->qu_name_plural, true) }}@endif @if (!empty($listItem->product_id))
{{ $__n($listItem->amount, $listItem->qu_name, $listItem->qu_name_plural, true) }}
@endif
</td> </td>
<td> <td>
@if(!empty($listItem->product_group_name)) {{ $listItem->product_group_name }} @else <span class="font-italic font-weight-light">{{ $__t('Ungrouped') }}</span> @endif @if (!empty($listItem->product_group_name))
{{ $listItem->product_group_name }}
@else
<span class="font-italic font-weight-light">{{ $__t('Ungrouped') }}</span>
@endif
</td> </td>
<td id="shoppinglistitem-{{ $listItem->id }}-status-info" <td id="shoppinglistitem-{{ $listItem->id }}-status-info" class="d-none">
class="d-none"> @if (FindObjectInArrayByPropertyValue($missingProducts, 'id', $listItem->product_id) !== null)
@if(FindObjectInArrayByPropertyValue($missingProducts, 'id', $listItem->product_id) !== null) belowminstockamount @endif belowminstockamount
@if($listItem->done == 1) xxDONExx @else xxUNDONExx @endif @endif
@if ($listItem->done == 1)
xxDONExx
@else
xxUNDONExx
@endif
</td> </td>
<td class="@if (!GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) d-none @endif"> <td class="@if (!GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) d-none @endif">
<span class="locale-number locale-number-currency">{{ $listItem->last_price_unit }}</span> <span
class="locale-number locale-number-currency">{{ $listItem->last_price_unit }}</span>
</td> </td>
<td class="@if (!GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) d-none @endif"> <td class="@if (!GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) d-none @endif">
<span class="locale-number locale-number-currency">{{ $listItem->last_price_total }}</span> <span
class="locale-number locale-number-currency">{{ $listItem->last_price_total }}</span>
</td> </td>
<td class="@if (!GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) d-none @endif"> <td class="@if (!GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) d-none @endif">
{{ $listItem->default_shopping_location_name }} {{ $listItem->default_shopping_location_name }}
@ -264,20 +255,27 @@
<td> <td>
@foreach (explode(',', $listItem->product_barcodes) as $barcode) @foreach (explode(',', $listItem->product_barcodes) as $barcode)
@if (!empty($barcode)) @if (!empty($barcode))
<img class="barcode img-fluid pr-2" <img class="barcode img-fluid pr-2" data-barcode="{{ $barcode }}">
data-barcode="{{ $barcode }}">
@endif @endif
@endforeach @endforeach
</td> </td>
@include('components.userfields_tbody', array( @include('components.userfields_tbody', [
'userfields' => $userfields, 'userfields' => $userfields,
'userfieldValues' => FindAllObjectsInArrayByPropertyValue($userfieldValues, 'object_id', $listItem->id) 'userfieldValues' => FindAllObjectsInArrayByPropertyValue(
)) $userfieldValues,
@include('components.userfields_tbody', array( 'object_id',
$listItem->id
),
])
@include('components.userfields_tbody', [
'userfields' => $productUserfields, 'userfields' => $productUserfields,
'userfieldValues' => FindAllObjectsInArrayByPropertyValue($productUserfieldValues, 'object_id', $listItem->product_id) 'userfieldValues' => FindAllObjectsInArrayByPropertyValue(
)) $productUserfieldValues,
'object_id',
$listItem->product_id
),
])
</tr> </tr>
@endforeach @endforeach
@ -293,40 +291,27 @@
<div class="@if (boolval($userSettings['shopping_list_show_calendar'])) col-12 col-md-8 @else col-12 @endif d-print-none pt-2"> <div class="@if (boolval($userSettings['shopping_list_show_calendar'])) col-12 col-md-8 @else col-12 @endif d-print-none pt-2">
<div class="form-group"> <div class="form-group">
<label class="text-larger font-weight-bold" <label class="text-larger font-weight-bold" for="notes">{{ $__t('Notes') }}</label>
for="notes">{{ $__t('Notes') }}</label> <a id="save-description-button" class="btn btn-success btn-sm ml-1 mb-2" href="#">{{ $__t('Save') }}</a>
<a id="save-description-button" <a id="clear-description-button" class="btn btn-danger btn-sm ml-1 mb-2" href="#">{{ $__t('Clear') }}</a>
class="btn btn-success btn-sm ml-1 mb-2" <textarea class="form-control wysiwyg-editor" id="description"
href="#">{{ $__t('Save') }}</a>
<a id="clear-description-button"
class="btn btn-danger btn-sm ml-1 mb-2"
href="#">{{ $__t('Clear') }}</a>
<textarea class="form-control wysiwyg-editor"
id="description"
name="description">{{ FindObjectInArrayByPropertyValue($shoppingLists, 'id', $selectedShoppingListId)->description }}</textarea> name="description">{{ FindObjectInArrayByPropertyValue($shoppingLists, 'id', $selectedShoppingListId)->description }}</textarea>
</div> </div>
</div> </div>
</div> </div>
<div class="modal fade" <div class="modal fade" id="shopping-list-stock-add-workflow-modal" tabindex="-1">
id="shopping-list-stock-add-workflow-modal"
tabindex="-1">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content text-center"> <div class="modal-content text-center">
<div class="modal-body"> <div class="modal-body">
<iframe id="shopping-list-stock-add-workflow-purchase-form-frame" <iframe id="shopping-list-stock-add-workflow-purchase-form-frame" class="embed-responsive"
class="embed-responsive"
src=""></iframe> src=""></iframe>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<span id="shopping-list-stock-add-workflow-purchase-item-count" <span id="shopping-list-stock-add-workflow-purchase-item-count" class="d-none mr-auto"></span>
class="d-none mr-auto"></span> <button id="shopping-list-stock-add-workflow-skip-button" type="button"
<button id="shopping-list-stock-add-workflow-skip-button"
type="button"
class="btn btn-primary">{{ $__t('Skip') }}</button> class="btn btn-primary">{{ $__t('Skip') }}</button>
<button type="button" <button type="button" class="btn btn-secondary" data-dismiss="modal">{{ $__t('Close') }}</button>
class="btn btn-secondary"
data-dismiss="modal">{{ $__t('Close') }}</button>
</div> </div>
</div> </div>
</div> </div>
@ -335,12 +320,11 @@
<div class="d-none d-print-block"> <div class="d-none d-print-block">
<div id="print-header"> <div id="print-header">
<h1 class="text-center"> <h1 class="text-center">
<img src="{{ $U('/img/grocy_logo.svg?v=', true) }}{{ $version }}" <img src="{{ $U('/img/grocy_logo.svg?v=', true) }}{{ $version }}" height="30"
height="30"
class="d-print-flex mx-auto"> class="d-print-flex mx-auto">
{{ $__t("Shopping list") }} {{ $__t('Shopping list') }}
</h1> </h1>
@if (FindObjectInArrayByPropertyValue($shoppingLists, 'id', $selectedShoppingListId)->name != $__t("Shopping list")) @if (FindObjectInArrayByPropertyValue($shoppingLists, 'id', $selectedShoppingListId)->name != $__t('Shopping list'))
<h3 class="text-center"> <h3 class="text-center">
{{ FindObjectInArrayByPropertyValue($shoppingLists, 'id', $selectedShoppingListId)->name }} {{ FindObjectInArrayByPropertyValue($shoppingLists, 'id', $selectedShoppingListId)->name }}
</h3> </h3>
@ -352,20 +336,20 @@
</div> </div>
<div class="w-75 print-layout-container print-layout-type-table d-none"> <div class="w-75 print-layout-container print-layout-type-table d-none">
<div> <div>
<table id="shopping-list-print-shadow-table" {{-- TODO: DataTables: dynamic data: uihelper_shopping_list --}}
class="table table-sm table-striped nowrap"> <table id="shopping-list-print-shadow-table" class="table table-sm table-striped nowrap">
<thead> <thead>
<tr> <tr>
<th>{{ $__t('Product') }} / <em>{{ $__t('Note') }}</em></th> <th>{{ $__t('Product') }} / <em>{{ $__t('Note') }}</em></th>
<th>{{ $__t('Amount') }}</th> <th>{{ $__t('Amount') }}</th>
<th>{{ $__t('Product group') }}</th> <th>{{ $__t('Product group') }}</th>
@include('components.userfields_thead', array( @include('components.userfields_thead', [
'userfields' => $userfields 'userfields' => $userfields,
)) ])
@include('components.userfields_thead', array( @include('components.userfields_thead', [
'userfields' => $productUserfields 'userfields' => $productUserfields,
)) ])
</tr> </tr>
</thead> </thead>
@ -373,23 +357,43 @@
@foreach ($listItems as $listItem) @foreach ($listItems as $listItem)
<tr> <tr>
<td> <td>
@if(!empty($listItem->product_id)) {{ $listItem->product_name }}<br>@endif<em>{!! nl2br($listItem->note) !!}</em> @if (!empty($listItem->product_id))
{{ $listItem->product_name }}<br>
@endif
<em>
{!! nl2br($listItem->note) !!}</em>
</td> </td>
<td> <td>
<span class="locale-number locale-number-quantity-amount">{{ $listItem->amount }}</span> @if(!empty($listItem->product_id)){{ $__n($listItem->amount, $listItem->qu_name, $listItem->qu_name_plural, true) }}@endif <span
class="locale-number locale-number-quantity-amount">{{ $listItem->amount }}</span>
@if (!empty($listItem->product_id))
{{ $__n($listItem->amount, $listItem->qu_name, $listItem->qu_name_plural, true) }}
@endif
</td> </td>
<td> <td>
@if(!empty($listItem->product_group_name)) {{ $listItem->product_group_name }} @else <span class="font-italic font-weight-light">{{ $__t('Ungrouped') }}</span> @endif @if (!empty($listItem->product_group_name))
{{ $listItem->product_group_name }}
@else
<span class="font-italic font-weight-light">{{ $__t('Ungrouped') }}</span>
@endif
</td> </td>
@include('components.userfields_tbody', array( @include('components.userfields_tbody', [
'userfields' => $userfields, 'userfields' => $userfields,
'userfieldValues' => FindAllObjectsInArrayByPropertyValue($userfieldValues, 'object_id', $listItem->id) 'userfieldValues' => FindAllObjectsInArrayByPropertyValue(
)) $userfieldValues,
@include('components.userfields_tbody', array( 'object_id',
$listItem->id
),
])
@include('components.userfields_tbody', [
'userfields' => $productUserfields, 'userfields' => $productUserfields,
'userfieldValues' => FindAllObjectsInArrayByPropertyValue($productUserfieldValues, 'object_id', $listItem->product_id) 'userfieldValues' => FindAllObjectsInArrayByPropertyValue(
)) $productUserfieldValues,
'object_id',
$listItem->product_id
),
])
</tr> </tr>
@endforeach @endforeach
@ -400,8 +404,14 @@
<div class="w-75 print-layout-container print-layout-type-list d-none"> <div class="w-75 print-layout-container print-layout-type-list d-none">
@foreach ($listItems as $listItem) @foreach ($listItems as $listItem)
<div class="py-0"> <div class="py-0">
<span class="locale-number locale-number-quantity-amount">{{ $listItem->amount }}</span> @if(!empty($listItem->product_id)){{ $__n($listItem->amount, $listItem->qu_name, $listItem->qu_name_plural, true) }}@endif <span class="locale-number locale-number-quantity-amount">{{ $listItem->amount }}</span>
@if(!empty($listItem->product_id)) {{ $listItem->product_name }}<br>@endif<em>{!! nl2br($listItem->note) !!}</em> @if (!empty($listItem->product_id))
{{ $__n($listItem->amount, $listItem->qu_name, $listItem->qu_name_plural, true) }}
@endif
@if (!empty($listItem->product_id))
{{ $listItem->product_name }}<br>
@endif
<em>{!! nl2br($listItem->note) !!}</em>
</div><br> </div><br>
@endforeach @endforeach
</div> </div>
@ -412,18 +422,14 @@
</div> </div>
</div> </div>
</div> </div>
<div class="modal fade" <div class="modal fade" id="shoppinglist-productcard-modal" tabindex="-1">
id="shoppinglist-productcard-modal"
tabindex="-1">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content text-center"> <div class="modal-content text-center">
<div class="modal-body"> <div class="modal-body">
@include('components.productcard') @include('components.productcard')
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" <button type="button" class="btn btn-secondary" data-dismiss="modal">{{ $__t('Close') }}</button>
class="btn btn-secondary"
data-dismiss="modal">{{ $__t('Close') }}</button>
</div> </div>
</div> </div>
</div> </div>

View File

@ -34,66 +34,77 @@
</script> </script>
@endif @endif
<form id="shoppinglist-form" <form id="shoppinglist-form" novalidate>
novalidate>
@if (GROCY_FEATURE_FLAG_SHOPPINGLIST_MULTIPLE_LISTS) @if (GROCY_FEATURE_FLAG_SHOPPINGLIST_MULTIPLE_LISTS)
<div class="form-group"> <div class="form-group">
<label for="shopping_list_id">{{ $__t('Shopping list') }}</label> <label for="shopping_list_id">{{ $__t('Shopping list') }}</label>
<select class="custom-control custom-select" {{-- TODO: Select2: dynamic data: shopping_lists --}}
id="shopping_list_id" <select class="custom-control custom-select" id="shopping_list_id" name="shopping_list_id">
name="shopping_list_id">
@foreach ($shoppingLists as $shoppingList) @foreach ($shoppingLists as $shoppingList)
<option @if($mode=='edit' <option @if ($mode == 'edit' && $shoppingList->id == $listItem->shopping_list_id) selected="selected" @endif
&& value="{{ $shoppingList->id }}">{{ $shoppingList->name }}</option>
$shoppingList->id == $listItem->shopping_list_id) selected="selected" @endif value="{{ $shoppingList->id }}">{{ $shoppingList->name }}</option>
@endforeach @endforeach
</select> </select>
</div> </div>
@else @else
<input type="hidden" <input type="hidden" id="shopping_list_id" name="shopping_list_id" value="1">
id="shopping_list_id"
name="shopping_list_id"
value="1">
@endif @endif
<div> <div>
@php if($mode == 'edit') { $productId = $listItem->product_id; } else { $productId = ''; } @endphp @php
@include('components.productpicker', array( if ($mode == 'edit') {
'products' => $products, $productId = $listItem->product_id;
} else {
$productId = '';
}
@endphp
@include('components.productpicker', [
'productsQuery' => 'query%5B%5D=active%3D1&order=name%3Acollate%20nocase',
'nextInputSelector' => '#amount', 'nextInputSelector' => '#amount',
'isRequired' => true, 'isRequired' => true,
'prefillById' => $productId, 'prefillById' => $productId,
'validationMessage' => 'A product or a note is required' 'validationMessage' => 'A product or a note is required',
)) ])
</div> </div>
@php if($mode == 'edit') { $value = $listItem->amount; } else { $value = 1; } @endphp @php
@php if($mode == 'edit') { $initialQuId = $listItem->qu_id; } else { $initialQuId = ''; } @endphp if ($mode == 'edit') {
@include('components.productamountpicker', array( $value = $listItem->amount;
} else {
$value = 1;
}
@endphp
@php
if ($mode == 'edit') {
$initialQuId = $listItem->qu_id;
} else {
$initialQuId = '';
}
@endphp
@include('components.productamountpicker', [
'value' => $value, 'value' => $value,
'initialQuId' => $initialQuId, 'initialQuId' => $initialQuId,
'min' => $DEFAULT_MIN_AMOUNT, 'min' => $DEFAULT_MIN_AMOUNT,
'isRequired' => false 'isRequired' => false,
)) ])
<div class="form-group"> <div class="form-group">
<label for="note">{{ $__t('Note') }}</label> <label for="note">{{ $__t('Note') }}</label>
<textarea class="form-control" <textarea class="form-control" required rows="10" id="note" name="note">
required @if ($mode == 'edit')
rows="10" {{ $listItem->note }}
id="note" @endif
name="note">@if($mode == 'edit'){{ $listItem->note }}@endif</textarea> </textarea>
<div class="invalid-feedback">{{ $__t('A product or a note is required') }}</div> <div class="invalid-feedback">{{ $__t('A product or a note is required') }}</div>
</div> </div>
@include('components.userfieldsform', array( @include('components.userfieldsform', [
'userfields' => $userfields, 'userfields' => $userfields,
'entity' => 'shopping_list' 'entity' => 'shopping_list',
)) ])
<button id="save-shoppinglist-button" <button id="save-shoppinglist-button" class="btn btn-success">{{ $__t('Save') }}</button>
class="btn btn-success">{{ $__t('Save') }}</button>
</form> </form>
</div> </div>

View File

@ -10,21 +10,16 @@
<div class="title-related-links"> <div class="title-related-links">
<h2 class="title">@yield('title')</h2> <h2 class="title">@yield('title')</h2>
<div class="float-right"> <div class="float-right">
<button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" <button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" type="button"
type="button" data-toggle="collapse" data-target="#table-filter-row">
data-toggle="collapse"
data-target="#table-filter-row">
<i class="fas fa-filter"></i> <i class="fas fa-filter"></i>
</button> </button>
<button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" <button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" type="button"
type="button" data-toggle="collapse" data-target="#related-links">
data-toggle="collapse"
data-target="#related-links">
<i class="fas fa-ellipsis-v"></i> <i class="fas fa-ellipsis-v"></i>
</button> </button>
</div> </div>
<div class="related-links collapse d-md-flex order-2 width-xs-sm-100" <div class="related-links collapse d-md-flex order-2 width-xs-sm-100" id="related-links">
id="related-links">
<a class="btn btn-primary responsive-button m-1 mt-md-0 mb-md-0 float-right show-as-dialog-link" <a class="btn btn-primary responsive-button m-1 mt-md-0 mb-md-0 float-right show-as-dialog-link"
href="{{ $U('/shoppinglocation/new?embedded') }}"> href="{{ $U('/shoppinglocation/new?embedded') }}">
{{ $__t('Add') }} {{ $__t('Add') }}
@ -40,24 +35,18 @@
<hr class="my-2"> <hr class="my-2">
<div class="row collapse d-md-flex" <div class="row collapse d-md-flex" id="table-filter-row">
id="table-filter-row">
<div class="col-12 col-md-6 col-xl-3"> <div class="col-12 col-md-6 col-xl-3">
<div class="input-group"> <div class="input-group">
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-search"></i></span> <span class="input-group-text"><i class="fas fa-search"></i></span>
</div> </div>
<input type="text" <input type="text" id="search" class="form-control" placeholder="{{ $__t('Search') }}">
id="search"
class="form-control"
placeholder="{{ $__t('Search') }}">
</div> </div>
</div> </div>
<div class="col"> <div class="col">
<div class="float-right"> <div class="float-right">
<a id="clear-filter-button" <a id="clear-filter-button" class="btn btn-sm btn-outline-info" href="#">
class="btn btn-sm btn-outline-info"
href="#">
{{ $__t('Clear filter') }} {{ $__t('Clear filter') }}
</a> </a>
</div> </div>
@ -66,23 +55,20 @@
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<table id="shoppinglocations-table" {{-- TODO: DataTables: dynamic data: shopping_locations --}}
class="table table-sm table-striped nowrap w-100"> <table id="shoppinglocations-table" class="table table-sm table-striped nowrap w-100">
<thead> <thead>
<tr> <tr>
<th class="border-right"><a class="text-muted change-table-columns-visibility-button" <th class="border-right"><a class="text-muted change-table-columns-visibility-button"
data-toggle="tooltip" data-toggle="tooltip" data-toggle="tooltip" title="{{ $__t('Table options') }}"
data-toggle="tooltip" data-table-selector="#shoppinglocations-table" href="#"><i class="fas fa-eye"></i></a>
title="{{ $__t('Table options') }}"
data-table-selector="#shoppinglocations-table"
href="#"><i class="fas fa-eye"></i></a>
</th> </th>
<th>{{ $__t('Name') }}</th> <th>{{ $__t('Name') }}</th>
<th>{{ $__t('Description') }}</th> <th>{{ $__t('Description') }}</th>
@include('components.userfields_thead', array( @include('components.userfields_thead', [
'userfields' => $userfields 'userfields' => $userfields,
)) ])
</tr> </tr>
</thead> </thead>
@ -92,15 +78,12 @@
<td class="fit-content border-right"> <td class="fit-content border-right">
<a class="btn btn-info btn-sm show-as-dialog-link" <a class="btn btn-info btn-sm show-as-dialog-link"
href="{{ $U('/shoppinglocation/') }}{{ $shoppinglocation->id }}?embedded" href="{{ $U('/shoppinglocation/') }}{{ $shoppinglocation->id }}?embedded"
data-toggle="tooltip" data-toggle="tooltip" title="{{ $__t('Edit this item') }}">
title="{{ $__t('Edit this item') }}">
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
</a> </a>
<a class="btn btn-danger btn-sm shoppinglocation-delete-button" <a class="btn btn-danger btn-sm shoppinglocation-delete-button" href="#"
href="#"
data-shoppinglocation-id="{{ $shoppinglocation->id }}" data-shoppinglocation-id="{{ $shoppinglocation->id }}"
data-shoppinglocation-name="{{ $shoppinglocation->name }}" data-shoppinglocation-name="{{ $shoppinglocation->name }}" data-toggle="tooltip"
data-toggle="tooltip"
title="{{ $__t('Delete this item') }}"> title="{{ $__t('Delete this item') }}">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
</a> </a>
@ -112,10 +95,14 @@
{{ $shoppinglocation->description }} {{ $shoppinglocation->description }}
</td> </td>
@include('components.userfields_tbody', array( @include('components.userfields_tbody', [
'userfields' => $userfields, 'userfields' => $userfields,
'userfieldValues' => FindAllObjectsInArrayByPropertyValue($userfieldValues, 'object_id', $shoppinglocation->id) 'userfieldValues' => FindAllObjectsInArrayByPropertyValue(
)) $userfieldValues,
'object_id',
$shoppinglocation->id
),
])
</tr> </tr>
@endforeach @endforeach

View File

@ -4,8 +4,7 @@
@section('viewJsName', 'stockentries') @section('viewJsName', 'stockentries')
@push('pageStyles') @push('pageStyles')
<link href="{{ $U('/node_modules/animate.css/animate.min.css?v=', true) }}{{ $version }}" <link href="{{ $U('/node_modules/animate.css/animate.min.css?v=', true) }}{{ $version }}" rel="stylesheet">
rel="stylesheet">
@endpush @endpush
@push('pageScripts') @push('pageScripts')
@ -17,9 +16,7 @@
<div class="col"> <div class="col">
<h2 class="title">@yield('title')</h2> <h2 class="title">@yield('title')</h2>
<div class="float-right"> <div class="float-right">
<button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" <button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" type="button" data-toggle="collapse"
type="button"
data-toggle="collapse"
data-target="#table-filter-row"> data-target="#table-filter-row">
<i class="fas fa-filter"></i> <i class="fas fa-filter"></i>
</button> </button>
@ -29,20 +26,17 @@
<hr class="my-2"> <hr class="my-2">
<div class="row collapse d-md-flex" <div class="row collapse d-md-flex" id="table-filter-row">
id="table-filter-row">
<div class="col-12 col-md-6 col-xl-3"> <div class="col-12 col-md-6 col-xl-3">
@include('components.productpicker', array( @include('components.productpicker', [
'products' => $products, 'productsQuery' => 'query%5B%5D=active%3D1&order=name%3Acollate%20nocase',
'disallowAllProductWorkflows' => true, 'disallowAllProductWorkflows' => true,
'isRequired' => false 'isRequired' => false,
)) ])
</div> </div>
<div class="col"> <div class="col">
<div class="float-right mt-3"> <div class="float-right mt-3">
<a id="clear-filter-button" <a id="clear-filter-button" class="btn btn-sm btn-outline-info" href="#">
class="btn btn-sm btn-outline-info"
href="#">
{{ $__t('Clear filter') }} {{ $__t('Clear filter') }}
</a> </a>
</div> </div>
@ -51,47 +45,44 @@
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<table id="stockentries-table" {{-- TODO: DataTables: dynamic data: stock --}}
class="table table-sm table-striped nowrap w-100"> <table id="stockentries-table" class="table table-sm table-striped nowrap w-100">
<thead> <thead>
<tr> <tr>
<th class="border-right"><a class="text-muted change-table-columns-visibility-button" <th class="border-right"><a class="text-muted change-table-columns-visibility-button"
data-toggle="tooltip" data-toggle="tooltip" data-toggle="tooltip" title="{{ $__t('Table options') }}"
data-toggle="tooltip" data-table-selector="#stockentries-table" href="#"><i class="fas fa-eye"></i></a>
title="{{ $__t('Table options') }}"
data-table-selector="#stockentries-table"
href="#"><i class="fas fa-eye"></i></a>
</th> </th>
<th class="d-none">Hidden product_id</th> <!-- This must be in the first column for searching --> <th class="d-none">Hidden product_id</th>
<!-- This must be in the first column for searching -->
<th class="allow-grouping">{{ $__t('Product') }}</th> <th class="allow-grouping">{{ $__t('Product') }}</th>
<th>{{ $__t('Amount') }}</th> <th>{{ $__t('Amount') }}</th>
<th class="@if(!GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING) d-none @endif allow-grouping">{{ $__t('Due date') }}</th> <th class="@if (!GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING) d-none @endif allow-grouping">{{ $__t('Due date') }}
<th class="@if(!GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING) d-none @endif allow-grouping">{{ $__t('Location') }}</th> </th>
<th class="@if(!GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) d-none @endif allow-grouping">{{ $__t('Store') }}</th> <th class="@if (!GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING) d-none @endif allow-grouping">{{ $__t('Location') }}
</th>
<th class="@if (!GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) d-none @endif allow-grouping">{{ $__t('Store') }}
</th>
<th class="@if (!GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) d-none @endif">{{ $__t('Price') }}</th> <th class="@if (!GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) d-none @endif">{{ $__t('Price') }}</th>
<th class="allow-grouping" <th class="allow-grouping" data-shadow-rowgroup-column="9">{{ $__t('Purchased date') }}</th>
data-shadow-rowgroup-column="9">{{ $__t('Purchased date') }}</th>
<th class="d-none">Hidden purchased_date</th> <th class="d-none">Hidden purchased_date</th>
<th>{{ $__t('Timestamp') }}</th> <th>{{ $__t('Timestamp') }}</th>
@include('components.userfields_thead', array( @include('components.userfields_thead', [
'userfields' => $userfields 'userfields' => $userfields,
)) ])
</tr> </tr>
</thead> </thead>
<tbody class="d-none"> <tbody class="d-none">
@foreach ($stockEntries as $stockEntry) @foreach ($stockEntries as $stockEntry)
<tr id="stock-{{ $stockEntry->id }}-row" <tr id="stock-{{ $stockEntry->id }}-row"
data-due-type="{{ FindObjectInArrayByPropertyValue($products, 'id', $stockEntry->product_id)->due_type }}" data-due-type="{{ FindObjectInArrayByPropertyValue($products, 'id', $stockEntry->product_id)->due_type }}"
class="@if(GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING && $stockEntry->best_before_date < date('Y-m-d 23:59:59', strtotime('-1 days')) && $stockEntry->amount > 0) @if(FindObjectInArrayByPropertyValue($products, 'id', $stockEntry->product_id)->due_type == 1) table-secondary @else table-danger @endif @elseif(GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING && $stockEntry->best_before_date < date('Y-m-d 23:59:59', strtotime('+' . $nextXDays . ' days')) class="@if (GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING && $stockEntry->best_before_date < date('Y-m-d 23:59:59', strtotime('-1 days')) && $stockEntry->amount > 0) @if (FindObjectInArrayByPropertyValue($products, 'id', $stockEntry->product_id)->due_type == 1) table-secondary @else table-danger @endif
&& @elseif(GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING && $stockEntry->best_before_date < date('Y-m-d 23:59:59', strtotime('+' . $nextXDays . ' days')) && $stockEntry->amount > 0)
$stockEntry->amount > 0) table-warning @endif"> table-warning @endif">
<td class="fit-content border-right"> <td class="fit-content border-right">
<a class="btn btn-danger btn-sm stock-consume-button" <a class="btn btn-danger btn-sm stock-consume-button" href="#" data-toggle="tooltip"
href="#" data-placement="left" title="{{ $__t('Consume this stock entry') }}"
data-toggle="tooltip"
data-placement="left"
title="{{ $__t('Consume this stock entry') }}"
data-product-id="{{ $stockEntry->product_id }}" data-product-id="{{ $stockEntry->product_id }}"
data-stock-id="{{ $stockEntry->stock_id }}" data-stock-id="{{ $stockEntry->stock_id }}"
data-stockrow-id="{{ $stockEntry->id }}" data-stockrow-id="{{ $stockEntry->id }}"
@ -103,9 +94,7 @@
</a> </a>
@if (GROCY_FEATURE_FLAG_STOCK_PRODUCT_OPENED_TRACKING) @if (GROCY_FEATURE_FLAG_STOCK_PRODUCT_OPENED_TRACKING)
<a class="btn btn-success btn-sm product-open-button @if ($stockEntry->open == 1 || FindObjectInArrayByPropertyValue($products, 'id', $stockEntry->product_id)->enable_tare_weight_handling == 1) disabled @endif" <a class="btn btn-success btn-sm product-open-button @if ($stockEntry->open == 1 || FindObjectInArrayByPropertyValue($products, 'id', $stockEntry->product_id)->enable_tare_weight_handling == 1) disabled @endif"
href="#" href="#" data-toggle="tooltip" data-placement="left"
data-toggle="tooltip"
data-placement="left"
title="{{ $__t('Mark this stock entry as open') }}" title="{{ $__t('Mark this stock entry as open') }}"
data-product-id="{{ $stockEntry->product_id }}" data-product-id="{{ $stockEntry->product_id }}"
data-product-name="{{ FindObjectInArrayByPropertyValue($products, 'id', $stockEntry->product_id)->name }}" data-product-name="{{ FindObjectInArrayByPropertyValue($products, 'id', $stockEntry->product_id)->name }}"
@ -116,133 +105,114 @@
</a> </a>
@endif @endif
<a class="btn btn-info btn-sm show-as-dialog-link" <a class="btn btn-info btn-sm show-as-dialog-link"
href="{{ $U('/stockentry/' . $stockEntry->id . '?embedded') }}" href="{{ $U('/stockentry/' . $stockEntry->id . '?embedded') }}" data-toggle="tooltip"
data-toggle="tooltip" data-placement="left" title="{{ $__t('Edit stock entry') }}">
data-placement="left"
title="{{ $__t('Edit stock entry') }}">
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
</a> </a>
<div class="dropdown d-inline-block"> <div class="dropdown d-inline-block">
<button class="btn btn-sm btn-light text-secondary" <button class="btn btn-sm btn-light text-secondary" type="button"
type="button"
data-toggle="dropdown"> data-toggle="dropdown">
<i class="fas fa-ellipsis-v"></i> <i class="fas fa-ellipsis-v"></i>
</button> </button>
<div class="dropdown-menu"> <div class="dropdown-menu">
@if (GROCY_FEATURE_FLAG_SHOPPINGLIST) @if (GROCY_FEATURE_FLAG_SHOPPINGLIST)
<a class="dropdown-item show-as-dialog-link" <a class="dropdown-item show-as-dialog-link" type="button"
type="button"
href="{{ $U('/shoppinglistitem/new?embedded&updateexistingproduct&product=' . $stockEntry->product_id) }}"> href="{{ $U('/shoppinglistitem/new?embedded&updateexistingproduct&product=' . $stockEntry->product_id) }}">
<i class="fas fa-shopping-cart"></i> {{ $__t('Add to shopping list') }} <i class="fas fa-shopping-cart"></i> {{ $__t('Add to shopping list') }}
</a> </a>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
@endif @endif
<a class="dropdown-item show-as-dialog-link" <a class="dropdown-item show-as-dialog-link" type="button"
type="button"
href="{{ $U('/purchase?embedded&product=' . $stockEntry->product_id) }}"> href="{{ $U('/purchase?embedded&product=' . $stockEntry->product_id) }}">
<i class="fas fa-cart-plus"></i> {{ $__t('Purchase') }} <i class="fas fa-cart-plus"></i> {{ $__t('Purchase') }}
</a> </a>
<a class="dropdown-item show-as-dialog-link" <a class="dropdown-item show-as-dialog-link" type="button"
type="button"
href="{{ $U('/consume?embedded&product=' .$stockEntry->product_id .'&locationId=' .$stockEntry->location_id .'&stockId=' .$stockEntry->stock_id) }}"> href="{{ $U('/consume?embedded&product=' .$stockEntry->product_id .'&locationId=' .$stockEntry->location_id .'&stockId=' .$stockEntry->stock_id) }}">
<i class="fas fa-utensils"></i> {{ $__t('Consume') }} <i class="fas fa-utensils"></i> {{ $__t('Consume') }}
</a> </a>
@if (GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING) @if (GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING)
<a class="dropdown-item show-as-dialog-link" <a class="dropdown-item show-as-dialog-link" type="button"
type="button"
href="{{ $U('/transfer?embedded&product=' .$stockEntry->product_id .'&locationId=' .$stockEntry->location_id .'&stockId=' .$stockEntry->stock_id) }}"> href="{{ $U('/transfer?embedded&product=' .$stockEntry->product_id .'&locationId=' .$stockEntry->location_id .'&stockId=' .$stockEntry->stock_id) }}">
<i class="fas fa-exchange-alt"></i> {{ $__t('Transfer') }} <i class="fas fa-exchange-alt"></i> {{ $__t('Transfer') }}
</a> </a>
@endif @endif
<a class="dropdown-item show-as-dialog-link" <a class="dropdown-item show-as-dialog-link" type="button"
type="button"
href="{{ $U('/inventory?embedded&product=' . $stockEntry->product_id) }}"> href="{{ $U('/inventory?embedded&product=' . $stockEntry->product_id) }}">
<i class="fas fa-list"></i> {{ $__t('Inventory') }} <i class="fas fa-list"></i> {{ $__t('Inventory') }}
</a> </a>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a class="dropdown-item stock-consume-button stock-consume-button-spoiled" <a class="dropdown-item stock-consume-button stock-consume-button-spoiled"
type="button" type="button" href="#" data-product-id="{{ $stockEntry->product_id }}"
href="#"
data-product-id="{{ $stockEntry->product_id }}"
data-product-name="{{ FindObjectInArrayByPropertyValue($products, 'id', $stockEntry->product_id)->name }}" data-product-name="{{ FindObjectInArrayByPropertyValue($products, 'id', $stockEntry->product_id)->name }}"
data-product-qu-name="{{ FindObjectInArrayByPropertyValue($quantityunits,'id',FindObjectInArrayByPropertyValue($products, 'id', $stockEntry->product_id)->qu_id_stock)->name }}" data-product-qu-name="{{ FindObjectInArrayByPropertyValue($quantityunits,'id',FindObjectInArrayByPropertyValue($products, 'id', $stockEntry->product_id)->qu_id_stock)->name }}"
data-stock-id="{{ $stockEntry->stock_id }}" data-stock-id="{{ $stockEntry->stock_id }}"
data-stockrow-id="{{ $stockEntry->id }}" data-stockrow-id="{{ $stockEntry->id }}"
data-location-id="{{ $stockEntry->location_id }}" data-location-id="{{ $stockEntry->location_id }}" data-consume-amount="1">
data-consume-amount="1">
{{ $__t('Consume this stock entry as spoiled','1 ' .FindObjectInArrayByPropertyValue($quantityunits,'id',FindObjectInArrayByPropertyValue($products, 'id', $stockEntry->product_id)->qu_id_stock)->name,FindObjectInArrayByPropertyValue($products, 'id', $stockEntry->product_id)->name) }} {{ $__t('Consume this stock entry as spoiled','1 ' .FindObjectInArrayByPropertyValue($quantityunits,'id',FindObjectInArrayByPropertyValue($products, 'id', $stockEntry->product_id)->qu_id_stock)->name,FindObjectInArrayByPropertyValue($products, 'id', $stockEntry->product_id)->name) }}
</a> </a>
@if (GROCY_FEATURE_FLAG_RECIPES) @if (GROCY_FEATURE_FLAG_RECIPES)
<a class="dropdown-item" <a class="dropdown-item" type="button"
type="button"
href="{{ $U('/recipes?search=') }}{{ FindObjectInArrayByPropertyValue($products, 'id', $stockEntry->product_id)->name }}"> href="{{ $U('/recipes?search=') }}{{ FindObjectInArrayByPropertyValue($products, 'id', $stockEntry->product_id)->name }}">
{{ $__t('Search for recipes containing this product') }} {{ $__t('Search for recipes containing this product') }}
</a> </a>
@endif @endif
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a class="dropdown-item product-name-cell" <a class="dropdown-item product-name-cell"
data-product-id="{{ $stockEntry->product_id }}" data-product-id="{{ $stockEntry->product_id }}" type="button" href="#">
type="button"
href="#">
{{ $__t('Product overview') }} {{ $__t('Product overview') }}
</a> </a>
<a class="dropdown-item show-as-dialog-link" <a class="dropdown-item show-as-dialog-link" type="button"
type="button"
href="{{ $U('/stockjournal?embedded&product=') }}{{ $stockEntry->product_id }}"> href="{{ $U('/stockjournal?embedded&product=') }}{{ $stockEntry->product_id }}">
{{ $__t('Stock journal') }} {{ $__t('Stock journal') }}
</a> </a>
<a class="dropdown-item show-as-dialog-link" <a class="dropdown-item show-as-dialog-link" type="button"
type="button"
href="{{ $U('/stockjournal/summary?embedded&product=') }}{{ $stockEntry->product_id }}"> href="{{ $U('/stockjournal/summary?embedded&product=') }}{{ $stockEntry->product_id }}">
{{ $__t('Stock journal summary') }} {{ $__t('Stock journal summary') }}
</a> </a>
<a class="dropdown-item link-return" <a class="dropdown-item link-return" type="button"
type="button"
data-href="{{ $U('/product/') }}{{ $stockEntry->product_id }}"> data-href="{{ $U('/product/') }}{{ $stockEntry->product_id }}">
{{ $__t('Edit product') }} {{ $__t('Edit product') }}
</a> </a>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a class="dropdown-item" <a class="dropdown-item" type="button"
type="button"
href="{{ $U('/stockentry/' . $stockEntry->id . '/grocycode?download=true') }}"> href="{{ $U('/stockentry/' . $stockEntry->id . '/grocycode?download=true') }}">
{!! str_replace('grocycode', '<span class="ls-n1">grocycode</span>', $__t('Download %s grocycode', $__t('Stock entry'))) !!} {!! str_replace('grocycode', '<span class="ls-n1">grocycode</span>', $__t('Download %s grocycode', $__t('Stock entry'))) !!}
</a> </a>
@if (GROCY_FEATURE_FLAG_LABEL_PRINTER) @if (GROCY_FEATURE_FLAG_LABEL_PRINTER)
<a class="dropdown-item stockentry-grocycode-label-print" <a class="dropdown-item stockentry-grocycode-label-print"
data-stock-id="{{ $stockEntry->id }}" data-stock-id="{{ $stockEntry->id }}" type="button" href="#">
type="button"
href="#">
{!! str_replace('grocycode', '<span class="ls-n1">grocycode</span>', $__t('Print %s grocycode on label printer', $__t('Stock entry'))) !!} {!! str_replace('grocycode', '<span class="ls-n1">grocycode</span>', $__t('Print %s grocycode on label printer', $__t('Stock entry'))) !!}
</a> </a>
@endif @endif
<a class="dropdown-item stockentry-label-link" <a class="dropdown-item stockentry-label-link" type="button" target="_blank"
type="button"
target="_blank"
href="{{ $U('/stockentry/' . $stockEntry->id . '/label') }}"> href="{{ $U('/stockentry/' . $stockEntry->id . '/label') }}">
{{ $__t('Open stock entry label in new window') }} {{ $__t('Open stock entry label in new window') }}
</a> </a>
</div> </div>
</div> </div>
</td> </td>
<td class="d-none" <td class="d-none" data-product-id="{{ $stockEntry->product_id }}">
data-product-id="{{ $stockEntry->product_id }}">
{{ $stockEntry->product_id }} {{ $stockEntry->product_id }}
</td> </td>
<td class="product-name-cell cursor-link" <td class="product-name-cell cursor-link" data-product-id="{{ $stockEntry->product_id }}">
data-product-id="{{ $stockEntry->product_id }}">
{{ FindObjectInArrayByPropertyValue($products, 'id', $stockEntry->product_id)->name }} {{ FindObjectInArrayByPropertyValue($products, 'id', $stockEntry->product_id)->name }}
</td> </td>
<td data-order="{{ $stockEntry->amount }}"> <td data-order="{{ $stockEntry->amount }}">
<span id="stock-{{ $stockEntry->id }}-amount" <span id="stock-{{ $stockEntry->id }}-amount"
class="locale-number locale-number-quantity-amount">{{ $stockEntry->amount }}</span> <span id="product-{{ $stockEntry->product_id }}-qu-name">{{ $__n($stockEntry->amount, FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $stockEntry->product_id)->qu_id_stock)->name, FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $stockEntry->product_id)->qu_id_stock)->name_plural, true) }}</span> class="locale-number locale-number-quantity-amount">{{ $stockEntry->amount }}</span>
<span id="stock-{{ $stockEntry->id }}-opened-amount" <span
class="small font-italic">@if($stockEntry->open == 1){{ $__t('Opened') }}@endif</span> id="product-{{ $stockEntry->product_id }}-qu-name">{{ $__n($stockEntry->amount,FindObjectInArrayByPropertyValue($quantityunits,'id',FindObjectInArrayByPropertyValue($products, 'id', $stockEntry->product_id)->qu_id_stock)->name,FindObjectInArrayByPropertyValue($quantityunits,'id',FindObjectInArrayByPropertyValue($products, 'id', $stockEntry->product_id)->qu_id_stock)->name_plural,true) }}</span>
<span id="stock-{{ $stockEntry->id }}-opened-amount" class="small font-italic">
@if ($stockEntry->open == 1)
{{ $__t('Opened') }}@endif
</span>
</td> </td>
<td class="@if (!GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING) d-none @endif"> <td class="@if (!GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING) d-none @endif">
<span id="stock-{{ $stockEntry->id }}-due-date">{{ $stockEntry->best_before_date }}</span> <span
id="stock-{{ $stockEntry->id }}-due-date">{{ $stockEntry->best_before_date }}</span>
<time id="stock-{{ $stockEntry->id }}-due-date-timeago" <time id="stock-{{ $stockEntry->id }}-due-date-timeago"
class="timeago timeago-contextual" class="timeago timeago-contextual"
@if($stockEntry->best_before_date != "") datetime="{{ $stockEntry->best_before_date }} 23:59:59" @endif></time> @if ($stockEntry->best_before_date != '') datetime="{{ $stockEntry->best_before_date }} 23:59:59" @endif></time>
</td> </td>
<td id="stock-{{ $stockEntry->id }}-location" <td id="stock-{{ $stockEntry->id }}-location"
class="@if (!GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING) d-none @endif" class="@if (!GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING) d-none @endif"
@ -258,12 +228,12 @@
</td> </td>
<td id="stock-{{ $stockEntry->id }}-price" <td id="stock-{{ $stockEntry->id }}-price"
class="@if (!GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) d-none @endif" class="@if (!GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) d-none @endif"
class="locale-number locale-number-currency" class="locale-number locale-number-currency" data-price-id="{{ $stockEntry->price }}">
data-price-id="{{ $stockEntry->price }}">
{{ $stockEntry->price }} {{ $stockEntry->price }}
</td> </td>
<td> <td>
<span id="stock-{{ $stockEntry->id }}-purchased-date">{{ $stockEntry->purchased_date }}</span> <span
id="stock-{{ $stockEntry->id }}-purchased-date">{{ $stockEntry->purchased_date }}</span>
<time id="stock-{{ $stockEntry->id }}-purchased-date-timeago" <time id="stock-{{ $stockEntry->id }}-purchased-date-timeago"
class="timeago timeago-contextual" class="timeago timeago-contextual"
@if (!empty($stockEntry->purchased_date)) datetime="{{ $stockEntry->purchased_date }} 23:59:59" @endif></time> @if (!empty($stockEntry->purchased_date)) datetime="{{ $stockEntry->purchased_date }} 23:59:59" @endif></time>
@ -275,10 +245,14 @@
datetime="{{ $stockEntry->row_created_timestamp }}"></time> datetime="{{ $stockEntry->row_created_timestamp }}"></time>
</td> </td>
@include('components.userfields_tbody', array( @include('components.userfields_tbody', [
'userfields' => $userfields, 'userfields' => $userfields,
'userfieldValues' => FindAllObjectsInArrayByPropertyValue($userfieldValues, 'object_id', $stockEntry->product_id) 'userfieldValues' => FindAllObjectsInArrayByPropertyValue(
)) $userfieldValues,
'object_id',
$stockEntry->product_id
),
])
</tr> </tr>
@endforeach @endforeach
@ -287,18 +261,14 @@
</div> </div>
</div> </div>
<div class="modal fade" <div class="modal fade" id="productcard-modal" tabindex="-1">
id="productcard-modal"
tabindex="-1">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content text-center"> <div class="modal-content text-center">
<div class="modal-body"> <div class="modal-body">
@include('components.productcard') @include('components.productcard')
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" <button type="button" class="btn btn-secondary" data-dismiss="modal">{{ $__t('Close') }}</button>
class="btn btn-secondary"
data-dismiss="modal">{{ $__t('Close') }}</button>
</div> </div>
</div> </div>
</div> </div>

View File

@ -8,21 +8,16 @@
<div class="title-related-links"> <div class="title-related-links">
<h2 class="title">@yield('title')</h2> <h2 class="title">@yield('title')</h2>
<div class="float-right"> <div class="float-right">
<button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" <button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" type="button" data-toggle="collapse"
type="button"
data-toggle="collapse"
data-target="#table-filter-row"> data-target="#table-filter-row">
<i class="fas fa-filter"></i> <i class="fas fa-filter"></i>
</button> </button>
<button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3 hide-when-embedded" <button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3 hide-when-embedded" type="button"
type="button" data-toggle="collapse" data-target="#related-links">
data-toggle="collapse"
data-target="#related-links">
<i class="fas fa-ellipsis-v"></i> <i class="fas fa-ellipsis-v"></i>
</button> </button>
</div> </div>
<div class="related-links collapse d-md-flex order-2 width-xs-sm-100" <div class="related-links collapse d-md-flex order-2 width-xs-sm-100" id="related-links">
id="related-links">
<a class="btn btn-outline-dark responsive-button m-1 mt-md-0 mb-md-0 float-right" <a class="btn btn-outline-dark responsive-button m-1 mt-md-0 mb-md-0 float-right"
href="{{ $U('/stockjournal/summary') }}"> href="{{ $U('/stockjournal/summary') }}">
{{ $__t('Journal summary') }} {{ $__t('Journal summary') }}
@ -32,17 +27,13 @@
<hr class="my-2"> <hr class="my-2">
<div class="row collapse d-md-flex" <div class="row collapse d-md-flex" id="table-filter-row">
id="table-filter-row">
<div class="col-12 col-md-6 col-xl-2"> <div class="col-12 col-md-6 col-xl-2">
<div class="input-group"> <div class="input-group">
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-search"></i></span> <span class="input-group-text"><i class="fas fa-search"></i></span>
</div> </div>
<input type="text" <input type="text" id="search" class="form-control" placeholder="{{ $__t('Search') }}">
id="search"
class="form-control"
placeholder="{{ $__t('Search') }}">
</div> </div>
</div> </div>
<div class="col-12 col-md-6 col-xl-3"> <div class="col-12 col-md-6 col-xl-3">
@ -50,22 +41,19 @@
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-filter"></i>&nbsp;{{ $__t('Product') }}</span> <span class="input-group-text"><i class="fas fa-filter"></i>&nbsp;{{ $__t('Product') }}</span>
</div> </div>
<select class="custom-control custom-select" <select class="select2 custom-control custom-select" id="product-filter">
id="product-filter">
<option value="all">{{ $__t('All') }}</option> <option value="all">{{ $__t('All') }}</option>
@foreach($products as $product)
<option value="{{ $product->id }}">{{ $product->name }}</option>
@endforeach
</select> </select>
</div> </div>
</div> </div>
<div class="col-12 col-md-6 col-xl-3"> <div class="col-12 col-md-6 col-xl-3">
<div class="input-group"> <div class="input-group">
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-filter"></i>&nbsp;{{ $__t('Transaction type') }}</span> <span class="input-group-text"><i
class="fas fa-filter"></i>&nbsp;{{ $__t('Transaction type') }}</span>
</div> </div>
<select class="custom-control custom-select" {{-- TODO: Select2: static data --}}
id="transaction-type-filter"> <select class="custom-control custom-select" id="transaction-type-filter">
<option value="all">{{ $__t('All') }}</option> <option value="all">{{ $__t('All') }}</option>
@foreach ($transactionTypes as $transactionType) @foreach ($transactionTypes as $transactionType)
<option value="{{ $transactionType }}">{{ $__t($transactionType) }}</option> <option value="{{ $transactionType }}">{{ $__t($transactionType) }}</option>
@ -78,8 +66,8 @@
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-filter"></i>&nbsp;{{ $__t('Location') }}</span> <span class="input-group-text"><i class="fas fa-filter"></i>&nbsp;{{ $__t('Location') }}</span>
</div> </div>
<select class="custom-control custom-select" {{-- TODO: Select2: dynamic data: locations --}}
id="location-filter"> <select class="custom-control custom-select" id="location-filter">
<option value="all">{{ $__t('All') }}</option> <option value="all">{{ $__t('All') }}</option>
@foreach ($locations as $location) @foreach ($locations as $location)
<option value="{{ $location->id }}">{{ $location->name }}</option> <option value="{{ $location->id }}">{{ $location->name }}</option>
@ -92,8 +80,8 @@
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-filter"></i>&nbsp;{{ $__t('User') }}</span> <span class="input-group-text"><i class="fas fa-filter"></i>&nbsp;{{ $__t('User') }}</span>
</div> </div>
<select class="custom-control custom-select" {{-- TODO: Select2: dynamic data: users --}}
id="user-filter"> <select class="custom-control custom-select" id="user-filter">
<option value="all">{{ $__t('All') }}</option> <option value="all">{{ $__t('All') }}</option>
@foreach ($users as $user) @foreach ($users as $user)
<option value="{{ $user->id }}">{{ $user->display_name }}</option> <option value="{{ $user->id }}">{{ $user->display_name }}</option>
@ -106,11 +94,9 @@
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-clock"></i>&nbsp;{{ $__t('Date range') }}</span> <span class="input-group-text"><i class="fas fa-clock"></i>&nbsp;{{ $__t('Date range') }}</span>
</div> </div>
<select class="custom-control custom-select" <select class="custom-control custom-select" id="daterange-filter">
id="daterange-filter">
<option value="1">{{ $__n(1, '%s month', '%s months') }}</option> <option value="1">{{ $__n(1, '%s month', '%s months') }}</option>
<option value="6" <option value="6" selected>{{ $__n(6, '%s month', '%s months') }}</option>
selected>{{ $__n(6, '%s month', '%s months') }}</option>
<option value="12">{{ $__n(1, '%s year', '%s years') }}</option> <option value="12">{{ $__n(1, '%s year', '%s years') }}</option>
<option value="24">{{ $__n(2, '%s month', '%s years') }}</option> <option value="24">{{ $__n(2, '%s month', '%s years') }}</option>
<option value="9999">{{ $__t('All') }}</option> <option value="9999">{{ $__t('All') }}</option>
@ -119,9 +105,7 @@
</div> </div>
<div class="col"> <div class="col">
<div class="float-right mt-1"> <div class="float-right mt-1">
<a id="clear-filter-button" <a id="clear-filter-button" class="btn btn-sm btn-outline-info" href="#">
class="btn btn-sm btn-outline-info"
href="#">
{{ $__t('Clear filter') }} {{ $__t('Clear filter') }}
</a> </a>
</div> </div>
@ -130,22 +114,20 @@
<div class="row mt-2"> <div class="row mt-2">
<div class="col"> <div class="col">
<table id="stock-journal-table" {{-- TODO: DataTables: dynamic data: uihelper_stock_journal --}}
class="table table-sm table-striped nowrap w-100"> <table id="stock-journal-table" class="table table-sm table-striped nowrap w-100">
<thead> <thead>
<tr> <tr>
<th class="border-right"><a class="text-muted change-table-columns-visibility-button" <th class="border-right"><a class="text-muted change-table-columns-visibility-button"
data-toggle="tooltip" data-toggle="tooltip" data-toggle="tooltip" title="{{ $__t('Table options') }}"
data-toggle="tooltip" data-table-selector="#stock-journal-table" href="#"><i class="fas fa-eye"></i></a>
title="{{ $__t('Table options') }}"
data-table-selector="#stock-journal-table"
href="#"><i class="fas fa-eye"></i></a>
</th> </th>
<th class="allow-grouping">{{ $__t('Product') }}</th> <th class="allow-grouping">{{ $__t('Product') }}</th>
<th>{{ $__t('Amount') }}</th> <th>{{ $__t('Amount') }}</th>
<th>{{ $__t('Transaction time') }}</th> <th>{{ $__t('Transaction time') }}</th>
<th class="allow-grouping">{{ $__t('Transaction type') }}</th> <th class="allow-grouping">{{ $__t('Transaction type') }}</th>
<th class="@if(!GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING) d-none @endif allow-grouping">{{ $__t('Location') }}</th> <th class="@if (!GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING) d-none @endif allow-grouping">{{ $__t('Location') }}
</th>
<th class="allow-grouping">{{ $__t('Done by') }}</th> <th class="allow-grouping">{{ $__t('Done by') }}</th>
</tr> </tr>
</thead> </thead>
@ -156,16 +138,14 @@
data-correlation-id="{{ $stockLogEntry->correlation_id }}"> data-correlation-id="{{ $stockLogEntry->correlation_id }}">
<td class="fit-content border-right"> <td class="fit-content border-right">
<a class="btn btn-secondary btn-xs undo-stock-booking-button @if ($stockLogEntry->undone == 1) disabled @endif" <a class="btn btn-secondary btn-xs undo-stock-booking-button @if ($stockLogEntry->undone == 1) disabled @endif"
href="#" href="#" data-booking-id="{{ $stockLogEntry->id }}" data-toggle="tooltip"
data-booking-id="{{ $stockLogEntry->id }}" data-placement="left" title="{{ $__t('Undo transaction') }}">
data-toggle="tooltip"
data-placement="left"
title="{{ $__t('Undo transaction') }}">
<i class="fas fa-undo"></i> <i class="fas fa-undo"></i>
</a> </a>
</td> </td>
<td> <td>
<span class="name-anchor @if($stockLogEntry->undone == 1) text-strike-through @endif">{{ $stockLogEntry->product_name }}</span> <span
class="name-anchor @if ($stockLogEntry->undone == 1) text-strike-through @endif">{{ $stockLogEntry->product_name }}</span>
@if ($stockLogEntry->undone == 1) @if ($stockLogEntry->undone == 1)
<br> <br>
{{ $__t('Undone on') . ' ' . $stockLogEntry->undone_timestamp }} {{ $__t('Undone on') . ' ' . $stockLogEntry->undone_timestamp }}
@ -174,7 +154,9 @@
@endif @endif
</td> </td>
<td> <td>
<span class="locale-number locale-number-quantity-amount">{{ $stockLogEntry->amount }}</span> {{ $__n($stockLogEntry->amount, $stockLogEntry->qu_name, $stockLogEntry->qu_name_plural, true) }} <span
class="locale-number locale-number-quantity-amount">{{ $stockLogEntry->amount }}</span>
{{ $__n($stockLogEntry->amount, $stockLogEntry->qu_name, $stockLogEntry->qu_name_plural, true) }}
</td> </td>
<td> <td>
{{ $stockLogEntry->row_created_timestamp }} {{ $stockLogEntry->row_created_timestamp }}

View File

@ -9,9 +9,7 @@
<div class="col"> <div class="col">
<h2 class="title">@yield('title')</h2> <h2 class="title">@yield('title')</h2>
<div class="float-right"> <div class="float-right">
<button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" <button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" type="button" data-toggle="collapse"
type="button"
data-toggle="collapse"
data-target="#table-filter-row"> data-target="#table-filter-row">
<i class="fas fa-filter"></i> <i class="fas fa-filter"></i>
</button> </button>
@ -21,17 +19,13 @@
<hr class="my-2"> <hr class="my-2">
<div class="row collapse d-md-flex" <div class="row collapse d-md-flex" id="table-filter-row">
id="table-filter-row">
<div class="col-12 col-md-6 col-xl-2"> <div class="col-12 col-md-6 col-xl-2">
<div class="input-group"> <div class="input-group">
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-search"></i></span> <span class="input-group-text"><i class="fas fa-search"></i></span>
</div> </div>
<input type="text" <input type="text" id="search" class="form-control" placeholder="{{ $__t('Search') }}">
id="search"
class="form-control"
placeholder="{{ $__t('Search') }}">
</div> </div>
</div> </div>
<div class="col-12 col-md-6 col-xl-3"> <div class="col-12 col-md-6 col-xl-3">
@ -39,22 +33,20 @@
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-filter"></i>&nbsp;{{ $__t('Product') }}</span> <span class="input-group-text"><i class="fas fa-filter"></i>&nbsp;{{ $__t('Product') }}</span>
</div> </div>
<select class="custom-control custom-select" {{-- TODO: Select2: dynamic data: products --}}
id="product-filter"> <select class="select2 custom-control custom-select" id="product-filter">
<option value="all">{{ $__t('All') }}</option> <option value="all">{{ $__t('All') }}</option>
@foreach($products as $product)
<option value="{{ $product->id }}">{{ $product->name }}</option>
@endforeach
</select> </select>
</div> </div>
</div> </div>
<div class="col-12 col-md-6 col-xl-3"> <div class="col-12 col-md-6 col-xl-3">
<div class="input-group"> <div class="input-group">
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-filter"></i>&nbsp;{{ $__t('Transaction type') }}</span> <span class="input-group-text"><i
class="fas fa-filter"></i>&nbsp;{{ $__t('Transaction type') }}</span>
</div> </div>
<select class="custom-control custom-select" {{-- TODO: Select2: static data --}}
id="transaction-type-filter"> <select class="custom-control custom-select" id="transaction-type-filter">
<option value="all">{{ $__t('All') }}</option> <option value="all">{{ $__t('All') }}</option>
@foreach ($transactionTypes as $transactionType) @foreach ($transactionTypes as $transactionType)
<option value="{{ $transactionType }}">{{ $__t($transactionType) }}</option> <option value="{{ $transactionType }}">{{ $__t($transactionType) }}</option>
@ -67,8 +59,8 @@
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-filter"></i>&nbsp;{{ $__t('User') }}</span> <span class="input-group-text"><i class="fas fa-filter"></i>&nbsp;{{ $__t('User') }}</span>
</div> </div>
<select class="custom-control custom-select" {{-- TODO: Select2: dynamic data: users --}}
id="user-filter"> <select class="custom-control custom-select" id="user-filter">
<option value="all">{{ $__t('All') }}</option> <option value="all">{{ $__t('All') }}</option>
@foreach ($users as $user) @foreach ($users as $user)
<option value="{{ $user->id }}">{{ $user->display_name }}</option> <option value="{{ $user->id }}">{{ $user->display_name }}</option>
@ -78,9 +70,7 @@
</div> </div>
<div class="col"> <div class="col">
<div class="float-right mt-1"> <div class="float-right mt-1">
<a id="clear-filter-button" <a id="clear-filter-button" class="btn btn-sm btn-outline-info" href="#">
class="btn btn-sm btn-outline-info"
href="#">
{{ $__t('Clear filter') }} {{ $__t('Clear filter') }}
</a> </a>
</div> </div>
@ -89,16 +79,14 @@
<div class="row mt-2"> <div class="row mt-2">
<div class="col"> <div class="col">
<table id="stock-journal-summary-table" {{-- TODO: DataTables: dynamic data: uihelper_stock_journal_summary --}}
class="table table-sm table-striped nowrap w-100"> <table id="stock-journal-summary-table" class="table table-sm table-striped nowrap w-100">
<thead> <thead>
<tr> <tr>
<th class="border-right"><a class="text-muted change-table-columns-visibility-button" <th class="border-right"><a class="text-muted change-table-columns-visibility-button"
data-toggle="tooltip" data-toggle="tooltip" data-toggle="tooltip" title="{{ $__t('Table options') }}"
data-toggle="tooltip" data-table-selector="#stock-journal-summary-table" href="#"><i
title="{{ $__t('Table options') }}" class="fas fa-eye"></i></a>
data-table-selector="#stock-journal-summary-table"
href="#"><i class="fas fa-eye"></i></a>
</th> </th>
<th class="allow-grouping">{{ $__t('Product') }}</th> <th class="allow-grouping">{{ $__t('Product') }}</th>
<th class="allow-grouping">{{ $__t('Transaction type') }}</th> <th class="allow-grouping">{{ $__t('Transaction type') }}</th>
@ -120,7 +108,9 @@
{{ $journalEntry->user_display_name }} {{ $journalEntry->user_display_name }}
</td> </td>
<td> <td>
<span class="locale-number locale-number-quantity-amount">{{ $journalEntry->amount }}</span> {{ $__n($journalEntry->amount, $journalEntry->qu_name, $journalEntry->qu_name_plural, true) }} <span
class="locale-number locale-number-quantity-amount">{{ $journalEntry->amount }}</span>
{{ $__n($journalEntry->amount, $journalEntry->qu_name, $journalEntry->qu_name_plural, true) }}
</td> </td>
</tr> </tr>
@endforeach @endforeach

View File

@ -5,8 +5,7 @@
@section('viewJsName', 'stockoverview') @section('viewJsName', 'stockoverview')
@push('pageStyles') @push('pageStyles')
<link href="{{ $U('/node_modules/animate.css/animate.min.css?v=', true) }}{{ $version }}" <link href="{{ $U('/node_modules/animate.css/animate.min.css?v=', true) }}{{ $version }}" rel="stylesheet">
rel="stylesheet">
@endpush @endpush
@push('pageScripts') @push('pageScripts')
@ -21,17 +20,13 @@
@yield('title') @yield('title')
</h2> </h2>
<h2 class="mb-0 mr-auto order-3 order-md-1 width-xs-sm-100"> <h2 class="mb-0 mr-auto order-3 order-md-1 width-xs-sm-100">
<span id="info-current-stock" <span id="info-current-stock" class="text-muted small"></span>
class="text-muted small"></span>
</h2> </h2>
<button class="btn btn-outline-dark d-md-none mt-2 float-right order-1 order-md-3" <button class="btn btn-outline-dark d-md-none mt-2 float-right order-1 order-md-3" type="button"
type="button" data-toggle="collapse" data-target="#related-links">
data-toggle="collapse"
data-target="#related-links">
<i class="fas fa-ellipsis-v"></i> <i class="fas fa-ellipsis-v"></i>
</button> </button>
<div class="related-links collapse d-md-flex order-2 width-xs-sm-100" <div class="related-links collapse d-md-flex order-2 width-xs-sm-100" id="related-links">
id="related-links">
<a class="btn btn-outline-dark responsive-button m-1 mt-md-0 mb-md-0 float-right" <a class="btn btn-outline-dark responsive-button m-1 mt-md-0 mb-md-0 float-right"
href="{{ $U('/stockjournal') }}"> href="{{ $U('/stockjournal') }}">
{{ $__t('Journal') }} {{ $__t('Journal') }}
@ -50,47 +45,34 @@
</div> </div>
<div class="border-top border-bottom my-2 py-1"> <div class="border-top border-bottom my-2 py-1">
@if (GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING) @if (GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING)
<div id="info-expired-products" <div id="info-expired-products" data-status-filter="expired"
data-status-filter="expired"
class="error-message status-filter-message responsive-button mr-2"></div> class="error-message status-filter-message responsive-button mr-2"></div>
<div id="info-overdue-products" <div id="info-overdue-products" data-status-filter="overdue"
data-status-filter="overdue"
class="secondary-message status-filter-message responsive-button mr-2"></div> class="secondary-message status-filter-message responsive-button mr-2"></div>
<div id="info-duesoon-products" <div id="info-duesoon-products" data-next-x-days="{{ $nextXDays }}" data-status-filter="duesoon"
data-next-x-days="{{ $nextXDays }}"
data-status-filter="duesoon"
class="warning-message status-filter-message responsive-button mr-2"></div> class="warning-message status-filter-message responsive-button mr-2"></div>
@endif @endif
<div id="info-missing-products" <div id="info-missing-products" data-status-filter="belowminstockamount"
data-status-filter="belowminstockamount"
class="normal-message status-filter-message responsive-button"></div> class="normal-message status-filter-message responsive-button"></div>
<div class="float-right"> <div class="float-right">
<a class="btn btn-sm btn-outline-info d-md-none mt-1" <a class="btn btn-sm btn-outline-info d-md-none mt-1" data-toggle="collapse" href="#table-filter-row"
data-toggle="collapse"
href="#table-filter-row"
role="button"> role="button">
<i class="fas fa-filter"></i> <i class="fas fa-filter"></i>
</a> </a>
<a id="clear-filter-button" <a id="clear-filter-button" class="btn btn-sm btn-outline-info mt-1" href="#">
class="btn btn-sm btn-outline-info mt-1"
href="#">
{{ $__t('Clear filter') }} {{ $__t('Clear filter') }}
</a> </a>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="row collapse d-md-flex" <div class="row collapse d-md-flex" id="table-filter-row">
id="table-filter-row">
<div class="col-12 col-md-6 col-xl-3"> <div class="col-12 col-md-6 col-xl-3">
<div class="input-group"> <div class="input-group">
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-search"></i></span> <span class="input-group-text"><i class="fas fa-search"></i></span>
</div> </div>
<input type="text" <input type="text" id="search" class="form-control" placeholder="{{ $__t('Search') }}">
id="search"
class="form-control"
placeholder="{{ $__t('Search') }}">
</div> </div>
</div> </div>
@if (GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING) @if (GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING)
@ -99,8 +81,8 @@
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-filter"></i>&nbsp;{{ $__t('Location') }}</span> <span class="input-group-text"><i class="fas fa-filter"></i>&nbsp;{{ $__t('Location') }}</span>
</div> </div>
<select class="custom-control custom-select" {{-- TODO: Select2: dynamic data: locations --}}
id="location-filter"> <select class="custom-control custom-select" id="location-filter">
<option value="all">{{ $__t('All') }}</option> <option value="all">{{ $__t('All') }}</option>
@foreach ($locations as $location) @foreach ($locations as $location)
<option value="{{ $location->name }}">{{ $location->name }}</option> <option value="{{ $location->name }}">{{ $location->name }}</option>
@ -114,8 +96,8 @@
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-filter"></i>&nbsp;{{ $__t('Product group') }}</span> <span class="input-group-text"><i class="fas fa-filter"></i>&nbsp;{{ $__t('Product group') }}</span>
</div> </div>
<select class="custom-control custom-select" {{-- TODO: Select2: dynamic data: product_groups --}}
id="product-group-filter"> <select class="custom-control custom-select" id="product-group-filter">
<option value="all">{{ $__t('All') }}</option> <option value="all">{{ $__t('All') }}</option>
@foreach ($productGroups as $productGroup) @foreach ($productGroups as $productGroup)
<option value="{{ $productGroup->name }}">{{ $productGroup->name }}</option> <option value="{{ $productGroup->name }}">{{ $productGroup->name }}</option>
@ -128,10 +110,8 @@
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-filter"></i>&nbsp;{{ $__t('Status') }}</span> <span class="input-group-text"><i class="fas fa-filter"></i>&nbsp;{{ $__t('Status') }}</span>
</div> </div>
<select class="custom-control custom-select" <select class="custom-control custom-select" id="status-filter">
id="status-filter"> <option class="bg-white" value="all">{{ $__t('All') }}</option>
<option class="bg-white"
value="all">{{ $__t('All') }}</option>
@if (GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING) @if (GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING)
<option value="duesoon">{{ $__t('Due soon') }}</option> <option value="duesoon">{{ $__t('Due soon') }}</option>
<option value="overdue">{{ $__t('Overdue') }}</option> <option value="overdue">{{ $__t('Overdue') }}</option>
@ -146,22 +126,20 @@
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<table id="stock-overview-table" {{-- TODO: DataTables: dynamic data: uihelper_stock_current_overview --}}
class="table table-sm table-striped nowrap w-100"> <table id="stock-overview-table" class="table table-sm table-striped nowrap w-100">
<thead> <thead>
<tr> <tr>
<th class="border-right"><a class="text-muted change-table-columns-visibility-button" <th class="border-right"><a class="text-muted change-table-columns-visibility-button"
data-toggle="tooltip" data-toggle="tooltip" data-toggle="tooltip" title="{{ $__t('Table options') }}"
data-toggle="tooltip" data-table-selector="#stock-overview-table" href="#"><i class="fas fa-eye"></i></a>
title="{{ $__t('Table options') }}"
data-table-selector="#stock-overview-table"
href="#"><i class="fas fa-eye"></i></a>
</th> </th>
<th>{{ $__t('Product') }}</th> <th>{{ $__t('Product') }}</th>
<th class="allow-grouping">{{ $__t('Product group') }}</th> <th class="allow-grouping">{{ $__t('Product group') }}</th>
<th>{{ $__t('Amount') }}</th> <th>{{ $__t('Amount') }}</th>
<th class="@if (!GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) d-none @endif">{{ $__t('Value') }}</th> <th class="@if (!GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) d-none @endif">{{ $__t('Value') }}</th>
<th class="@if(!GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING) d-none @endif allow-grouping">{{ $__t('Next due date') }}</th> <th class="@if (!GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING) d-none @endif allow-grouping">
{{ $__t('Next due date') }}</th>
<th class="d-none">Hidden location</th> <th class="d-none">Hidden location</th>
<th class="d-none">Hidden status</th> <th class="d-none">Hidden status</th>
<th class="d-none">Hidden product group</th> <th class="d-none">Hidden product group</th>
@ -176,33 +154,34 @@
<th>{{ $__t('Product picture') }}</th> <th>{{ $__t('Product picture') }}</th>
<th>{{ $__t('Average price') }}</th> <th>{{ $__t('Average price') }}</th>
@include('components.userfields_thead', array( @include('components.userfields_thead', [
'userfields' => $userfields 'userfields' => $userfields,
)) ])
</tr> </tr>
</thead> </thead>
<tbody class="d-none"> <tbody class="d-none">
@foreach ($currentStock as $currentStockEntry) @foreach ($currentStock as $currentStockEntry)
<tr id="product-{{ $currentStockEntry->product_id }}-row" <tr id="product-{{ $currentStockEntry->product_id }}-row"
class="@if(GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING && $currentStockEntry->best_before_date < date('Y-m-d 23:59:59', strtotime('-1 days')) && $currentStockEntry->amount > 0) @if($currentStockEntry->due_type == 1) table-secondary @else table-danger @endif @elseif(GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING && $currentStockEntry->best_before_date < date('Y-m-d 23:59:59', strtotime('+' . $nextXDays . ' days')) && $currentStockEntry->amount > 0) table-warning @elseif ($currentStockEntry->product_missing) table-info @endif"> class="@if (GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING && $currentStockEntry->best_before_date < date('Y-m-d 23:59:59', strtotime('-1 days')) && $currentStockEntry->amount > 0) @if ($currentStockEntry->due_type == 1) table-secondary @else table-danger @endif
@elseif(GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING && $currentStockEntry->best_before_date < date('Y-m-d 23:59:59', strtotime('+' . $nextXDays . ' days')) && $currentStockEntry->amount > 0)
table-warning
@elseif ($currentStockEntry->product_missing)
table-info @endif">
<td class="fit-content border-right"> <td class="fit-content border-right">
<a class="permission-STOCK_CONSUME btn btn-success btn-sm product-consume-button @if ($currentStockEntry->amount_aggregated < $currentStockEntry->quick_consume_amount || $currentStockEntry->enable_tare_weight_handling == 1) disabled @endif" <a class="permission-STOCK_CONSUME btn btn-success btn-sm product-consume-button @if ($currentStockEntry->amount_aggregated < $currentStockEntry->quick_consume_amount || $currentStockEntry->enable_tare_weight_handling == 1) disabled @endif"
href="#" href="#" data-toggle="tooltip" data-placement="left"
data-toggle="tooltip"
data-placement="left"
title="{{ $__t('Consume %1$s of %2$s',floatval($currentStockEntry->quick_consume_amount) . ' ' . $currentStockEntry->qu_unit_name,$currentStockEntry->product_name) }}" title="{{ $__t('Consume %1$s of %2$s',floatval($currentStockEntry->quick_consume_amount) . ' ' . $currentStockEntry->qu_unit_name,$currentStockEntry->product_name) }}"
data-product-id="{{ $currentStockEntry->product_id }}" data-product-id="{{ $currentStockEntry->product_id }}"
data-product-name="{{ $currentStockEntry->product_name }}" data-product-name="{{ $currentStockEntry->product_name }}"
data-product-qu-name="{{ $currentStockEntry->qu_unit_name }}" data-product-qu-name="{{ $currentStockEntry->qu_unit_name }}"
data-consume-amount="{{ $currentStockEntry->quick_consume_amount }}"> data-consume-amount="{{ $currentStockEntry->quick_consume_amount }}">
<i class="fas fa-utensils"></i> <span class="locale-number locale-number-quantity-amount">{{ $currentStockEntry->quick_consume_amount }}</span> <i class="fas fa-utensils"></i> <span
class="locale-number locale-number-quantity-amount">{{ $currentStockEntry->quick_consume_amount }}</span>
</a> </a>
<a id="product-{{ $currentStockEntry->product_id }}-consume-all-button" <a id="product-{{ $currentStockEntry->product_id }}-consume-all-button"
class="permission-STOCK_CONSUME btn btn-danger btn-sm product-consume-button @if ($currentStockEntry->amount_aggregated == 0) disabled @endif" class="permission-STOCK_CONSUME btn btn-danger btn-sm product-consume-button @if ($currentStockEntry->amount_aggregated == 0) disabled @endif"
href="#" href="#" data-toggle="tooltip" data-placement="right"
data-toggle="tooltip"
data-placement="right"
title="{{ $__t('Consume all %s which are currently in stock', $currentStockEntry->product_name) }}" title="{{ $__t('Consume all %s which are currently in stock', $currentStockEntry->product_name) }}"
data-product-id="{{ $currentStockEntry->product_id }}" data-product-id="{{ $currentStockEntry->product_id }}"
data-product-name="{{ $currentStockEntry->product_name }}" data-product-name="{{ $currentStockEntry->product_name }}"
@ -213,20 +192,18 @@
</a> </a>
@if (GROCY_FEATURE_FLAG_STOCK_PRODUCT_OPENED_TRACKING) @if (GROCY_FEATURE_FLAG_STOCK_PRODUCT_OPENED_TRACKING)
<a class="btn btn-success btn-sm product-open-button @if ($currentStockEntry->amount_aggregated < $currentStockEntry->quick_consume_amount || $currentStockEntry->amount_aggregated == $currentStockEntry->amount_opened_aggregated || $currentStockEntry->enable_tare_weight_handling == 1) disabled @endif" <a class="btn btn-success btn-sm product-open-button @if ($currentStockEntry->amount_aggregated < $currentStockEntry->quick_consume_amount || $currentStockEntry->amount_aggregated == $currentStockEntry->amount_opened_aggregated || $currentStockEntry->enable_tare_weight_handling == 1) disabled @endif"
href="#" href="#" data-toggle="tooltip" data-placement="left"
data-toggle="tooltip"
data-placement="left"
title="{{ $__t('Mark %1$s of %2$s as open',floatval($currentStockEntry->quick_consume_amount) . ' ' . $currentStockEntry->qu_unit_name,$currentStockEntry->product_name) }}" title="{{ $__t('Mark %1$s of %2$s as open',floatval($currentStockEntry->quick_consume_amount) . ' ' . $currentStockEntry->qu_unit_name,$currentStockEntry->product_name) }}"
data-product-id="{{ $currentStockEntry->product_id }}" data-product-id="{{ $currentStockEntry->product_id }}"
data-product-name="{{ $currentStockEntry->product_name }}" data-product-name="{{ $currentStockEntry->product_name }}"
data-product-qu-name="{{ $currentStockEntry->qu_unit_name }}" data-product-qu-name="{{ $currentStockEntry->qu_unit_name }}"
data-open-amount="{{ $currentStockEntry->quick_consume_amount }}"> data-open-amount="{{ $currentStockEntry->quick_consume_amount }}">
<i class="fas fa-box-open"></i> <span class="locale-number locale-number-quantity-amount">{{ $currentStockEntry->quick_consume_amount }}</span> <i class="fas fa-box-open"></i> <span
class="locale-number locale-number-quantity-amount">{{ $currentStockEntry->quick_consume_amount }}</span>
</a> </a>
@endif @endif
<div class="dropdown d-inline-block"> <div class="dropdown d-inline-block">
<button class="btn btn-sm btn-light text-secondary" <button class="btn btn-sm btn-light text-secondary" type="button"
type="button"
data-toggle="dropdown"> data-toggle="dropdown">
<i class="fas fa-ellipsis-v"></i> <i class="fas fa-ellipsis-v"></i>
</button> </button>
@ -235,77 +212,75 @@
<a class="dropdown-item show-as-dialog-link permission-SHOPPINGLIST_ITEMS_ADD" <a class="dropdown-item show-as-dialog-link permission-SHOPPINGLIST_ITEMS_ADD"
type="button" type="button"
href="{{ $U('/shoppinglistitem/new?embedded&updateexistingproduct&product=' . $currentStockEntry->product_id) }}"> href="{{ $U('/shoppinglistitem/new?embedded&updateexistingproduct&product=' . $currentStockEntry->product_id) }}">
<span class="dropdown-item-icon"><i class="fas fa-shopping-cart"></i></span> <span class="dropdown-item-text">{{ $__t('Add to shopping list') }}</span> <span class="dropdown-item-icon"><i class="fas fa-shopping-cart"></i></span>
<span
class="dropdown-item-text">{{ $__t('Add to shopping list') }}</span>
</a> </a>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
@endif @endif
<a class="dropdown-item show-as-dialog-link permission-STOCK_PURCHASE" <a class="dropdown-item show-as-dialog-link permission-STOCK_PURCHASE" type="button"
type="button"
href="{{ $U('/purchase?embedded&product=' . $currentStockEntry->product_id) }}"> href="{{ $U('/purchase?embedded&product=' . $currentStockEntry->product_id) }}">
<span class="dropdown-item-icon"><i class="fas fa-cart-plus"></i></span> <span class="dropdown-item-text">{{ $__t('Purchase') }}</span> <span class="dropdown-item-icon"><i class="fas fa-cart-plus"></i></span> <span
class="dropdown-item-text">{{ $__t('Purchase') }}</span>
</a> </a>
<a class="dropdown-item show-as-dialog-link permission-STOCK_CONSUME @if ($currentStockEntry->amount_aggregated <= 0) disabled @endif" <a class="dropdown-item show-as-dialog-link permission-STOCK_CONSUME @if ($currentStockEntry->amount_aggregated <= 0) disabled @endif"
type="button" type="button"
href="{{ $U('/consume?embedded&product=' . $currentStockEntry->product_id) }}"> href="{{ $U('/consume?embedded&product=' . $currentStockEntry->product_id) }}">
<span class="dropdown-item-icon"><i class="fas fa-utensils"></i></span> <span class="dropdown-item-text">{{ $__t('Consume') }}</span> <span class="dropdown-item-icon"><i class="fas fa-utensils"></i></span> <span
class="dropdown-item-text">{{ $__t('Consume') }}</span>
</a> </a>
@if (GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING) @if (GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING)
<a class="dropdown-item show-as-dialog-link permission-STOCK_TRANSFER @if ($currentStockEntry->amount <= 0) disabled @endif" <a class="dropdown-item show-as-dialog-link permission-STOCK_TRANSFER @if ($currentStockEntry->amount <= 0) disabled @endif"
type="button" type="button"
href="{{ $U('/transfer?embedded&product=' . $currentStockEntry->product_id) }}"> href="{{ $U('/transfer?embedded&product=' . $currentStockEntry->product_id) }}">
<span class="dropdown-item-icon"><i class="fas fa-exchange-alt"></i></span> <span class="dropdown-item-text">{{ $__t('Transfer') }}</span> <span class="dropdown-item-icon"><i class="fas fa-exchange-alt"></i></span>
<span class="dropdown-item-text">{{ $__t('Transfer') }}</span>
</a> </a>
@endif @endif
<a class="dropdown-item show-as-dialog-link permission-STOCK_INVENTORY" <a class="dropdown-item show-as-dialog-link permission-STOCK_INVENTORY"
type="button" type="button"
href="{{ $U('/inventory?embedded&product=' . $currentStockEntry->product_id) }}"> href="{{ $U('/inventory?embedded&product=' . $currentStockEntry->product_id) }}">
<span class="dropdown-item-icon"><i class="fas fa-list"></i></span> <span class="dropdown-item-text">{{ $__t('Inventory') }}</span> <span class="dropdown-item-icon"><i class="fas fa-list"></i></span> <span
class="dropdown-item-text">{{ $__t('Inventory') }}</span>
</a> </a>
@if (GROCY_FEATURE_FLAG_RECIPES) @if (GROCY_FEATURE_FLAG_RECIPES)
<a class="dropdown-item" <a class="dropdown-item" type="button"
type="button"
href="{{ $U('/recipes?search=') }}{{ $currentStockEntry->product_name }}"> href="{{ $U('/recipes?search=') }}{{ $currentStockEntry->product_name }}">
<span class="dropdown-item-text">{{ $__t('Search for recipes containing this product') }}</span> <span
class="dropdown-item-text">{{ $__t('Search for recipes containing this product') }}</span>
</a> </a>
@endif @endif
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a class="dropdown-item product-name-cell" <a class="dropdown-item product-name-cell"
data-product-id="{{ $currentStockEntry->product_id }}" data-product-id="{{ $currentStockEntry->product_id }}" type="button"
type="button"
href="#"> href="#">
<span class="dropdown-item-text">{{ $__t('Product overview') }}</span> <span class="dropdown-item-text">{{ $__t('Product overview') }}</span>
</a> </a>
<a class="dropdown-item show-as-dialog-link" <a class="dropdown-item show-as-dialog-link" type="button"
type="button"
href="{{ $U('/stockentries?embedded&product=') }}{{ $currentStockEntry->product_id }}" href="{{ $U('/stockentries?embedded&product=') }}{{ $currentStockEntry->product_id }}"
data-product-id="{{ $currentStockEntry->product_id }}"> data-product-id="{{ $currentStockEntry->product_id }}">
<span class="dropdown-item-text">{{ $__t('Stock entries') }}</span> <span class="dropdown-item-text">{{ $__t('Stock entries') }}</span>
</a> </a>
<a class="dropdown-item show-as-dialog-link" <a class="dropdown-item show-as-dialog-link" type="button"
type="button"
href="{{ $U('/stockjournal?embedded&product=') }}{{ $currentStockEntry->product_id }}"> href="{{ $U('/stockjournal?embedded&product=') }}{{ $currentStockEntry->product_id }}">
<span class="dropdown-item-text">{{ $__t('Stock journal') }}</span> <span class="dropdown-item-text">{{ $__t('Stock journal') }}</span>
</a> </a>
<a class="dropdown-item show-as-dialog-link" <a class="dropdown-item show-as-dialog-link" type="button"
type="button"
href="{{ $U('/stockjournal/summary?embedded&product_id=') }}{{ $currentStockEntry->product_id }}"> href="{{ $U('/stockjournal/summary?embedded&product_id=') }}{{ $currentStockEntry->product_id }}">
<span class="dropdown-item-text">{{ $__t('Stock journal summary') }}</span> <span class="dropdown-item-text">{{ $__t('Stock journal summary') }}</span>
</a> </a>
<a class="dropdown-item permission-MASTER_DATA_EDIT link-return" <a class="dropdown-item permission-MASTER_DATA_EDIT link-return" type="button"
type="button"
data-href="{{ $U('/product/') }}{{ $currentStockEntry->product_id }}"> data-href="{{ $U('/product/') }}{{ $currentStockEntry->product_id }}">
<span class="dropdown-item-text">{{ $__t('Edit product') }}</span> <span class="dropdown-item-text">{{ $__t('Edit product') }}</span>
</a> </a>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a class="dropdown-item" <a class="dropdown-item" type="button"
type="button"
href="{{ $U('/product/' . $currentStockEntry->product_id . '/grocycode?download=true') }}"> href="{{ $U('/product/' . $currentStockEntry->product_id . '/grocycode?download=true') }}">
{!! str_replace('grocycode', '<span class="ls-n1">grocycode</span>', $__t('Download %s grocycode', $__t('Product'))) !!} {!! str_replace('grocycode', '<span class="ls-n1">grocycode</span>', $__t('Download %s grocycode', $__t('Product'))) !!}
</a> </a>
@if (GROCY_FEATURE_FLAG_LABEL_PRINTER) @if (GROCY_FEATURE_FLAG_LABEL_PRINTER)
<a class="dropdown-item product-grocycode-label-print" <a class="dropdown-item product-grocycode-label-print"
data-product-id="{{ $currentStockEntry->product_id }}" data-product-id="{{ $currentStockEntry->product_id }}" type="button"
type="button"
href="#"> href="#">
{!! str_replace('grocycode', '<span class="ls-n1">grocycode</span>', $__t('Print %s grocycode on label printer', $__t('Product'))) !!} {!! str_replace('grocycode', '<span class="ls-n1">grocycode</span>', $__t('Print %s grocycode on label printer', $__t('Product'))) !!}
</a> </a>
@ -319,25 +294,34 @@
<span class="d-none">{{ $currentStockEntry->product_barcodes }}</span> <span class="d-none">{{ $currentStockEntry->product_barcodes }}</span>
</td> </td>
<td> <td>
@if($currentStockEntry->product_group_name !== null){{ $currentStockEntry->product_group_name }}@endif @if ($currentStockEntry->product_group_name !== null)
{{ $currentStockEntry->product_group_name }}@endif
</td> </td>
<td data-order="{{ $currentStockEntry->amount }}"> <td data-order="{{ $currentStockEntry->amount }}">
<span id="product-{{ $currentStockEntry->product_id }}-amount" <span id="product-{{ $currentStockEntry->product_id }}-amount"
class="locale-number locale-number-quantity-amount">{{ $currentStockEntry->amount }}</span> <span id="product-{{ $currentStockEntry->product_id }}-qu-name">{{ $__n($currentStockEntry->amount, $currentStockEntry->qu_unit_name, $currentStockEntry->qu_unit_name_plural) }}</span> class="locale-number locale-number-quantity-amount">{{ $currentStockEntry->amount }}</span>
<span
id="product-{{ $currentStockEntry->product_id }}-qu-name">{{ $__n($currentStockEntry->amount, $currentStockEntry->qu_unit_name, $currentStockEntry->qu_unit_name_plural) }}</span>
<span id="product-{{ $currentStockEntry->product_id }}-opened-amount" <span id="product-{{ $currentStockEntry->product_id }}-opened-amount"
class="small font-italic">@if($currentStockEntry->amount_opened > 0){{ $__t('%s opened', $currentStockEntry->amount_opened) }}@endif</span> class="small font-italic">
@if ($currentStockEntry->amount_opened > 0)
{{ $__t('%s opened', $currentStockEntry->amount_opened) }}@endif
</span>
@if ($currentStockEntry->is_aggregated_amount == 1) @if ($currentStockEntry->is_aggregated_amount == 1)
<span class="pl-1 text-secondary"> <span class="pl-1 text-secondary">
<i class="fas fa-custom-sigma-sign"></i> <span id="product-{{ $currentStockEntry->product_id }}-amount-aggregated" <i class="fas fa-custom-sigma-sign"></i> <span
class="locale-number locale-number-quantity-amount">{{ $currentStockEntry->amount_aggregated }}</span> {{ $__n($currentStockEntry->amount_aggregated, $currentStockEntry->qu_unit_name, $currentStockEntry->qu_unit_name_plural, true) }} id="product-{{ $currentStockEntry->product_id }}-amount-aggregated"
@if($currentStockEntry->amount_opened_aggregated > 0)<span id="product-{{ $currentStockEntry->product_id }}-opened-amount-aggregated" class="locale-number locale-number-quantity-amount">{{ $currentStockEntry->amount_aggregated }}</span>
class="small font-italic">{{ $__t('%s opened', $currentStockEntry->amount_opened_aggregated) }}</span>@endif {{ $__n($currentStockEntry->amount_aggregated,$currentStockEntry->qu_unit_name,$currentStockEntry->qu_unit_name_plural,true) }}
@if ($currentStockEntry->amount_opened_aggregated > 0)<span
id="product-{{ $currentStockEntry->product_id }}-opened-amount-aggregated"
class="small font-italic">{{ $__t('%s opened', $currentStockEntry->amount_opened_aggregated) }}</span>
@endif
</span> </span>
@endif @endif
@if (boolval($userSettings['show_icon_on_stock_overview_page_when_product_is_on_shopping_list'])) @if (boolval($userSettings['show_icon_on_stock_overview_page_when_product_is_on_shopping_list']))
@if ($currentStockEntry->on_shopping_list) @if ($currentStockEntry->on_shopping_list)
<span class="text-muted cursor-normal" <span class="text-muted cursor-normal" data-toggle="tooltip"
data-toggle="tooltip"
title="{{ $__t('This product is currently on a shopping list') }}"> title="{{ $__t('This product is currently on a shopping list') }}">
<i class="fas fa-shopping-cart"></i> <i class="fas fa-shopping-cart"></i>
</span> </span>
@ -349,7 +333,8 @@
class="locale-number locale-number-currency">{{ $currentStockEntry->value }}</span> class="locale-number locale-number-currency">{{ $currentStockEntry->value }}</span>
</td> </td>
<td class="@if (!GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING) d-none @endif"> <td class="@if (!GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING) d-none @endif">
<span id="product-{{ $currentStockEntry->product_id }}-next-due-date">{{ $currentStockEntry->best_before_date }}</span> <span
id="product-{{ $currentStockEntry->product_id }}-next-due-date">{{ $currentStockEntry->best_before_date }}</span>
<time id="product-{{ $currentStockEntry->product_id }}-next-due-date-timeago" <time id="product-{{ $currentStockEntry->product_id }}-next-due-date-timeago"
class="timeago timeago-contextual" class="timeago timeago-contextual"
@if (!empty($currentStockEntry->best_before_date)) datetime="{{ $currentStockEntry->best_before_date }} 23:59:59" @endif></time> @if (!empty($currentStockEntry->best_before_date)) datetime="{{ $currentStockEntry->best_before_date }} 23:59:59" @endif></time>
@ -360,33 +345,37 @@
@endforeach @endforeach
</td> </td>
<td class="d-none"> <td class="d-none">
@if($currentStockEntry->best_before_date < date('Y-m-d @if ($currentStockEntry->best_before_date <
date(
'Y-m-d
23:59:59', 23:59:59',
strtotime('-' strtotime('-' . '1' . ' days'),
. '1' ) && $currentStockEntry->amount > 0)
. ' days' @if ($currentStockEntry->due_type == 1) overdue
)) @else
&& expired @endif
$currentStockEntry->amount > 0) @if($currentStockEntry->due_type == 1) overdue @else expired @endif @elseif($currentStockEntry->best_before_date < date('Y-m-d @elseif($currentStockEntry->best_before_date <
date(
'Y-m-d
23:59:59', 23:59:59',
strtotime('+' strtotime('+' . $nextXDays . ' days'),
. ) && $currentStockEntry->amount > 0)
$nextXDays duesoon
. ' days' @endif
))
&&
$currentStockEntry->amount > 0) duesoon @endif
@if ($currentStockEntry->amount_aggregated > 0) instockX @endif @if ($currentStockEntry->amount_aggregated > 0) instockX @endif
@if ($currentStockEntry->product_missing) belowminstockamount @endif @if ($currentStockEntry->product_missing) belowminstockamount
@endif
</td> </td>
<td class="d-none"> <td class="d-none">
xx{{ $currentStockEntry->product_group_name }}xx xx{{ $currentStockEntry->product_group_name }}xx
</td> </td>
<td> <td>
<span class="locale-number locale-number-quantity-amount">{{ $currentStockEntry->product_calories }}</span> <span
class="locale-number locale-number-quantity-amount">{{ $currentStockEntry->product_calories }}</span>
</td> </td>
<td> <td>
<span class="locale-number locale-number-quantity-amount">{{ $currentStockEntry->calories }}</span> <span
class="locale-number locale-number-quantity-amount">{{ $currentStockEntry->calories }}</span>
</td> </td>
<td> <td>
{{ $currentStockEntry->last_purchased }} {{ $currentStockEntry->last_purchased }}
@ -394,10 +383,12 @@
datetime="{{ $currentStockEntry->last_purchased }}"></time> datetime="{{ $currentStockEntry->last_purchased }}"></time>
</td> </td>
<td class="@if (!GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) d-none @endif"> <td class="@if (!GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) d-none @endif">
<span class="locale-number locale-number-currency">{{ $currentStockEntry->last_price }}</span> <span
class="locale-number locale-number-currency">{{ $currentStockEntry->last_price }}</span>
</td> </td>
<td> <td>
<span class="locale-number locale-number-quantity-amount">{{ $currentStockEntry->min_stock_amount }}</span> <span
class="locale-number locale-number-quantity-amount">{{ $currentStockEntry->min_stock_amount }}</span>
</td> </td>
<td> <td>
{!! $currentStockEntry->product_description !!} {!! $currentStockEntry->product_description !!}
@ -416,13 +407,18 @@
@endif @endif
</td> </td>
<td class="@if (!GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) d-none @endif"> <td class="@if (!GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) d-none @endif">
<span class="locale-number locale-number-currency">{{ $currentStockEntry->average_price }}</span> <span
class="locale-number locale-number-currency">{{ $currentStockEntry->average_price }}</span>
</td> </td>
@include('components.userfields_tbody', array( @include('components.userfields_tbody', [
'userfields' => $userfields, 'userfields' => $userfields,
'userfieldValues' => FindAllObjectsInArrayByPropertyValue($userfieldValues, 'object_id', $currentStockEntry->product_id) 'userfieldValues' => FindAllObjectsInArrayByPropertyValue(
)) $userfieldValues,
'object_id',
$currentStockEntry->product_id
),
])
</tr> </tr>
@endforeach @endforeach
@ -431,18 +427,14 @@
</div> </div>
</div> </div>
<div class="modal fade" <div class="modal fade" id="stockoverview-productcard-modal" tabindex="-1">
id="stockoverview-productcard-modal"
tabindex="-1">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content text-center"> <div class="modal-content text-center">
<div class="modal-body"> <div class="modal-body">
@include('components.productcard') @include('components.productcard')
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" <button type="button" class="btn btn-secondary" data-dismiss="modal">{{ $__t('Close') }}</button>
class="btn btn-secondary"
data-dismiss="modal">{{ $__t('Close') }}</button>
</div> </div>
</div> </div>
</div> </div>

View File

@ -21,8 +21,8 @@
@if (GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING) @if (GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING)
<div class="form-group"> <div class="form-group">
<label for="product_presets_location_id">{{ $__t('Location') }}</label> <label for="product_presets_location_id">{{ $__t('Location') }}</label>
<select class="custom-control custom-select user-setting-control" {{-- TODO: Select2: dynamic data: locations --}}
id="product_presets_location_id" <select class="custom-control custom-select user-setting-control" id="product_presets_location_id"
data-setting-key="product_presets_location_id"> data-setting-key="product_presets_location_id">
<option value="-1"></option> <option value="-1"></option>
@foreach ($locations as $location) @foreach ($locations as $location)
@ -34,8 +34,8 @@
<div class="form-group"> <div class="form-group">
<label for="product_presets_product_group_id">{{ $__t('Product group') }}</label> <label for="product_presets_product_group_id">{{ $__t('Product group') }}</label>
<select class="custom-control custom-select user-setting-control" {{-- TODO: Select2: dynamic data: product_groups --}}
id="product_presets_product_group_id" <select class="custom-control custom-select user-setting-control" id="product_presets_product_group_id"
data-setting-key="product_presets_product_group_id"> data-setting-key="product_presets_product_group_id">
<option value="-1"></option> <option value="-1"></option>
@foreach ($productGroups as $productGroup) @foreach ($productGroups as $productGroup)
@ -46,8 +46,8 @@
<div class="form-group"> <div class="form-group">
<label for="product_presets_qu_id">{{ $__t('Quantity unit') }}</label> <label for="product_presets_qu_id">{{ $__t('Quantity unit') }}</label>
<select class="custom-control custom-select user-setting-control" {{-- TODO: Select2: dynamic data: quantity_units --}}
id="product_presets_qu_id" <select class="custom-control custom-select user-setting-control" id="product_presets_qu_id"
data-setting-key="product_presets_qu_id"> data-setting-key="product_presets_qu_id">
<option value="-1"></option> <option value="-1"></option>
@foreach ($quantityunits as $quantityunit) @foreach ($quantityunits as $quantityunit)
@ -56,19 +56,18 @@
</select> </select>
</div> </div>
@include('components.numberpicker', array( @include('components.numberpicker', [
'id' => 'product_presets_default_due_days', 'id' => 'product_presets_default_due_days',
'additionalAttributes' => 'data-setting-key="product_presets_default_due_days"', 'additionalAttributes' => 'data-setting-key="product_presets_default_due_days"',
'label' => 'Default due days', 'label' => 'Default due days',
'min' => -1, 'min' => -1,
'additionalCssClasses' => 'user-setting-control' 'additionalCssClasses' => 'user-setting-control',
)) ])
@if (GROCY_FEATURE_FLAG_STOCK_PRODUCT_OPENED_TRACKING) @if (GROCY_FEATURE_FLAG_STOCK_PRODUCT_OPENED_TRACKING)
<div class="form-group"> <div class="form-group">
<div class="custom-control custom-checkbox"> <div class="custom-control custom-checkbox">
<input type="checkbox" <input type="checkbox" class="form-check-input custom-control-input user-setting-control"
class="form-check-input custom-control-input user-setting-control"
id="product_presets_treat_opened_as_out_of_stock" id="product_presets_treat_opened_as_out_of_stock"
data-setting-key="product_presets_treat_opened_as_out_of_stock"> data-setting-key="product_presets_treat_opened_as_out_of_stock">
<label class="form-check-label custom-control-label" <label class="form-check-label custom-control-label"
@ -81,18 +80,17 @@
</div> </div>
<h4 class="mt-2">{{ $__t('Stock overview') }}</h4> <h4 class="mt-2">{{ $__t('Stock overview') }}</h4>
@include('components.numberpicker', array( @include('components.numberpicker', [
'id' => 'stock_due_soon_days', 'id' => 'stock_due_soon_days',
'additionalAttributes' => 'data-setting-key="stock_due_soon_days"', 'additionalAttributes' => 'data-setting-key="stock_due_soon_days"',
'label' => 'Due soon days', 'label' => 'Due soon days',
'min' => 1, 'min' => 1,
'additionalCssClasses' => 'user-setting-control' 'additionalCssClasses' => 'user-setting-control',
)) ])
<div class="form-group"> <div class="form-group">
<div class="custom-control custom-checkbox"> <div class="custom-control custom-checkbox">
<input type="checkbox" <input type="checkbox" class="form-check-input custom-control-input user-setting-control"
class="form-check-input custom-control-input user-setting-control"
id="show_icon_on_stock_overview_page_when_product_is_on_shopping_list" id="show_icon_on_stock_overview_page_when_product_is_on_shopping_list"
data-setting-key="show_icon_on_stock_overview_page_when_product_is_on_shopping_list"> data-setting-key="show_icon_on_stock_overview_page_when_product_is_on_shopping_list">
<label class="form-check-label custom-control-label" <label class="form-check-label custom-control-label"
@ -103,23 +101,20 @@
</div> </div>
<h4 class="mt-2">{{ $__t('Purchase') }}</h4> <h4 class="mt-2">{{ $__t('Purchase') }}</h4>
@include('components.numberpicker', array( @include('components.numberpicker', [
'id' => 'stock_default_purchase_amount', 'id' => 'stock_default_purchase_amount',
'additionalAttributes' => 'data-setting-key="stock_default_purchase_amount"', 'additionalAttributes' => 'data-setting-key="stock_default_purchase_amount"',
'label' => 'Default amount for purchase', 'label' => 'Default amount for purchase',
'min' => '0.', 'min' => '0.',
'decimals' => $userSettings['stock_decimal_places_amounts'], 'decimals' => $userSettings['stock_decimal_places_amounts'],
'additionalCssClasses' => 'user-setting-control locale-number-input locale-number-quantity-amount', 'additionalCssClasses' => 'user-setting-control locale-number-input locale-number-quantity-amount',
)) ])
<div class="form-group"> <div class="form-group">
<div class="custom-control custom-checkbox"> <div class="custom-control custom-checkbox">
<input type="checkbox" <input type="checkbox" class="form-check-input custom-control-input user-setting-control"
class="form-check-input custom-control-input user-setting-control" id="show_purchased_date_on_purchase" data-setting-key="show_purchased_date_on_purchase">
id="show_purchased_date_on_purchase" <label class="form-check-label custom-control-label" for="show_purchased_date_on_purchase">
data-setting-key="show_purchased_date_on_purchase">
<label class="form-check-label custom-control-label"
for="show_purchased_date_on_purchase">
{{ $__t('Show purchased date on purchase and inventory page (otherwise the purchased date defaults to today)') }} {{ $__t('Show purchased date on purchase and inventory page (otherwise the purchased date defaults to today)') }}
</label> </label>
</div> </div>
@ -127,8 +122,7 @@
<div class="form-group"> <div class="form-group">
<div class="custom-control custom-checkbox"> <div class="custom-control custom-checkbox">
<input type="checkbox" <input type="checkbox" class="form-check-input custom-control-input user-setting-control"
class="form-check-input custom-control-input user-setting-control"
id="show_warning_on_purchase_when_due_date_is_earlier_than_next" id="show_warning_on_purchase_when_due_date_is_earlier_than_next"
data-setting-key="show_warning_on_purchase_when_due_date_is_earlier_than_next"> data-setting-key="show_warning_on_purchase_when_due_date_is_earlier_than_next">
<label class="form-check-label custom-control-label" <label class="form-check-label custom-control-label"
@ -139,20 +133,19 @@
</div> </div>
<h4 class="mt-2">{{ $__t('Consume') }}</h4> <h4 class="mt-2">{{ $__t('Consume') }}</h4>
@include('components.numberpicker', array( @include('components.numberpicker', [
'id' => 'stock_default_consume_amount', 'id' => 'stock_default_consume_amount',
'additionalAttributes' => 'data-setting-key="stock_default_consume_amount"', 'additionalAttributes' => 'data-setting-key="stock_default_consume_amount"',
'label' => 'Default amount for consume', 'label' => 'Default amount for consume',
'min' => 0, 'min' => 0,
'decimals' => $userSettings['stock_decimal_places_amounts'], 'decimals' => $userSettings['stock_decimal_places_amounts'],
'additionalCssClasses' => 'user-setting-control locale-number-input locale-number-quantity-amount', 'additionalCssClasses' => 'user-setting-control locale-number-input locale-number-quantity-amount',
'additionalGroupCssClasses' => 'mb-0' 'additionalGroupCssClasses' => 'mb-0',
)) ])
<div class="form-group"> <div class="form-group">
<div class="custom-control custom-checkbox"> <div class="custom-control custom-checkbox">
<input type="checkbox" <input type="checkbox" class="form-check-input custom-control-input user-setting-control"
class="form-check-input custom-control-input user-setting-control"
id="stock_default_consume_amount_use_quick_consume_amount" id="stock_default_consume_amount_use_quick_consume_amount"
data-setting-key="stock_default_consume_amount_use_quick_consume_amount"> data-setting-key="stock_default_consume_amount_use_quick_consume_amount">
<label class="form-check-label custom-control-label" <label class="form-check-label custom-control-label"
@ -164,43 +157,37 @@
<h4 class="mt-2">{{ $__t('Common') }}</h4> <h4 class="mt-2">{{ $__t('Common') }}</h4>
@include('components.numberpicker', array( @include('components.numberpicker', [
'id' => 'stock_decimal_places_amounts', 'id' => 'stock_decimal_places_amounts',
'additionalAttributes' => 'data-setting-key="stock_decimal_places_amounts"', 'additionalAttributes' => 'data-setting-key="stock_decimal_places_amounts"',
'label' => 'Decimal places allowed for amounts', 'label' => 'Decimal places allowed for amounts',
'min' => 0, 'min' => 0,
'additionalCssClasses' => 'user-setting-control' 'additionalCssClasses' => 'user-setting-control',
)) ])
@if (GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) @if (GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)
@include('components.numberpicker', array( @include('components.numberpicker', [
'id' => 'stock_decimal_places_prices', 'id' => 'stock_decimal_places_prices',
'additionalAttributes' => 'data-setting-key="stock_decimal_places_prices"', 'additionalAttributes' => 'data-setting-key="stock_decimal_places_prices"',
'label' => 'Decimal places allowed for prices', 'label' => 'Decimal places allowed for prices',
'min' => 0, 'min' => 0,
'additionalCssClasses' => 'user-setting-control' 'additionalCssClasses' => 'user-setting-control',
)) ])
<div class="form-group mt-n3"> <div class="form-group mt-n3">
<div class="custom-control custom-checkbox"> <div class="custom-control custom-checkbox">
<input type="checkbox" <input type="checkbox" class="form-check-input custom-control-input user-setting-control"
class="form-check-input custom-control-input user-setting-control" id="stock_auto_decimal_separator_prices" data-setting-key="stock_auto_decimal_separator_prices">
id="stock_auto_decimal_separator_prices" <label class="form-check-label custom-control-label" for="stock_auto_decimal_separator_prices">
data-setting-key="stock_auto_decimal_separator_prices">
<label class="form-check-label custom-control-label"
for="stock_auto_decimal_separator_prices">
{{ $__t('Add decimal separator automatically for price inputs') }} {{ $__t('Add decimal separator automatically for price inputs') }}
<i class="fas fa-question-circle text-muted" <i class="fas fa-question-circle text-muted" data-toggle="tooltip" data-trigger="hover click"
data-toggle="tooltip"
data-trigger="hover click"
title="{{ $__t('When enabled, you always have to enter the value including decimal places, the decimal separator will be automatically added based on the amount of allowed decimal places') }}"></i> title="{{ $__t('When enabled, you always have to enter the value including decimal places, the decimal separator will be automatically added based on the amount of allowed decimal places') }}"></i>
</label> </label>
</div> </div>
</div> </div>
@endif @endif
<a href="{{ $U('/stockoverview') }}" <a href="{{ $U('/stockoverview') }}" class="btn btn-success">{{ $__t('OK') }}</a>
class="btn btn-success">{{ $__t('OK') }}</a>
</div> </div>
</div> </div>
@stop @stop

View File

@ -10,27 +10,21 @@
<div class="title-related-links"> <div class="title-related-links">
<h2 class="title">@yield('title')</h2> <h2 class="title">@yield('title')</h2>
<div class="float-right"> <div class="float-right">
<button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" <button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" type="button"
type="button" data-toggle="collapse" data-target="#table-filter-row">
data-toggle="collapse"
data-target="#table-filter-row">
<i class="fas fa-filter"></i> <i class="fas fa-filter"></i>
</button> </button>
<button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" <button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" type="button"
type="button" data-toggle="collapse" data-target="#related-links">
data-toggle="collapse"
data-target="#related-links">
<i class="fas fa-ellipsis-v"></i> <i class="fas fa-ellipsis-v"></i>
</button> </button>
</div> </div>
<div class="related-links collapse d-md-flex order-2 width-xs-sm-100" <div class="related-links collapse d-md-flex order-2 width-xs-sm-100" id="related-links">
id="related-links">
<a class="btn btn-primary responsive-button m-1 mt-md-0 mb-md-0 float-right show-as-dialog-link" <a class="btn btn-primary responsive-button m-1 mt-md-0 mb-md-0 float-right show-as-dialog-link"
href="{{ $U('/taskcategory/new?embedded') }}"> href="{{ $U('/taskcategory/new?embedded') }}">
{{ $__t('Add') }} {{ $__t('Add') }}
</a> </a>
<a class="btn btn-outline-secondary" <a class="btn btn-outline-secondary" href="{{ $U('/userfields?entity=task_categories') }}">
href="{{ $U('/userfields?entity=task_categories') }}">
{{ $__t('Configure userfields') }} {{ $__t('Configure userfields') }}
</a> </a>
</div> </div>
@ -40,24 +34,18 @@
<hr class="my-2"> <hr class="my-2">
<div class="row collapse d-md-flex" <div class="row collapse d-md-flex" id="table-filter-row">
id="table-filter-row">
<div class="col-12 col-md-6 col-xl-3"> <div class="col-12 col-md-6 col-xl-3">
<div class="input-group"> <div class="input-group">
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-search"></i></span> <span class="input-group-text"><i class="fas fa-search"></i></span>
</div> </div>
<input type="text" <input type="text" id="search" class="form-control" placeholder="{{ $__t('Search') }}">
id="search"
class="form-control"
placeholder="{{ $__t('Search') }}">
</div> </div>
</div> </div>
<div class="col"> <div class="col">
<div class="float-right"> <div class="float-right">
<a id="clear-filter-button" <a id="clear-filter-button" class="btn btn-sm btn-outline-info" href="#">
class="btn btn-sm btn-outline-info"
href="#">
{{ $__t('Clear filter') }} {{ $__t('Clear filter') }}
</a> </a>
</div> </div>
@ -66,23 +54,20 @@
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<table id="taskcategories-table" {{-- TODO: DataTables: dynamic data: task_categories --}}
class="table table-sm table-striped nowrap w-100"> <table id="taskcategories-table" class="table table-sm table-striped nowrap w-100">
<thead> <thead>
<tr> <tr>
<th class="border-right"><a class="text-muted change-table-columns-visibility-button" <th class="border-right"><a class="text-muted change-table-columns-visibility-button"
data-toggle="tooltip" data-toggle="tooltip" data-toggle="tooltip" title="{{ $__t('Table options') }}"
data-toggle="tooltip" data-table-selector="#taskcategories-table" href="#"><i class="fas fa-eye"></i></a>
title="{{ $__t('Table options') }}"
data-table-selector="#taskcategories-table"
href="#"><i class="fas fa-eye"></i></a>
</th> </th>
<th>{{ $__t('Name') }}</th> <th>{{ $__t('Name') }}</th>
<th>{{ $__t('Description') }}</th> <th>{{ $__t('Description') }}</th>
@include('components.userfields_thead', array( @include('components.userfields_thead', [
'userfields' => $userfields 'userfields' => $userfields,
)) ])
</tr> </tr>
</thead> </thead>
@ -92,15 +77,12 @@
<td class="fit-content border-right"> <td class="fit-content border-right">
<a class="btn btn-info btn-sm show-as-dialog-link" <a class="btn btn-info btn-sm show-as-dialog-link"
href="{{ $U('/taskcategory/') }}{{ $taskCategory->id }}?embedded" href="{{ $U('/taskcategory/') }}{{ $taskCategory->id }}?embedded"
data-toggle="tooltip" data-toggle="tooltip" title="{{ $__t('Edit this item') }}">
title="{{ $__t('Edit this item') }}">
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
</a> </a>
<a class="btn btn-danger btn-sm task-category-delete-button" <a class="btn btn-danger btn-sm task-category-delete-button" href="#"
href="#"
data-category-id="{{ $taskCategory->id }}" data-category-id="{{ $taskCategory->id }}"
data-category-name="{{ $taskCategory->name }}" data-category-name="{{ $taskCategory->name }}" data-toggle="tooltip"
data-toggle="tooltip"
title="{{ $__t('Delete this item') }}"> title="{{ $__t('Delete this item') }}">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
</a> </a>
@ -112,10 +94,14 @@
{{ $taskCategory->description }} {{ $taskCategory->description }}
</td> </td>
@include('components.userfields_tbody', array( @include('components.userfields_tbody', [
'userfields' => $userfields, 'userfields' => $userfields,
'userfieldValues' => FindAllObjectsInArrayByPropertyValue($userfieldValues, 'object_id', $taskCategory->id) 'userfieldValues' => FindAllObjectsInArrayByPropertyValue(
)) $userfieldValues,
'object_id',
$taskCategory->id
),
])
</tr> </tr>
@endforeach @endforeach

View File

@ -29,36 +29,31 @@
</script> </script>
@endif @endif
<form id="task-form" <form id="task-form" novalidate>
novalidate>
<div class="form-group"> <div class="form-group">
<label for="name">{{ $__t('Name') }}</label> <label for="name">{{ $__t('Name') }}</label>
<input type="text" <input type="text" class="form-control" required id="name" name="name"
class="form-control"
required
id="name"
name="name"
value="@if ($mode == 'edit') {{ $task->name }} @endif"> value="@if ($mode == 'edit') {{ $task->name }} @endif">
<div class="invalid-feedback">{{ $__t('A name is required') }}</div> <div class="invalid-feedback">{{ $__t('A name is required') }}</div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="description">{{ $__t('Description') }}</label> <label for="description">{{ $__t('Description') }}</label>
<textarea class="form-control" <textarea class="form-control" rows="4" id="description" name="description">
rows="4" @if ($mode == 'edit')
id="description" {{ $task->description }}
name="description">@if($mode == 'edit'){{ $task->description }}@endif</textarea> @endif
</textarea>
</div> </div>
@php @php
$initialDueDate = null; $initialDueDate = null;
if ($mode == 'edit' && !empty($task->due_date)) if ($mode == 'edit' && !empty($task->due_date)) {
{
$initialDueDate = date('Y-m-d', strtotime($task->due_date)); $initialDueDate = date('Y-m-d', strtotime($task->due_date));
} }
@endphp @endphp
@include('components.datetimepicker', array( @include('components.datetimepicker', [
'id' => 'due_date', 'id' => 'due_date',
'label' => 'Due', 'label' => 'Due',
'format' => 'YYYY-MM-DD', 'format' => 'YYYY-MM-DD',
@ -69,46 +64,44 @@
'invalidFeedback' => $__t('A due date is required'), 'invalidFeedback' => $__t('A due date is required'),
'nextInputSelector' => 'category_id', 'nextInputSelector' => 'category_id',
'additionalGroupCssClasses' => 'date-only-datetimepicker', 'additionalGroupCssClasses' => 'date-only-datetimepicker',
'isRequired' => false 'isRequired' => false,
)) ])
<div class="form-group"> <div class="form-group">
<label for="category_id">{{ $__t('Category') }}</label> <label for="category_id">{{ $__t('Category') }}</label>
<select class="custom-control custom-select" {{-- TODO: Select2: dynamic data: task_categories --}}
id="category_id" <select class="custom-control custom-select" id="category_id" name="category_id">
name="category_id">
<option></option> <option></option>
@foreach ($taskCategories as $taskCategory) @foreach ($taskCategories as $taskCategory)
<option @if($mode=='edit' <option @if ($mode == 'edit' && $taskCategory->id == $task->category_id) selected="selected" @endif
&& value="{{ $taskCategory->id }}">{{ $taskCategory->name }}</option>
$taskCategory->id == $task->category_id) selected="selected" @endif value="{{ $taskCategory->id }}">{{ $taskCategory->name }}</option>
@endforeach @endforeach
</select> </select>
</div> </div>
@php @php
$initUserId = GROCY_USER_ID; $initUserId = GROCY_USER_ID;
if ($mode == 'edit') if ($mode == 'edit') {
{
$initUserId = $task->assigned_to_user_id; $initUserId = $task->assigned_to_user_id;
} }
@endphp @endphp
@include('components.userpicker', array( @include('components.userpicker', [
'label' => 'Assigned to', 'label' => 'Assigned to',
'users' => $users, 'users' => $users,
'prefillByUserId' => $initUserId 'prefillByUserId' => $initUserId,
)) ])
@include('components.userfieldsform', array( @include('components.userfieldsform', [
'userfields' => $userfields, 'userfields' => $userfields,
'entity' => 'tasks' 'entity' => 'tasks',
)) ])
@if ($mode == 'edit') @if ($mode == 'edit')
<button class="btn btn-success save-task-button">{{ $__t('Save') }}</button> <button class="btn btn-success save-task-button">{{ $__t('Save') }}</button>
@else @else
<button class="btn btn-success save-task-button">{{ $__t('Save & close') }}</button> <button class="btn btn-success save-task-button">{{ $__t('Save & close') }}</button>
<button class="btn btn-primary save-task-button add-another">{{ $__t('Save & add another task') }}</button> <button
class="btn btn-primary save-task-button add-another">{{ $__t('Save & add another task') }}</button>
@endif @endif
</form> </form>
</div> </div>

View File

@ -5,8 +5,7 @@
@section('viewJsName', 'tasks') @section('viewJsName', 'tasks')
@push('pageStyles') @push('pageStyles')
<link href="{{ $U('/node_modules/animate.css/animate.min.css?v=', true) }}{{ $version }}" <link href="{{ $U('/node_modules/animate.css/animate.min.css?v=', true) }}{{ $version }}" rel="stylesheet">
rel="stylesheet">
@endpush @endpush
@section('content') @section('content')
@ -14,41 +13,31 @@
<div class="col"> <div class="col">
<div class="title-related-links"> <div class="title-related-links">
<h2 class="title">@yield('title')</h2> <h2 class="title">@yield('title')</h2>
<button class="btn btn-outline-dark d-md-none mt-2 float-right order-1 order-md-3" <button class="btn btn-outline-dark d-md-none mt-2 float-right order-1 order-md-3" type="button"
type="button" data-toggle="collapse" data-target="#related-links">
data-toggle="collapse"
data-target="#related-links">
<i class="fas fa-ellipsis-v"></i> <i class="fas fa-ellipsis-v"></i>
</button> </button>
<div class="related-links collapse d-md-flex order-2 width-xs-sm-100 m-1 mt-md-0 mb-md-0 float-right" <div class="related-links collapse d-md-flex order-2 width-xs-sm-100 m-1 mt-md-0 mb-md-0 float-right"
id="related-links"> id="related-links">
<a class="btn btn-primary responsive-button show-as-dialog-link" <a class="btn btn-primary responsive-button show-as-dialog-link" href="{{ $U('/task/new?embedded') }}">
href="{{ $U('/task/new?embedded') }}">
{{ $__t('Add') }} {{ $__t('Add') }}
</a> </a>
</div> </div>
</div> </div>
<div class="border-top border-bottom my-2 py-1"> <div class="border-top border-bottom my-2 py-1">
<div id="info-overdue-tasks" <div id="info-overdue-tasks" data-status-filter="overdue"
data-status-filter="overdue"
class="error-message status-filter-message responsive-button mr-2"></div> class="error-message status-filter-message responsive-button mr-2"></div>
<div id="info-due-today-tasks" <div id="info-due-today-tasks" data-status-filter="duetoday"
data-status-filter="duetoday"
class="normal-message status-filter-message responsive-button mr-2"></div> class="normal-message status-filter-message responsive-button mr-2"></div>
<div id="info-due-soon-tasks" <div id="info-due-soon-tasks" data-status-filter="duesoon" data-next-x-days="{{ $nextXDays }}"
data-status-filter="duesoon" class="warning-message status-filter-message responsive-button @if ($nextXDays == 0) d-none @endif">
data-next-x-days="{{ $nextXDays }}" </div>
class="warning-message status-filter-message responsive-button @if($nextXDays == 0) d-none @endif"></div>
<div class="float-right"> <div class="float-right">
<a class="btn btn-sm btn-outline-info d-md-none mt-1" <a class="btn btn-sm btn-outline-info d-md-none mt-1" data-toggle="collapse" href="#table-filter-row"
data-toggle="collapse"
href="#table-filter-row"
role="button"> role="button">
<i class="fas fa-filter"></i> <i class="fas fa-filter"></i>
</a> </a>
<a id="clear-filter-button" <a id="clear-filter-button" class="btn btn-sm btn-outline-info mt-1" href="#">
class="btn btn-sm btn-outline-info mt-1"
href="#">
{{ $__t('Clear filter') }} {{ $__t('Clear filter') }}
</a> </a>
</div> </div>
@ -56,17 +45,13 @@
</div> </div>
</div> </div>
<div class="row collapse d-md-flex" <div class="row collapse d-md-flex" id="table-filter-row">
id="table-filter-row">
<div class="col-12 col-md-6 col-xl-3"> <div class="col-12 col-md-6 col-xl-3">
<div class="input-group"> <div class="input-group">
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-search"></i></span> <span class="input-group-text"><i class="fas fa-search"></i></span>
</div> </div>
<input type="text" <input type="text" id="search" class="form-control" placeholder="{{ $__t('Search') }}">
id="search"
class="form-control"
placeholder="{{ $__t('Search') }}">
</div> </div>
</div> </div>
<div class="col-12 col-md-6 col-xl-3"> <div class="col-12 col-md-6 col-xl-3">
@ -74,8 +59,7 @@
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-filter"></i>&nbsp;{{ $__t('Status') }}</span> <span class="input-group-text"><i class="fas fa-filter"></i>&nbsp;{{ $__t('Status') }}</span>
</div> </div>
<select class="custom-control custom-select" <select class="custom-control custom-select" id="status-filter">
id="status-filter">
<option value="all">{{ $__t('All') }}</option> <option value="all">{{ $__t('All') }}</option>
<option value="overdue">{{ $__t('Overdue') }}</option> <option value="overdue">{{ $__t('Overdue') }}</option>
<option value="duetoday">{{ $__t('Due today') }}</option> <option value="duetoday">{{ $__t('Due today') }}</option>
@ -87,11 +71,8 @@
</div> </div>
<div class="col-12 col-md-6 col-xl-3"> <div class="col-12 col-md-6 col-xl-3">
<div class="form-check custom-control custom-checkbox"> <div class="form-check custom-control custom-checkbox">
<input class="form-check-input custom-control-input" <input class="form-check-input custom-control-input" type="checkbox" id="show-done-tasks">
type="checkbox" <label class="form-check-label custom-control-label" for="show-done-tasks">
id="show-done-tasks">
<label class="form-check-label custom-control-label"
for="show-done-tasks">
{{ $__t('Show done tasks') }} {{ $__t('Show done tasks') }}
</label> </label>
</div> </div>
@ -100,28 +81,24 @@
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<table id="tasks-table" {{-- TODO: DataTables: dynamic data: tasks --}}
class="table table-sm table-striped nowrap w-100"> <table id="tasks-table" class="table table-sm table-striped nowrap w-100">
<thead> <thead>
<tr> <tr>
<th class="border-right"><a class="text-muted change-table-columns-visibility-button" <th class="border-right"><a class="text-muted change-table-columns-visibility-button"
data-toggle="tooltip" data-toggle="tooltip" data-toggle="tooltip" title="{{ $__t('Table options') }}"
data-toggle="tooltip" data-table-selector="#tasks-table" href="#"><i class="fas fa-eye"></i></a>
title="{{ $__t('Table options') }}"
data-table-selector="#tasks-table"
href="#"><i class="fas fa-eye"></i></a>
</th> </th>
<th>{{ $__t('Task') }}</th> <th>{{ $__t('Task') }}</th>
<th class="allow-grouping">{{ $__t('Due') }}</th> <th class="allow-grouping">{{ $__t('Due') }}</th>
<th class="allow-grouping" <th class="allow-grouping" data-shadow-rowgroup-column="6">{{ $__t('Category') }}</th>
data-shadow-rowgroup-column="6">{{ $__t('Category') }}</th>
<th class="allow-grouping">{{ $__t('Assigned to') }}</th> <th class="allow-grouping">{{ $__t('Assigned to') }}</th>
<th class="d-none">Hidden status</th> <th class="d-none">Hidden status</th>
<th class="d-none">Hidden category_id</th> <th class="d-none">Hidden category_id</th>
@include('components.userfields_thead', array( @include('components.userfields_thead', [
'userfields' => $userfields 'userfields' => $userfields,
)) ])
</tr> </tr>
</thead> </thead>
@ -131,38 +108,26 @@
class="@if ($task->due_type == 'overdue') table-danger @elseif($task->due_type == 'duetoday') table-info @elseif($task->due_type == 'duesoon') table-warning @endif"> class="@if ($task->due_type == 'overdue') table-danger @elseif($task->due_type == 'duetoday') table-info @elseif($task->due_type == 'duesoon') table-warning @endif">
<td class="fit-content border-right"> <td class="fit-content border-right">
@if ($task->done == 0) @if ($task->done == 0)
<a class="btn btn-success btn-sm do-task-button" <a class="btn btn-success btn-sm do-task-button" href="#" data-toggle="tooltip"
href="#" data-placement="left" title="{{ $__t('Mark task as completed') }}"
data-toggle="tooltip" data-task-id="{{ $task->id }}" data-task-name="{{ $task->name }}">
data-placement="left"
title="{{ $__t('Mark task as completed') }}"
data-task-id="{{ $task->id }}"
data-task-name="{{ $task->name }}">
<i class="fas fa-check"></i> <i class="fas fa-check"></i>
</a> </a>
@else @else
<a class="btn btn-secondary btn-sm undo-task-button" <a class="btn btn-secondary btn-sm undo-task-button" href="#" data-toggle="tooltip"
href="#" data-placement="left" title="{{ $__t('Undo task', $task->name) }}"
data-toggle="tooltip" data-task-id="{{ $task->id }}" data-task-name="{{ $task->name }}">
data-placement="left"
title="{{ $__t('Undo task', $task->name) }}"
data-task-id="{{ $task->id }}"
data-task-name="{{ $task->name }}">
<i class="fas fa-undo"></i> <i class="fas fa-undo"></i>
</a> </a>
@endif @endif
<a class="btn btn-info btn-sm show-as-dialog-link" <a class="btn btn-info btn-sm show-as-dialog-link"
href="{{ $U('/task/') }}{{ $task->id }}?embedded" href="{{ $U('/task/') }}{{ $task->id }}?embedded" data-toggle="tooltip"
data-toggle="tooltip"
title="{{ $__t('Edit this item') }}"> title="{{ $__t('Edit this item') }}">
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
</a> </a>
<a class="btn btn-sm btn-danger delete-task-button" <a class="btn btn-sm btn-danger delete-task-button" href="#"
href="#" data-task-id="{{ $task->id }}" data-task-name="{{ $task->name }}"
data-task-id="{{ $task->id }}" data-toggle="tooltip" title="{{ $__t('Delete this item') }}">
data-task-name="{{ $task->name }}"
data-toggle="tooltip"
title="{{ $__t('Delete this item') }}">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
</a> </a>
</td> </td>
@ -172,14 +137,19 @@
</td> </td>
<td> <td>
<span>{{ $task->due_date }}</span> <span>{{ $task->due_date }}</span>
<time class="timeago timeago-contextual" <time class="timeago timeago-contextual" datetime="{{ $task->due_date }}"></time>
datetime="{{ $task->due_date }}"></time>
</td> </td>
<td> <td>
@if($task->category_id != null) <span>{{ FindObjectInArrayByPropertyValue($taskCategories, 'id', $task->category_id)->name }}</span> @else <span class="font-italic font-weight-light">{{ $__t('Uncategorized') }}</span>@endif @if ($task->category_id != null)
<span>{{ FindObjectInArrayByPropertyValue($taskCategories, 'id', $task->category_id)->name }}</span>
@else
<span class="font-italic font-weight-light">{{ $__t('Uncategorized') }}</span>
@endif
</td> </td>
<td> <td>
@if($task->assigned_to_user_id != null) <span>{{ GetUserDisplayName(FindObjectInArrayByPropertyValue($users, 'id', $task->assigned_to_user_id)) }}</span> @endif @if ($task->assigned_to_user_id != null)
<span>{{ GetUserDisplayName(FindObjectInArrayByPropertyValue($users, 'id', $task->assigned_to_user_id)) }}</span>
@endif
</td> </td>
<td class="d-none"> <td class="d-none">
{{ $task->due_type }} {{ $task->due_type }}
@ -188,12 +158,20 @@
@endif @endif
</td> </td>
<td class="d-none"> <td class="d-none">
@if($task->category_id != null) {{ FindObjectInArrayByPropertyValue($taskCategories, 'id', $task->category_id)->name }} @else {{ $__t('Uncategorized') }} @endif @if ($task->category_id != null)
{{ FindObjectInArrayByPropertyValue($taskCategories, 'id', $task->category_id)->name }}
@else
{{ $__t('Uncategorized') }}
@endif
</td> </td>
@include('components.userfields_tbody', @include('components.userfields_tbody', [
array( 'userfields'=> $userfields, 'userfields' => $userfields,
'userfieldValues' => FindAllObjectsInArrayByPropertyValue($userfieldValues, 'object_id', $task->id) 'userfieldValues' => FindAllObjectsInArrayByPropertyValue(
)) $userfieldValues,
'object_id',
$task->id
),
])
</tr> </tr>
@endforeach @endforeach

View File

@ -16,47 +16,48 @@
<hr class="my-2"> <hr class="my-2">
<form id="transfer-form" <form id="transfer-form" novalidate>
novalidate>
@include('components.productpicker', array( @include('components.productpicker', [
'products' => $products, 'productsQuery' => 'query%5B%5D=active%3D1&only_in_stock=1&order=name%3Acollate%20nocase',
'barcodes' => $barcodes,
'nextInputSelector' => '#location_id_from', 'nextInputSelector' => '#location_id_from',
'disallowAddProductWorkflows' => true 'disallowAddProductWorkflows' => true,
)) ])
<div class="form-group"> <div class="form-group">
<label for="location_id_from">{{ $__t('From location') }}</label> <label for="location_id_from">{{ $__t('From location') }}</label>
<select required {{-- TODO: Select2: dynamic data: locations --}}
class="custom-control custom-select location-combobox" <select required class="custom-control custom-select location-combobox" id="location_id_from"
id="location_id_from"
name="location_id_from"> name="location_id_from">
<option></option> <option></option>
@foreach ($locations as $location) @foreach ($locations as $location)
<option value="{{ $location->id }}" <option value="{{ $location->id }}" data-is-freezer="{{ $location->is_freezer }}">
data-is-freezer="{{ $location->is_freezer }}">{{ $location->name }}</option> {{ $location->name }}</option>
@endforeach @endforeach
</select> </select>
<div class="invalid-feedback">{{ $__t('A location is required') }}</div> <div class="invalid-feedback">{{ $__t('A location is required') }}</div>
</div> </div>
@include('components.productamountpicker', array( @include('components.productamountpicker', [
'value' => 1, 'value' => 1,
'additionalHtmlContextHelp' => '<div id="tare-weight-handling-info" 'additionalHtmlContextHelp' =>
class="text-info font-italic d-none">' . $__t('Tare weight handling enabled - please weigh the whole container, the amount to be posted will be automatically calculcated') . '</div>' '<div id="tare-weight-handling-info"
)) class="text-info font-italic d-none">' .
$__t(
'Tare weight handling enabled - please weigh the whole container, the amount to be posted will be automatically calculcated'
) .
'</div>',
])
<div class="form-group"> <div class="form-group">
<label for="location_id_to">{{ $__t('To location') }}</label> <label for="location_id_to">{{ $__t('To location') }}</label>
<select required {{-- TODO: Select2: dynamic data: locations --}}
class="custom-control custom-select location-combobox" <select required class="custom-control custom-select location-combobox" id="location_id_to"
id="location_id_to"
name="location_id_to"> name="location_id_to">
<option></option> <option></option>
@foreach ($locations as $location) @foreach ($locations as $location)
<option value="{{ $location->id }}" <option value="{{ $location->id }}" data-is-freezer="{{ $location->is_freezer }}">
data-is-freezer="{{ $location->is_freezer }}">{{ $location->name }}</option> {{ $location->name }}</option>
@endforeach @endforeach
</select> </select>
<div class="invalid-feedback">{{ $__t('A location is required') }}</div> <div class="invalid-feedback">{{ $__t('A location is required') }}</div>
@ -64,29 +65,22 @@
<div class="form-group"> <div class="form-group">
<div class="custom-control custom-checkbox"> <div class="custom-control custom-checkbox">
<input class="form-check-input custom-control-input" <input class="form-check-input custom-control-input" type="checkbox" id="use_specific_stock_entry"
type="checkbox" name="use_specific_stock_entry" value="1">
id="use_specific_stock_entry"
name="use_specific_stock_entry"
value="1">
<label class="form-check-label custom-control-label" <label class="form-check-label custom-control-label"
for="use_specific_stock_entry">{{ $__t('Use a specific stock item') }} for="use_specific_stock_entry">{{ $__t('Use a specific stock item') }}
&nbsp;<i class="fas fa-question-circle text-muted" &nbsp;<i class="fas fa-question-circle text-muted" data-toggle="tooltip"
data-toggle="tooltip"
data-trigger="hover click" data-trigger="hover click"
title="{{ $__t('The first item in this list would be picked by the default rule which is "Opened first, then first due first, then first in first out"') }}"></i> title="{{ $__t('The first item in this list would be picked by the default rule which is "Opened first, then first due first, then first in first out"') }}"></i>
</label> </label>
</div> </div>
<select disabled <select disabled class="custom-control custom-select mt-2" id="specific_stock_entry"
class="custom-control custom-select mt-2"
id="specific_stock_entry"
name="specific_stock_entry"> name="specific_stock_entry">
<option></option> <option></option>
</select> </select>
</div> </div>
<button id="save-transfer-button" <button id="save-transfer-button" class="btn btn-success">{{ $__t('OK') }}</button>
class="btn btn-success">{{ $__t('OK') }}</button>
</form> </form>
</div> </div>

View File

@ -10,16 +10,12 @@
<div class="title-related-links"> <div class="title-related-links">
<h2 class="title">@yield('title')</h2> <h2 class="title">@yield('title')</h2>
<div class="float-right"> <div class="float-right">
<button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" <button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" type="button"
type="button" data-toggle="collapse" data-target="#table-filter-row">
data-toggle="collapse"
data-target="#table-filter-row">
<i class="fas fa-filter"></i> <i class="fas fa-filter"></i>
</button> </button>
<button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" <button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" type="button"
type="button" data-toggle="collapse" data-target="#related-links">
data-toggle="collapse"
data-target="#related-links">
<i class="fas fa-ellipsis-v"></i> <i class="fas fa-ellipsis-v"></i>
</button> </button>
</div> </div>
@ -36,24 +32,18 @@
<hr class="my-2"> <hr class="my-2">
<div class="row collapse d-md-flex" <div class="row collapse d-md-flex" id="table-filter-row">
id="table-filter-row">
<div class="col-12 col-md-6 col-xl-3"> <div class="col-12 col-md-6 col-xl-3">
<div class="input-group"> <div class="input-group">
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-search"></i></span> <span class="input-group-text"><i class="fas fa-search"></i></span>
</div> </div>
<input type="text" <input type="text" id="search" class="form-control" placeholder="{{ $__t('Search') }}">
id="search"
class="form-control"
placeholder="{{ $__t('Search') }}">
</div> </div>
</div> </div>
<div class="col"> <div class="col">
<div class="float-right"> <div class="float-right">
<a id="clear-filter-button" <a id="clear-filter-button" class="btn btn-sm btn-outline-info" href="#">
class="btn btn-sm btn-outline-info"
href="#">
{{ $__t('Clear filter') }} {{ $__t('Clear filter') }}
</a> </a>
</div> </div>
@ -62,16 +52,13 @@
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<table id="userentities-table" {{-- TODO: DataTables: dynamic data: userentities --}}
class="table table-sm table-striped nowrap w-100"> <table id="userentities-table" class="table table-sm table-striped nowrap w-100">
<thead> <thead>
<tr> <tr>
<th class="border-right"><a class="text-muted change-table-columns-visibility-button" <th class="border-right"><a class="text-muted change-table-columns-visibility-button"
data-toggle="tooltip" data-toggle="tooltip" data-toggle="tooltip" title="{{ $__t('Table options') }}"
data-toggle="tooltip" data-table-selector="#userentities-table" href="#"><i class="fas fa-eye"></i></a>
title="{{ $__t('Table options') }}"
data-table-selector="#userentities-table"
href="#"><i class="fas fa-eye"></i></a>
</th> </th>
<th>{{ $__t('Name') }}</th> <th>{{ $__t('Name') }}</th>
<th>{{ $__t('Caption') }}</th> <th>{{ $__t('Caption') }}</th>
@ -82,16 +69,13 @@
<tr> <tr>
<td class="fit-content border-right"> <td class="fit-content border-right">
<a class="btn btn-info btn-sm show-as-dialog-link" <a class="btn btn-info btn-sm show-as-dialog-link"
href="{{ $U('/userentity/') }}{{ $userentity->id }}?embedded" href="{{ $U('/userentity/') }}{{ $userentity->id }}?embedded" data-toggle="tooltip"
data-toggle="tooltip"
title="{{ $__t('Edit this item') }}"> title="{{ $__t('Edit this item') }}">
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
</a> </a>
<a class="btn btn-danger btn-sm userentity-delete-button" <a class="btn btn-danger btn-sm userentity-delete-button" href="#"
href="#"
data-userentity-id="{{ $userentity->id }}" data-userentity-id="{{ $userentity->id }}"
data-userentity-name="{{ $userentity->name }}" data-userentity-name="{{ $userentity->name }}" data-toggle="tooltip"
data-toggle="tooltip"
title="{{ $__t('Delete this item') }}"> title="{{ $__t('Delete this item') }}">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
</a> </a>

View File

@ -29,20 +29,16 @@
</script> </script>
@endif @endif
<form id="userfield-form" <form id="userfield-form" novalidate>
novalidate>
<div class="form-group"> <div class="form-group">
<label for="entity">{{ $__t('Entity') }}</label> <label for="entity">{{ $__t('Entity') }}</label>
<select required {{-- TODO: Select2: dynamic data: userentities --}}
class="custom-control custom-select" <select required class="custom-control custom-select" id="entity" name="entity">
id="entity"
name="entity">
<option></option> <option></option>
@foreach ($entities as $entity) @foreach ($entities as $entity)
<option @if($mode=='edit' <option @if ($mode == 'edit' && $userfield->entity == $entity) selected="selected" @endif
&& value="{{ $entity }}">{{ $entity }}</option>
$userfield->entity == $entity) selected="selected" @endif value="{{ $entity }}">{{ $entity }}</option>
@endforeach @endforeach
</select> </select>
<div class="invalid-feedback">{{ $__t('A entity is required') }}</div> <div class="invalid-feedback">{{ $__t('A entity is required') }}</div>
@ -51,59 +47,50 @@
<div class="form-group"> <div class="form-group">
<label for="name"> <label for="name">
{{ $__t('Name') }} {{ $__t('Name') }}
<i class="fas fa-question-circle text-muted" <i class="fas fa-question-circle text-muted" data-toggle="tooltip" data-trigger="hover click"
data-toggle="tooltip"
data-trigger="hover click"
title="{{ $__t('This is the internal field name, e. g. for the API') }}"></i> title="{{ $__t('This is the internal field name, e. g. for the API') }}"></i>
</label> </label>
<input type="text" <input type="text" class="form-control" required pattern="^[a-zA-Z0-9]*$" id="name" name="name"
class="form-control"
required
pattern="^[a-zA-Z0-9]*$"
id="name"
name="name"
value="@if ($mode == 'edit') {{ $userfield->name }} @endif"> value="@if ($mode == 'edit') {{ $userfield->name }} @endif">
<div class="invalid-feedback">{{ $__t('This is required and can only contain letters and numbers') }}</div> <div class="invalid-feedback">{{ $__t('This is required and can only contain letters and numbers') }}
</div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="name"> <label for="name">
{{ $__t('Caption') }} {{ $__t('Caption') }}
<i class="fas fa-question-circle text-muted" <i class="fas fa-question-circle text-muted" data-toggle="tooltip" data-trigger="hover click"
data-toggle="tooltip"
data-trigger="hover click"
title="{{ $__t('This is used to display the field on the frontend') }}"></i> title="{{ $__t('This is used to display the field on the frontend') }}"></i>
</label> </label>
<input type="text" <input type="text" class="form-control" required id="caption" name="caption"
class="form-control"
required
id="caption"
name="caption"
value="@if ($mode == 'edit') {{ $userfield->caption }} @endif"> value="@if ($mode == 'edit') {{ $userfield->caption }} @endif">
<div class="invalid-feedback">{{ $__t('A caption is required') }}</div> <div class="invalid-feedback">{{ $__t('A caption is required') }}</div>
</div> </div>
@php if($mode == 'edit' && !empty($userfield->sort_number)) { $value = $userfield->sort_number; } else { $value = ''; } @endphp @php
@include('components.numberpicker', array( if ($mode == 'edit' && !empty($userfield->sort_number)) {
$value = $userfield->sort_number;
} else {
$value = '';
}
@endphp
@include('components.numberpicker', [
'id' => 'sort_number', 'id' => 'sort_number',
'label' => 'Sort number', 'label' => 'Sort number',
'min' => 0, 'min' => 0,
'value' => $value, 'value' => $value,
'isRequired' => false, 'isRequired' => false,
'hint' => $__t('Multiple Userfields will be ordered by that number on the input form') 'hint' => $__t('Multiple Userfields will be ordered by that number on the input form'),
)) ])
<div class="form-group"> <div class="form-group">
<label for="type">{{ $__t('Type') }}</label> <label for="type">{{ $__t('Type') }}</label>
<select required {{-- TODO: Select2: static data --}}
class="custom-control custom-select" <select required class="custom-control custom-select" id="type" name="type">
id="type"
name="type">
<option></option> <option></option>
@foreach ($userfieldTypes as $userfieldType) @foreach ($userfieldTypes as $userfieldType)
<option @if($mode=='edit' <option @if ($mode == 'edit' && $userfield->type == $userfieldType) selected="selected" @endif
&& value="{{ $userfieldType }}">{{ $__t($userfieldType) }}</option>
$userfield->type == $userfieldType) selected="selected" @endif value="{{ $userfieldType }}">{{ $__t($userfieldType) }}</option>
@endforeach @endforeach
</select> </select>
<div class="invalid-feedback">{{ $__t('A type is required') }}</div> <div class="invalid-feedback">{{ $__t('A type is required') }}</div>
@ -112,17 +99,18 @@
<div class="form-group d-none"> <div class="form-group d-none">
<label for="config">{{ $__t('Configuration') }} <span id="config-hint" <label for="config">{{ $__t('Configuration') }} <span id="config-hint"
class="small text-muted"></span></label> class="small text-muted"></span></label>
<textarea class="form-control" <textarea class="form-control" rows="10" id="config" name="config">
rows="10" @if ($mode == 'edit')
id="config" {{ $userfield->config }}
name="config">@if($mode == 'edit'){{ $userfield->config }}@endif</textarea> @endif
</textarea>
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="custom-control custom-checkbox"> <div class="custom-control custom-checkbox">
<input @if($mode=='edit' <input @if ($mode == 'edit' && $userfield->show_as_column_in_tables == 1) checked @endif
&& class="form-check-input custom-control-input" type="checkbox" id="show_as_column_in_tables"
$userfield->show_as_column_in_tables == 1) checked @endif class="form-check-input custom-control-input" type="checkbox" id="show_as_column_in_tables" name="show_as_column_in_tables" value="1"> name="show_as_column_in_tables" value="1">
<label class="form-check-label custom-control-label" <label class="form-check-label custom-control-label"
for="show_as_column_in_tables">{{ $__t('Show as column in tables') }}</label> for="show_as_column_in_tables">{{ $__t('Show as column in tables') }}</label>
</div> </div>
@ -130,22 +118,19 @@
<div class="form-group"> <div class="form-group">
<div class="custom-control custom-checkbox"> <div class="custom-control custom-checkbox">
<input @if($mode=='edit' <input @if ($mode == 'edit' && $userfield->input_required == 1) checked @endif
&& class="form-check-input custom-control-input" type="checkbox" id="input_required"
$userfield->input_required == 1) checked @endif class="form-check-input custom-control-input" type="checkbox" id="input_required" name="input_required" value="1"> name="input_required" value="1">
<label class="form-check-label custom-control-label" <label class="form-check-label custom-control-label" for="input_required">
for="input_required">
{{ $__t('Mandatory') }} {{ $__t('Mandatory') }}
&nbsp;<i class="fas fa-question-circle text-muted" &nbsp;<i class="fas fa-question-circle text-muted" data-toggle="tooltip"
data-toggle="tooltip"
data-trigger="hover click" data-trigger="hover click"
title="{{ $__t('When enabled, then this field must be filled on the destination form') }}"></i> title="{{ $__t('When enabled, then this field must be filled on the destination form') }}"></i>
</label> </label>
</div> </div>
</div> </div>
<button id="save-userfield-button" <button id="save-userfield-button" class="btn btn-success">{{ $__t('Save') }}</button>
class="btn btn-success">{{ $__t('Save') }}</button>
</form> </form>
</div> </div>

View File

@ -10,23 +10,18 @@
<div class="title-related-links"> <div class="title-related-links">
<h2 class="title">@yield('title')</h2> <h2 class="title">@yield('title')</h2>
<div class="float-right"> <div class="float-right">
<button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" <button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" type="button"
type="button" data-toggle="collapse" data-target="#table-filter-row">
data-toggle="collapse"
data-target="#table-filter-row">
<i class="fas fa-filter"></i> <i class="fas fa-filter"></i>
</button> </button>
<button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" <button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" type="button"
type="button" data-toggle="collapse" data-target="#related-links">
data-toggle="collapse"
data-target="#related-links">
<i class="fas fa-ellipsis-v"></i> <i class="fas fa-ellipsis-v"></i>
</button> </button>
</div> </div>
<div class="related-links collapse d-md-flex order-2 width-xs-sm-100 m-1 mt-md-0 mb-md-0 float-right" <div class="related-links collapse d-md-flex order-2 width-xs-sm-100 m-1 mt-md-0 mb-md-0 float-right"
id="related-links"> id="related-links">
<a id="new-userfield-button" <a id="new-userfield-button" class="btn btn-primary responsive-button show-as-dialog-link"
class="btn btn-primary responsive-button show-as-dialog-link"
href="{{ $U('/userfield/new?embedded') }}"> href="{{ $U('/userfield/new?embedded') }}">
{{ $__t('Add') }} {{ $__t('Add') }}
</a> </a>
@ -37,17 +32,13 @@
<hr class="my-2"> <hr class="my-2">
<div class="row collapse d-md-flex" <div class="row collapse d-md-flex" id="table-filter-row">
id="table-filter-row">
<div class="col-12 col-md-6 col-xl-3"> <div class="col-12 col-md-6 col-xl-3">
<div class="input-group"> <div class="input-group">
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-search"></i></span> <span class="input-group-text"><i class="fas fa-search"></i></span>
</div> </div>
<input type="text" <input type="text" id="search" class="form-control" placeholder="{{ $__t('Search') }}">
id="search"
class="form-control"
placeholder="{{ $__t('Search') }}">
</div> </div>
</div> </div>
<div class="col-12 col-md-6 col-xl-3"> <div class="col-12 col-md-6 col-xl-3">
@ -55,8 +46,8 @@
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-filter"></i>&nbsp;{{ $__t('Entity') }}</span> <span class="input-group-text"><i class="fas fa-filter"></i>&nbsp;{{ $__t('Entity') }}</span>
</div> </div>
<select class="custom-control custom-select" {{-- TODO: Select2: dynamic data: userfields --}}
id="entity-filter"> <select class="custom-control custom-select" id="entity-filter">
<option value="all">{{ $__t('All') }}</option> <option value="all">{{ $__t('All') }}</option>
@foreach ($entities as $entity) @foreach ($entities as $entity)
<option value="{{ $entity }}">{{ $entity }}</option> <option value="{{ $entity }}">{{ $entity }}</option>
@ -66,9 +57,7 @@
</div> </div>
<div class="col"> <div class="col">
<div class="float-right"> <div class="float-right">
<a id="clear-filter-button" <a id="clear-filter-button" class="btn btn-sm btn-outline-info" href="#">
class="btn btn-sm btn-outline-info"
href="#">
{{ $__t('Clear filter') }} {{ $__t('Clear filter') }}
</a> </a>
</div> </div>
@ -77,16 +66,13 @@
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<table id="userfields-table" {{-- TODO: DataTables: dynamic data: userfields --}}
class="table table-sm table-striped nowrap w-100"> <table id="userfields-table" class="table table-sm table-striped nowrap w-100">
<thead> <thead>
<tr> <tr>
<th class="border-right"><a class="text-muted change-table-columns-visibility-button" <th class="border-right"><a class="text-muted change-table-columns-visibility-button"
data-toggle="tooltip" data-toggle="tooltip" data-toggle="tooltip" title="{{ $__t('Table options') }}"
data-toggle="tooltip" data-table-selector="#userfields-table" href="#"><i class="fas fa-eye"></i></a>
title="{{ $__t('Table options') }}"
data-table-selector="#userfields-table"
href="#"><i class="fas fa-eye"></i></a>
</th> </th>
<th class="allow-grouping">{{ $__t('Entity') }}</th> <th class="allow-grouping">{{ $__t('Entity') }}</th>
<th>{{ $__t('Name') }}</th> <th>{{ $__t('Name') }}</th>
@ -100,16 +86,13 @@
<tr> <tr>
<td class="fit-content border-right"> <td class="fit-content border-right">
<a class="btn btn-info btn-sm show-as-dialog-link" <a class="btn btn-info btn-sm show-as-dialog-link"
href="{{ $U('/userfield/') }}{{ $userfield->id }}?embedded" href="{{ $U('/userfield/') }}{{ $userfield->id }}?embedded" data-toggle="tooltip"
data-toggle="tooltip"
title="{{ $__t('Edit this item') }}"> title="{{ $__t('Edit this item') }}">
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
</a> </a>
<a class="btn btn-danger btn-sm userfield-delete-button" <a class="btn btn-danger btn-sm userfield-delete-button" href="#"
href="#"
data-userfield-id="{{ $userfield->id }}" data-userfield-id="{{ $userfield->id }}"
data-userfield-name="{{ $userfield->name }}" data-userfield-name="{{ $userfield->name }}" data-toggle="tooltip"
data-toggle="tooltip"
title="{{ $__t('Delete this item') }}"> title="{{ $__t('Delete this item') }}">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
</a> </a>

View File

@ -15,16 +15,12 @@
<span class="text-muted small">{{ $userentity->description }}</span> <span class="text-muted small">{{ $userentity->description }}</span>
</h2> </h2>
<div class="float-right"> <div class="float-right">
<button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" <button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" type="button"
type="button" data-toggle="collapse" data-target="#table-filter-row">
data-toggle="collapse"
data-target="#table-filter-row">
<i class="fas fa-filter"></i> <i class="fas fa-filter"></i>
</button> </button>
<button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" <button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" type="button"
type="button" data-toggle="collapse" data-target="#related-links">
data-toggle="collapse"
data-target="#related-links">
<i class="fas fa-ellipsis-v"></i> <i class="fas fa-ellipsis-v"></i>
</button> </button>
</div> </div>
@ -45,24 +41,18 @@
<hr class="my-2"> <hr class="my-2">
<div class="row collapse d-md-flex" <div class="row collapse d-md-flex" id="table-filter-row">
id="table-filter-row">
<div class="col-12 col-md-6 col-xl-3"> <div class="col-12 col-md-6 col-xl-3">
<div class="input-group"> <div class="input-group">
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-search"></i></span> <span class="input-group-text"><i class="fas fa-search"></i></span>
</div> </div>
<input type="text" <input type="text" id="search" class="form-control" placeholder="{{ $__t('Search') }}">
id="search"
class="form-control"
placeholder="{{ $__t('Search') }}">
</div> </div>
</div> </div>
<div class="col"> <div class="col">
<div class="float-right"> <div class="float-right">
<a id="clear-filter-button" <a id="clear-filter-button" class="btn btn-sm btn-outline-info" href="#">
class="btn btn-sm btn-outline-info"
href="#">
{{ $__t('Clear filter') }} {{ $__t('Clear filter') }}
</a> </a>
</div> </div>
@ -71,15 +61,15 @@
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<table id="userobjects-table" {{-- TODO: DataTables: dynamic data: userobjects --}}
class="table table-sm table-striped nowrap w-100"> <table id="userobjects-table" class="table table-sm table-striped nowrap w-100">
<thead> <thead>
<tr> <tr>
<th class="border-right d-print-none"></th> <th class="border-right d-print-none"></th>
@include('components.userfields_thead', array( @include('components.userfields_thead', [
'userfields' => $userfields 'userfields' => $userfields,
)) ])
</tr> </tr>
</thead> </thead>
@ -89,23 +79,24 @@
<td class="fit-content border-right d-print-none"> <td class="fit-content border-right d-print-none">
<a class="btn btn-info btn-sm show-as-dialog-link" <a class="btn btn-info btn-sm show-as-dialog-link"
href="{{ $U('/userobject/' . $userentity->name . '/') }}{{ $userobject->id }}?embedded" href="{{ $U('/userobject/' . $userentity->name . '/') }}{{ $userobject->id }}?embedded"
data-toggle="tooltip" data-toggle="tooltip" title="{{ $__t('Edit this item') }}">
title="{{ $__t('Edit this item') }}">
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
</a> </a>
<a class="btn btn-danger btn-sm userobject-delete-button" <a class="btn btn-danger btn-sm userobject-delete-button" href="#"
href="#" data-userobject-id="{{ $userobject->id }}" data-toggle="tooltip"
data-userobject-id="{{ $userobject->id }}"
data-toggle="tooltip"
title="{{ $__t('Delete this item') }}"> title="{{ $__t('Delete this item') }}">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
</a> </a>
</td> </td>
@include('components.userfields_tbody', array( @include('components.userfields_tbody', [
'userfields' => $userfields, 'userfields' => $userfields,
'userfieldValues' => FindAllObjectsInArrayByPropertyValue($userfieldValues, 'object_id', $userobject->id) 'userfieldValues' => FindAllObjectsInArrayByPropertyValue(
)) $userfieldValues,
'object_id',
$userobject->id
),
])
</tr> </tr>
@endforeach @endforeach

View File

@ -10,23 +10,18 @@
<div class="title-related-links"> <div class="title-related-links">
<h2 class="title">@yield('title')</h2> <h2 class="title">@yield('title')</h2>
<div class="float-right"> <div class="float-right">
<button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" <button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" type="button"
type="button" data-toggle="collapse" data-target="#table-filter-row">
data-toggle="collapse"
data-target="#table-filter-row">
<i class="fas fa-filter"></i> <i class="fas fa-filter"></i>
</button> </button>
<button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" <button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" type="button"
type="button" data-toggle="collapse" data-target="#related-links">
data-toggle="collapse"
data-target="#related-links">
<i class="fas fa-ellipsis-v"></i> <i class="fas fa-ellipsis-v"></i>
</button> </button>
</div> </div>
<div class="related-links collapse d-md-flex order-2 width-xs-sm-100 m-1 mt-md-0 mb-md-0 float-right" <div class="related-links collapse d-md-flex order-2 width-xs-sm-100 m-1 mt-md-0 mb-md-0 float-right"
id="related-links"> id="related-links">
<a class="btn btn-primary responsive-button" <a class="btn btn-primary responsive-button" href="{{ $U('/user/new') }}">
href="{{ $U('/user/new') }}">
{{ $__t('Add') }} {{ $__t('Add') }}
</a> </a>
<a class="btn btn-outline-secondary m-1 mt-md-0 mb-md-0 float-right" <a class="btn btn-outline-secondary m-1 mt-md-0 mb-md-0 float-right"
@ -40,24 +35,18 @@
<hr class="my-2"> <hr class="my-2">
<div class="row collapse d-md-flex" <div class="row collapse d-md-flex" id="table-filter-row">
id="table-filter-row">
<div class="col-12 col-md-6 col-xl-3"> <div class="col-12 col-md-6 col-xl-3">
<div class="input-group"> <div class="input-group">
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-search"></i></span> <span class="input-group-text"><i class="fas fa-search"></i></span>
</div> </div>
<input type="text" <input type="text" id="search" class="form-control" placeholder="{{ $__t('Search') }}">
id="search"
class="form-control"
placeholder="{{ $__t('Search') }}">
</div> </div>
</div> </div>
<div class="col"> <div class="col">
<div class="float-right"> <div class="float-right">
<a id="clear-filter-button" <a id="clear-filter-button" class="btn btn-sm btn-outline-info" href="#">
class="btn btn-sm btn-outline-info"
href="#">
{{ $__t('Clear filter') }} {{ $__t('Clear filter') }}
</a> </a>
</div> </div>
@ -66,47 +55,38 @@
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<table id="users-table" {{-- TODO: DataTables: dynamic data: users --}}
class="table table-sm table-striped nowrap w-100"> <table id="users-table" class="table table-sm table-striped nowrap w-100">
<thead> <thead>
<tr> <tr>
<th class="border-right"><a class="text-muted change-table-columns-visibility-button" <th class="border-right"><a class="text-muted change-table-columns-visibility-button"
data-toggle="tooltip" data-toggle="tooltip" data-toggle="tooltip" title="{{ $__t('Table options') }}"
data-toggle="tooltip" data-table-selector="#users-table" href="#"><i class="fas fa-eye"></i></a>
title="{{ $__t('Table options') }}"
data-table-selector="#users-table"
href="#"><i class="fas fa-eye"></i></a>
</th> </th>
<th>{{ $__t('Username') }}</th> <th>{{ $__t('Username') }}</th>
<th>{{ $__t('First name') }}</th> <th>{{ $__t('First name') }}</th>
<th>{{ $__t('Last name') }}</th> <th>{{ $__t('Last name') }}</th>
@include('components.userfields_thead', array( @include('components.userfields_thead', [
'userfields' => $userfields 'userfields' => $userfields,
)) ])
</tr> </tr>
</thead> </thead>
<tbody class="d-none"> <tbody class="d-none">
@foreach ($users as $user) @foreach ($users as $user)
<tr> <tr>
<td class="fit-content border-right"> <td class="fit-content border-right">
<a class="btn btn-info btn-sm" <a class="btn btn-info btn-sm" href="{{ $U('/user/') }}{{ $user->id }}"
href="{{ $U('/user/') }}{{ $user->id }}" data-toggle="tooltip" title="{{ $__t('Edit this item') }}">
data-toggle="tooltip"
title="{{ $__t('Edit this item') }}">
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
</a> </a>
<a class="btn btn-info btn-sm" <a class="btn btn-info btn-sm" href="{{ $U('/user/' . $user->id . '/permissions') }}"
href="{{ $U('/user/' . $user->id . '/permissions') }}" data-toggle="tooltip" title="{{ $__t('Configure user permissions') }}">
data-toggle="tooltip"
title="{{ $__t('Configure user permissions') }}">
<i class="fas fa-lock"></i> <i class="fas fa-lock"></i>
</a> </a>
<a class="btn btn-danger btn-sm user-delete-button @if ($user->id == GROCY_USER_ID) disabled @endif" <a class="btn btn-danger btn-sm user-delete-button @if ($user->id == GROCY_USER_ID) disabled @endif"
href="#" href="#" data-user-id="{{ $user->id }}"
data-user-id="{{ $user->id }}" data-user-username="{{ $user->username }}" data-toggle="tooltip"
data-user-username="{{ $user->username }}"
data-toggle="tooltip"
title="{{ $__t('Delete this item') }}"> title="{{ $__t('Delete this item') }}">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
</a> </a>
@ -121,10 +101,14 @@
{{ $user->last_name }} {{ $user->last_name }}
</td> </td>
@include('components.userfields_tbody', array( @include('components.userfields_tbody', [
'userfields' => $userfields, 'userfields' => $userfields,
'userfieldValues' => FindAllObjectsInArrayByPropertyValue($userfieldValues, 'object_id', $user->id) 'userfieldValues' => FindAllObjectsInArrayByPropertyValue(
)) $userfieldValues,
'object_id',
$user->id
),
])
</tr> </tr>
@endforeach @endforeach
</tbody> </tbody>

View File

@ -238,7 +238,7 @@ datatables.net-bs4@1.10.16:
datatables.net "1.10.16" datatables.net "1.10.16"
jquery ">=1.7" jquery ">=1.7"
datatables.net-bs4@>=1.11.3, datatables.net-bs4@^1.10.22: datatables.net-bs4@>=1.11.3:
version "1.11.4" version "1.11.4"
resolved "https://registry.yarnpkg.com/datatables.net-bs4/-/datatables.net-bs4-1.11.4.tgz#caa82ab1a989bf1462f075b91213df865060d1ec" resolved "https://registry.yarnpkg.com/datatables.net-bs4/-/datatables.net-bs4-1.11.4.tgz#caa82ab1a989bf1462f075b91213df865060d1ec"
integrity sha512-4V2uSxFloX1jRIsy4eAt1INyp5M5Pq5SV017/naq3zpVKraaFwqFjLhtkx64UHGXqcPj7egvj27dVcdnNIKnNA== integrity sha512-4V2uSxFloX1jRIsy4eAt1INyp5M5Pq5SV017/naq3zpVKraaFwqFjLhtkx64UHGXqcPj7egvj27dVcdnNIKnNA==
@ -246,7 +246,15 @@ datatables.net-bs4@>=1.11.3, datatables.net-bs4@^1.10.22:
datatables.net ">=1.11.3" datatables.net ">=1.11.3"
jquery ">=1.7" jquery ">=1.7"
datatables.net-colreorder-bs4@^1.5.2: datatables.net-bs4@^1.11.5:
version "1.11.5"
resolved "https://registry.yarnpkg.com/datatables.net-bs4/-/datatables.net-bs4-1.11.5.tgz#7767a27991c75716ce7fef1aa6a41d01bfeaefbd"
integrity sha512-tYE9MnyWIW4P8nm41yQnCqW4dUPK3M7qsuxQY5rViFFKDEU0bUUNAnE9+69P5/4Exe4uyXaY05xFkD00niPZUA==
dependencies:
datatables.net ">=1.11.3"
jquery ">=1.7"
datatables.net-colreorder-bs4@^1.5.5:
version "1.5.5" version "1.5.5"
resolved "https://registry.yarnpkg.com/datatables.net-colreorder-bs4/-/datatables.net-colreorder-bs4-1.5.5.tgz#52ca2f95148572583ad619d6c9cdb44f1952d355" resolved "https://registry.yarnpkg.com/datatables.net-colreorder-bs4/-/datatables.net-colreorder-bs4-1.5.5.tgz#52ca2f95148572583ad619d6c9cdb44f1952d355"
integrity sha512-MGAJ/k/FeSK2Kccio5k0oBRacBpmaIKP2wXYfC64ONZFjOFxKBSmFevfg7yPdipYdYDwoQqDnmw0MpdIL7UUug== integrity sha512-MGAJ/k/FeSK2Kccio5k0oBRacBpmaIKP2wXYfC64ONZFjOFxKBSmFevfg7yPdipYdYDwoQqDnmw0MpdIL7UUug==
@ -255,7 +263,7 @@ datatables.net-colreorder-bs4@^1.5.2:
datatables.net-colreorder ">=1.5.4" datatables.net-colreorder ">=1.5.4"
jquery ">=1.7" jquery ">=1.7"
datatables.net-colreorder@>=1.5.4, datatables.net-colreorder@^1.5.2: datatables.net-colreorder@>=1.5.4, datatables.net-colreorder@^1.5.5:
version "1.5.5" version "1.5.5"
resolved "https://registry.yarnpkg.com/datatables.net-colreorder/-/datatables.net-colreorder-1.5.5.tgz#0de93e460cba5eb0167c0c491a2da0c76a2e3b12" resolved "https://registry.yarnpkg.com/datatables.net-colreorder/-/datatables.net-colreorder-1.5.5.tgz#0de93e460cba5eb0167c0c491a2da0c76a2e3b12"
integrity sha512-AUwv5A/87I4hg7GY/WbhRrDhqng9b019jLvvKutHibSPCEtMDWqyNtuP0q8zYoquqU9UQ1/nqXLW/ld8TzIDYQ== integrity sha512-AUwv5A/87I4hg7GY/WbhRrDhqng9b019jLvvKutHibSPCEtMDWqyNtuP0q8zYoquqU9UQ1/nqXLW/ld8TzIDYQ==
@ -263,12 +271,12 @@ datatables.net-colreorder@>=1.5.4, datatables.net-colreorder@^1.5.2:
datatables.net ">=1.11.3" datatables.net ">=1.11.3"
jquery ">=1.7" jquery ">=1.7"
datatables.net-plugins@^1.10.20: datatables.net-plugins@^1.11.5:
version "1.11.4" version "1.11.5"
resolved "https://registry.yarnpkg.com/datatables.net-plugins/-/datatables.net-plugins-1.11.4.tgz#8eb7915cd9f43ba8ba256b89e06917aa24d1ac41" resolved "https://registry.yarnpkg.com/datatables.net-plugins/-/datatables.net-plugins-1.11.5.tgz#c53ebbd0ab3473a08c6ae36eb1990a66de599b89"
integrity sha512-39yyyoCCavagE0mO1BFsrRPeak5BwOlbtSACdGpPNf3jG5Lm7D6vEUPPcpGy6eLxjCiG/orMxlAqb8E5lSZtoA== integrity sha512-+Rsf/fyLG8GyFqp7Bvd1ElqWGQO3NPsx2VADn9X8QaZbctshGVW0sqvR5V7iHHgY6OY1LR0+t6qIMhan9BM4gA==
datatables.net-rowgroup-bs4@^1.1.2: datatables.net-rowgroup-bs4@^1.1.4:
version "1.1.4" version "1.1.4"
resolved "https://registry.yarnpkg.com/datatables.net-rowgroup-bs4/-/datatables.net-rowgroup-bs4-1.1.4.tgz#dd4fad888edea895acd06fe8cf66816809d78eec" resolved "https://registry.yarnpkg.com/datatables.net-rowgroup-bs4/-/datatables.net-rowgroup-bs4-1.1.4.tgz#dd4fad888edea895acd06fe8cf66816809d78eec"
integrity sha512-D0+LxraRjvV1RpPNtXJuW9Z4Jn90Sykb7ytJC/5eJsEYc9WnLUvxWEok7fqPpl3dWphQKg5ZbWpKG55Gd1IIXA== integrity sha512-D0+LxraRjvV1RpPNtXJuW9Z4Jn90Sykb7ytJC/5eJsEYc9WnLUvxWEok7fqPpl3dWphQKg5ZbWpKG55Gd1IIXA==
@ -277,7 +285,7 @@ datatables.net-rowgroup-bs4@^1.1.2:
datatables.net-rowgroup ">=1.1.3" datatables.net-rowgroup ">=1.1.3"
jquery ">=1.7" jquery ">=1.7"
datatables.net-rowgroup@>=1.1.3, datatables.net-rowgroup@^1.1.2: datatables.net-rowgroup@>=1.1.3, datatables.net-rowgroup@^1.1.4:
version "1.1.4" version "1.1.4"
resolved "https://registry.yarnpkg.com/datatables.net-rowgroup/-/datatables.net-rowgroup-1.1.4.tgz#3eea91951d46f6c207d2e0c03cb3d635b7b09689" resolved "https://registry.yarnpkg.com/datatables.net-rowgroup/-/datatables.net-rowgroup-1.1.4.tgz#3eea91951d46f6c207d2e0c03cb3d635b7b09689"
integrity sha512-Oe9mL3X8RXLOQZblJVWTYD0melyw3xoPeQ3T2x1k2guTFxob8/2caKuzn95oFJau6tvbhsvY/QneTaCzHRKnnQ== integrity sha512-Oe9mL3X8RXLOQZblJVWTYD0melyw3xoPeQ3T2x1k2guTFxob8/2caKuzn95oFJau6tvbhsvY/QneTaCzHRKnnQ==
@ -285,7 +293,7 @@ datatables.net-rowgroup@>=1.1.3, datatables.net-rowgroup@^1.1.2:
datatables.net ">=1.11.3" datatables.net ">=1.11.3"
jquery ">=1.7" jquery ">=1.7"
datatables.net-select-bs4@^1.3.1: datatables.net-select-bs4@^1.3.4:
version "1.3.4" version "1.3.4"
resolved "https://registry.yarnpkg.com/datatables.net-select-bs4/-/datatables.net-select-bs4-1.3.4.tgz#07336260be0aa7741a61b82188350f4d1e422a02" resolved "https://registry.yarnpkg.com/datatables.net-select-bs4/-/datatables.net-select-bs4-1.3.4.tgz#07336260be0aa7741a61b82188350f4d1e422a02"
integrity sha512-hXxxTwR9Mx8xwD55g8hNiLt035afguKZ9Ejsdm5/mo3wmm9ml7gs8QG5fJuMRwtrdP9EKcnjc54+zVoHymEcgw== integrity sha512-hXxxTwR9Mx8xwD55g8hNiLt035afguKZ9Ejsdm5/mo3wmm9ml7gs8QG5fJuMRwtrdP9EKcnjc54+zVoHymEcgw==
@ -294,7 +302,7 @@ datatables.net-select-bs4@^1.3.1:
datatables.net-select ">=1.3.3" datatables.net-select ">=1.3.3"
jquery ">=1.7" jquery ">=1.7"
datatables.net-select@>=1.3.3, datatables.net-select@^1.3.1: datatables.net-select@>=1.3.3, datatables.net-select@^1.3.4:
version "1.3.4" version "1.3.4"
resolved "https://registry.yarnpkg.com/datatables.net-select/-/datatables.net-select-1.3.4.tgz#7970587a8d8db8ba70a4cccb89b8519bb518116d" resolved "https://registry.yarnpkg.com/datatables.net-select/-/datatables.net-select-1.3.4.tgz#7970587a8d8db8ba70a4cccb89b8519bb518116d"
integrity sha512-iQ/dBHIWkhfCBxzNdtef79seCNO1ZsA5zU0Uiw3R2mlwmjcJM1xn6pFNajke6SX7VnlzndGDHGqzzEljSqz4pA== integrity sha512-iQ/dBHIWkhfCBxzNdtef79seCNO1ZsA5zU0Uiw3R2mlwmjcJM1xn6pFNajke6SX7VnlzndGDHGqzzEljSqz4pA==
@ -309,13 +317,20 @@ datatables.net@1.10.16:
dependencies: dependencies:
jquery ">=1.7" jquery ">=1.7"
datatables.net@>=1.11.3, datatables.net@^1.10.22: datatables.net@>=1.11.3:
version "1.11.4" version "1.11.4"
resolved "https://registry.yarnpkg.com/datatables.net/-/datatables.net-1.11.4.tgz#5f3e1ec134fa532e794fbd47c13f8333d7a5c455" resolved "https://registry.yarnpkg.com/datatables.net/-/datatables.net-1.11.4.tgz#5f3e1ec134fa532e794fbd47c13f8333d7a5c455"
integrity sha512-z9LG4O0VYOYzp+rnArLExvnUWV8ikyWBcHYZEKDfVuz7BKxQdEq4a/tpO0Trbm+FL1+RY7UEIh+UcYNY/hwGxA== integrity sha512-z9LG4O0VYOYzp+rnArLExvnUWV8ikyWBcHYZEKDfVuz7BKxQdEq4a/tpO0Trbm+FL1+RY7UEIh+UcYNY/hwGxA==
dependencies: dependencies:
jquery ">=1.7" jquery ">=1.7"
datatables.net@^1.11.5:
version "1.11.5"
resolved "https://registry.yarnpkg.com/datatables.net/-/datatables.net-1.11.5.tgz#858a69953a01e1d5b18786769802117b04b8e3c9"
integrity sha512-nlFst2xfwSWaQgaOg5sXVG3cxYC0tH8E8d65289w9ROgF2TmLULOOpcdMpyxxUim/qEwVSEem42RjkTWEpr3eA==
dependencies:
jquery ">=1.7"
delayed-stream@~1.0.0: delayed-stream@~1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
@ -686,6 +701,16 @@ safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
select2-theme-bootstrap4@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/select2-theme-bootstrap4/-/select2-theme-bootstrap4-1.0.1.tgz#aa476930988cb61b05c77d1173a937688d2bb4be"
integrity sha512-gn9zN+ZppzsjRCQzHMfhDyizcCNlzbsB059qAXjyEaFYx7OA3677LYEIbYiGmliuF6y9Dbuae8X8Rk/Z4SBleQ==
select2@^4.0.13:
version "4.0.13"
resolved "https://registry.yarnpkg.com/select2/-/select2-4.0.13.tgz#0dbe377df3f96167c4c1626033e924372d8ef44d"
integrity sha512-1JeB87s6oN/TDxQQYCvS5EFoQyvV6eYMZZ0AeA4tdFDYWN3BAGZ8npr17UBFddU0lgAt3H0yjX3X6/ekOj1yjw==
sprintf-js@^1.0.3, sprintf-js@^1.1.2: sprintf-js@^1.0.3, sprintf-js@^1.1.2:
version "1.1.2" version "1.1.2"
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.2.tgz#da1765262bf8c0f571749f2ad6c26300207ae673" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.2.tgz#da1765262bf8c0f571749f2ad6c26300207ae673"