Merge branch 'couponPage' into 'campaignPage'

Coupon page

See merge request ldc-web/projets/mesbr!3
This commit is contained in:
Vincent BRIDIER 2023-11-24 15:09:54 +00:00
commit 3c70f892eb
32 changed files with 1251 additions and 67 deletions

View file

@ -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();
}
/**

View file

@ -18,4 +18,4 @@ enum CampaignTypes: int
self::WALL->name => "Mur",
self::BASKET->name => "Panier",
];
}
}

View file

@ -10,7 +10,6 @@ use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class BrandController extends Controller
{
/**

View file

@ -0,0 +1,156 @@
<?php
namespace App\Http\Controllers\Bo;
use App\Http\Controllers\Controller;
use App\Http\Requests\Bo\Coupons\StoreCouponRequest;
use App\Http\Requests\Bo\Coupons\UpdateCouponRequest;
use App\Models\Brand;
use App\Models\Coupon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use App\Traits\Controllers\UpdateModelStatus;
class CouponController extends Controller
{
use UpdateModelStatus;
/**
* Display a listing of the resource.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function index(Request $request)
{
/** @var \Illuminate\Database\Eloquent\Builder $coupon */
$coupon = Coupon::query();
if ($request->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'))]
));
}
}

View file

@ -0,0 +1,67 @@
<?php
namespace App\Http\Requests\Bo\Coupons;
use App\Traits\Requests\HasPicture;
use Illuminate\Foundation\Http\FormRequest;
class StoreCouponRequest extends FormRequest
{
use HasPicture;
/**
* Determine if the user is authorized to make this request.
*
* @return boolean
*/
public function authorize()
{
return auth('backend')->check();
}
/**
* Prepare the data for validation.
*
* @return void
*/
protected function prepareForValidation()
{
$this->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),
);
}
}

View file

@ -0,0 +1,7 @@
<?php
namespace App\Http\Requests\Bo\Coupons;
class UpdateCouponRequest extends StoreCouponRequest
{
}

View file

@ -0,0 +1,24 @@
<?php
namespace App\Jobs;
use App\Models\Coupon;
/**
* Update can_be_used of the coupon.
*/
class CouponCanBeUsedUpdate
{
/**
* Execute the job.
*
* @return void
*/
public function handle(): void
{
foreach (Coupon::all() as $coupon) {
$coupon->can_be_used = $coupon->checkIfCanBeUsed();
$coupon->save();
}
}
}

View file

@ -17,7 +17,7 @@ class Basket extends Model
'campaign_id'
];
public function content() : HasMany
public function content(): HasMany
{
return $this->hasMany(BasketsContent::class);
}

View file

@ -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);
}

View file

@ -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<int, string>
*/
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;
}
}

View file

@ -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);
}
/**

View file

@ -0,0 +1,60 @@
<?php
namespace Database\Factories;
use Carbon\Carbon;
use App\Models\Coupon;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Facades\DB;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Coupon>
*/
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
];
}
}

View file

@ -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");

View file

@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('coupons', function (Blueprint $table) {
$table->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']);
});
}
};

View file

@ -0,0 +1,34 @@
<?php
namespace Database\Seeders;
use App\Lib\Helpers\FileStorageHelper;
use App\Models\Coupon;
use Illuminate\Database\Seeder;
class CouponSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
Coupon::factory(25)->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();
});
}
}

View file

@ -15,7 +15,8 @@ class DatabaseSeeder extends Seeder
{
$this->call([
BrandSeeder::class,
CampaignSeeder::class
CampaignSeeder::class,
CouponSeeder::class
]);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

View file

@ -13,6 +13,7 @@ return [
'storeregions' => 'Store Regions',
'storetypes' => 'Store Types',
'brands' => 'Brands',
'campaigns' => 'Campaigns'
'campaigns' => 'Campaigns',
'coupons' => 'Coupons'
]
];

View file

@ -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'
],
];

View file

@ -4,6 +4,7 @@ return [
'classes' => [
'users' => 'Utilisateurs',
'brands' => 'Marques',
'campaigns' => 'Campagnes'
'campaigns' => 'Campagnes',
'coupons' => 'Coupons'
]
];

View file

@ -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é'
],
];

View file

@ -1,22 +1,24 @@
@auth('backend')
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link @if (request()->routeIs('bo.dashboard')) active @endif" data-bs-toggle="tooltip" href="{{ route('bo.dashboard') }}"
title="{{ __('Revenir à l\'accueil') }}">
{{ __('Dashboard') }}
</a>
</li>
<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="{{ __('Panneau des marques') }}">
{{ __('Brands') }}
</a>
</li>
<li class="nav-item">
<a class="nav-link @if (request()->routeIs('bo.campaigns.*')) active @endif" data-bs-toggle="tooltip"
href="{{ route('bo.campaigns.index') }}" title="{{ __('Panneau des campagnes') }}">
{{ __('Campaigns') }}
</a>
</li>
</ul>
@endauth
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link @if (request()->routeIs('bo.dashboard')) active @endif" data-bs-toggle="tooltip" href="{{ route('bo.dashboard') }}" title="{{ __('Revenir à l\'accueil') }}">
{{ __('Dashboard') }}
</a>
</li>
<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="{{ __('Panneau des marques') }}">
{{ __('Brands') }}
</a>
</li>
<li class="nav-item">
<a class="nav-link @if (request()->routeIs('bo.campaigns.*')) active @endif" data-bs-toggle="tooltip" href="{{ route('bo.campaigns.index') }}" title="{{ __('Panneau des campagnes') }}">
{{ __('Campaigns') }}
</a>
</li>
<li class="nav-item">
<a class="nav-link @if (request()->routeIs('bo.coupons.*')) active @endif" data-bs-toggle="tooltip" href="{{ route('bo.coupons.index') }}" title="{{ __('Gestion des coupons') }}">
{{ __('Coupons') }}
</a>
</li>
</ul>
@endauth

View file

@ -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')
<div class="card">
<div class="card-header">
<div class="row">
<div class="col">
<h1 class="col h2">
<a class="btn btn-primary text-decoration-none m-0" data-bs="tooltip" data-bs-placement="top" data-bs-toggle="tooltip"
href="{{ route('bo.coupons.index') }}"
title="{{ __('Return to the :model list', ['model' => __('models.classes.coupons')]) }}">
<i class="fa-solid fa-arrow-left"></i>
</a>
<span class="fw-bold">{{ Str::singular(__('models.classes.coupons')) }}</span>&nbsp;<small
class="text-muted">{{ __('create') }}</small>
</h1>
</div>
</div>
</div>
<div class="card-body bg-light">
<form id="formSubmit" action="{{ route('bo.coupons.store') }}" method="POST" enctype="multipart/form-data">
@csrf
@include('bo.pages.coupons.form-inputs')
<div class="row">
<div class="col text-center">
<button class="btn btn-primary" data-bs-toggle="tooltip" type="submit"
title="{{ __('Save :model', ['model' => Str::singular(__('models.classes.coupons'))]) }}">
{{ __('crud.actions.save') }}
</button>
</div>
</div>
</form>
</div>
</div>
@endsection

View file

@ -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')
<div class="card">
<div class="card-header">
<div class="row">
<div class="col">
<h1 class="col h2">
<a class="btn btn-primary text-decoration-none m-0" data-bs="tooltip" data-bs-placement="top" data-bs-toggle="tooltip"
href="{{ route('bo.coupons.index') }}"
title="{{ __('Return to the :model list', ['model' => __('models.classes.coupons')]) }}">
<i class="fa-solid fa-arrow-left"></i>
</a>
<span class="fw-bold">{{ Str::singular(__('models.classes.coupons')) }}</span>&nbsp;<small
class="text-muted">{{ __('crud.actions.edit') }}</small>
</h1>
</div>
</div>
</div>
<div class="card-body bg-light">
<form id="formSubmit" action="{{ route('bo.coupons.update', $couponModel->id) }}" method="POST" enctype="multipart/form-data">
@csrf
@method('PUT')
@include('bo.pages.coupons.form-inputs', ['model' => $couponModel])
<div class="row">
<div class="col text-center">
<button class="btn btn-primary" data-bs-toggle="tooltip" type="submit"
title="{{ __('Save :model', ['model' => Str::singular(__('models.classes.coupons'))]) }}">
{{ __('crud.actions.save') }}
</button>
</div>
</div>
</form>
</div>
</div>
@endsection

View file

@ -0,0 +1,395 @@
<div class="row">
{{-- NAME --}}
<div class="col-12 col-md-6 mb-3">
<fieldset class="h-100 border bg-white p-3">
<legend>{{ __('validation.attributes.name') }}</legend>
<div class="row">
<div class="col-12 mb-3">
@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'),
])
</div>
</div>
</fieldset>
</div>
{{-- BRAND_ID --}}
<div class="col-12 mb-3">
<fieldset class="h-100 border bg-white p-3">
<legend>{{ __('validation.attributes.brand_id') }}</legend>
<div class="row">
<div class="col-12 mb-3">
@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')),
]),
])
</div>
</div>
</fieldset>
</div>
{{-- PROCESS CODE --}}
<div class="col-12 col-md-6 mb-3">
<fieldset class="h-100 border bg-white p-3">
<legend>{{ __('validation.attributes.process_code') }}</legend>
<div class="row">
<div class="col-12 mb-3">
@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,
]),
])
</div>
</div>
</fieldset>
</div>
{{-- AMOUNT --}}
<div class="col-12 col-md-6 mb-3">
<fieldset class="h-100 border bg-white p-3">
<legend>{{ __('validation.attributes.amount') }}</legend>
<div class="row">
<div class="col-12 mb-3">
@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'),
])
</div>
</div>
</fieldset>
</div>
{{-- VALID ON --}}
<div class="col-12 col-md-6 mb-3">
<fieldset class="h-100 border bg-white p-3">
<legend>{{ __('validation.attributes.valid_on') }}</legend>
<div class="row">
<div class="col-12 mb-3">
@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,
]),
])
</div>
</div>
</fieldset>
</div>
{{-- VALIDITY PERIOD --}}
<div class="col-12 col-md-6 mb-3">
<fieldset class="h-100 border bg-white p-3">
<legend>{{ __('validation.attributes.validity_period') }}</legend>
<div class="row">
<div class="col-12 mb-3">
@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'),
])
</div>
</div>
</fieldset>
</div>
{{-- LEGAL NOTICES --}}
<div class="col-12 col-md-6 mb-3">
<fieldset class="h-100 border bg-white p-3">
<legend>{{ __('validation.attributes.legal_notices') }}</legend>
<div class="row">
<div class="col-12 mb-3">
@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,
]),
])
</div>
</div>
</fieldset>
</div>
{{-- DECORATION --}}
<div class="col-12 mb-3">
<fieldset class="h-100 border bg-white p-3">
<legend>{{ __('validation.attributes.decoration') }}</legend>
<div class="row">
<div class="col-12 mb-3">
@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,
])
</div>
</div>
</fieldset>
</div>
{{-- GS1 --}}
<div class="col-12 mb-3">
<fieldset class="h-100 border bg-white p-3">
<legend>{{ __('validation.attributes.gs1') }}</legend>
<div class="row">
<div class="col-12 mb-3">
@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,
])
</div>
</div>
</fieldset>
</div>
{{-- WEBSITE PICTURE --}}
<div class="col-12 mb-3">
<fieldset class="h-100 border bg-white p-3">
<legend>{{ __('validation.attributes.website_picture') }}</legend>
<div class="row">
<div class="col-12 mb-3">
@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,
])
</div>
</div>
</fieldset>
</div>
{{-- WEBSITE PICTURE MOBILE --}}
<div class="col-12 mb-3">
<fieldset class="h-100 border bg-white p-3">
<legend>{{ __('validation.attributes.website_picture_mobile') }}</legend>
<div class="row">
<div class="col-12 mb-3">
@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,
])
</div>
</div>
</fieldset>
</div>
{{-- LIMITY QTY --}}
<div class="col-12 col-md-6 mb-3">
<fieldset class="h-100 border bg-white p-3">
<legend>{{ __('validation.attributes.limit_qty') }}</legend>
<div class="row">
<div class="col-12 mb-3">
@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'),
])
</div>
</div>
</fieldset>
</div>
{{-- LIMITY PERIOD --}}
<div class="col-12 col-md-6 mb-3">
<fieldset class="h-100 border bg-white p-3">
<legend>{{ __('validation.attributes.limit_period') }}</legend>
<div class="row">
<div class="col-12 mb-3">
@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)'),
])
</div>
</div>
</fieldset>
</div>
{{-- START DIFFUSION DATE --}}
<div class="col-12 col-md-6 mb-3">
<fieldset class="h-100 border bg-white p-3">
<legend>{{ __('validation.attributes.start_diffusion_date') }}</legend>
<div class="row">
<div class="col-12 mb-3">
@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')),
]),
]),
])
</div>
</div>
</fieldset>
</div>
</div>
{{-- END DIFFUSION DATE --}}
<div class="col-12 col-md-6 mb-3">
<fieldset class="h-100 border bg-white p-3">
<legend>{{ __('validation.attributes.end_diffusion_date') }}</legend>
<div class="row">
<div class="col-12 mb-3">
@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')),
]),
]),
])
</div>
</div>
</fieldset>
</div>
{{-- END VALIDITY DATE --}}
<div class="col-12 col-md-6 mb-3">
<fieldset class="h-100 border bg-white p-3">
<legend>{{ __('validation.attributes.end_validity_date') }}</legend>
<div class="row">
<div class="col-12 mb-3">
@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')),
]),
]),
])
</div>
</div>
</fieldset>
</div>
{{-- STATUS --}}
<div class="col-12 col-md-6 mb-3">
<fieldset class="h-100 border bg-white p-3">
<legend>{{ __('validation.attributes.status') }}</legend>
<div class="row">
<div class="col-12 mb-3">
@include('bo.partials.inputs.tinyint-boolean', [
'id' => 'couponStatus',
'name' => 'status',
'label' => __('crud.minicrud.visibility_label'),
'helper' => __('crud.minicrud.visibility_helper'),
'model' => $couponModel ?? null,
])
</div>
</div>
</fieldset>
</div>

View file

@ -0,0 +1,87 @@
<div class="table-responsive">
<table class="fix-action m-0 table">
<thead>
@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']])
</thead>
<tbody>
@if ($couponModels->count())
@foreach ($couponModels as $couponModel)
<tr>
<td class="align-middle">{{ $couponModel->name }}</td>
<td class="text-center align-middle">{{ $couponModel->brand_id }}</td>
@include('bo.modules.change-status', [
'routeName' => 'coupons',
'model' => $couponModel,
])
<td class="text-center align-middle">{{ $couponModel->process_code }}</td>
<td class="text-center align-middle">{{ $couponModel->amount }}€</td>
<td class="align-middle">{{ $couponModel->valid_on }}</td>
<td class="text-center align-middle">{{ $couponModel->validity_period }} {{ __('jours') }}</td>
<td class="align-middle">{{ $couponModel->legal_notices }}</td>
<td class="align-middle">{{ $couponModel->decoration }}</td>
<td class="align-middle">{{ $couponModel->gs1 }}</td>
<td class="align-middle">{{ $couponModel->website_picture }}</td>
<td class="align-middle">{{ $couponModel->website_picture_mobile }}</td>
<td class="text-center align-middle">{{ $couponModel->limit_qty }}</td>
<td class="text-center align-middle">{{ $couponModel->limit_period }} {{ __('jours') }}</td>
<td class="text-center align-middle">{{ $couponModel->can_be_used }}</td>
<td class="text-center align-middle">{{ $couponModel->start_diffusion_date }}</td>
<td class="text-center align-middle">{{ $couponModel->end_diffusion_date }}</td>
<td class="text-center align-middle">{{ $couponModel->end_validity_date }}</td>
<td class="text-center align-middle">
<span
class="badge bg-warning text-dark text-wrap">{{ $couponModel->updated_at->formatLocalized('%d/%m/%Y à %H:%M') }}</span>
</td>
<td class="align-middle">
<form class="btn-group confirmDeleteJS" action="{{ route('bo.coupons.destroy', $couponModel->id) }}"
method="POST" novalidate>
<a class="btn btn-sm btn-primary" data-bs-toggle="tooltip"
href="{{ route('bo.coupons.edit', $couponModel->id) }}"
title="{{ __('crud.helpers.update_model', ['model' => Str::singular(__('models.classes.coupons'))]) }}">
<i class="fa-solid fa-pencil"></i>
</a>
@method('DELETE')
@csrf
<div class="btn-group" role="group">
<button class="btn btn-sm btn-danger" data-bs-toggle="tooltip"
title="{{ __('crud.helpers.delete_model', ['model' => Str::singular(__('models.classes.coupons'))]) }}">
<i class="fa-solid fa-trash"></i>
</button>
</div>
</form>
</td>
</tr>
@endforeach
@else
<tr>
<td class="text-center" colspan="9">
<span class="h3">{{ __('crud.helpers.no_results', ['model' => __('models.classes.coupons')]) }}</span>
</td>
</tr>
@endif
</tbody>
</table>
</div>

View file

@ -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')
<div class="container">
<div class="row">
<div class="col">
<div class="card">
<div class="card-header">
<div class="row">
<div class="col">
<h1 class="h2 d-inline"><span class="fw-bold">{{ __('models.classes.coupons') }}</span>&nbsp;<small class="text-muted">{{ Str::ucfirst(__('crud.helpers.list')) }}</small>&nbsp;&nbsp;</h1>
</div>
<div class="col-md-3 d-flex align-items-center">
<div class="input-group justify-content-end">
<a href="{{ route('bo.coupons.create') }}" class="btn btn-primary" title="{{ __('crud.helpers.create_model', ['model' => Str::singular(__('models.classes.coupons'))]) }}" data-bs-toggle="tooltip">
<i class="fa-solid fa-plus"></i>
</a>
</div>
</div>
</div>
</div>
<div class="card-body">
@include('bo.modules.search-bar')
<div class="row justify-content-center">
<div class="col-12 col-md-6 p-0">
{{ $couponModels->withQueryString()->onEachSide(1)->links() }}
</div>
</div>
<div class="row">
<div class="col">
@include('bo.pages.coupons.index-list')
</div>
</div>
<div class="row justify-content-center">
<div class="col-12 col-md-6 p-0">
{{ $couponModels->withQueryString()->onEachSide(1)->links() }}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@endsection

View file

@ -0,0 +1,19 @@
<label for="{{ $id }}">
<b>{{ $label }}</b>
</label>
{{-- blade-formatter-disable --}}
<input
aria-describedby="{{ "{$id}Help" }}"
id="{{ $id }}"
class="form-control"
name="{{ $name }}"
type="date"
data-form-type="date"
value="{{ old($name, ((isset($model) and $model->{$name}) ? ($model->{$name}->format('Y-m-d') ?? '') : '')) }}"
@if(!empty($disabled)) disabled @endif>
{{-- blade-formatter-enable --}}
@include('modules.input-error', [
'inputName' => $name,
'helperId' => "{$id}Help",
'helper' => $helper,
])

View file

@ -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');