Filtering of API-Results (#985)

* Add FilteredApiResponse

* Use FilteredApiResponse for Generic-Entity-Search

* Use FilteredApiResponse for Recipe-Fullfillment

* Use FilteredApiResponse for GetUsers

* Use FilteredApiResponse for current Tasks

* Use FilteredApiResponse for ProductStockEntries & ProductStockLocations

* Use FilteredApiResponse for current chores

* Use FilteredApiResponse for batteries-current

* Fix missing highlighting of "< X days"

* Keep to use existing views

Co-authored-by: Bernd Bestel <bernd@berrnd.de>
This commit is contained in:
fipwmaqzufheoxq92ebc 2020-09-01 19:59:40 +02:00 committed by GitHub
parent 60f3d900e8
commit 32a4f81f62
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 197 additions and 57 deletions

View File

@ -2,10 +2,16 @@
namespace Grocy\Controllers;
use LessQL\Result;
class BaseApiController extends BaseController
{
protected $OpenApiSpec = null;
const PATTERN_FIELD = '[A-Za-z_][A-Za-z0-9_]+';
const PATTERN_OPERATOR = '!?(=|~|<|>|(>=)|(<=))';
const PATTERN_VALUE = '[A-Za-z_0-9.]+';
public function __construct(\DI\Container $container)
{
parent::__construct($container);
@ -29,6 +35,68 @@ class BaseApiController extends BaseController
]);
}
public function FilteredApiResponse(\Psr\Http\Message\ResponseInterface $response, Result $data, array $query)
{
$data = $this->queryData($data, $query);
return $this->ApiResponse($response, $data);
}
protected function queryData(Result $data, array $query)
{
if (isset($query['query']))
$data = $this->filter($data, $query['query']);
if (isset($query['limit']))
$data = $data->limit(intval($query['limit']), intval($query['offset'] ?? 0));
if (isset($query['order']))
$data = $data->orderBy($query['order']);
return $data;
}
protected function filter(Result $data, array $query): Result
{
foreach ($query as $q) {
$matches = array();
preg_match('/(?P<field>' . self::PATTERN_FIELD . ')'
. '(?P<op>' . self::PATTERN_OPERATOR . ')'
. '(?P<value>' . self::PATTERN_VALUE . ')/',
$q, $matches
);
error_log(var_export($matches, true));
switch ($matches['op']) {
case '=':
$data = $data->where($matches['field'], $matches['value']);
break;
case '!=':
$data = $data->whereNot($matches['field'], $matches['value']);
break;
case '~':
$data = $data->where($matches['field'] . ' LIKE ?', '%' . $matches['value'] . '%');
break;
case '!~':
$data = $data->where($matches['field'] . ' NOT LIKE ?', '%' . $matches['value'] . '%');
break;
case '!>=':
case '<':
$data = $data->where($matches['field'] . ' < ?', $matches['value']);
break;
case '!<=':
case '>':
$data = $data->where($matches['field'] . ' > ?', $matches['value']);
break;
case '!<':
case '>=':
$data = $data->where($matches['field'] . ' >= ?', $matches['value']);
break;
case '!>':
case '<=':
$data = $data->where($matches['field'] . ' <= ?', $matches['value']);
break;
}
}
return $data;
}
protected function getOpenApispec()
{
if ($this->OpenApiSpec == null)

View File

@ -21,7 +21,7 @@ class BatteriesApiController extends BaseApiController
public function Current(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{
return $this->ApiResponse($response, $this->getBatteriesService()->GetCurrent());
return $this->FilteredApiResponse($response, $this->getBatteriesService()->GetCurrent(), $request->getQueryParams());
}
public function TrackChargeCycle(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)

View File

@ -58,7 +58,7 @@ class ChoresApiController extends BaseApiController
public function Current(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{
return $this->ApiResponse($response, $this->getChoresService()->GetCurrent());
return $this->FilteredApiResponse($response, $this->getChoresService()->GetCurrent(), $request->getQueryParams());
}
public function TrackChoreExecution(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)

View File

@ -3,6 +3,7 @@
namespace Grocy\Controllers;
use Grocy\Controllers\Users\User;
use Slim\Exception\HttpBadRequestException;
class GenericEntityApiController extends BaseApiController
{
@ -178,12 +179,13 @@ class GenericEntityApiController extends BaseApiController
{
try
{
return $this->ApiResponse($response, $this->getDatabase()->{$args['entity']}
()->where('name LIKE ?', '%' . $args['searchString'] . '%'));
return $this->FilteredApiResponse($response, $this->getDatabase()->{$args['entity']}
()->where('name LIKE ?', '%' . $args['searchString'] . '%'), $request->getQueryParams());
}
catch (\PDOException $ex)
{
return $this->GenericErrorResponse($response, 'The given entity has no field "name"');
throw new HttpBadRequestException($request, $ex->getMessage(), $ex);
//return $this->GenericErrorResponse($response, 'The given entity has no field "name"', $ex);
}
}

View File

@ -44,7 +44,7 @@ class RecipesApiController extends BaseApiController
{
if (!isset($args['recipeId']))
{
return $this->ApiResponse($response, $this->getRecipesService()->GetRecipesResolved());
return $this->FilteredApiResponse($response, $this->getRecipesService()->GetRecipesResolved(), $request->getQueryParams());
}
$recipeResolved = FindObjectInArrayByPropertyValue($this->getRecipesService()->GetRecipesResolved(), 'recipe_id', $args['recipeId']);

View File

@ -552,12 +552,12 @@ class StockApiController extends BaseApiController
$allowSubproductSubstitution = true;
}
return $this->ApiResponse($response, $this->getStockService()->GetProductStockEntries($args['productId'], false, $allowSubproductSubstitution));
return $this->FilteredApiResponse($response, $this->getStockService()->GetProductStockEntries($args['productId'], false, $allowSubproductSubstitution, false), $request->getQueryParams());
}
public function ProductStockLocations(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{
return $this->ApiResponse($response, $this->getStockService()->GetProductStockLocations($args['productId']));
return $this->FilteredApiResponse($response, $this->getStockService()->GetProductStockLocations($args['productId']), $request->getQueryParams());
}
public function RemoveProductFromShoppingList(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)

View File

@ -8,7 +8,7 @@ class TasksApiController extends BaseApiController
{
public function Current(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{
return $this->ApiResponse($response, $this->getTasksService()->GetCurrent());
return $this->FilteredApiResponse($response, $this->getTasksService()->GetCurrent(), $request->getQueryParams());
}
public function MarkTaskAsCompleted(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)

View File

@ -123,7 +123,7 @@ class UsersApiController extends BaseApiController
User::checkPermission($request, User::PERMISSION_USERS_READ);
try
{
return $this->ApiResponse($response, $this->getUsersService()->GetUsersAsDto());
return $this->FilteredApiResponse($response, $this->getUsersService()->GetUsersAsDto(), $request->getQueryParams());
}
catch (\Exception $ex)
{

92
migrations/0114.sql Normal file
View File

@ -0,0 +1,92 @@
CREATE VIEW users_dto
AS
SELECT
id,
username,
first_name,
last_name,
row_created_timestamp,
(CASE
WHEN IFNULL(first_name, '') = '' AND IFNULL(last_name, '') != '' THEN last_name
WHEN IFNULL(last_name, '') = '' AND IFNULL(first_name, '') != '' THEN first_name
WHEN IFNULL(last_name, '') != '' AND IFNULL(first_name, '') != '' THEN first_name + ' ' + last_name
ELSE username
END
) AS display_name
FROM users;
DROP VIEW chores_current;
CREATE VIEW chores_current
AS
SELECT
x.chore_id AS id, -- Dummy, LessQL needs an id column
x.chore_id,
x.chore_name,
x.last_tracked_time,
CASE WHEN x.rollover = 1 AND DATETIME('now', 'localtime') > x.next_estimated_execution_time THEN
DATETIME(STRFTIME('%Y-%m-%d', DATETIME('now', 'localtime')) || ' ' || STRFTIME('%H:%M:%S', x.next_estimated_execution_time))
ELSE
x.next_estimated_execution_time
END AS next_estimated_execution_time,
x.track_date_only,
x.next_execution_assigned_to_user_id
FROM (
SELECT
h.id AS chore_id,
h.name AS chore_name,
MAX(l.tracked_time) AS last_tracked_time,
CASE h.period_type
WHEN 'manually' THEN '2999-12-31 23:59:59'
WHEN 'dynamic-regular' THEN DATETIME(MAX(l.tracked_time), '+' || CAST(h.period_days AS TEXT) || ' day')
WHEN 'daily' THEN DATETIME(IFNULL(MAX(l.tracked_time), DATETIME('now', 'localtime')), '+' || CAST(h.period_interval AS TEXT) || ' day')
WHEN 'weekly' THEN (
SELECT next
FROM (
SELECT 'sunday' AS day, DATETIME(COALESCE((SELECT tracked_time FROM chores_log WHERE chore_id = h.id ORDER BY tracked_time DESC LIMIT 1), DATETIME('now', 'localtime')), '1 days', '+' || CAST((h.period_interval - 1) * 7 AS TEXT) || ' days', 'weekday 0') AS next
UNION
SELECT 'monday' AS day, DATETIME(COALESCE((SELECT tracked_time FROM chores_log WHERE chore_id = h.id ORDER BY tracked_time DESC LIMIT 1), DATETIME('now', 'localtime')), '1 days', '+' || CAST((h.period_interval - 1) * 7 AS TEXT) || ' days', 'weekday 1') AS next
UNION
SELECT 'tuesday' AS day, DATETIME(COALESCE((SELECT tracked_time FROM chores_log WHERE chore_id = h.id ORDER BY tracked_time DESC LIMIT 1), DATETIME('now', 'localtime')), '1 days', '+' || CAST((h.period_interval - 1) * 7 AS TEXT) || ' days', 'weekday 2') AS next
UNION
SELECT 'wednesday' AS day, DATETIME(COALESCE((SELECT tracked_time FROM chores_log WHERE chore_id = h.id ORDER BY tracked_time DESC LIMIT 1), DATETIME('now', 'localtime')), '1 days', '+' || CAST((h.period_interval - 1) * 7 AS TEXT) || ' days', 'weekday 3') AS next
UNION
SELECT 'thursday' AS day, DATETIME(COALESCE((SELECT tracked_time FROM chores_log WHERE chore_id = h.id ORDER BY tracked_time DESC LIMIT 1), DATETIME('now', 'localtime')), '1 days', '+' || CAST((h.period_interval - 1) * 7 AS TEXT) || ' days', 'weekday 4') AS next
UNION
SELECT 'friday' AS day, DATETIME(COALESCE((SELECT tracked_time FROM chores_log WHERE chore_id = h.id ORDER BY tracked_time DESC LIMIT 1), DATETIME('now', 'localtime')), '1 days', '+' || CAST((h.period_interval - 1) * 7 AS TEXT) || ' days', 'weekday 5') AS next
UNION
SELECT 'saturday' AS day, DATETIME(COALESCE((SELECT tracked_time FROM chores_log WHERE chore_id = h.id ORDER BY tracked_time DESC LIMIT 1), DATETIME('now', 'localtime')), '1 days', '+' || CAST((h.period_interval - 1) * 7 AS TEXT) || ' days', 'weekday 6') AS next
)
WHERE INSTR(period_config, day) > 0
ORDER BY next
LIMIT 1
)
WHEN 'monthly' THEN DATETIME(IFNULL(MAX(l.tracked_time), DATETIME('now', 'localtime')), '+' || CAST(h.period_interval AS TEXT) || ' month', 'start of month', '+' || CAST(h.period_days - 1 AS TEXT) || ' day')
WHEN 'yearly' THEN DATETIME(IFNULL(MAX(l.tracked_time), DATETIME('now', 'localtime')), '+' || CAST(h.period_interval AS TEXT) || ' years')
END AS next_estimated_execution_time,
h.track_date_only,
h.rollover,
h.next_execution_assigned_to_user_id
FROM chores h
LEFT JOIN chores_log l
ON h.id = l.chore_id
AND l.undone = 0
GROUP BY h.id, h.name, h.period_days
) x;
DROP VIEW batteries_current;
CREATE VIEW batteries_current
AS
SELECT
b.id, -- Dummy, LessQL needs an id column
b.id AS battery_id,
MAX(l.tracked_time) AS last_tracked_time,
CASE WHEN b.charge_interval_days = 0
THEN '2999-12-31 23:59:59'
ELSE datetime(MAX(l.tracked_time), '+' || CAST(b.charge_interval_days AS TEXT) || ' day')
END AS next_estimated_charge_time
FROM batteries b
LEFT JOIN battery_charge_cycles l
ON b.id = l.battery_id
GROUP BY b.id, b.charge_interval_days;

View File

@ -153,7 +153,7 @@ $app->group('/api', function(RouteCollectorProxy $group)
// Generic entity interaction
$group->get('/objects/{entity}', '\Grocy\Controllers\GenericEntityApiController:GetObjects');
$group->get('/objects/{entity}/{objectId}', '\Grocy\Controllers\GenericEntityApiController:GetObject');
$group->get('/objects/{entity}/search/{searchString}', '\Grocy\Controllers\GenericEntityApiController:SearchObjects');
$group->get('/objects/{entity}/search/{searchString:.*}', '\Grocy\Controllers\GenericEntityApiController:SearchObjects');
$group->post('/objects/{entity}', '\Grocy\Controllers\GenericEntityApiController:AddObject');
$group->put('/objects/{entity}/{objectId}', '\Grocy\Controllers\GenericEntityApiController:EditObject');
$group->delete('/objects/{entity}/{objectId}', '\Grocy\Controllers\GenericEntityApiController:DeleteObject');

View File

@ -26,8 +26,7 @@ class BatteriesService extends BaseService
public function GetCurrent()
{
$sql = 'SELECT * from batteries_current';
return $this->getDatabaseService()->ExecuteDbQuery($sql)->fetchAll(\PDO::FETCH_OBJ);
return $this->getDatabase()->batteries_current();
}
public function TrackChargeCycle(int $batteryId, string $trackedTime)

View File

@ -152,8 +152,7 @@ class ChoresService extends BaseService
public function GetCurrent()
{
$sql = 'SELECT chores_current.*, chores.name AS chore_name from chores_current join chores on chores_current.chore_id = chores.id';
return $this->getDatabaseService()->ExecuteDbQuery($sql)->fetchAll(\PDO::FETCH_OBJ);
return $this->getDatabase()->chores_current();
}
public function TrackChore(int $choreId, string $trackedTime, $doneBy = GROCY_USER_ID)

View File

@ -4,6 +4,8 @@ namespace Grocy\Services;
#use \Grocy\Services\StockService;
use LessQL\Result;
class RecipesService extends BaseService
{
const RECIPE_TYPE_MEALPLAN_DAY = 'mealplan-day';
@ -82,10 +84,9 @@ class RecipesService extends BaseService
return $this->getDataBaseService()->ExecuteDbQuery($sql)->fetchAll(\PDO::FETCH_OBJ);
}
public function GetRecipesResolved()
public function GetRecipesResolved(): Result
{
$sql = 'SELECT * FROM recipes_resolved';
return $this->getDataBaseService()->ExecuteDbQuery($sql)->fetchAll(\PDO::FETCH_OBJ);
return $this->getDatabase()->recipes_resolved();
}
public function __construct()

View File

@ -603,7 +603,7 @@ class StockService extends BaseService
return $returnData;
}
public function GetProductStockEntries($productId, $excludeOpened = false, $allowSubproductSubstitution = false)
public function GetProductStockEntries($productId, $excludeOpened = false, $allowSubproductSubstitution = false, $ordered = true)
{
// In order of next use:
// First expiring first, then first in first out
@ -621,8 +621,10 @@ class StockService extends BaseService
{
$sqlWhereAndOpen = 'AND open = 0';
}
return $this->getDatabase()->stock()->where($sqlWhereProductId . ' ' . $sqlWhereAndOpen, $productId)->orderBy('best_before_date', 'ASC')->orderBy('purchased_date', 'ASC')->fetchAll();
$result = $this->getDatabase()->stock()->where($sqlWhereProductId . ' ' . $sqlWhereAndOpen, $productId);
if ($ordered)
return $result->orderBy('best_before_date', 'ASC')->orderBy('purchased_date', 'ASC');
return $result;
}
public function GetProductStockEntriesForLocation($productId, $locationId, $excludeOpened = false, $allowSubproductSubstitution = false)
@ -633,7 +635,7 @@ class StockService extends BaseService
public function GetProductStockLocations($productId)
{
return $this->getDatabase()->stock_current_locations()->where('product_id', $productId)->fetchAll();
return $this->getDatabase()->stock_current_locations()->where('product_id', $productId);
}
public function GetStockEntry($entryId)

View File

@ -4,10 +4,9 @@ namespace Grocy\Services;
class TasksService extends BaseService
{
public function GetCurrent()
public function GetCurrent(): \LessQL\Result
{
$sql = 'SELECT * from tasks_current';
return $this->getDatabaseService()->ExecuteDbQuery($sql)->fetchAll(\PDO::FETCH_OBJ);
return $this->getDatabase()->tasks_current();
}
public function MarkTaskAsCompleted($taskId, $doneTime)

View File

@ -81,19 +81,9 @@ class UsersService extends BaseService
return array_merge($GROCY_DEFAULT_USER_SETTINGS, $settings);
}
public function GetUsersAsDto()
public function GetUsersAsDto(): \LessQL\Result
{
$users = $this->getDatabase()->users();
$returnUsers = [];
foreach ($users as $user)
{
unset($user->password);
$user->display_name = GetUserDisplayName($user);
$returnUsers[] = $user;
}
return $returnUsers;
return $this->getDatabase()->users_dto();
}
public function SetUserSetting($userId, $settingKey, $settingValue)

View File

@ -80,9 +80,7 @@
<tbody class="d-none">
@foreach($current as $currentBatteryEntry)
<tr id="battery-{{ $currentBatteryEntry->battery_id }}-row"
class="@if(FindObjectInArrayByPropertyValue($batteries, 'id', $currentBatteryEntry->battery_id)->charge_interval_days > 0 && $currentBatteryEntry->next_estimated_charge_time < date('Y-m-d H:i:s')) table-danger @elseif(FindObjectInArrayByPropertyValue($batteries, 'id', $currentBatteryEntry->battery_id)->charge_interval_days > 0 && $currentBatteryEntry->next_estimated_charge_time < date('Y-m-d H:i:s', strtotime("
+$nextXDays
days")))
class="@if(FindObjectInArrayByPropertyValue($batteries, 'id', $currentBatteryEntry->battery_id)->charge_interval_days > 0 && $currentBatteryEntry->next_estimated_charge_time < date('Y-m-d H:i:s')) table-danger @elseif(FindObjectInArrayByPropertyValue($batteries, 'id', $currentBatteryEntry->battery_id)->charge_interval_days > 0 && $currentBatteryEntry->next_estimated_charge_time < date('Y-m-d H:i:s', strtotime("+$nextXDays days")))
table-warning
@endif">
<td class="fit-content border-right">
@ -149,8 +147,7 @@
,
$currentBatteryEntry->battery_id)->charge_interval_days > 0 && $currentBatteryEntry->next_estimated_charge_time < date('Y-m-d
H:i:s',
strtotime("+$nextXDays
days")))
strtotime("+$nextXDays days")))
duesoon
@endif
</td>

View File

@ -110,9 +110,7 @@
<tbody class="d-none">
@foreach($currentChores as $curentChoreEntry)
<tr id="chore-{{ $curentChoreEntry->chore_id }}-row"
class="@if(FindObjectInArrayByPropertyValue($chores, 'id', $curentChoreEntry->chore_id)->period_type !== \Grocy\Services\ChoresService::CHORE_PERIOD_TYPE_MANUALLY && $curentChoreEntry->next_estimated_execution_time < date('Y-m-d H:i:s')) table-danger @elseif(FindObjectInArrayByPropertyValue($chores, 'id', $curentChoreEntry->chore_id)->period_type !== \Grocy\Services\ChoresService::CHORE_PERIOD_TYPE_MANUALLY && $curentChoreEntry->next_estimated_execution_time < date('Y-m-d H:i:s', strtotime("
+$nextXDays
days")))
class="@if(FindObjectInArrayByPropertyValue($chores, 'id', $curentChoreEntry->chore_id)->period_type !== \Grocy\Services\ChoresService::CHORE_PERIOD_TYPE_MANUALLY && $curentChoreEntry->next_estimated_execution_time < date('Y-m-d H:i:s')) table-danger @elseif(FindObjectInArrayByPropertyValue($chores, 'id', $curentChoreEntry->chore_id)->period_type !== \Grocy\Services\ChoresService::CHORE_PERIOD_TYPE_MANUALLY && $curentChoreEntry->next_estimated_execution_time < date('Y-m-d H:i:s', strtotime("+$nextXDays days")))
table-warning
@endif">
<td class="fit-content border-right">
@ -191,8 +189,7 @@
,
$curentChoreEntry->chore_id)->period_type !== \Grocy\Services\ChoresService::CHORE_PERIOD_TYPE_MANUALLY && $curentChoreEntry->next_estimated_execution_time < date('Y-m-d
H:i:s',
strtotime("+$nextXDays
days")))
strtotime("+$nextXDays days")))
duesoon
@endif
</td>

View File

@ -54,9 +54,7 @@
<tbody class="d-none">
@foreach($stockEntries as $stockEntry)
<tr id="stock-{{ $stockEntry->id }}-row"
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) table-danger @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) table-danger @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) table-warning @endif">
<td class="fit-content border-right">

View File

@ -142,9 +142,7 @@
<tbody class="d-none">
@foreach($currentStock as $currentStockEntry)
<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) table-danger @elseif(GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING && $currentStockEntry->best_before_date < date('Y-m-d 23:59:59', strtotime("
+$nextXDays
days"))
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) table-danger @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">
@ -324,8 +322,7 @@
&&
$currentStockEntry->amount > 0) expired @elseif($currentStockEntry->best_before_date < date('Y-m-d
23:59:59',
strtotime("+$nextXDays
days"))
strtotime("+$nextXDays days"))
&&
$currentStockEntry->amount > 0) expiring @endif @if($currentStockEntry->product_missing) belowminstockamount @endif
</td>

View File

@ -156,8 +156,7 @@
@if($task->done == 1) text-muted @endif @if(!empty($task->due_date) && $task->due_date < date('Y-m-d'))
overdue
@elseif(!empty($task->due_date) && $task->due_date < date('Y-m-d',
strtotime("+$nextXDays
days")))
strtotime("+$nextXDays days")))
duesoon
@endif
</td>