diff --git a/app/Http/Controllers/Bo/ProductController.php b/app/Http/Controllers/Bo/ProductController.php new file mode 100644 index 0000000..37e08df --- /dev/null +++ b/app/Http/Controllers/Bo/ProductController.php @@ -0,0 +1,181 @@ +authorizeResource(Product::class); + } + + /** + * Display a listing of the resource. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Contracts\View\View + */ + public function index(Request $request): \Illuminate\Contracts\View\View + { + /** @var \Illuminate\Database\Eloquent\Builder $productModels */ + $productModels = Product::query(); + + /** @var string $search Search field */ + $search = $request->search; + if ($search) { + $this->searchQuery( + $productModels, + $search, + null, + 'title', + 'subtitle', + 'description', + ); + } + $searchFields = \implode(', ', [ + Str::ucfirst(trans('validation.attributes.title')), + trans('validation.custom.subtitle'), + trans('validation.custom.description'), + ]); + + /** Sort columns with a query */ + $this->sortQuery($productModels); + + /** Custom pagination */ + $productModels = $this->paginate($productModels); + + return view('bo.pages.products.index', compact('productModels', 'search', 'searchFields')); + } + + + /** + * Show the form for creating a new resource. + * + * @param \App\Models\Product $product + * @return \Illuminate\Contracts\View\View + */ + public function create(Product $product): \Illuminate\Contracts\View\View + { + $rangeModels = Range::all(); + return view('bo.pages.products.create', [ + 'productModel' => $product, + 'rangeModels' => $rangeModels, + ]); + } + + /** + * Store a newly created resource in storage. + * + * @param \App\Http\Requests\Bo\Products\StoreProductRequest $request + * @return \Illuminate\Http\RedirectResponse + */ + public function store(StoreProductRequest $request): \Illuminate\Http\RedirectResponse + { + return DB::transaction(function () use ($request) { + $product = new Product(); + $product->fill($request->validated()); + + if ($product->saveOrFail()) { + return redirect()->route('bo.products.edit', $product) + ->with('success', __('crud.messages.has_been_created', [ + 'model' => Str::ucfirst(__('models.classes.product')) + ])); + } + return redirect()->back() + ->with('error', __('crud.messages.cannot_be_created', [ + 'model' => Str::ucfirst(__('models.classes.product')) + ])); + }); + } + + /** + * Show the form for editing the specified resource. + * + * @param App\Models\Product $product + * @return \Illuminate\Contracts\View\View + */ + public function edit(Product $product): \Illuminate\Contracts\View\View + { + $rangeModels = Range::all(); + return view('bo.pages.products.edit', [ + 'productModel' => $product, + 'rangeModels' => $rangeModels, + ]); + } + + /** + * Update the specified resource in storage. + * + * @param \App\Http\Requests\Bo\Products\UpdateProductRequest $request + * @param \App\Models\Product $product + * @return \Illuminate\Http\RedirectResponse + */ + public function update(UpdateProductRequest $request, Product $product): \Illuminate\Http\RedirectResponse + { + return DB::transaction(function () use ($request, $product) { + $product->fill($request->validated()); + + if ($product->saveOrFail()) { + return redirect()->route('bo.products.edit', $product) + ->with('success', __('crud.messages.has_been_updated', [ + 'model' => Str::ucfirst(__('models.classes.product')) + ])); + } + return redirect()->route('bo.products.edit', $product) + ->with('error', __('crud.messages.cannot_be_updated', [ + 'model' => Str::ucfirst(__('models.classes.product')) + ])); + }); + } + + /** + * Remove the specified resource from storage. + * + * @param \App\Models\Product $product + * @return \Illuminate\Http\RedirectResponse + */ + public function destroy(Product $product): \Illuminate\Http\RedirectResponse + { + return DB::transaction(function () use ($product) { + $product->commitments()->detach(); + if ($product->deleteOrFail()) { + return redirect()->route('bo.products.index') + ->with('success', __('crud.messages.has_been_deleted', [ + 'model' => Str::ucfirst(__('models.classes.product')) + ])); + } + return redirect()->back() + ->with('error', __('crud.messages.cannot_be_deleted', [ + 'model' => Str::ucfirst(__('models.classes.product')) + ])); + }); + } + + /** + * Duplicate the specified resource. + * + * @param \App\Models\Product $product + * @return \Illuminate\Contracts\View\View + */ + public function duplicate(Product $product): \Illuminate\Contracts\View\View + { + return $this->create($product->replicate()); + } +} diff --git a/app/Http/Requests/Bo/Products/StoreProductRequest.php b/app/Http/Requests/Bo/Products/StoreProductRequest.php new file mode 100644 index 0000000..7219c9c --- /dev/null +++ b/app/Http/Requests/Bo/Products/StoreProductRequest.php @@ -0,0 +1,156 @@ +check(); + } + + /** + * Prepare the data for validation. + * + * @return void + */ + protected function prepareForValidation() + { + $this->merge([ + 'published' => $this->published ? true : false + ]); + $this->mergePicture('picture_thumbnail'); + $this->mergePicture('picture_ambiance'); + $this->mergePicture('picture_packshot'); + } + + /** + * Get the validation rules that apply to the request. + * + * @return array + */ + public function rules(): array + { + $rules = [ + 'seo_title' => 'required|between:45,70', + 'seo_description' => 'required|between:50,160', + 'title' => 'required|string|between:3,100', + 'subtitle' => 'required|string|between:3,255', + 'picture_thumbnail_alt' => 'required|string|between:3,255', + 'picture_thumbnail_title' => 'required|string|between:3,255', + 'picture_ambiance_alt' => 'required|string|between:3,255', + 'picture_ambiance_title' => 'required|string|between:3,255', + 'picture_packshot_alt' => 'required|string|between:3,255', + 'picture_packshot_title' => 'required|string|between:3,255', + 'description' => 'required|string|min:15', + 'cooking_mode' => ['required', new Enum(CookingModeEnum::class)], + 'cooking_advice' => 'required|string|min:15', + 'ingredients' => 'required|string|min:15', + 'nutritionnal_values' => 'required|array', + 'nutritionnal_values.*' => 'required|numeric|min:0', + 'range_id' => 'required|exists:ranges,id', + 'published' => 'required|boolean', + ]; + return \array_merge( + $rules, + $this->pictureRules('picture_thumbnail', 332, 252, 332, 252, false), + $this->pictureRules('picture_ambiance', 774, 515, 774, 515, false), + $this->pictureRules('picture_packshot', 327, 252, 327, 252, false), + ); + } + + /** + * Override validated() function from FormRequest. + * + * @param array|integer|string|null $key + * @param mixed $default + * @return array + * @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassAfterLastUsed + */ + public function validated($key = null, mixed $default = null): array + { + // phpcs:enable + $nutritionnal_values = json_encode([ + "energyKj" => [ + "label" => "Energie en Kj", + "value" => $this->nutritionnal_values['energy_kj'], + ], + "energyKcal" => [ + "label" => "Energie en kcal", + "value" => $this->nutritionnal_values['energy_kcal'], + ], + "fat" => [ + "label" => "Matières grasses", + "value" => $this->nutritionnal_values['fat'], + ], + "saturated-fat" => [ + "label" => "dont acides gras saturés", + "value" => $this->nutritionnal_values['saturated_fat'], + ], + "carbohydrate" => [ + "label" => "Glucides", + "value" => $this->nutritionnal_values['carbohydrate'], + ], + "sugar" => [ + "label" => "dont sucres", + "value" => $this->nutritionnal_values['sugar'], + ], + "proteins" => [ + "label" => "Protéines", + "value" => $this->nutritionnal_values['proteins'], + ], + "salt" => [ + "label" => "Sel", + "value" => $this->nutritionnal_values['salt'], + ] + ]); + + return array_merge( + parent::validated(), + [ + 'nutritionnal_values' => $nutritionnal_values + ] + ); + } + + + /** + * Get custom attributes for validator errors. + * + * @return array + */ + public function attributes(): array + { + return [ + 'seo_title' => trans('validation.custom.seo_title'), + 'seo_description' => trans('validation.custom.seo_description'), + 'title' => trans('validation.attributes.title'), + 'subtitle' => trans('validation.custom.subtitle'), + 'description' => trans('validation.custom.description'), + 'picture_thumbnail_alt' => trans('validation.custom.picture_thumbnail_alt'), + 'picture_thumbnail_title' => trans('validation.custom.picture_thumbnail_title'), + 'picture_ambiance_alt' => trans('validation.custom.picture_ambiance_alt'), + 'picture_ambiance_title' => trans('validation.custom.picture_ambiance_title'), + 'picture_packshot_alt' => trans('validation.custom.picture_packshot_alt'), + 'picture_packshot_title' => trans('validation.custom.picture_packshot_title'), + 'cooking_mode' => trans('validation.custom.cooking_mode'), + 'cooking_advice' => trans('validation.custom.cooking_advice'), + 'ingredients' => trans('validation.custom.ingredients'), + 'nutritionnal_values' => trans('validation.custom.nutritionnal_values'), + 'range_id' => trans('models.classes.range'), + 'published' => trans('validation.custom.activated'), + ]; + } +} diff --git a/app/Http/Requests/Bo/Products/UpdateProductRequest.php b/app/Http/Requests/Bo/Products/UpdateProductRequest.php new file mode 100644 index 0000000..3f0ae9d --- /dev/null +++ b/app/Http/Requests/Bo/Products/UpdateProductRequest.php @@ -0,0 +1,7 @@ +sentence(); + $nutritionnal_values = [ + 'energyKj' => [ + 'label' => 'Energie en Kj', + 'value' => fake()->randomFloat(2, 0, 70) + ], + 'energyKcal' => [ + 'label' => 'Energie en kcal', + 'value' => fake()->randomFloat(2, 0, 70) + ], + 'fat' => [ + 'label' => 'Matières grasses', + 'value' => fake()->randomFloat(2, 0, 70) + ], + 'saturated-fat' => [ + 'label' => 'dont acides gras saturés', + 'value' => fake()->randomFloat(2, 0, 70) + ], + 'carbohydrate' => [ + 'label' => 'Glucides', + 'value' => fake()->randomFloat(2, 0, 70) + ], + 'sugar' => [ + 'label' => 'dont sucres', + 'value' => fake()->randomFloat(2, 0, 70) + ], + 'proteins' => [ + 'label' => 'Protéines', + 'value' => fake()->randomFloat(2, 0, 70) + ], + 'salt' => [ + 'label' => 'Sel', + 'value' => fake()->randomFloat(2, 0, 70) + ], + ]; + return [ 'seo_title' => fake()->word(), 'seo_description' => fake()->text(160), @@ -34,15 +69,7 @@ class ProductFactory extends Factory 'cooking_mode' => \collect(CookingModeEnum::cases())->random(), 'cooking_advice' => \clean(\fake()->randomHtml(6, 7)), 'ingredients' => \clean(\fake()->randomHtml(6, 7)), - 'nutritionnal_values' => json_encode([ - 'Energie en Kj' => fake()->randomFloat(2), - 'Energie en kcal' => fake()->randomFloat(2), - 'Matières grasses' => fake()->randomFloat(2), - 'dont acides gras saturés' => fake()->randomFloat(2), - 'Glucides' => fake()->randomFloat(2), - 'Protéines' => fake()->randomFloat(2), - 'Sel' => fake()->randomFloat(2), - ]), + 'nutritionnal_values' => json_encode($nutritionnal_values), 'published' => fake()->boolean(75), 'range_id' => Range::all()->random()->id, ]; diff --git a/database/seeders/ProductSeeder.php b/database/seeders/ProductSeeder.php index 0ee156b..ab0c4a3 100644 --- a/database/seeders/ProductSeeder.php +++ b/database/seeders/ProductSeeder.php @@ -17,17 +17,18 @@ class ProductSeeder extends Seeder public function run() { FileStorageHelper::removeModelFiles(new Product()); - Product::factory(25)->create([ - 'picture_thumbnail' => FileStorageHelper::storeFile(new Product(), new \SplFileInfo(\resource_path( + Product::factory(25)->make()->each(function (Product $product) { + $product->picture_thumbnail = FileStorageHelper::storeFile(new Product(), new \SplFileInfo(\resource_path( '../database/factories/assets/products/default-picture-332x252.png' - ))), - 'picture_ambiance' => FileStorageHelper::storeFile(new Product(), new \SplFileInfo(\resource_path( + ))); + $product->picture_ambiance = FileStorageHelper::storeFile(new Product(), new \SplFileInfo(\resource_path( '../database/factories/assets/products/default-picture-774x515.png' - ))), - 'picture_packshot' => FileStorageHelper::storeFile(new Product(), new \SplFileInfo(\resource_path( + ))); + $product->picture_packshot = FileStorageHelper::storeFile(new Product(), new \SplFileInfo(\resource_path( '../database/factories/assets/products/default-picture-327x252.png' - ))) - ]); + ))); + $product->save(); + }); $commitments = Commitment::query()->get(); Product::query()->each(function (Product $product) use ($commitments) { diff --git a/resources/lang/en/crud.php b/resources/lang/en/crud.php index 93b34d8..04fe327 100644 --- a/resources/lang/en/crud.php +++ b/resources/lang/en/crud.php @@ -32,6 +32,7 @@ return [ 'visibility_with_dates' => 'This field will only have an effect if no date is entered.', 'publish_at' => 'The :model will be published from that date.', 'unpublish_at' => 'The :model will be unpublished from that date.', + 'nutritionnal_values' => 'Values must be given in grams, except for energy in kJ and kcal.', ], 'libs' => [ diff --git a/resources/lang/en/models.php b/resources/lang/en/models.php index 8584e6e..49acfd5 100644 --- a/resources/lang/en/models.php +++ b/resources/lang/en/models.php @@ -9,9 +9,9 @@ return [ 'seasonal_section' => 'seasonal section|seasonal sections', 'news' => 'news', 'stamp' => 'stamp', - 'commitment' => 'commitment', - 'range' => 'range', - 'homepage' => 'home page', - 'explorerpage' => 'explorer page', + 'homepage' => 'home page', + 'explorerpage' => 'explorer page', + 'commitment' => 'commitment', + 'product' => 'product', ] ]; diff --git a/resources/lang/en/validation.php b/resources/lang/en/validation.php index 49aab12..4fa21f3 100644 --- a/resources/lang/en/validation.php +++ b/resources/lang/en/validation.php @@ -252,5 +252,10 @@ return [ 'category' => 'category', 'publish_at' => 'publication date', 'unpublish_at' => 'end of publication date', + 'picture_ambiance' => 'ambiance picture', + 'picture_packshot' => 'packshot picture', + 'cooking_advice' => 'cooking advice', + 'ingredients' => 'ingredients', + 'nutritionnal_values' => 'nutritionnal values', ], ]; diff --git a/resources/lang/fr/crud.php b/resources/lang/fr/crud.php index 7117ede..644d141 100644 --- a/resources/lang/fr/crud.php +++ b/resources/lang/fr/crud.php @@ -32,6 +32,8 @@ return [ 'visibility_with_dates' => 'Ce champ n\'aura d\'effet que si aucune date n\'est renseignée.', 'publish_at' => 'Le :model sera publié à partir de cette date.', 'unpublish_at' => 'Le :model sera dépublié à partir de cette date.', + 'nutritionnal_values' => 'Les valeurs doivent être renseignées en grammes, + exceptés pour l\'énergie en kJ et en kcal.', ], 'libs' => [ @@ -68,7 +70,7 @@ return [ 'picture_title_label' => 'Attribut du titre de l\'image', 'picture_alt_helper' => 'Doit décrire votre image pour les moteurs de recherche et les aveugles (5 à 6 mots)', 'picture_alt_label' => 'Attribut Alt de l\'image', - 'text_warning' => 'Relisez votre contenu avant de le publier', + 'text_warning' => 'Relisez votre contenu avant de le publier.', ], 'pagination' => [ diff --git a/resources/lang/fr/models.php b/resources/lang/fr/models.php index f5661eb..5432d52 100644 --- a/resources/lang/fr/models.php +++ b/resources/lang/fr/models.php @@ -12,5 +12,6 @@ return [ 'news' => 'actualité', 'stamp' => 'tampon', 'explorerpage' => 'page explorateur', + 'product' => 'produit', ] ]; diff --git a/resources/lang/fr/validation.php b/resources/lang/fr/validation.php index 679e364..bdbde5b 100644 --- a/resources/lang/fr/validation.php +++ b/resources/lang/fr/validation.php @@ -254,5 +254,10 @@ return [ 'category' => 'catégorie', 'publish_at' => 'date de publication', 'unpublish_at' => 'date de fin de publication', + 'picture_ambiance' => 'image d\'ambiance', + 'picture_packshot' => 'image du packshot', + 'cooking_advice' => 'conseil de réchauffage', + 'ingredients' => 'ingrédients', + 'nutritionnal_values' => 'valeurs nutritionnelles', ], ]; diff --git a/resources/views/bo/layouts/nav-list.blade.php b/resources/views/bo/layouts/nav-list.blade.php index 1bba1c8..3b5ee44 100644 --- a/resources/views/bo/layouts/nav-list.blade.php +++ b/resources/views/bo/layouts/nav-list.blade.php @@ -24,14 +24,6 @@ @endcan - @can('viewAny', \App\Models\Range::class) -
  • - - {{ Str::of(__('models.classes.range'))->plural()->ucfirst() }} - -
  • - @endcan @can('viewAny', \App\Models\News::class) @endcan + @canAny(['isConceptor', 'viewAny'], \App\Models\User::class)