mirror of https://github.com/kcal-app/kcal.git
Merge remote-tracking branch 'origin/main' into demo
This commit is contained in:
commit
7aec69651d
|
|
@ -3,7 +3,7 @@ APP_NAME=kcal
|
|||
APP_ENV=local
|
||||
APP_KEY=
|
||||
APP_DEBUG=true
|
||||
APP_URL=http://kcal.test
|
||||
APP_URL=http://127.0.0.1
|
||||
APP_PORT=8080
|
||||
APP_SERVICE=app
|
||||
APP_TIMEZONE=UTC
|
||||
|
|
@ -12,7 +12,7 @@ LOG_CHANNEL=stack
|
|||
LOG_LEVEL=debug
|
||||
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=mysql
|
||||
DB_HOST=db
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=kcal
|
||||
DB_USERNAME=kcal
|
||||
|
|
|
|||
|
|
@ -2,13 +2,12 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\UpdateFoodRequest;
|
||||
use App\Models\Food;
|
||||
use App\Rules\StringIsDecimalOrFraction;
|
||||
use App\Support\Number;
|
||||
use App\Support\Nutrients;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
|
|
@ -35,7 +34,7 @@ class FoodController extends Controller
|
|||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
public function store(UpdateFoodRequest $request): RedirectResponse
|
||||
{
|
||||
return $this->update($request, new Food());
|
||||
}
|
||||
|
|
@ -72,25 +71,10 @@ class FoodController extends Controller
|
|||
/**
|
||||
* Update the specified resource in storage.
|
||||
*/
|
||||
public function update(Request $request, Food $food): RedirectResponse
|
||||
public function update(UpdateFoodRequest $request, Food $food): RedirectResponse
|
||||
{
|
||||
$attributes = $request->validate([
|
||||
'name' => 'required|string',
|
||||
'detail' => 'nullable|string',
|
||||
'brand' => 'nullable|string',
|
||||
'source' => 'nullable|string',
|
||||
'notes' => 'nullable|string',
|
||||
'serving_size' => ['required', new StringIsDecimalOrFraction],
|
||||
'serving_unit' => 'nullable|string',
|
||||
'serving_unit_name' => 'nullable|string',
|
||||
'serving_weight' => 'required|numeric',
|
||||
'calories' => 'nullable|numeric',
|
||||
'fat' => 'nullable|numeric',
|
||||
'cholesterol' => 'nullable|numeric',
|
||||
'sodium' => 'nullable|numeric',
|
||||
'carbohydrates' => 'nullable|numeric',
|
||||
'protein' => 'nullable|numeric',
|
||||
]);
|
||||
$attributes = $request->validated();
|
||||
|
||||
$attributes['serving_size'] = Number::floatFromString($attributes['serving_size']);
|
||||
$attributes['name'] = Str::lower($attributes['name']);
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\UpdateGoalRequest;
|
||||
use App\Models\Goal;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
|
|
@ -40,7 +41,7 @@ class GoalController extends Controller
|
|||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
public function store(UpdateGoalRequest $request): RedirectResponse
|
||||
{
|
||||
return $this->update($request, new Goal());
|
||||
}
|
||||
|
|
@ -70,15 +71,9 @@ class GoalController extends Controller
|
|||
/**
|
||||
* Update the specified resource in storage.
|
||||
*/
|
||||
public function update(Request $request, Goal $goal): RedirectResponse
|
||||
public function update(UpdateGoalRequest $request, Goal $goal): RedirectResponse
|
||||
{
|
||||
$attributes = $request->validate([
|
||||
'from' => ['nullable', 'date'],
|
||||
'to' => ['nullable', 'date'],
|
||||
'frequency' => ['required', 'string'],
|
||||
'name' => ['required', 'string'],
|
||||
'goal' => ['required', 'numeric'],
|
||||
]);
|
||||
$attributes = $request->validated();
|
||||
$goal->fill($attributes)->user()->associate(Auth::user());
|
||||
$goal->save();
|
||||
session()->flash('message', "Goal updated!");
|
||||
|
|
|
|||
|
|
@ -5,13 +5,11 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\StoreFromNutrientsJournalEntryRequest;
|
||||
use App\Http\Requests\StoreJournalEntryRequest;
|
||||
use App\Models\Food;
|
||||
use App\Models\JournalEntry;
|
||||
use App\Models\Recipe;
|
||||
use App\Rules\ArrayNotEmpty;
|
||||
use App\Rules\InArray;
|
||||
use App\Rules\StringIsDecimalOrFraction;
|
||||
use App\Rules\UsesIngredientTrait;
|
||||
use App\Support\ArrayFormat;
|
||||
use App\Support\Number;
|
||||
use App\Support\Nutrients;
|
||||
|
|
@ -85,6 +83,7 @@ class JournalEntryController extends Controller
|
|||
continue;
|
||||
}
|
||||
$ingredients[$key] = [
|
||||
'key' => $key,
|
||||
'date' => $old['date'][$key],
|
||||
'meal' => $old['meal'][$key],
|
||||
'amount' => $amount,
|
||||
|
|
@ -130,28 +129,9 @@ class JournalEntryController extends Controller
|
|||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
public function store(StoreJournalEntryRequest $request): RedirectResponse
|
||||
{
|
||||
$input = $request->validate([
|
||||
'ingredients.date' => ['required', 'array', new ArrayNotEmpty],
|
||||
'ingredients.date.*' => ['nullable', 'date', 'required_with:ingredients.id.*'],
|
||||
'ingredients.meal' => ['required', 'array', new ArrayNotEmpty],
|
||||
'ingredients.meal.*' => [
|
||||
'nullable',
|
||||
'string',
|
||||
'required_with:ingredients.id.*',
|
||||
new InArray(JournalEntry::meals()->pluck('value')->toArray())
|
||||
],
|
||||
'ingredients.amount' => ['required', 'array', new ArrayNotEmpty],
|
||||
'ingredients.amount.*' => ['required_with:ingredients.id.*', 'nullable', new StringIsDecimalOrFraction],
|
||||
'ingredients.unit' => ['required', 'array'],
|
||||
'ingredients.unit.*' => ['required_with:ingredients.id.*'],
|
||||
'ingredients.id' => ['required', 'array', new ArrayNotEmpty],
|
||||
'ingredients.id.*' => 'required_with:ingredients.amount.*|nullable',
|
||||
'ingredients.type' => ['required', 'array', new ArrayNotEmpty],
|
||||
'ingredients.type.*' => ['required_with:ingredients.id.*', 'nullable', new UsesIngredientTrait()],
|
||||
'group_entries' => ['nullable', 'boolean'],
|
||||
]);
|
||||
$input = $request->validated();
|
||||
|
||||
$ingredients = ArrayFormat::flipTwoDimensionalKeys($input['ingredients']);
|
||||
|
||||
|
|
@ -284,23 +264,8 @@ class JournalEntryController extends Controller
|
|||
/**
|
||||
* Store an entry from nutrients.
|
||||
*/
|
||||
public function storeFromNutrients(Request $request): RedirectResponse {
|
||||
$attributes = $request->validate([
|
||||
'date' => ['required', 'date'],
|
||||
'meal' => [
|
||||
'required',
|
||||
'string',
|
||||
new InArray(JournalEntry::meals()->pluck('value')->toArray())
|
||||
],
|
||||
'summary' => ['required', 'string'],
|
||||
'calories' => ['nullable', 'required_without_all:fat,cholesterol,sodium,carbohydrates,protein', 'numeric'],
|
||||
'fat' => ['nullable', 'required_without_all:calories,cholesterol,sodium,carbohydrates,protein', 'numeric'],
|
||||
'cholesterol' => ['nullable', 'required_without_all:calories,fat,sodium,carbohydrates,protein', 'numeric'],
|
||||
'sodium' => ['nullable', 'required_without_all:calories,fat,cholesterol,carbohydrates,protein', 'numeric'],
|
||||
'carbohydrates' => ['nullable', 'required_without_all:calories,fat,cholesterol,sodium,protein', 'numeric'],
|
||||
'protein' => ['nullable', 'required_without_all:calories,fat,cholesterol,sodium,carbohydrates', 'numeric'],
|
||||
]);
|
||||
|
||||
public function storeFromNutrients(StoreFromNutrientsJournalEntryRequest $request): RedirectResponse {
|
||||
$attributes = $request->validated();
|
||||
$entry = JournalEntry::make(array_filter($attributes))
|
||||
->user()->associate(Auth::user());
|
||||
$entry->save();
|
||||
|
|
|
|||
|
|
@ -2,19 +2,16 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\UpdateRecipeRequest;
|
||||
use App\Models\Food;
|
||||
use App\Models\IngredientAmount;
|
||||
use App\Models\Recipe;
|
||||
use App\Models\RecipeSeparator;
|
||||
use App\Models\RecipeStep;
|
||||
use App\Rules\ArrayNotEmpty;
|
||||
use App\Rules\StringIsDecimalOrFraction;
|
||||
use App\Rules\UsesIngredientTrait;
|
||||
use App\Support\Number;
|
||||
use App\Support\Nutrients;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
|
|
@ -44,12 +41,9 @@ class RecipeController extends Controller
|
|||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
public function store(UpdateRecipeRequest $request): RedirectResponse
|
||||
{
|
||||
return $this->update($request, new Recipe());
|
||||
}
|
||||
|
|
@ -96,18 +90,18 @@ class RecipeController extends Controller
|
|||
'ingredient_name' => $old['name'][$key],
|
||||
'detail' => $old['detail'][$key],
|
||||
];
|
||||
}
|
||||
|
||||
// Add supported units for the ingredient.
|
||||
$ingredient = NULL;
|
||||
if ($ingredients[$key]['ingredient_type'] === Food::class) {
|
||||
$ingredient = Food::whereId($ingredients[$key]['ingredient_id'])->first();
|
||||
}
|
||||
elseif ($ingredients[$key]['ingredient_type'] === Recipe::class) {
|
||||
$ingredient = Recipe::whereId($ingredients[$key]['ingredient_id'])->first();
|
||||
}
|
||||
if ($ingredient) {
|
||||
$ingredients[$key]['units_supported'] = $ingredient->units_supported;
|
||||
// Add supported units for the ingredient.
|
||||
$ingredient = NULL;
|
||||
if ($ingredients[$key]['ingredient_type'] === Food::class) {
|
||||
$ingredient = Food::whereId($ingredients[$key]['ingredient_id'])->first();
|
||||
}
|
||||
elseif ($ingredients[$key]['ingredient_type'] === Recipe::class) {
|
||||
$ingredient = Recipe::whereId($ingredients[$key]['ingredient_id'])->first();
|
||||
}
|
||||
if ($ingredient) {
|
||||
$ingredients[$key]['units_supported'] = $ingredient->units_supported;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
|
@ -187,50 +181,11 @@ class RecipeController extends Controller
|
|||
/**
|
||||
* Update the specified resource in storage.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \App\Models\Recipe $recipe
|
||||
*
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function update(Request $request, Recipe $recipe): RedirectResponse
|
||||
public function update(UpdateRecipeRequest $request, Recipe $recipe): RedirectResponse
|
||||
{
|
||||
$input = $request->validate([
|
||||
'name' => ['required', 'string'],
|
||||
'description' => ['nullable', 'string'],
|
||||
'description_delta' => ['nullable', 'string'],
|
||||
'image' => ['nullable', 'file', 'mimes:jpg,png,gif'],
|
||||
'remove_image' => ['nullable', 'boolean'],
|
||||
'servings' => ['required', 'numeric'],
|
||||
'time_prep' => ['nullable', 'numeric'],
|
||||
'time_cook' => ['nullable', 'numeric'],
|
||||
'weight' => ['nullable', 'numeric'],
|
||||
'source' => ['nullable', 'string'],
|
||||
'ingredients.amount' => ['required', 'array', new ArrayNotEmpty],
|
||||
'ingredients.amount.*' => ['required_with:ingredients.id.*', 'nullable', new StringIsDecimalOrFraction],
|
||||
'ingredients.unit' => ['required', 'array'],
|
||||
'ingredients.unit.*' => ['required_with:ingredients.id.*'],
|
||||
'ingredients.detail' => ['required', 'array'],
|
||||
'ingredients.detail.*' => ['nullable', 'string'],
|
||||
'ingredients.id' => ['required', 'array', new ArrayNotEmpty],
|
||||
'ingredients.id.*' => 'required_with:ingredients.amount.*|nullable',
|
||||
'ingredients.type' => ['required', 'array', new ArrayNotEmpty],
|
||||
'ingredients.type.*' => ['required_with:ingredients.id.*', 'nullable', new UsesIngredientTrait()],
|
||||
'ingredients.key' => ['nullable', 'array'],
|
||||
'ingredients.key.*' => ['nullable', 'int'],
|
||||
'ingredients.weight' => ['required', 'array', new ArrayNotEmpty],
|
||||
'ingredients.weight.*' => ['required', 'int'],
|
||||
'separators.key' => ['nullable', 'array'],
|
||||
'separators.key.*' => ['nullable', 'int'],
|
||||
'separators.weight' => ['nullable', 'array'],
|
||||
'separators.weight.*' => ['required', 'int'],
|
||||
'separators.text' => ['nullable', 'array'],
|
||||
'separators.text.*' => ['nullable', 'string'],
|
||||
'steps.step' => ['required', 'array', new ArrayNotEmpty],
|
||||
'steps.step.*' => ['nullable', 'string'],
|
||||
'steps.key' => ['nullable', 'array'],
|
||||
]);
|
||||
$input = $request->validated();
|
||||
|
||||
// Validate that no ingredients are recursive.
|
||||
// TODO: refactor as custom validator.
|
||||
|
|
@ -246,6 +201,7 @@ class RecipeController extends Controller
|
|||
'description_delta' => $input['description_delta'],
|
||||
'servings' => (int) $input['servings'],
|
||||
'weight' => $input['weight'],
|
||||
'volume' => Number::floatFromString($input['volume']),
|
||||
'time_prep' => (int) $input['time_prep'],
|
||||
'time_cook' => (int) $input['time_cook'],
|
||||
'source' => $input['source'],
|
||||
|
|
@ -285,7 +241,7 @@ class RecipeController extends Controller
|
|||
->usingFileName("{$recipe->slug}.{$file->extension()}")
|
||||
->toMediaCollection();
|
||||
}
|
||||
elseif (isset($input['remove_image']) && (bool) $input['remove_image']) {
|
||||
elseif (isset($input['remove_image']) && $input['remove_image']) {
|
||||
$recipe->clearMediaCollection();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Models\JournalEntry;
|
||||
use App\Rules\ArrayNotEmpty;
|
||||
use App\Rules\InArray;
|
||||
use App\Rules\StringIsPositiveDecimalOrFraction;
|
||||
use App\Rules\UsesIngredientTrait;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreFromNutrientsJournalEntryRequest extends FormRequest
|
||||
{
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'date' => ['required', 'date'],
|
||||
'meal' => [
|
||||
'required',
|
||||
'string',
|
||||
new InArray(JournalEntry::meals()->pluck('value')->toArray())
|
||||
],
|
||||
'summary' => ['required', 'string'],
|
||||
'calories' => ['nullable', 'numeric', 'min:0', 'required_without_all:fat,cholesterol,sodium,carbohydrates,protein'],
|
||||
'fat' => ['nullable', 'numeric', 'min:0', 'required_without_all:calories,cholesterol,sodium,carbohydrates,protein'],
|
||||
'cholesterol' => ['nullable', 'numeric', 'min:0', 'required_without_all:calories,fat,sodium,carbohydrates,protein'],
|
||||
'sodium' => ['nullable', 'numeric', 'min:0', 'required_without_all:calories,fat,cholesterol,carbohydrates,protein'],
|
||||
'carbohydrates' => ['nullable', 'numeric', 'min:0', 'required_without_all:calories,fat,cholesterol,sodium,protein'],
|
||||
'protein' => ['nullable', 'numeric', 'min:0', 'required_without_all:calories,fat,cholesterol,sodium,carbohydrates'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Models\JournalEntry;
|
||||
use App\Rules\ArrayNotEmpty;
|
||||
use App\Rules\InArray;
|
||||
use App\Rules\StringIsPositiveDecimalOrFraction;
|
||||
use App\Rules\UsesIngredientTrait;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreJournalEntryRequest extends FormRequest
|
||||
{
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'ingredients.date' => ['required', 'array', new ArrayNotEmpty],
|
||||
'ingredients.date.*' => ['nullable', 'date', 'required_with:ingredients.id.*'],
|
||||
'ingredients.meal' => ['required', 'array', new ArrayNotEmpty],
|
||||
'ingredients.meal.*' => [
|
||||
'nullable',
|
||||
'string',
|
||||
'required_with:ingredients.id.*',
|
||||
new InArray(JournalEntry::meals()->pluck('value')->toArray())
|
||||
],
|
||||
'ingredients.amount' => ['required', 'array', new ArrayNotEmpty],
|
||||
'ingredients.amount.*' => ['required_with:ingredients.id.*', 'nullable', new StringIsPositiveDecimalOrFraction],
|
||||
'ingredients.unit' => ['required', 'array'],
|
||||
'ingredients.unit.*' => ['required_with:ingredients.id.*'],
|
||||
'ingredients.id.*' => 'required_with:ingredients.amount.*|nullable',
|
||||
'ingredients.type.*' => ['required_with:ingredients.id.*', 'nullable', new UsesIngredientTrait()],
|
||||
'group_entries' => ['nullable', 'boolean'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function attributes(): array
|
||||
{
|
||||
return [
|
||||
'ingredients.amount' => 'amount',
|
||||
'ingredients.amount.*' => 'amount',
|
||||
'ingredients.date' => 'date',
|
||||
'ingredients.date.*' => 'date',
|
||||
'ingredients.id.*' => 'item',
|
||||
'ingredients.meal' => 'meal',
|
||||
'ingredients.meal.*' => 'meal',
|
||||
'ingredients.unit' => 'unit',
|
||||
'ingredients.unit.*' => 'unit',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Rules\StringIsPositiveDecimalOrFraction;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateFoodRequest extends FormRequest
|
||||
{
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'string'],
|
||||
'detail' => ['nullable', 'string'],
|
||||
'brand' => ['nullable', 'string'],
|
||||
'source' => ['nullable', 'string'],
|
||||
'notes' => ['nullable', 'string'],
|
||||
'serving_size' => ['required', new StringIsPositiveDecimalOrFraction],
|
||||
'serving_unit' => ['nullable', 'string'],
|
||||
'serving_unit_name' => ['nullable', 'string'],
|
||||
'serving_weight' => ['required', 'numeric', 'min:0'],
|
||||
'calories' => ['nullable', 'numeric', 'min:0'],
|
||||
'fat' => ['nullable', 'numeric', 'min:0'],
|
||||
'cholesterol' => ['nullable', 'numeric', 'min:0'],
|
||||
'sodium' => ['nullable', 'numeric', 'min:0'],
|
||||
'carbohydrates' => ['nullable', 'numeric', 'min:0'],
|
||||
'protein' => ['nullable', 'numeric', 'min:0'],
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Rules\StringIsPositiveDecimalOrFraction;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateGoalRequest extends FormRequest
|
||||
{
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'from' => ['nullable', 'date'],
|
||||
'to' => ['nullable', 'date'],
|
||||
'frequency' => ['required', 'string'],
|
||||
'name' => ['required', 'string'],
|
||||
'goal' => ['required', 'numeric', 'min:0'],
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Rules\ArrayNotEmpty;
|
||||
use App\Rules\StringIsPositiveDecimalOrFraction;
|
||||
use App\Rules\UsesIngredientTrait;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateRecipeRequest extends FormRequest
|
||||
{
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'string'],
|
||||
'description' => ['nullable', 'string'],
|
||||
'description_delta' => ['nullable', 'string'],
|
||||
'image' => ['nullable', 'file', 'mimes:jpg,png,gif'],
|
||||
'remove_image' => ['nullable', 'boolean'],
|
||||
'servings' => ['required', 'numeric'],
|
||||
'time_prep' => ['nullable', 'numeric'],
|
||||
'time_cook' => ['nullable', 'numeric'],
|
||||
'weight' => ['nullable', 'numeric', 'min:0'],
|
||||
'volume' => ['nullable', new StringIsPositiveDecimalOrFraction],
|
||||
'source' => ['nullable', 'string'],
|
||||
'ingredients.amount' => ['required', 'array', new ArrayNotEmpty],
|
||||
'ingredients.amount.*' => ['required_with:ingredients.id.*', 'nullable', new StringIsPositiveDecimalOrFraction],
|
||||
'ingredients.unit' => ['required', 'array'],
|
||||
'ingredients.unit.*' => ['required_with:ingredients.id.*'],
|
||||
'ingredients.detail' => ['required', 'array'],
|
||||
'ingredients.detail.*' => ['nullable', 'string'],
|
||||
'ingredients.id' => ['required', 'array', new ArrayNotEmpty],
|
||||
'ingredients.id.*' => ['required_with:ingredients.amount.*', 'required_with:ingredients.unit.*', 'nullable'],
|
||||
'ingredients.type' => ['required', 'array', new ArrayNotEmpty],
|
||||
'ingredients.type.*' => ['required_with:ingredients.id.*', 'nullable', new UsesIngredientTrait()],
|
||||
'ingredients.key' => ['nullable', 'array'],
|
||||
'ingredients.key.*' => ['nullable', 'int'],
|
||||
'ingredients.weight' => ['required', 'array', new ArrayNotEmpty],
|
||||
'ingredients.weight.*' => ['required', 'int'],
|
||||
'separators.key' => ['nullable', 'array'],
|
||||
'separators.key.*' => ['nullable', 'int'],
|
||||
'separators.weight' => ['nullable', 'array'],
|
||||
'separators.weight.*' => ['required', 'int'],
|
||||
'separators.text' => ['nullable', 'array'],
|
||||
'separators.text.*' => ['nullable', 'string'],
|
||||
'steps.step' => ['required', 'array', new ArrayNotEmpty],
|
||||
'steps.step.*' => ['nullable', 'string'],
|
||||
'steps.key' => ['nullable', 'array'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'ingredients.id.*.required_with' => 'Missing :attribute in Ingredients.',
|
||||
'ingredients.amount.*.required_with' => 'Missing :attribute in Ingredients.',
|
||||
'ingredients.unit.*.required_with' => 'Missing :attribute in Ingredients.',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function attributes(): array
|
||||
{
|
||||
return [
|
||||
'ingredients.id.*' => 'ingredient name',
|
||||
'ingredients.amount.*' => 'ingredient amount',
|
||||
'ingredients.unit.*' => 'ingredient unit',
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -36,6 +36,8 @@ class RecipeSchema extends SchemaProvider
|
|||
'servings' => $resource->servings,
|
||||
'weight' => $resource->weight,
|
||||
'serving_weight' => $resource->serving_weight,
|
||||
'volume' => $resource->volume,
|
||||
'volumeFormatted' => $resource->volume_formatted,
|
||||
'units_supported' => $resource->units_supported->pluck('label'),
|
||||
'caloriesPerServing' => $resource->caloriesPerServing(),
|
||||
'carbohydratesPerServing' => $resource->carbohydratesPerServing(),
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ use App\Support\Nutrients;
|
|||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* App\Models\Goal
|
||||
|
|
@ -44,7 +45,10 @@ final class Goal extends Model
|
|||
* Supported options for thr frequency attribute.
|
||||
*/
|
||||
public static array $frequencyOptions = [
|
||||
'daily' => ['value' => 'daily', 'label' => 'daily'],
|
||||
'daily' => [
|
||||
'value' => 'daily',
|
||||
'label' => 'daily'
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
@ -94,7 +98,7 @@ final class Goal extends Model
|
|||
foreach (Nutrients::all() as $nutrient) {
|
||||
$options[$nutrient['value']] = [
|
||||
'value' => $nutrient['value'],
|
||||
'label' => $nutrient['label'],
|
||||
'label' => Str::ucfirst($nutrient['label']),
|
||||
'unit' => $nutrient['unit'],
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ use App\Models\Traits\Ingredient;
|
|||
use App\Models\Traits\Journalable;
|
||||
use App\Models\Traits\Sluggable;
|
||||
use App\Models\Traits\Taggable;
|
||||
use App\Support\Number;
|
||||
use App\Support\Nutrients;
|
||||
use ElasticScoutDriverPlus\QueryDsl;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
|
|
@ -79,6 +80,9 @@ use Spatie\MediaLibrary\MediaCollections\Models\Media;
|
|||
* @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\RecipeSeparator[] $separators
|
||||
* @property-read int|null $separators_count
|
||||
* @property-read Collection $units_supported
|
||||
* @property float|null $volume
|
||||
* @property-read string|null $volume_formatted
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Recipe whereVolume($value)
|
||||
*/
|
||||
final class Recipe extends Model implements HasMedia
|
||||
{
|
||||
|
|
@ -104,6 +108,7 @@ final class Recipe extends Model implements HasMedia
|
|||
'source',
|
||||
'servings',
|
||||
'weight',
|
||||
'volume',
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
@ -114,6 +119,7 @@ final class Recipe extends Model implements HasMedia
|
|||
'time_prep' => 'int',
|
||||
'time_cook' => 'int',
|
||||
'weight' => 'float',
|
||||
'volume' => 'float',
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
@ -133,6 +139,7 @@ final class Recipe extends Model implements HasMedia
|
|||
*/
|
||||
protected $appends = [
|
||||
'serving_weight',
|
||||
'volume_formatted',
|
||||
'time_total',
|
||||
'units_supported'
|
||||
];
|
||||
|
|
@ -169,6 +176,17 @@ final class Recipe extends Model implements HasMedia
|
|||
return round($this->weight / $this->servings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the volume as a formatted string (e.g. 0.5 = 1/2).
|
||||
*/
|
||||
public function getVolumeFormattedAttribute(): ?string {
|
||||
$result = null;
|
||||
if (!empty($this->volume)) {
|
||||
$result = Number::rationalStringFromFloat($this->volume);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ingredients list (ingredient amounts and separators).
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -65,6 +65,9 @@ trait Ingredient
|
|||
if (!empty($this->serving_weight)) {
|
||||
$supported = $supported->merge($units->where('type', 'weight'));
|
||||
}
|
||||
if (isset($this->volume) && !empty($this->volume)) {
|
||||
$supported = $supported->merge($units->where('type', 'volume'));
|
||||
}
|
||||
return $supported->sortBy('label');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ namespace App\Rules;
|
|||
use App\Support\Number;
|
||||
use Illuminate\Contracts\Validation\Rule;
|
||||
|
||||
class StringIsDecimalOrFraction implements Rule
|
||||
class StringIsPositiveDecimalOrFraction implements Rule
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
|
|
@ -14,7 +14,7 @@ class StringIsDecimalOrFraction implements Rule
|
|||
{
|
||||
try {
|
||||
$result = Number::floatFromString($value);
|
||||
return $result != 0;
|
||||
return $result > 0;
|
||||
}
|
||||
catch (\InvalidArgumentException $e) {
|
||||
// Allow to pass through, method will return false.
|
||||
|
|
@ -27,6 +27,6 @@ class StringIsDecimalOrFraction implements Rule
|
|||
*/
|
||||
public function message(): string
|
||||
{
|
||||
return 'The :attribute must be a decimal or fraction.';
|
||||
return 'The :attribute must be a positive decimal or fraction.';
|
||||
}
|
||||
}
|
||||
|
|
@ -175,6 +175,8 @@ class Nutrients
|
|||
|
||||
/**
|
||||
* Calculate a nutrient amount for a recipe.
|
||||
*
|
||||
* Weight base unit is grams, volume base unit is cups.
|
||||
*/
|
||||
public static function calculateRecipeNutrientAmount(
|
||||
Recipe $recipe,
|
||||
|
|
@ -182,18 +184,19 @@ class Nutrients
|
|||
float $amount,
|
||||
string $fromUnit
|
||||
): float {
|
||||
if ($fromUnit === 'oz') {
|
||||
return $amount * self::$gramsPerOunce / $recipe->weight * $recipe->{"{$nutrient}Total"}();
|
||||
}
|
||||
elseif ($fromUnit === 'serving') {
|
||||
if ($fromUnit === 'serving') {
|
||||
// Use "per serving" methods directly.
|
||||
return $recipe->{"{$nutrient}PerServing"}() * $amount;
|
||||
}
|
||||
elseif ($fromUnit === 'gram') {
|
||||
return $amount / $recipe->weight * $recipe->{"{$nutrient}Total"}();
|
||||
}
|
||||
else {
|
||||
throw new \DomainException("Unsupported recipe unit: {$fromUnit}");
|
||||
}
|
||||
$multiplier = match ($fromUnit) {
|
||||
'oz' => $amount * self::$gramsPerOunce / $recipe->weight,
|
||||
'gram' => $amount / $recipe->weight,
|
||||
'tsp' => $amount / 48 / $recipe->volume,
|
||||
'tbsp' => $amount / 16 / $recipe->volume,
|
||||
'cup' => $amount / $recipe->volume,
|
||||
default => throw new \DomainException("Unsupported recipe unit: {$fromUnit}"),
|
||||
};
|
||||
return $multiplier * $recipe->{"{$nutrient}Total"}();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -8,20 +8,20 @@ use Illuminate\View\Component;
|
|||
|
||||
class Select extends Component
|
||||
{
|
||||
public ?bool $hasError;
|
||||
public Collection|array $options;
|
||||
public ?string $selectedValue;
|
||||
|
||||
/**
|
||||
* Select constructor.
|
||||
*
|
||||
* @param \Illuminate\Support\Collection|array $options
|
||||
* @param ?string $selectedValue
|
||||
*/
|
||||
public function __construct(
|
||||
Collection|array $options,
|
||||
?bool $hasError = false,
|
||||
?string $selectedValue = '',
|
||||
) {
|
||||
$this->options = $options;
|
||||
$this->hasError = $hasError;
|
||||
$this->selectedValue = $selectedValue;
|
||||
}
|
||||
|
||||
|
|
@ -29,6 +29,7 @@ class Select extends Component
|
|||
{
|
||||
return view('components.inputs.select')
|
||||
->with('options', $this->options)
|
||||
->with('hasError', $this->hasError)
|
||||
->with('selectedValue', $this->selectedValue);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ class RecipeFactory extends Factory
|
|||
public function definition(): array
|
||||
{
|
||||
$description = htmlspecialchars($this->faker->realText(500));
|
||||
$volumes = [1/4, 1/3, 1/2, 2/3, 3/4, 1, 1 + 1/2, 1 + 3/4, 2, 2 + 1/2, 3, 3 + 1/2, 4, 5];
|
||||
return [
|
||||
'name' => Words::randomWords(Arr::random(['npan', 'npn', 'anpn'])),
|
||||
'description' => "<p>{$description}</p>",
|
||||
|
|
@ -31,7 +32,8 @@ class RecipeFactory extends Factory
|
|||
'time_cook' => $this->faker->numberBetween(0, 90),
|
||||
'source' => $this->faker->optional()->url,
|
||||
'servings' => $this->faker->numberBetween(1, 10),
|
||||
'weight' => $this->faker->randomFloat(1, 60, 2000),
|
||||
'weight' => $this->faker->optional()->randomFloat(1, 60, 2000),
|
||||
'volume' => $this->faker->optional()->randomElement($volumes),
|
||||
'tags' => Words::randomWords(Arr::random(['a', 'aa', 'aaa']), TRUE),
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ class CreateRecipesTable extends Migration
|
|||
$table->string('source')->nullable();
|
||||
$table->unsignedInteger('servings');
|
||||
$table->unsignedFloat('weight')->nullable();
|
||||
//$table->decimal('volume', 10, 8)->unsigned()->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class AddVolumeToRecipes extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('recipes', function (Blueprint $table) {
|
||||
$table->decimal('volume', 10, 8)->unsigned()->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('recipes', function (Blueprint $table) {
|
||||
$table->dropColumn('volume');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -39,7 +39,7 @@ services:
|
|||
image: phpmyadmin
|
||||
restart: always
|
||||
ports:
|
||||
- 8080:80
|
||||
- 8081:80
|
||||
environment:
|
||||
PMA_HOST: db
|
||||
MYSQL_ROOT_PASSWORD: '${DB_PASSWORD:-kcal}'
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -1,3 +1,5 @@
|
|||
@props(['hasError' => false])
|
||||
|
||||
<div x-data="picker()">
|
||||
<div>
|
||||
<div>
|
||||
|
|
@ -11,7 +13,7 @@
|
|||
x-ref="ingredients_type"/>
|
||||
<x-inputs.input type="text"
|
||||
name="ingredients[name][]"
|
||||
class="w-full"
|
||||
class="w-full{{ $hasError ? ' border-red-600' : '' }}"
|
||||
value="{{ $defaultName ?? '' }}"
|
||||
placeholder="Search..."
|
||||
autocomplete="off"
|
||||
|
|
|
|||
|
|
@ -1,3 +1,20 @@
|
|||
@props(['disabled' => false])
|
||||
@props(['disabled' => false, 'hasError' => false])
|
||||
|
||||
<input {{ $disabled ? 'disabled' : '' }} {!! $attributes->merge(['class' => 'rounded-md shadow-sm border-gray-300 focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50']) !!}>
|
||||
@php
|
||||
$classes = [
|
||||
'rounded-md',
|
||||
'shadow-sm',
|
||||
'border-gray-300',
|
||||
'focus:border-indigo-300',
|
||||
'focus:ring',
|
||||
'focus:ring-indigo-200',
|
||||
'focus:ring-opacity-50'
|
||||
];
|
||||
if ($hasError) {
|
||||
$classes[] = 'border-red-600';
|
||||
}
|
||||
@endphp
|
||||
|
||||
<input
|
||||
{{ $disabled ? 'disabled' : '' }}
|
||||
{!! $attributes->merge(['class' => implode(' ', $classes)]) !!}>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,23 @@
|
|||
<select {{ $attributes->merge(['class' => 'rounded-md shadow-sm border-gray-300 focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50']) }}>
|
||||
@props(['disabled' => false, 'hasError' => false])
|
||||
|
||||
@php
|
||||
$classes = [
|
||||
'rounded-md',
|
||||
'shadow-sm',
|
||||
'border-gray-300',
|
||||
'focus:border-indigo-300',
|
||||
'focus:ring',
|
||||
'focus:ring-indigo-200',
|
||||
'focus:ring-opacity-50',
|
||||
];
|
||||
if ($hasError) {
|
||||
$classes[] = 'border-red-600';
|
||||
}
|
||||
@endphp
|
||||
|
||||
<select
|
||||
{{ $disabled ? 'disabled' : '' }}
|
||||
{!! $attributes->merge(['class' => implode(' ', $classes)]) !!}>
|
||||
{{ $slot }}
|
||||
<x-inputs.select-options :options="$options" :selectedValue="$selectedValue" />
|
||||
</select>
|
||||
|
|
|
|||
|
|
@ -5,140 +5,143 @@
|
|||
<h1 class="font-semibold text-xl text-gray-800 leading-tight">{{ $title }}</h1>
|
||||
</x-slot>
|
||||
<form method="POST" action="{{ ($food->exists ? route('foods.update', $food) : route('foods.store')) }}">
|
||||
@if ($food->exists)@method('put')@endif
|
||||
@csrf
|
||||
<div class="flex flex-col space-y-4">
|
||||
<div class="flex flex-col space-y-4 md:flex-row md:space-x-4 md:space-y-0">
|
||||
<!-- Name -->
|
||||
<div class="flex-auto">
|
||||
<x-inputs.label for="name" value="Name"/>
|
||||
@if ($food->exists)@method('put')@endif
|
||||
@csrf
|
||||
<div class="flex flex-col space-y-4">
|
||||
<div class="flex flex-col space-y-4 md:flex-row md:space-x-4 md:space-y-0">
|
||||
<!-- Name -->
|
||||
<div class="flex-auto">
|
||||
<x-inputs.label for="name" value="Name"/>
|
||||
|
||||
<x-inputs.input name="name"
|
||||
type="text"
|
||||
class="block mt-1 w-full"
|
||||
autocapitalize="none"
|
||||
:value="old('name', $food->name)"
|
||||
required/>
|
||||
</div>
|
||||
<x-inputs.input name="name"
|
||||
type="text"
|
||||
class="block mt-1 w-full"
|
||||
autocapitalize="none"
|
||||
:value="old('name', $food->name)"
|
||||
:hasError="$errors->has('name')"/>
|
||||
</div>
|
||||
|
||||
<!-- Detail -->
|
||||
<div class="flex-auto">
|
||||
<x-inputs.label for="detail" value="Detail"/>
|
||||
<!-- Detail -->
|
||||
<div class="flex-auto">
|
||||
<x-inputs.label for="detail" value="Detail"/>
|
||||
|
||||
<x-inputs.input name="detail"
|
||||
type="text"
|
||||
class="block mt-1 w-full"
|
||||
autocapitalize="none"
|
||||
:value="old('detail', $food->detail)"/>
|
||||
</div>
|
||||
<x-inputs.input name="detail"
|
||||
type="text"
|
||||
class="block mt-1 w-full"
|
||||
autocapitalize="none"
|
||||
:value="old('detail', $food->detail)"/>
|
||||
</div>
|
||||
|
||||
<!-- Brand -->
|
||||
<div class="flex-auto">
|
||||
<x-inputs.label for="brand" value="Brand"/>
|
||||
<!-- Brand -->
|
||||
<div class="flex-auto">
|
||||
<x-inputs.label for="brand" value="Brand"/>
|
||||
|
||||
<x-inputs.input name="brand"
|
||||
type="text"
|
||||
class="block mt-1 w-full"
|
||||
:value="old('brand', $food->brand)"/>
|
||||
</div>
|
||||
</div>
|
||||
<x-inputs.input name="brand"
|
||||
type="text"
|
||||
class="block mt-1 w-full"
|
||||
:value="old('brand', $food->brand)"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col space-y-4 sm:flex-row sm:space-x-4 sm:space-y-0">
|
||||
<!-- Serving size -->
|
||||
<div>
|
||||
<x-inputs.label for="serving_size" value="Serving size"/>
|
||||
<div class="flex flex-col space-y-4 sm:flex-row sm:space-x-4 sm:space-y-0">
|
||||
<!-- Serving size -->
|
||||
<div>
|
||||
<x-inputs.label for="serving_size" value="Serving size"/>
|
||||
|
||||
<x-inputs.input name="serving_size"
|
||||
type="text"
|
||||
class="block mt-1 w-full"
|
||||
size="10"
|
||||
:value="old('serving_size', $food->serving_size_formatted)"
|
||||
required/>
|
||||
</div>
|
||||
<x-inputs.input name="serving_size"
|
||||
type="text"
|
||||
class="block mt-1 w-full"
|
||||
size="10"
|
||||
:value="old('serving_size', $food->serving_size_formatted)"
|
||||
:hasError="$errors->has('serving_size')"
|
||||
required/>
|
||||
</div>
|
||||
|
||||
<!-- Serving unit -->
|
||||
<div>
|
||||
<x-inputs.label for="serving_unit" value="Serving unit"/>
|
||||
<!-- Serving unit -->
|
||||
<div>
|
||||
<x-inputs.label for="serving_unit" value="Serving unit"/>
|
||||
|
||||
<x-inputs.select name="serving_unit"
|
||||
class="block mt-1 w-full"
|
||||
:options="$serving_units"
|
||||
:selectedValue="old('serving_unit', $food->serving_unit)">
|
||||
<option value=""></option>
|
||||
</x-inputs.select>
|
||||
</div>
|
||||
<x-inputs.select name="serving_unit"
|
||||
class="block mt-1 w-full"
|
||||
:options="$serving_units"
|
||||
:selectedValue="old('serving_unit', $food->serving_unit)">
|
||||
<option value=""></option>
|
||||
</x-inputs.select>
|
||||
</div>
|
||||
|
||||
<!-- Serving unit name -->
|
||||
<div>
|
||||
<x-inputs.label for="serving_unit_name" value="Serving unit name"/>
|
||||
<!-- Serving unit name -->
|
||||
<div>
|
||||
<x-inputs.label for="serving_unit_name" value="Serving unit name"/>
|
||||
|
||||
<x-inputs.input name="serving_unit_name"
|
||||
type="text"
|
||||
autocapitalize="none"
|
||||
class="block mt-1 w-full"
|
||||
placeholder="e.g. clove, egg"
|
||||
size="10"
|
||||
:value="old('serving_unit_name', $food->serving_unit_name)"/>
|
||||
</div>
|
||||
<x-inputs.input name="serving_unit_name"
|
||||
type="text"
|
||||
autocapitalize="none"
|
||||
class="block mt-1 w-full"
|
||||
placeholder="e.g. clove, egg"
|
||||
size="10"
|
||||
:value="old('serving_unit_name', $food->serving_unit_name)"/>
|
||||
</div>
|
||||
|
||||
<!-- Serving weight -->
|
||||
<div>
|
||||
<x-inputs.label for="serving_weight" value="Serving weight (g)"/>
|
||||
<!-- Serving weight -->
|
||||
<div>
|
||||
<x-inputs.label for="serving_weight" value="Serving weight (g)"/>
|
||||
|
||||
<x-inputs.input name="serving_weight"
|
||||
type="number"
|
||||
step="any"
|
||||
class="block mt-1 w-full"
|
||||
size="10"
|
||||
:value="old('serving_weight', $food->serving_weight)"
|
||||
required/>
|
||||
</div>
|
||||
</div>
|
||||
<x-inputs.input name="serving_weight"
|
||||
type="number"
|
||||
step="any"
|
||||
class="block mt-1 w-full"
|
||||
size="10"
|
||||
:value="old('serving_weight', $food->serving_weight)"
|
||||
:hasError="$errors->has('serving_weight')"
|
||||
required/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col space-y-4 md:flex-row md:space-y-0">
|
||||
@foreach (\App\Support\Nutrients::all()->sortBy('weight') as $nutrient)
|
||||
<!-- {{ ucfirst($nutrient['value']) }} -->
|
||||
<div class="flex-auto">
|
||||
<x-inputs.label for="{{ $nutrient['value'] }}"
|
||||
:value="ucfirst($nutrient['value']) . ($nutrient['unit'] ? ' (' . $nutrient['unit'] . ')' : '')"/>
|
||||
<div class="flex flex-col space-y-4 md:flex-row md:space-y-0">
|
||||
@foreach (\App\Support\Nutrients::all()->sortBy('weight') as $nutrient)
|
||||
<!-- {{ ucfirst($nutrient['value']) }} -->
|
||||
<div class="flex-auto">
|
||||
<x-inputs.label for="{{ $nutrient['value'] }}"
|
||||
:value="ucfirst($nutrient['value']) . ($nutrient['unit'] ? ' (' . $nutrient['unit'] . ')' : '')"/>
|
||||
|
||||
<x-inputs.input name="{{ $nutrient['value'] }}"
|
||||
type="number"
|
||||
step="any"
|
||||
class="block w-full mt-1 md:w-5/6"
|
||||
:value="old($nutrient['value'], $food->{$nutrient['value']})"/>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<!-- Tags -->
|
||||
<x-tagger :defaultTags="$food_tags"/>
|
||||
|
||||
<!-- Source -->
|
||||
<div class="flex-auto">
|
||||
<x-inputs.label for="source" value="Source" />
|
||||
|
||||
<x-inputs.input name="source"
|
||||
type="text"
|
||||
class="block mt-1 w-full"
|
||||
inputmode="url"
|
||||
:value="old('source', $food->source)" />
|
||||
</div>
|
||||
|
||||
<!-- Notes -->
|
||||
<div>
|
||||
<x-inputs.label for="description" value="Description" />
|
||||
|
||||
<x-inputs.textarea name="notes"
|
||||
class="block mt-1 w-full"
|
||||
:value="old('notes', $food->notes)" />
|
||||
</div>
|
||||
<x-inputs.input name="{{ $nutrient['value'] }}"
|
||||
type="number"
|
||||
step="any"
|
||||
class="block w-full mt-1 md:w-5/6"
|
||||
:value="old($nutrient['value'], $food->{$nutrient['value']})"
|
||||
:hasError="$errors->has($nutrient['value'])"/>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end mt-4">
|
||||
<x-inputs.button class="ml-3">
|
||||
{{ ($food->exists ? 'Save' : 'Add') }}
|
||||
</x-inputs.button>
|
||||
</div>
|
||||
</form>
|
||||
<!-- Tags -->
|
||||
<x-tagger :defaultTags="$food_tags"/>
|
||||
|
||||
<!-- Source -->
|
||||
<div class="flex-auto">
|
||||
<x-inputs.label for="source" value="Source" />
|
||||
|
||||
<x-inputs.input name="source"
|
||||
type="text"
|
||||
class="block mt-1 w-full"
|
||||
inputmode="url"
|
||||
:value="old('source', $food->source)" />
|
||||
</div>
|
||||
|
||||
<!-- Notes -->
|
||||
<div>
|
||||
<x-inputs.label for="description" value="Description" />
|
||||
|
||||
<x-inputs.textarea name="notes"
|
||||
class="block mt-1 w-full"
|
||||
:value="old('notes', $food->notes)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end mt-4">
|
||||
<x-inputs.button class="ml-3">
|
||||
{{ ($food->exists ? 'Save' : 'Add') }}
|
||||
</x-inputs.button>
|
||||
</div>
|
||||
</form>
|
||||
</x-app-layout>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
</div>
|
||||
</h1>
|
||||
</x-slot>
|
||||
<div class="flex flex-col justify-between pb-4 md:flex-row md:space-x-4">
|
||||
<div class="flex flex-col-reverse justify-between pb-4 md:flex-row md:space-x-4">
|
||||
<div class="flex-1">
|
||||
<section class="flex flex-col space-y-2">
|
||||
@if($food->brand)
|
||||
|
|
@ -56,7 +56,7 @@
|
|||
</section>
|
||||
</div>
|
||||
<aside class="flex flex-col space-y-4 mt-8 sm:mt-0 sm:max-w-xs">
|
||||
<section class="p-1 mb-2 border-2 border-black font-sans md:w-72">
|
||||
<section class="p-1 border-2 border-black font-sans md:w-72">
|
||||
<h1 class="text-3xl font-extrabold leading-none">Nutrition Facts</h1>
|
||||
<section class="flex justify-between font-bold border-b-8 border-black">
|
||||
<h1>Serving size</h1>
|
||||
|
|
@ -99,8 +99,7 @@
|
|||
</section>
|
||||
</div>
|
||||
</section>
|
||||
<hr />
|
||||
<section class="flex flex-col space-y-2">
|
||||
<section class="flex flex-row space-x-2 justify-around md:flex-col md:space-y-2 md:space-x-0">
|
||||
<x-button-link.base href="{{ route('foods.edit', $food) }}">
|
||||
Edit Food
|
||||
</x-button-link.base>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,8 @@
|
|||
<x-inputs.input name="from"
|
||||
type="date"
|
||||
class="block w-full"
|
||||
:value="old('from', $goal->from?->toDateString())" />
|
||||
:value="old('from', $goal->from?->toDateString())"
|
||||
:hasError="$errors->has('from')" />
|
||||
</div>
|
||||
|
||||
<!-- To -->
|
||||
|
|
@ -24,7 +25,8 @@
|
|||
<x-inputs.input name="to"
|
||||
type="date"
|
||||
class="block w-full"
|
||||
:value="old('to', $goal->to?->toDateString())" />
|
||||
:value="old('to', $goal->to?->toDateString())"
|
||||
:hasError="$errors->has('to')" />
|
||||
</div>
|
||||
|
||||
<!-- Frequency -->
|
||||
|
|
@ -33,7 +35,8 @@
|
|||
<x-inputs.select name="frequency"
|
||||
class="block w-full"
|
||||
:options="$frequencyOptions"
|
||||
:selectedValue="old('frequency', $goal->frequency)">
|
||||
:selectedValue="old('frequency', $goal->frequency)"
|
||||
:hasError="$errors->has('frequency')">
|
||||
</x-inputs.select>
|
||||
</div>
|
||||
|
||||
|
|
@ -44,6 +47,7 @@
|
|||
class="block w-full"
|
||||
:options="$nameOptions"
|
||||
:selectedValue="old('name', $goal->name)"
|
||||
:hasError="$errors->has('name')"
|
||||
required>
|
||||
</x-inputs.select>
|
||||
</div>
|
||||
|
|
@ -56,6 +60,7 @@
|
|||
step="any"
|
||||
class="block w-full"
|
||||
:value="old('goal', $goal->goal)"
|
||||
:hasError="$errors->has('goal')"
|
||||
required />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
type="date"
|
||||
class="block w-full"
|
||||
:value="old('date', $default_date->toDateString())"
|
||||
:hasError="$errors->has('date')"
|
||||
required />
|
||||
</div>
|
||||
|
||||
|
|
@ -30,6 +31,7 @@
|
|||
class="block w-full"
|
||||
:options="$meals"
|
||||
:selectedValue="old('meal')"
|
||||
:hasError="$errors->has('meal')"
|
||||
required>
|
||||
<option value=""></option>
|
||||
</x-inputs.select>
|
||||
|
|
@ -43,6 +45,7 @@
|
|||
type="text"
|
||||
class="block w-full"
|
||||
:value="old('summary')"
|
||||
:hasError="$errors->has('summary')"
|
||||
required />
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -58,7 +61,8 @@
|
|||
type="number"
|
||||
step="any"
|
||||
class="block w-full"
|
||||
:value="old($nutrient['value'])"/>
|
||||
:value="old($nutrient['value'])"
|
||||
:hasError="$errors->has($nutrient['value'])"/>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
@php($key = $key ?? null)
|
||||
<div x-data class="entry-item flex items-center space-x-2">
|
||||
<div class="flex flex-col space-y-4 w-full">
|
||||
<!-- Ingredient -->
|
||||
|
|
@ -15,6 +16,7 @@
|
|||
type="date"
|
||||
class="block w-full"
|
||||
:value="$date ?? $default_date->toDateString()"
|
||||
:hasError="$errors->has('ingredients.date.' . $key)"
|
||||
required />
|
||||
</div>
|
||||
|
||||
|
|
@ -25,8 +27,9 @@
|
|||
class="block w-full"
|
||||
:options="$meals"
|
||||
:selectedValue="$meal ?? null"
|
||||
:hasError="$errors->has('ingredients.meal.' . $key)"
|
||||
required>
|
||||
<option value="">-- Meal --</option>
|
||||
@if(is_null($key))<option value="">-- Meal --</option>@endif
|
||||
</x-inputs.select>
|
||||
</div>
|
||||
|
||||
|
|
@ -39,6 +42,7 @@
|
|||
class="block w-full"
|
||||
placeholder="Amount"
|
||||
:value="$amount ?? null"
|
||||
:hasError="$errors->has('ingredients.amount.' . $key)"
|
||||
required />
|
||||
</div>
|
||||
|
||||
|
|
@ -48,8 +52,9 @@
|
|||
<x-inputs.select name="ingredients[unit][]"
|
||||
class="block w-full"
|
||||
:options="$units ?? []"
|
||||
:selectedValue="$unit ?? null">
|
||||
<option value="">-- Unit --</option>
|
||||
:selectedValue="$unit ?? null"
|
||||
:hasError="$errors->has('ingredients.unit.' . $key)">
|
||||
@if(is_null($key))<option value="">-- Unit --</option>@endif
|
||||
</x-inputs.select>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@
|
|||
|
||||
<!-- Weight -->
|
||||
<div class="flex-auto">
|
||||
<x-inputs.label for="weight" value="Total weight (g)" />
|
||||
<x-inputs.label for="weight" value="Weight (g)" />
|
||||
|
||||
<x-inputs.input name="weight"
|
||||
type="number"
|
||||
|
|
@ -42,9 +42,19 @@
|
|||
:value="old('weight', $recipe->weight)" />
|
||||
</div>
|
||||
|
||||
<!-- Volume -->
|
||||
<div class="flex-auto">
|
||||
<x-inputs.label for="volume" value="Volume (cups)" />
|
||||
|
||||
<x-inputs.input name="volume"
|
||||
type="text"
|
||||
class="block mt-1 w-full"
|
||||
:value="old('volume', $recipe->volume_formatted)" />
|
||||
</div>
|
||||
|
||||
<!-- Prep Time -->
|
||||
<div class="flex-auto">
|
||||
<x-inputs.label for="time_prep" value="Prep time (minutes)" />
|
||||
<x-inputs.label for="time_prep" value="Prep time (min.)" />
|
||||
|
||||
<x-inputs.input name="time_prep"
|
||||
type="number"
|
||||
|
|
@ -56,7 +66,7 @@
|
|||
|
||||
<!-- Cooke Time -->
|
||||
<div class="flex-auto">
|
||||
<x-inputs.label for="time_cook" value="Cook time (minutes)" />
|
||||
<x-inputs.label for="time_cook" value="Cook time (min.)" />
|
||||
|
||||
<x-inputs.input name="time_cook"
|
||||
type="number"
|
||||
|
|
|
|||
|
|
@ -16,7 +16,10 @@
|
|||
<a x-bind:href="recipe.showUrl"
|
||||
class="hover:text-gray-600" x-text="recipe.name"></a>
|
||||
</h1>
|
||||
<h2 class="leading-snug" x-text="`${recipe.servings} servings`"></h2>
|
||||
<h2 class="leading-snug">
|
||||
<span x-text="`${recipe.servings} servings`"></span>
|
||||
<span x-show="recipe.volume" x-text="` / ${recipe.volumeFormatted} cups`"></span>
|
||||
</h2>
|
||||
<section class="flex justify-between items-end font-extrabold" x-show="recipe.serving_weight">
|
||||
<h1>Serving weight</h1>
|
||||
<div x-text="`${recipe.serving_weight}g`"></div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
@php($key = $key ?? null)
|
||||
@error("ingredients.amount.{$key}")
|
||||
@php($amount_error = 'border-red-600')
|
||||
@enderror
|
||||
@error("ingredients.unit.{$key}")
|
||||
@php($unit_error = 'border-red-600')
|
||||
@enderror
|
||||
|
||||
<div class="ingredient draggable">
|
||||
<x-inputs.input type="hidden" name="ingredients[key][]" :value="$key ?? null" />
|
||||
<x-inputs.input type="hidden" name="ingredients[key][]" :value="$key" />
|
||||
<x-inputs.input type="hidden" name="ingredients[weight][]" :value="$weight ?? null" />
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="flex flex-col space-y-4 md:flex-row md:space-x-4 md:space-y-0 w-full">
|
||||
|
|
@ -11,16 +19,17 @@
|
|||
<div class="w-full">
|
||||
<x-ingredient-picker :default-id="$ingredient_id ?? null"
|
||||
:default-type="$ingredient_type ?? null"
|
||||
:default-name="$ingredient_name ?? null" />
|
||||
:default-name="$ingredient_name ?? null"
|
||||
:has-error="(isset($amount) || isset($unit)) && empty($ingredient_id)"/>
|
||||
</div>
|
||||
<x-inputs.input name="ingredients[amount][]"
|
||||
type="text"
|
||||
size="5"
|
||||
placeholder="Amount"
|
||||
class="block"
|
||||
class="block {{ $amount_error ?? null }}"
|
||||
:value="$amount ?? null" />
|
||||
<x-inputs.select name="ingredients[unit][]"
|
||||
class="block"
|
||||
class="block {{ $unit_error ?? null }}"
|
||||
:options="$units_supported ?? []"
|
||||
:selectedValue="$unit ?? null">
|
||||
<option value="" selected>Unit</option>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
{{ $recipe->name }}
|
||||
</h1>
|
||||
</x-slot>
|
||||
<div class="flex flex-col justify-between pb-4 md:flex-row md:space-x-4">
|
||||
<div class="flex flex-col-reverse justify-between md:flex-row md:space-x-4">
|
||||
<div class="flex-1" x-data="{showNutrientsSummary: false}">
|
||||
@if($recipe->time_total > 0)
|
||||
<section class="flex justify-between mb-2 p-2 bg-gray-100 rounded max-w-3xl">
|
||||
|
|
@ -81,7 +81,7 @@
|
|||
</ol>
|
||||
</div>
|
||||
</section>
|
||||
<footer>
|
||||
<footer class="space-y-2">
|
||||
@if(!$recipe->tags->isEmpty())
|
||||
<section>
|
||||
<h1 class="mb-2 font-bold text-2xl">Tags</h1>
|
||||
|
|
@ -92,12 +92,28 @@
|
|||
</div>
|
||||
</section>
|
||||
@endif
|
||||
@if($recipe->source)
|
||||
<section>
|
||||
<h1 class="mb-2 font-bold text-2xl">Source</h1>
|
||||
@if(filter_var($recipe->source, FILTER_VALIDATE_URL))
|
||||
<a class="text-gray-500 hover:text-gray-700 hover:border-gray-300"
|
||||
href="{{ $recipe->source }}">{{ $recipe->source }}</a>
|
||||
@else
|
||||
{{ $recipe->source }}
|
||||
@endif
|
||||
</section>
|
||||
@endif
|
||||
</footer>
|
||||
</div>
|
||||
<aside class="flex flex-col space-y-4 mt-8 md:mt-0 sm:max-w-xs">
|
||||
<aside class="flex flex-col space-y-4 mb-8 md:mt-0 sm:max-w-xs">
|
||||
<div class="p-1 border-2 border-black font-sans md:w-72">
|
||||
<div class="text-3xl font-extrabold leading-none">Nutrition Facts</div>
|
||||
<div class="leading-snug">{{ $recipe->servings }} {{ \Illuminate\Support\Str::plural('serving', $recipe->servings ) }}</div>
|
||||
<div class="leading-snug">
|
||||
{{ $recipe->servings }} {{ \Illuminate\Support\Str::plural('serving', $recipe->servings ) }}
|
||||
@if($recipe->volume)
|
||||
/ {{ $recipe->volume_formatted }} {{ \Illuminate\Support\Str::plural('cup', $recipe->volume ) }}
|
||||
@endif
|
||||
</div>
|
||||
@if($recipe->serving_weight)
|
||||
<div class="flex justify-between items-end font-extrabold">
|
||||
<div>Serving weight</div>
|
||||
|
|
@ -137,19 +153,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@if($recipe->source)
|
||||
<section>
|
||||
<h1 class="mb-2 font-bold text-2xl">Source</h1>
|
||||
@if(filter_var($recipe->source, FILTER_VALIDATE_URL))
|
||||
<a class="text-gray-500 hover:text-gray-700 hover:border-gray-300"
|
||||
href="{{ $recipe->source }}">{{ $recipe->source }}</a>
|
||||
@else
|
||||
{{ $recipe->source }}
|
||||
@endif
|
||||
</section>
|
||||
@endif
|
||||
<hr />
|
||||
<section class="flex flex-col space-y-2">
|
||||
<section class="flex flex-row space-x-2 justify-around md:flex-col md:space-y-2 md:space-x-0">
|
||||
<x-button-link.base href="{{ route('recipes.edit', $recipe) }}">
|
||||
Edit Recipe
|
||||
</x-button-link.base>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
namespace Tests\Feature\Support;
|
||||
|
||||
use App\Models\Food;
|
||||
use App\Models\IngredientAmount;
|
||||
use App\Models\Recipe;
|
||||
use App\Support\Nutrients;
|
||||
use Tests\TestCase;
|
||||
|
||||
|
|
@ -35,8 +37,45 @@ class NutrientsTest extends TestCase
|
|||
float $expectedMultiplier
|
||||
): void {
|
||||
$this->assertEquals(
|
||||
Nutrients::calculateFoodNutrientMultiplier($food, $amount, $fromUnit),
|
||||
$expectedMultiplier
|
||||
$expectedMultiplier,
|
||||
Nutrients::calculateFoodNutrientMultiplier($food, $amount, $fromUnit)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test valid Recipe nutrient amount calculation.
|
||||
*
|
||||
* @dataProvider recipesValidRecipeNutrientAmountsProvider
|
||||
*/
|
||||
public function testCalculateValidRecipeNutrientAmount(
|
||||
string $nutrient,
|
||||
float $amount,
|
||||
string $fromUnit,
|
||||
float $expectedAmount
|
||||
): void {
|
||||
/** @var \App\Models\Recipe $recipe */
|
||||
$recipe = Recipe::factory()
|
||||
->create(['volume' => 2, 'weight' => 400]);
|
||||
/** @var \App\Models\Food $food */
|
||||
$food = Food::factory()->create([
|
||||
'calories' => 20,
|
||||
'carbohydrates' => 20,
|
||||
'cholesterol' => 200,
|
||||
'fat' => 20,
|
||||
'protein' => 20,
|
||||
'sodium' => 200,
|
||||
]);
|
||||
$ingredient = new IngredientAmount();
|
||||
$ingredient->fill([
|
||||
'amount' => 1,
|
||||
'unit' => 'serving',
|
||||
'weight' => 0,
|
||||
])->ingredient()->associate($food);
|
||||
$recipe->ingredientAmounts()->save($ingredient);
|
||||
|
||||
$this->assertEquals(
|
||||
$expectedAmount,
|
||||
Nutrients::calculateRecipeNutrientAmount($recipe, $nutrient, $amount, $fromUnit)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -44,6 +83,26 @@ class NutrientsTest extends TestCase
|
|||
* Data providers.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Provide example recipe and expected nutrient amounts.
|
||||
*/
|
||||
public function recipesValidRecipeNutrientAmountsProvider(): array {
|
||||
return [
|
||||
['calories', 1, 'cup', 10],
|
||||
['calories', 2, 'cup', 20],
|
||||
['carbohydrates', 8, 'tbsp', 5],
|
||||
['carbohydrates', 16, 'tbsp', 10],
|
||||
['cholesterol', 48, 'tsp', 100],
|
||||
['cholesterol', 96, 'tsp', 200],
|
||||
['fat', 100, 'gram', 5],
|
||||
['fat', 200, 'gram', 10],
|
||||
['protein', 100, 'gram', 5],
|
||||
['protein', 200, 'gram', 10],
|
||||
['sodium', 2, 'oz', Nutrients::$gramsPerOunce],
|
||||
['sodium', 4, 'oz', Nutrients::$gramsPerOunce * 2],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide example foods and expected nutrient multipliers.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@
|
|||
|
||||
namespace Tests\Unit\Rules;
|
||||
|
||||
use App\Rules\StringIsDecimalOrFraction;
|
||||
use App\Rules\StringIsPositiveDecimalOrFraction;
|
||||
|
||||
class StringIsDecimalOrFractionTest extends RulesTestCase
|
||||
class StringIsPositiveDecimalOrFractionTest extends RulesTestCase
|
||||
{
|
||||
|
||||
/**
|
||||
|
|
@ -13,7 +13,7 @@ class StringIsDecimalOrFractionTest extends RulesTestCase
|
|||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->validator->setRules([new StringIsDecimalOrFraction()]);
|
||||
$this->validator->setRules([new StringIsPositiveDecimalOrFraction()]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -49,16 +49,16 @@ class StringIsDecimalOrFractionTest extends RulesTestCase
|
|||
/**
|
||||
* Provide valid decimals or fractions
|
||||
*
|
||||
* @see \Tests\Unit\Rules\StringIsDecimalOrFractionTest::testStringIsDecimalOrFractionRule()
|
||||
* @see \Tests\Unit\Rules\StringIsPositiveDecimalOrFractionTest::testStringIsDecimalOrFractionRule()
|
||||
*/
|
||||
public function invalidDecimalsAndFractions(): array {
|
||||
return [['0'], [0], ['string']];
|
||||
return [['-1'], [-1], ['0'], [0], ['string']];
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide valid decimals or fractions
|
||||
*
|
||||
* @see \Tests\Unit\Rules\StringIsDecimalOrFractionTest::testStringIsDecimalOrFractionRule()
|
||||
* @see \Tests\Unit\Rules\StringIsPositiveDecimalOrFractionTest::testStringIsDecimalOrFractionRule()
|
||||
*/
|
||||
public function validDecimalsAndFractions(): array {
|
||||
return [
|
||||
Loading…
Reference in New Issue