Product crud

This commit is contained in:
Jérémy Munsch 2022-07-13 15:37:42 +02:00
parent 91c3f379ec
commit aecab74e93
No known key found for this signature in database
GPG key ID: 18361AF3BFEC0FEC
22 changed files with 1176 additions and 79 deletions

View file

@ -0,0 +1,153 @@
<?php
namespace App\Http\Controllers\Bo;
use App\Http\Controllers\Controller;
use App\Http\Requests\Bo\Products\StoreProductRequest;
use App\Http\Requests\Bo\Products\UpdateProductRequest;
use App\Models\Product;
use App\Models\Range;
use App\Traits\Controllers\UpdateModelStatus;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class ProductController extends Controller
{
use UpdateModelStatus;
/**
* Display a listing of the resource.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function index(Request $request)
{
/** @var \Illuminate\Database\Eloquent\Builder $products */
$products = Product::query();
if ($request->search) {
$this->searchQuery(
$products,
$request->search,
// * Search in tags
function ($q) use ($request) {
$q->orWhereHas('tags', function ($q) use ($request) {
$q->whereRaw(
'UPPER(CONCAT(`tags`.`name`)) LIKE UPPER(?)',
['%' . filter_var($request->search, FILTER_SANITIZE_STRING) . '%']
);
});
},
'title',
'description'
);
}
$this->sortQuery($products);
$searchFields = trans('Titre, Description');
$products->orderBy('updated_at', 'DESC');
$products = $products->paginate();
return view('back.products.index', compact('products', 'searchFields'));
}
/**
* Show the form for creating a new resource.
*
* @return \Illuminate\Http\Response
*/
public function create()
{
$ranges = Range::all();
return view('back.products.create', compact('ranges'));
}
/**
* Store a newly created resource in storage.
*
* @param \App\Http\Requests\Bo\Products\StoreProductRequest $request
* @return \Illuminate\Http\Response
*/
public function store(StoreProductRequest $request)
{
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', trans(
':model a été créé',
['model' => Str::singular(trans('models.classes.products'))]
));
}
return back();
});
}
/**
* Show the form for editing the specified resource.
*
* @param \App\Models\Product $product
* @return \Illuminate\Http\Response
*/
public function edit(Product $product)
{
$ranges = Range::all();
return view('back.products.edit', [
'productModel' => $product,
'ranges' => $ranges,
]);
}
/**
* Update the specified resource in storage.
*
* @param \App\Http\Requests\Bo\Products\UpdateProductRequest $request
* @param \App\Models\Product $product
* @return \Illuminate\Http\Response
*/
public function update(UpdateProductRequest $request, Product $product)
{
return DB::transaction(function () use ($request, $product) {
$product->fill($request->validated());
/** @var \App\Models\IncoNutritionalValue */
$nut = $product->incoNutritionalValues()->firstOrNew();
$nut->fill($request->validated());
$nut->saveOrFail();
if ($product->saveOrFail()) {
return redirect()
->route('bo.products.edit', $product)
->with('success', trans(
':model a été mis à jour',
['model' => Str::singular(trans('models.classes.products'))]
));
}
return back();
});
}
/**
* Remove the specified resource from storage.
*
* @param \App\Models\Product $product
* @return \Illuminate\Http\Response
*/
public function destroy(Product $product)
{
if ($product->deleteOrFail()) {
return redirect()
->route('bo.products.index')
->with('warning', trans(
':model a bien été supprimé',
['model' => Str::singular(trans('models.classes.products'))]
));
}
return back();
}
}

View file

@ -0,0 +1,141 @@
<?php
namespace App\Http\Requests\Bo\Products;
use App\Enums\Products\ProductLabelEnum;
use App\Traits\Requests\HasPicture;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Str;
use Kwaadpepper\Enum\Rules\EnumIsValidRule;
/**
* Unicity for fields is handled by model events
*/
class StoreProductRequest extends FormRequest
{
use HasPicture;
/**
* Determine if the user is authorized to make this request.
*
* @return boolean
*/
public function authorize()
{
return auth('backend')->check();
}
/**
* Prepare the data for validation.
*
* @return void
*/
protected function prepareForValidation()
{
$this->merge([
'slug' => Str::slug($this->title),
'status' => $this->status ? true : false,
'show_ingredients' => $this->show_ingredients ? true : false,
'show_preparation_advices' => $this->show_preparation_advices ? true : false,
'show_sales_pitch' => $this->show_sales_pitch ? true : false,
'show_allergen_list' => $this->show_allergen_list ? true : false,
'show_nutrionnal_values' => $this->show_nutrionnal_values ? true : false,
'show_portions' => $this->show_portions ? true : false,
'labels' => $this->labels ?? []
]);
$this->mergePicture();
$this->mergePicture('picture_thumb');
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return \array_merge(
[
'title' => 'required|min:6|max:255',
'slug' => 'required|max:255',
'seo_title' => 'required|min:45|max:70',
'seo_description' => 'required|min:50|max:160',
'description' => 'required|string|min:15',
'picture_alt' => 'required_with:picture|nullable|string|min:6|max:255',
'picture_title' => 'required_with:picture|nullable|string|min:6|max:255',
'picture_thumb_alt' => 'required_with:picture_thumb|nullable|string|min:6|max:255',
'picture_thumb_title' => 'required_with:picture_thumb|nullable|string|min:6|max:255',
'publish_at' => 'nullable|date_format:Y-m-d\TH:i',
'unpublish_at' => 'nullable|date_format:Y-m-d\TH:i',
'status' => 'required|boolean',
'range_id' => 'required|exists:ranges,id',
'ingredients' => 'required|string|min:15',
'preparation_advices' => 'required|string|min:15',
'sales_pitch' => 'required|string|min:15',
'allergen_list' => 'required|string|min:15',
'number_portion' => 'required|numeric|min:0',
'show_ingredients' => 'required|boolean',
'show_preparation_advices' => 'required|boolean',
'show_sales_pitch' => 'required|boolean',
'show_allergen_list' => 'required|boolean',
'show_nutrionnal_values' => 'required|boolean',
'show_portions' => 'required|boolean',
'labels' => 'sometimes|array',
'labels.*' => 'sometimes|array',
'labels.*.value' => ['required', new EnumIsValidRule(ProductLabelEnum::class)],
'weight' => 'required|numeric',
'energy' => 'required|numeric',
'energy_kj' => 'required|numeric',
'protein' => 'required|numeric',
'carbohydrates' => 'required|numeric',
'sugar' => 'required|numeric',
'fat' => 'required|numeric',
'saturated_fatty_acids' => 'required|numeric',
'dietary_fiber' => 'required|numeric',
'salt' => 'required|numeric',
],
$this->pictureRules('picture', 700, 700, 700, 700),
$this->pictureRules('picture_thumb', 300, 300, 300, 300)
);
}
/**
* Get custom attributes for validator errors.
*
* @return array
*/
public function attributes(): array
{
return [
'title' => trans('title'),
'slug' => trans('title'),
'seo_title' => trans('title') . ' seo',
'seo_description' => trans('description') . ' seo',
'description' => trans('description'),
'picture' => trans('illustration'),
'picture_alt' => trans('Texte alternatif de l\'illustration'),
'picture_title' => trans('Titre de l\'illustration'),
'picture_thumb' => trans('vignette'),
'picture_thumb_alt' => trans('Texte alternatif de la vignette'),
'picture_thumb_title' => trans('Titre de la vignette'),
'status' => trans('publié ?'),
'publish_at' => trans('date de publication'),
'unpublish_at' => trans('date de dépublication'),
'nutritional' => trans('valeurs nutritionelles'),
'nutritional.energy' => trans('énergie'),
'nutritional.energy_kj' => trans('énergie kj'),
'nutritional.protein' => trans('protéines'),
'nutritional.carbohydrates' => trans('glucides'),
'nutritional.sugar' => trans('sucres'),
'nutritional.fat' => trans('matière grasse'),
'nutritional.saturated_fatty_acids' => trans('acides gras saturés'),
'nutritional.dietary_fiber' => trans('fibres alimentaires'),
'nutritional.salt' => trans('sel'),
];
}
}

View file

@ -0,0 +1,10 @@
<?php
namespace App\Http\Requests\Bo\Products;
/**
* Unicity for fields is handled by model events
*/
class UpdateProductRequest extends StoreProductRequest
{
}

View file

@ -15,6 +15,7 @@ class IncoNutritionalValue extends Model
* @var array
*/
protected $fillable = [
'weight',
'energy',
'energy_kj',
'protein',
@ -24,15 +25,6 @@ class IncoNutritionalValue extends Model
'saturated_fatty_acids',
'dietary_fiber',
'salt',
'energy_percent',
'energy_kj_percent',
'protein_percent',
'carbohydrates_percent',
'sugar_percent',
'fat_percent',
'saturated_fatty_acids_percent',
'dietary_fiber_percent',
'salt_percent'
];
// * METHODS
@ -46,24 +38,16 @@ class IncoNutritionalValue extends Model
public static function convertTranslateXmlInco(array $xmlInco): array
{
$cvt = [
'' => 'weight',
'val_energ_kcal_r' => 'energy',
'val_energ_kcal_p' => 'energy_percent',
'val_energ_kj_r' => 'energy_kj',
'pourcent_apport_ref_kjo_r' => 'energy_kj_percent',
'proteine_r' => 'protein',
'pourcent_apport_ref_pro_r' => 'protein_percent',
'sucre_r' => 'sugar',
'pourcent_apport_ref_sugar_r' => 'sugar_percent',
'mat_grass_r' => 'fat',
'pourcent_apport_ref_fat_r' => 'fat_percent',
'fib_alim_r' => 'dietary_fiber',
'pourcent_apport_ref_fibtg_r' => 'dietary_fiber_percent',
'glucides_r' => 'carbohydrates',
'glucides_p' => 'carbohydrates_percent',
'ags_r' => 'saturated_fatty_acids',
'pourcent_ags' => 'saturated_fatty_acids_percent',
'sel_r' => 'salt',
'pourcent_apport_ref_salteq_r' => 'salt_percent'
'sel_r' => 'salt'
];
$o = [];

View file

@ -39,6 +39,13 @@ class Product extends Model
'status',
'publish_at',
'unpublish_at',
'range_id',
'show_ingredients',
'show_preparation_advices',
'show_sales_pitch',
'show_allergen_list',
'show_nutrionnal_values',
'show_portions'
];
/**
@ -49,6 +56,11 @@ class Product extends Model
protected $casts = [
'labels' => 'array',
'status' => 'bool',
'show_ingredients' => 'bool',
'show_preparation_advices' => 'bool',
'show_sales_pitch' => 'bool',
'show_allergen_list' => 'bool',
'show_nutrionnal_values' => 'bool',
'publish_at' => 'datetime',
'unpublish_at' => 'datetime'
];
@ -137,7 +149,17 @@ class Product extends Model
*/
public function incoNutritionalValues()
{
return $this->hasOne(IncoNutritionalValues::class);
return $this->hasOne(IncoNutritionalValue::class);
}
/**
* BelongsTo range relation
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function range()
{
return $this->belongsTo(Range::class);
}
// * METHODS

View file

@ -4,6 +4,7 @@ namespace App\Providers;
use App\Lib\ToolboxHelper;
use App\Models\News;
use App\Models\Product;
use App\Models\Range;
use App\Models\Slider;
use Illuminate\Cache\RateLimiting\Limit;
@ -44,6 +45,7 @@ class RouteServiceProvider extends ServiceProvider
->group(base_path('routes/web-fo.php'));
});
Route::model('product', Product::class);
Route::model('range', Range::class);
Route::model('slider', Slider::class);
Route::model('news', News::class, function ($value) {

View file

@ -14,7 +14,7 @@ trait UpdateModelStatus
/**
* Change model published
*
* @param Model $model
* @param \Illuminate\Database\Eloquent\Model $model
* @return \Illuminate\Http\RedirectResponse
*/
public function changePublished(Model $model)

View file

@ -25,6 +25,8 @@ class IncoNutritionalValueFactory extends Factory
public function definition()
{
return [
"weight" => $this->faker->boolean(10) ?
null : $this->faker->numberBetween(231 - 150, 231 + 150),
"energy" => $this->faker->boolean(10) ?
null : $this->faker->numberBetween(231 - 150, 231 + 150),
"energy_kj" => $this->faker->boolean(10) ?
@ -42,25 +44,7 @@ class IncoNutritionalValueFactory extends Factory
"dietary_fiber" => $this->faker->boolean(10) ?
null : $this->faker->numberBetween(2.9 - 2, 2.9 + 6),
"salt" => $this->faker->boolean(10) ?
null : $this->faker->numberBetween(1.1 - 1, 1.1 + 1.1),
"energy_percent" => $this->faker->boolean(10) ?
null : $this->faker->numberBetween(231 - 150, 231 + 160),
"energy_kj_percent" => $this->faker->boolean(10) ?
null : $this->faker->numberBetween(12 - 6, 12 + 25),
"protein_percent" => $this->faker->boolean(10) ?
null : $this->faker->numberBetween(18 - 6, 18 + 25),
"carbohydrates_percent" => $this->faker->boolean(10) ?
null : $this->faker->numberBetween(20 - 6, 20 + 25),
"sugar_percent" => $this->faker->boolean(10) ?
null : $this->faker->numberBetween(2 - 1, 2 + 6),
"fat_percent" => $this->faker->boolean(10) ?
null : $this->faker->numberBetween(17 - 1, 17 + 6),
"saturated_fatty_acids_percent" => $this->faker->boolean(10) ?
null : $this->faker->numberBetween(28 - 1, 28 + 6),
"dietary_fiber_percent" => $this->faker->boolean(10) ?
null : $this->faker->numberBetween(28 - 1, 28 + 6),
"salt_percent" => $this->faker->boolean(10) ?
null : $this->faker->numberBetween(18 - 1, 18 + 6)
null : $this->faker->numberBetween(1.1 - 1, 1.1 + 1.1)
];
}
}

View file

@ -32,7 +32,7 @@ class ProductFactory extends Factory
'picture' => '/assets/images/products/product_fake_700_700.jpg',
'picture_title' => $this->faker->words(8, true),
'picture_alt' => $this->faker->words(8, true),
'picture_thumb' => '/assets/products/product_fake_thumb_300_300.jpg',
'picture_thumb' => '/assets/images/products/product_fake_thumb_300_300.jpg',
'picture_thumb_title' => $this->faker->words(8, true),
'picture_thumb_alt' => $this->faker->words(8, true),
'labels' => collect(ProductLabelEnum::toArray())->random(rand(0, 4))->all(),
@ -41,7 +41,13 @@ class ProductFactory extends Factory
'sales_pitch' => $this->faker->text(\mt_rand(15, 150)),
'allergen_list' => $this->faker->words(8, true),
'number_portion' => $this->faker->numberBetween(0, 8),
'status' => $this->faker->boolean(75)
'status' => $this->faker->boolean(75),
'show_ingredients' => $this->faker->boolean(75),
'show_preparation_advices' => $this->faker->boolean(75),
'show_sales_pitch' => $this->faker->boolean(75),
'show_allergen_list' => $this->faker->boolean(75),
'show_nutrionnal_values' => $this->faker->boolean(75),
'show_portions' => $this->faker->boolean(75),
];
}
}

View file

@ -1,5 +1,6 @@
<?php
use App\Models\Range;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
@ -38,10 +39,17 @@ class CreateProductsTable extends Migration
$table->text('ingredients')->comment('Ingredients');
$table->text('preparation_advices')->comment('Preparation advices');
$table->text('sales_pitch')->comment('Preparation advices');
$table->text('allergen_list')->comment('Preparation advices');
$table->text('sales_pitch')->comment('Arguments de vente');
$table->text('allergen_list')->comment('Liste allergene issues de inco');
$table->unsignedTinyInteger('number_portion')->comment('Portion number');
$table->boolean('show_ingredients')->comment('Afficher en front ou non.');
$table->boolean('show_preparation_advices')->comment('Afficher en front ou non.');
$table->boolean('show_sales_pitch')->comment('Afficher en front ou non.');
$table->boolean('show_allergen_list')->comment('Afficher en front ou non.');
$table->boolean('show_nutrionnal_values')->comment('Afficher en front ou non.');
$table->boolean('show_portions')->comment('Afficher en front ou non.');
$table->boolean('status')->comment('Le produit est visible ou non');
$table->timestamp('publish_at')->nullable()
@ -49,6 +57,8 @@ class CreateProductsTable extends Migration
$table->timestamp('unpublish_at')->nullable()
->comment('The date where the product status state should be set off.');
$table->foreignIdFor(Range::class)->constrained();
$table->timestamps();
});
}

View file

@ -17,6 +17,7 @@ class CreateIncoNutritionalValuesTable extends Migration
Schema::create('inco_nutritional_values', function (Blueprint $table) {
$table->id();
$table->double('weight')->nullable();
$table->double('energy')->nullable();
$table->double('energy_kj')->nullable();
$table->double('protein')->nullable();
@ -27,16 +28,6 @@ class CreateIncoNutritionalValuesTable extends Migration
$table->double('dietary_fiber')->nullable();
$table->double('salt')->nullable();
$table->float('energy_percent')->nullable();
$table->float('energy_kj_percent')->nullable();
$table->float('protein_percent')->nullable();
$table->float('carbohydrates_percent')->nullable();
$table->float('sugar_percent')->nullable();
$table->float('fat_percent')->nullable();
$table->float('saturated_fatty_acids_percent')->nullable();
$table->float('dietary_fiber_percent')->nullable();
$table->float('salt_percent')->nullable();
$table->foreignId('product_id');
$table->foreign('product_id')->references('id')->on('products')->onDelete('cascade');

View file

@ -4,6 +4,7 @@ namespace Database\Seeders;
use App\Models\IncoNutritionalValue;
use App\Models\Product;
use App\Models\Range;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
@ -20,9 +21,11 @@ class ProductSeeder extends Seeder
Product::all()->map(function (Product $product) {
$product->deleteOrFail();
});
Product::factory(150)->create()->each(function (Product $product) {
$ranges = Range::all();
Product::factory(150)->make()->each(function (Product $product) use ($ranges) {
\mt_srand();
if (\mt_rand(0, 2)) {
$product->range()->associate($ranges->random(1)->first());
$product->saveOrFail();
$product->fresh();
/** @var \App\Models\IncoNutritionalValue $incValue */

View file

@ -4,7 +4,6 @@ namespace Database\Seeders;
use App\Models\Range;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
class RangeSeeder extends Seeder
{
@ -15,7 +14,9 @@ class RangeSeeder extends Seeder
*/
public function run()
{
DB::table('ranges')->truncate();
Range::all()->map(function (Range $range) {
$range->deleteOrFail();
});
Range::factory(8)->create();
}
}

View file

@ -0,0 +1,165 @@
<template>
<div class="taggableDropdown">
<div v-if="errorMessages.length">
<span class="d-block text-danger" v-for="errorMessage of errorMessages">
{{ errorMessage }}
</span>
</div>
<div class="d-inline-block tagInput">
<VueTagsInputCls
v-model="tag"
:tags="tags"
:autocomplete-items="filteredItems"
:autocomplete-min-length="0"
@before-adding-tag="beforeTagSave"
@tags-changed="changedTags"
:max-tags="6"
:placeholder="__('Ajoutez ici vos étiquettes')"
/>
<div class="text-center py-2" v-if="waitingListObjects.length">
<button class="btn btn-sm btn-dark ms-2" v-on:click.prevent="cancelTags">
{{ __("Annuler") }}
</button>
</div>
</div>
<template v-for="(sTag, index) in tags" :key="index">
<input
:name="`${name}[${String(index)}][value]`"
type="hidden"
:value="sTag.value"
/>
<input
:name="`${name}[${String(index)}][label]`"
type="hidden"
:value="sTag.text"
/>
</template>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import type VueTagsInput from "@sipec/vue3-tags-input";
import { VueTagsInput as VueTagsInputCls } from "@sipec/vue3-tags-input";
import slugify from "slugify";
import route from "../../modules/route";
import toast from "../../modules/toast";
import trans from "../../modules/trans";
export default defineComponent({
mixins: [toast, route, trans],
components: {
VueTagsInputCls,
},
beforeMount() {
if (!this.$attrs.json) {
throw new Error("this component requires json");
}
},
mounted() {
const json = String(this.$attrs.json ?? "{}"),
data = JSON.parse(json);
this.name = data.name;
this.tags = this.parseTags(data.value);
this.autocompleteItems = this.parseTags(data.tags);
},
data(): {
name: string;
tag: string;
tags: Array<CustomTag>;
autocompleteItems: Array<CustomTag>;
waitingListObjects: Array<VueTagsInput.IAddArgs>;
errorMessages: Array<string>;
} {
return {
name: "",
tag: "",
tags: [],
autocompleteItems: [],
waitingListObjects: [],
errorMessages: [],
};
},
methods: {
parseTags(tags: Array<CustomTag>): Array<CustomTag> {
return tags.map((tag: CustomTag) => {
tag.text = String(tag.label);
return tag;
});
},
cancelTags(): void {
this.$nextTick(() => {
this.waitingListObjects = [];
this.tag = "";
this.showErrorMessages([]);
});
},
beforeTagSave(obj: VueTagsInput.IAddArgs): void {
this.waitingListObjects = [];
this.showErrorMessages([]);
// * Tag exists in selected tags
if (this.tagExists(this.tags, obj.tag)) {
this.showErrorMessages([this.__("Cette étiquette est déjà dans la liste.")]);
return;
}
// * Tag exists in local list
let customTag = this.tagExists(this.autocompleteItems, obj.tag);
if (customTag) {
obj.tag = customTag;
this.tags.push(customTag);
this.tag = "";
return;
}
// * Add tag in waiting list
this.waitingListObjects.push(obj);
},
changedTags(newTags: Array<CustomTag>): void {
this.tags = newTags as Array<CustomTag>;
},
tagExists(items: Array<CustomTag>, tagToCheck: VueTagsInput.ITag): CustomTag | null {
let customTag: CustomTag | null = null;
items.filter((tag: CustomTag) => {
if (
tag.value === (tagToCheck as CustomTag).value ||
tag.label === slugify(tagToCheck.text)
) {
customTag = tag;
}
});
return customTag;
},
showErrorMessages(messages: Array<string>): void {
this.errorMessages = messages;
},
},
computed: {
filteredItems() {
return this.autocompleteItems.filter((i) => {
return i.text.toLowerCase().indexOf(this.tag.toLowerCase()) !== -1;
});
},
},
});
</script>
<style lang="scss">
@import "../../../../node_modules/bootstrap/scss/bootstrap-utilities.scss";
.taggableDropdown {
.tagInput {
width: 100%;
@include media-breakpoint-up(lg) {
width: 75%;
}
.vue-tags-input {
max-width: inherit !important;
}
.ti-autocomplete {
overflow-y: scroll !important;
max-height: 10rem !important;
}
}
}
</style>

View file

@ -53,7 +53,7 @@ window.addEventListener("DOMContentLoaded", () => {
const el = e.target;
if (!el || !(el instanceof HTMLFormElement)) {
throw new Error(
"confirmDoneJS can only be executed on an exising form"
"confirmSubmitJS can only be executed on an exising form"
);
}
confirm.methods.confirm(
@ -70,24 +70,42 @@ window.addEventListener("DOMContentLoaded", () => {
}
elements = document.getElementsByClassName("confirmJS");
for (let element of elements) {
element.addEventListener("submit", function (e) {
e.preventDefault();
const el = e.target;
if (!el || !(el instanceof HTMLFormElement)) {
throw new Error(
"confirmDoneJS can only be executed on an exising form"
);
}
confirm.methods.confirm(
"Confirmez",
"Êtes vous sure ?",
el,
function (response: SweetAlertResult<any>) {
(el as HTMLFormElement).submit();
},
{ icon: "warning" }
);
return false;
});
element.addEventListener(
"click",
function (e) {
if (!element.getAttribute("confirmJS")) {
e.preventDefault();
const el = e.target;
if (!el || !(el instanceof HTMLElement)) {
throw new Error(
"confirmJS can only be executed on a html element"
);
}
(async () => {
const promise = new Promise(
(resolve: (value: boolean) => void, reject) => {
console.log(el);
confirm.methods.confirm(
"Confirmez",
"Êtes vous sure ?",
el,
function (response: SweetAlertResult<any>) {
resolve(response.isConfirmed);
},
{ icon: "warning" }
);
return false;
}
);
if (await promise) {
element.setAttribute("confirmJS", "true");
el.click();
element.removeAttribute("confirmJS");
}
})();
}
},
false
);
}
});

View file

@ -9,7 +9,7 @@ export default {
confirm(
title: string,
text: string = "",
self: HTMLElement,
self: Object,
after: (response: SweetAlertResult<any>) => void = (
response: SweetAlertResult<any>
) => {},

View file

@ -0,0 +1,30 @@
@extends('layouts.back')
@section('title', sprintf('%s - %s', __('models.classes.products'), Str::ucfirst(__('création'))))
@section('breadcrumb', request()->route()->getName())
@section('content')
<div class="card">
<div class="card-header">
<div class="row">
<div class="col">
@include('back.breadcrumb.breadcrumb-body')
</div>
</div>
<div class="row">
<h1 class="col h4">
<a href="{{ route('bo.products.index') }}" class="text-decoration-none btn btn-sm btn-primary" role="button" aria-roledescription="{{ _('Revenir en arrière') }}"><i class="fa-solid fa-arrow-left-long"></i></a>
<span>&nbsp;{{ __('models.classes.products') }}&nbsp;{{ __('création') }}</span>
</h1>
</div>
</div>
<div class="card-body">
<form action="{{ route('bo.products.store') }}" method="POST" enctype="multipart/form-data">
@csrf
@include('back.products.form')
<button type="submit" class="btn btn-primary">{{ __('Enregister') }}</button>
</form>
</div>
</div>
@endsection

View file

@ -0,0 +1,54 @@
@extends('layouts.back')
@section('title', sprintf('%s - %s %s', __('models.classes.products'), Str::ucfirst(__('édition')), $productModel->title))
@section('breadcrumb', request()->route()->getName())
@section('content')
<div class="card">
<div class="card-header">
<div class="row">
<div class="col-12">
@include('back.breadcrumb.breadcrumb-body', [
'brParam' => $productModel
])
</div>
</div>
<div class="row">
<div class="col">
<h1 class="col h4">
<a href="{{ route('bo.products.index') }}" class="text-decoration-none btn btn-sm btn-primary" role="button" aria-roledescription="{{ _('Revenir en arrière') }}"><i class="fa-solid fa-arrow-left-long"></i></a>
<span>&nbsp;{{ __('models.classes.products') }}&nbsp;{{ __('édition') }}</span>
</h1>
</div>
<div class="col text-end">
<form action="{{ route('bo.products.destroy', $productModel->id) }}" method="POST" class="d-inline confirmDeleteJS">
@method('DELETE')
@csrf
<div class="btn-group" role="group">
{{-- Public View Page --}}
@if($productModel->status)
<a class="btn btn-warning" href="" target="_blank" title="{{ __('Visualiser :model', [
'model' => Str::singular(__('models.classes.products'))
]) }}" data-bs-toggle="tooltip"><i class="fa-solid fa-eye"></i></a>
@endif
<button class="btn btn-danger" title="{{ __('Supprimer :model', [
'model' => Str::singular(__('models.classes.products'))
]) }}" data-bs-toggle="tooltip"><i class="fa-solid fa-trash"></i></button>
</div>
</form>
</div>
</div>
</div>
<div class="card-body">
<form action="{{ route('bo.products.update', $productModel->id) }}" method="POST" enctype="multipart/form-data">
@csrf
@method('PUT')
@include('back.products.form', compact('productModel'))
<button type="submit" class="btn btn-primary" title="{{ __('Enregister :model en base', [
'model' => Str::singular(__('models.classes.products'))
]) }}" data-bs-toggle="tooltip">{{ __('Enregister') }}</button>
</form>
</div>
</div>
@endsection

View file

@ -0,0 +1,415 @@
<div class="row">
<div class="col">
<fieldset class="border p-3 mb-3">
<legend>{{ __('Informations générales') }}</legend>
<div class="row mb-3">
<div class="col-12 col-md-6 form-group">
<label for="productModelTitle" class="col-form-label"><b>{{ __('Titre') }}</b></label>
<div class="word-counter" data-json='@json(['id' => 'productModelTitle'])'></div>
<input name="title" type="text" value="{{ old('title', $productModel->title ?? '') }}" class="form-control"
id="productModelTitle" aria-describedby="productModelTitleHelp" placeholder="{{ __('Un titre beau et explicite') }}" required>
@include('back.modules.input-error', [
'inputName' => 'title', 'helperId' => 'productModelTitleHelp' , 'helper' => __('Doit décrire précisément le produit')
])
@include('back.modules.input-error', [
'inputName' => 'slug'
])
</div>
<div class="col-12 col-md-6 form-group">
<label for="productModelType" class="col-form-label"><b>{{ __('Gamme') }}</b></label>
<select id="productModelType" class="form-select" name="range_id" aria-describedby="productModelTypeHelp" required>
<option value="" @if(is_null(old('range_id', $productModel->range_id ?? null))) selected @endif disabled>{{ __('Séléctionnez une gamme') }}</option>
@foreach($ranges as $range)
<option value="{{ $range->id }}" @if(old('range_id', optional($productModel ?? null)->range_id) == $range->id) selected @endif>{{ $range->title }}</option>
@endforeach
</select>
@include('back.modules.input-error', [
'inputName' => 'range_id', 'helperId' => 'productModelTypeHelp' , 'helper' => __('La gamme du produit')
])
</div>
</div>
</fieldset>
</div>
</div>
<div class="row">
<div class="col">
<fieldset class="border p-3 mb-3">
<legend>{{ __('Seo') }}</legend>
<div class="row mb-3">
<div class="col-12 col-md-6 form-group">
<label for="productModelSeoTitle" class="col-form-label"><b>{{ __('Titre seo') }}</b></label>
<div class="word-counter" data-json='@json(['id' => 'productModelSeoTitle'])'></div>
<input name="seo_title" type="text" value="{{ old('seo_title', $productModel->seo_title ?? '') }}" class="form-control"
id="productModelSeoTitle" aria-describedby="productModelSeoTitleHelp" placeholder="{{ __('Un titre en 70 caractères maximum') }}" required>
@include('back.modules.input-error', [
'inputName' => 'seo_title', 'helperId' => 'productModelSeoTitleHelp' , 'helper' => __('Doit décrire précisément le produit, recommandation pour un positionnement optimal dans les moteurs de recherche : entre 53 et 60 caractères, balise <title> ')
])
</div>
<div class="col-12 col-md-6 form-group">
<label for="productModelSeoDescription" class="col-form-label"><b>{{ __('Description seo') }}</b></label>
@php
$data = [
'id' => 'productModelSeoDescription',
'class' => 'form-control',
'name' => 'seo_description',
'placeholder' => __('Une description du produit pour les moteurs de recherche'),
'ariaDescribedby' => 'productModelSeoDescriptionHelp',
'required' => true,
'value' => old('seo_description', $productModel->seo_description ?? '')
];
@endphp
<div id="input-text-auto" data-json='@json($data)'></div>
@include('back.modules.input-error', [
'inputName' => 'seo_description', 'helperId' => 'productModelSeoDescriptionHelp' , 'helper' => __(' Doit décrire précisément le produit, requiert entre 50 et 160 caractères, balise <meta> description')
])
</div>
</div>
</fieldset>
</div>
</div>
<div class="row">
<div class="col">
<fieldset class="border p-3 mb-3">
<legend>{{ __('Labels') }}</legend>
<div class="row mb-3">
<div class="col-12 form-group">
@php
$data = [
'name' => 'labels',
'value' => old('labels', $productModel->labels ?? []),
'tags' => App\Enums\Products\ProductLabelEnum::toArray(),
];
@endphp
<div id="enum-tag-dropdown" data-json='@json($data)'></div>
@include('back.modules.input-error', [
'inputName' => 'labels', 'helperId' => 'newsModelTagsHelp' , 'helper' => __('Les labels associés au produit')
])
</div>
</div>
</fieldset>
</div>
</div>
<div class="row">
<div class="col">
<fieldset class="border p-3 mb-3">
<legend>{{ __('Publication') }}</legend>
<div class="row mb-3">
<div class="col">
<div class="form-group">
<label for="productPublishedAt"><b>{{ __('Date de publication (Europe/Paris)') }}</b></label>
<input id="productPublishedAt" name="publish_at" class="form-control publishTime" type="datetime-local" data-form-type="date" aria-describedby="productPublishAtHelp" value="{{ old('publish_at', ((isset($productModel) and $productModel->publish_at) ? $productModel->publish_at->format('Y-m-d\TH:i') : '')) }}">
@include('back.modules.input-error', [
'inputName' => 'publish_at', 'helperId' => 'productPublishAtHelp' , 'helper' => (isset($productModel) and $productModel->publish_at) ? __('Laisser vide pour un affichage immédiat du produit.') : __('Vider pour un affichage immédiat du produit')
])
</div>
</div>
<div class="col">
<div class="form-group">
<label for="productUnPublishedAt"><b>{{ __('Date de fin de publication (Europe/Paris)') }}</b></label>
<input id="productUnPublishedAt" name="unpublish_at" class="form-control publishTime" type="datetime-local" data-form-type="date" aria-describedby="productUnPublishAtHelp" value="{{ old('unpublish_at', ((isset($productModel) and $productModel->unpublish_at) ? $productModel->unpublish_at->format('Y-m-d\TH:i') : '')) }}">
@include('back.modules.input-error', [
'inputName' => 'unpublish_at', 'helperId' => 'productUnPublishAtHelp' , 'helper' => (isset($productModel) and $productModel->unpublish_at) ? __('Laisser vide pour un affichage permanent du produit.') : __('Vider pour un affichage permanent du produit')
])
</div>
</div>
</div>
</fieldset>
</div>
</div>
<div class="row">
<div class="col">
<fieldset class="border p-3 mb-3">
<legend>{{ __('Illustration') }}</legend>
<div class="row mb-3">
<div class="col-12 col-md-6 form-group">
<label for="productPictureAlt" class="col-form-label"><b>{{ __('Texte alternatif de l\'image') }}</b></label>
<div class="word-counter" data-json='@json(['id' => 'productPictureAlt'])'></div>
<input name="picture_alt" type="text" value="{{ old('picture_alt', $productModel->picture_alt ?? '') }}" class="form-control"
id="productPictureAlt" aria-describedby="productPictureAltHelp" placeholder="{{ __('Une image de petits chatons') }}" required>
@include('back.modules.input-error', [
'inputName' => 'picture_alt', 'helperId' => 'productPictureAltHelp' , 'helper' => __('Doit décrire votre image pour les moteurs de recherches et les non voyants (5 à 6 mots)')
])
</div>
<div class="col-12 col-md-6 form-group">
<label for="productPictureTitle" class="col-form-label"><b>{{ __('Titre de l\'image') }}</b></label>
<div class="word-counter" data-json='@json(['id' => 'productPictureTitle'])'></div>
<input name="picture_title" type="text" value="{{ old('picture_title', $productModel->picture_title ?? '') }}" class="form-control"
id="productPictureTitle" aria-describedby="productPictureTitleHelp" placeholder="{{ __('Une description du produit pour les moteurs de recherche') }}" required>
@include('back.modules.input-error', [
'inputName' => 'picture_title', 'helperId' => 'productPictureTitleHelp' , 'helper' => __('Utile pour les utilisateurs, s\'affiche en passant la souris sur l\'image.')
])
</div>
</div>
<div class="row">
<div class="col-12">
@php
$data = [
'id' => 'productModelPicture',
'name' => 'picture',
'helper' => __('L\'image qui va servir de vignette à le produit, doit être de 700px (hauteur) par 700px (largeur)'),
'width' => 700,
'height' => 700,
'value' => $productModel->picture ?? '',
];
@endphp
@include('back.modules.input-error', [ 'inputName' => 'picture'])
<div class="image-input" data-json='@json($data)'></div>
</div>
</div>
</fieldset>
</div>
</div>
<div class="row">
<div class="col">
<fieldset class="border p-3 mb-3">
<legend>{{ __('Vignette') }}</legend>
<div class="row mb-3">
<div class="col-12 col-md-6 form-group">
<label for="productPictureThumbAlt" class="col-form-label"><b>{{ __('Texte alternatif de l\'image') }}</b></label>
<div class="word-counter" data-json='@json(['id' => 'productPictureThumbAlt'])'></div>
<input name="picture_thumb_alt" type="text" value="{{ old('picture_thumb_alt', $productModel->picture_thumb_alt ?? '') }}" class="form-control"
id="productPictureThumbAlt" aria-describedby="productPictureThumbAltHelp" placeholder="{{ __('Une image de petits chatons') }}" required>
@include('back.modules.input-error', [
'inputName' => 'picture_thumb_alt', 'helperId' => 'productPictureThumbAltHelp' , 'helper' => __('Doit décrire votre image pour les moteurs de recherches et les non voyants (5 à 6 mots)')
])
</div>
<div class="col-12 col-md-6 form-group">
<label for="productPictureThumbTitle" class="col-form-label"><b>{{ __('Titre de l\'image') }}</b></label>
<div class="word-counter" data-json='@json(['id' => 'productPictureThumbTitle'])'></div>
<input name="picture_thumb_title" type="text" value="{{ old('picture_thumb_title', $productModel->picture_thumb_title ?? '') }}" class="form-control"
id="productPictureThumbTitle" aria-describedby="productPictureThumbTitleHelp" placeholder="{{ __('Une description du produit pour les moteurs de recherche') }}" required>
@include('back.modules.input-error', [
'inputName' => 'picture_thumb_title', 'helperId' => 'productPictureThumbTitleHelp' , 'helper' => __('Utile pour les utilisateurs, s\'affiche en passant la souris sur l\'image.')
])
</div>
</div>
<div class="row">
<div class="col-12">
@php
$data = [
'id' => 'productModelThumbPicture',
'name' => 'picture_thumb',
'helper' => __('L\'image qui va servir de vignette à le produit, doit être de 300px (hauteur) par 300px (largeur)'),
'width' => 300,
'height' => 300,
'value' => $productModel->picture_thumb ?? '',
];
@endphp
@include('back.modules.input-error', [ 'inputName' => 'picture_thumb'])
<div class="image-input" data-json='@json($data)'></div>
</div>
</div>
</fieldset>
</div>
</div>
<div class="row">
<div class="col">
<fieldset class="border p-3 mb-3">
<legend>{{ __('Description du produit') }}</legend>
<div class="row mb-3">
<div class="col-12 form-group">
@include('back.modules.input-error', [
'inputName' => 'description', 'helperId' => 'productModelDescriptionHelp' , 'helper' => __('Relisez bien votre description avant de la publier.')
])
@php
$data = [
'name' => 'description',
'text' => old('description', $productModel->description ?? ''),
];
@endphp
<div class="ck-editor-bo" data-json='@json($data)'></div>
</div>
</div>
</fieldset>
</div>
</div>
<div class="row">
<div class="col">
<fieldset class="border p-3 mb-3">
<legend>{{ __('Informations Inco') }}</legend>
@php $attributes = [
'ingredients' => __('Ingrédients'),
'preparation_advices' => __('Conseils de préparation'),
'sales_pitch' => __('Arguments de vente'),
'allergen_list' => __('Liste d\'allergènes')
]; @endphp
@foreach($attributes as $attribute => $helperText)
<div class="row mb-3">
<div class="col-12 form-group">
<span class="h6 fw-bold d-inline-block mb-2">{{ $helperText }}</span>
<div class="form-check form-switch d-inline-block ms-5">
<input class="form-check-input" name="{{ "show_{$attribute}" }}" type="checkbox" role="switch" id="{{ "{$attribute}Switch" }}" value="1" @if(old("show_{$attribute}", $productModel->{"show_{$attribute}"} ?? null)) checked @endif>
<label class="form-check-label" for="{{ "{$attribute}Switch" }}"><span class="fw-bold fst-italic">{{ __('Afficher ? ') }}</span></label>
</div>
@include('back.modules.input-error', [
'inputName' => $attribute
])
@php
$data = [
'name' => $attribute,
'text' => old($attribute, $productModel->{$attribute} ?? ''),
];
@endphp
<div class="ck-editor-bo" data-json='@json($data)'></div>
</div>
</div>
@endforeach
<div class="row mb-3">
<div class="col-12 col-md-6 form-group">
<label for="productModelPortions" class="col-form-label d-inline-block"><b>{{ __('Nombre de portions') }}</b></label>
<div class="form-check form-switch d-inline-block ms-5">
<input class="form-check-input" name="show_portions" type="checkbox" role="switch" id="portionsSwitch" value="1" @if(old('show_portions', $productModel->show_portions ?? null)) checked @endif>
<label class="form-check-label" for="portionsSwitch"><span class="fw-bold fst-italic">{{ __('Afficher ? ') }}</span></label>
</div>
<input name="number_portion" type="number" value="{{ old('number_portion', $productModel->number_portion ?? 0) }}" class="form-control"
id="productModelPortions" aria-describedby="productModelPortionsHelp" min="0" step="1" required>
@include('back.modules.input-error', [
'inputName' => 'number_portion', 'helperId' => 'productModelPortionsHelp' , 'helper' => __('Nombre de portions du produit')
])
</div>
</div>
<div class="form-group">
<div class="row">
<label class="h6 fw-bold d-inline-block">{{ __('Valeurs nutritionelles') }}</label>
<div class="form-check form-switch d-inline-block ms-5">
<input class="form-check-input" name="show_nutrionnal_values" type="checkbox" role="switch" id="nutrionnaSwitch" value="1" @if(old('show_nutrionnal_values', $productModel->show_nutrionnal_values ?? null)) checked @endif>
<label class="form-check-label" for="nutrionnaSwitch"><span class="fw-bold fst-italic">{{ __('Afficher ? ') }}</span></label>
</div>
@php
$incoNut = (isset($productModel) and $productModel->xmlInco) ? json_decode($productModel->xmlInco->nutritional_values) : null;
@endphp
<div class="table-responsive">
<table class="table table-light text-center table-sm table-bordered table-striped">
<thead class="table-group-divider">
<tr>
<th scope="col">Nutriment</th>
<th scope="col">Valeur</th>
@if(isset($incoNut))
<th scope="col">Val. Source Inco</th>
@endif
</tr>
</thead>
<tbody class="table-group-divider">
@php $incoValues = [
'Poids de référence (en g)' => ['product' => 'weight', 'inco' => ''],
'Énergie (en kCal)' => ['product' => 'energy', 'inco' => 'val_energ_kcal_r'],
'Énergie (en kJ)' => ['product' => 'energy_kj', 'inco' => 'val_energ_kj_r'],
'Protéines (en g)' => ['product' => 'protein', 'inco' => 'proteine_r'],
'Glucides (en g)' => ['product' => 'carbohydrates', 'inco' => 'glucides_r'],
'Sucres (en g)' => ['product' => 'sugar', 'inco' => 'sucre_r'],
'Matière grasse (en g)' => ['product' => 'fat', 'inco' => 'mat_grass_r'],
'Acides Gras Saturés (en g)' => ['product' => 'saturated_fatty_acids', 'inco' => 'ags_r'],
'Fibres alimentaires (en g)' => ['product' => 'dietary_fiber', 'inco' => 'fib_alim_r'],
'Sel (en g)' => ['product' => 'salt', 'inco' => 'sel_r']
]; @endphp
@foreach($incoValues as $title => $incoValue)
<tr>
<td class="w-50">{{ $title }}</td>
<td class="w-25"><input class="form-control form-control-sm w-100 h-100" type="number" step="1" name="{{ $incoValue['product'] }}" value="{{ old($incoValue['product'], isset($productModel->incoNutritionalValues->{$incoValue['product']}) ? str_replace(',', '.', (string)$productModel->incoNutritionalValues->{$incoValue['product']}) : 0) }}"></td>
@if(isset($incoNut))
<td class="w-25"><span class="text-danger">{{ isset($incoNut->{$incoValue['inco']}) ? str_replace(',', '.', $incoNut->{$incoValue['inco']}) : 0 }}</span></td>
@endif
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
@if(isset($productModel) and isset($productModel->xmlInco))
<div class="row justify-content-center">
<div class="col-12 col-md-5 text-center">
<a href="#" class="btn btn-danger confirmJS">{{ __('Copier les Valeurs nutritionelles') }}</a>
</div>
</div>
@endif
</div>
</fieldset>
</div>
</div>
<div class="row mb-3">
<div class="col-12 col-md-6">
<div class="form-check">
<input name="status" class="form-check-input mt-2" type="checkbox" id="productStatus" aria-describedby="productStatusHelp" value="1" @if(old('status', $productModel->status ?? ''))checked @endif>
<label class="form-check-label user-select-none pt-1" for="productStatus"><b>{{ __('Publié') }}</b></label><br>
@include('back.modules.input-error', [
'inputName' => 'status', 'helperId' => 'productStatusHelp' , 'helper' => __('Le produit est visible ou non')
])
</div>
</div>
</div>
@push('scripts')
<script nonce="{{ $nonce }}">
document.addEventListener("DOMContentLoaded", () => {
// Publish times
let publishTimes = document.getElementsByClassName('publishTime');
for (let input of publishTimes) {
let parent = input.parentNode,
dates = {
start: 0,
end: 0
};
input.originalValue = (new Date(input.value)).valueOf();
if(input.originalValue === NaN) {
input.originalValue = 0
}
if(input.id === 'productPublishedAt') {
dates.start = input.originalValue;
} else {
dates.end = input.originalValue;
}
let getEuropeParisTime = () => {
return new Date((new Date()).getTime() + (60000*((new Date()).getTimezoneOffset()+60)));
};
let setDateHelpers = (input, parent) => {
let small = parent.children.productPublishAtHelp ? parent.children.productPublishAtHelp : parent.children.productUnPublishAtHelp;
let isPlanned = false;
if(input.id === 'productPublishedAt') {
isPlanned = dates.start > dates.end && dates.end != 0;
} else {
isPlanned = dates.start < dates.end && dates.start != 0;
}
if (isPlanned || getEuropeParisTime().valueOf() < (new Date(input.value)).valueOf()) {
let date = new Date(input.value);
// small.textContent = "{{ __('Planifié pour le ') }}" + date.toLocaleString();
small.textContent = input.id === 'productPublishedAt' ?
"{{ __('Vider pour un affichage immédiat du produit.') }}" :
"{{ __('Vider pour un affichage permanent du produit.') }}";
} else {
// small.textContent = input.id === 'productPublishedAt' ?
// "{{ __('Uniquement pour planifier la publication') }}" :
// "{{ __('Uniquement pour planifier la fin de la publication') }}";
small.textContent = input.id === 'productPublishedAt' ?
"{{ __('Laisser vide pour un affichage immédiat du produit.') }}" :
"{{ __('Laisser vide pour un affichage permanent du produit.') }}";
}
};
setDateHelpers(input, parent);
input.addEventListener('change', (event) => {
let parent = event.target.parentNode,
input = event.target, value;
value = (new Date(input.value)).valueOf();
if(input.originalValue === NaN) {
value = 0
}
if(input.id === 'productPublishedAt') {
dates.start = value;
} else {
dates.end = value;
}
setDateHelpers(input, parent);
});
}
});
</script>
@endpush

View file

@ -0,0 +1,103 @@
@extends('layouts.back')
@section('title', sprintf('%s - %s', __('models.classes.products'), Str::ucfirst(__('liste'))))
@section('breadcrumb', request()->route()->getName())
@section('content')
<div class="card">
<div class="card-header">
<div class="row">
<div class="col-12">
@include('back.breadcrumb.breadcrumb-body')
</div>
</div>
<div class="row">
<div class="col">
<h1 class="h2">{{ __('models.classes.products') }}&nbsp;{{ Str::ucfirst(__('liste')) }}</h1>
</div>
<div class="col-md-3">
<div class="input-group justify-content-end">
<a href="{{ route('bo.products.create') }}" class="btn btn-primary" title="{{ __('Ajouter un :model', [
'model' => Str::singular(__('models.classes.products'))
]) }}" data-bs-toggle="tooltip"><i class="fa-solid fa-plus"></i></a>
</div>
</div>
</div>
</div>
<div class="card-body">
@include('back.modules.searchBar')
<div class="row justify-content-center">
<div class="col-12 col-md-6 p-0 p-md-3">
{{ $products->withQueryString()->onEachSide(1)->links() }}
</div>
</div>
<div class="row">
<div class="col">
<div class="table-responsive">
<table class="table fixAction">
<thead>
@php
$cols = ['title' => __('Titre'), 'description' => __('Description'), 'status' => __('Publication'), 'updated_at' => __('Mis à jour le')];
@endphp
@include('back.modules.tableColSorter', [ 'cols' => $cols, 'mobileHide' => ['status']])
</thead>
<tbody>
@if($products->count())
@foreach ($products as $productModel)
<tr>
<td>
<span>{{ $productModel->title }}</span>
</td>
<td>
<span>{{ Str::limit(strip_tags($productModel->description)) }}</span>
</td>
<td class="d-none d-md-table-cell">
<form action="{{ route('bo.products.change-published', $productModel->id) }}" method="POST">
@csrf
<button type="submit" class="btn btn-sm" title="{{ __(':action :model', [
'action' => $productModel->status ? __('Déplublier') : __('Publier'),
'model' => Str::singular(__('models.classes.products'))
]) }}" data-bs-toggle="tooltip">
<i class="fas @if($productModel->status)fa-check check-green @else fa-times cross-red @endif"></i>
</button>
</form>
</td>
<td>
<span>{{ $productModel->updated_at->formatLocalized('%d/%m/%Y à %I:%M') }}</span>
</td>
<td>
<div class="btn-group">
{{-- Public View Page --}}
@if($productModel->status)
<a class="btn btn-sm btn-warning" href="" target="_blank" title="{{ __('Visualiser :model', [
'model' => Str::singular(__('models.classes.products'))
]) }}" data-bs-toggle="tooltip"><i class="fa-solid fa-eye"></i></a>
@endif
<a class="btn btn-sm btn-primary" href="{{ route('bo.products.edit', $productModel->id) }}" title="{{ __('Modifier :model', [
'model' => Str::singular(__('models.classes.products'))
]) }}" data-bs-toggle="tooltip"><i class="fa-solid fa-pencil"></i></a>
</div>
</td>
</tr>
@endforeach
@else
<tr>
<td colspan="5" class="text-center"><span class="h3">{{
__('Aucun résultat pour les :models', ['models' => __('models.classes.products')])
}}</span></td>
</tr>
@endif
</tbody>
</table>
</div>
</div>
</div>
<div class="row justify-content-center">
<div class="col-12 col-md-6 p-0 p-md-3">
{{ $products->withQueryString()->onEachSide(1)->links() }}
</div>
</div>
</div>
</div>
@endsection

View file

@ -40,7 +40,12 @@ Route::prefix('bo')->name('bo.')->group(function () {
Route::get('/ranges/model-title', [RangeController::class, 'modelTitle'])
->name('ranges.model-title');
Route::resource('sliders', SliderController::class)->except(['show']);
// * PRODUCTS
Route::resource('products', ProductController::class)->except(['show'])
->withoutMiddleware('htmlsanitize')
->middleware('htmlsanitize:description,ingredients,preparation_advices,sales_pitch,allergen_list');
Route::post('/products/{product}/change-published', [ProductController::class, 'changePublished'])
->name('products.change-published');
// * TAGS
Route::resource('tags', TagController::class)->except(['show']);