mirror of https://github.com/kcal-app/kcal.git
				
				
				
			Add support for recipe recipe ingredients
This commit is contained in:
		
							parent
							
								
									ff829d9d8d
								
							
						
					
					
						commit
						983b7695dd
					
				|  | @ -8,6 +8,7 @@ use App\Models\Recipe; | ||||||
| use App\Models\RecipeStep; | use App\Models\RecipeStep; | ||||||
| use App\Rules\ArrayNotEmpty; | use App\Rules\ArrayNotEmpty; | ||||||
| use App\Rules\StringIsDecimalOrFraction; | use App\Rules\StringIsDecimalOrFraction; | ||||||
|  | use App\Rules\UsesIngredientTrait; | ||||||
| use App\Support\Number; | use App\Support\Number; | ||||||
| use Illuminate\Contracts\View\View; | use Illuminate\Contracts\View\View; | ||||||
| use Illuminate\Http\RedirectResponse; | use Illuminate\Http\RedirectResponse; | ||||||
|  | @ -83,6 +84,7 @@ class RecipeController extends Controller | ||||||
|                     'amount' => $old['amount'][$key], |                     'amount' => $old['amount'][$key], | ||||||
|                     'unit' => $old['unit'][$key], |                     'unit' => $old['unit'][$key], | ||||||
|                     'ingredient_id' => $ingredient_id, |                     'ingredient_id' => $ingredient_id, | ||||||
|  |                     'ingredient_type' => $old['type'][$key], | ||||||
|                     'ingredient_name' => $old['name'][$key], |                     'ingredient_name' => $old['name'][$key], | ||||||
|                     'detail' => $old['detail'][$key], |                     'detail' => $old['detail'][$key], | ||||||
|                 ]; |                 ]; | ||||||
|  | @ -94,7 +96,8 @@ class RecipeController extends Controller | ||||||
|                     'original_key' => $key, |                     'original_key' => $key, | ||||||
|                     'amount' => $ingredientAmount->amount_formatted, |                     'amount' => $ingredientAmount->amount_formatted, | ||||||
|                     'unit' => $ingredientAmount->unit, |                     'unit' => $ingredientAmount->unit, | ||||||
|                     'ingredient_id' => $ingredientAmount->ingredient->id, |                     'ingredient_id' => $ingredientAmount->ingredient_id, | ||||||
|  |                     'ingredient_type' => $ingredientAmount->ingredient_type, | ||||||
|                     'ingredient_name' => $ingredientAmount->ingredient->name, |                     'ingredient_name' => $ingredientAmount->ingredient->name, | ||||||
|                     'detail' => $ingredientAmount->detail, |                     'detail' => $ingredientAmount->detail, | ||||||
|                 ]; |                 ]; | ||||||
|  | @ -160,12 +163,22 @@ class RecipeController extends Controller | ||||||
|             'ingredients.detail.*' => 'nullable|string', |             'ingredients.detail.*' => 'nullable|string', | ||||||
|             'ingredients.id' => ['required', 'array', new ArrayNotEmpty], |             'ingredients.id' => ['required', 'array', new ArrayNotEmpty], | ||||||
|             'ingredients.id.*' => 'required_with:ingredients.amount.*|nullable', |             'ingredients.id.*' => 'required_with:ingredients.amount.*|nullable', | ||||||
|  |             'ingredients.type' => ['required', 'array', new ArrayNotEmpty], | ||||||
|  |             'ingredients.type.*' => ['required_with:ingredients.id.*', 'nullable', new UsesIngredientTrait()], | ||||||
|             'ingredients.original_key' => 'nullable|array', |             'ingredients.original_key' => 'nullable|array', | ||||||
|             'steps.step' => ['required', 'array', new ArrayNotEmpty], |             'steps.step' => ['required', 'array', new ArrayNotEmpty], | ||||||
|             'steps.step.*' => 'nullable|string', |             'steps.step.*' => 'nullable|string', | ||||||
|             'steps.original_key' => 'nullable|array', |             'steps.original_key' => 'nullable|array', | ||||||
|         ]); |         ]); | ||||||
| 
 | 
 | ||||||
|  |         // Validate that no ingredients are recursive.
 | ||||||
|  |         // TODO: refactor as custom validator.
 | ||||||
|  |         foreach (array_filter($input['ingredients']['id']) as $key => $id) { | ||||||
|  |             if ($input['ingredients']['type'][$key] == Recipe::class && $id == $recipe->id) { | ||||||
|  |                 return back()->withInput()->withErrors('To understand recursion, you must understand recursion. Remove this recipe from this recipe.'); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         $recipe->fill([ |         $recipe->fill([ | ||||||
|             'name' => Str::lower($input['name']), |             'name' => Str::lower($input['name']), | ||||||
|             'description' => $input['description'], |             'description' => $input['description'], | ||||||
|  | @ -202,7 +215,7 @@ class RecipeController extends Controller | ||||||
|                         'weight' => $weight++, |                         'weight' => $weight++, | ||||||
|                     ]); |                     ]); | ||||||
|                     $ingredient_amounts[$key]->ingredient() |                     $ingredient_amounts[$key]->ingredient() | ||||||
|                         ->associate(Food::where('id', $ingredient_id)->first()); |                         ->associate($input['ingredients']['type'][$key]::where('id', $ingredient_id)->first()); | ||||||
|                 } |                 } | ||||||
|                 $recipe->ingredientAmounts()->saveMany($ingredient_amounts); |                 $recipe->ingredientAmounts()->saveMany($ingredient_amounts); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -78,11 +78,6 @@ class Recipe extends Model | ||||||
|         'sodiumPerServing', |         'sodiumPerServing', | ||||||
|     ]; |     ]; | ||||||
| 
 | 
 | ||||||
|     /** |  | ||||||
|      * @inheritdoc |  | ||||||
|      */ |  | ||||||
|     protected $with = ['ingredientAmounts']; |  | ||||||
| 
 |  | ||||||
|     /** |     /** | ||||||
|      * Get the steps for this Recipe. |      * Get the steps for this Recipe. | ||||||
|      */ |      */ | ||||||
|  |  | ||||||
|  | @ -8,6 +8,22 @@ use Illuminate\Support\Collection; | ||||||
| 
 | 
 | ||||||
| trait Ingredient | trait Ingredient | ||||||
| { | { | ||||||
|  |     /** | ||||||
|  |      * Add special `type` attribute to appends. | ||||||
|  |      */ | ||||||
|  |     public function initializeIngredient(): void { | ||||||
|  |         $this->appends[] = 'type'; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Gets the class name. | ||||||
|  |      * | ||||||
|  |      * This is necessary e.g. to provide data in ingredient picker responses. | ||||||
|  |      */ | ||||||
|  |     public function getTypeAttribute(): string { | ||||||
|  |         return $this::class; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * Get all of the ingredient amounts associated with the ingredient. |      * Get all of the ingredient amounts associated with the ingredient. | ||||||
|      */ |      */ | ||||||
|  |  | ||||||
|  | @ -0,0 +1,34 @@ | ||||||
|  | <?php | ||||||
|  | 
 | ||||||
|  | namespace App\Rules; | ||||||
|  | 
 | ||||||
|  | use App\Models\Traits\Ingredient; | ||||||
|  | use Illuminate\Contracts\Validation\Rule; | ||||||
|  | 
 | ||||||
|  | class UsesIngredientTrait implements Rule | ||||||
|  | { | ||||||
|  |     /** | ||||||
|  |      * Determine if the array is empty. | ||||||
|  |      * | ||||||
|  |      * @param string $attribute | ||||||
|  |      * @param mixed $value | ||||||
|  |      * @return bool | ||||||
|  |      */ | ||||||
|  |     public function passes($attribute, $value): bool | ||||||
|  |     { | ||||||
|  |         return ( | ||||||
|  |             class_exists($value) | ||||||
|  |             && in_array(Ingredient::class, class_uses_recursive($value)) | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Get the validation error message. | ||||||
|  |      * | ||||||
|  |      * @return string | ||||||
|  |      */ | ||||||
|  |     public function message(): string | ||||||
|  |     { | ||||||
|  |         return 'Invalid ingredient type :input.'; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -5,6 +5,10 @@ | ||||||
|                             name="ingredients[id][]" |                             name="ingredients[id][]" | ||||||
|                             value="{{ $defaultId ?? '' }}" |                             value="{{ $defaultId ?? '' }}" | ||||||
|                             x-ref="ingredients"/> |                             x-ref="ingredients"/> | ||||||
|  |             <x-inputs.input type="hidden" | ||||||
|  |                             name="ingredients[type][]" | ||||||
|  |                             value="{{ $defaultType ?? '' }}" | ||||||
|  |                             x-ref="ingredients_type"/> | ||||||
|             <x-inputs.input type="text" |             <x-inputs.input type="text" | ||||||
|                             name="ingredients[name][]" |                             name="ingredients[name][]" | ||||||
|                             value="{{ $defaultName ?? '' }}" |                             value="{{ $defaultName ?? '' }}" | ||||||
|  | @ -19,9 +23,12 @@ | ||||||
|                 <template x-for="result in results" :key="result.id"> |                 <template x-for="result in results" :key="result.id"> | ||||||
|                     <div class="p-1 border-b-2 border-gray-500 hover:bg-yellow-300 cursor-pointer" |                     <div class="p-1 border-b-2 border-gray-500 hover:bg-yellow-300 cursor-pointer" | ||||||
|                          x-bind:data-id="result.id" |                          x-bind:data-id="result.id" | ||||||
|                          x-bind:data-value="result.name"> |                          x-bind:data-type="result.type" | ||||||
|  |                          x-bind:data-name="result.name"> | ||||||
|                         <div class="pointer-events-none"> |                         <div class="pointer-events-none"> | ||||||
|                             <div x-text="result.name"></div> |                             <div> | ||||||
|  |                                 <span x-text="result.name"></span><span class="text-gray-600" x-text="', ' + result.detail" x-show="result.detail"></span> | ||||||
|  |                             </div> | ||||||
|                             <div x-show="result.serving_size"> |                             <div x-show="result.serving_size"> | ||||||
|                                 <div class="text-sm text-gray-600" x-text="result.brand" x-show="result.brand"></div> |                                 <div class="text-sm text-gray-600" x-text="result.brand" x-show="result.brand"></div> | ||||||
|                                 <div class="text-sm"> |                                 <div class="text-sm"> | ||||||
|  | @ -54,7 +61,10 @@ | ||||||
|                             if ($event.target.value !== '') { |                             if ($event.target.value !== '') { | ||||||
|                                 fetch('{{ route('ingredient-picker.search') }}?term=' + $event.target.value) |                                 fetch('{{ route('ingredient-picker.search') }}?term=' + $event.target.value) | ||||||
|                                     .then(response => response.json()) |                                     .then(response => response.json()) | ||||||
|                                     .then(data => { this.results = data; this.searching = true; }); |                                     .then(data => { | ||||||
|  |                                         this.results = data; | ||||||
|  |                                         this.searching = true; | ||||||
|  |                                     }); | ||||||
|                             } |                             } | ||||||
|                         }, |                         }, | ||||||
|                         ['@focusout.debounce.200ms']() { |                         ['@focusout.debounce.200ms']() { | ||||||
|  | @ -65,8 +75,9 @@ | ||||||
|                         ['@click']($event) { |                         ['@click']($event) { | ||||||
|                             let selected = $event.target; |                             let selected = $event.target; | ||||||
|                             if (selected.dataset.id) { |                             if (selected.dataset.id) { | ||||||
|                                 this.$refs.ingredients_name.value = selected.dataset.value; |  | ||||||
|                                 this.$refs.ingredients.value = selected.dataset.id; |                                 this.$refs.ingredients.value = selected.dataset.id; | ||||||
|  |                                 this.$refs.ingredients_type.value = selected.dataset.type; | ||||||
|  |                                 this.$refs.ingredients_name.value = selected.dataset.name; | ||||||
|                                 this.searching = false; |                                 this.searching = false; | ||||||
|                             } |                             } | ||||||
|                         } |                         } | ||||||
|  |  | ||||||
|  | @ -10,6 +10,7 @@ | ||||||
|         <option value=""></option> |         <option value=""></option> | ||||||
|     </x-inputs.select> |     </x-inputs.select> | ||||||
|     <x-ingredient-picker :default-id="$ingredient_id ?? null" |     <x-ingredient-picker :default-id="$ingredient_id ?? null" | ||||||
|  |                          :default-type="$ingredient_type ?? null" | ||||||
|                          :default-name="$ingredient_name ?? null" /> |                          :default-name="$ingredient_name ?? null" /> | ||||||
|     <x-inputs.input type="text" |     <x-inputs.input type="text" | ||||||
|                     class="block" |                     class="block" | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue