From 117c64c5b776b803772d04e55dfbc69eb32de623 Mon Sep 17 00:00:00 2001 From: System User Date: Wed, 25 Mar 2026 10:45:26 +0100 Subject: [PATCH] feat :sparkles: refactor ordering logic for models --- app/Http/Controllers/Bo/AdviceController.php | 3 + .../Bo/BrigadeSectionController.php | 5 +- app/Http/Controllers/Bo/ProductController.php | 3 + app/Http/Requests/Bo/UpdateOrderRequest.php | 72 +++++++++++++++++++ app/Models/Advice.php | 19 ++++- app/Models/BrigadeSection.php | 2 +- app/Models/Product.php | 9 ++- app/Traits/Controllers/ManagesOrder.php | 55 ++++++++++++++ app/Traits/Models/Orderable.php | 61 ++++++++++++++++ database/factories/SlideFactory.php | 3 +- ...2026_03_09_125845_reorder_all_products.php | 38 ++++++++++ ...3_09_142733_add_column_to_advice_table.php | 43 +++++++++++ .../Development/ExpertiseProductSeeder.php | 2 +- resources/lang/en/validation.php | 5 ++ resources/lang/fr/validation.php | 6 +- resources/ts/bo/utilities/fontawesome.ts | 3 +- .../change-model-order-select.blade.php | 21 ++++++ .../views/bo/pages/advice/index.blade.php | 12 ++-- .../views/bo/pages/products/index.blade.php | 8 ++- routes/web-bo.php | 13 +++- 20 files changed, 367 insertions(+), 16 deletions(-) create mode 100644 app/Http/Requests/Bo/UpdateOrderRequest.php create mode 100644 app/Traits/Controllers/ManagesOrder.php create mode 100644 app/Traits/Models/Orderable.php create mode 100644 database/migrations/2026_03_09_125845_reorder_all_products.php create mode 100644 database/migrations/2026_03_09_142733_add_column_to_advice_table.php create mode 100644 resources/views/bo/modules/change-model-order-select.blade.php diff --git a/app/Http/Controllers/Bo/AdviceController.php b/app/Http/Controllers/Bo/AdviceController.php index b11eb00..2c0028d 100644 --- a/app/Http/Controllers/Bo/AdviceController.php +++ b/app/Http/Controllers/Bo/AdviceController.php @@ -7,9 +7,11 @@ use App\Enums\TagTypeEnum; use App\Http\Controllers\Controller; use App\Http\Requests\Bo\Advice\StoreAdviceRequest; use App\Http\Requests\Bo\Advice\UpdateAdviceRequest; +use App\Http\Requests\Bo\UpdateOrderRequest; use App\Models\Advice; use App\Models\Product; use App\Models\Tag; +use App\Traits\Controllers\ManagesOrder; use App\Traits\Controllers\UpdateModelPublished; use Illuminate\Contracts\Database\Eloquent\Builder; use Illuminate\Http\Request; @@ -20,6 +22,7 @@ use Illuminate\Support\Str; class AdviceController extends Controller { use UpdateModelPublished; + use ManagesOrder; /** * Display a listing of the resource. diff --git a/app/Http/Controllers/Bo/BrigadeSectionController.php b/app/Http/Controllers/Bo/BrigadeSectionController.php index 76ebd2c..d689d31 100644 --- a/app/Http/Controllers/Bo/BrigadeSectionController.php +++ b/app/Http/Controllers/Bo/BrigadeSectionController.php @@ -7,6 +7,7 @@ use App\Http\Requests\Bo\BrigadeSections\UpdateBrigadeSectionsRequest; use App\Models\BrigadeSection; use App\Traits\Controllers\ChangesModelOrder; use App\Traits\Controllers\UpdateModelPublished; +use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; use Uwil\Laravel\Common\Controllers\Controller; @@ -69,7 +70,7 @@ class BrigadeSectionController extends Controller /** * Store a newly created resource in storage. * - * @param \App\Http\Requests\Bo\BrigadeSections\StoreBrigadeSectionRequest $request + * @param \App\Http\Requests\Bo\BrigadeSections\StoreBrigadeSectionsRequest $request * @return \Illuminate\Http\RedirectResponse */ public function store(StoreBrigadeSectionsRequest $request): \Illuminate\Http\RedirectResponse @@ -111,7 +112,7 @@ class BrigadeSectionController extends Controller * @param \App\Models\BrigadeSection $brigadeSection * @return \Illuminate\Http\RedirectResponse */ - public function update(UpdateBrigadeSectionsRequest $request, BrigadeSection $brigadeSection): \Illuminate\Http\RedirectResponse + public function update(UpdateBrigadeSectionsRequest $request, BrigadeSection $brigadeSection): RedirectResponse { return DB::transaction(function () use ($request, $brigadeSection) { $brigadeSection->fill($request->validated()); diff --git a/app/Http/Controllers/Bo/ProductController.php b/app/Http/Controllers/Bo/ProductController.php index 297ced9..5fa3fe5 100644 --- a/app/Http/Controllers/Bo/ProductController.php +++ b/app/Http/Controllers/Bo/ProductController.php @@ -6,6 +6,7 @@ use App\Enums\TagTypeEnum; use App\Http\Controllers\Controller; use App\Http\Requests\Bo\Products\StoreProductRequest; use App\Http\Requests\Bo\Products\UpdateProductRequest; +use App\Http\Requests\Bo\UpdateOrderRequest; use App\Lib\Helpers\FileStorageHelper; use App\Models\Product; use App\Models\Range; @@ -13,6 +14,7 @@ use App\Models\Subrange; use App\Models\Tag; use App\Traits\Controllers\UpdateModelPublished; use App\Traits\Controllers\ChangesModelOrder; +use App\Traits\Controllers\ManagesOrder; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; use Illuminate\Support\Str; @@ -21,6 +23,7 @@ class ProductController extends Controller { use ChangesModelOrder; use UpdateModelPublished; + use ManagesOrder; /** * Display a listing of the resource. diff --git a/app/Http/Requests/Bo/UpdateOrderRequest.php b/app/Http/Requests/Bo/UpdateOrderRequest.php new file mode 100644 index 0000000..dd17030 --- /dev/null +++ b/app/Http/Requests/Bo/UpdateOrderRequest.php @@ -0,0 +1,72 @@ +check(); + } + + + /** + * Prepare the data for validation. + * + * @return void + */ + protected function prepareForValidation(): void + { + $this->orderable = collect($this->route()->parameters()) + ->first(fn($param) => $param instanceof Model); + + $groupField = $this->orderable->getOrderGroupField(); + + if ($groupField) { + $this->orderable::query()->where($groupField, $this->orderable->{$groupField}); + } + } + + /** + * Get the validation rules that apply to the request. + * + * @return array + */ + public function rules(): array + { + return [ + 'priority_order' => [ + 'required', + 'integer', + // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundBeforeLastUsed + function ($attribute, $value, $fail) { + if ($this->orderable && intval($value) < 1) { + $fail(trans('validation.custom.min_priority')); + } + }, + sprintf('max:%s', $this->orderable->max('priority_order')), + // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundBeforeLastUsed + function ($attribute, $value, $fail) { + if ($this->orderable && intval($value) === $this->orderable->priority_order) { + $fail(trans('validation.custom.same_priority')); + } + }, + ], + ]; + } +} diff --git a/app/Models/Advice.php b/app/Models/Advice.php index 07fbdbe..d83f1f3 100644 --- a/app/Models/Advice.php +++ b/app/Models/Advice.php @@ -6,6 +6,7 @@ use App\Enums\Advices\AdviceTypeEnum; use App\Enums\Advices\AdviceDifficultyEnum; use App\Lib\Helpers\FileStorageHelper; use App\Lib\Helpers\ToolboxHelper; +use App\Traits\Models\Orderable; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Foundation\Auth\User as Authenticatable; @@ -46,6 +47,7 @@ use Uwil\Laravel\Redirections\Traits\IsRedirectable; * @property integer|null $preparation_time Time of preparation. * @property \App\Enums\Advices\AdviceDifficultyEnum|null $difficulty Difficulty. * @property boolean $published Status. + * @property integer $priority_order Priority Order. * @property-read \Illuminate\Support\Carbon $created_at Created date. * @property-read \Illuminate\Support\Carbon $updated_at Updated date. * @@ -65,6 +67,7 @@ class Advice extends Authenticatable implements SeoInterface, Sitemapable, Redir use HasFactory; use SeoTrait; use IsRedirectable; + use Orderable; /** * The attributes that are mass assignable. @@ -95,7 +98,8 @@ class Advice extends Authenticatable implements SeoInterface, Sitemapable, Redir 'cooking_time', 'preparation_time', 'difficulty', - 'published' + 'published', + 'priority_order' ]; /** @@ -119,6 +123,7 @@ class Advice extends Authenticatable implements SeoInterface, Sitemapable, Redir { static::creating(function (self $model) { self::manageFields($model); + self::bootOrderable($model); }); static::updating(function (self $model) { self::manageFields($model); @@ -133,6 +138,7 @@ class Advice extends Authenticatable implements SeoInterface, Sitemapable, Redir FileStorageHelper::removeFile($model, 'picture'); FileStorageHelper::removeFile($model, 'sticker'); FileStorageHelper::removeFile($model, 'sticker_sm'); + self::setOrder($model); }); } @@ -375,4 +381,15 @@ class Advice extends Authenticatable implements SeoInterface, Sitemapable, Redir $params = ['publishedAdvice' => $model]; return SitemapHelper::generateUrl($route, $params, $model); } + + + /** + * Group ordering by type. + * + * @return string + */ + public function getOrderGroupField(): string + { + return 'type'; + } } diff --git a/app/Models/BrigadeSection.php b/app/Models/BrigadeSection.php index c893361..fe447eb 100644 --- a/app/Models/BrigadeSection.php +++ b/app/Models/BrigadeSection.php @@ -106,7 +106,7 @@ class BrigadeSection extends Model * @param self $model * @param boolean $isDeletion * @return void - * @throws \Illuminate\Validation\ValidationException + * @throws \Illuminate\Validation\ValidationException Returns if fails. */ private static function assertLimitPublishedSections(self $model, bool $isDeletion = false) { diff --git a/app/Models/Product.php b/app/Models/Product.php index db3ee44..4dfa670 100644 --- a/app/Models/Product.php +++ b/app/Models/Product.php @@ -13,6 +13,7 @@ use App\Lib\Helpers\FileStorageHelper; use Illuminate\Database\Eloquent\Model; use Spatie\Sitemap\Contracts\Sitemapable; use App\Enums\Products\ProductSpeciesEnum; +use App\Traits\Models\Orderable; use Uwil\SitemapGenerator\Lib\SitemapHelper; use Uwil\Seo\Laravel\Interfaces\SeoInterface; use Uwil\Laravel\Redirections\Traits\IsRedirectable; @@ -42,6 +43,7 @@ use Uwil\Laravel\Redirections\Interfaces\RedirectableInterface; * @property boolean $is_egalim Is egalim or not. * @property boolean $published Status. * @property integer $order Order. + * @property integer $priority_order Priority Order. * @property-read \Illuminate\Support\Carbon $created_at Created date. * @property-read \Illuminate\Support\Carbon $updated_at Updated date. * @@ -56,6 +58,7 @@ class Product extends Model implements SeoInterface, Sitemapable, RedirectableIn use HasFactory; use SeoTrait; use IsRedirectable; + use Orderable; /** * The attributes that are mass assignable. @@ -83,7 +86,8 @@ class Product extends Model implements SeoInterface, Sitemapable, RedirectableIn 'is_vip', 'is_egalim', 'order', - 'published' + 'published', + 'priority_order', ]; /** @@ -113,7 +117,7 @@ class Product extends Model implements SeoInterface, Sitemapable, RedirectableIn // * EVENT CLOSURES. static::creating(function (self $model) { self::manageFields($model); - $model->order = \intval(self::query()->max('order')) + 1; + self::bootOrderable($model); }); static::updating(function (self $model) { self::manageFields($model); @@ -131,6 +135,7 @@ class Product extends Model implements SeoInterface, Sitemapable, RedirectableIn $model, !is_array($model->getOriginal('slider_pictures')) ? [] : $model->getOriginal('slider_pictures'), ); + self::setOrder($model); }); } diff --git a/app/Traits/Controllers/ManagesOrder.php b/app/Traits/Controllers/ManagesOrder.php new file mode 100644 index 0000000..b79b9bc --- /dev/null +++ b/app/Traits/Controllers/ManagesOrder.php @@ -0,0 +1,55 @@ +validated('priority_order'); + + return DB::transaction(function () use ($model, $newPriority): RedirectResponse { + + $query = $model::query() + ->whereKeyNot($model) + ->whereBetween('priority_order', [ + min($model->priority_order, $newPriority), + max($model->priority_order, $newPriority), + ]); + + $groupField = $model->getOrderGroupField(); + if ($groupField) { + $query->where($groupField, $model->{$groupField}); + } + + $model->priority_order < $newPriority + ? $query->decrement('priority_order', 1) + : $query->increment('priority_order', 1); + + $model->priority_order = $newPriority; + $model->saveOrFail(); + + return redirect()->back()->with('success', trans('crud.messages.order_changed')); + }); + + } +} diff --git a/app/Traits/Models/Orderable.php b/app/Traits/Models/Orderable.php new file mode 100644 index 0000000..7c8b3e1 --- /dev/null +++ b/app/Traits/Models/Orderable.php @@ -0,0 +1,61 @@ +getOrderGroupField(); + $query = $model::query()->where('priority_order', '>', $model->priority_order); + + if ($groupField) { + $query->where($groupField, $model->{$groupField}); + } + + $query->each(function (Model $item): void { + $item->priority_order -= 1; + $item->saveOrFail(); + }); + } + + /** + * Assigns the next order value on creation. + * + * @return void + */ + public static function bootOrderable(): void + { + static::creating(function (Model $model): void { + if (!$model->priority_order) { + $groupField = $model->getOrderGroupField(); + $query = static::query(); + + if ($groupField) { + $query->where($groupField, $model->{$groupField}); + } + + $model->priority_order = ($query->max('priority_order') ?? 0) + 1; + } + }); + } +} diff --git a/database/factories/SlideFactory.php b/database/factories/SlideFactory.php index 6101968..426f6e3 100644 --- a/database/factories/SlideFactory.php +++ b/database/factories/SlideFactory.php @@ -44,7 +44,8 @@ class SlideFactory extends Factory 'picture_alt' => $picture_alt, 'picture_title' => $picture_title, 'picture_thumbnail_alt' => $picture_alt, - 'picture_thumbnail_title' => $picture_title + 'picture_thumbnail_title' => $picture_title, + 'published' => $this->faker->boolean(), ]; } } diff --git a/database/migrations/2026_03_09_125845_reorder_all_products.php b/database/migrations/2026_03_09_125845_reorder_all_products.php new file mode 100644 index 0000000..e3ec421 --- /dev/null +++ b/database/migrations/2026_03_09_125845_reorder_all_products.php @@ -0,0 +1,38 @@ +unsignedInteger('priority_order')->comment('Ordre de priorite')->default(0)->after('order'); + }); + + $order = 1; + + Product::query() + ->orderBy('order') + ->each(function (Product $product) use (&$order) { + $product->priority_order = $order++; + $product->saveOrFail(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('products', function (Blueprint $table) { + $table->dropColumn('priority_order'); + }); + } +}; diff --git a/database/migrations/2026_03_09_142733_add_column_to_advice_table.php b/database/migrations/2026_03_09_142733_add_column_to_advice_table.php new file mode 100644 index 0000000..ad9813d --- /dev/null +++ b/database/migrations/2026_03_09_142733_add_column_to_advice_table.php @@ -0,0 +1,43 @@ +unsignedInteger('priority_order')->comment('Ordre de priorite')->default(0)->after('published'); + }); + + // loop through all types and in each type we order + Advice::query()->select('type')->pluck('type') + ->each(function (AdviceTypeEnum $type) { + $order = 1; + Advice::query() + ->where('type', $type) + ->orderBy('created_at') + ->each(function (Advice $advice) use (&$order) { + $advice->priority_order = $order++; + $advice->saveOrFail(); + }); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('advice', function (Blueprint $table) { + $table->dropColumn('priority_order'); + }); + } +}; diff --git a/database/seeders/Development/ExpertiseProductSeeder.php b/database/seeders/Development/ExpertiseProductSeeder.php index bbb457c..03f5817 100644 --- a/database/seeders/Development/ExpertiseProductSeeder.php +++ b/database/seeders/Development/ExpertiseProductSeeder.php @@ -21,7 +21,7 @@ class ExpertiseProductSeeder extends Seeder ExpertiseProduct::factory(rand(0, 3))->make()->each( // @phpstan-ignore-next-line function (ExpertiseProduct $expertiseProduct) use ($enum) { - $expertiseProduct->page = $enum; + $expertiseProduct->page = ExpertiseProductPageEnum::make($enum->value); $expertiseProduct->saveQuietly(); } ); diff --git a/resources/lang/en/validation.php b/resources/lang/en/validation.php index 447cc9a..c1b219d 100644 --- a/resources/lang/en/validation.php +++ b/resources/lang/en/validation.php @@ -141,5 +141,10 @@ return [ 'seo_title' => 'SEO title', 'seo_description' => 'SEO description', 'type' => 'type', + 'new_priority' => 'New priority', + 'priority' => 'priority', + 'same_priority' => 'the priority value must be different from the current value.', + 'min_priority' => 'the priority value must be greater than 0.' + ], ]; diff --git a/resources/lang/fr/validation.php b/resources/lang/fr/validation.php index 3464cd3..1620511 100644 --- a/resources/lang/fr/validation.php +++ b/resources/lang/fr/validation.php @@ -151,7 +151,11 @@ return [ 'seo_title' => 'titre SEO', 'seo_description' => 'description SEO', 'type' => 'type', - 'url' => 'Le champ :field doit être une URL valide.' + 'url' => 'Le champ :field doit être une URL valide.', + 'new_priority' => 'Nouvelle priorité', + 'priority' => 'priorité', + 'same_priority' => 'La valeur de priorité doit être différente de la valeur actuelle.', + 'min_priority' => 'La valeur de priorité doit être supérieure à 0.' ], 'attributes' => [ 'advantages' => 'Avantages', diff --git a/resources/ts/bo/utilities/fontawesome.ts b/resources/ts/bo/utilities/fontawesome.ts index 50b69d7..00ccaa1 100644 --- a/resources/ts/bo/utilities/fontawesome.ts +++ b/resources/ts/bo/utilities/fontawesome.ts @@ -37,6 +37,7 @@ import { faTrash, faUndoAlt, faWandMagic, + faChevronDown } from "@fortawesome/free-solid-svg-icons"; config.autoReplaceSvg = false; @@ -72,7 +73,7 @@ library.add(faListUl); library.add(faCircleCheck); library.add(faCircleExclamation); library.add(faCircleInfo); - +library.add(faChevronDown); // Front library.add(faChevronLeft); library.add(faChevronRight); diff --git a/resources/views/bo/modules/change-model-order-select.blade.php b/resources/views/bo/modules/change-model-order-select.blade.php new file mode 100644 index 0000000..1858ab0 --- /dev/null +++ b/resources/views/bo/modules/change-model-order-select.blade.php @@ -0,0 +1,21 @@ + +
+ {{ $value ?? '' }} + +
+ \ No newline at end of file diff --git a/resources/views/bo/pages/advice/index.blade.php b/resources/views/bo/pages/advice/index.blade.php index f9d20a1..7c0f584 100644 --- a/resources/views/bo/pages/advice/index.blade.php +++ b/resources/views/bo/pages/advice/index.blade.php @@ -22,6 +22,7 @@ @include('bo.modules.table-col-sorter', [ 'cols' => [ + 'priority_order' => Str::ucfirst(__('validation.custom.priority')), 'title' => __('validation.attributes.title'), 'picture' => __('validation.attributes.picture'), 'published' => __('validation.attributes.published'), @@ -32,6 +33,11 @@ @foreach ($adviceModels as $adviceModel) + @include('bo.modules.change-model-order-select', [ + 'model' => $adviceModel, + 'value' => $adviceModel->priority_order, + 'route' => route('bo.advice.change-priority', $adviceModel) + ]) {{ $adviceModel->title }} @if($adviceModel->picture) @@ -46,10 +52,8 @@ 'model' => $adviceModel, ]) {{ $adviceModel->updated_at }} - @php - $routeName = request()->route()->getName(); - @endphp - @if (empty(request()->search) && Session::get("$routeName.sort_col") === 'order') + @if (empty(request()->search) && request()->range) + @php $routeName = request()->route()->getName(); @endphp @include('bo.modules.change-model-order', [ 'routeName' => 'advice', 'models' => $adviceModels, diff --git a/resources/views/bo/pages/products/index.blade.php b/resources/views/bo/pages/products/index.blade.php index 963329b..013cc6b 100644 --- a/resources/views/bo/pages/products/index.blade.php +++ b/resources/views/bo/pages/products/index.blade.php @@ -33,6 +33,7 @@ $cols = []; $routeName = request()->route()->getName(); $cols = array_merge($cols, [ + 'priority_order' => Str::ucfirst(__('validation.custom.priority')), 'label' => Str::ucfirst(__('validation.attributes.label')), 'species' => Str::ucfirst(__('validation.attributes.species')), 'picture' => Str::ucfirst(__('validation.attributes.picture')), @@ -46,12 +47,17 @@ } @endphp @include('bo.modules.table-col-sorter', [ - 'cols' => $cols, + 'cols' => $cols, ]) @foreach ($productModels as $productModel) + @include('bo.modules.change-model-order-select', [ + 'model' => $productModel, + 'value' => $productModel->priority_order, + 'route' => route('bo.products.change-priority', $productModel) + ]) {{ $productModel->label }} {{ $productModel->species->label() }} diff --git a/routes/web-bo.php b/routes/web-bo.php index 6e20b80..c213150 100644 --- a/routes/web-bo.php +++ b/routes/web-bo.php @@ -61,6 +61,12 @@ Route::prefix('bo') ])->where('direction', 'up|down')->name('products.change-order'); Route::post('/products/{product}/change-published', [ProductController::class, 'changePublished']) ->name('products.change-published'); + Route::patch('/products/{product}/change-priority', [ProductController::class, 'changeOrderPriority']) + ->name('products.change-priority'); + Route::patch('/products/{product}/change-order/{direction}', [ + BrigadeSectionController::class, + 'changeOrder' + ])->where('direction', 'up|down')->name('products.change-order'); // * NEWS Route::resource('news', NewsController::class)->except(['show']) @@ -83,6 +89,8 @@ Route::prefix('bo') Route::get('advice/{type}/create', [AdviceController::class, 'create'])->name('advice.create'); Route::post('/advice/{advice}/change-published', [AdviceController::class, 'changePublished']) ->name('advice.change-published'); + Route::patch('/advice/{advice}/change-priority', [AdviceController::class, 'changeOrderPriority']) + ->name('advice.change-priority'); // * EXPERTISE PRODUITS VIP Route::resource('vips', ExpertiseProductController::class)->except(['show']); @@ -95,7 +103,10 @@ Route::prefix('bo') Route::resource('brigade_sections', BrigadeSectionController::class)->except(['show']) ->withoutMiddleware('htmlsanitize') ->middleware('htmlsanitize:content'); - Route::post('/brigade_sections/{brigade_section}/change-published', [BrigadeSectionController::class, 'changePublished']) + Route::post( + '/brigade_sections/{brigade_section}/change-published', + [BrigadeSectionController::class, 'changePublished'] + ) ->name('brigade_sections.change-published'); Route::patch('/brigade_sections/{brigade_section}/change-order/{direction}', [ BrigadeSectionController::class,