From 3923f2a89d14d52ff2126e155a08ac01dba82de3 Mon Sep 17 00:00:00 2001 From: "nicolas.demangeat" Date: Fri, 24 Nov 2023 15:09:54 +0000 Subject: [PATCH] Add BO Coupon page --- app/Console/Kernel.php | 2 + app/Enums/CampaignTypes.php | 2 +- app/Http/Controllers/Bo/BrandController.php | 1 - app/Http/Controllers/Bo/CouponController.php | 156 +++++++ .../Bo/Coupons/StoreCouponRequest.php | 67 +++ .../Bo/Coupons/UpdateCouponRequest.php | 7 + app/Jobs/CouponCanBeUsedUpdate.php | 24 ++ app/Models/Basket.php | 2 +- app/Models/BasketsContent.php | 4 +- app/Models/Coupon.php | 160 ++++++- app/Providers/RouteServiceProvider.php | 2 + database/factories/CouponFactory.php | 60 +++ ...2023_08_20_090058_create_coupons_table.php | 2 +- ...0818_add_foreign_keys_to_coupons_table.php | 29 ++ database/seeders/CouponSeeder.php | 34 ++ database/seeders/DatabaseSeeder.php | 3 +- resources/assets/images/coupon/decoration.jpg | Bin 0 -> 1863 bytes resources/assets/images/coupon/gs1.jpg | Bin 0 -> 5808 bytes .../assets/images/coupon/website_picture.jpg | Bin 0 -> 4414 bytes .../images/coupon/website_picture_mobile.jpg | Bin 0 -> 5636 bytes resources/lang/en/models.php | 3 +- resources/lang/en/validation.php | 35 +- resources/lang/fr/models.php | 3 +- resources/lang/fr/validation.php | 35 +- resources/views/bo/layouts/nav-menu.blade.php | 44 +- .../views/bo/pages/coupons/create.blade.php | 41 ++ .../views/bo/pages/coupons/edit.blade.php | 43 ++ .../bo/pages/coupons/form-inputs.blade.php | 395 ++++++++++++++++++ .../bo/pages/coupons/index-list.blade.php | 87 ++++ .../views/bo/pages/coupons/index.blade.php | 48 +++ .../partials/inputs/timestamp-date.blade.php | 19 + routes/web-bo.php | 10 +- 32 files changed, 1251 insertions(+), 67 deletions(-) create mode 100644 app/Http/Controllers/Bo/CouponController.php create mode 100644 app/Http/Requests/Bo/Coupons/StoreCouponRequest.php create mode 100644 app/Http/Requests/Bo/Coupons/UpdateCouponRequest.php create mode 100644 app/Jobs/CouponCanBeUsedUpdate.php create mode 100644 database/factories/CouponFactory.php create mode 100644 database/migrations/2023_09_18_110818_add_foreign_keys_to_coupons_table.php create mode 100644 database/seeders/CouponSeeder.php create mode 100644 resources/assets/images/coupon/decoration.jpg create mode 100644 resources/assets/images/coupon/gs1.jpg create mode 100644 resources/assets/images/coupon/website_picture.jpg create mode 100644 resources/assets/images/coupon/website_picture_mobile.jpg create mode 100644 resources/views/bo/pages/coupons/create.blade.php create mode 100644 resources/views/bo/pages/coupons/edit.blade.php create mode 100644 resources/views/bo/pages/coupons/form-inputs.blade.php create mode 100644 resources/views/bo/pages/coupons/index-list.blade.php create mode 100644 resources/views/bo/pages/coupons/index.blade.php create mode 100644 resources/views/bo/partials/inputs/timestamp-date.blade.php diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index be055ac..363401a 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -2,6 +2,7 @@ namespace App\Console; +use App\Jobs\CouponCanBeUsedUpdate; use Illuminate\Console\Scheduling\Schedule; use Illuminate\Foundation\Console\Kernel as ConsoleKernel; @@ -15,6 +16,7 @@ class Kernel extends ConsoleKernel */ protected function schedule(Schedule $schedule) { + $schedule->job(CouponCanBeUsedUpdate::class)->everyMinute(); } /** diff --git a/app/Enums/CampaignTypes.php b/app/Enums/CampaignTypes.php index e4da7dd..0238b68 100644 --- a/app/Enums/CampaignTypes.php +++ b/app/Enums/CampaignTypes.php @@ -18,4 +18,4 @@ enum CampaignTypes: int self::WALL->name => "Mur", self::BASKET->name => "Panier", ]; -} \ No newline at end of file +} diff --git a/app/Http/Controllers/Bo/BrandController.php b/app/Http/Controllers/Bo/BrandController.php index 8568d21..98cf714 100644 --- a/app/Http/Controllers/Bo/BrandController.php +++ b/app/Http/Controllers/Bo/BrandController.php @@ -10,7 +10,6 @@ use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; use Illuminate\Support\Str; - class BrandController extends Controller { /** diff --git a/app/Http/Controllers/Bo/CouponController.php b/app/Http/Controllers/Bo/CouponController.php new file mode 100644 index 0000000..4ca0cef --- /dev/null +++ b/app/Http/Controllers/Bo/CouponController.php @@ -0,0 +1,156 @@ +search) { + $this->searchQuery( + $coupon, + $request->search, + null, + 'name', + ); + } + + $this->sortQuery($coupon); + $searchFields = trans('Name'); + + $couponModels = $coupon->paginate(10); + + return view('bo.pages.coupons.index', compact('couponModels', 'searchFields')); + } + + /** + * Show the form for creating a new resource. + * + * @return \Illuminate\Http\Response + */ + public function create() + { + $brandModels = Brand::orderBy('name', 'ASC')->get(); + + return view('bo.pages.coupons.create', compact('brandModels')); + } + + + /** + * Store a newly created resource in storage. + * + * @param \App\Http\Requests\Bo\Coupons\StoreCouponRequest $request + * @return \Illuminate\Http\Response + */ + public function store(StoreCouponRequest $request) + { + return DB::transaction(function () use ($request) { + $coupon = new Coupon(); + $coupon->fill($request->validated()); + + if ($coupon->saveOrFail()) { + return redirect() + ->route('bo.coupons.edit', $coupon) + ->with('success', trans( + 'crud.messages.has_been_created', + ['model' => Str::singular(trans('models.classes.coupons'))] + )); + } + return redirect() + ->back() + ->with('error', trans( + 'crud.messages.cannot_be_created', + ['model' => Str::singular(trans('models.classes.coupons'))] + )); + }); + } + + /** + * Display a listing of the resource. + * + * @param \App\Models\Coupon $coupon + * @return \Illuminate\Contracts\View\View|\Illuminate\Contracts\View\Factory + */ + public function edit(Coupon $coupon) + { + $brandModels = Brand::orderBy('name', 'ASC')->get(); + + return view('bo.pages.coupons.edit', [ + 'couponModel' => $coupon, + 'brandModels' => $brandModels + ]); + } + + /** + * Update the specified resource in storage. + * + * @param \App\Http\Requests\Bo\Coupons\UpdateCouponRequest $request + * @param \App\Models\Coupon $coupon + * @return \Illuminate\Http\Response + */ + public function update(UpdateCouponRequest $request, Coupon $coupon) + { + return DB::transaction(function () use ($request, $coupon) { + $coupon->fill($request->validated()); + if ($coupon->saveOrFail()) { + return redirect() + ->route('bo.coupons.edit', $coupon) + ->with('success', trans( + 'crud.messages.has_been_updated', + ['model' => Str::singular(trans('models.classes.coupons'))] + )); + } //end if + return redirect() + ->back() + ->with('error', trans( + 'crud.messages.cannot_be_updated', + ['model' => Str::singular(trans('models.classes.coupons'))] + )); + }); + } + + /** + * Remove the specified resource from storage. + * + * @param \App\Models\Coupon $coupon + * @return \Illuminate\Http\Response + */ + public function destroy(Coupon $coupon) + { + if ($coupon->deleteOrFail()) { + return redirect() + ->route('bo.coupons.index') + ->with('success', trans( + 'crud.messages.has_been_deleted', + ['model' => Str::singular(trans('models.classes.coupons'))] + )); + } + return redirect() + ->back() + ->with('error', trans( + ':crud.messages.cannot_be_deleted', + ['model' => Str::singular(trans('models.classes.coupons'))] + )); + } +} diff --git a/app/Http/Requests/Bo/Coupons/StoreCouponRequest.php b/app/Http/Requests/Bo/Coupons/StoreCouponRequest.php new file mode 100644 index 0000000..5953c4e --- /dev/null +++ b/app/Http/Requests/Bo/Coupons/StoreCouponRequest.php @@ -0,0 +1,67 @@ +check(); + } + + /** + * Prepare the data for validation. + * + * @return void + */ + protected function prepareForValidation() + { + $this->mergePicture('decoration'); + $this->mergePicture('gs1'); + $this->mergePicture('website_picture'); + $this->mergePicture('website_picture_mobile'); + $this->merge([ + 'status' => $this->status ? true : false + ]); + } + + /** + * Get the validation rules that apply to the request. + * + * @return array + */ + public function rules() + { + $rules = [ + 'name' => 'required|string|min:3|max:255', + 'status' => 'required|boolean', + 'brand_id' => 'required|exists:brands,id', + 'process_code' => 'required|string|min:3|max:255', + 'amount' => 'required|numeric|min:0.1|max:3', + 'valid_on' => 'required|string', + 'validity_period' => 'required|numeric|min:1|max:31', + 'legal_notices' => 'required|string', + 'limit_qty' => 'required|numeric|digits_between:1,3', + 'limit_period' => 'required|numeric|digits_between:1,3', + 'start_diffusion_date' => 'nullable|date|before:end_diffusion_date', + 'end_diffusion_date' => 'nullable|date|after:start_diffusion_date' + ]; + return \array_merge( + $rules, + $this->pictureRules('decoration', 280, 160, 280, 160, false), + $this->pictureRules('gs1', 628, 360, 628, 360, true), + $this->pictureRules('website_picture', 580, 190, 580, 1900, true), + $this->pictureRules('website_picture_mobile', 570, 560, 570, 560, true), + ); + } +} diff --git a/app/Http/Requests/Bo/Coupons/UpdateCouponRequest.php b/app/Http/Requests/Bo/Coupons/UpdateCouponRequest.php new file mode 100644 index 0000000..e071bb3 --- /dev/null +++ b/app/Http/Requests/Bo/Coupons/UpdateCouponRequest.php @@ -0,0 +1,7 @@ +can_be_used = $coupon->checkIfCanBeUsed(); + $coupon->save(); + } + } +} diff --git a/app/Models/Basket.php b/app/Models/Basket.php index 50ec75a..b67aab2 100644 --- a/app/Models/Basket.php +++ b/app/Models/Basket.php @@ -17,7 +17,7 @@ class Basket extends Model 'campaign_id' ]; - public function content() : HasMany + public function content(): HasMany { return $this->hasMany(BasketsContent::class); } diff --git a/app/Models/BasketsContent.php b/app/Models/BasketsContent.php index 400ff5c..68b3518 100644 --- a/app/Models/BasketsContent.php +++ b/app/Models/BasketsContent.php @@ -10,12 +10,12 @@ class BasketsContent extends Model { use HasFactory; - public function basket() : BelongsTo + public function basket(): BelongsTo { return $this->belongsTo(Basket::class); } - public function coupon() : BelongsTo + public function coupon(): BelongsTo { return $this->BelongsTo(Coupon::class); } diff --git a/app/Models/Coupon.php b/app/Models/Coupon.php index 7daaa0e..05cd8b7 100644 --- a/app/Models/Coupon.php +++ b/app/Models/Coupon.php @@ -2,17 +2,87 @@ namespace App\Models; +use App\Lib\Helpers\FileStorageHelper; use Carbon\Carbon; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsToMany; +/** + * @property integer $id Model id. + * @property integer $brand_id ID de la marque sur laquelle le coupon s'applique. + * @property string $name Nom du coupon. + * @property string $process_code Code de traitement. + * @property double $amount Montant de la réduction - Max 3 euros. + * @property string $valid_on Texte réduction valable sur. + * @property integer $validity_period Période de validité en jours - default 30 j - max 1 mois. + * @property string $legal_notices Mentions légales. + * @property string $decoration Path de l'image de décoration + * - 280x160px + * - JPG. + * @property string|null $gs1 Path de l'image GS1 pour coupon Benelux + * - 628x360px + * - JPG + * @property string|null $website_picture Path de l'image a destination des sites internet + * - 580x190px + * - JPG/PNG + * @property string|null $website_picture_mobile Path de l'image a destination des sites internet mobile + * - 570x560px + * - JPG/PNG + * @property integer $limit_qty Limite de quantité d'impression / customer. + * @property integer $limit_period Limite d'impression dans une periode (en jours) / customer. + * @property integer $status Statut du coupon, Publié / Non Publié. + * @property integer $can_be_used Statut d'utilisation du coupon, Utilisable / Non utilisable, + * Modifié par calcul dans un Job en fonction des dates. + * @property \Carbon\Carbon|null $start_diffusion_date Date de début de diffusion - diffusion immédiate si null. + * @property \Carbon\Carbon|null $end_diffusion_date Date de fin de diffusion - diffusion sans fin si null. + * @property \Carbon\Carbon|null $end_validity_date Date de fin de validité + * - diffusion immédiate si null + * - required si end_diffusion_date est set et date + * au minimum egale à end_diffusion_date + validity_period jours. + * @package App\Models + */ class Coupon extends Model { - // FIXME : Ajouter les mouvement de quantité, mettre juste une colonne et se servir des logs avec un type pour mouvement ? - // FIXME / Ajouter logs impressions use HasFactory; + // FIXME : Ajouter les mouvement de quantité, mettre juste une colonne et se servir des logs avec un type pour mouvement ? + // FIXME / Ajouter logs impressions. + + /** + * The attributes that are mass assignable. + * + * @var array + */ + protected $fillable = [ + 'name', + 'status', + 'brand_id', + 'process_code', + 'amount', + 'valid_on', + 'validity_period', + 'legal_notices', + 'decoration', + 'gs1', + 'website_picture', + 'website_picture_mobile', + 'limit_qty', + 'limit_period', + 'start_diffusion_date', + 'end_diffusion_date', + ]; + + /** + * The attributes that aren't mass assignable. + * + * @var array + */ + protected $guarded = [ + 'can_be_used', + 'end_validity_date', + ]; + /** * The attributes that should be cast. * @@ -22,38 +92,96 @@ class Coupon extends Model 'start_diffusion_date' => 'date:Y-m-d', 'end_diffusion_date' => 'date:Y-m-d', 'end_validity_date' => 'date:Y-m-d', + 'status' => 'bool' ]; - public function campaigns() : BelongsToMany + /** + * Perform any actions required after the model boots. + * + * @return void + */ + protected static function booted(): void + { + static::creating(function (self $model) { + self::manageFields($model); + }); + static::updating(function (self $model) { + self::manageFields($model); + }); + static::updated(function (self $model) { + FileStorageHelper::removeOldFile($model, 'decoration'); + FileStorageHelper::removeOldFile($model, 'gs1'); + FileStorageHelper::removeOldFile($model, 'website_picture'); + FileStorageHelper::removeOldFile($model, 'website_picture_mobile'); + }); + static::deleted(function (self $model) { + FileStorageHelper::removeFile($model, 'decoration'); + FileStorageHelper::removeFile($model, 'gs1'); + FileStorageHelper::removeFile($model, 'website_picture'); + FileStorageHelper::removeFile($model, 'website_picture_mobile'); + }); + } + + // * RELATIONSHIPS + + /** + * The campaigns that belong to the Coupon + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany + */ + public function campaigns(): BelongsToMany { return $this->belongsToMany(Campaign::class); } + // * METHODS + /** - * The "booted" method of the model. + * Manage fields + * + * @param self $model + * @return void */ - protected static function booted(): void + private static function manageFields(self $model) { - static::creating(function (Coupon $coupon) { - $coupon->can_be_used = $this->checkIfCanBeUsed(); - }); - static::updating(function (Coupon $coupon) { - $coupon->can_be_used = $this->checkIfCanBeUsed(); - }); + $model->decoration = FileStorageHelper::storeFile($model, $model->decoration, true); + $model->gs1 = FileStorageHelper::storeFile($model, $model->gs1, true); + $model->website_picture = FileStorageHelper::storeFile($model, $model->website_picture, true); + $model->website_picture_mobile = FileStorageHelper::storeFile($model, $model->website_picture_mobile, true); + + if (is_null($model->start_diffusion_date)) { + $model->start_diffusion_date = Carbon::now(); + } + if (isset($model->end_diffusion_date)) { + $model->end_validity_date = $model->end_diffusion_date->addDays($model->validity_period); + } else { + $model->end_validity_date = Carbon::now()->addDays($model->validity_period); + } + // Last action is to test if model can be used. + $model->can_be_used = $model->checkIfCanBeUsed(); } + /** + * Get can be used + * + * @return boolean + */ public function checkIfCanBeUsed() { if ( - (is_null($this->start_diffusion_date) or $this->start_diffusion_date->lessThanOrEqualTo(Carbon::now())) and - (is_null($this->end_diffusion_date) or $this->end_diffusion_date->greaterThanOrEqualTo(Carbon::now())) and - (is_null($this->end_validity_date) or $this->end_validity_date->greaterThanOrEqualTo(Carbon::now()->addDays($this->validity_period))) - ){ + (!is_null($this->start_diffusion_date) and + $this->start_diffusion_date->lessThanOrEqualTo(Carbon::now()) + ) and + (!is_null($this->end_diffusion_date) and + $this->end_diffusion_date->greaterThanOrEqualTo(Carbon::now()) + ) and + (!is_null($this->end_validity_date) and + $this->end_validity_date->greaterThanOrEqualTo(Carbon::now()->addDays($this->validity_period))) + ) { return true; } // FIXME : Ajouter calendrier de diffusion et le prendre en compte dans la condition - return false; } } diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index f426039..a3d2fca 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -3,6 +3,7 @@ namespace App\Providers; use App\Models\Campaign; +use App\Models\Coupon; use Illuminate\Cache\RateLimiting\Limit; use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider; use Illuminate\Http\Request; @@ -42,6 +43,7 @@ class RouteServiceProvider extends ServiceProvider }); Route::model('campaign', Campaign::class); + Route::model('coupon', Coupon::class); } /** diff --git a/database/factories/CouponFactory.php b/database/factories/CouponFactory.php new file mode 100644 index 0000000..873907a --- /dev/null +++ b/database/factories/CouponFactory.php @@ -0,0 +1,60 @@ + + */ +class CouponFactory extends Factory +{ + /** + * The name of the factory's corresponding model. + * + * @var string + */ + protected $model = Coupon::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition() + { + $coupon = new Coupon(); + + // Create a carbon instance date. + $start_diffusion_date = Carbon::parse(fake()->date()); + + // Add random days to start diffusion date. + $end_diffusion_date = Carbon::parse($start_diffusion_date)->addDays(fake()->numberBetween(30, 365)); + + // Add random days to end diddusion date. + $end_validity_date = Carbon::parse($end_diffusion_date)->addDays(fake()->numberBetween(1, 30)); + + // Get a brand in DB to have is name and id. + $random_brand = fake()->randomElement(DB::table('brands')->get()); + + return [ + 'name' => fake()->unique()->word(), + 'process_code' => fake()->unique()->bothify('?????-#####'), + 'amount' => fake()->randomFloat(1, 0.1, 3), + 'valid_on' => $random_brand->name, + 'validity_period' => fake()->numberBetween(1, 30), + 'legal_notices' => fake()->text(100), + 'limit_qty' => fake()->numberBetween(0, 2), + 'limit_period' => fake()->numberBetween(0, 30), + 'status' => fake()->boolean(75), + 'can_be_used' => $coupon->checkIfCanBeUsed(), + 'start_diffusion_date' => fake()->boolean(75) ? $start_diffusion_date : null, + 'end_diffusion_date' => fake()->boolean(75) ? $end_diffusion_date : null, + 'end_validity_date' => fake()->boolean(75) ? $end_validity_date : null, + 'brand_id' => $random_brand->id + ]; + } +} diff --git a/database/migrations/2023_08_20_090058_create_coupons_table.php b/database/migrations/2023_08_20_090058_create_coupons_table.php index 441ae0c..627d792 100644 --- a/database/migrations/2023_08_20_090058_create_coupons_table.php +++ b/database/migrations/2023_08_20_090058_create_coupons_table.php @@ -17,7 +17,7 @@ return new class extends Migration $table->string('process_code')->unique()->comment("Code de traitement"); $table->float('amount')->comment("Montant de la réduction - Max 3 euros"); $table->text('valid_on')->comment("Texte réduction valable sur ..."); - $table->unsignedInteger('validity_period')->default(30)->comment("Période de validité en jours - default 30 j - max 1 mois"); + $table->unsignedSmallInteger('validity_period')->default(30)->comment("Période de validité en jours - default 30 j - max 1 mois"); $table->text('legal_notices')->comment("Mentions légales"); $table->string('decoration')->comment("Path de l'image de décoration - 280x160px - JPG"); $table->string('gs1')->nullable()->comment("Path de l'image GS1 pour coupon Benelux - 628x360px - JPG"); diff --git a/database/migrations/2023_09_18_110818_add_foreign_keys_to_coupons_table.php b/database/migrations/2023_09_18_110818_add_foreign_keys_to_coupons_table.php new file mode 100644 index 0000000..25ab156 --- /dev/null +++ b/database/migrations/2023_09_18_110818_add_foreign_keys_to_coupons_table.php @@ -0,0 +1,29 @@ +foreignId('brand_id')->after('id')->comment('ID de la marque sur laquelle le coupon s\'applique')->constrained(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('coupons', function (Blueprint $table) { + // + $table->dropForeign(['brand_id']); + }); + } +}; diff --git a/database/seeders/CouponSeeder.php b/database/seeders/CouponSeeder.php new file mode 100644 index 0000000..01e7ee6 --- /dev/null +++ b/database/seeders/CouponSeeder.php @@ -0,0 +1,34 @@ +make()->each(function (Coupon $coupon) { + $coupon->decoration = FileStorageHelper::storeFile($coupon, new \SplFileInfo(\resource_path( + '../resources/assets/images/coupon/decoration.jpg' + ))); + $coupon->gs1 = FileStorageHelper::storeFile($coupon, new \SplFileInfo(\resource_path( + '../resources/assets/images/coupon/gs1.jpg' + ))); + $coupon->website_picture = FileStorageHelper::storeFile($coupon, new \SplFileInfo(\resource_path( + '../resources/assets/images/coupon/website_picture.jpg' + ))); + $coupon->website_picture_mobile = FileStorageHelper::storeFile($coupon, new \SplFileInfo(\resource_path( + '../resources/assets/images/coupon/website_picture_mobile.jpg' + ))); + $coupon->saveOrFail(); + }); + } +} diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 794e057..2bf3bd3 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -15,7 +15,8 @@ class DatabaseSeeder extends Seeder { $this->call([ BrandSeeder::class, - CampaignSeeder::class + CampaignSeeder::class, + CouponSeeder::class ]); } } diff --git a/resources/assets/images/coupon/decoration.jpg b/resources/assets/images/coupon/decoration.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a40f55db640c2b28fcacc79d8d0d1da2d705f6b0 GIT binary patch literal 1863 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 MJ@DT=fZyi*4YtFQ$^ZZW literal 0 HcmV?d00001 diff --git a/resources/assets/images/coupon/website_picture.jpg b/resources/assets/images/coupon/website_picture.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b69766cddf8335ad0c54b292038ccfc13500b532 GIT binary patch literal 4414 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 literal 0 HcmV?d00001 diff --git a/resources/lang/en/models.php b/resources/lang/en/models.php index 0a80140..5207b30 100644 --- a/resources/lang/en/models.php +++ b/resources/lang/en/models.php @@ -13,6 +13,7 @@ return [ 'storeregions' => 'Store Regions', 'storetypes' => 'Store Types', 'brands' => 'Brands', - 'campaigns' => 'Campaigns' + 'campaigns' => 'Campaigns', + 'coupons' => 'Coupons' ] ]; diff --git a/resources/lang/en/validation.php b/resources/lang/en/validation.php index 54b913f..d30abc5 100644 --- a/resources/lang/en/validation.php +++ b/resources/lang/en/validation.php @@ -119,15 +119,30 @@ return [ "uuid" => "The :attribute must be a valid UUID.", "custom" => [], 'attributes' => [ - 'name' => 'Name', - 'contact_mail' => 'E-mail', - 'RCS' => 'RCS', - 'logo' => 'Logo', - 'crm_token' => 'CRM Token', - 'crm_password' => 'CRM Password', - 'type' => 'Type', - 'status' => 'Status', - 'brand_id' => 'Associated brand', - 'updated_at' => 'Updated at' + 'name' => 'Name', + 'contact_mail' => 'E-mail', + 'RCS' => 'RCS', + 'logo' => 'Logo', + 'crm_token' => 'CRM Token', + 'crm_password' => 'CRM Password', + 'type' => 'Type', + 'status' => 'Status', + 'brand_id' => 'Brand id', + 'updated_at' => 'Updated at', + 'process_code' => 'Process code', + 'amount' => 'Amount', + 'valid_on' => 'Valid on', + 'validity_period' => 'Validity period', + 'legal_notices' => 'Legal notices', + 'decoration' => 'Decoration', + 'gs1' => 'gs1', + 'website_picture' => 'Website picture', + 'website_picture_mobile' => 'Website picture mobile', + 'limit_qty' => 'Limit quantity', + 'limit_period' => 'Limit period', + 'can_be_used' => 'Can be used', + 'start_diffusion_date' => 'Start diffusion date', + 'end_diffusion_date' => 'End diffusion date', + 'end_validity_date' => 'End validity date' ], ]; diff --git a/resources/lang/fr/models.php b/resources/lang/fr/models.php index a0265d2..6bb3911 100644 --- a/resources/lang/fr/models.php +++ b/resources/lang/fr/models.php @@ -4,6 +4,7 @@ return [ 'classes' => [ 'users' => 'Utilisateurs', 'brands' => 'Marques', - 'campaigns' => 'Campagnes' + 'campaigns' => 'Campagnes', + 'coupons' => 'Coupons' ] ]; diff --git a/resources/lang/fr/validation.php b/resources/lang/fr/validation.php index 1468a8e..e5e9c3a 100644 --- a/resources/lang/fr/validation.php +++ b/resources/lang/fr/validation.php @@ -119,15 +119,30 @@ return [ 'uuid' => 'Le champ :attribute doit être un UUID valide', 'custom' => [], 'attributes' => [ - 'name' => 'Nom', - 'contact_mail' => 'E-mail', - 'rcs' => 'RCS', - 'logo' => 'Logo', - 'crm_token' => 'CRM Token', - 'crm_password' => 'Mot de passe CRM', - 'type' => 'Type', - 'status' => 'Status', - 'brand_id' => 'Marque associée', - 'updated_at' => 'Date de modification' + 'name' => 'Nom', + 'contact_mail' => 'E-mail', + 'rcs' => 'RCS', + 'logo' => 'Logo', + 'crm_token' => 'CRM Token', + 'crm_password' => 'Mot de passe CRM', + 'type' => 'Type', + 'status' => 'Status', + 'brand_id' => 'ID de la marque', + 'updated_at' => 'Date de modification', + 'process_code' => 'Code de processus', + 'amount' => 'Montant', + 'valid_on' => 'Valable sur', + 'validity_period' => 'Période de validité', + 'legal_notices' => 'Mentions légales', + 'decoration' => 'Décoration', + 'gs1' => 'gs1', + 'website_picture' => 'Photo pour site web', + 'website_picture_mobile' => 'Photo pour le site web mobile', + 'limit_qty' => 'Quantité limite', + 'limit_period' => 'Période limite', + 'can_be_used' => 'Peut être utilisé', + 'start_diffusion_date' => 'Date de début de diffusion', + 'end_diffusion_date' => 'Date de fin de diffusion', + 'end_validity_date' => 'Date de fin de validité' ], ]; diff --git a/resources/views/bo/layouts/nav-menu.blade.php b/resources/views/bo/layouts/nav-menu.blade.php index b03479b..caa5e81 100644 --- a/resources/views/bo/layouts/nav-menu.blade.php +++ b/resources/views/bo/layouts/nav-menu.blade.php @@ -1,22 +1,24 @@ @auth('backend') - -@endauth + +@endauth \ No newline at end of file diff --git a/resources/views/bo/pages/coupons/create.blade.php b/resources/views/bo/pages/coupons/create.blade.php new file mode 100644 index 0000000..4654295 --- /dev/null +++ b/resources/views/bo/pages/coupons/create.blade.php @@ -0,0 +1,41 @@ +@extends('bo.layout') + +@section('title', sprintf('%s - %s', Str::singular(__('models.classes.coupons')), Str::ucfirst(__('crud.actions.create')))) +@section('description', __('Creation d\'un nouveau coupon.')) + +@section('breadcrumb', + request()->route()->getName()) + +@section('content') +
+
+
+
+

+ + + + {{ Str::singular(__('models.classes.coupons')) }} {{ __('create') }} +

+
+
+
+
+
+ @csrf + @include('bo.pages.coupons.form-inputs') +
+
+ +
+
+
+
+
+@endsection diff --git a/resources/views/bo/pages/coupons/edit.blade.php b/resources/views/bo/pages/coupons/edit.blade.php new file mode 100644 index 0000000..708bdd1 --- /dev/null +++ b/resources/views/bo/pages/coupons/edit.blade.php @@ -0,0 +1,43 @@ +@extends('bo.layout', ['brParam' => $couponModel]) + +@section('title', sprintf('%s - %s %s', Str::singular(__('models.classes.coupons')), Str::ucfirst(__('crud.actions.edit')), + $couponModel->name)) +@section('description', $couponModel->valid_on) + +@section('breadcrumb', + request()->route()->getName()) + +@section('content') +
+
+
+
+

+ + + + {{ Str::singular(__('models.classes.coupons')) }} {{ __('crud.actions.edit') }} +

+
+
+
+
+
+ @csrf + @method('PUT') + @include('bo.pages.coupons.form-inputs', ['model' => $couponModel]) +
+
+ +
+
+
+
+
+@endsection diff --git a/resources/views/bo/pages/coupons/form-inputs.blade.php b/resources/views/bo/pages/coupons/form-inputs.blade.php new file mode 100644 index 0000000..c54cd1f --- /dev/null +++ b/resources/views/bo/pages/coupons/form-inputs.blade.php @@ -0,0 +1,395 @@ +
+ {{-- NAME --}} +
+
+ {{ __('validation.attributes.name') }} +
+
+ @include('bo.partials.inputs.varchar', [ + 'id' => 'couponName', + 'label' => __('Nom du :model', ['model' => Str::singular(__('models.classes.coupons'))]), + 'hasWordCounter' => true, + 'name' => 'name', + 'required' => true, + 'model' => $couponModel ?? null, + 'helper' => __('validation.between.string', [ + 'attribute' => __('Nom du :model', ['model' => Str::singular(__('models.classes.coupons'))]), + 'min' => 3, + 'max' => 255, + ]), + 'placeholder' => __('Nom du coupon'), + ]) +
+
+
+
+ {{-- BRAND_ID --}} +
+
+ {{ __('validation.attributes.brand_id') }} +
+
+ @include('bo.partials.inputs.bigint-foreign-id', [ + 'id' => 'couponBrandId', + 'label' => __(':model associée', ['model' => Str::singular(trans('models.classes.brands'))]), + 'name' => 'brand_id', + 'size' => 5, + 'model' => $couponModel ?? null, + 'associatedModels' => $brandModels, + 'foreignLabel' => 'name', + 'helper' => __('Chose one :model among this list.', [ + 'model' => Str::singular(trans('models.classes.brands')), + ]), + ]) +
+
+
+
+ {{-- PROCESS CODE --}} +
+
+ {{ __('validation.attributes.process_code') }} +
+
+ @include('bo.partials.inputs.varchar', [ + 'id' => 'couponProcessCode', + 'label' => __('Code processus du :model', ['model' => Str::singular(__('models.classes.coupons'))]), + 'hasWordCounter' => true, + 'name' => 'process_code', + 'required' => true, + 'model' => $couponModel ?? null, + 'helper' => __('validation.between.string', [ + 'attribute' => __('Code processus du :model', ['model' => Str::singular(__('models.classes.coupons'))]), + 'min' => 3, + 'max' => 255, + ]), + ]) +
+
+
+
+ {{-- AMOUNT --}} +
+
+ {{ __('validation.attributes.amount') }} +
+
+ @include('bo.partials.inputs.int-number', [ + 'id' => 'couponAmount', + 'label' => __('Montant du :model', ['model' => Str::singular(__('models.classes.coupons'))]), + 'name' => 'amount', + 'step' => 0.1, + 'required' => true, + 'model' => $couponModel ?? null, + 'helper' => __('validation.between.numeric', [ + 'attribute' => __('Montant du :model', ['model' => Str::singular(__('models.classes.coupons'))]), + 'min' => 0.1, + 'max' => 3, + ]), + 'placeholder' => __('Montant du coupon'), + ]) +
+
+
+
+ {{-- VALID ON --}} +
+
+ {{ __('validation.attributes.valid_on') }} +
+
+ @include('bo.partials.inputs.text-html', [ + 'id' => 'couponValidOn', + 'label' => __(':model valable sur', ['model' => Str::singular(__('models.classes.coupons'))]), + 'name' => 'valid_on', + 'required' => true, + 'model' => $couponModel ?? null, + 'helper' => __('validation.gt.string', [ + 'attribute' => __(':model valable sur', ['model' => Str::singular(__('models.classes.coupons'))]), + 'value' => 15, + ]), + ]) +
+
+
+
+ {{-- VALIDITY PERIOD --}} +
+
+ {{ __('validation.attributes.validity_period') }} +
+
+ @include('bo.partials.inputs.smallint-number', [ + 'id' => 'couponValidityPeriod', + 'label' => __('Période de validité du :model', ['model' => Str::singular(__('models.classes.coupons'))]), + 'name' => 'validity_period', + 'required' => true, + 'model' => $couponModel ?? null, + 'helper' => __('validation.between.numeric', [ + 'attribute' => __('Période de validité du :model', [ + 'model' => Str::singular(__('models.classes.coupons')), + ]), + 'min' => 1, + 'max' => 30, + ]), + 'placeholder' => __('Période de validité du coupon'), + ]) +
+
+
+
+ {{-- LEGAL NOTICES --}} +
+
+ {{ __('validation.attributes.legal_notices') }} +
+
+ @include('bo.partials.inputs.text-html', [ + 'id' => 'couponLegalNotices', + 'label' => __('Mentions légales du :model', ['model' => Str::singular(__('models.classes.coupons'))]), + 'name' => 'legal_notices', + 'required' => true, + 'model' => $couponModel ?? null, + 'helper' => __('validation.gt.string', [ + 'attribute' => __('Mentions légales du :model', ['model' => Str::singular(__('models.classes.coupons'))]), + 'value' => 15, + ]), + ]) +
+
+
+
+ {{-- DECORATION --}} +
+
+ {{ __('validation.attributes.decoration') }} +
+
+ @include('bo.partials.inputs.varchar-picture', [ + 'id' => 'couponDecoration', + 'name' => 'decoration', + 'width' => 280, + 'height' => 160, + 'required' => true, + 'model' => $couponModel ?? null, + 'helper' => __('crud.minicrud.dimensions_must_be', [ + 'widthpx' => '280px', + 'heightpx' => '160px', + ]), + 'preview' => true, + ]) +
+
+
+
+ {{-- GS1 --}} +
+
+ {{ __('validation.attributes.gs1') }} +
+
+ @include('bo.partials.inputs.varchar-picture', [ + 'id' => 'couponGs1', + 'name' => 'gs1', + 'width' => 628, + 'height' => 360, + 'required' => true, + 'model' => $couponModel ?? null, + 'helper' => __('crud.minicrud.dimensions_must_be', [ + 'widthpx' => '628px', + 'heightpx' => '360px', + ]), + 'required' => false, + 'preview' => true, + ]) +
+
+
+
+ {{-- WEBSITE PICTURE --}} +
+
+ {{ __('validation.attributes.website_picture') }} +
+
+ @include('bo.partials.inputs.varchar-picture', [ + 'id' => 'couponWebsitePicture', + 'name' => 'website_picture', + 'width' => 580, + 'height' => 190, + 'model' => $couponModel ?? null, + 'helper' => __('crud.minicrud.dimensions_must_be', [ + 'widthpx' => '580px', + 'heightpx' => '190px', + ]), + 'required' => false, + 'preview' => true, + ]) +
+
+
+
+ {{-- WEBSITE PICTURE MOBILE --}} +
+
+ {{ __('validation.attributes.website_picture_mobile') }} +
+
+ @include('bo.partials.inputs.varchar-picture', [ + 'id' => 'couponWebsitePictureMobile', + 'name' => 'website_picture_mobile', + 'width' => 570, + 'height' => 560, + 'model' => $couponModel ?? null, + 'helper' => __('crud.minicrud.dimensions_must_be', [ + 'widthpx' => '570px', + 'heightpx' => '560px', + ]), + 'required' => false, + 'preview' => true, + ]) +
+
+
+
+ {{-- LIMITY QTY --}} +
+
+ {{ __('validation.attributes.limit_qty') }} +
+
+ @include('bo.partials.inputs.smallint-number', [ + 'id' => 'couponLimitQty', + 'label' => __('Limite de quantité d\'impression du :model par clients', [ + 'model' => Str::singular(__('models.classes.coupons')), + ]), + 'name' => 'limit_qty', + 'required' => true, + 'model' => $couponModel ?? null, + 'helper' => __('validation.between.numeric', [ + 'attribute' => __('Limite de quantité d\'impression du :model par clients', [ + 'model' => Str::singular(__('models.classes.coupons')), + ]), + 'min' => 1, + 'max' => 30, + ]), + 'placeholder' => __('Limite de quantité d\'impression du coupon par clients'), + ]) +
+
+
+
+ {{-- LIMITY PERIOD --}} +
+
+ {{ __('validation.attributes.limit_period') }} +
+
+ @include('bo.partials.inputs.smallint-number', [ + 'id' => 'couponLimitPeriod', + 'label' => __('Limite d\'impression du :model dans une période (en jours)', [ + 'model' => Str::singular(__('models.classes.coupons')), + ]), + 'name' => 'limit_period', + 'required' => true, + 'model' => $couponModel ?? null, + 'helper' => __('validation.between.numeric', [ + 'attribute' => __('Limite d\'impression du :model dans une période (en jours)', [ + 'model' => Str::singular(__('models.classes.coupons')), + ]), + 'min' => 1, + 'max' => 30, + ]), + 'placeholder' => __('Limite d\'impression du coupon dans une période (en jours)'), + ]) +
+
+
+
+ {{-- START DIFFUSION DATE --}} +
+
+ {{ __('validation.attributes.start_diffusion_date') }} +
+
+ @include('bo.partials.inputs.timestamp-date', [ + 'id' => 'couponStartDiffusionDate', + 'label' => __('Date de début de diffusion du :model', [ + 'model' => Str::singular(__('models.classes.coupons')), + ]), + 'name' => 'start_diffusion_date', + 'required' => false, + 'model' => $couponModel ?? null, + 'helper' => __('validation.date', [ + 'attribute' => __('Date de début de diffusion du :model', [ + 'model' => Str::singular(__('models.classes.coupons')), + ]), + ]), + ]) +
+
+
+
+
+{{-- END DIFFUSION DATE --}} +
+
+ {{ __('validation.attributes.end_diffusion_date') }} +
+
+ @include('bo.partials.inputs.timestamp-date', [ + 'id' => 'couponEndDiffusionDate', + 'label' => __('Date de fin de diffusion du :model', ['model' => Str::singular(__('models.classes.coupons'))]), + 'name' => 'end_diffusion_date', + 'required' => false, + 'model' => $couponModel ?? null, + 'helper' => __('validation.date', [ + 'attribute' => __('Date de fin de diffusion du :model', [ + 'model' => Str::singular(__('models.classes.coupons')), + ]), + ]), + ]) +
+
+
+
+{{-- END VALIDITY DATE --}} +
+
+ {{ __('validation.attributes.end_validity_date') }} +
+
+ @include('bo.partials.inputs.timestamp-date', [ + 'id' => 'couponEndDiffusionDate', + 'label' => __('Date de fin de validité du :model', ['model' => Str::singular(__('models.classes.coupons'))]), + 'name' => 'end_validity_date', + 'required' => false, + 'disabled' => true, + 'model' => $couponModel ?? null, + 'helper' => __('validation.date', [ + 'attribute' => __('Date de fin de validité du :model', [ + 'model' => Str::singular(__('models.classes.coupons')), + ]), + ]), + ]) +
+
+
+
+{{-- STATUS --}} +
+
+ {{ __('validation.attributes.status') }} +
+
+ @include('bo.partials.inputs.tinyint-boolean', [ + 'id' => 'couponStatus', + 'name' => 'status', + 'label' => __('crud.minicrud.visibility_label'), + 'helper' => __('crud.minicrud.visibility_helper'), + 'model' => $couponModel ?? null, + ]) +
+
+
+
diff --git a/resources/views/bo/pages/coupons/index-list.blade.php b/resources/views/bo/pages/coupons/index-list.blade.php new file mode 100644 index 0000000..0afb8c3 --- /dev/null +++ b/resources/views/bo/pages/coupons/index-list.blade.php @@ -0,0 +1,87 @@ +
+ + + @php + $cols = [ + 'name' => __('validation.attributes.name'), + 'brand_id' => __('validation.attributes.brand_id'), + 'status' => __('validation.attributes.status'), + 'process_code' => __('validation.attributes.process_code'), + 'amount' => __('validation.attributes.amount'), + 'valid_on' => __('validation.attributes.valid_on'), + 'validity_period' => __('validation.attributes.validity_period'), + 'legal_notices' => __('validation.attributes.legal_notices'), + 'decoration' => __('validation.attributes.decoration'), + 'gs1' => __('validation.attributes.gs1'), + 'website_picture' => __('validation.attributes.website_picture'), + 'website_picture_mobile' => __('validation.attributes.website_picture_mobile'), + 'limit_qty' => __('validation.attributes.limit_qty'), + 'limit_period' => __('validation.attributes.limit_period'), + 'can_be_used' => __('validation.attributes.can_be_used'), + 'start_diffusion_date' => __('validation.attributes.start_diffusion_date'), + 'end_diffusion_date' => __('validation.attributes.end_diffusion_date'), + 'end_validity_date' => __('validation.attributes.end_validity_date'), + 'updated_at' => __('validation.attributes.updated_at'), + ]; + @endphp + @include('bo.modules.table-col-sorter', ['cols' => $cols, 'mobileHide' => ['status']]) + + + @if ($couponModels->count()) + @foreach ($couponModels as $couponModel) + + + + @include('bo.modules.change-status', [ + 'routeName' => 'coupons', + 'model' => $couponModel, + ]) + + + + + + + + + + + + + + + + + + + @endforeach + @else + + + + @endif + +
{{ $couponModel->name }}{{ $couponModel->brand_id }}{{ $couponModel->process_code }}{{ $couponModel->amount }}€{{ $couponModel->valid_on }}{{ $couponModel->validity_period }} {{ __('jours') }}{{ $couponModel->legal_notices }}{{ $couponModel->decoration }}{{ $couponModel->gs1 }}{{ $couponModel->website_picture }}{{ $couponModel->website_picture_mobile }}{{ $couponModel->limit_qty }}{{ $couponModel->limit_period }} {{ __('jours') }}{{ $couponModel->can_be_used }}{{ $couponModel->start_diffusion_date }}{{ $couponModel->end_diffusion_date }}{{ $couponModel->end_validity_date }} + {{ $couponModel->updated_at->formatLocalized('%d/%m/%Y à %H:%M') }} + +
+ + + + @method('DELETE') + @csrf +
+ +
+
+
+ {{ __('crud.helpers.no_results', ['model' => __('models.classes.coupons')]) }} +
+
diff --git a/resources/views/bo/pages/coupons/index.blade.php b/resources/views/bo/pages/coupons/index.blade.php new file mode 100644 index 0000000..3bcfb41 --- /dev/null +++ b/resources/views/bo/pages/coupons/index.blade.php @@ -0,0 +1,48 @@ +@extends('bo.layout') + +@section('title', __('crud.helpers.list_all', ['model' => __('models.classes.coupons')])) +@section('description', __('crud.helpers.list_all', ['model' => __('models.classes.coupons')])) +@section('breadcrumb', request()->route()->getName()) + +@section('content') +
+
+
+
+
+
+
+

{{ __('models.classes.coupons') }} {{ Str::ucfirst(__('crud.helpers.list')) }}  

+
+
+
+ + + +
+
+
+
+
+ @include('bo.modules.search-bar') +
+
+ {{ $couponModels->withQueryString()->onEachSide(1)->links() }} +
+
+
+
+ @include('bo.pages.coupons.index-list') +
+
+
+
+ {{ $couponModels->withQueryString()->onEachSide(1)->links() }} +
+
+
+
+
+
+
+@endsection diff --git a/resources/views/bo/partials/inputs/timestamp-date.blade.php b/resources/views/bo/partials/inputs/timestamp-date.blade.php new file mode 100644 index 0000000..aabdbde --- /dev/null +++ b/resources/views/bo/partials/inputs/timestamp-date.blade.php @@ -0,0 +1,19 @@ + +{{-- blade-formatter-disable --}} + +{{-- blade-formatter-enable --}} +@include('modules.input-error', [ + 'inputName' => $name, + 'helperId' => "{$id}Help", + 'helper' => $helper, +]) diff --git a/routes/web-bo.php b/routes/web-bo.php index 499638e..96f5a57 100644 --- a/routes/web-bo.php +++ b/routes/web-bo.php @@ -2,6 +2,7 @@ use App\Http\Controllers\Bo\BrandController; use App\Http\Controllers\Bo\CampaignController; +use App\Http\Controllers\Bo\CouponController; use App\Http\Controllers\Bo\DashboardController; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Route; @@ -25,9 +26,16 @@ Route::prefix('bo')->name('bo.')->group( Route::middleware('auth:backend')->group(function () { Route::get('/', [DashboardController::class, 'index'])->name('dashboard'); + + // * BRANDS Route::resource('brands', BrandController::class)->except('show'); - //* CAMPAIGNS + // * COUPONS + Route::resource('coupons', CouponController::class)->except('show'); + Route::post('/coupons/{coupon}/change-published', [CouponController::class, 'changePublished']) + ->name('coupons.change-published'); + + // * CAMPAIGNS Route::resource('campaigns', CampaignController::class)->except('show'); Route::post('/campaigns/{campaign}/change-published', [CampaignController::class, 'changePublished']) ->name('campaigns.change-published');