Brands Ranges and Products CRUDS full ok

This commit is contained in:
Jérémy Munsch 2024-01-05 14:54:08 +01:00
parent 9bf55c411d
commit d657296693
No known key found for this signature in database
GPG key ID: 18361AF3BFEC0FEC
56 changed files with 2942 additions and 59 deletions

55
app/Casts/HtmlColor.php Normal file
View file

@ -0,0 +1,55 @@
<?php
namespace App\Casts;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;
class HtmlColor implements CastsAttributes
{
/**
* Cast the given value.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param string $key
* @param mixed $value
* @param array<string,mixed> $attributes
* @return mixed
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInImplementedInterfaceBeforeLastUsed
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInImplementedInterfaceAfterLastUsed
*/
public function get(Model $model, string $key, mixed $value, array $attributes): mixed
{
// phpcs:enable
return '#' . $this->zeropad(dechex($value), 6);
}
/**
* Prepare the given value for storage.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param string $key
* @param mixed|null $value
* @param array<string,mixed> $attributes
* @return mixed
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInImplementedInterfaceBeforeLastUsed
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInImplementedInterfaceAfterLastUsed
*/
public function set(Model $model, string $key, mixed $value, array $attributes): mixed
{
// phpcs:enable
return \intval(hexdec(ltrim($value, '#')));
}
/**
* Pad with zeros a numeric string
*
* @param string $num
* @param integer $lim
* @return string
*/
private function zeropad(string $num, int $lim): string
{
return (strlen($num) >= $lim) ? $num : $this->zeropad("0{$num}", $lim);
}
}

View file

@ -0,0 +1,149 @@
<?php
namespace App\Http\Controllers\Bo;
use App\Http\Controllers\Controller;
use App\Http\Requests\Bo\Brands\StoreBrandRequest;
use App\Http\Requests\Bo\Brands\UpdateBrandRequest;
use App\Models\Brand;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class BrandController extends Controller
{
/**
* Create the controller instance.
*/
public function __construct()
{
$this->authorizeResource(Brand::class);
}
/**
* Display a listing of the resource.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Contracts\View\View
*/
public function index(Request $request): View
{
/** @var \Illuminate\Database\Eloquent\Builder $query */
$query = Brand::query();
/** @var string $search Search field */
$search = $request->input('search');
if ($search) {
$this->searchQuery(
$query,
$search,
null,
'name',
);
}
$searchFields = \implode(', ', [
trans('validation.attributes.name'),
]);
/** Sort columns with a query */
$this->sortQuery($query);
/** Custom pagination */
$brandModelList = $this->paginate($query);
return view('bo.pages.brands.index', compact('brandModelList', 'search', 'searchFields'));
}
/**
* Show the form for creating a new resource.
*
* @return \Illuminate\Contracts\View\View
*/
public function create(): \Illuminate\Contracts\View\View
{
return view('bo.pages.brands.create');
}
/**
* Store a newly created resource in storage.
*
* @param \App\Http\Requests\Bo\Brands\StoreBrandRequest $request
* @return \Illuminate\Http\RedirectResponse
*/
public function store(StoreBrandRequest $request): RedirectResponse
{
return DB::transaction(function () use ($request) {
$brand = new Brand();
$brand->fill($request->validated());
if ($brand->saveOrFail()) {
return redirect()->route('bo.brand.edit', $brand)
->with('success', __('crud.messages.has_been_created', [
'model' => Str::ucfirst(\trans_choice('models.classes.brand', 1))
]));
}
return redirect()->back()
->with('error', __('crud.messages.cannot_be_created', [
'model' => Str::ucfirst(\trans_choice('models.classes.brand', 1))
]));
});
}
/**
* Show the form for editing the specified resource.
*
* @param \App\Models\Brand $brand
* @return \Illuminate\Contracts\View\View
*/
public function edit(Brand $brand): View
{
return view('bo.pages.brands.edit', ['brandModel' => $brand]);
}
/**
* Update the specified resource in storage.
*
* @param \App\Http\Requests\Bo\Brands\UpdateBrandRequest $request
* @param \App\Models\Brand $brand
* @return \Illuminate\Http\RedirectResponse
*/
public function update(UpdateBrandRequest $request, Brand $brand): RedirectResponse
{
return DB::transaction(function () use ($request, $brand) {
$brand->fill($request->validated());
if ($brand->saveOrFail()) {
return redirect()->route('bo.brands.edit', $brand)
->with('success', __('crud.messages.has_been_updated', [
'model' => Str::ucfirst(\trans_choice('models.classes.brand', 1))
]));
}
return redirect()->route('bo.brands.edit', $brand)
->with('error', __('crud.messages.cannot_be_updated', [
'model' => Str::ucfirst(\trans_choice('models.classes.brand', 1))
]));
});
}
/**
* Remove the specified resource from storage.
*
* @param \App\Models\Brand $brand
* @return \Illuminate\Http\RedirectResponse
*/
public function destroy(Brand $brand): RedirectResponse
{
if ($brand->deleteOrFail()) {
return redirect()->route('bo.brands.index')
->with('success', __('crud.messages.has_been_deleted', [
'model' => Str::ucfirst(\trans_choice('models.classes.brand', 1))
]));
}
return redirect()->back()
->with('error', __('crud.messages.cannot_be_deleted', [
'model' => Str::ucfirst(\trans_choice('models.classes.brand', 1))
]));
}
}

View file

@ -0,0 +1,155 @@
<?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 Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class ProductController extends Controller
{
/**
* Create the controller instance.
*/
public function __construct()
{
$this->authorizeResource(Product::class);
}
/**
* Display a listing of the resource.
*
* @param \Illuminate\Http\Client\Request $request
* @return \Illuminate\Contracts\View\View
*/
public function index(Request $request): View
{
/** @var \Illuminate\Database\Eloquent\Builder $query */
$query = Product::query()->with('range');
/** @var string $search Search field */
$search = $request->input('search');
if ($search) {
$this->searchQuery(
$query,
$search,
null,
'name',
);
}
$searchFields = \implode(', ', [
trans('validation.attributes.name'),
]);
/** Sort columns with a query */
$this->sortQuery($query);
/** Custom pagination */
$productModelList = $this->paginate($query);
return view('bo.pages.products.index', compact('productModelList', 'search', 'searchFields'));
}
/**
* Show the form for creating a new resource.
*
* @return \Illuminate\Contracts\View\View
*/
public function create(): \Illuminate\Contracts\View\View
{
$rangeModelList = Range::all();
return view('bo.pages.products.create', ['rangeModelList' => $rangeModelList]);
}
/**
* Store a newly created resource in storage.
*
* @param \App\Http\Requests\Bo\Products\StoreProductRequest $request
* @return \Illuminate\Http\RedirectResponse
*/
public function store(StoreProductRequest $request): RedirectResponse
{
return DB::transaction(function () use ($request) {
$product = new Product();
$product->fill($request->validated());
if ($product->saveOrFail()) {
return redirect()->route('bo.product.edit', $product)
->with('success', __('crud.messages.has_been_created', [
'model' => Str::ucfirst(\trans_choice('models.classes.product', 1))
]));
}
return redirect()->back()
->with('error', __('crud.messages.cannot_be_created', [
'model' => Str::ucfirst(\trans_choice('models.classes.product', 1))
]));
});
}
/**
* Show the form for editing the specified resource.
*
* @param \App\Models\Product $product
* @return \Illuminate\Contracts\View\View
*/
public function edit(Product $product): View
{
$rangeModelList = Range::all();
return view('bo.pages.products.edit', [
'rangeModelList' => $rangeModelList,
'productModel' => $product
]);
}
/**
* 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): 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(\trans_choice('models.classes.product', 1))
]));
}
return redirect()->route('bo.products.edit', $product)
->with('error', __('crud.messages.cannot_be_updated', [
'model' => Str::ucfirst(\trans_choice('models.classes.product', 1))
]));
});
}
/**
* Remove the specified resource from storage.
*
* @param \App\Models\Product $product
* @return \Illuminate\Http\RedirectResponse
*/
public function destroy(Product $product): RedirectResponse
{
if ($product->deleteOrFail()) {
return redirect()->route('bo.products.index')
->with('success', __('crud.messages.has_been_deleted', [
'model' => Str::ucfirst(\trans_choice('models.classes.product', 1))
]));
}
return redirect()->back()
->with('error', __('crud.messages.cannot_be_deleted', [
'model' => Str::ucfirst(\trans_choice('models.classes.product', 1))
]));
}
}

View file

@ -0,0 +1,155 @@
<?php
namespace App\Http\Controllers\Bo;
use App\Http\Controllers\Controller;
use App\Http\Requests\Bo\Ranges\StoreRangeRequest;
use App\Http\Requests\Bo\Ranges\UpdateRangeRequest;
use App\Models\Brand;
use App\Models\Range;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class RangeController extends Controller
{
/**
* Create the controller instance.
*/
public function __construct()
{
$this->authorizeResource(Range::class);
}
/**
* Display a listing of the resource.
*
* @param \Illuminate\Http\Client\Request $request
* @return \Illuminate\Contracts\View\View
*/
public function index(Request $request): View
{
/** @var \Illuminate\Database\Eloquent\Builder $query */
$query = Range::query()->with('brand');
/** @var string $search Search field */
$search = $request->input('search');
if ($search) {
$this->searchQuery(
$query,
$search,
null,
'name',
);
}
$searchFields = \implode(', ', [
trans('validation.attributes.name'),
]);
/** Sort columns with a query */
$this->sortQuery($query);
/** Custom pagination */
$rangeModelList = $this->paginate($query);
return view('bo.pages.ranges.index', compact('rangeModelList', 'search', 'searchFields'));
}
/**
* Show the form for creating a new resource.
*
* @return \Illuminate\Contracts\View\View
*/
public function create(): \Illuminate\Contracts\View\View
{
$brandModelList = Brand::all();
return view('bo.pages.ranges.create', ['brandModelList' => $brandModelList]);
}
/**
* Store a newly created resource in storage.
*
* @param \App\Http\Requests\Bo\Ranges\StoreRangeRequest $request
* @return \Illuminate\Http\RedirectResponse
*/
public function store(StoreRangeRequest $request): RedirectResponse
{
return DB::transaction(function () use ($request) {
$range = new Range();
$range->fill($request->validated());
if ($range->saveOrFail()) {
return redirect()->route('bo.range.edit', $range)
->with('success', __('crud.messages.has_been_created', [
'model' => Str::ucfirst(\trans_choice('models.classes.range', 1))
]));
}
return redirect()->back()
->with('error', __('crud.messages.cannot_be_created', [
'model' => Str::ucfirst(\trans_choice('models.classes.range', 1))
]));
});
}
/**
* Show the form for editing the specified resource.
*
* @param \App\Models\Range $range
* @return \Illuminate\Contracts\View\View
*/
public function edit(Range $range): View
{
$brandModelList = Brand::all();
return view('bo.pages.ranges.edit', [
'brandModelList' => $brandModelList,
'rangeModel' => $range
]);
}
/**
* Update the specified resource in storage.
*
* @param \App\Http\Requests\Bo\Ranges\UpdateRangeRequest $request
* @param \App\Models\Range $range
* @return \Illuminate\Http\RedirectResponse
*/
public function update(UpdateRangeRequest $request, Range $range): RedirectResponse
{
return DB::transaction(function () use ($request, $range) {
$range->fill($request->validated());
if ($range->saveOrFail()) {
return redirect()->route('bo.ranges.edit', $range)
->with('success', __('crud.messages.has_been_updated', [
'model' => Str::ucfirst(\trans_choice('models.classes.range', 1))
]));
}
return redirect()->route('bo.ranges.edit', $range)
->with('error', __('crud.messages.cannot_be_updated', [
'model' => Str::ucfirst(\trans_choice('models.classes.range', 1))
]));
});
}
/**
* Remove the specified resource from storage.
*
* @param \App\Models\Range $range
* @return \Illuminate\Http\RedirectResponse
*/
public function destroy(Range $range): RedirectResponse
{
if ($range->deleteOrFail()) {
return redirect()->route('bo.ranges.index')
->with('success', __('crud.messages.has_been_deleted', [
'model' => Str::ucfirst(\trans_choice('models.classes.range', 1))
]));
}
return redirect()->back()
->with('error', __('crud.messages.cannot_be_deleted', [
'model' => Str::ucfirst(\trans_choice('models.classes.range', 1))
]));
}
}

View file

@ -0,0 +1,51 @@
<?php
namespace App\Http\Requests\Bo\Brands;
use App\Models\Brand;
use App\Traits\Requests\HasPicture;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Gate;
class StoreBrandRequest extends FormRequest
{
use HasPicture;
/**
* Determine if the user is authorized to make this request.
*
* @return boolean
*/
public function authorize(): bool
{
return Gate::allows('create', Brand::class);
}
/**
* Prepare the data for validation.
*
* @return void
*/
protected function prepareForValidation()
{
$this->mergePicture('picture');
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string,\Illuminate\Contracts\Validation\ValidationRule|array|string>
*/
public function rules(): array
{
$rules = [
'name' => 'required|min:2|max:60',
'color' => 'required|color_hex'
];
return \array_merge(
$rules,
$this->pictureRules('picture', 500, 350, 800, 350, false)
);
}
}

View file

@ -0,0 +1,18 @@
<?php
namespace App\Http\Requests\Bo\Brands;
use Illuminate\Support\Facades\Gate;
class UpdateBrandRequest extends StoreBrandRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return boolean
*/
public function authorize(): bool
{
return Gate::allows('update', $this->route('brand'));
}
}

View file

@ -0,0 +1,56 @@
<?php
namespace App\Http\Requests\Bo\Products;
use App\Models\Product;
use App\Traits\Requests\HasPicture;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Str;
class StoreProductRequest extends FormRequest
{
use HasPicture;
/**
* Determine if the user is authorized to make this request.
*
* @return boolean
*/
public function authorize(): bool
{
return Gate::allows('create', Product::class);
}
/**
* Prepare the data for validation.
*
* @return void
*/
protected function prepareForValidation()
{
$this->merge([
'description' => (string)Str::of($this->input('description', ''))
]);
$this->mergePicture('picture');
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array|string>
*/
public function rules(): array
{
$rules = [
'name' => 'required|min:2|max:130',
'description' => 'present',
'range_id' => 'required|exists:ranges,id'
];
return \array_merge(
$rules,
$this->pictureRules('picture', 800, 800, 800, 800, false)
);
}
}

View file

@ -0,0 +1,18 @@
<?php
namespace App\Http\Requests\Bo\Products;
use Illuminate\Support\Facades\Gate;
class UpdateProductRequest extends StoreProductRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return boolean
*/
public function authorize(): bool
{
return Gate::allows('update', $this->route('product'));
}
}

View file

@ -0,0 +1,51 @@
<?php
namespace App\Http\Requests\Bo\Ranges;
use App\Models\Range;
use App\Traits\Requests\HasPicture;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Gate;
class StoreRangeRequest extends FormRequest
{
use HasPicture;
/**
* Determine if the user is authorized to make this request.
*
* @return boolean
*/
public function authorize(): bool
{
return Gate::allows('create', Range::class);
}
/**
* Prepare the data for validation.
*
* @return void
*/
protected function prepareForValidation()
{
$this->mergePicture('picture');
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array|string>
*/
public function rules(): array
{
$rules = [
'name' => 'required|min:2|max:90',
'brand_id' => 'required|exists:brands,id'
];
return \array_merge(
$rules,
$this->pictureRules('picture', 500, 500, 800, 500, false)
);
}
}

View file

@ -0,0 +1,18 @@
<?php
namespace App\Http\Requests\Bo\Ranges;
use Illuminate\Support\Facades\Gate;
class UpdateRangeRequest extends StoreRangeRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return boolean
*/
public function authorize(): bool
{
return Gate::allows('update', $this->route('range'));
}
}

View file

@ -471,19 +471,19 @@ class ToolboxHelper
$height = intval($dimensions[1]);
$xMessage = trans(
":attribute la largeur doit être comprise entre {$sizes->minWith}px et {$sizes->maxWith}px"
":attribute la largeur doit être comprise entre {$sizes->minWidth}px et {$sizes->maxWidth}px"
);
if ($sizes->minWith === $sizes->maxWith) {
$xMessage = trans(":attribute la largeur doit être de {$sizes->minWith}px");
if ($sizes->minWidth === $sizes->maxWidth) {
$xMessage = trans(":attribute la largeur doit être de {$sizes->minWidth}px");
}
$yMessage = trans(
":attribute la hauteur doit être comprise entre {$sizes->minWith}px et {$sizes->maxWith}px"
":attribute la hauteur doit être comprise entre {$sizes->minWidth}px et {$sizes->maxWidth}px"
);
if ($sizes->minHeight === $sizes->maxHeight) {
$yMessage = trans(":attribute la hauteur doit être de {$sizes->minHeight}px");
}
if (($width < $sizes->minWith) or ($width > $sizes->maxWith)) {
if (($width < $sizes->minWidth) or ($width > $sizes->maxWidth)) {
$fail($xMessage);
}
if (($height < $sizes->minHeight) or ($height > $sizes->maxHeight)) {

105
app/Models/Brand.php Normal file
View file

@ -0,0 +1,105 @@
<?php
namespace App\Models;
use App\Casts\HtmlColor;
use App\Lib\Helpers\FileStorageHelper;
use App\Lib\Helpers\ToolboxHelper;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Str;
/**
* @property string $name Name of the brand.
* @property string $slug Slug of the name for the brand.
* @property string $color HTML Color string.
* @property-read \Illuminate\Support\Carbon $created_at Created date.
* @property-read \Illuminate\Support\Carbon $updated_at Updated date.
*
* @property-read \Illuminate\Database\Eloquent\Collection<\App\Models\Range> $ranges Ranges Has-many relationship.
*/
class Brand extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'name',
'color',
'picture',
];
/**
* The attributes that should be cast.
*
* @var array
*/
protected $casts = [
'color' => HtmlColor::class
];
/**
* Perform any actions required after the model boots.
*
* @return void
*/
protected static function booted(): void
{
static::creating(function (self $model) {
// * ASSERTIONS
static::assertNameIsUnique($model->name);
// * TRANSFORMATIONS
$model->slug = Str::slug($model->name);
$model->picture = FileStorageHelper::storeFile($model, $model->picture, true);
});
static::updating(function (self $model) {
// * ASSERTIONS
static::assertNameIsUnique($model->name, $model->id);
// * TRANSFORMATIONS
$model->slug = Str::slug($model->name);
$model->picture = FileStorageHelper::storeFile($model, $model->picture, true);
});
static::updated(function (self $model) {
FileStorageHelper::removeOldFile($model, 'picture');
});
static::deleted(function (self $model) {
FileStorageHelper::removeFile($model, 'picture');
});
}
// * RELATIONSHIPS
/**
* Get all of the ranges for the Brand
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function ranges(): HasMany
{
return $this->hasMany(Range::class);
}
// * METHODS
/**
* Asserts using validation that the name is unique
*
* @param string|null $name
* @param integer|null $id
* @return void
* @throws \Illuminate\Validation\ValidationException If name already exists.
*/
private static function assertNameIsUnique(string|null $name, ?int $id = null)
{
$table = (new self())->getTable();
ToolboxHelper::assertFieldIsUnique($table, 'name', $name, $id);
ToolboxHelper::assertFieldIsUnique($table, 'slug', Str::slug($name), $id);
}
}

96
app/Models/Product.php Normal file
View file

@ -0,0 +1,96 @@
<?php
namespace App\Models;
use App\Lib\Helpers\FileStorageHelper;
use App\Lib\Helpers\ToolboxHelper;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Str;
/**
* @property string $name Name of the product.
* @property string $slug Slug of the name for the product.
* @property string $description WYSIWYG text description using ckeditor.
* @property-read \Illuminate\Support\Carbon $created_at Created date.
* @property-read \Illuminate\Support\Carbon $updated_at Updated date.
*
* @property-read \App\Models\Range $range Range Belongs-to.
*/
class Product extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'name',
'description',
'picture',
'range_id',
];
/**
* Perform any actions required after the model boots.
*
* @return void
*/
protected static function booted(): void
{
static::creating(function (self $model) {
// * ASSERTIONS
static::assertNameIsUnique($model->name);
// * TRANSFORMATIONS
$model->slug = Str::slug($model->name);
$model->picture = FileStorageHelper::storeFile($model, $model->picture, true);
});
static::updating(function (self $model) {
// * ASSERTIONS
static::assertNameIsUnique($model->name, $model->id);
// * TRANSFORMATIONS
$model->slug = Str::slug($model->name);
$model->picture = FileStorageHelper::storeFile($model, $model->picture, true);
});
static::updated(function (self $model) {
FileStorageHelper::removeOldFile($model, 'picture');
});
static::deleted(function (self $model) {
FileStorageHelper::removeFile($model, 'picture');
});
}
// * RELATIONSHIPS
/**
* Get the range that owns the Product
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function range(): BelongsTo
{
return $this->belongsTo(Range::class);
}
// * METHODS
/**
* Asserts using validation that the name is unique
*
* @param string|null $name
* @param integer|null $id
* @return void
* @throws \Illuminate\Validation\ValidationException If name already exists.
*/
private static function assertNameIsUnique(string|null $name, ?int $id = null)
{
$table = (new self())->getTable();
ToolboxHelper::assertFieldIsUnique($table, 'name', $name, $id);
ToolboxHelper::assertFieldIsUnique($table, 'slug', Str::slug($name), $id);
}
}

106
app/Models/Range.php Normal file
View file

@ -0,0 +1,106 @@
<?php
namespace App\Models;
use App\Lib\Helpers\FileStorageHelper;
use App\Lib\Helpers\ToolboxHelper;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Str;
/**
* @property string $name Name of the range.
* @property string $slug Slug of the name for the range.
* @property-read \Illuminate\Support\Carbon $created_at Created date.
* @property-read \Illuminate\Support\Carbon $updated_at Updated date.
*
* @property-read \App\Models\Brand $brand Brand Belongs-to.
* @property-read \Illuminate\Database\Eloquent\Collection<\App\Models\Product> $products Products Has-many.
*/
class Range extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'name',
'picture',
'brand_id',
];
/**
* Perform any actions required after the model boots.
*
* @return void
*/
protected static function booted(): void
{
static::creating(function (self $model) {
// * ASSERTIONS
static::assertNameIsUnique($model->name);
// * TRANSFORMATIONS
$model->slug = Str::slug($model->name);
$model->picture = FileStorageHelper::storeFile($model, $model->picture, true);
});
static::updating(function (self $model) {
// * ASSERTIONS
static::assertNameIsUnique($model->name, $model->id);
// * TRANSFORMATIONS
$model->slug = Str::slug($model->name);
$model->picture = FileStorageHelper::storeFile($model, $model->picture, true);
});
static::updated(function (self $model) {
FileStorageHelper::removeOldFile($model, 'picture');
});
static::deleted(function (self $model) {
FileStorageHelper::removeFile($model, 'picture');
});
}
// * RELATIONSHIPS
/**
* Get the brand that owns the Product
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function brand(): BelongsTo
{
return $this->belongsTo(Brand::class);
}
/**
* Get all of the products for the Brand
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function products(): HasMany
{
return $this->hasMany(Product::class);
}
// * METHODS
/**
* Asserts using validation that the name is unique
*
* @param string|null $name
* @param integer|null $id
* @return void
* @throws \Illuminate\Validation\ValidationException If name already exists.
*/
private static function assertNameIsUnique(string|null $name, ?int $id = null)
{
$table = (new self())->getTable();
ToolboxHelper::assertFieldIsUnique($table, 'name', $name, $id);
ToolboxHelper::assertFieldIsUnique($table, 'slug', Str::slug($name), $id);
}
}

View file

@ -0,0 +1,103 @@
<?php
namespace App\Policies;
use App\Enums\Users\RoleEnum;
use App\Models\Brand;
use App\Models\User;
use App\Policies\Rules\UserStaticRules;
class BrandPolicy
{
/**
* Determine whether the user can view any models.
*
* @param \App\Models\User $authUser
* @return boolean
*/
public function viewAny(User $authUser): bool
{
return UserStaticRules::atLeastRole($authUser, RoleEnum::admin);
}
/**
* Determine whether the user can view the model.
*
* @param \App\Models\User $authUser
* @param \App\Models\Brand $brand
* @return boolean
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
*/
public function view(User $authUser, Brand $brand): bool
{
// phpcs:enable
return UserStaticRules::atLeastRole($authUser, RoleEnum::admin);
}
/**
* Determine whether the user can create models.
*
* @param \App\Models\User $authUser
* @return boolean
*/
public function create(User $authUser): bool
{
return UserStaticRules::atLeastRole($authUser, RoleEnum::admin);
}
/**
* Determine whether the user can update the model.
*
* @param \App\Models\User $authUser
* @param \App\Models\Brand $brand
* @return boolean
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
*/
public function update(User $authUser, Brand $brand): bool
{
// phpcs:enable
return UserStaticRules::atLeastRole($authUser, RoleEnum::admin);
}
/**
* Determine whether the user can delete the model.
*
* @param \App\Models\User $authUser
* @param \App\Models\Brand $brand
* @return boolean
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
*/
public function delete(User $authUser, Brand $brand): bool
{
// phpcs:enable
return UserStaticRules::atLeastRole($authUser, RoleEnum::admin);
}
/**
* Determine whether the user can restore the model.
*
* @param \App\Models\User $authUser
* @param \App\Models\Brand $brand
* @return boolean
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
*/
public function restore(User $authUser, Brand $brand): bool
{
// phpcs:enable
return false;
}
/**
* Determine whether the user can permanently delete the model.
*
* @param \App\Models\User $authUser
* @param \App\Models\Brand $brand
* @return boolean
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
*/
public function forceDelete(User $authUser, Brand $brand): bool
{
// phpcs:enable
return false;
}
}

View file

@ -0,0 +1,103 @@
<?php
namespace App\Policies;
use App\Enums\Users\RoleEnum;
use App\Models\Product;
use App\Models\User;
use App\Policies\Rules\UserStaticRules;
class ProductPolicy
{
/**
* Determine whether the user can view any models.
*
* @param \App\Models\User $authUser
* @return boolean
*/
public function viewAny(User $authUser): bool
{
return UserStaticRules::atLeastRole($authUser, RoleEnum::admin);
}
/**
* Determine whether the user can view the model.
*
* @param \App\Models\User $authUser
* @param \App\Models\Product $product
* @return boolean
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
*/
public function view(User $authUser, Product $product): bool
{
// phpcs:enable
return UserStaticRules::atLeastRole($authUser, RoleEnum::admin);
}
/**
* Determine whether the user can create models.
*
* @param \App\Models\User $authUser
* @return boolean
*/
public function create(User $authUser): bool
{
return UserStaticRules::atLeastRole($authUser, RoleEnum::admin);
}
/**
* Determine whether the user can update the model.
*
* @param \App\Models\User $authUser
* @param \App\Models\Product $product
* @return boolean
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
*/
public function update(User $authUser, Product $product): bool
{
// phpcs:enable
return UserStaticRules::atLeastRole($authUser, RoleEnum::admin);
}
/**
* Determine whether the user can delete the model.
*
* @param \App\Models\User $authUser
* @param \App\Models\Product $product
* @return boolean
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
*/
public function delete(User $authUser, Product $product): bool
{
// phpcs:enable
return UserStaticRules::atLeastRole($authUser, RoleEnum::admin);
}
/**
* Determine whether the user can restore the model.
*
* @param \App\Models\User $authUser
* @param \App\Models\Product $product
* @return boolean
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
*/
public function restore(User $authUser, Product $product): bool
{
// phpcs:enable
return false;
}
/**
* Determine whether the user can permanently delete the model.
*
* @param \App\Models\User $authUser
* @param \App\Models\Product $product
* @return boolean
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
*/
public function forceDelete(User $authUser, Product $product): bool
{
// phpcs:enable
return false;
}
}

View file

@ -0,0 +1,103 @@
<?php
namespace App\Policies;
use App\Enums\Users\RoleEnum;
use App\Models\Range;
use App\Models\User;
use App\Policies\Rules\UserStaticRules;
class RangePolicy
{
/**
* Determine whether the user can view any models.
*
* @param \App\Models\User $authUser
* @return boolean
*/
public function viewAny(User $authUser): bool
{
return UserStaticRules::atLeastRole($authUser, RoleEnum::admin);
}
/**
* Determine whether the user can view the model.
*
* @param \App\Models\User $authUser
* @param \App\Models\Range $range
* @return boolean
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
*/
public function view(User $authUser, Range $range): bool
{
// phpcs:enable
return UserStaticRules::atLeastRole($authUser, RoleEnum::admin);
}
/**
* Determine whether the user can create models.
*
* @param \App\Models\User $authUser
* @return boolean
*/
public function create(User $authUser): bool
{
return UserStaticRules::atLeastRole($authUser, RoleEnum::admin);
}
/**
* Determine whether the user can update the model.
*
* @param \App\Models\User $authUser
* @param \App\Models\Range $range
* @return boolean
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
*/
public function update(User $authUser, Range $range): bool
{
// phpcs:enable
return UserStaticRules::atLeastRole($authUser, RoleEnum::admin);
}
/**
* Determine whether the user can delete the model.
*
* @param \App\Models\User $authUser
* @param \App\Models\Range $range
* @return boolean
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
*/
public function delete(User $authUser, Range $range): bool
{
// phpcs:enable
return UserStaticRules::atLeastRole($authUser, RoleEnum::admin);
}
/**
* Determine whether the user can restore the model.
*
* @param \App\Models\User $authUser
* @param \App\Models\Range $range
* @return boolean
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
*/
public function restore(User $authUser, Range $range): bool
{
// phpcs:enable
return false;
}
/**
* Determine whether the user can permanently delete the model.
*
* @param \App\Models\User $authUser
* @param \App\Models\Range $range
* @return boolean
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
*/
public function forceDelete(User $authUser, Range $range): bool
{
// phpcs:enable
return false;
}
}

View file

@ -0,0 +1,183 @@
<?php
namespace App\Traits\Requests;
use App\Lib\Helpers\ToolboxHelper;
/**
* ! This assumes file system is using local storage
*/
trait HasPicture
{
/**
* Minimum width
*
* @var integer
*/
private $minWidth = 400;
/**
* Maximum width
*
* @var integer
*/
private $maxWidth = 1800;
/**
* Minimum height
*
* @var integer
*/
private $minHeight = 300;
/**
* Maximum height
*
* @var integer
*/
private $maxHeight = 1800;
/**
* Image rules
*
* @param string $name
* @param integer|null $minWidth
* @param integer|null $minHeight
* @param integer|null $maxWidth
* @param integer|null $maxHeight
* @param boolean $nullable
* @param integer|null $size
* @param string|null $requiredWith
* @param string|null $requiredWithout
* @return array
*/
public function pictureRules(
string $name = 'picture',
?int $minWidth = null,
?int $minHeight = null,
?int $maxWidth = null,
?int $maxHeight = null,
bool $nullable = false,
?int $size = null,
?string $requiredWith = null,
?string $requiredWithout = null,
): array {
$dimensions = (object)[
'minWidth' => $minWidth ?? $this->minWidth,
'minHeight' => $minHeight ?? $this->minHeight,
'maxWidth' => $maxWidth ?? $this->maxWidth,
'maxHeight' => $maxHeight ?? $this->maxHeight
];
return [
$name => [
!$nullable ? 'required' : 'nullable',
(strlen($requiredWith)) ? 'required_with:' . $requiredWith : '',
(strlen($requiredWithout)) ? 'required_without:' . $requiredWithout : '',
($size) ? 'max:' . $size : '',
function (string $attribute, $value, callable $fail) use ($dimensions) {
return ToolboxHelper::validatePicture($attribute, $value, $fail, $dimensions);
}
]
];
}
/**
* Images rules
*
* @param string $name
* @param integer|null $minWidth
* @param integer|null $minHeight
* @param integer|null $maxWidth
* @param integer|null $maxHeight
* @return array
*/
public function picturesRules(
string $name = 'pictures',
?int $minWidth = null,
?int $minHeight = null,
?int $maxWidth = null,
?int $maxHeight = null
): array {
$sizes = (object)[
'minWidth' => $minWidth ?? $this->minWidth,
'minHeight' => $minHeight ?? $this->minHeight,
'maxWidth' => $maxWidth ?? $this->maxWidth,
'maxHeight' => $maxHeight ?? $this->maxHeight
];
return [
$name => 'sometimes|array',
"{$name}Validate" => "required_with:{$name}|array",
"{$name}Validate.*" => [
'required',
function (string $attribute, $value, callable $fail) use ($sizes) {
return $this->validatePicture($attribute, $value, $fail, $sizes);
}
]
];
}
/**
* Merges products pictures fields with request data
*
* @param string $name
* @return void
*/
protected function mergePicture(string $name = 'picture'): void
{
$picturePath = $this->{$name};
if (is_string($picturePath)) {
$this->merge(["{$name}Validate" => []]);
// ! Empty picture.
if ($picturePath === 'false') {
// * Empty picture and exit
$this->merge([$name => []]);
return;
}
$decodedPath = \urldecode($picturePath);
$this->merge([
"{$name}Validate" => array_merge(
$this->{"{$name}Validate"},
[\public_path(ltrim($decodedPath, '/'))]
)
]);
$changedPicture = ltrim($decodedPath, '/');
$this->merge([
$name => $changedPicture
]);
} //end if
}
/**
* Merges products pictures fields with request data
*
* @param string $name
* @return void
*/
protected function mergePictures(string $name = 'pictures'): void
{
$pictures = $this->{$name};
if (is_array($pictures)) {
$this->merge(["{$name}Validate" => []]);
foreach ($pictures as $key => $picturePath) {
// ! Empty picture.
if ($picturePath === 'false') {
// * Empty picture and exit
$this->merge([$name => []]);
return;
}
$decodedPath = \urldecode($picturePath);
$this->merge([
"{$name}Validate" => array_merge(
$this->{"{$name}Validate"},
[\public_path(ltrim($decodedPath, '/'))]
)
]);
$changedPicture = $this->{$name};
$changedPicture[$key] = ltrim($decodedPath, '/');
$this->merge([
$name => $changedPicture
]);
}
} //end if
}
}

View file

@ -15,6 +15,7 @@
"php": "^8.2",
"diglactic/laravel-breadcrumbs": "^8.0",
"guzzlehttp/guzzle": "^7.2",
"kaishiyoku/laravel-color-validation": "^2.1",
"laravel/framework": "^10.0",
"laravel/tinker": "^2.8",
"laravel/ui": "^4.2",

128
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "f6f8667757d8be0e48544e0bc4e250e5",
"content-hash": "be23935d916f62e36467d9c3fa5f5917",
"packages": [
{
"name": "brick/math",
@ -1338,6 +1338,69 @@
},
"time": "2021-10-08T21:21:46+00:00"
},
{
"name": "kaishiyoku/laravel-color-validation",
"version": "2.1.1",
"source": {
"type": "git",
"url": "https://github.com/Kaishiyoku/laravel-color-validation.git",
"reference": "4a77492335e37d18055ec9547332d224186d074b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Kaishiyoku/laravel-color-validation/zipball/4a77492335e37d18055ec9547332d224186d074b",
"reference": "4a77492335e37d18055ec9547332d224186d074b",
"shasum": ""
},
"require": {
"illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0",
"illuminate/validation": "^6.0|^7.0|^8.0|^9.0|^10.0",
"php": "^7.4|^8.0",
"spatie/regex": "^1.4|^3.1"
},
"require-dev": {
"orchestra/testbench": "^6.0",
"phpunit/phpunit": "^9.5"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Kaishiyoku\\Validation\\Color\\ServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Kaishiyoku\\": "src/Kaishiyoku"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Kaishiyoku",
"email": "dev@andreas-wiedel.de",
"homepage": "https://andreas-wiedel.de",
"role": "Maintainer"
}
],
"description": "Laravel color validation rules",
"homepage": "https://github.com/Kaishiyoku/laravel-color-validation",
"keywords": [
"color",
"laravel",
"validation rules",
"validator"
],
"support": {
"issues": "https://github.com/Kaishiyoku/laravel-color-validation/issues",
"source": "https://github.com/Kaishiyoku/laravel-color-validation/tree/2.1.1"
},
"time": "2023-02-13T20:23:52+00:00"
},
{
"name": "laravel/framework",
"version": "v10.23.1",
@ -4247,6 +4310,69 @@
],
"time": "2023-09-05T11:02:34+00:00"
},
{
"name": "spatie/regex",
"version": "3.1.1",
"source": {
"type": "git",
"url": "https://github.com/spatie/regex.git",
"reference": "d543de2019a0068e7b80da0ba24f1c51c7469303"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/regex/zipball/d543de2019a0068e7b80da0ba24f1c51c7469303",
"reference": "d543de2019a0068e7b80da0ba24f1c51c7469303",
"shasum": ""
},
"require": {
"php": "^8.0|^8.1"
},
"require-dev": {
"phpunit/phpunit": "^9.3"
},
"type": "library",
"autoload": {
"psr-4": {
"Spatie\\Regex\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Sebastian De Deyne",
"email": "sebastian@spatie.be",
"homepage": "https://spatie.be",
"role": "Developer"
}
],
"description": "A sane interface for php's built in preg_* functions",
"homepage": "https://github.com/spatie/regex",
"keywords": [
"expression",
"expressions",
"regex",
"regular",
"spatie"
],
"support": {
"issues": "https://github.com/spatie/regex/issues",
"source": "https://github.com/spatie/regex/tree/3.1.1"
},
"funding": [
{
"url": "https://spatie.be/open-source/support-us",
"type": "custom"
},
{
"url": "https://github.com/spatie",
"type": "github"
}
],
"time": "2021-11-30T21:13:59+00:00"
},
{
"name": "symfony/console",
"version": "v6.3.4",

View file

@ -0,0 +1,24 @@
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Brand>
*/
class BrandFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'name' => 'product ' . fake()->unique()->safeColorName(),
'color' => fake()->unique()->hexColor()
];
}
}

View file

@ -0,0 +1,24 @@
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Product>
*/
class ProductFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'name' => 'product' . fake()->unique()->words(3, true),
'description' => '<strong>TEXT</strong>' . fake()->realText()
];
}
}

View file

@ -0,0 +1,23 @@
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Range>
*/
class RangeFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'name' => 'gamme ' . fake()->unique()->streetName(),
];
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

View file

@ -0,0 +1,39 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up(): void
{
Schema::create('brands', function (Blueprint $table) {
$table->id();
$table->string('name')->comment('Nom de la marque');
$table->string('slug')->comment('Slug de la marque');
$table->unsignedInteger('color')->comment('Couleur de la marque');
$table->string('picture')->comment('Logo de la marque');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down(): void
{
Schema::dropIfExists('brands');
}
};

View file

@ -0,0 +1,40 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up(): void
{
Schema::create('ranges', function (Blueprint $table) {
$table->id();
$table->string('name')->comment('Nom de la gamme');
$table->string('slug')->comment('Slug de la gamme');
$table->string('picture')->comment('Logo de la marque');
$table->foreignId('brand_id');
$table->foreign('brand_id')->references('id')->on('brands')->cascadeOnDelete();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down(): void
{
Schema::dropIfExists('ranges');
}
};

View file

@ -0,0 +1,42 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up(): void
{
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->string('name')->comment('Nom du produit');
$table->string('slug')->comment('Slug du produit');
$table->text('description')->comment('Description du produit');
$table->string('picture')->comment('Logo du produit');
$table->foreignId('range_id');
$table->foreign('range_id')->references('id')->on('ranges')->cascadeOnDelete();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down(): void
{
Schema::dropIfExists('products');
}
};

View file

@ -0,0 +1,36 @@
<?php
namespace Database\Seeders;
use App\Lib\Helpers\FileStorageHelper;
use App\Models\Brand;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Schema;
class BrandSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run(): void
{
Schema::disableForeignKeyConstraints();
DB::table((new Brand())->getTable())->truncate();
Schema::enableForeignKeyConstraints();
File::deleteDirectory(\storage_path('app/public/modelfiles/' . (new Brand())->getTable()));
Brand::factory(fake()->randomDigitNot([0, 1, 2, 3]))->make()->each(function (Brand $brand) {
$brand->picture = FileStorageHelper::storeFile(
new Brand(),
new \SplFileInfo(
\resource_path('../database/factories/assets/brands/default-picture-600x350.jpg')
)
);
$brand->save();
});
}
}

View file

@ -15,7 +15,10 @@ class DatabaseSeeder extends Seeder
{
$this->call([
UserSeeder::class,
StaticPageSeeder::class
StaticPageSeeder::class,
BrandSeeder::class,
RangeSeeder::class,
ProductSeeder::class,
]);
}
}

View file

@ -0,0 +1,41 @@
<?php
namespace Database\Seeders;
use App\Lib\Helpers\FileStorageHelper;
use App\Models\Product;
use App\Models\Range;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Schema;
class ProductSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run(): void
{
Schema::disableForeignKeyConstraints();
DB::table((new Product())->getTable())->truncate();
Schema::enableForeignKeyConstraints();
File::deleteDirectory(\storage_path('app/public/modelfiles/' . (new Product())->getTable()));
Range::all()->each(
fn (Range $range) =>
Product::factory(\random_int(6, 10))->make()->each(function (Product $product) use ($range) {
$product->picture = FileStorageHelper::storeFile(
new Product(),
new \SplFileInfo(
\resource_path('../database/factories/assets/products/default-picture-800x800.jpg')
)
);
$product->range()->associate($range);
$product->save();
})
);
}
}

View file

@ -0,0 +1,42 @@
<?php
namespace Database\Seeders;
use App\Lib\Helpers\FileStorageHelper;
use App\Models\Brand;
use App\Models\Range;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Schema;
class RangeSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run(): void
{
Schema::disableForeignKeyConstraints();
DB::table((new Range())->getTable())->truncate();
Schema::enableForeignKeyConstraints();
File::deleteDirectory(\storage_path('app/public/modelfiles/' . (new Range())->getTable()));
Brand::all()->each(
fn (Brand $brand) =>
Range::factory(\random_int(6, 16))->make()->each(function (Range $range) use ($brand) {
$range->picture = FileStorageHelper::storeFile(
new Range(),
new \SplFileInfo(
\resource_path('../database/factories/assets/ranges/default-picture-600x500.jpg')
)
);
$range->brand()->associate($brand);
$range->save();
})
);
}
}

View file

@ -87,5 +87,21 @@ return [
'sort_delete' => 'Supprimer le tri',
'sort_arrow' => 'Ici, les flèches sont utilisées pour trier et non pour filtrer',
'right' => 'Vous n\'avez pas les droits',
],
'meta' => [
'title' => [
'create' => '{0} Création d\'un :model|{1} Création d\'une :model',
'edit' => '{0} Edition d\'un :model|{1} Edition d\'une :model',
'show' => '{0} Visualisation d\'un :model|{1} Visualisation d\'une :model',
],
// phpcs:disable Generic.Files.LineLength.TooLong
'description' => [
'index' => '{0} Liste de tous les :model|{1} Liste de toutes les :model',
'create' => '{0} Création d\'un tout nouveau :model|{1} Création d\'une toute nouvelle :model|{2} Création d\'un tout nouvel :model',
'edit' => '{0} Edition d\'un :model déjà existant|{1} Edition d\'une :model déjà existante',
'show' => '{0} Visualisation d\'un :model déjà existant|{1} Visualisation d\'une :model déjà existante',
],
// phpcs:enable
]
];

View file

@ -4,6 +4,9 @@ return [
'classes' => [
'user' => 'utilisateur',
'activity_log' => 'journal d\'activité|journaux d\'activité',
'static_page' => 'page statique|pages statiques'
'static_page' => 'page statique|pages statiques',
'brand' => 'marque|marques',
'range' => 'gamme|gammes',
'product' => 'produit|produits',
]
];

View file

@ -229,5 +229,6 @@ return [
'static_page_type' => 'type de page',
'seo_title' => 'titre SEO',
'seo_description' => 'description SEO',
'color' => 'couleur',
],
];

View file

@ -6,6 +6,30 @@
{{ __('Accueil') }}
</a>
</li>
@can('viewAny', \App\Models\Brand::class)
<li class="nav-item">
<a class="nav-link @if (request()->routeIs('bo.brands.*')) active @endif" data-bs-toggle="tooltip" href="{{ route('bo.brands.index') }}"
title="{{ __('crud.helpers.list_all', ['model' => Str::of(trans_choice('models.classes.brand', '*'))->title()]) }}">
{{ Str::of(trans_choice('models.classes.brand', '*'))->title() }}
</a>
</li>
@endcan
@can('viewAny', \App\Models\Range::class)
<li class="nav-item">
<a class="nav-link @if (request()->routeIs('bo.ranges.*')) active @endif" data-bs-toggle="tooltip" href="{{ route('bo.ranges.index') }}"
title="{{ __('crud.helpers.list_all', ['model' => Str::of(trans_choice('models.classes.range', '*'))->title()]) }}">
{{ Str::of(trans_choice('models.classes.range', '*'))->title() }}
</a>
</li>
@endcan
@can('viewAny', \App\Models\Product::class)
<li class="nav-item">
<a class="nav-link @if (request()->routeIs('bo.products.*')) active @endif" data-bs-toggle="tooltip" href="{{ route('bo.products.index') }}"
title="{{ __('crud.helpers.list_all', ['model' => Str::of(trans_choice('models.classes.product', '*'))->title()]) }}">
{{ Str::of(trans_choice('models.classes.product', '*'))->title() }}
</a>
</li>
@endcan
{{-- @can('viewAny', \App\Models\StaticPage::class)
<li class="nav-item">
<a class="nav-link @if (request()->routeIs('bo.static_pages.*')) active @endif" data-bs-toggle="tooltip"

View file

@ -0,0 +1,36 @@
@extends('bo.layout', ['brParam' => ''])
@section('title', trans_choice('crud.meta.title.create', 1, ['model' => trans_choice('models.classes.brand', 1)]))
@section('description', trans_choice('crud.meta.description.create', 1, ['model' => trans_choice('models.classes.brand', 1)]))
@section('breadcrumb',
request()->route()->getName())
@section('content')
<div class="d-flex justify-content-between flex-md-nowrap align-items-center flex-wrap pb-3">
<div class="d-flex align-items-start flex-row">
<a class="btn btn-primary text-decoration-none m-0" data-bs-toggle="tooltip" href="{{ route('bo.brands.index') }}"
title="{{ __('crud.helpers.list_all', ['model' => trans_choice('models.classes.brand', 1)]) }}">
<i class="fa-solid fa-arrow-left"></i>
</a>
@include('modules.breadcrumbs.breadcrumb-body', ['brParam' => ''])
</div>
<div class="mb-md-0 mb-2">
<button class="btn btn-primary" id="formSubmitClone" data-bs-toggle="tooltip" type="submit"
title="{{ __('crud.helpers.save_model', ['model' => trans_choice('models.classes.brand', 1)]) }}">
{{ Str::ucfirst(__('crud.actions.save')) }}
</button>
</div>
</div>
<form action="{{ route('bo.brands.store') }}" method="POST" enctype="multipart/form-data">
@csrf
@include('bo.pages.brands.form')
<div class="row mt-3">
<div class="col text-center">
<button class="btn btn-primary" id="formSubmit" data-bs-toggle="tooltip" type="submit"
title="{{ __('crud.helpers.save_model', ['model' => trans_choice('models.classes.brand', 1)]) }}">
{{ Str::ucfirst(__('crud.actions.save')) }}
</button>
</div>
</div>
</form>
@endsection

View file

@ -0,0 +1,52 @@
@extends('bo.layout', ['brParam' => $brandModel])
@section('title', trans_choice('crud.meta.title.edit', 1, ['model' => trans_choice('models.classes.brand', 1)]))
@section('description', trans_choice('crud.meta.description.edit', 1, ['model' => trans_choice('models.classes.brand', 1)]))
@section('breadcrumb',
request()->route()->getName())
@section('content')
<div class="d-flex justify-content-between flex-md-nowrap align-items-center flex-wrap pb-3">
<div class="d-flex align-items-start flex-row">
<a class="btn btn-primary text-decoration-none m-0" data-bs-toggle="tooltip" href="{{ route('bo.brands.index') }}"
title="{{ __('crud.helpers.list_all', ['model' => trans_choice('models.classes.brand', '*')]) }}">
<i class="fa-solid fa-arrow-left"></i>
</a>
@include('modules.breadcrumbs.breadcrumb-body', ['brParam' => $brandModel])
</div>
<div class="mb-md-0 mb-2">
<form class="btn-group confirmDeleteTS" action="{{ route('bo.brands.destroy', $brandModel) }}" method="POST" novalidate>
@can('duplicate', $brandModel)
<a class="btn btn-secondary" data-bs-toggle="tooltip" href="{{ route('bo.brands.duplicate', ['brands' => $brandModel]) }}"
title="{{ Str::ucfirst(__('crud.actions.duplicate')) }}">
<i class="fa-solid fa-copy"></i>
</a>
@endcan
<button class="btn btn-primary" id="formSubmitClone" data-bs-toggle="tooltip" type="submit"
title="{{ __('crud.helpers.save_model', ['model' => trans_choice('models.classes.brand', 1)]) }}">
{{ Str::ucfirst(__('crud.actions.save')) }}
</button>
@can('delete', $brandModel)
@csrf
@method('DELETE')
<button class="btn btn-danger" data-bs-toggle="tooltip" type="submit" title="{{ Str::ucfirst(__('crud.actions.delete')) }}">
<i class="fa-solid fa-trash"></i>
</button>
@endcan
</form>
</div>
</div>
<form action="{{ route('bo.brands.update', $brandModel) }}" method="POST" enctype="multipart/form-data">
@csrf
@method('PUT')
@include('bo.pages.brands.form')
<div class="row">
<div class="col text-center">
<button class="btn btn-primary" id="formSubmit" data-bs-toggle="tooltip" type="submit"
title="{{ __('crud.helpers.save_model', ['model' => trans_choice('models.classes.brand', 1)]) }}">
{{ Str::ucfirst(__('crud.actions.save')) }}
</button>
</div>
</div>
</form>
@endsection

View file

@ -0,0 +1,107 @@
<div class="row">
{{-- NAME --}}
<div class="col-12 col-md-6 mb-3">
<fieldset class="h-100 border p-3">
<legend>{{ Str::ucfirst(__('validation.attributes.name')) }}</legend>
<div class="row">
<div class="col-12">
@include('bo.partials.inputs.varchar', [
'id' => 'brandText',
'label' => __(':field :inter:model', [
'field' => Str::ucfirst(__('validation.attributes.name')),
'inter' => __('validation.custom.of_the.female'),
'model' => trans_choice('models.classes.brand', 1),
]),
'hasWordCounter' => true,
'name' => 'name',
'required' => true,
'model' => $brandModel ?? null,
'helper' => __('validation.string', [
'attribute' => __(':field :inter:model', [
'field' => Str::ucfirst(__('validation.attributes.name')),
'inter' => __('validation.custom.of_the.female'),
'model' => trans_choice('models.classes.brand', 1),
]),
]),
'placeholder' => Str::ucfirst(__('validation.attributes.name')),
])
</div>
</div>
</fieldset>
</div>
{{-- COLOR --}}
<div class="col-12 col-md-6 mb-3">
<fieldset class="h-100 border p-3">
<legend>{{ Str::ucfirst(__('validation.custom.color')) }}</legend>
<div class="row">
<div class="col-12">
@include('bo.partials.inputs.int-color', [
'id' => 'brandCouleur',
'label' => __(':field :inter:model', [
'field' => Str::ucfirst(__('validation.custom.color')),
'inter' => __('validation.custom.of_the.female'),
'model' => trans_choice('models.classes.brand', 1),
]),
'name' => 'color',
'required' => true,
'model' => $brandModel ?? null,
'helper' => __('validation.string', [
'attribute' => __(':field :inter:model', [
'field' => Str::ucfirst(__('validation.custom.color')),
'inter' => __('validation.custom.of_the.female'),
'model' => trans_choice('models.classes.brand', 1),
]),
]),
'placeholder' => Str::ucfirst(__('validation.attributes.name')),
])
</div>
</div>
</fieldset>
</div>
{{-- PICTURE --}}
<div class="col-12 pb-3">
<fieldset class="h-100 border p-3">
<legend>{{ Str::ucfirst(__('validation.attributes.image')) }}&nbsp;*</legend>
<div class="row mb-3">
{{-- PICTURE --}}
<div class="col-12">
@include('bo.partials.inputs.varchar-picture', [
'id' => 'brandPicture',
'name' => 'picture',
'width' => 600,
'height' => 350,
'model' => $brandModel ?? null,
'helper' => __('crud.minicrud.dimensions_must_be', [
'widthpx' => '600px',
'heightpx' => '350px',
]),
'required' => true,
'preview' => true,
])
</div>
</div>
</fieldset>
</div>
</div>
<div class="row">
<div class="col text-center">
<p><b>{{ __('* Champs obligatoires') }}</b></p>
</div>
</div>
@push('scripts')
<script nonce="{{ $nonce }}">
document.addEventListener("DOMContentLoaded", () => {
// Submit button clone.
const submit = document.getElementById('formSubmit')
submitClone = document.getElementById('formSubmitClone');
submitClone.addEventListener('click', (event) => {
event.preventDefault();
submit.click('');
})
});
</script>
@endpush

View file

@ -0,0 +1,67 @@
<table class="table-hover table-fix-action m-0 table">
@if (count($brandModelList) > 0)
<thead>
@include('bo.modules.table-col-sorter', [
'cols' => [
'name' => Str::ucfirst(__('validation.attributes.name')),
'color' => Str::ucfirst(__('validation.custom.color')),
'updated_at' => Str::ucfirst(__('validation.attributes.updated_at')),
],
])
</thead>
<tbody>
@foreach ($brandModelList as $brandModel)
<tr class="border-bottom list-item">
<td class="text-center align-middle">{{ $brandModel->name }}</td>
<td class="text-center align-middle">
<span class="badge" style="background-color: {{ $brandModel->color }};">
{{ $brandModel->color }}
</span>
</td>
<td class="text-center align-middle">
<span class="badge bg-secondary">{{ $brandModel->updated_at->isoFormat('LLLL') }}</span>
</td>
<td class="text-end align-middle">
@canAny(['delete', 'duplicate', 'update'], $brandModel)
<form class="btn-group confirmDeleteTS" action="{{ route('bo.brands.destroy', $brandModel) }}" method="POST"
novalidate>
@can('duplicate', $brandModel)
<a class="btn btn-sm btn-secondary" data-bs-toggle="tooltip"
href="{{ route('bo.brands.duplicate', ['brand' => $brandModel]) }}"
title="{{ Str::ucfirst(__('crud.actions.duplicate')) }}">
<i class="fa-solid fa-copy"></i>
</a>
@endcan
@can('update', $brandModel)
<a class="btn btn-sm btn-primary" data-bs-toggle="tooltip"
href="{{ route('bo.brands.edit', ['brand' => $brandModel]) }}"
title="{{ Str::ucfirst(__('crud.actions.update')) }}">
<i class="fa-solid fa-pencil"></i>
</a>
@endcan
@can('delete', $brandModel)
@csrf
@method('DELETE')
<button class="btn btn-sm btn-danger" data-bs-toggle="tooltip" type="submit"
title="{{ Str::ucfirst(__('crud.actions.delete')) }}">
<i class="fa-solid fa-trash"></i>
</button>
@endcan
</form>
@else
<span class="text-danger" data-bs-toggle="tooltip" title="{{ __('list.right') }}">
<i class="fa-solid fa-ban"></i>
</span>
@endcan
</td>
</tr>
@endforeach
</tbody>
@else
<tr>
<td class="border-0">
{{ trans_choice('crud.other.no_model_found', 1, ['model' => trans_choice('models.classes.brand', '*')]) }}
</td>
</tr>
@endif
</table>

View file

@ -0,0 +1,21 @@
@extends('bo.layout')
@section('title', Str::ucfirst(trans_choice('models.classes.brand', '*')))
@section('description', __('Liste de tous les :model', ['model' => trans_choice('models.classes.brand', '*')]))
@section('content')
<div class="d-flex justify-content-between flex-md-nowrap align-items-center flex-wrap">
@include('modules.breadcrumbs.breadcrumb-body')
@can('create', \App\Models\Brand::class)
<a class="btn btn-primary float-right" data-bs-toggle="tooltip" href="{{ route('bo.brands.create') }}"
title="{{ __('crud.helpers.create_model', ['model' => trans_choice('models.classes.brand', 1)]) }}">
<i class="fa-solid fa-plus"></i>
</a>
@endcan
</div>
@include('bo.modules.search-bar')
<div class="table-responsive mb-3">
@include('bo.pages.brands.index-list')
</div>
{{ $brandModelList->links() }}
@endsection

View file

@ -0,0 +1,36 @@
@extends('bo.layout', ['brParam' => ''])
@section('title', trans_choice('crud.meta.title.create', 1, ['model' => trans_choice('models.classes.product', 1)]))
@section('description', trans_choice('crud.meta.description.create', 1, ['model' => trans_choice('models.classes.product', 1)]))
@section('breadcrumb',
request()->route()->getName())
@section('content')
<div class="d-flex justify-content-between flex-md-nowrap align-items-center flex-wrap pb-3">
<div class="d-flex align-items-start flex-row">
<a class="btn btn-primary text-decoration-none m-0" data-bs-toggle="tooltip" href="{{ route('bo.products.index') }}"
title="{{ __('crud.helpers.list_all', ['model' => trans_choice('models.classes.product', '*')]) }}">
<i class="fa-solid fa-arrow-left"></i>
</a>
@include('modules.breadcrumbs.breadcrumb-body', ['brParam' => ''])
</div>
<div class="mb-md-0 mb-2">
<button class="btn btn-primary" id="formSubmitClone" data-bs-toggle="tooltip" type="submit"
title="{{ __('crud.helpers.save_model', ['model' => trans_choice('models.classes.product', 1)]) }}">
{{ Str::ucfirst(__('crud.actions.save')) }}
</button>
</div>
</div>
<form action="{{ route('bo.products.store') }}" method="POST" enctype="multipart/form-data">
@csrf
@include('bo.pages.products.form')
<div class="row mt-3">
<div class="col text-center">
<button class="btn btn-primary" id="formSubmit" data-bs-toggle="tooltip" type="submit"
title="{{ __('crud.helpers.save_model', ['model' => trans_choice('models.classes.product', 1)]) }}">
{{ Str::ucfirst(__('crud.actions.save')) }}
</button>
</div>
</div>
</form>
@endsection

View file

@ -0,0 +1,52 @@
@extends('bo.layout', ['brParam' => $productModel])
@section('title', trans_choice('crud.meta.title.edit', 1, ['model' => trans_choice('models.classes.product', 1)]))
@section('description', trans_choice('crud.meta.description.edit', 1, ['model' => trans_choice('models.classes.product', 1)]))
@section('breadcrumb',
request()->route()->getName())
@section('content')
<div class="d-flex justify-content-between flex-md-nowrap align-items-center flex-wrap pb-3">
<div class="d-flex align-items-start flex-row">
<a class="btn btn-primary text-decoration-none m-0" data-bs-toggle="tooltip" href="{{ route('bo.products.index') }}"
title="{{ __('crud.helpers.list_all', ['model' => trans_choice('models.classes.product', '*')]) }}">
<i class="fa-solid fa-arrow-left"></i>
</a>
@include('modules.breadcrumbs.breadcrumb-body', ['brParam' => $productModel])
</div>
<div class="mb-md-0 mb-2">
<form class="btn-group confirmDeleteTS" action="{{ route('bo.products.destroy', $productModel) }}" method="POST" novalidate>
@can('duplicate', $productModel)
<a class="btn btn-secondary" data-bs-toggle="tooltip" href="{{ route('bo.products.duplicate', ['products' => $productModel]) }}"
title="{{ Str::ucfirst(__('crud.actions.duplicate')) }}">
<i class="fa-solid fa-copy"></i>
</a>
@endcan
<button class="btn btn-primary" id="formSubmitClone" data-bs-toggle="tooltip" type="submit"
title="{{ __('crud.helpers.save_model', ['model' => trans_choice('models.classes.product', 1)]) }}">
{{ Str::ucfirst(__('crud.actions.save')) }}
</button>
@can('delete', $productModel)
@csrf
@method('DELETE')
<button class="btn btn-danger" data-bs-toggle="tooltip" type="submit" title="{{ Str::ucfirst(__('crud.actions.delete')) }}">
<i class="fa-solid fa-trash"></i>
</button>
@endcan
</form>
</div>
</div>
<form action="{{ route('bo.products.update', $productModel) }}" method="POST" enctype="multipart/form-data">
@csrf
@method('PUT')
@include('bo.pages.products.form')
<div class="row">
<div class="col text-center">
<button class="btn btn-primary" id="formSubmit" data-bs-toggle="tooltip" type="submit"
title="{{ __('crud.helpers.save_model', ['model' => trans_choice('models.classes.product', 1)]) }}">
{{ Str::ucfirst(__('crud.actions.save')) }}
</button>
</div>
</div>
</form>
@endsection

View file

@ -0,0 +1,124 @@
<div class="row">
{{-- NAME --}}
<div class="col-12 col-md-6 mb-3">
<fieldset class="h-100 border p-3">
<legend>{{ Str::ucfirst(__('validation.attributes.name')) }}</legend>
<div class="row">
<div class="col-12">
@include('bo.partials.inputs.varchar', [
'id' => 'productText',
'label' => __(':field :inter:model', [
'field' => Str::ucfirst(__('validation.attributes.name')),
'inter' => __('validation.custom.of_the.male'),
'model' => trans_choice('models.classes.product', 1),
]),
'hasWordCounter' => true,
'name' => 'name',
'required' => true,
'model' => $productModel ?? null,
'helper' => __('validation.string', [
'attribute' => __(':field :inter:model', [
'field' => Str::ucfirst(__('validation.attributes.name')),
'inter' => __('validation.custom.of_the.male'),
'model' => trans_choice('models.classes.product', 1),
]),
]),
'placeholder' => Str::ucfirst(__('validation.attributes.name')),
])
</div>
</div>
</fieldset>
</div>
{{-- RANGE_ID --}}
<div class="col-12 col-md-6 mb-3">
<fieldset class="h-100 border p-3">
<legend>{{ Str::ucfirst(trans_choice('models.classes.range', 1)) }}</legend>
<div class="row">
<div class="col-12">
@include('bo.partials.inputs.bigint-foreign-id', [
'id' => 'productRange',
'label' => __(':field :inter:model', [
'field' => Str::ucfirst(trans_choice('models.classes.range', 1)),
'inter' => __('validation.custom.of_the.male'),
'model' => trans_choice('models.classes.product', 1),
]),
'name' => 'range_id',
'required' => true,
'model' => $productModel ?? null,
'associatedModels' => $rangeModelList ?? null,
'foreignLabel' => 'name',
'helper' => __('crud.minicrud.enum_select', [
'enum' => trans_choice('models.classes.range', 1),
]),
])
</div>
</div>
</fieldset>
</div>
{{-- PICTURE --}}
<div class="col-12 pb-3">
<fieldset class="h-100 border p-3">
<legend>{{ Str::ucfirst(__('validation.attributes.image')) }}&nbsp;*</legend>
<div class="row mb-3">
{{-- PICTURE --}}
<div class="col-12">
@include('bo.partials.inputs.varchar-picture', [
'id' => 'productPicture',
'name' => 'picture',
'width' => 800,
'height' => 800,
'model' => $productModel ?? null,
'helper' => __('crud.minicrud.dimensions_must_be', [
'widthpx' => '800px',
'heightpx' => '800px',
]),
'required' => true,
'preview' => true,
])
</div>
</div>
</fieldset>
</div>
{{-- DESCRIPTION --}}
<div class="col-12 mb-3">
<fieldset class="border p-3">
<legend>{{ Str::ucfirst(__('validation.attributes.description')) }}</legend>
<div class="row mb-3">
<div class="col-12">
@include('bo.partials.inputs.text-html', [
'id' => 'productDescription',
'label' => __(':field :inter:model', [
'field' => Str::ucfirst(__('validation.attributes.description')),
'inter' => __('validation.custom.of_the.male'),
'model' => trans_choice('models.classes.product', 1),
]),
'name' => 'description',
'model' => $productModel ?? null,
'required' => true,
'helper' => __('crud.minicrud.text_warning'),
])
</div>
</div>
</fieldset>
</div>
</div>
<div class="row">
<div class="col text-center">
<p><b>{{ __('* Champs obligatoires') }}</b></p>
</div>
</div>
@push('scripts')
<script nonce="{{ $nonce }}">
document.addEventListener("DOMContentLoaded", () => {
// Submit button clone.
const submit = document.getElementById('formSubmit')
submitClone = document.getElementById('formSubmitClone');
submitClone.addEventListener('click', (event) => {
event.preventDefault();
submit.click('');
})
});
</script>
@endpush

View file

@ -0,0 +1,68 @@
<table class="table-hover table-fix-action m-0 table">
@if (count($productModelList) > 0)
<thead>
@include('bo.modules.table-col-sorter', [
'cols' => [
'name' => Str::ucfirst(__('validation.attributes.name')),
'range_id' => Str::ucfirst(trans_choice('models.classes.range', 1)),
'updated_at' => Str::ucfirst(__('validation.attributes.updated_at')),
],
])
</thead>
<tbody>
@foreach ($productModelList as $productModel)
<tr class="border-bottom list-item">
<td class="text-center align-middle">{{ $productModel->name }}</td>
<td class="text-center align-middle">
<a class="btn btn-link" data-bs-toggle="tooltip" href="{{ route('bo.ranges.edit', $productModel->range) }}"
title="{{ __('Voir la gamme') }}">
{{ Str::limit($productModel->range->name, 35) }}
</a>
</td>
<td class="text-center align-middle">
<span class="badge bg-secondary">{{ $productModel->updated_at->isoFormat('LLLL') }}</span>
</td>
<td class="text-end align-middle">
@canAny(['delete', 'duplicate', 'update'], $productModel)
<form class="btn-group confirmDeleteTS" action="{{ route('bo.products.destroy', $productModel) }}" method="POST"
novalidate>
@can('duplicate', $productModel)
<a class="btn btn-sm btn-secondary" data-bs-toggle="tooltip"
href="{{ route('bo.products.duplicate', ['product' => $productModel]) }}"
title="{{ Str::ucfirst(__('crud.actions.duplicate')) }}">
<i class="fa-solid fa-copy"></i>
</a>
@endcan
@can('update', $productModel)
<a class="btn btn-sm btn-primary" data-bs-toggle="tooltip"
href="{{ route('bo.products.edit', ['product' => $productModel]) }}"
title="{{ Str::ucfirst(__('crud.actions.update')) }}">
<i class="fa-solid fa-pencil"></i>
</a>
@endcan
@can('delete', $productModel)
@csrf
@method('DELETE')
<button class="btn btn-sm btn-danger" data-bs-toggle="tooltip" type="submit"
title="{{ Str::ucfirst(__('crud.actions.delete')) }}">
<i class="fa-solid fa-trash"></i>
</button>
@endcan
</form>
@else
<span class="text-danger" data-bs-toggle="tooltip" title="{{ __('list.right') }}">
<i class="fa-solid fa-ban"></i>
</span>
@endcan
</td>
</tr>
@endforeach
</tbody>
@else
<tr>
<td class="border-0">
{{ trans_choice('crud.other.no_model_found', 1, ['model' => trans_choice('models.classes.product', '*')]) }}
</td>
</tr>
@endif
</table>

View file

@ -0,0 +1,21 @@
@extends('bo.layout')
@section('title', Str::ucfirst(trans_choice('models.classes.product', '*')))
@section('description', __('Liste de tous les :model', ['model' => trans_choice('models.classes.product', '*')]))
@section('content')
<div class="d-flex justify-content-between flex-md-nowrap align-items-center flex-wrap">
@include('modules.breadcrumbs.breadcrumb-body')
@can('create', \App\Models\Product::class)
<a class="btn btn-primary float-right" data-bs-toggle="tooltip" href="{{ route('bo.products.create') }}"
title="{{ __('crud.helpers.create_model', ['model' => trans_choice('models.classes.product', 1)]) }}">
<i class="fa-solid fa-plus"></i>
</a>
@endcan
</div>
@include('bo.modules.search-bar')
<div class="table-responsive mb-3">
@include('bo.pages.products.index-list')
</div>
{{ $productModelList->links() }}
@endsection

View file

@ -0,0 +1,36 @@
@extends('bo.layout', ['brParam' => ''])
@section('title', trans_choice('crud.meta.title.create', 1, ['model' => trans_choice('models.classes.range', 1)]))
@section('description', trans_choice('crud.meta.description.create', 1, ['model' => trans_choice('models.classes.range', 1)]))
@section('breadcrumb',
request()->route()->getName())
@section('content')
<div class="d-flex justify-content-between flex-md-nowrap align-items-center flex-wrap pb-3">
<div class="d-flex align-items-start flex-row">
<a class="btn btn-primary text-decoration-none m-0" data-bs-toggle="tooltip" href="{{ route('bo.ranges.index') }}"
title="{{ __('crud.helpers.list_all', ['model' => trans_choice('models.classes.range', '*')]) }}">
<i class="fa-solid fa-arrow-left"></i>
</a>
@include('modules.breadcrumbs.breadcrumb-body', ['brParam' => ''])
</div>
<div class="mb-md-0 mb-2">
<button class="btn btn-primary" id="formSubmitClone" data-bs-toggle="tooltip" type="submit"
title="{{ __('crud.helpers.save_model', ['model' => trans_choice('models.classes.range', 1)]) }}">
{{ Str::ucfirst(__('crud.actions.save')) }}
</button>
</div>
</div>
<form action="{{ route('bo.ranges.store') }}" method="POST" enctype="multipart/form-data">
@csrf
@include('bo.pages.ranges.form')
<div class="row mt-3">
<div class="col text-center">
<button class="btn btn-primary" id="formSubmit" data-bs-toggle="tooltip" type="submit"
title="{{ __('crud.helpers.save_model', ['model' => trans_choice('models.classes.range', 1)]) }}">
{{ Str::ucfirst(__('crud.actions.save')) }}
</button>
</div>
</div>
</form>
@endsection

View file

@ -0,0 +1,52 @@
@extends('bo.layout', ['brParam' => $rangeModel])
@section('title', trans_choice('crud.meta.title.edit', 1, ['model' => trans_choice('models.classes.range', 1)]))
@section('description', trans_choice('crud.meta.description.edit', 1, ['model' => trans_choice('models.classe.ranges', 1)]))
@section('breadcrumb',
request()->route()->getName())
@section('content')
<div class="d-flex justify-content-between flex-md-nowrap align-items-center flex-wrap pb-3">
<div class="d-flex align-items-start flex-row">
<a class="btn btn-primary text-decoration-none m-0" data-bs-toggle="tooltip" href="{{ route('bo.ranges.index') }}"
title="{{ __('crud.helpers.list_all', ['model' => trans_choice('models.classes.range', '*')]) }}">
<i class="fa-solid fa-arrow-left"></i>
</a>
@include('modules.breadcrumbs.breadcrumb-body', ['brParam' => $rangeModel])
</div>
<div class="mb-md-0 mb-2">
<form class="btn-group confirmDeleteTS" action="{{ route('bo.ranges.destroy', $rangeModel) }}" method="POST" novalidate>
@can('duplicate', $rangeModel)
<a class="btn btn-secondary" data-bs-toggle="tooltip" href="{{ route('bo.ranges.duplicate', ['ranges' => $rangeModel]) }}"
title="{{ Str::ucfirst(__('crud.actions.duplicate')) }}">
<i class="fa-solid fa-copy"></i>
</a>
@endcan
<button class="btn btn-primary" id="formSubmitClone" data-bs-toggle="tooltip" type="submit"
title="{{ __('crud.helpers.save_model', ['model' => trans_choice('models.classes.range', 1)]) }}">
{{ Str::ucfirst(__('crud.actions.save')) }}
</button>
@can('delete', $rangeModel)
@csrf
@method('DELETE')
<button class="btn btn-danger" data-bs-toggle="tooltip" type="submit" title="{{ Str::ucfirst(__('crud.actions.delete')) }}">
<i class="fa-solid fa-trash"></i>
</button>
@endcan
</form>
</div>
</div>
<form action="{{ route('bo.ranges.update', $rangeModel) }}" method="POST" enctype="multipart/form-data">
@csrf
@method('PUT')
@include('bo.pages.ranges.form')
<div class="row">
<div class="col text-center">
<button class="btn btn-primary" id="formSubmit" data-bs-toggle="tooltip" type="submit"
title="{{ __('crud.helpers.save_model', ['model' => trans_choice('models.classes.range', 1)]) }}">
{{ Str::ucfirst(__('crud.actions.save')) }}
</button>
</div>
</div>
</form>
@endsection

View file

@ -0,0 +1,102 @@
<div class="row">
{{-- NAME --}}
<div class="col-12 col-md-6 mb-3">
<fieldset class="h-100 border p-3">
<legend>{{ Str::ucfirst(__('validation.attributes.name')) }}</legend>
<div class="row">
<div class="col-12">
@include('bo.partials.inputs.varchar', [
'id' => 'rangeText',
'label' => __(':field :inter:model', [
'field' => Str::ucfirst(__('validation.attributes.name')),
'inter' => __('validation.custom.of_the.female'),
'model' => trans_choice('models.classes.range', 1),
]),
'hasWordCounter' => true,
'name' => 'name',
'required' => true,
'model' => $rangeModel ?? null,
'helper' => __('validation.string', [
'attribute' => __(':field :inter:model', [
'field' => Str::ucfirst(__('validation.attributes.name')),
'inter' => __('validation.custom.of_the.female'),
'model' => trans_choice('models.classes.range', 1),
]),
]),
'placeholder' => Str::ucfirst(__('validation.attributes.name')),
])
</div>
</div>
</fieldset>
</div>
{{-- BRAND_ID --}}
<div class="col-12 col-md-6 mb-3">
<fieldset class="h-100 border p-3">
<legend>{{ Str::ucfirst(trans_choice('models.classes.brand', 1)) }}</legend>
<div class="row">
<div class="col-12">
@include('bo.partials.inputs.bigint-foreign-id', [
'id' => 'rangeBrand',
'label' => __(':field :inter:model', [
'field' => Str::ucfirst(trans_choice('models.classes.brand', 1)),
'inter' => __('validation.custom.of_the.female'),
'model' => trans_choice('models.classes.range', 1),
]),
'name' => 'brand_id',
'required' => true,
'model' => $rangeModel ?? null,
'associatedModels' => $brandModelList ?? null,
'foreignLabel' => 'name',
'helper' => __('crud.minicrud.enum_select', [
'enum' => trans_choice('models.classes.brand', 1),
]),
])
</div>
</div>
</fieldset>
</div>
{{-- PICTURE --}}
<div class="col-12 pb-3">
<fieldset class="h-100 border p-3">
<legend>{{ Str::ucfirst(__('validation.attributes.image')) }}&nbsp;*</legend>
<div class="row mb-3">
{{-- PICTURE --}}
<div class="col-12">
@include('bo.partials.inputs.varchar-picture', [
'id' => 'rangePicture',
'name' => 'picture',
'width' => 600,
'height' => 500,
'model' => $rangeModel ?? null,
'helper' => __('crud.minicrud.dimensions_must_be', [
'widthpx' => '600px',
'heightpx' => '500px',
]),
'required' => true,
'preview' => true,
])
</div>
</div>
</fieldset>
</div>
</div>
<div class="row">
<div class="col text-center">
<p><b>{{ __('* Champs obligatoires') }}</b></p>
</div>
</div>
@push('scripts')
<script nonce="{{ $nonce }}">
document.addEventListener("DOMContentLoaded", () => {
// Submit button clone.
const submit = document.getElementById('formSubmit')
submitClone = document.getElementById('formSubmitClone');
submitClone.addEventListener('click', (event) => {
event.preventDefault();
submit.click('');
})
});
</script>
@endpush

View file

@ -0,0 +1,68 @@
<table class="table-hover table-fix-action m-0 table">
@if (count($rangeModelList) > 0)
<thead>
@include('bo.modules.table-col-sorter', [
'cols' => [
'name' => Str::ucfirst(__('validation.attributes.name')),
'brand_id' => Str::ucfirst(trans_choice('models.classes.brand', 1)),
'updated_at' => Str::ucfirst(__('validation.attributes.updated_at')),
],
])
</thead>
<tbody>
@foreach ($rangeModelList as $rangeModel)
<tr class="border-bottom list-item">
<td class="text-center align-middle">{{ $rangeModel->name }}</td>
<td class="text-center align-middle">
<a class="btn btn-link" data-bs-toggle="tooltip" href="{{ route('bo.brands.edit', $rangeModel->brand) }}"
title="{{ __('Voir la marque') }}">
{{ Str::limit($rangeModel->brand->name, 35) }}
</a>
</td>
<td class="text-center align-middle">
<span class="badge bg-secondary">{{ $rangeModel->updated_at->isoFormat('LLLL') }}</span>
</td>
<td class="text-end align-middle">
@canAny(['delete', 'duplicate', 'update'], $rangeModel)
<form class="btn-group confirmDeleteTS" action="{{ route('bo.ranges.destroy', $rangeModel) }}" method="POST"
novalidate>
@can('duplicate', $rangeModel)
<a class="btn btn-sm btn-secondary" data-bs-toggle="tooltip"
href="{{ route('bo.ranges.duplicate', ['range' => $rangeModel]) }}"
title="{{ Str::ucfirst(__('crud.actions.duplicate')) }}">
<i class="fa-solid fa-copy"></i>
</a>
@endcan
@can('update', $rangeModel)
<a class="btn btn-sm btn-primary" data-bs-toggle="tooltip"
href="{{ route('bo.ranges.edit', ['range' => $rangeModel]) }}"
title="{{ Str::ucfirst(__('crud.actions.update')) }}">
<i class="fa-solid fa-pencil"></i>
</a>
@endcan
@can('delete', $rangeModel)
@csrf
@method('DELETE')
<button class="btn btn-sm btn-danger" data-bs-toggle="tooltip" type="submit"
title="{{ Str::ucfirst(__('crud.actions.delete')) }}">
<i class="fa-solid fa-trash"></i>
</button>
@endcan
</form>
@else
<span class="text-danger" data-bs-toggle="tooltip" title="{{ __('list.right') }}">
<i class="fa-solid fa-ban"></i>
</span>
@endcan
</td>
</tr>
@endforeach
</tbody>
@else
<tr>
<td class="border-0">
{{ trans_choice('crud.other.no_model_found', 1, ['model' => trans_choice('models.classes.range', '*')]) }}
</td>
</tr>
@endif
</table>

View file

@ -0,0 +1,21 @@
@extends('bo.layout')
@section('title', Str::ucfirst(trans_choice('models.classes.range', '*')))
@section('description', __('Liste de tous les :model', ['model' => trans_choice('models.classes.range', '*')]))
@section('content')
<div class="d-flex justify-content-between flex-md-nowrap align-items-center flex-wrap">
@include('modules.breadcrumbs.breadcrumb-body')
@can('create', \App\Models\Range::class)
<a class="btn btn-primary float-right" data-bs-toggle="tooltip" href="{{ route('bo.ranges.create') }}"
title="{{ __('crud.helpers.create_model', ['model' => trans_choice('models.classes.range', 1)]) }}">
<i class="fa-solid fa-plus"></i>
</a>
@endcan
</div>
@include('bo.modules.search-bar')
<div class="table-responsive mb-3">
@include('bo.pages.ranges.index-list')
</div>
{{ $rangeModelList->links() }}
@endsection

View file

@ -10,7 +10,7 @@
<input
id="{{ $id }}"
name="{{ $name }}"
type="text"
type="color"
data-jscolor=""
class="form-control"
aria-describedby="{{ $id }}Help"
@ -27,45 +27,3 @@
'helperId' => "{$id}Help",
'helper' => $helper
])
@push('scripts')
<script @if(isset($nonce) and is_string($nonce)) nonce="{{ $nonce }}" @endif>
window.addEventListener("DOMContentLoaded", (event) => {
// https://stackoverflow.com/a/35970186/4355295
let __invertColor = function(hex, bw) {
if (hex.indexOf('#') === 0) {
hex = hex.slice(1);
}
// convert 3-digit hex to 6-digits.
if (hex.length === 3) {
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
}
if (hex.length !== 6) {
throw new Error('Invalid HEX color.');
}
var r = parseInt(hex.slice(0, 2), 16),
g = parseInt(hex.slice(2, 4), 16),
b = parseInt(hex.slice(4, 6), 16);
if (bw) {
// https://stackoverflow.com/a/3943023/112731
return (r * 0.299 + g * 0.587 + b * 0.114) > 186
? '#000000'
: '#FFFFFF';
}
// invert color components
r = (255 - r).toString(16);
g = (255 - g).toString(16);
b = (255 - b).toString(16);
// pad each with zeros and return
return "#" + r.padStart(1, '0') + g.padStart(1, '0') + b.padStart(1, '0');
}
let colorSpans = document.getElementsByClassName('colorize');
if(colorSpans.length) {
for(let colorSpan of colorSpans) {
colorSpan.style.color = __invertColor(colorSpan.textContent, true);
colorSpan.style.backgroundColor = colorSpan.textContent;
}
}
});
</script>
@endpush

View file

@ -1,7 +1,9 @@
<?php
use App\Models\ActivityLog;
use App\Models\StaticPage;
use App\Models\Brand;
use App\Models\Product;
use App\Models\Range;
use App\Models\User;
use Diglactic\Breadcrumbs\Breadcrumbs;
use Diglactic\Breadcrumbs\Generator;
@ -39,14 +41,50 @@ Breadcrumbs::for('bo.activity_logs.show', function (Generator $trail, ActivityLo
$trail->push(Str::ucfirst(trans('crud.actions.view')), route('bo.activity_logs.show', $activity_log));
});
// * STATIC PAGES
Breadcrumbs::for('bo.static_pages.index', function (Generator $trail) {
// * BRANDS
Breadcrumbs::for('bo.brands.index', function (Generator $trail) {
$trail->push(
Str::of(trans_choice('models.classes.static_page', '*'))->ucfirst(),
route('bo.static_pages.index', ['sort_col' => 'updated_at', 'sort_way' => 'desc'])
Str::of(trans_choice('models.classes.brand', '*'))->ucfirst(),
route('bo.brands.index', ['sort_col' => 'updated_at', 'sort_way' => 'desc'])
);
});
Breadcrumbs::for('bo.static_pages.edit', function (Generator $trail, StaticPage $static_page) {
$trail->parent('bo.static_pages.index');
$trail->push(Str::ucfirst(trans('crud.actions.edit')), route('bo.static_pages.edit', $static_page));
Breadcrumbs::for('bo.brands.create', function (Generator $trail) {
$trail->parent('bo.brands.index');
$trail->push(Str::ucfirst(trans('crud.actions.create')), route('bo.brands.create'));
});
Breadcrumbs::for('bo.brands.edit', function (Generator $trail, Brand $brand) {
$trail->parent('bo.brands.index');
$trail->push(Str::ucfirst(trans('crud.actions.edit')), route('bo.brands.edit', $brand));
});
// * RANGES
Breadcrumbs::for('bo.ranges.index', function (Generator $trail) {
$trail->push(
Str::of(trans_choice('models.classes.range', '*'))->ucfirst(),
route('bo.ranges.index', ['sort_col' => 'updated_at', 'sort_way' => 'desc'])
);
});
Breadcrumbs::for('bo.ranges.create', function (Generator $trail) {
$trail->parent('bo.ranges.index');
$trail->push(Str::ucfirst(trans('crud.actions.create')), route('bo.ranges.create'));
});
Breadcrumbs::for('bo.ranges.edit', function (Generator $trail, Range $range) {
$trail->parent('bo.ranges.index');
$trail->push(Str::ucfirst(trans('crud.actions.edit')), route('bo.ranges.edit', $range));
});
// * PRODUCTS
Breadcrumbs::for('bo.products.index', function (Generator $trail) {
$trail->push(
Str::of(trans_choice('models.classes.product', '*'))->ucfirst(),
route('bo.products.index', ['sort_col' => 'updated_at', 'sort_way' => 'desc'])
);
});
Breadcrumbs::for('bo.products.create', function (Generator $trail) {
$trail->parent('bo.products.index');
$trail->push(Str::ucfirst(trans('crud.actions.create')), route('bo.products.create'));
});
Breadcrumbs::for('bo.products.edit', function (Generator $trail, Product $product) {
$trail->parent('bo.products.index');
$trail->push(Str::ucfirst(trans('crud.actions.edit')), route('bo.products.edit', $product));
});

View file

@ -1,7 +1,10 @@
<?php
use App\Http\Controllers\Bo\ActivityLogController;
use App\Http\Controllers\Bo\BrandController;
use App\Http\Controllers\Bo\DashboardController;
use App\Http\Controllers\Bo\ProductController;
use App\Http\Controllers\Bo\RangeController;
use App\Http\Controllers\Bo\UserController;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Auth;
@ -36,6 +39,17 @@ Route::prefix('bo')
UserController::class, 'duplicate'
])->name('users.duplicate');
// * BRANDS
Route::resource('brands', BrandController::class)->except('show');
// * RANGES
Route::resource('ranges', RangeController::class)->except('show');
// * PRODUCTS
Route::resource('products', ProductController::class)->except('show')
->withoutMiddleware('htmlsanitize')
->middleware('htmlsanitize:description');
Route::middleware('can:isConceptor')->group(function () {
// * ACTIVITY LOGS.
Route::resource('activity_logs', ActivityLogController::class)->only(['index', 'show']);