Started working on user-defined-fields for all entities (references #176)

This commit is contained in:
Bernd Bestel
2019-04-22 22:16:35 +02:00
parent 77f3b80540
commit fc11da3c3f
45 changed files with 1161 additions and 78 deletions

View File

@@ -4,6 +4,7 @@ namespace Grocy\Controllers;
use \Grocy\Services\BatteriesService; use \Grocy\Services\BatteriesService;
use \Grocy\Services\UsersService; use \Grocy\Services\UsersService;
use \Grocy\Services\UserfieldsService;
class BatteriesController extends BaseController class BatteriesController extends BaseController
{ {
@@ -11,9 +12,11 @@ class BatteriesController extends BaseController
{ {
parent::__construct($container); parent::__construct($container);
$this->BatteriesService = new BatteriesService(); $this->BatteriesService = new BatteriesService();
$this->UserfieldsService = new UserfieldsService();
} }
protected $BatteriesService; protected $BatteriesService;
protected $UserfieldsService;
public function Overview(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) public function Overview(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{ {
@@ -23,7 +26,9 @@ class BatteriesController extends BaseController
return $this->AppContainer->view->render($response, 'batteriesoverview', [ return $this->AppContainer->view->render($response, 'batteriesoverview', [
'batteries' => $this->Database->batteries()->orderBy('name'), 'batteries' => $this->Database->batteries()->orderBy('name'),
'current' => $this->BatteriesService->GetCurrent(), 'current' => $this->BatteriesService->GetCurrent(),
'nextXDays' => $nextXDays 'nextXDays' => $nextXDays,
'userfields' => $this->UserfieldsService->GetFields('batteries'),
'userfieldValues' => $this->UserfieldsService->GetAllValues('batteries')
]); ]);
} }
@@ -37,7 +42,9 @@ class BatteriesController extends BaseController
public function BatteriesList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) public function BatteriesList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{ {
return $this->AppContainer->view->render($response, 'batteries', [ return $this->AppContainer->view->render($response, 'batteries', [
'batteries' => $this->Database->batteries()->orderBy('name') 'batteries' => $this->Database->batteries()->orderBy('name'),
'userfields' => $this->UserfieldsService->GetFields('batteries'),
'userfieldValues' => $this->UserfieldsService->GetAllValues('batteries')
]); ]);
} }
@@ -46,14 +53,16 @@ class BatteriesController extends BaseController
if ($args['batteryId'] == 'new') if ($args['batteryId'] == 'new')
{ {
return $this->AppContainer->view->render($response, 'batteryform', [ return $this->AppContainer->view->render($response, 'batteryform', [
'mode' => 'create' 'mode' => 'create',
'userfields' => $this->UserfieldsService->GetFields('batteries')
]); ]);
} }
else else
{ {
return $this->AppContainer->view->render($response, 'batteryform', [ return $this->AppContainer->view->render($response, 'batteryform', [
'battery' => $this->Database->batteries($args['batteryId']), 'battery' => $this->Database->batteries($args['batteryId']),
'mode' => 'edit' 'mode' => 'edit',
'userfields' => $this->UserfieldsService->GetFields('batteries')
]); ]);
} }
} }

View File

@@ -4,6 +4,7 @@ namespace Grocy\Controllers;
use \Grocy\Services\ChoresService; use \Grocy\Services\ChoresService;
use \Grocy\Services\UsersService; use \Grocy\Services\UsersService;
use \Grocy\Services\UserfieldsService;
class ChoresController extends BaseController class ChoresController extends BaseController
{ {
@@ -11,9 +12,11 @@ class ChoresController extends BaseController
{ {
parent::__construct($container); parent::__construct($container);
$this->ChoresService = new ChoresService(); $this->ChoresService = new ChoresService();
$this->UserfieldsService = new UserfieldsService();
} }
protected $ChoresService; protected $ChoresService;
protected $UserfieldsService;
public function Overview(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) public function Overview(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{ {
@@ -23,7 +26,9 @@ class ChoresController extends BaseController
return $this->AppContainer->view->render($response, 'choresoverview', [ return $this->AppContainer->view->render($response, 'choresoverview', [
'chores' => $this->Database->chores()->orderBy('name'), 'chores' => $this->Database->chores()->orderBy('name'),
'currentChores' => $this->ChoresService->GetCurrent(), 'currentChores' => $this->ChoresService->GetCurrent(),
'nextXDays' => $nextXDays 'nextXDays' => $nextXDays,
'userfields' => $this->UserfieldsService->GetFields('chores'),
'userfieldValues' => $this->UserfieldsService->GetAllValues('chores')
]); ]);
} }
@@ -38,7 +43,9 @@ class ChoresController extends BaseController
public function ChoresList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) public function ChoresList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{ {
return $this->AppContainer->view->render($response, 'chores', [ return $this->AppContainer->view->render($response, 'chores', [
'chores' => $this->Database->chores()->orderBy('name') 'chores' => $this->Database->chores()->orderBy('name'),
'userfields' => $this->UserfieldsService->GetFields('chores'),
'userfieldValues' => $this->UserfieldsService->GetAllValues('chores')
]); ]);
} }
@@ -57,7 +64,8 @@ class ChoresController extends BaseController
{ {
return $this->AppContainer->view->render($response, 'choreform', [ return $this->AppContainer->view->render($response, 'choreform', [
'periodTypes' => GetClassConstants('\Grocy\Services\ChoresService'), 'periodTypes' => GetClassConstants('\Grocy\Services\ChoresService'),
'mode' => 'create' 'mode' => 'create',
'userfields' => $this->UserfieldsService->GetFields('chores')
]); ]);
} }
else else
@@ -65,7 +73,8 @@ class ChoresController extends BaseController
return $this->AppContainer->view->render($response, 'choreform', [ return $this->AppContainer->view->render($response, 'choreform', [
'chore' => $this->Database->chores($args['choreId']), 'chore' => $this->Database->chores($args['choreId']),
'periodTypes' => GetClassConstants('\Grocy\Services\ChoresService'), 'periodTypes' => GetClassConstants('\Grocy\Services\ChoresService'),
'mode' => 'edit' 'mode' => 'edit',
'userfields' => $this->UserfieldsService->GetFields('chores')
]); ]);
} }
} }

View File

@@ -2,8 +2,18 @@
namespace Grocy\Controllers; namespace Grocy\Controllers;
use \Grocy\Services\UserfieldsService;
class GenericEntityApiController extends BaseApiController class GenericEntityApiController extends BaseApiController
{ {
public function __construct(\Slim\Container $container)
{
parent::__construct($container);
$this->UserfieldsService = new UserfieldsService();
}
protected $UserfieldsService;
public function GetObjects(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) public function GetObjects(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{ {
if ($this->IsValidEntity($args['entity']) && !$this->IsEntityWithPreventedListing($args['entity'])) if ($this->IsValidEntity($args['entity']) && !$this->IsEntityWithPreventedListing($args['entity']))
@@ -44,7 +54,9 @@ class GenericEntityApiController extends BaseApiController
$newRow = $this->Database->{$args['entity']}()->createRow($requestBody); $newRow = $this->Database->{$args['entity']}()->createRow($requestBody);
$newRow->save(); $newRow->save();
$success = $newRow->isClean(); $success = $newRow->isClean();
return $this->EmptyApiResponse($response); return $this->ApiResponse(array(
'created_object_id' => $this->Database->lastInsertId()
));
} }
catch (\Exception $ex) catch (\Exception $ex)
{ {
@@ -101,6 +113,38 @@ class GenericEntityApiController extends BaseApiController
} }
} }
public function GetUserfields(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
try
{
return $this->ApiResponse($this->UserfieldsService->GetValues($args['entity'], $args['objectId']));
}
catch (\Exception $ex)
{
return $this->GenericErrorResponse($response, $ex->getMessage());
}
}
public function SetUserfields(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
$requestBody = $request->getParsedBody();
try
{
if ($requestBody === null)
{
throw new \Exception('Request body could not be parsed (probably invalid JSON format or missing/wrong Content-Type header)');
}
$this->UserfieldsService->SetValues($args['entity'], $args['objectId'], $requestBody);
return $this->EmptyApiResponse($response);
}
catch (\Exception $ex)
{
return $this->GenericErrorResponse($response, $ex->getMessage());
}
}
private function IsValidEntity($entity) private function IsValidEntity($entity)
{ {
return in_array($entity, $this->OpenApiSpec->components->internalSchemas->ExposedEntity->enum); return in_array($entity, $this->OpenApiSpec->components->internalSchemas->ExposedEntity->enum);

View File

@@ -0,0 +1,45 @@
<?php
namespace Grocy\Controllers;
use \Grocy\Services\UserfieldsService;
class GenericEntityController extends BaseController
{
public function __construct(\Slim\Container $container)
{
parent::__construct($container);
$this->UserfieldsService = new UserfieldsService();
}
protected $UserfieldsService;
public function UserfieldsList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
return $this->AppContainer->view->render($response, 'userfields', [
'userfields' => $this->UserfieldsService->GetAllFields(),
'entities' => $this->UserfieldsService->GetEntities()
]);
}
public function UserfieldEditForm(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
if ($args['userfieldId'] == 'new')
{
return $this->AppContainer->view->render($response, 'userfieldform', [
'mode' => 'create',
'userfieldTypes' => $this->UserfieldsService->GetFieldTypes(),
'entities' => $this->UserfieldsService->GetEntities()
]);
}
else
{
return $this->AppContainer->view->render($response, 'userfieldform', [
'mode' => 'edit',
'userfield' => $this->UserfieldsService->GetField($args['userfieldId']),
'userfieldTypes' => $this->UserfieldsService->GetFieldTypes(),
'entities' => $this->UserfieldsService->GetEntities()
]);
}
}
}

View File

@@ -4,6 +4,7 @@ namespace Grocy\Controllers;
use \Grocy\Services\StockService; use \Grocy\Services\StockService;
use \Grocy\Services\UsersService; use \Grocy\Services\UsersService;
use \Grocy\Services\UserfieldsService;
class StockController extends BaseController class StockController extends BaseController
{ {
@@ -12,9 +13,11 @@ class StockController extends BaseController
{ {
parent::__construct($container); parent::__construct($container);
$this->StockService = new StockService(); $this->StockService = new StockService();
$this->UserfieldsService = new UserfieldsService();
} }
protected $StockService; protected $StockService;
protected $UserfieldsService;
public function Overview(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) public function Overview(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{ {
@@ -29,7 +32,9 @@ class StockController extends BaseController
'currentStockLocations' => $this->StockService->GetCurrentStockLocations(), 'currentStockLocations' => $this->StockService->GetCurrentStockLocations(),
'missingProducts' => $this->StockService->GetMissingProducts(), 'missingProducts' => $this->StockService->GetMissingProducts(),
'nextXDays' => $nextXDays, 'nextXDays' => $nextXDays,
'productGroups' => $this->Database->product_groups()->orderBy('name') 'productGroups' => $this->Database->product_groups()->orderBy('name'),
'userfields' => $this->UserfieldsService->GetFields('products'),
'userfieldValues' => $this->UserfieldsService->GetAllValues('products')
]); ]);
} }
@@ -82,7 +87,9 @@ class StockController extends BaseController
'products' => $this->Database->products()->orderBy('name'), 'products' => $this->Database->products()->orderBy('name'),
'locations' => $this->Database->locations()->orderBy('name'), 'locations' => $this->Database->locations()->orderBy('name'),
'quantityunits' => $this->Database->quantity_units()->orderBy('name'), 'quantityunits' => $this->Database->quantity_units()->orderBy('name'),
'productGroups' => $this->Database->product_groups()->orderBy('name') 'productGroups' => $this->Database->product_groups()->orderBy('name'),
'userfields' => $this->UserfieldsService->GetFields('products'),
'userfieldValues' => $this->UserfieldsService->GetAllValues('products')
]); ]);
} }
@@ -98,7 +105,9 @@ class StockController extends BaseController
public function LocationsList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) public function LocationsList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{ {
return $this->AppContainer->view->render($response, 'locations', [ return $this->AppContainer->view->render($response, 'locations', [
'locations' => $this->Database->locations()->orderBy('name') 'locations' => $this->Database->locations()->orderBy('name'),
'userfields' => $this->UserfieldsService->GetFields('locations'),
'userfieldValues' => $this->UserfieldsService->GetAllValues('locations')
]); ]);
} }
@@ -106,14 +115,18 @@ class StockController extends BaseController
{ {
return $this->AppContainer->view->render($response, 'productgroups', [ return $this->AppContainer->view->render($response, 'productgroups', [
'productGroups' => $this->Database->product_groups()->orderBy('name'), 'productGroups' => $this->Database->product_groups()->orderBy('name'),
'products' => $this->Database->products()->orderBy('name') 'products' => $this->Database->products()->orderBy('name'),
'userfields' => $this->UserfieldsService->GetFields('product_groups'),
'userfieldValues' => $this->UserfieldsService->GetAllValues('product_groups')
]); ]);
} }
public function QuantityUnitsList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) public function QuantityUnitsList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{ {
return $this->AppContainer->view->render($response, 'quantityunits', [ return $this->AppContainer->view->render($response, 'quantityunits', [
'quantityunits' => $this->Database->quantity_units()->orderBy('name') 'quantityunits' => $this->Database->quantity_units()->orderBy('name'),
'userfields' => $this->UserfieldsService->GetFields('quantity_units'),
'userfieldValues' => $this->UserfieldsService->GetAllValues('quantity_units')
]); ]);
} }
@@ -125,6 +138,7 @@ class StockController extends BaseController
'locations' => $this->Database->locations()->orderBy('name'), 'locations' => $this->Database->locations()->orderBy('name'),
'quantityunits' => $this->Database->quantity_units()->orderBy('name'), 'quantityunits' => $this->Database->quantity_units()->orderBy('name'),
'productgroups' => $this->Database->product_groups()->orderBy('name'), 'productgroups' => $this->Database->product_groups()->orderBy('name'),
'userfields' => $this->UserfieldsService->GetFields('products'),
'mode' => 'create' 'mode' => 'create'
]); ]);
} }
@@ -135,6 +149,7 @@ class StockController extends BaseController
'locations' => $this->Database->locations()->orderBy('name'), 'locations' => $this->Database->locations()->orderBy('name'),
'quantityunits' => $this->Database->quantity_units()->orderBy('name'), 'quantityunits' => $this->Database->quantity_units()->orderBy('name'),
'productgroups' => $this->Database->product_groups()->orderBy('name'), 'productgroups' => $this->Database->product_groups()->orderBy('name'),
'userfields' => $this->UserfieldsService->GetFields('products'),
'mode' => 'edit' 'mode' => 'edit'
]); ]);
} }
@@ -145,14 +160,16 @@ class StockController extends BaseController
if ($args['locationId'] == 'new') if ($args['locationId'] == 'new')
{ {
return $this->AppContainer->view->render($response, 'locationform', [ return $this->AppContainer->view->render($response, 'locationform', [
'mode' => 'create' 'mode' => 'create',
'userfields' => $this->UserfieldsService->GetFields('locations')
]); ]);
} }
else else
{ {
return $this->AppContainer->view->render($response, 'locationform', [ return $this->AppContainer->view->render($response, 'locationform', [
'location' => $this->Database->locations($args['locationId']), 'location' => $this->Database->locations($args['locationId']),
'mode' => 'edit' 'mode' => 'edit',
'userfields' => $this->UserfieldsService->GetFields('locations')
]); ]);
} }
} }
@@ -162,14 +179,16 @@ class StockController extends BaseController
if ($args['productGroupId'] == 'new') if ($args['productGroupId'] == 'new')
{ {
return $this->AppContainer->view->render($response, 'productgroupform', [ return $this->AppContainer->view->render($response, 'productgroupform', [
'mode' => 'create' 'mode' => 'create',
'userfields' => $this->UserfieldsService->GetFields('product_groups')
]); ]);
} }
else else
{ {
return $this->AppContainer->view->render($response, 'productgroupform', [ return $this->AppContainer->view->render($response, 'productgroupform', [
'group' => $this->Database->product_groups($args['productGroupId']), 'group' => $this->Database->product_groups($args['productGroupId']),
'mode' => 'edit' 'mode' => 'edit',
'userfields' => $this->UserfieldsService->GetFields('product_groups')
]); ]);
} }
} }
@@ -179,14 +198,16 @@ class StockController extends BaseController
if ($args['quantityunitId'] == 'new') if ($args['quantityunitId'] == 'new')
{ {
return $this->AppContainer->view->render($response, 'quantityunitform', [ return $this->AppContainer->view->render($response, 'quantityunitform', [
'mode' => 'create' 'mode' => 'create',
'userfields' => $this->UserfieldsService->GetFields('quantity_units')
]); ]);
} }
else else
{ {
return $this->AppContainer->view->render($response, 'quantityunitform', [ return $this->AppContainer->view->render($response, 'quantityunitform', [
'quantityunit' => $this->Database->quantity_units($args['quantityunitId']), 'quantityunit' => $this->Database->quantity_units($args['quantityunitId']),
'mode' => 'edit' 'mode' => 'edit',
'userfields' => $this->UserfieldsService->GetFields('quantity_units')
]); ]);
} }
} }

View File

@@ -4,6 +4,7 @@ namespace Grocy\Controllers;
use \Grocy\Services\TasksService; use \Grocy\Services\TasksService;
use \Grocy\Services\UsersService; use \Grocy\Services\UsersService;
use \Grocy\Services\UserfieldsService;
class TasksController extends BaseController class TasksController extends BaseController
{ {
@@ -11,9 +12,11 @@ class TasksController extends BaseController
{ {
parent::__construct($container); parent::__construct($container);
$this->TasksService = new TasksService(); $this->TasksService = new TasksService();
$this->UserfieldsService = new UserfieldsService();
} }
protected $TasksService; protected $TasksService;
protected $UserfieldsService;
public function Overview(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) public function Overview(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{ {
@@ -44,7 +47,8 @@ class TasksController extends BaseController
return $this->AppContainer->view->render($response, 'taskform', [ return $this->AppContainer->view->render($response, 'taskform', [
'mode' => 'create', 'mode' => 'create',
'taskCategories' => $this->Database->task_categories()->orderBy('name'), 'taskCategories' => $this->Database->task_categories()->orderBy('name'),
'users' => $this->Database->users()->orderBy('username') 'users' => $this->Database->users()->orderBy('username'),
'userfields' => $this->UserfieldsService->GetFields('tasks')
]); ]);
} }
else else
@@ -53,7 +57,8 @@ class TasksController extends BaseController
'task' => $this->Database->tasks($args['taskId']), 'task' => $this->Database->tasks($args['taskId']),
'mode' => 'edit', 'mode' => 'edit',
'taskCategories' => $this->Database->task_categories()->orderBy('name'), 'taskCategories' => $this->Database->task_categories()->orderBy('name'),
'users' => $this->Database->users()->orderBy('username') 'users' => $this->Database->users()->orderBy('username'),
'userfields' => $this->UserfieldsService->GetFields('tasks')
]); ]);
} }
} }
@@ -61,7 +66,9 @@ class TasksController extends BaseController
public function TaskCategoriesList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) public function TaskCategoriesList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{ {
return $this->AppContainer->view->render($response, 'taskcategories', [ return $this->AppContainer->view->render($response, 'taskcategories', [
'taskCategories' => $this->Database->task_categories()->orderBy('name') 'taskCategories' => $this->Database->task_categories()->orderBy('name'),
'userfields' => $this->UserfieldsService->GetFields('task_categories'),
'userfieldValues' => $this->UserfieldsService->GetAllValues('task_categories')
]); ]);
} }
@@ -70,14 +77,16 @@ class TasksController extends BaseController
if ($args['categoryId'] == 'new') if ($args['categoryId'] == 'new')
{ {
return $this->AppContainer->view->render($response, 'taskcategoryform', [ return $this->AppContainer->view->render($response, 'taskcategoryform', [
'mode' => 'create' 'mode' => 'create',
'userfields' => $this->UserfieldsService->GetFields('task_categories')
]); ]);
} }
else else
{ {
return $this->AppContainer->view->render($response, 'taskcategoryform', [ return $this->AppContainer->view->render($response, 'taskcategoryform', [
'category' => $this->Database->task_categories($args['categoryId']), 'category' => $this->Database->task_categories($args['categoryId']),
'mode' => 'edit' 'mode' => 'edit',
'userfields' => $this->UserfieldsService->GetFields('task_categories')
]); ]);
} }
} }

View File

@@ -1,2 +0,0 @@
*
!.gitignore

View File

@@ -234,8 +234,22 @@
} }
}, },
"responses": { "responses": {
"204": { "200": {
"description": "The operation was successful" "description": "The operation was successful",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"created_object_id": {
"type": "number",
"format": "integer",
"description": "The id of the created object"
}
}
}
}
}
}, },
"400": { "400": {
"description": "The operation was not successful", "description": "The operation was not successful",
@@ -438,6 +452,109 @@
} }
} }
}, },
"/userfields/{entity}/{objectId}": {
"get": {
"summary": "Returns all userfields with their values of the given object of the given entity",
"tags": [
"Generic entity interactions"
],
"parameters": [
{
"in": "path",
"name": "entity",
"required": true,
"description": "A valid entity name",
"schema": {
"$ref": "#/components/internalSchemas/ExposedEntity"
}
},
{
"in": "path",
"name": "objectId",
"required": true,
"description": "A valid object id of the given entity",
"schema": {
"type": "integer"
}
}
],
"responses": {
"200": {
"description": "An entity object",
"content": {
"application/json": {
"schema": {
"type": "object",
"description": "Just key/value pairs of userfields"
}
}
}
},
"400": {
"description": "The operation was not successful",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/GenericErrorResponse"
}
}
}
}
}
},
"put": {
"summary": "Edits the given userfields of the given object of the given entity",
"tags": [
"Generic entity interactions"
],
"parameters": [
{
"in": "path",
"name": "entity",
"required": true,
"description": "A valid entity name",
"schema": {
"$ref": "#/components/internalSchemas/ExposedEntity"
}
},
{
"in": "path",
"name": "objectId",
"required": true,
"description": "A valid object id of the given entity",
"schema": {
"type": "integer"
}
}
],
"requestBody": {
"description": "A valid entity object of the entity specified in parameter *entity*",
"required": true,
"content": {
"application/json": {
"schema": {
"description": "Just key/value pairs of userfields"
}
}
}
},
"responses": {
"204": {
"description": "The operation was successful"
},
"400": {
"description": "The operation was not successful",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/GenericErrorResponse"
}
}
}
}
}
}
},
"/files/{group}/{fileName}": { "/files/{group}/{fileName}": {
"get": { "get": {
"summary": "Serves the given file", "summary": "Serves the given file",
@@ -2030,7 +2147,8 @@
"task_categories", "task_categories",
"product_groups", "product_groups",
"equipment", "equipment",
"api_keys" "api_keys",
"userfields"
] ]
}, },
"ExposedEntitiesPreventListing": { "ExposedEntitiesPreventListing": {

View File

@@ -0,0 +1,11 @@
<?php
return array(
'text-single-line' => 'Text (single line)',
'text-multi-line' => 'Text (multi line)',
'number-integral' => 'Number (integral)',
'number-decimal' => 'Number (decimal)',
'date' => 'Date (without time)',
'datetime' => 'Date & time',
'checkbox' => 'Checkbox'
);

31
migrations/0066.sql Normal file
View File

@@ -0,0 +1,31 @@
CREATE TABLE userfields (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
entity TEXT NOT NULL,
name TEXT NOT NULL,
caption TEXT NOT NULL,
type TEXT NOT NULL,
show_as_column_in_tables TINYINT NOT NULL DEFAULT 0,
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime')),
UNIQUE(entity, name)
);
CREATE TABLE userfield_values (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
field_id INTEGER NOT NULL,
object_id INTEGER NOT NULL,
value TEXT NOT NULL,
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime')),
UNIQUE(field_id, object_id)
);
CREATE VIEW userfield_values_resolved
AS
SELECT
u.*,
uv.object_id,
uv.value
FROM userfields u
JOIN userfield_values uv
ON u.id = uv.field_id;

View File

@@ -9,8 +9,12 @@
{ {
Grocy.Api.Post('objects/batteries', jsonData, Grocy.Api.Post('objects/batteries', jsonData,
function(result) function(result)
{
Grocy.EditObjectId = result.created_object_id;
Grocy.Components.UserfieldsForm.Save(function()
{ {
window.location.href = U('/batteries'); window.location.href = U('/batteries');
});
}, },
function(xhr) function(xhr)
{ {
@@ -23,8 +27,11 @@
{ {
Grocy.Api.Put('objects/batteries/' + Grocy.EditObjectId, jsonData, Grocy.Api.Put('objects/batteries/' + Grocy.EditObjectId, jsonData,
function(result) function(result)
{
Grocy.Components.UserfieldsForm.Save(function()
{ {
window.location.href = U('/batteries'); window.location.href = U('/batteries');
});
}, },
function(xhr) function(xhr)
{ {
@@ -57,5 +64,6 @@ $('#battery-form input').keydown(function(event)
} }
}); });
Grocy.Components.UserfieldsForm.Load();
$('#name').focus(); $('#name').focus();
Grocy.FrontendHelpers.ValidateForm('battery-form'); Grocy.FrontendHelpers.ValidateForm('battery-form');

View File

@@ -9,8 +9,12 @@
{ {
Grocy.Api.Post('objects/chores', jsonData, Grocy.Api.Post('objects/chores', jsonData,
function(result) function(result)
{
Grocy.EditObjectId = result.created_object_id;
Grocy.Components.UserfieldsForm.Save(function()
{ {
window.location.href = U('/chores'); window.location.href = U('/chores');
});
}, },
function(xhr) function(xhr)
{ {
@@ -23,8 +27,11 @@
{ {
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()
{ {
window.location.href = U('/chores'); window.location.href = U('/chores');
});
}, },
function(xhr) function(xhr)
{ {
@@ -66,6 +73,7 @@ for (var i = 0; i < checkboxValues.length; i++)
} }
} }
Grocy.Components.UserfieldsForm.Load();
$('#name').focus(); $('#name').focus();
Grocy.FrontendHelpers.ValidateForm('chore-form'); Grocy.FrontendHelpers.ValidateForm('chore-form');

View File

@@ -0,0 +1,69 @@
Grocy.Components.UserfieldsForm = { };
Grocy.Components.UserfieldsForm.Save = function(success, error)
{
var jsonData = { };
$("#userfields-form .userfield-input").each(function()
{
var input = $(this);
var fieldName = input.attr("id");
var fieldValue = input.val();
if (input.attr("type") == "checkbox")
{
jsonData[fieldName] = "0";
if (input.is(":checked"))
{
jsonData[fieldName] = "1";
}
}
else
{
jsonData[fieldName] = fieldValue;
}
});
Grocy.Api.Put('userfields/' + $("#userfields-form").data("entity") + '/' + Grocy.EditObjectId, jsonData,
function(result)
{
if (success)
{
success();
}
},
function(xhr)
{
if (error)
{
error();
}
}
);
}
Grocy.Components.UserfieldsForm.Load = function()
{
Grocy.Api.Get('userfields/' + $("#userfields-form").data("entity") + '/' + Grocy.EditObjectId,
function(result)
{
$.each(result, function(key, value)
{
var input = $("#" + key + ".userfield-input");
if (input.attr("type") == "checkbox" && value == 1)
{
input.prop("checked", true);
}
else
{
input.val(value);
}
});
},
function(xhr)
{
console.log(xhr);
}
);
}

View File

@@ -9,8 +9,12 @@
{ {
Grocy.Api.Post('objects/locations', jsonData, Grocy.Api.Post('objects/locations', jsonData,
function(result) function(result)
{
Grocy.EditObjectId = result.created_object_id;
Grocy.Components.UserfieldsForm.Save(function()
{ {
window.location.href = U('/locations'); window.location.href = U('/locations');
});
}, },
function(xhr) function(xhr)
{ {
@@ -23,8 +27,11 @@
{ {
Grocy.Api.Put('objects/locations/' + Grocy.EditObjectId, jsonData, Grocy.Api.Put('objects/locations/' + Grocy.EditObjectId, jsonData,
function(result) function(result)
{
Grocy.Components.UserfieldsForm.Save(function()
{ {
window.location.href = U('/locations'); window.location.href = U('/locations');
});
}, },
function(xhr) function(xhr)
{ {
@@ -57,5 +64,6 @@ $('#location-form input').keydown(function (event)
} }
}); });
Grocy.Components.UserfieldsForm.Load();
$('#name').focus(); $('#name').focus();
Grocy.FrontendHelpers.ValidateForm('location-form'); Grocy.FrontendHelpers.ValidateForm('location-form');

View File

@@ -27,6 +27,9 @@
{ {
Grocy.Api.Post('objects/products', jsonData, Grocy.Api.Post('objects/products', jsonData,
function(result) function(result)
{
Grocy.EditObjectId = result.created_object_id;
Grocy.Components.UserfieldsForm.Save(function()
{ {
if (jsonData.hasOwnProperty("picture_file_name") && !Grocy.DeleteProductPictureOnSave) if (jsonData.hasOwnProperty("picture_file_name") && !Grocy.DeleteProductPictureOnSave)
{ {
@@ -46,6 +49,7 @@
{ {
window.location.href = redirectDestination; window.location.href = redirectDestination;
} }
});
}, },
function (xhr) function (xhr)
{ {
@@ -73,6 +77,8 @@
Grocy.Api.Put('objects/products/' + Grocy.EditObjectId, jsonData, Grocy.Api.Put('objects/products/' + Grocy.EditObjectId, jsonData,
function(result) function(result)
{
Grocy.Components.UserfieldsForm.Save(function()
{ {
if (jsonData.hasOwnProperty("picture_file_name") && !Grocy.DeleteProductPictureOnSave) if (jsonData.hasOwnProperty("picture_file_name") && !Grocy.DeleteProductPictureOnSave)
{ {
@@ -92,6 +98,7 @@
{ {
window.location.href = redirectDestination; window.location.href = redirectDestination;
} }
});
}, },
function(xhr) function(xhr)
{ {
@@ -258,6 +265,7 @@ if (Grocy.EditMode === 'create')
} }
} }
Grocy.Components.UserfieldsForm.Load();
$('#name').focus(); $('#name').focus();
$('.input-group-qu').trigger('change'); $('.input-group-qu').trigger('change');
Grocy.FrontendHelpers.ValidateForm('product-form'); Grocy.FrontendHelpers.ValidateForm('product-form');

View File

@@ -9,8 +9,12 @@
{ {
Grocy.Api.Post('objects/product_groups', jsonData, Grocy.Api.Post('objects/product_groups', jsonData,
function(result) function(result)
{
Grocy.EditObjectId = result.created_object_id;
Grocy.Components.UserfieldsForm.Save(function()
{ {
window.location.href = U('/productgroups'); window.location.href = U('/productgroups');
});
}, },
function(xhr) function(xhr)
{ {
@@ -23,8 +27,11 @@
{ {
Grocy.Api.Put('objects/product_groups/' + Grocy.EditObjectId, jsonData, Grocy.Api.Put('objects/product_groups/' + Grocy.EditObjectId, jsonData,
function(result) function(result)
{
Grocy.Components.UserfieldsForm.Save(function()
{ {
window.location.href = U('/productgroups'); window.location.href = U('/productgroups');
});
}, },
function(xhr) function(xhr)
{ {
@@ -57,5 +64,6 @@ $('#product-group-form input').keydown(function (event)
} }
}); });
Grocy.Components.UserfieldsForm.Load();
$('#name').focus(); $('#name').focus();
Grocy.FrontendHelpers.ValidateForm('product-group-form'); Grocy.FrontendHelpers.ValidateForm('product-group-form');

View File

@@ -9,8 +9,12 @@
{ {
Grocy.Api.Post('objects/quantity_units', jsonData, Grocy.Api.Post('objects/quantity_units', jsonData,
function(result) function(result)
{
Grocy.EditObjectId = result.created_object_id;
Grocy.Components.UserfieldsForm.Save(function()
{ {
window.location.href = U('/quantityunits'); window.location.href = U('/quantityunits');
});
}, },
function(xhr) function(xhr)
{ {
@@ -23,8 +27,11 @@
{ {
Grocy.Api.Put('objects/quantity_units/' + Grocy.EditObjectId, jsonData, Grocy.Api.Put('objects/quantity_units/' + Grocy.EditObjectId, jsonData,
function(result) function(result)
{
Grocy.Components.UserfieldsForm.Save(function()
{ {
window.location.href = U('/quantityunits'); window.location.href = U('/quantityunits');
});
}, },
function(xhr) function(xhr)
{ {
@@ -57,5 +64,6 @@ $('#quantityunit-form input').keydown(function(event)
} }
}); });
Grocy.Components.UserfieldsForm.Load();
$('#name').focus(); $('#name').focus();
Grocy.FrontendHelpers.ValidateForm('quantityunit-form'); Grocy.FrontendHelpers.ValidateForm('quantityunit-form');

View File

@@ -9,8 +9,12 @@
{ {
Grocy.Api.Post('objects/task_categories', jsonData, Grocy.Api.Post('objects/task_categories', jsonData,
function(result) function(result)
{
Grocy.EditObjectId = result.created_object_id;
Grocy.Components.UserfieldsForm.Save(function()
{ {
window.location.href = U('/taskcategories'); window.location.href = U('/taskcategories');
});
}, },
function(xhr) function(xhr)
{ {
@@ -23,8 +27,11 @@
{ {
Grocy.Api.Put('objects/task_categories/' + Grocy.EditObjectId, jsonData, Grocy.Api.Put('objects/task_categories/' + Grocy.EditObjectId, jsonData,
function(result) function(result)
{
Grocy.Components.UserfieldsForm.Save(function()
{ {
window.location.href = U('/taskcategories'); window.location.href = U('/taskcategories');
});
}, },
function(xhr) function(xhr)
{ {
@@ -57,5 +64,6 @@ $('#task-category-form input').keydown(function (event)
} }
}); });
Grocy.Components.UserfieldsForm.Load();
$('#name').focus(); $('#name').focus();
Grocy.FrontendHelpers.ValidateForm('task-category-form'); Grocy.FrontendHelpers.ValidateForm('task-category-form');

View File

@@ -0,0 +1,74 @@
$('#save-userfield-button').on('click', function(e)
{
e.preventDefault();
var jsonData = $('#userfield-form').serializeJSON();
Grocy.FrontendHelpers.BeginUiBusy("userfield-form");
if (Grocy.EditMode === 'create')
{
Grocy.Api.Post('objects/userfields', jsonData,
function(result)
{
window.location.href = U('/userfields');
},
function(xhr)
{
Grocy.FrontendHelpers.EndUiBusy("userfield-form");
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response)
}
);
}
else
{
Grocy.Api.Put('objects/userfields/' + Grocy.EditObjectId, jsonData,
function(result)
{
window.location.href = U('/userfields');
},
function(xhr)
{
Grocy.FrontendHelpers.EndUiBusy("userfield-form");
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response)
}
);
}
});
$('#userfield-form input').keyup(function(event)
{
Grocy.FrontendHelpers.ValidateForm('userfield-form');
});
$('#userfield-form select').change(function(event)
{
Grocy.FrontendHelpers.ValidateForm('userfield-form');
});
$('#userfield-form input').keydown(function(event)
{
if (event.keyCode === 13) //Enter
{
event.preventDefault();
if (document.getElementById('userfield-form').checkValidity() === false) //There is at least one validation error
{
return false;
}
else
{
$('#save-userfield-button').click();
}
}
});
$('#entity').focus();
if (typeof GetUriParam("entity") !== "undefined")
{
$("#entity").val(GetUriParam("entity"));
$("#entity").trigger("change");
$('#name').focus();
}
Grocy.FrontendHelpers.ValidateForm('userfield-form');

View File

@@ -0,0 +1,86 @@
var userfieldsTable = $('#userfields-table').DataTable({
'paginate': false,
'order': [[1, 'asc']],
'columnDefs': [
{ 'orderable': false, 'targets': 0 }
],
'language': JSON.parse(L('datatables_localization')),
'scrollY': false,
'colReorder': true,
'stateSave': true,
'stateSaveParams': function(settings, data)
{
data.search.search = "";
data.columns.forEach(column =>
{
column.search.search = "";
});
}
});
$('#userfields-table tbody').removeClass("d-none");
userfieldsTable.columns.adjust().draw();
$("#search").on("keyup", function()
{
var value = $(this).val();
if (value === "all")
{
value = "";
}
userfieldsTable.search(value).draw();
});
$("#entity-filter").on("change", function()
{
var value = $("#entity-filter option:selected").text();
if (value === L("All"))
{
value = "";
}
userfieldsTable.column(1).search(value).draw();
});
$(document).on('click', '.userfield-delete-button', function (e)
{
var objectName = $(e.currentTarget).attr('data-userfield-name');
var objectId = $(e.currentTarget).attr('data-userfield-id');
bootbox.confirm({
message: L('Are you sure to delete user field "#1"?', objectName),
buttons: {
confirm: {
label: L('Yes'),
className: 'btn-success'
},
cancel: {
label: L('No'),
className: 'btn-danger'
}
},
callback: function(result)
{
if (result === true)
{
Grocy.Api.Delete('objects/userfields/' + objectId, {},
function(result)
{
window.location.href = U('/userfields');
},
function(xhr)
{
console.error(xhr);
}
);
}
}
});
});
if (typeof GetUriParam("entity") !== "undefined")
{
$("#entity-filter").val(GetUriParam("entity"));
$("#entity-filter").trigger("change");
}

View File

@@ -16,6 +16,10 @@ $app->group('', function()
$this->post('/login', 'LoginControllerInstance:ProcessLogin')->setName('login'); $this->post('/login', 'LoginControllerInstance:ProcessLogin')->setName('login');
$this->get('/logout', 'LoginControllerInstance:Logout'); $this->get('/logout', 'LoginControllerInstance:Logout');
// Generic entity interaction
$this->get('/userfields', '\Grocy\Controllers\GenericEntityController:UserfieldsList');
$this->get('/userfield/{userfieldId}', '\Grocy\Controllers\GenericEntityController:UserfieldEditForm');
// User routes // User routes
$this->get('/users', '\Grocy\Controllers\UsersController:UsersList'); $this->get('/users', '\Grocy\Controllers\UsersController:UsersList');
$this->get('/user/{userId}', '\Grocy\Controllers\UsersController:UserEditForm'); $this->get('/user/{userId}', '\Grocy\Controllers\UsersController:UserEditForm');
@@ -119,6 +123,8 @@ $app->group('/api', function()
$this->post('/objects/{entity}', '\Grocy\Controllers\GenericEntityApiController:AddObject'); $this->post('/objects/{entity}', '\Grocy\Controllers\GenericEntityApiController:AddObject');
$this->put('/objects/{entity}/{objectId}', '\Grocy\Controllers\GenericEntityApiController:EditObject'); $this->put('/objects/{entity}/{objectId}', '\Grocy\Controllers\GenericEntityApiController:EditObject');
$this->delete('/objects/{entity}/{objectId}', '\Grocy\Controllers\GenericEntityApiController:DeleteObject'); $this->delete('/objects/{entity}/{objectId}', '\Grocy\Controllers\GenericEntityApiController:DeleteObject');
$this->get('/userfields/{entity}/{objectId}', '\Grocy\Controllers\GenericEntityApiController:GetUserfields');
$this->put('/userfields/{entity}/{objectId}', '\Grocy\Controllers\GenericEntityApiController:SetUserfields');
// Files // Files
$this->put('/files/{group}/{fileName}', '\Grocy\Controllers\FilesApiController:UploadFile'); $this->put('/files/{group}/{fileName}', '\Grocy\Controllers\FilesApiController:UploadFile');

View File

@@ -28,6 +28,7 @@ class LocalizationService
'strings.php', 'strings.php',
'stock_transaction_types.php', 'stock_transaction_types.php',
'chore_types.php', 'chore_types.php',
'userfield_types.php',
'component_translations.php', 'component_translations.php',
'demo_data.php' 'demo_data.php'
); );

View File

@@ -0,0 +1,121 @@
<?php
namespace Grocy\Services;
class UserfieldsService extends BaseService
{
const USERFIELD_TYPE_SINGLE_LINE_TEXT = 'text-single-line';
const USERFIELD_TYPE_SINGLE_MULTILINE_TEXT = 'text-multi-line';
const USERFIELD_TYPE_INTEGRAL_NUMBER = 'number-integral';
const USERFIELD_TYPE_DECIMAL_NUMBER = 'number-decimal';
const USERFIELD_TYPE_DATE = 'date';
const USERFIELD_TYPE_DATETIME = 'datetime';
const USERFIELD_TYPE_CHECKBOX = 'checkbox';
public function __construct()
{
parent::__construct();
$this->OpenApiSpec = json_decode(file_get_contents(__DIR__ . '/../grocy.openapi.json'));
}
protected $OpenApiSpec;
public function GetFields($entity)
{
if (!$this->IsValidEntity($entity))
{
throw new \Exception('Entity does not exist or is not exposed');
}
return $this->Database->userfields()->where('entity', $entity)->orderBy('name')->fetchAll();
}
public function GetField($fieldId)
{
return $this->Database->userfields($fieldId);
}
public function GetAllFields()
{
return $this->Database->userfields()->orderBy('name')->fetchAll();
}
public function GetValues($entity, $objectId)
{
if (!$this->IsValidEntity($entity))
{
throw new \Exception('Entity does not exist or is not exposed');
}
$userfields = $this->Database->userfield_values_resolved()->where('entity = :1 AND object_id = :2', $entity, $objectId)->orderBy('name')->fetchAll();
$userfieldKeyValuePairs = array();
foreach ($userfields as $userfield)
{
$userfieldKeyValuePairs[$userfield->name] = $userfield->value;
}
return $userfieldKeyValuePairs;
}
public function GetAllValues($entity)
{
if (!$this->IsValidEntity($entity))
{
throw new \Exception('Entity does not exist or is not exposed');
}
return $this->Database->userfield_values_resolved()->where('entity', $entity)->orderBy('name')->fetchAll();
}
public function SetValues($entity, $objectId, $userfields)
{
if (!$this->IsValidEntity($entity))
{
throw new \Exception('Entity does not exist or is not exposed');
}
foreach ($userfields as $key => $value)
{
$fieldRow = $this->Database->userfields()->where('entity = :1 AND name = :2', $entity, $key)->fetch();
if ($fieldRow === null)
{
throw new \Exception("Field $key is not a valid userfield of the given entity");
}
$fieldId = $fieldRow->id;
$alreadyExistingEntry = $this->Database->userfield_values()->where('field_id = :1 AND object_id = :2', $fieldId, $objectId)->fetch();
if ($alreadyExistingEntry) // Update
{
$alreadyExistingEntry->update(array(
'value' => $value
));
}
else // Insert
{
$newRow = $this->Database->userfield_values()->createRow(array(
'field_id' => $fieldId,
'object_id' => $objectId,
'value' => $value
));
$newRow->save();
}
}
}
public function GetEntities()
{
return $this->OpenApiSpec->components->internalSchemas->ExposedEntity->enum;
}
public function GetFieldTypes()
{
return GetClassConstants('\Grocy\Services\UserfieldsService');
}
private function IsValidEntity($entity)
{
return in_array($entity, $this->OpenApiSpec->components->internalSchemas->ExposedEntity->enum);
}
}

View File

@@ -12,6 +12,9 @@
<a class="btn btn-outline-dark" href="{{ $U('/battery/new') }}"> <a class="btn btn-outline-dark" href="{{ $U('/battery/new') }}">
<i class="fas fa-plus"></i>&nbsp;{{ $L('Add') }} <i class="fas fa-plus"></i>&nbsp;{{ $L('Add') }}
</a> </a>
<a class="btn btn-outline-secondary" href="{{ $U('/userfields?entity=batteries') }}">
<i class="fas fa-sliders-h"></i>&nbsp;{{ $L('Configure userfields') }}
</a>
</h1> </h1>
</div> </div>
</div> </div>
@@ -33,6 +36,11 @@
<th>{{ $L('Description') }}</th> <th>{{ $L('Description') }}</th>
<th>{{ $L('Used in') }}</th> <th>{{ $L('Used in') }}</th>
<th>{{ $L('Charge cycle interval (days)') }}</th> <th>{{ $L('Charge cycle interval (days)') }}</th>
@include('components.userfields_thead', array(
'userfields' => $userfields
))
</tr> </tr>
</thead> </thead>
<tbody class="d-none"> <tbody class="d-none">
@@ -58,6 +66,12 @@
<td> <td>
{{ $battery->charge_interval_days }} {{ $battery->charge_interval_days }}
</td> </td>
@include('components.userfields_tbody', array(
'userfields' => $userfields,
'userfieldValues' => FindAllObjectsInArrayByPropertyValue($userfieldValues, 'object_id', $battery->id)
))
</tr> </tr>
@endforeach @endforeach
</tbody> </tbody>

View File

@@ -46,6 +46,11 @@
<th>{{ $L('Last charged') }}</th> <th>{{ $L('Last charged') }}</th>
<th>{{ $L('Next planned charge cycle') }}</th> <th>{{ $L('Next planned charge cycle') }}</th>
<th class="d-none">Hidden status</th> <th class="d-none">Hidden status</th>
@include('components.userfields_thead', array(
'userfields' => $userfields
))
</tr> </tr>
</thead> </thead>
<tbody class="d-none"> <tbody class="d-none">
@@ -92,6 +97,12 @@
<td class="d-none"> <td class="d-none">
"@if(FindObjectInArrayByPropertyValue($batteries, 'id', $curentBatteryEntry->battery_id)->charge_interval_days > 0 && $curentBatteryEntry->next_estimated_charge_time < date('Y-m-d H:i:s')) overdue @elseif(FindObjectInArrayByPropertyValue($batteries, 'id', $curentBatteryEntry->battery_id)->charge_interval_days > 0 && $curentBatteryEntry->next_estimated_charge_time < date('Y-m-d H:i:s', strtotime("+$nextXDays days"))) duesoon @endif "@if(FindObjectInArrayByPropertyValue($batteries, 'id', $curentBatteryEntry->battery_id)->charge_interval_days > 0 && $curentBatteryEntry->next_estimated_charge_time < date('Y-m-d H:i:s')) overdue @elseif(FindObjectInArrayByPropertyValue($batteries, 'id', $curentBatteryEntry->battery_id)->charge_interval_days > 0 && $curentBatteryEntry->next_estimated_charge_time < date('Y-m-d H:i:s', strtotime("+$nextXDays days"))) duesoon @endif
</td> </td>
@include('components.userfields_tbody', array(
'userfields' => $userfields,
'userfieldValues' => FindAllObjectsInArrayByPropertyValue($userfieldValues, 'object_id', $curentBatteryEntry->battery_id)
))
</tr> </tr>
@endforeach @endforeach
</tbody> </tbody>

View File

@@ -47,6 +47,11 @@
'invalidFeedback' => $L('This cannot be negative') 'invalidFeedback' => $L('This cannot be negative')
)) ))
@include('components.userfieldsform', array(
'userfields' => $userfields,
'entity' => 'batteries'
))
<button id="save-battery-button" class="btn btn-success">{{ $L('Save') }}</button> <button id="save-battery-button" class="btn btn-success">{{ $L('Save') }}</button>
</form> </form>

View File

@@ -87,6 +87,11 @@
<input type="hidden" id="period_config" name="period_config" value="@if($mode == 'edit'){{ $chore->period_config }}@endif"> <input type="hidden" id="period_config" name="period_config" value="@if($mode == 'edit'){{ $chore->period_config }}@endif">
@include('components.userfieldsform', array(
'userfields' => $userfields,
'entity' => 'chores'
))
<button id="save-chore-button" class="btn btn-success">{{ $L('Save') }}</button> <button id="save-chore-button" class="btn btn-success">{{ $L('Save') }}</button>
</form> </form>

View File

@@ -12,6 +12,9 @@
<a class="btn btn-outline-dark" href="{{ $U('/chore/new') }}"> <a class="btn btn-outline-dark" href="{{ $U('/chore/new') }}">
<i class="fas fa-plus"></i>&nbsp;{{ $L('Add') }} <i class="fas fa-plus"></i>&nbsp;{{ $L('Add') }}
</a> </a>
<a class="btn btn-outline-secondary" href="{{ $U('/userfields?entity=chores') }}">
<i class="fas fa-sliders-h"></i>&nbsp;{{ $L('Configure userfields') }}
</a>
</h1> </h1>
</div> </div>
</div> </div>
@@ -32,6 +35,11 @@
<th>{{ $L('Name') }}</th> <th>{{ $L('Name') }}</th>
<th>{{ $L('Period type') }}</th> <th>{{ $L('Period type') }}</th>
<th>{{ $L('Description') }}</th> <th>{{ $L('Description') }}</th>
@include('components.userfields_thead', array(
'userfields' => $userfields
))
</tr> </tr>
</thead> </thead>
<tbody class="d-none"> <tbody class="d-none">
@@ -54,6 +62,12 @@
<td> <td>
{{ $chore->description }} {{ $chore->description }}
</td> </td>
@include('components.userfields_tbody', array(
'userfields' => $userfields,
'userfieldValues' => FindAllObjectsInArrayByPropertyValue($userfieldValues, 'object_id', $chore->id)
))
</tr> </tr>
@endforeach @endforeach
</tbody> </tbody>

View File

@@ -46,6 +46,11 @@
<th>{{ $L('Next estimated tracking') }}</th> <th>{{ $L('Next estimated tracking') }}</th>
<th>{{ $L('Last tracked') }}</th> <th>{{ $L('Last tracked') }}</th>
<th class="d-none">Hidden status</th> <th class="d-none">Hidden status</th>
@include('components.userfields_thead', array(
'userfields' => $userfields
))
</tr> </tr>
</thead> </thead>
<tbody class="d-none"> <tbody class="d-none">
@@ -92,6 +97,12 @@
<td class="d-none"> <td class="d-none">
@if(FindObjectInArrayByPropertyValue($chores, 'id', $curentChoreEntry->chore_id)->period_type !== \Grocy\Services\ChoresService::CHORE_TYPE_MANUALLY && $curentChoreEntry->next_estimated_execution_time < date('Y-m-d H:i:s')) overdue @elseif(FindObjectInArrayByPropertyValue($chores, 'id', $curentChoreEntry->chore_id)->period_type !== \Grocy\Services\ChoresService::CHORE_TYPE_MANUALLY && $curentChoreEntry->next_estimated_execution_time < date('Y-m-d H:i:s', strtotime("+$nextXDays days"))) duesoon @endif @if(FindObjectInArrayByPropertyValue($chores, 'id', $curentChoreEntry->chore_id)->period_type !== \Grocy\Services\ChoresService::CHORE_TYPE_MANUALLY && $curentChoreEntry->next_estimated_execution_time < date('Y-m-d H:i:s')) overdue @elseif(FindObjectInArrayByPropertyValue($chores, 'id', $curentChoreEntry->chore_id)->period_type !== \Grocy\Services\ChoresService::CHORE_TYPE_MANUALLY && $curentChoreEntry->next_estimated_execution_time < date('Y-m-d H:i:s', strtotime("+$nextXDays days"))) duesoon @endif
</td> </td>
@include('components.userfields_tbody', array(
'userfields' => $userfields,
'userfieldValues' => FindAllObjectsInArrayByPropertyValue($userfieldValues, 'object_id', $curentChoreEntry->chore_id)
))
</tr> </tr>
@endforeach @endforeach
</tbody> </tbody>

View File

@@ -0,0 +1,16 @@
@push('componentScripts')
<script src="{{ $U('/viewjs/components/userfieldsform.js', true) }}?v={{ $version }}"></script>
@endpush
@if(count($userfields) > 0)
@foreach($userfields as $userfield)
@if($userfield->show_as_column_in_tables == 1)
@php $userfieldObject = FindObjectInArrayByPropertyValue($userfieldValues, 'name', $userfield->name) @endphp
<td>@if($userfieldObject !== null){{ $userfieldObject->value }}@endif</td>
@endif
@endforeach
@endif

View File

@@ -0,0 +1,15 @@
@push('componentScripts')
<script src="{{ $U('/viewjs/components/userfieldsform.js', true) }}?v={{ $version }}"></script>
@endpush
@if(count($userfields) > 0)
@foreach($userfields as $userfield)
@if($userfield->show_as_column_in_tables == 1)
<th>{{ $userfield->name }}</th>
@endif
@endforeach
@endif

View File

@@ -0,0 +1,32 @@
@push('componentScripts')
<script src="{{ $U('/viewjs/components/userfieldsform.js', true) }}?v={{ $version }}"></script>
@endpush
@if(count($userfields) > 0)
<div id="userfields-form" data-entity="{{ $entity }}" class="border border-info p-2 mb-2" novalidate>
<h2 class="small">{{ $L('Userfields') }}</h2>
@foreach($userfields as $userfield)
@if($userfield->type == \Grocy\Services\UserfieldsService::USERFIELD_TYPE_SINGLE_LINE_TEXT)
<div class="form-group">
<label for="name">{{ $userfield->caption }}</label>
<input type="text" class="form-control userfield-input" id="{{ $userfield->name }}" value="">
</div>
@endif
@if($userfield->type == \Grocy\Services\UserfieldsService::USERFIELD_TYPE_CHECKBOX)
<div class="form-group">
<div class="form-check">
<input class="form-check-input userfield-input" type="checkbox" id="{{ $userfield->name }}" value="1">
<label class="form-check-label" for="{{ $userfield->name }}">{{ $userfield->caption }}</label>
</div>
</div>
@endif
@endforeach
</div>
@endif

View File

@@ -32,6 +32,11 @@
<textarea class="form-control" rows="2" id="description" name="description">@if($mode == 'edit'){{ $location->description }}@endif</textarea> <textarea class="form-control" rows="2" id="description" name="description">@if($mode == 'edit'){{ $location->description }}@endif</textarea>
</div> </div>
@include('components.userfieldsform', array(
'userfields' => $userfields,
'entity' => 'locations'
))
<button id="save-location-button" class="btn btn-success">{{ $L('Save') }}</button> <button id="save-location-button" class="btn btn-success">{{ $L('Save') }}</button>
</form> </form>

View File

@@ -12,6 +12,9 @@
<a class="btn btn-outline-dark" href="{{ $U('/location/new') }}"> <a class="btn btn-outline-dark" href="{{ $U('/location/new') }}">
<i class="fas fa-plus"></i>&nbsp;{{ $L('Add') }} <i class="fas fa-plus"></i>&nbsp;{{ $L('Add') }}
</a> </a>
<a class="btn btn-outline-secondary" href="{{ $U('/userfields?entity=locations') }}">
<i class="fas fa-sliders-h"></i>&nbsp;{{ $L('Configure userfields') }}
</a>
</h1> </h1>
</div> </div>
</div> </div>
@@ -31,6 +34,11 @@
<th class="border-right"></th> <th class="border-right"></th>
<th>{{ $L('Name') }}</th> <th>{{ $L('Name') }}</th>
<th>{{ $L('Description') }}</th> <th>{{ $L('Description') }}</th>
@include('components.userfields_thead', array(
'userfields' => $userfields
))
</tr> </tr>
</thead> </thead>
<tbody class="d-none"> <tbody class="d-none">
@@ -50,6 +58,12 @@
<td> <td>
{{ $location->description }} {{ $location->description }}
</td> </td>
@include('components.userfields_tbody', array(
'userfields' => $userfields,
'userfieldValues' => FindAllObjectsInArrayByPropertyValue($userfieldValues, 'object_id', $location->id)
))
</tr> </tr>
@endforeach @endforeach
</tbody> </tbody>

View File

@@ -187,8 +187,14 @@
</div> </div>
</div> </div>
@include('components.userfieldsform', array(
'userfields' => $userfields,
'entity' => 'products'
))
<button id="save-product-button" class="btn btn-success">{{ $L('Save') }}</button> <button id="save-product-button" class="btn btn-success">{{ $L('Save') }}</button>
</form> </form>
</div> </div>
<div class="col-lg-6 col-xs-12"> <div class="col-lg-6 col-xs-12">

View File

@@ -32,6 +32,11 @@
<textarea class="form-control" rows="2" id="description" name="description">@if($mode == 'edit'){{ $group->description }}@endif</textarea> <textarea class="form-control" rows="2" id="description" name="description">@if($mode == 'edit'){{ $group->description }}@endif</textarea>
</div> </div>
@include('components.userfieldsform', array(
'userfields' => $userfields,
'entity' => 'product_groups'
))
<button id="save-product-group-button" class="btn btn-success">{{ $L('Save') }}</button> <button id="save-product-group-button" class="btn btn-success">{{ $L('Save') }}</button>
</form> </form>

View File

@@ -12,6 +12,9 @@
<a class="btn btn-outline-dark" href="{{ $U('/productgroup/new') }}"> <a class="btn btn-outline-dark" href="{{ $U('/productgroup/new') }}">
<i class="fas fa-plus"></i>&nbsp;{{ $L('Add') }} <i class="fas fa-plus"></i>&nbsp;{{ $L('Add') }}
</a> </a>
<a class="btn btn-outline-secondary" href="{{ $U('/userfields?entity=product_groups') }}">
<i class="fas fa-sliders-h"></i>&nbsp;{{ $L('Configure userfields') }}
</a>
</h1> </h1>
</div> </div>
</div> </div>
@@ -32,6 +35,10 @@
<th>{{ $L('Name') }}</th> <th>{{ $L('Name') }}</th>
<th>{{ $L('Description') }}</th> <th>{{ $L('Description') }}</th>
<th>{{ $L('Product count') }}</th> <th>{{ $L('Product count') }}</th>
@include('components.userfields_thead', array(
'userfields' => $userfields
))
</tr> </tr>
</thead> </thead>
<tbody class="d-none"> <tbody class="d-none">
@@ -57,6 +64,12 @@
<i class="fas fa-external-link-alt"></i> <i class="fas fa-external-link-alt"></i>
</a> </a>
</td> </td>
@include('components.userfields_tbody', array(
'userfields' => $userfields,
'userfieldValues' => FindAllObjectsInArrayByPropertyValue($userfieldValues, 'object_id', $productGroup->id)
))
</tr> </tr>
@endforeach @endforeach
</tbody> </tbody>

View File

@@ -12,6 +12,9 @@
<a class="btn btn-outline-dark" href="{{ $U('/product/new') }}"> <a class="btn btn-outline-dark" href="{{ $U('/product/new') }}">
<i class="fas fa-plus"></i>&nbsp;{{ $L('Add') }} <i class="fas fa-plus"></i>&nbsp;{{ $L('Add') }}
</a> </a>
<a class="btn btn-outline-secondary" href="{{ $U('/userfields?entity=products') }}">
<i class="fas fa-sliders-h"></i>&nbsp;{{ $L('Configure userfields') }}
</a>
<a class="btn btn-outline-secondary" href="{{ $U('/stocksettings#productpresets') }}"> <a class="btn btn-outline-secondary" href="{{ $U('/stocksettings#productpresets') }}">
<i class="fas fa-sliders-h"></i>&nbsp;{{ $L('Presets for new products') }} <i class="fas fa-sliders-h"></i>&nbsp;{{ $L('Presets for new products') }}
</a> </a>
@@ -48,6 +51,11 @@
<th>{{ $L('QU stock') }}</th> <th>{{ $L('QU stock') }}</th>
<th>{{ $L('QU factor') }}</th> <th>{{ $L('QU factor') }}</th>
<th>{{ $L('Product group') }}</th> <th>{{ $L('Product group') }}</th>
@include('components.userfields_thead', array(
'userfields' => $userfields
))
</tr> </tr>
</thead> </thead>
<tbody class="d-none"> <tbody class="d-none">
@@ -82,6 +90,12 @@
<td> <td>
@if(!empty($product->product_group_id)) {{ FindObjectInArrayByPropertyValue($productGroups, 'id', $product->product_group_id)->name }} @endif @if(!empty($product->product_group_id)) {{ FindObjectInArrayByPropertyValue($productGroups, 'id', $product->product_group_id)->name }} @endif
</td> </td>
@include('components.userfields_tbody', array(
'userfields' => $userfields,
'userfieldValues' => FindAllObjectsInArrayByPropertyValue($userfieldValues, 'object_id', $product->id)
))
</tr> </tr>
@endforeach @endforeach
</tbody> </tbody>

View File

@@ -37,6 +37,11 @@
<textarea class="form-control" rows="2" id="description" name="description">@if($mode == 'edit'){{ $quantityunit->description }}@endif</textarea> <textarea class="form-control" rows="2" id="description" name="description">@if($mode == 'edit'){{ $quantityunit->description }}@endif</textarea>
</div> </div>
@include('components.userfieldsform', array(
'userfields' => $userfields,
'entity' => 'quantity_units'
))
<button id="save-quantityunit-button" class="btn btn-success">{{ $L('Save') }}</button> <button id="save-quantityunit-button" class="btn btn-success">{{ $L('Save') }}</button>
</form> </form>

View File

@@ -12,6 +12,9 @@
<a class="btn btn-outline-dark" href="{{ $U('/quantityunit/new') }}"> <a class="btn btn-outline-dark" href="{{ $U('/quantityunit/new') }}">
<i class="fas fa-plus"></i>&nbsp;{{ $L('Add') }} <i class="fas fa-plus"></i>&nbsp;{{ $L('Add') }}
</a> </a>
<a class="btn btn-outline-secondary" href="{{ $U('/userfields?entity=quantity_units') }}">
<i class="fas fa-sliders-h"></i>&nbsp;{{ $L('Configure userfields') }}
</a>
</h1> </h1>
</div> </div>
</div> </div>
@@ -31,6 +34,10 @@
<th class="border-right"></th> <th class="border-right"></th>
<th>{{ $L('Name') }}</th> <th>{{ $L('Name') }}</th>
<th>{{ $L('Description') }}</th> <th>{{ $L('Description') }}</th>
@include('components.userfields_thead', array(
'userfields' => $userfields
))
</tr> </tr>
</thead> </thead>
<tbody class="d-none"> <tbody class="d-none">
@@ -50,6 +57,12 @@
<td> <td>
{{ $quantityunit->description }} {{ $quantityunit->description }}
</td> </td>
@include('components.userfields_tbody', array(
'userfields' => $userfields,
'userfieldValues' => FindAllObjectsInArrayByPropertyValue($userfieldValues, 'object_id', $quantityunit->id)
))
</tr> </tr>
@endforeach @endforeach
</tbody> </tbody>

View File

@@ -77,6 +77,11 @@
<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>
@include('components.userfields_thead', array(
'userfields' => $userfields
))
</tr> </tr>
</thead> </thead>
<tbody class="d-none"> <tbody class="d-none">
@@ -143,6 +148,12 @@
<td class="d-none"> <td class="d-none">
@if($productGroup !== null){{ $productGroup->name }}@endif @if($productGroup !== null){{ $productGroup->name }}@endif
</td> </td>
@include('components.userfields_tbody', array(
'userfields' => $userfields,
'userfieldValues' => FindAllObjectsInArrayByPropertyValue($userfieldValues, 'object_id', $currentStockEntry->product_id)
))
</tr> </tr>
@endforeach @endforeach
</tbody> </tbody>

View File

@@ -12,6 +12,9 @@
<a class="btn btn-outline-dark" href="{{ $U('/taskcategory/new') }}"> <a class="btn btn-outline-dark" href="{{ $U('/taskcategory/new') }}">
<i class="fas fa-plus"></i>&nbsp;{{ $L('Add') }} <i class="fas fa-plus"></i>&nbsp;{{ $L('Add') }}
</a> </a>
<a class="btn btn-outline-secondary" href="{{ $U('/userfields?entity=task_categories') }}">
<i class="fas fa-sliders-h"></i>&nbsp;{{ $L('Configure userfields') }}
</a>
</h1> </h1>
</div> </div>
</div> </div>
@@ -31,6 +34,11 @@
<th class="border-right"></th> <th class="border-right"></th>
<th>{{ $L('Name') }}</th> <th>{{ $L('Name') }}</th>
<th>{{ $L('Description') }}</th> <th>{{ $L('Description') }}</th>
@include('components.userfields_thead', array(
'userfields' => $userfields
))
</tr> </tr>
</thead> </thead>
<tbody class="d-none"> <tbody class="d-none">
@@ -50,6 +58,12 @@
<td> <td>
{{ $taskCategory->description }} {{ $taskCategory->description }}
</td> </td>
@include('components.userfields_tbody', array(
'userfields' => $userfields,
'userfieldValues' => FindAllObjectsInArrayByPropertyValue($userfieldValues, 'object_id', $taskCategory->id)
))
</tr> </tr>
@endforeach @endforeach
</tbody> </tbody>

View File

@@ -32,6 +32,11 @@
<textarea class="form-control" rows="2" id="description" name="description">@if($mode == 'edit'){{ $category->description }}@endif</textarea> <textarea class="form-control" rows="2" id="description" name="description">@if($mode == 'edit'){{ $category->description }}@endif</textarea>
</div> </div>
@include('components.userfieldsform', array(
'userfields' => $userfields,
'entity' => 'task_categories'
))
<button id="save-task-category-button" class="btn btn-success">{{ $L('Save') }}</button> <button id="save-task-category-button" class="btn btn-success">{{ $L('Save') }}</button>
</form> </form>

View File

@@ -0,0 +1,71 @@
@extends('layout.default')
@if($mode == 'edit')
@section('title', $L('Edit userfield'))
@else
@section('title', $L('Create userfield'))
@endif
@section('viewJsName', 'userfieldform')
@section('content')
<div class="row">
<div class="col-lg-6 col-xs-12">
<h1>@yield('title')</h1>
<script>Grocy.EditMode = '{{ $mode }}';</script>
@if($mode == 'edit')
<script>Grocy.EditObjectId = {{ $userfield->id }};</script>
@endif
<form id="userfield-form" novalidate>
<div class="form-group">
<label for="entity">{{ $L('Entity') }}</label>
<select required class="form-control" id="entity" name="entity">
<option></option>
@foreach($entities as $entity)
<option @if($mode == 'edit' && $userfield->entity == $entity) selected="selected" @endif value="{{ $entity }}">{{ $entity }}</option>
@endforeach
</select>
<div class="invalid-feedback">{{ $L('A entity is required') }}</div>
</div>
<div class="form-group">
<label for="name">{{ $L('Name') }}</label>
<input type="text" class="form-control" required id="name" name="name" value="@if($mode == 'edit'){{ $userfield->name }}@endif">
<div class="invalid-feedback">{{ $L('A name is required') }}</div>
</div>
<div class="form-group">
<label for="name">{{ $L('Caption') }}</label>
<input type="text" class="form-control" required id="caption" name="caption" value="@if($mode == 'edit'){{ $userfield->caption }}@endif">
<div class="invalid-feedback">{{ $L('A caption is required') }}</div>
</div>
<div class="form-group">
<label for="entity">{{ $L('Type') }}</label>
<select required class="form-control" id="type" name="type">
<option></option>
@foreach($userfieldTypes as $userfieldType)
<option @if($mode == 'edit' && $userfield->type == $userfieldType) selected="selected" @endif value="{{ $userfieldType }}">{{ $L($userfieldType) }}</option>
@endforeach
</select>
<div class="invalid-feedback">{{ $L('A type is required') }}</div>
</div>
<div class="form-group">
<div class="form-check">
<input type="hidden" name="show_as_column_in_tables" value="0">
<input @if($mode == 'edit' && $userfield->show_as_column_in_tables == 1) checked @endif class="form-check-input" type="checkbox" id="show_as_column_in_tables" name="show_as_column_in_tables" value="1">
<label class="form-check-label" for="show_as_column_in_tables">{{ $L('Show as column in tables') }}</label>
</div>
</div>
<button id="save-userfield-button" class="btn btn-success">{{ $L('Save') }}</button>
</form>
</div>
</div>
@stop

View File

@@ -0,0 +1,76 @@
@extends('layout.default')
@section('title', $L('Userfields'))
@section('activeNav', 'userfields')
@section('viewJsName', 'userfields')
@section('content')
<div class="row">
<div class="col">
<h1>
@yield('title')
<a class="btn btn-outline-dark" href="{{ $U('/userfield/new') }}">
<i class="fas fa-plus"></i>&nbsp;{{ $L('Add') }}
</a>
</h1>
</div>
</div>
<div class="row mt-3">
<div class="col-xs-12 col-md-6 col-xl-3">
<label for="search">{{ $L('Search') }}</label> <i class="fas fa-search"></i>
<input type="text" class="form-control" id="search">
</div>
<div class="col-xs-12 col-md-6 col-xl-3">
<label for="entity-filter">{{ $L('Filter by entity') }}</label> <i class="fas fa-filter"></i>
<select class="form-control" id="entity-filter">
<option value="all">{{ $L('All') }}</option>
@foreach($entities as $entity)
<option value="{{ $entity }}">{{ $entity }}</option>
@endforeach
</select>
</div>
</div>
<div class="row">
<div class="col">
<table id="userfields-table" class="table table-sm table-striped dt-responsive">
<thead>
<tr>
<th class="border-right"></th>
<th>{{ $L('Entity') }}</th>
<th>{{ $L('Name') }}</th>
<th>{{ $L('Caption') }}</th>
<th>{{ $L('Type') }}</th>
</tr>
</thead>
<tbody class="d-none">
@foreach($userfields as $userfield)
<tr>
<td class="fit-content border-right">
<a class="btn btn-info btn-sm" href="{{ $U('/userfield/') }}{{ $userfield->id }}">
<i class="fas fa-edit"></i>
</a>
<a class="btn btn-danger btn-sm userfield-delete-button" href="#" data-userfield-id="{{ $userfield->id }}" data-userfield-name="{{ $userfield->name }}">
<i class="fas fa-trash"></i>
</a>
</td>
<td>
{{ $userfield->entity }}
</td>
<td>
{{ $userfield->name }}
</td>
<td>
{{ $userfield->caption }}
</td>
<td>
{{ $L($userfield->type) }}
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
@stop