Improve Jorunal Entry validation feedback

This commit is contained in:
Christopher C. Wells 2021-04-17 19:39:07 -07:00
parent 0982ac1601
commit ff5661fdf1
11 changed files with 150 additions and 63 deletions

View File

@ -5,13 +5,11 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Http\Requests\StoreFromNutrientsJournalEntryRequest;
use App\Http\Requests\StoreJournalEntryRequest;
use App\Models\Food; use App\Models\Food;
use App\Models\JournalEntry; use App\Models\JournalEntry;
use App\Models\Recipe; 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\ArrayFormat;
use App\Support\Number; use App\Support\Number;
use App\Support\Nutrients; use App\Support\Nutrients;
@ -85,6 +83,7 @@ class JournalEntryController extends Controller
continue; continue;
} }
$ingredients[$key] = [ $ingredients[$key] = [
'key' => $key,
'date' => $old['date'][$key], 'date' => $old['date'][$key],
'meal' => $old['meal'][$key], 'meal' => $old['meal'][$key],
'amount' => $amount, 'amount' => $amount,
@ -130,28 +129,9 @@ class JournalEntryController extends Controller
/** /**
* Store a newly created resource in storage. * Store a newly created resource in storage.
*/ */
public function store(Request $request): RedirectResponse public function store(StoreJournalEntryRequest $request): RedirectResponse
{ {
$input = $request->validate([ $input = $request->validated();
'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'],
]);
$ingredients = ArrayFormat::flipTwoDimensionalKeys($input['ingredients']); $ingredients = ArrayFormat::flipTwoDimensionalKeys($input['ingredients']);
@ -284,23 +264,8 @@ class JournalEntryController extends Controller
/** /**
* Store an entry from nutrients. * Store an entry from nutrients.
*/ */
public function storeFromNutrients(Request $request): RedirectResponse { public function storeFromNutrients(StoreFromNutrientsJournalEntryRequest $request): RedirectResponse {
$attributes = $request->validate([ $attributes = $request->validated();
'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'],
]);
$entry = JournalEntry::make(array_filter($attributes)) $entry = JournalEntry::make(array_filter($attributes))
->user()->associate(Auth::user()); ->user()->associate(Auth::user());
$entry->save(); $entry->save();

View File

@ -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'],
];
}
}

View File

@ -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',
];
}
}

View File

@ -2,7 +2,7 @@
namespace App\Http\Requests; namespace App\Http\Requests;
use App\Rules\StringIsDecimalOrFraction; use App\Rules\StringIsPositiveDecimalOrFraction;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
class UpdateFoodRequest extends FormRequest class UpdateFoodRequest extends FormRequest
@ -19,7 +19,7 @@ class UpdateFoodRequest extends FormRequest
'brand' => ['nullable', 'string'], 'brand' => ['nullable', 'string'],
'source' => ['nullable', 'string'], 'source' => ['nullable', 'string'],
'notes' => ['nullable', 'string'], 'notes' => ['nullable', 'string'],
'serving_size' => ['required', new StringIsDecimalOrFraction], 'serving_size' => ['required', new StringIsPositiveDecimalOrFraction],
'serving_unit' => ['nullable', 'string'], 'serving_unit' => ['nullable', 'string'],
'serving_unit_name' => ['nullable', 'string'], 'serving_unit_name' => ['nullable', 'string'],
'serving_weight' => ['required', 'numeric', 'min:0'], 'serving_weight' => ['required', 'numeric', 'min:0'],

View File

@ -3,7 +3,7 @@
namespace App\Http\Requests; namespace App\Http\Requests;
use App\Rules\ArrayNotEmpty; use App\Rules\ArrayNotEmpty;
use App\Rules\StringIsDecimalOrFraction; use App\Rules\StringIsPositiveDecimalOrFraction;
use App\Rules\UsesIngredientTrait; use App\Rules\UsesIngredientTrait;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
@ -27,7 +27,7 @@ class UpdateRecipeRequest extends FormRequest
'weight' => ['nullable', 'numeric'], 'weight' => ['nullable', 'numeric'],
'source' => ['nullable', 'string'], 'source' => ['nullable', 'string'],
'ingredients.amount' => ['required', 'array', new ArrayNotEmpty], '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', 'array'],
'ingredients.unit.*' => ['required_with:ingredients.id.*'], 'ingredients.unit.*' => ['required_with:ingredients.id.*'],
'ingredients.detail' => ['required', 'array'], 'ingredients.detail' => ['required', 'array'],

View File

@ -5,7 +5,7 @@ namespace App\Rules;
use App\Support\Number; use App\Support\Number;
use Illuminate\Contracts\Validation\Rule; use Illuminate\Contracts\Validation\Rule;
class StringIsDecimalOrFraction implements Rule class StringIsPositiveDecimalOrFraction implements Rule
{ {
/** /**
* {@inheritdoc} * {@inheritdoc}
@ -14,7 +14,7 @@ class StringIsDecimalOrFraction implements Rule
{ {
try { try {
$result = Number::floatFromString($value); $result = Number::floatFromString($value);
return $result != 0; return $result > 0;
} }
catch (\InvalidArgumentException $e) { catch (\InvalidArgumentException $e) {
// Allow to pass through, method will return false. // Allow to pass through, method will return false.
@ -27,6 +27,6 @@ class StringIsDecimalOrFraction implements Rule
*/ */
public function message(): string public function message(): string
{ {
return 'The :attribute must be a decimal or fraction.'; return 'The :attribute must be a positive decimal or fraction.';
} }
} }

View File

@ -8,20 +8,20 @@ use Illuminate\View\Component;
class Select extends Component class Select extends Component
{ {
public ?bool $hasError;
public Collection|array $options; public Collection|array $options;
public ?string $selectedValue; public ?string $selectedValue;
/** /**
* Select constructor. * Select constructor.
*
* @param \Illuminate\Support\Collection|array $options
* @param ?string $selectedValue
*/ */
public function __construct( public function __construct(
Collection|array $options, Collection|array $options,
?bool $hasError = false,
?string $selectedValue = '', ?string $selectedValue = '',
) { ) {
$this->options = $options; $this->options = $options;
$this->hasError = $hasError;
$this->selectedValue = $selectedValue; $this->selectedValue = $selectedValue;
} }
@ -29,6 +29,7 @@ class Select extends Component
{ {
return view('components.inputs.select') return view('components.inputs.select')
->with('options', $this->options) ->with('options', $this->options)
->with('hasError', $this->hasError)
->with('selectedValue', $this->selectedValue); ->with('selectedValue', $this->selectedValue);
} }

View File

@ -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 }} {{ $slot }}
<x-inputs.select-options :options="$options" :selectedValue="$selectedValue" /> <x-inputs.select-options :options="$options" :selectedValue="$selectedValue" />
</select> </select>

View File

@ -19,6 +19,7 @@
type="date" type="date"
class="block w-full" class="block w-full"
:value="old('date', $default_date->toDateString())" :value="old('date', $default_date->toDateString())"
:hasError="$errors->has('date')"
required /> required />
</div> </div>
@ -30,6 +31,7 @@
class="block w-full" class="block w-full"
:options="$meals" :options="$meals"
:selectedValue="old('meal')" :selectedValue="old('meal')"
:hasError="$errors->has('meal')"
required> required>
<option value=""></option> <option value=""></option>
</x-inputs.select> </x-inputs.select>
@ -43,6 +45,7 @@
type="text" type="text"
class="block w-full" class="block w-full"
:value="old('summary')" :value="old('summary')"
:hasError="$errors->has('summary')"
required /> required />
</div> </div>
</div> </div>
@ -58,7 +61,8 @@
type="number" type="number"
step="any" step="any"
class="block w-full" class="block w-full"
:value="old($nutrient['value'])"/> :value="old($nutrient['value'])"
:hasError="$errors->has($nutrient['value'])"/>
</div> </div>
@endforeach @endforeach
</div> </div>

View File

@ -1,3 +1,4 @@
@php($key = $key ?? null)
<div x-data class="entry-item flex items-center space-x-2"> <div x-data class="entry-item flex items-center space-x-2">
<div class="flex flex-col space-y-4 w-full"> <div class="flex flex-col space-y-4 w-full">
<!-- Ingredient --> <!-- Ingredient -->
@ -15,6 +16,7 @@
type="date" type="date"
class="block w-full" class="block w-full"
:value="$date ?? $default_date->toDateString()" :value="$date ?? $default_date->toDateString()"
:hasError="$errors->has('ingredients.date.' . $key)"
required /> required />
</div> </div>
@ -25,8 +27,9 @@
class="block w-full" class="block w-full"
:options="$meals" :options="$meals"
:selectedValue="$meal ?? null" :selectedValue="$meal ?? null"
:hasError="$errors->has('ingredients.meal.' . $key)"
required> required>
<option value="">-- Meal --</option> @if(is_null($key))<option value="">-- Meal --</option>@endif
</x-inputs.select> </x-inputs.select>
</div> </div>
@ -39,6 +42,7 @@
class="block w-full" class="block w-full"
placeholder="Amount" placeholder="Amount"
:value="$amount ?? null" :value="$amount ?? null"
:hasError="$errors->has('ingredients.amount.' . $key)"
required /> required />
</div> </div>
@ -48,8 +52,9 @@
<x-inputs.select name="ingredients[unit][]" <x-inputs.select name="ingredients[unit][]"
class="block w-full" class="block w-full"
:options="$units ?? []" :options="$units ?? []"
:selectedValue="$unit ?? null"> :selectedValue="$unit ?? null"
<option value="">-- Unit --</option> :hasError="$errors->has('ingredients.unit.' . $key)">
@if(is_null($key))<option value="">-- Unit --</option>@endif
</x-inputs.select> </x-inputs.select>
</div> </div>
</div> </div>

View File

@ -2,9 +2,9 @@
namespace Tests\Unit\Rules; 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 public function setUp(): void
{ {
parent::setUp(); 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 * Provide valid decimals or fractions
* *
* @see \Tests\Unit\Rules\StringIsDecimalOrFractionTest::testStringIsDecimalOrFractionRule() * @see \Tests\Unit\Rules\StringIsPositiveDecimalOrFractionTest::testStringIsDecimalOrFractionRule()
*/ */
public function invalidDecimalsAndFractions(): array { public function invalidDecimalsAndFractions(): array {
return [['0'], [0], ['string']]; return [['-1'], [-1], ['0'], [0], ['string']];
} }
/** /**
* Provide valid decimals or fractions * Provide valid decimals or fractions
* *
* @see \Tests\Unit\Rules\StringIsDecimalOrFractionTest::testStringIsDecimalOrFractionRule() * @see \Tests\Unit\Rules\StringIsPositiveDecimalOrFractionTest::testStringIsDecimalOrFractionRule()
*/ */
public function validDecimalsAndFractions(): array { public function validDecimalsAndFractions(): array {
return [ return [