mirror of https://github.com/kcal-app/kcal.git
Improve Jorunal Entry validation feedback
This commit is contained in:
parent
0982ac1601
commit
ff5661fdf1
|
@ -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();
|
||||
|
|
|
@ -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',
|
||||
];
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Rules\StringIsDecimalOrFraction;
|
||||
use App\Rules\StringIsPositiveDecimalOrFraction;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateFoodRequest extends FormRequest
|
||||
|
@ -19,7 +19,7 @@ class UpdateFoodRequest extends FormRequest
|
|||
'brand' => ['nullable', 'string'],
|
||||
'source' => ['nullable', 'string'],
|
||||
'notes' => ['nullable', 'string'],
|
||||
'serving_size' => ['required', new StringIsDecimalOrFraction],
|
||||
'serving_size' => ['required', new StringIsPositiveDecimalOrFraction],
|
||||
'serving_unit' => ['nullable', 'string'],
|
||||
'serving_unit_name' => ['nullable', 'string'],
|
||||
'serving_weight' => ['required', 'numeric', 'min:0'],
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
namespace App\Http\Requests;
|
||||
|
||||
use App\Rules\ArrayNotEmpty;
|
||||
use App\Rules\StringIsDecimalOrFraction;
|
||||
use App\Rules\StringIsPositiveDecimalOrFraction;
|
||||
use App\Rules\UsesIngredientTrait;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
|
@ -27,7 +27,7 @@ class UpdateRecipeRequest extends FormRequest
|
|||
'weight' => ['nullable', 'numeric'],
|
||||
'source' => ['nullable', 'string'],
|
||||
'ingredients.amount' => ['required', 'array', new ArrayNotEmpty],
|
||||
'ingredients.amount.*' => ['required_with:ingredients.id.*', 'nullable', new StringIsDecimalOrFraction],
|
||||
'ingredients.amount.*' => ['required_with:ingredients.id.*', 'nullable', new StringIsPositiveDecimalOrFraction],
|
||||
'ingredients.unit' => ['required', 'array'],
|
||||
'ingredients.unit.*' => ['required_with:ingredients.id.*'],
|
||||
'ingredients.detail' => ['required', 'array'],
|
||||
|
|
|
@ -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.';
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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