Compare commits

..

69 commits

Author SHA1 Message Date
Vincent BRIDIER
317577fb9b Merge branch 'issue-49' into 'develop'
Comments are now duplicated with products

See merge request ldc-web/projets/startcoq!37
2023-10-20 14:47:00 +00:00
Vincent BRIDIER
2a2490b302 Merge branch 'issue-58' into 'develop'
Inverted dates fields in filters

See merge request ldc-web/projets/startcoq!36
2023-10-20 14:46:25 +00:00
Vincent BRIDIER
53524b96d0 Merge branch 'issue-56' into 'develop'
Source details aren't required anymore

See merge request ldc-web/projets/startcoq!35
2023-10-20 14:45:46 +00:00
Vincent BRIDIER
e7fe9d920b Merge branch 'issue-55' into 'develop'
Gencod check is now called at every input change

See merge request ldc-web/projets/startcoq!33
2023-10-20 14:28:57 +00:00
Vincent BRIDIER
6e678b88c9 Merge branch 'issue-57' into 'develop'
Fixed BelongsToDropdown component: crashed when there was no value

See merge request ldc-web/projets/startcoq!32
2023-10-20 14:27:31 +00:00
Vincent BRIDIER
9c5cef6d99 Merge branch 'issue-51' into 'develop'
Comments are now open in edition form

See merge request ldc-web/projets/startcoq!31
2023-10-20 14:26:17 +00:00
noah.lancien
0ab895e7ac Comments are now open in edition form 2023-10-20 14:26:16 +00:00
Vincent BRIDIER
06301a44fa Merge branch 'issue-48' into 'develop'
Removed hours for seen_at displaying in pdf

See merge request ldc-web/projets/startcoq!30
2023-10-20 14:25:41 +00:00
Vincent BRIDIER
4274b21eea Merge branch 'issue-53' into 'develop'
Displayed specified claims in pdf and fixed specified claims update

See merge request ldc-web/projets/startcoq!29
2023-10-20 14:25:17 +00:00
Vincent BRIDIER
dec444160b Merge branch 'issue-46' into 'develop'
Added the weight unit in pdf

See merge request ldc-web/projets/startcoq!28
2023-10-20 14:24:33 +00:00
Vincent BRIDIER
4dfa2d0412 Merge branch 'issue-44' into 'develop'
Certifications are now visible in pdf

See merge request ldc-web/projets/startcoq!27
2023-10-20 14:24:01 +00:00
Vincent BRIDIER
a230697de0 Merge branch 'issue-32-allow-images-in-documents' into 'develop'
Issue 32 allow images in documents

Closes #32

See merge request ldc-web/projets/startcoq!24
2023-10-20 14:23:22 +00:00
Noah Lancien
2a9e109855 Comments are now duplicated with products 2023-10-17 10:12:05 +02:00
Noah Lancien
e19964fc89 Inverted dates fields in filters 2023-10-03 09:23:46 +02:00
Noah Lancien
edad0b4279 Source details aren't required anymore 2023-10-02 16:15:08 +00:00
Jérémy Munsch
d70135b6af
Migration fixes 2023-10-02 16:00:50 +02:00
Jérémy Munsch
adae4da02f
Fix enums valus with DB 2023-10-02 15:38:56 +02:00
Noah Lancien
2f7289f531 Gencod check is now called at every input change 2023-10-02 15:00:01 +02:00
Noah Lancien
6b85d63f5e Fixed BelongsToDropdown component: crashed when there was no value 2023-10-02 14:28:16 +02:00
Noah Lancien
5c5b34f33e Removed hours for seen_at displaying in pdf 2023-10-02 12:49:44 +02:00
Noah Lancien
ca47520a74 Displayed specified claims in pdf and fixed specified claims update 2023-10-02 12:34:49 +02:00
Noah Lancien
af3a63e36f Added the weight unit in pdf 2023-10-02 09:24:59 +02:00
Noah Lancien
21cc60457d Certifications are now visible in pdf 2023-10-02 09:20:59 +02:00
Vincent BRIDIER
36703bc9ac Merge branch 'issue-52-comments-placeholder' into 'develop'
Removed placeholders for comments

See merge request ldc-web/projets/startcoq!26
2023-09-29 16:22:16 +00:00
Noah Lancien
8a99cf98fc Removed placeholders for comments 2023-09-29 18:10:30 +02:00
Vincent BRIDIER
a4ed3dbb0f Merge branch 'issue-59' into 'develop'
Products are now ordered by 'created_at' desc

Closes #59

See merge request ldc-web/projets/startcoq!25
2023-09-29 15:59:50 +00:00
Noah Lancien
dae0c6271a Products are now ordered by 'created_at' desc 2023-09-29 17:57:09 +02:00
Jérémy Munsch
98762d68a8
Fix date sort on products 2023-09-18 11:55:53 +02:00
Jérémy Munsch
c450d71bed
Set max file sizes on products 2023-09-11 17:18:29 +02:00
Jérémy Munsch
99f7545595
Closes #32 Multiple file inputs for all doc types 2023-09-11 16:55:46 +02:00
Jérémy Munsch
a0ddeda5a9
pagination upgrade 2023-09-11 15:07:25 +02:00
Jérémy Munsch
771978412a
Fix keyword seeder (silent fail on Required keywords) 2023-09-11 12:28:44 +02:00
Jérémy Munsch
f43ea7c9ff
phpcs fixes 2023-09-11 12:22:32 +02:00
Jérémy Munsch
c88583d727
Fix printMultiProductSheet
making 2 sheets for the firs product item
2023-09-11 11:28:07 +02:00
Jérémy Munsch
6aab7d4eff
Fix product resource with comments 2023-09-11 11:23:42 +02:00
Jérémy Munsch
0681bf8f0a
Oci migration Products comments 2023-09-11 11:08:19 +02:00
Jérémy Munsch
923c3e0f6a
oci_migration fix wrong keywords associated with products 2023-09-11 09:57:57 +02:00
Jérémy Munsch
8e48fc5ea1
Fix product Filter
Fixes #39
Fixes #35
Fixes #36
2023-09-08 17:38:56 +02:00
Jérémy Munsch
615263f233
Refactor and fixes
Fixes #33
Fixes #34
2023-09-08 16:02:21 +02:00
Jérémy Munsch
16f4af41fd
Fix product-body division by ero
when product->weight is 0
2023-09-08 13:29:25 +02:00
Jérémy Munsch
d51f78a8db
Fix for ProductResource resource 2023-09-08 13:27:32 +02:00
Jérémy Munsch
25b2e42f14
Merge branch 'migration' into develop 2023-09-08 12:06:59 +02:00
Jérémy Munsch
b9f51a1a5d
Products form move tendance au dessus de commentaire 2023-09-06 18:29:35 +02:00
Jérémy Munsch
e381de5e9c
Closes #28
Fix oci migration picture on products
2023-09-06 18:19:22 +02:00
Jérémy Munsch
042e78583e
add image resizer 2023-09-06 18:19:22 +02:00
Jérémy Munsch
e6796029ec
oci_migrate set picture as nullable 2023-09-06 18:19:22 +02:00
Jérémy Munsch
39fc13dfe2
re set oci_migration elements 2023-09-06 18:19:21 +02:00
Jérémy Munsch
686bf307a2
Push down migrate_oci_table order 2023-09-06 18:19:21 +02:00
Jérémy Munsch
549f874abd
Set validation image as non required as it should be 2023-09-06 18:19:21 +02:00
Noah Lancien
17ac0aaa5c
Fixed Products and Brands factories 2023-09-06 18:19:21 +02:00
Noah Lancien
f5367815e1
Fixed product brand field in the form 2023-09-06 18:19:20 +02:00
Jérémy Munsch
0d1f135616
lint on product Controller 2023-09-06 18:19:20 +02:00
Jérémy Munsch
817ffa68fc
Product migration should be ok 2023-09-06 18:19:20 +02:00
Jérémy Munsch
a31512216c
Change Enum Components dans enum styore on products to what it should be (array on integers) 2023-09-06 18:19:20 +02:00
Jérémy Munsch
cc24e547d2
Fix SelectMultipleEnumComponent as ti should be (generate array of integers) 2023-09-06 18:19:20 +02:00
Noah Lancien
16fe3773a9
Added brand creation in product form, fixed missing * when required, cleaning 2023-09-06 18:19:19 +02:00
Jérémy Munsch
8ce9d5734b
Vue component add name on components 2023-09-06 18:19:19 +02:00
Jérémy Munsch
6de828bc31
Fix product old file delete 2023-09-06 18:19:19 +02:00
Jérémy Munsch
fe3eb7a955
laravel lang install 2023-09-06 18:19:18 +02:00
Jérémy Munsch
c0bce88e75
Overhaul pagination 2023-09-06 18:19:18 +02:00
Jérémy Munsch
61e900ee32
Fixes #27 ProductController fix filter code 2023-09-06 18:19:14 +02:00
Jérémy Munsch
f44d9d7849
upgrade vite config 2023-09-06 14:26:19 +02:00
Jérémy Munsch
ecaec2823f
color setup 2023-09-06 14:26:19 +02:00
Jérémy Munsch
22b598a11a
console output fix 2023-09-06 14:26:18 +02:00
Jérémy Munsch
d9c132ad25
Fix comp alleg 2023-09-06 14:26:18 +02:00
Jérémy Munsch
634dbe832e
Products and Brands Migration OK except for products assets
Waiting for
https://lab.s-web.fr/ldc-web/projets/startcoq/-/issues/28#note_9782
2023-09-06 14:26:18 +02:00
Jérémy Munsch
f539cdd99a
add drop temp table system 2023-09-06 14:23:18 +02:00
Jérémy Munsch
52dda4e346
Migration automatique de table START_C 2023-09-06 14:23:18 +02:00
Noah Lancien
8a3058e7bc Fixed some requests from the provided excel document 2023-09-05 15:28:55 +02:00
92 changed files with 3974 additions and 1498 deletions

22
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,22 @@
{
"workbench.colorCustomizations": {
"activityBar.activeBackground": "#90431a",
"activityBar.background": "#90431a",
"activityBar.foreground": "#e7e7e7",
"activityBar.inactiveForeground": "#e7e7e799",
"activityBarBadge.background": "#1da24b",
"activityBarBadge.foreground": "#e7e7e7",
"commandCenter.border": "#e7e7e799",
"sash.hoverBorder": "#90431a",
"statusBar.background": "#652f12",
"statusBar.foreground": "#e7e7e7",
"statusBarItem.hoverBackground": "#90431a",
"statusBarItem.remoteBackground": "#652f12",
"statusBarItem.remoteForeground": "#e7e7e7",
"titleBar.activeBackground": "#652f12",
"titleBar.activeForeground": "#e7e7e7",
"titleBar.inactiveBackground": "#652f1299",
"titleBar.inactiveForeground": "#e7e7e799"
},
"peacock.remoteColor": "#652f12"
}

View file

@ -9,14 +9,18 @@ class Kernel extends ConsoleKernel
{ {
/** /**
* Define the application's command schedule. * Define the application's command schedule.
*
* @param \Illuminate\Console\Scheduling\Schedule $schedule
* @return void
*/ */
protected function schedule(Schedule $schedule): void protected function schedule(Schedule $schedule): void
{ {
// $schedule->command('inspire')->hourly();
} }
/** /**
* Register the commands for the application. * Register the commands for the application.
*
* @return void
*/ */
protected function commands(): void protected function commands(): void
{ {

View file

@ -0,0 +1,16 @@
<?php
namespace App\Enums\Pagination;
use App\Traits\Enums\BaseEnum;
enum ItemsPerPaginationEnum: int
{
use BaseEnum;
case five = 5;
case twelve = 12;
case twentyfive = 25;
case fifty = 50;
case onehundred = 100;
}

View file

@ -4,7 +4,7 @@ namespace App\Enums\Products;
use App\Traits\Enums\BaseEnum; use App\Traits\Enums\BaseEnum;
enum CertificationEnum : int enum CertificationEnum: int
{ {
use BaseEnum; use BaseEnum;
@ -12,8 +12,8 @@ enum CertificationEnum : int
case labelRouge = 2; case labelRouge = 2;
case certifiedCQ = 3; case certifiedCQ = 3;
case standard = 4; case standard = 4;
case freeRange = 5; case freeRange = 6;
case others = 6; case others = 5;
/** /**
* Custom added functions definition * Custom added functions definition

View file

@ -4,7 +4,7 @@ namespace App\Enums\Products;
use App\Traits\Enums\BaseEnum; use App\Traits\Enums\BaseEnum;
enum ClaimEnum : int enum ClaimEnum: int
{ {
use BaseEnum; use BaseEnum;

View file

@ -4,7 +4,7 @@ namespace App\Enums\Products;
use App\Traits\Enums\BaseEnum; use App\Traits\Enums\BaseEnum;
enum DistributionNetworkEnum : int enum DistributionNetworkEnum: int
{ {
use BaseEnum; use BaseEnum;

View file

@ -4,7 +4,7 @@ namespace App\Enums\Products;
use App\Traits\Enums\BaseEnum; use App\Traits\Enums\BaseEnum;
enum GeoAreaEnum : int enum GeoAreaEnum: int
{ {
use BaseEnum; use BaseEnum;

View file

@ -4,7 +4,7 @@ namespace App\Enums\Products;
use App\Traits\Enums\BaseEnum; use App\Traits\Enums\BaseEnum;
enum OriginEnum : int enum OriginEnum: int
{ {
use BaseEnum; use BaseEnum;

View file

@ -4,7 +4,7 @@ namespace App\Enums\Products;
use App\Traits\Enums\BaseEnum; use App\Traits\Enums\BaseEnum;
enum RangeEnum : int enum RangeEnum: int
{ {
use BaseEnum; use BaseEnum;
@ -18,9 +18,9 @@ enum RangeEnum : int
case rawMeat = 8; case rawMeat = 8;
case cookedMeats = 9; case cookedMeats = 9;
case farcis = 10; case farcis = 10;
case plant = 11; case plant = 12;
case frozenSpecific = 12; case frozenSpecific = 13;
case others = 13; case others = 11;
/** /**
* Custom added functions definition * Custom added functions definition

View file

@ -4,7 +4,7 @@ namespace App\Enums\Products;
use App\Traits\Enums\BaseEnum; use App\Traits\Enums\BaseEnum;
enum RayEnum : int enum RayEnum: int
{ {
use BaseEnum; use BaseEnum;

View file

@ -4,17 +4,24 @@ namespace App\Enums\Products;
use App\Traits\Enums\BaseEnum; use App\Traits\Enums\BaseEnum;
enum SourceEnum : int enum SourceEnum: int
{ {
use BaseEnum; use BaseEnum;
case catalogArgumentary = 1; // * Catalogue argumentaire
case magazin = 2; case catalogArgumentary = 7;
case press = 3; // * Magasin
case showConsumer = 4; case magazin = 1;
case showCustomer = 5; // * Presse
case showTrade = 6; case press = 3;
case web = 7; // * Salon Grand Public
case showConsumer = 6;
// * Salon Clients
case showCustomer = 5;
// * Salon Professionnel
case showTrade = 4;
// * Web
case web = 2;
/** /**
* Custom added functions definition * Custom added functions definition

View file

@ -12,16 +12,14 @@ class Handler extends ExceptionHandler
* *
* @var array<class-string<\Throwable>, \Psr\Log\LogLevel::*> * @var array<class-string<\Throwable>, \Psr\Log\LogLevel::*>
*/ */
protected $levels = [ protected $levels = [];
];
/** /**
* A list of the exception types that are not reported. * A list of the exception types that are not reported.
* *
* @var array<int, class-string<\Throwable>> * @var array<int, class-string<\Throwable>>
*/ */
protected $dontReport = [ protected $dontReport = [];
];
/** /**
* A list of the inputs that are never flashed to the session on validation exceptions. * A list of the inputs that are never flashed to the session on validation exceptions.
@ -36,6 +34,8 @@ class Handler extends ExceptionHandler
/** /**
* Register the exception handling callbacks for the application. * Register the exception handling callbacks for the application.
*
* @return void
*/ */
public function register(): void public function register(): void
{ {

View file

@ -0,0 +1,29 @@
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Http\Requests\Brands\StoreBrandRequest;
use App\Models\Brand;
use Illuminate\Support\Facades\DB;
class BrandController extends Controller
{
/**
* Store a newly created resource in storage.
*
* @param \App\Http\Requests\Brands\StoreBrandRequest $request
* @return \Illuminate\Http\JsonResponse
*/
public function store(StoreBrandRequest $request)
{
return DB::transaction(function () use ($request) {
$brand = new Brand();
$brand->fill($request->validated());
if ($brand->saveOrFail()) {
return \response()->json(['status' => true, 'brand' => $brand]);
}
return \response()->json(['status' => false, 'message' => __('La marque n\'a pas pu être créée')]);
});
}
}

View file

@ -2,13 +2,18 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Enums\Pagination\ItemsPerPaginationEnum;
use App\Lib\Helpers\ToolboxHelper;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Bus\DispatchesJobs; use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests; use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController; use Illuminate\Routing\Controller as BaseController;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Session; use Illuminate\Support\Facades\Session;
use Illuminate\Support\Str;
class Controller extends BaseController class Controller extends BaseController
{ {
@ -122,4 +127,26 @@ class Controller extends BaseController
} }
return $query; return $query;
} }
/**
* Customize pagination with cache or config (default).
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param \App\Enums\Pagination\ItemsPerPaginationEnum|null $pagination
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
*/
protected function paginate(Builder $query, ItemsPerPaginationEnum $pagination = null): LengthAwarePaginator
{
$currentRoutePath = Str::slug(request()->route()->getName());
$cacheKey = "pagination.{$currentRoutePath}";
$pagination = $pagination ?? ItemsPerPaginationEnum::twelve->value;
$pagination = ToolboxHelper::getValidatedPaginate(
Cache::get($cacheKey, $pagination)
);
if (Cache::get($cacheKey) !== $pagination) {
Cache::put($cacheKey, $pagination);
}
$query = $query->paginate($pagination);
return $query;
}
} }

View file

@ -3,6 +3,7 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\Products\IndexFilterRequest;
use App\Http\Requests\Products\StoreProductRequest; use App\Http\Requests\Products\StoreProductRequest;
use App\Http\Requests\Products\UpdateProductRequest; use App\Http\Requests\Products\UpdateProductRequest;
use App\Jobs\ExportProduct; use App\Jobs\ExportProduct;
@ -12,6 +13,7 @@ use App\Models\Product;
use App\Traits\Controllers\HasDocuments; use App\Traits\Controllers\HasDocuments;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str; use Illuminate\Support\Str;
@ -22,99 +24,54 @@ class ProductController extends Controller
/** /**
* Display a listing of the resource. * Display a listing of the resource.
* *
* @param \Illuminate\Http\Request $request * @param \App\Http\Requests\Products\IndexFilterRequest $request
* @return \Illuminate\Contracts\View\View * @return \Illuminate\Contracts\View\View
*/ */
public function index(Request $request) public function index(IndexFilterRequest $request)
{ {
/** @var \Illuminate\Database\Eloquent\Builder $brands */ /** @var \Illuminate\Database\Eloquent\Builder $brands */
$brands = Brand::get(); $brandsQuery = Brand::query();
/** @var \Illuminate\Database\Eloquent\Builder $products */ /** @var \Illuminate\Database\Eloquent\Builder $productsQuery */
$products = Product::query(); $productsQuery = Product::query()->orderBy('created_at', 'desc');
$this->requestFilters($products, $request); $this->requestFilters($request, $productsQuery);
$this->sortQuery($products); $this->sortQuery($productsQuery);
$products = $products->paginate(10); $products = $this->paginate($productsQuery);
/** @var array $printMultiProduct Array of id */ /** @var array $printMultiProduct Array of id */
$printMultiProduct = session('printMultiProduct') ?? []; $printMultiProduct = session('printMultiProduct') ?? [];
/** @var \Illuminate\Database\Eloquent\Builder $brands */ $brands = $brandsQuery->get();
$brands = Brand::get();
/** @var \Illuminate\Database\Eloquent\Builder $keyword */ $keywords = Keyword::query()->where('keyword_cat', 'LIKE', 'product_%')->get();
$keywords = Keyword::where('keyword_cat', 'LIKE', 'product_%')->get();
return view('pages.products.index', [ return view('pages.products.index', [
'products' => $products, 'products' => $products,
'brands' => $brands, 'brands' => $brands,
'keywords' => $keywords, 'keywords' => $keywords,
'printMultiProduct' => $printMultiProduct, 'printMultiProduct' => $printMultiProduct
'queryParameters' => $request->all(),
]); ]);
} }
/**
* Show the form for creating a new resource.
*
* @param \Illuminate\Database\Eloquent\Builder $products
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Contracts\View\View
* @phpcs:disable Generic.Metrics.NestingLevel.TooHigh
*/
private function requestFilters(Builder $products, Request $request): \Illuminate\Database\Eloquent\Builder
{
//phpcs:enable
foreach ($request->all() as $key => $queryParameter) {
if (request()->sort_col === null && request()->sort_way === null && request()->rst === null) {
$products->when(
isset($queryParameter) && $queryParameter != 0,
function ($q) use ($queryParameter, $key) {
if ($key === "name") {
$q->where($key, 'LIKE', '%' . $queryParameter . '%');
} elseif ($key === "claims" || $key === "origins") {
foreach ($queryParameter as $item) {
$q->where($key, 'LIKE', '%{"id":"' . $item['value'] . '"}%');
}
} elseif ($key === "keywords") {
foreach ($queryParameter as $keyword) {
$q->whereHas('productKeywords', function ($q) use ($keyword, $key) {
$q->where($key . '.id', intval($keyword['value']));
});
}
} elseif ($key === "date_before" || $key === "date_after") {
if ($key === "date_before") {
$q->whereDate('created_at', '>=', $queryParameter);
} else {
$q->whereDate('created_at', '<=', $queryParameter);
}
} else {
$q->where($key, $queryParameter);
} //end if
}
);
} //end if
} //end foreach
return $products;
}
/** /**
* Show the form for creating a new resource. * Show the form for creating a new resource.
* *
* @param App\Models\Product $productModel * @param App\Models\Product $productModel
* @param Illuminate\Support\Collection $commentModel
* @return \Illuminate\Contracts\View\View * @return \Illuminate\Contracts\View\View
* phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClass * phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClass
*/ */
public function create(Product $productModel): \Illuminate\Contracts\View\View public function create(Product $productModel, Collection $commentModel): \Illuminate\Contracts\View\View
{ {
//phpcs:enable //phpcs:enable
$brands = Brand::get(); $brands = Brand::all();
$productsFair = Product::pluck('fair'); $productsFair = Product::query()->pluck('fair');
$productsShop = Product::pluck('shop'); $productsShop = Product::query()->pluck('shop');
$productsCatalog = Product::pluck('catalogue'); $productsCatalog = Product::query()->pluck('catalogue');
$keywords = Keyword::where('keyword_cat', 'product_pleasure') $keywords = Keyword::query()->where('keyword_cat', 'product_pleasure')
->orWhere('keyword_cat', 'product_shape') ->orWhere('keyword_cat', 'product_shape')
->orWhere('keyword_cat', 'product_practicality') ->orWhere('keyword_cat', 'product_practicality')
->orWhere('keyword_cat', 'product_health') ->orWhere('keyword_cat', 'product_health')
@ -127,7 +84,8 @@ class ProductController extends Controller
'productsFair', 'productsFair',
'productsShop', 'productsShop',
'productsCatalog', 'productsCatalog',
'keywords' 'keywords',
'commentModel'
)); ));
} }
@ -170,12 +128,12 @@ class ProductController extends Controller
*/ */
public function edit(Product $product) public function edit(Product $product)
{ {
$brands = Brand::get(); $brands = Brand::all();
$productsFair = Product::pluck('fair'); $productsFair = Product::query()->pluck('fair');
$productsShop = Product::pluck('shop'); $productsShop = Product::query()->pluck('shop');
$productsCatalog = Product::pluck('catalogue'); $productsCatalog = Product::query()->pluck('catalogue');
$commentModel = $product->productComments; $commentModel = $product->productComments;
$keywords = Keyword::where('keyword_cat', 'product_pleasure') $keywords = Keyword::query()->where('keyword_cat', 'product_pleasure')
->orWhere('keyword_cat', 'product_shape') ->orWhere('keyword_cat', 'product_shape')
->orWhere('keyword_cat', 'product_practicality') ->orWhere('keyword_cat', 'product_practicality')
->orWhere('keyword_cat', 'product_health') ->orWhere('keyword_cat', 'product_health')
@ -300,7 +258,11 @@ class ProductController extends Controller
*/ */
public function duplicate(Product $product) public function duplicate(Product $product)
{ {
return $this->create($product->replicate()); $duplicatedProduct = $product->replicate();
foreach ($product->productComments as $comment) {
$duplicatedComments[] = $comment->replicate();
}
return $this->create($duplicatedProduct, collect($duplicatedComments ?? null));
} }
/** /**
@ -312,20 +274,20 @@ class ProductController extends Controller
public function checkGencode(Request $request) public function checkGencode(Request $request)
{ {
if ( if (
isset($request->GENCODE) && isset($request->GENCOD) &&
$request->GENCODE !== null && $request->GENCOD !== null &&
ctype_digit($request->GENCODE) && ctype_digit($request->GENCOD) &&
strlen($request->GENCODE) > 0 strlen($request->GENCOD) > 0
) { ) {
if ( if (
isset($request->ID) && isset($request->ID) &&
$request->ID !== null && $request->ID !== null &&
intval($request->GENCODE) === Product::where('id', $request->ID)->firstOrFail()->gencod intval($request->GENCOD) === Product::where('id', $request->ID)->firstOrFail()->gencod
) { ) {
$result = null; $result = null;
} else { } else {
$result = in_array(intval($request->GENCODE), Product::pluck('gencod')->toArray()) ? $result = in_array(intval($request->GENCOD), Product::pluck('gencod')->toArray()) ?
Product::orderBy('created_at', 'desc')->where('gencod', intval($request->GENCODE))->first() : null; Product::orderBy('created_at', 'desc')->where('gencod', intval($request->GENCOD))->first() : null;
} }
} else { } else {
$result = null; $result = null;
@ -363,6 +325,7 @@ class ProductController extends Controller
*/ */
public function printMultiProductSheet() public function printMultiProductSheet()
{ {
// FIXME: This should not use session.
/** @var array printMultiProduct */ /** @var array printMultiProduct */
$printMultiProduct = session('printMultiProduct') ?? []; $printMultiProduct = session('printMultiProduct') ?? [];
asort($printMultiProduct); asort($printMultiProduct);
@ -427,4 +390,62 @@ class ProductController extends Controller
{ {
\dispatch(new ExportProduct($request->id))->onConnection('database'); \dispatch(new ExportProduct($request->id))->onConnection('database');
} }
/**
* Applies filters from products.index
*
* @param \App\Http\Requests\Products\IndexFilterRequest $request
* @param \Illuminate\Database\Eloquent\Builder $productsQuery
* @return \Illuminate\Contracts\View\View
* @phpcs:disable Generic.Metrics.NestingLevel.TooHigh
*/
private function requestFilters(
IndexFilterRequest $request,
Builder $productsQuery
): \Illuminate\Database\Eloquent\Builder {
//phpcs:enable
foreach ($request->validated() as $parameterName => $queryParameter) {
$productsQuery->when(
!empty($queryParameter),
function (Builder $q) use ($queryParameter, $parameterName) {
switch ($parameterName) {
// * String type parameters (Approx search)
case 'name':
case 'shop':
case 'fair':
case 'catalogue':
case 'specified_claim':
$q->where($parameterName, 'LIKE', "%{$queryParameter}%");
break;
// * Number type parameters (Approx search)
case 'gencode':
$q->where($parameterName, \intval($queryParameter));
break;
// * Date type parameters
case 'date_before':
$q->whereDate('created_at', '<=', $queryParameter);
break;
case 'date_after':
$q->whereDate('created_at', '>=', $queryParameter);
break;
// * Enums Array
case 'claims':
case 'origins':
$q->whereJsonContains($parameterName, \collect($queryParameter)
->map(fn ($enumValue) => \intval($enumValue)));
break;
// * RelationShips parameters
case 'keywords':
$q->whereHas('productKeywords', function (Builder $subQ) use ($queryParameter) {
$subQ->whereIn('keywords.id', $queryParameter);
});
break;
default:
$q->where($parameterName, $queryParameter);
} //end switch
}
);
} //end foreach
return $productsQuery;
}
} }

View file

@ -14,7 +14,7 @@ class Kernel extends HttpKernel
* @var array<int, class-string|string> * @var array<int, class-string|string>
*/ */
protected $middleware = [ protected $middleware = [
// \App\Http\Middleware\TrustHosts::class, \App\Http\Middleware\TrustHosts::class,
\App\Http\Middleware\TrustProxies::class, \App\Http\Middleware\TrustProxies::class,
\Illuminate\Http\Middleware\HandleCors::class, \Illuminate\Http\Middleware\HandleCors::class,
\App\Http\Middleware\PreventRequestsDuringMaintenance::class, \App\Http\Middleware\PreventRequestsDuringMaintenance::class,

View file

@ -9,6 +9,9 @@ class Authenticate extends Middleware
{ {
/** /**
* Get the path the user should be redirected to when they are not authenticated. * Get the path the user should be redirected to when they are not authenticated.
*
* @param \Illuminate\Http\Request $request
* @return string|null
*/ */
protected function redirectTo(Request $request): ?string protected function redirectTo(Request $request): ?string
{ {

View file

@ -3,7 +3,6 @@
namespace App\Http\Middleware; namespace App\Http\Middleware;
use App\Providers\RouteServiceProvider; use App\Providers\RouteServiceProvider;
use Closure;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
@ -13,10 +12,15 @@ class RedirectIfAuthenticated
/** /**
* Handle an incoming request. * Handle an incoming request.
* *
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next * @param \Illuminate\Http\Request $request
* @param \Closure(\Illuminate\Http\Request):(\Symfony\Component\HttpFoundation\Response) $next
* @param string ...$guards
* @return \Symfony\Component\HttpFoundation\Response
* @phpcs:disable Squiz.Commenting.FunctionComment.IncorrectTypeHint
*/ */
public function handle(Request $request, Closure $next, string ...$guards): Response public function handle(Request $request, \Closure $next, string ...$guards): Response
{ {
// phpcs:enable
$guards = empty($guards) ? [null] : $guards; $guards = empty($guards) ? [null] : $guards;
foreach ($guards as $guard) { foreach ($guards as $guard) {

View file

@ -10,13 +10,13 @@ class ValidateSignature extends Middleware
* The names of the query string parameters that should be ignored. * The names of the query string parameters that should be ignored.
* *
* @var array<int, string> * @var array<int, string>
*
* 'fbclid',
* 'utm_campaign',
* 'utm_content',
* 'utm_medium',
* 'utm_source',
* 'utm_term',
*/ */
protected $except = [ protected $except = [];
// 'fbclid',
// 'utm_campaign',
// 'utm_content',
// 'utm_medium',
// 'utm_source',
// 'utm_term',
];
} }

View file

@ -0,0 +1,42 @@
<?php
namespace App\Http\Requests\Brands;
use Illuminate\Foundation\Http\FormRequest;
class StoreBrandRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return boolean
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules(): array
{
return [
'name' => 'required|string|min:3|max:255',
];
}
/**
* Get custom attributes for validator errors.
*
* @return array
*/
public function attributes(): array
{
return [
'name' => trans('nom de la marque'),
];
}
}

View file

@ -0,0 +1,60 @@
<?php
namespace App\Http\Requests\Products;
use App\Enums\Products\CertificationEnum;
use App\Enums\Products\ClaimEnum;
use App\Enums\Products\DistributionNetworkEnum;
use App\Enums\Products\GeoAreaEnum;
use App\Enums\Products\OriginEnum;
use App\Enums\Products\RangeEnum;
use App\Enums\Products\RayEnum;
use App\Enums\Products\SourceEnum;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rules\Enum;
class IndexFilterRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return boolean
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array|string>
*/
public function rules(): array
{
return [
'name' => 'nullable|string',
'brand_id' => 'nullable|exists:brands,id',
'gencod' => 'nullable|string',
'source' => ['nullable', new Enum(SourceEnum::class)],
'shop' => 'nullable|string',
'fair' => 'nullable|string',
'catalogue' => 'nullable|string',
'claims' => 'nullable|array',
'claims.*' => ['required', new Enum(ClaimEnum::class)],
'specified_claim' => 'nullable|string',
'origins' => 'nullable|array',
'origins.*' => ['required', new Enum(OriginEnum::class)],
'certification' => ['nullable', new Enum(CertificationEnum::class)],
'distribution_network' => ['nullable', new Enum(DistributionNetworkEnum::class)],
'ray' => ['nullable', new Enum(RayEnum::class)],
'product_range' => ['nullable', new Enum(RangeEnum::class)],
'geo_area' => ['nullable', new Enum(GeoAreaEnum::class)],
'keywords' => 'nullable|array',
'keywords.*' => 'required|exists:keywords,id',
'date_before' => 'nullable|date:Y-m-d',
'date_after' => 'nullable|date:Y-m-d',
];
}
}

View file

@ -11,7 +11,7 @@ use App\Enums\Products\RangeEnum;
use App\Enums\Products\RayEnum; use App\Enums\Products\RayEnum;
use App\Enums\Products\SourceEnum; use App\Enums\Products\SourceEnum;
use App\Lib\Helpers\ToolboxHelper; use App\Lib\Helpers\ToolboxHelper;
use App\Traits\Requests\HasDocument; use App\Traits\Requests\HasDocumentAndPictureList;
use App\Traits\Requests\HasPicture; use App\Traits\Requests\HasPicture;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rules\Enum; use Illuminate\Validation\Rules\Enum;
@ -20,7 +20,7 @@ use Illuminate\Validation\ValidationException;
class StoreProductRequest extends FormRequest class StoreProductRequest extends FormRequest
{ {
use HasPicture; use HasPicture;
use HasDocument; use HasDocumentAndPictureList;
/** /**
* Determine if the user is authorized to make this request. * Determine if the user is authorized to make this request.
@ -44,6 +44,9 @@ class StoreProductRequest extends FormRequest
'is_tasted' => $this->is_tasted ? true : false, 'is_tasted' => $this->is_tasted ? true : false,
'origins' => $this->origins ? $this->origins : [], 'origins' => $this->origins ? $this->origins : [],
'claims' => $this->claims ? $this->claims : [], 'claims' => $this->claims ? $this->claims : [],
'picture_alt' => $this->name,
'picture_title' => $this->name,
'certification' => $this->certification == 0 ? null : $this->certification,
]); ]);
$this->mergePicture('picture'); $this->mergePicture('picture');
$this->mergeDocuments('docs'); $this->mergeDocuments('docs');
@ -60,9 +63,9 @@ class StoreProductRequest extends FormRequest
'name' => 'required|string|min:3|max:255', 'name' => 'required|string|min:3|max:255',
'brand_id' => 'required|exists:brands,id', 'brand_id' => 'required|exists:brands,id',
'source' => [new Enum(SourceEnum::class)], 'source' => [new Enum(SourceEnum::class)],
'fair' => 'nullable|string|min:3|max:255|required_if:source,' . SourceEnum::showConsumer->value . ',' . SourceEnum::showCustomer->value . ',' . SourceEnum::showTrade->value, 'fair' => 'nullable|string|min:3|max:255',
'shop' => 'nullable|string|min:3|max:255|required_if:source,' . SourceEnum::magazin->value, 'shop' => 'nullable|string|min:3|max:255',
'catalogue' => 'nullable|string|min:3|max:255|required_if:source,' . SourceEnum::catalogArgumentary->value, 'catalogue' => 'nullable|string|min:3|max:255',
'gencod' => 'nullable|numeric|min:0', 'gencod' => 'nullable|numeric|min:0',
'picture_alt' => 'required|min:3|max:255', 'picture_alt' => 'required|min:3|max:255',
'picture_title' => 'required|min:3|max:255', 'picture_title' => 'required|min:3|max:255',
@ -76,19 +79,17 @@ class StoreProductRequest extends FormRequest
} }
], ],
'certification' => ['nullable', new Enum(CertificationEnum::class)], 'certification' => ['nullable', new Enum(CertificationEnum::class)],
'origins' => 'nullable|array', 'origins' => 'present|array',
'origins.*' => 'present|array', 'origins.*' => [new Enum(OriginEnum::class)],
'origins.*.id' => [new Enum(OriginEnum::class)], 'claims' => 'present|array',
'claims' => 'nullable|array', 'claims.*' => [new Enum(ClaimEnum::class)],
'claims.*' => 'present|array',
'claims.*.id' => [new Enum(ClaimEnum::class)],
'specified_claim' => 'string|min:3|max:255|required_if:claims,' . ClaimEnum::others->value, 'specified_claim' => 'string|min:3|max:255|required_if:claims,' . ClaimEnum::others->value,
'distribution_network' => ['required', new Enum(DistributionNetworkEnum::class)], 'distribution_network' => ['required', new Enum(DistributionNetworkEnum::class)],
'ray' => ['required', new Enum(RayEnum::class)], 'ray' => ['required', new Enum(RayEnum::class)],
'product_range' => ['required', new Enum(RangeEnum::class)], 'product_range' => ['required', new Enum(RangeEnum::class)],
'geo_area' => ['required', new Enum(GeoAreaEnum::class)], 'geo_area' => ['required', new Enum(GeoAreaEnum::class)],
'weight' => 'nullable|integer', 'weight' => 'nullable|integer',
'pvc' => 'nullable|integer', 'pvc' => 'nullable|decimal:0,2',
'comment' => 'nullable|string|min:15', 'comment' => 'nullable|string|min:15',
'is_tasted' => 'required|boolean', 'is_tasted' => 'required|boolean',
'tasters_number' => 'nullable|integer', 'tasters_number' => 'nullable|integer',
@ -106,7 +107,7 @@ class StoreProductRequest extends FormRequest
return \array_merge( return \array_merge(
$rules, $rules,
$this->pictureRules('picture', false, 1200, 900, 1200, 900), $this->pictureRules('picture', false, 1200, 900, 1200, 900),
$this->documentsRules('docs', false), $this->documentsRules('docs'),
); );
} }

View file

@ -2,13 +2,11 @@
namespace App\Http\Resources; namespace App\Http\Resources;
use App\Enums\Products\CertificationEnum;
use App\Enums\Products\ClaimEnum; use App\Enums\Products\ClaimEnum;
use App\Enums\Products\OriginEnum; use App\Enums\Products\OriginEnum;
use App\Models\Brand; use App\Models\Keyword;
use App\Models\Product; use App\Models\Product;
use App\Models\User; use App\Models\ProductComment;
use Carbon\Carbon;
use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Http\Resources\Json\JsonResource;
class ProductResource extends JsonResource class ProductResource extends JsonResource
@ -19,8 +17,8 @@ class ProductResource extends JsonResource
/** @var array */ /** @var array */
private static $skeleton = [ private static $skeleton = [
'id' => null, 'id' => null,
'user_id' => null, 'user' => null,
'brand_id' => null, 'brand' => null,
'name' => null, 'name' => null,
'seen_at' => null, 'seen_at' => null,
'source' => null, 'source' => null,
@ -36,6 +34,7 @@ class ProductResource extends JsonResource
'certification' => null, 'certification' => null,
'origins' => null, 'origins' => null,
'claims' => null, 'claims' => null,
'specified_claim' => null,
'distribution_network' => null, 'distribution_network' => null,
'ray' => null, 'ray' => null,
'product_range' => null, 'product_range' => null,
@ -65,54 +64,13 @@ class ProductResource extends JsonResource
*/ */
public function toArray($request = null) public function toArray($request = null)
{ {
foreach (CertificationEnum::toArray() as $enumValue) {
if ($this->product->certification->value === $enumValue->value) {
$certification = $enumValue->label;
}
}
if (count($this->product->origins)) {
$origins = "";
foreach ($this->product->origins as $originValue) {
foreach (OriginEnum::toArray() as $enumValue) {
if (intval($originValue['id']) === $enumValue->value) {
$origins = $origins . $enumValue->label . ", ";
}
}
}
}
if (count($this->product->claims)) {
$claims = "";
foreach ($this->product->claims as $originValue) {
foreach (ClaimEnum::toArray() as $enumValue) {
if (intval($originValue['id']) === $enumValue->value) {
if ($enumValue->value === ClaimEnum::others->value) {
$claims = $claims . $enumValue->label . ": " . $this->product->specified_claim . ", ";
} else {
$claims = $claims . $enumValue->label . ", ";
}
}
}
}
}
foreach ($this->product->productComments as $productComment) {
$productComment->date = Carbon::createFromFormat(
'Y-m-d H:i:s',
$productComment->created_at
)->format('d/m/Y');
}
if (count($this->product->productKeywords)) {
$productKeywords = "";
foreach ($this->product->productKeywords as $productKeyword) {
$productKeywords = $productKeywords . $productKeyword->label . ", ";
}
}
return array_intersect_key( return array_intersect_key(
[ [
'id' => $this->product->id, 'id' => $this->product->id,
'user_id' => User::where('id', $this->product->user_id)->firstOrFail(), 'user' => (object)$this->product->user->toArray(),
'brand_id' => Brand::where('id', $this->product->brand_id)->firstOrFail(), 'brand' => (object)$this->product->brand->toArray(),
'name' => $this->product->name, 'name' => $this->product->name,
'seen_at' => Carbon::createFromFormat('Y-m-d H:i:s', $this->product->seen_at)->format('d/m/Y'), 'seen_at' => $this->product->seen_at->isoFormat('DD\/MM\/YYYY'),
'source' => $this->product->source->label(), 'source' => $this->product->source->label(),
'fair' => $this->product->fair, 'fair' => $this->product->fair,
'shop' => $this->product->shop, 'shop' => $this->product->shop,
@ -123,16 +81,25 @@ class ProductResource extends JsonResource
'picture' => $this->product->picture, 'picture' => $this->product->picture,
'picture_alt' => $this->product->picture_alt, 'picture_alt' => $this->product->picture_alt,
'picture_title' => $this->product->picture_title, 'picture_title' => $this->product->picture_title,
'certification' => $certification, 'certification' => \optional($this->product->certification)->label(),
'origins' => $origins ?? null, 'origins' => $this->product->origins_enum->map(fn (OriginEnum $enum) => $enum->label())->implode(', '),
'claims' => $claims ?? null, 'claims' => $this->product->claims_enum->map(fn (ClaimEnum $enum) => $enum->label())->implode(', '),
'specified_claim' => $this->product->specified_claim,
'distribution_network' => $this->product->distribution_network->label(), 'distribution_network' => $this->product->distribution_network->label(),
'ray' => $this->product->ray->label(), 'ray' => $this->product->ray->label(),
'product_range' => $this->product->product_range->label(), 'product_range' => $this->product->product_range->label(),
'geo_area' => $this->product->geo_area->label(), 'geo_area' => $this->product->geo_area->label(),
'freeComment' => $this->product->comment, 'freeComment' => $this->product->comment,
'productComments' => $this->product->productComments, // FIXME: Use dedicaced resources for relations.
'productKeywords' => $productKeywords ?? null, 'productComments' => $this->product->productComments->map(function (ProductComment $comment) {
return (object)\array_merge($comment->toArray(), [
'created_at' => $comment->created_at->isoFormat('DD\/MM\/YYYY'),
'updated_at' => $comment->updated_at->isoFormat('DD\/MM\/YYYY'),
]);
}),
'productKeywords' => $this->product->productKeywords
->map(fn (Keyword $keyword) => $keyword->label)
->implode(', '),
], ],
$this->getSkeleton() $this->getSkeleton()
); );

View file

@ -22,6 +22,7 @@ class ExportProduct implements ShouldQueue
/** /**
* Execute the job. * Execute the job.
* *
* @param integer $productId
* @return void * @return void
* @throws \Error If file storage config is wrong. * @throws \Error If file storage config is wrong.
* @throws \RuntimeException If the storage disk export is not local file system. * @throws \RuntimeException If the storage disk export is not local file system.
@ -50,7 +51,10 @@ class ExportProduct implements ShouldQueue
throw new \RuntimeException("The ZIP file cannot be created."); throw new \RuntimeException("The ZIP file cannot be created.");
} }
$dirName = Str::slug(Carbon::parse($product->updated_at)->format('y-m-d') . '-' . $product->user->lastname . '-' . $product->user->id); $dirName = Str::slug(
Carbon::parse($product->updated_at)->format('y-m-d') . '-' .
$product->user->lastname . '-' . $product->user->id
);
if (!$zip->addEmptyDir($dirName)) { if (!$zip->addEmptyDir($dirName)) {
throw new \RuntimeException("The folder cannot be created inside the ZIP."); throw new \RuntimeException("The folder cannot be created inside the ZIP.");

View file

@ -2,13 +2,12 @@
namespace App\Lib\Helpers; namespace App\Lib\Helpers;
use Illuminate\Database\Eloquent\Model; use App\Enums\Pagination\ItemsPerPaginationEnum;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule; use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\Enum;
use Illuminate\Validation\ValidationException; use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
@ -140,75 +139,15 @@ class ToolboxHelper
return $text; return $text;
} }
/**
* Generates an array from a dot separated string
*
* @param string $path
* @param mixed $value
* @param string $separator
* @return array
*/
public static function dotStringToNestedArray(string $path, $value, string $separator = '.'): array
{
$arr = $value;
$keys = array_reverse(explode($separator, $path));
foreach ($keys as $key) {
$arr = [$key => $arr];
}
return $arr;
}
/**
* Generate url using laravel route passing our group query if present
* This is used to keep ou group tracking
* Curd-policies is only used for root users as it cannot keep track of our group
* and validate things
*
* @param array|string $name
* @param mixed $parameters
* @param boolean $absolute
* @return string
* @throws \ErrorException If an object given as parameter is not a Model.
*/
public static function routeWithGroup($name, $parameters = [], bool $absolute = true): string
{
// * Handle if parameter if is an object.
if (\is_object($parameters)) {
$parents = \class_parents($parameters);
if ($parents === false or !\in_array(Model::class, $parents)) {
throw new \ErrorException(\sprintf(
'%s only support object of type %s',
__FUNCTION__,
Model::class
));
}
/** @var \Illuminate\Database\Eloquent\Model $model */
$model = $parameters;
$parameters = [Str::singular($model->getTable()) => $model];
}
// * Handle if parameter isnt and array like slug or else.
if (!is_array($parameters)) {
// * Best effort, result is not garantee.
$parameters = [$parameters];
}
// * Pass our group.
if ($group = Session::get('activeGroup', request()->group)) {
$parameters = array_merge($parameters, ['group' => $group]);
}
// * Call laravel route helper.
return route($name, $parameters, $absolute);
}
/** /**
* Mutltibyte string replace * Mutltibyte string replace
* @param mixed $search * @param string|array $search
* @param mixed $replace * @param string|array $replace
* @param mixed $subject * @param mixed $subject
* @param integer $count * @param integer $count
* @return mixed * @return mixed
*/ */
public static function mbReplace($search, $replace, $subject, int &$count = 0) public static function mbReplace(string|array $search, string|array $replace, mixed $subject, int &$count = 0)
{ {
if (!is_array($search) && is_array($replace)) { if (!is_array($search) && is_array($replace)) {
return false; return false;
@ -253,7 +192,7 @@ class ToolboxHelper
public static function assertFieldIsUnique( public static function assertFieldIsUnique(
string $table, string $table,
string $field, string $field,
$value, mixed $value,
?int $id = null, ?int $id = null,
?callable $query = null ?callable $query = null
) { ) {
@ -288,4 +227,22 @@ class ToolboxHelper
{ {
return (array_diff($array1, $array2) == [] && array_diff($array2, $array1) == []); return (array_diff($array1, $array2) == [] && array_diff($array2, $array1) == []);
} }
/**
* Get validated pagination from request
*
* @param integer $default
* @return integer
*/
public static function getValidatedPaginate(int $default): int
{
try {
return \intval(Validator::make(
['pagination' => \request()->get('pagination')],
['pagination' => new Enum(ItemsPerPaginationEnum::class)]
)->validated()['pagination']);
} catch (ValidationException $e) {
return $default;
}
}
} }

View file

@ -7,7 +7,7 @@ use Illuminate\Database\Eloquent\Model;
/** /**
* @property integer $id * @property integer $id
* @property string $brand * @property string $name
* @property string $filter * @property string $filter
* @property string $created_at * @property string $created_at
* @property string $updated_at * @property string $updated_at

View file

@ -11,7 +11,10 @@ use Illuminate\Database\Eloquent\Model;
* @property \App\Enums\KeywordCategoryEnum $keyword_cat * @property \App\Enums\KeywordCategoryEnum $keyword_cat
* @property \Carbon\Carbon $created_at * @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at * @property \Carbon\Carbon $updated_at
* @property Keywordable[] $keywordables *
* * RELATIONSHIPS
* @property-read \Illuminate\Database\Eloquent\Collection<\App\Models\Product> $products
* @property-read \Illuminate\Database\Eloquent\Collection<\App\Models\Keyword> $keywordables
*/ */
class Keyword extends Model class Keyword extends Model
{ {
@ -34,6 +37,8 @@ class Keyword extends Model
'keyword_cat' => KeywordCategoryEnum::class 'keyword_cat' => KeywordCategoryEnum::class
]; ];
// * RELATIONSHIPS
/** /**
* @return \Illuminate\Database\Eloquent\Relations\HasMany * @return \Illuminate\Database\Eloquent\Relations\HasMany
*/ */

View file

@ -3,8 +3,10 @@
namespace App\Models; namespace App\Models;
use App\Enums\Products\CertificationEnum; use App\Enums\Products\CertificationEnum;
use App\Enums\Products\ClaimEnum;
use App\Enums\Products\DistributionNetworkEnum; use App\Enums\Products\DistributionNetworkEnum;
use App\Enums\Products\GeoAreaEnum; use App\Enums\Products\GeoAreaEnum;
use App\Enums\Products\OriginEnum;
use App\Enums\Products\RangeEnum; use App\Enums\Products\RangeEnum;
use App\Enums\Products\RayEnum; use App\Enums\Products\RayEnum;
use App\Enums\Products\SourceEnum; use App\Enums\Products\SourceEnum;
@ -13,43 +15,53 @@ use App\Lib\Helpers\FileStorageHelper;
use App\Lib\Helpers\ToolboxHelper; use App\Lib\Helpers\ToolboxHelper;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Mccarlosen\LaravelMpdf\LaravelMpdf; use Mccarlosen\LaravelMpdf\LaravelMpdf;
use Symfony\Component\HttpFoundation\StreamedResponse; use Symfony\Component\HttpFoundation\StreamedResponse;
/** /**
* @property integer $id * FIXME: using migration, do not set nullable props without a real need.
* @property integer $user_id *
* @property integer $brand_id * @property integer $id
* @property string $name * @property integer $user_id
* @property string $brand * @property integer $brand_id
* @property string $seen_at * @property string $name
* @property enum $source * @property \Carbon\Carbon $seen_at
* @property string $fair * @property \App\Enums\Products\SourceEnum $source
* @property string $shop * @property string|null $fair
* @property string $catalogue * @property string|null $shop
* @property string $gencod * @property string|null $catalogue
* @property string $picture * @property string|null $gencod
* @property array $docs * @property string|null $picture
* @property string $url * @property string $picture_alt
* @property enum $certification * @property string $picture_title
* @property object $origins * @property array|null $docs
* @property object $claims * @property string|null $url
* @property string $specified_claim * @property \App\Enums\Products\CertificationEnum|null $certification
* @property enum $distribution_network * @property array<integer>|null $origins
* @property enum $ray * @property \Illuminate\Support\Collection<\App\Enums\Products\OriginEnum> $origins_enum
* @property enum $product_range * @property array<integer>|null $claims
* @property enum $geo area * @property \Illuminate\Support\Collection<\App\Enums\Products\OriginEnum> $claims_enum
* @property integer $weight * @property string|null $specified_claim
* @property integer $pvc * @property \App\Enums\Products\DistributionNetworkEnum $distribution_network
* @property string $comment * @property \App\Enums\Products\RayEnum $ray
* @property boolean $is_tasted * @property \App\Enums\Products\RangeEnum $product_range
* @property integer $tasters_number * @property \App\Enums\Products\GeoAreaEnum $geo area
* @property string $created_at * @property integer|null $weight
* @property string $updated_at * @property integer|null $pvc
* @property string $deleted_at * @property string|null $comment
* @property ProductComment[] $productComments * @property boolean $is_tasted
* @property User $user * @property integer|null $tasters_number
* @property \Carbon\Carbon|null $created_at
* @property \Carbon\Carbon|null $updated_at
* @property \Carbon\Carbon|null $deleted_at
*
* * RELATIONSHIPS
* @property-read \App\Models\Brand $brand
* @property-read \App\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<\App\Models\ProductComment> $productComments
* @property-read \Illuminate\Database\Eloquent\Collection<\App\Models\Keyword> $productKeywords
*/ */
class Product extends Model class Product extends Model
{ {
@ -109,10 +121,10 @@ class Product extends Model
'product_range' => RangeEnum::class, 'product_range' => RangeEnum::class,
'geo_area' => GeoAreaEnum::class, 'geo_area' => GeoAreaEnum::class,
'is_tasted' => 'boolean', 'is_tasted' => 'boolean',
'seen_at' => 'datetime:Y-m-d', 'seen_at' => 'datetime',
'created_at' => 'datetime:Y-m-d', 'created_at' => 'datetime',
'updated_at' => 'datetime:Y-m-d', 'updated_at' => 'datetime',
'deleted_at' => 'datetime:Y-m-d', 'deleted_at' => 'datetime',
]; ];
// * EVENTS // * EVENTS
@ -132,7 +144,7 @@ class Product extends Model
$model->user_id = 1; $model->user_id = 1;
}); });
static::updating(function (self $model) { static::updating(function (self $model) {
self::deleteOldDocuments($model); self::deleteOldDocuments($model, true);
$model->picture = FileStorageHelper::storeFile($model, $model->picture, true); $model->picture = FileStorageHelper::storeFile($model, $model->picture, true);
self::updateSource($model); self::updateSource($model);
self::updateSpecifiedClaim($model); self::updateSpecifiedClaim($model);
@ -171,8 +183,11 @@ class Product extends Model
*/ */
private static function updateSpecifiedClaim(Product $model) private static function updateSpecifiedClaim(Product $model)
{ {
foreach ($model->claims as $claim) { $check = false;
$check = (intval($claim['id']) === \App\Enums\Products\ClaimEnum::others->value) ? true : false; foreach ($model->claims as $claimValue) {
if (!$check) {
$check = (intval($claimValue) === \App\Enums\Products\ClaimEnum::others->value) ? true : false;
}
} }
$model->specified_claim = ($check) ? $model->specified_claim : null; $model->specified_claim = ($check) ? $model->specified_claim : null;
} }
@ -208,15 +223,23 @@ class Product extends Model
/** /**
* Delete old documents. * Delete old documents.
* *
* @param self $model * @param self $model
* @param boolean $onlyIfUpdated
* @return void * @return void
*/ */
public static function deleteOldDocuments(self $model) public static function deleteOldDocuments(self $model, bool $onlyIfUpdated = false)
{ {
foreach ($model->getOriginal('docs') as $document) { $original = \collect($model->getOriginal('docs'))->map(fn ($document) => strval($document));
$new = \collect($model->docs)->map(fn ($document) => strval($document))->flip();
foreach ($original as $document) {
// * Ignore if document is to keep.
if ($onlyIfUpdated and $new->has($document)) {
continue;
}
$originalPath = \ltrim(\urldecode($document), '/'); $originalPath = \ltrim(\urldecode($document), '/');
if (\strpos($originalPath, 'storage') !== 0) { if (\strpos($originalPath, 'storage') !== 0) {
return; continue;
} }
$originalPath = ToolboxHelper::mbReplace('storage/', '', $originalPath); $originalPath = ToolboxHelper::mbReplace('storage/', '', $originalPath);
if (Storage::disk('public')->exists($originalPath)) { if (Storage::disk('public')->exists($originalPath)) {
@ -283,26 +306,27 @@ class Product extends Model
/** /**
* Generate a pdf with multiple products. * Generate a pdf with multiple products.
* *
* @param array $printMultiProduct * @param array<integer> $printMultiProduct
* @param string $title * @param string $title
* @return \Mccarlosen\LaravelMpdf\LaravelMpdf * @return \Mccarlosen\LaravelMpdf\LaravelMpdf
*/ */
public static function generateMultipleProductPdf(array $printMultiProduct, string $title) public static function generateMultipleProductPdf(array $printMultiProduct, string $title)
{ {
/** @var \Mccarlosen\LaravelMpdf\LaravelMpdf */ /** @var \Mccarlosen\LaravelMpdf\LaravelMpdf|null */
$pdf = null; $pdf = null;
// Loop all views. // * Loop all Model ids.
foreach ($printMultiProduct as $productId) { foreach ($printMultiProduct as $productId) {
$product = Product::where('id', $productId)->firstOrFail(); /** @var \App\Models\Product */
// If not yet defined, set the first page. $product = Product::query()->where('id', $productId)->firstOrFail();
// * If not yet defined, set the first page.
if (is_null($pdf)) { if (is_null($pdf)) {
$pdf = Product::generateProductPdf($product, $title); $pdf = Product::generateProductPdf($product, $title);
} else { } else {
// Add another page and add HTML from view to this. // * Add another page and add HTML from view to this.
$pdf->getMpdf()->AddPage(); $pdf->getMpdf()->AddPage();
$pdf->getMpdf()->WriteHTML((string)view( $pdf->getMpdf()->WriteHTML((string)view(
'documents.product-body', 'documents.product-body',
['product' => \json_decode(\json_encode(new ProductResource($product)))], ['product' => (object)(new ProductResource($product))->toArray()],
)); ));
} }
} }
@ -340,14 +364,64 @@ class Product extends Model
])->send(); ])->send();
} }
// * RELATIONS // * GETTERS
/**
* Get origins_enum attribute
*
* @return \Illuminate\Support\Collection<\App\Enums\Ingredients\OriginEnum>
*/
public function getOriginsEnumAttribute(): Collection
{
return \collect($this->getAttribute('origins'))->filter()
->map(fn (int $originEnumValue) => OriginEnum::make($originEnumValue));
}
/**
* Set origins_enum attribute
*
* @param mixed $value
* @return void
*/
public function setOriginsEnumAttribute(mixed $value): void
{
$this->setAttribute('origins', \collect($value)->filter()
->map(fn ($originEnumValue) => OriginEnum::make($originEnumValue)->value())
->all());
}
/**
* Get claims_enum attribute
*
* @return \Illuminate\Support\Collection<\App\Enums\Ingredients\ClaimEnum>
*/
public function getClaimsEnumAttribute(): Collection
{
return \collect($this->getAttribute('claims'))->filter()
->map(fn (int $originEnumValue) => ClaimEnum::make($originEnumValue));
}
/**
* Set claims_enum attribute
*
* @param mixed $value
* @return void
*/
public function setClaimsEnumAttribute(mixed $value): void
{
$this->setAttribute('claims', \collect($value)->filter()
->map(fn ($claimEnumValue) => OriginEnum::make($claimEnumValue)->value())
->all());
}
// * RELATIONSHIPS
/** /**
* @return \Illuminate\Database\Eloquent\Relations\HasMany * @return \Illuminate\Database\Eloquent\Relations\HasMany
*/ */
public function productComments() public function productComments()
{ {
return $this->hasMany('App\Models\ProductComment'); return $this->hasMany(ProductComment::class);
} }
/** /**
@ -355,7 +429,7 @@ class Product extends Model
*/ */
public function user() public function user()
{ {
return $this->belongsTo('App\Models\User'); return $this->belongsTo(User::class);
} }
/** /**
@ -363,7 +437,7 @@ class Product extends Model
*/ */
public function brand() public function brand()
{ {
return $this->belongsTo('App\Models\Brand'); return $this->belongsTo(Brand::class);
} }
/** /**

View file

@ -6,16 +6,18 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
/** /**
* @property integer $id * @property integer $id
* @property integer $product_id * @property integer $product_id
* @property string $evaluation * @property string $evaluation
* @property string $aspect * @property string|null $aspect
* @property string $taste * @property string|null $taste
* @property string $odor * @property string|null $odor
* @property string $texture * @property string|null $texture
* @property string $created_at * @property \Carbon\Carbon $created_at
* @property string $updated_at * @property \Carbon\Carbon $updated_at
* @property Product $product *
* * RELATIONSHIPS
* @property-read \App\Models\Product $product
*/ */
class ProductComment extends Model class ProductComment extends Model
{ {
@ -35,11 +37,23 @@ class ProductComment extends Model
'updated_at' 'updated_at'
]; ];
/**
* The attributes that should be cast.
*
* @var array
*/
protected $casts = [
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
// * RELATIONSHIPS
/** /**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/ */
public function product() public function product()
{ {
return $this->belongsTo('App\Models\Product'); return $this->belongsTo(Product::class);
} }
} }

View file

@ -11,6 +11,8 @@ class AppServiceProvider extends ServiceProvider
{ {
/** /**
* Bootstrap any application services. * Bootstrap any application services.
*
* @return void
*/ */
public function boot(): void public function boot(): void
{ {
@ -25,7 +27,7 @@ class AppServiceProvider extends ServiceProvider
return request()->getClientIp(); return request()->getClientIp();
} else { } else {
return request()->server('HTTP_X_FORWARDED_FOR') ?? return request()->server('HTTP_X_FORWARDED_FOR') ??
request()->getClientIp(); request()->getClientIp();
} }
}); });

View file

@ -2,7 +2,6 @@
namespace App\Providers; namespace App\Providers;
// use Illuminate\Support\Facades\Gate;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider; use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
class AuthServiceProvider extends ServiceProvider class AuthServiceProvider extends ServiceProvider
@ -12,12 +11,12 @@ class AuthServiceProvider extends ServiceProvider
* *
* @var array<class-string, class-string> * @var array<class-string, class-string>
*/ */
protected $policies = [ protected $policies = [];
// 'App\Models\Model' => 'App\Policies\ModelPolicy',
];
/** /**
* Register any authentication / authorization services. * Register any authentication / authorization services.
*
* @return void
*/ */
public function boot(): void public function boot(): void
{ {

View file

@ -21,6 +21,7 @@ class EventServiceProvider extends ServiceProvider
/** /**
* Register any events for your application. * Register any events for your application.
* @return void
*/ */
public function boot(): void public function boot(): void
{ {
@ -28,6 +29,7 @@ class EventServiceProvider extends ServiceProvider
/** /**
* Determine if events and listeners should be automatically discovered. * Determine if events and listeners should be automatically discovered.
* @return boolean
*/ */
public function shouldDiscoverEvents(): bool public function shouldDiscoverEvents(): bool
{ {

View file

@ -17,12 +17,10 @@ trait HasDocuments
*/ */
private function storeDocuments(Request $request, Model $model): void private function storeDocuments(Request $request, Model $model): void
{ {
if (is_array($request->docs)) { $model->docs = [];
$model->docs = []; $model->docs = collect($request->docs)
$model->docs = collect($request->docs) ->map(function ($document) use ($model) {
->map(function ($document) use ($model) { return FileStorageHelper::storeFile($model, $document);
return FileStorageHelper::storeFile($model, $document); })->all();
})->all();
}
} }
} }

View file

@ -4,24 +4,43 @@ namespace App\Traits\Requests;
use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
trait HasDocument trait HasDocumentAndPictureList
{ {
/**
* Autorised mimtypes
*
* @var string[]
*/
private $autorisedMimTypes = [
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'image/jpeg',
'image/png',
'image/gif',
'application/pdf',
'application/vnd.ms-powerpoint',
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'text/csv',
'text/plain',
];
/** /**
* Images rules * Images rules
* *
* @param string $name * @param string $name
* @param boolean $required
* @return array * @return array
*/ */
public function documentsRules( public function documentsRules(
string $name = 'documents', string $name = 'docs',
?bool $required = null
): array { ): array {
return [ return [
"{$name}" => ($required ? 'required' : 'nullable') . '|array', "{$name}" => 'present|array',
"{$name}.*" => [ "{$name}.*" => [
$required ? 'required' : 'nullable', 'required',
function (string $attribute, $value, callable $fail) { function (string $attribute, $value, callable $fail) {
return $this->validateDocument($attribute, $value, $fail); return $this->validateDocument($attribute, $value, $fail);
} }
@ -38,32 +57,36 @@ trait HasDocument
* @return void * @return void
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundBeforeLastUsed * @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundBeforeLastUsed
*/ */
private function validateDocument(string $attribute, $value, callable $fail) private function validateDocument(string $attribute, mixed $value, callable $fail)
{ {
// phpcs:enable
if (!($useStorage = Storage::exists($value)) and !File::exists($value)) { if (!($useStorage = Storage::exists($value)) and !File::exists($value)) {
$fail(trans(":attribute n'existe pas dans le système de fichier")); $fail(trans(":attribute n'existe pas dans le système de fichier"));
return; return;
} }
if ( if (!\in_array($this->getUploadedFileMimeType($value, $useStorage), $this->autorisedMimTypes)) {
!\in_array($this->getUploadedFileMimeType($value, $useStorage), [ $fail(trans(':attribute doit être un document de type :mimetypes', [
'application/pdf' 'mimetypes' => Str::of($this->autorisedMimTypes)->implode(' ou ')
]) ]));
) { }
$fail(trans(":attribute doit être une image de type 'application/pdf'")); $maxSize = 6 * 1024 * 1024;
if (File::size($value) > $maxSize) {
$fail(trans(':attribute doit être inférieur à :size Mo', ['size' => $maxSize / (1024 * 1024)]));
} }
} }
/** /**
* Merges documents fields with request data. * Merges docs fields with request data.
* *
* @param string $name * @param string $name
* @return void * @return void
*/ */
protected function mergeDocuments(string $name = 'documents'): void protected function mergeDocuments(string $name = 'docs'): void
{ {
$documents = $this->{$name}; $documents = $this->{$name};
$this->merge([$name => $this->{$name} ?? []]);
$this->merge(["{$name}Validate" => []]);
if (is_array($documents)) { if (is_array($documents)) {
$this->merge(["{$name}Validate" => []]);
foreach ($documents as $key => $documentPath) { foreach ($documents as $key => $documentPath) {
// ! Empty document. // ! Empty document.
if ($documentPath === 'false') { if ($documentPath === 'false') {

View file

@ -123,7 +123,7 @@ trait HasPicture
* @return void * @return void
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundBeforeLastUsed * @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundBeforeLastUsed
*/ */
private function validatePicture(string $attribute, $value, callable $fail, object $sizes) private function validatePicture(string $attribute, mixed $value, callable $fail, object $sizes)
{ {
if (!($useStorage = Storage::exists($value)) and !File::exists($value)) { if (!($useStorage = Storage::exists($value)) and !File::exists($value)) {
$fail(trans(":attribute n'existe pas dans le système de fichier")); $fail(trans(":attribute n'existe pas dans le système de fichier"));
@ -138,6 +138,11 @@ trait HasPicture
) { ) {
$fail(trans(":attribute doit être une image de type 'image/jpeg,image/jpg,image/png,image/gif'")); $fail(trans(":attribute doit être une image de type 'image/jpeg,image/jpg,image/png,image/gif'"));
} }
$filesize = \filesize($fileFullPath);
$maxSize = 6 * 1024 * 1024;
if ($filesize === false || $filesize > $maxSize) {
$fail(trans(':attribute doit être inférieur à :size Mo', ['size' => $maxSize / (1024 * 1024)]));
}
if (!($dimensions = \getimagesize($fileFullPath)) or !isset($dimensions[0]) or !isset($dimensions[1])) { if (!($dimensions = \getimagesize($fileFullPath)) or !isset($dimensions[0]) or !isset($dimensions[1])) {
$fail(trans(":attribute impossible de valider les dimensions")); $fail(trans(":attribute impossible de valider les dimensions"));
} else { } else {
@ -239,7 +244,7 @@ trait HasPicture
*/ */
private function getUploadedFileFullPath(string $value, bool $useStorage): string private function getUploadedFileFullPath(string $value, bool $useStorage): string
{ {
return !$useStorage ? realpath($value) ?: $value : Storage::getAdapter()->applyPathPrefix($value); return !$useStorage ? (realpath($value) ?: $value) : Storage::path($value);
} }
/** /**

View file

@ -12,6 +12,7 @@
"carlos-meneses/laravel-mpdf": "^2.1", "carlos-meneses/laravel-mpdf": "^2.1",
"diglactic/laravel-breadcrumbs": "^8.0", "diglactic/laravel-breadcrumbs": "^8.0",
"guzzlehttp/guzzle": "^7.2", "guzzlehttp/guzzle": "^7.2",
"kwaadpepper/image-resizer": "^2.1",
"laravel/framework": "^10.0", "laravel/framework": "^10.0",
"laravel/tinker": "^2.8" "laravel/tinker": "^2.8"
}, },
@ -19,6 +20,7 @@
"fakerphp/faker": "^1.9.1", "fakerphp/faker": "^1.9.1",
"kitloong/laravel-migrations-generator": "^6.8", "kitloong/laravel-migrations-generator": "^6.8",
"krlove/eloquent-model-generator": "dev-l10-compatibility", "krlove/eloquent-model-generator": "dev-l10-compatibility",
"laravel-lang/common": "^4.0",
"laravel/pint": "^1.0", "laravel/pint": "^1.0",
"laravel/sail": "^1.18", "laravel/sail": "^1.18",
"mockery/mockery": "^1.4.4", "mockery/mockery": "^1.4.4",

1145
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -140,7 +140,7 @@ return [
'maintenance' => [ 'maintenance' => [
'driver' => 'file', 'driver' => 'file',
// 'store' => 'redis', // 'store' 'redis'
], ],
/* /*
@ -159,6 +159,7 @@ return [
/* /*
* Laravel Framework Service Providers... * Laravel Framework Service Providers...
*/ */
Illuminate\Auth\AuthServiceProvider::class, Illuminate\Auth\AuthServiceProvider::class,
Illuminate\Broadcasting\BroadcastServiceProvider::class, Illuminate\Broadcasting\BroadcastServiceProvider::class,
Illuminate\Bus\BusServiceProvider::class, Illuminate\Bus\BusServiceProvider::class,
@ -185,11 +186,13 @@ return [
/* /*
* Package Service Providers... * Package Service Providers...
*/ */
Krlove\EloquentModelGenerator\Provider\GeneratorServiceProvider::class, Krlove\EloquentModelGenerator\Provider\GeneratorServiceProvider::class,
/* /*
* Application Service Providers... * Application Service Providers...
*/ */
App\Providers\AppServiceProvider::class, App\Providers\AppServiceProvider::class,
App\Providers\AuthServiceProvider::class, App\Providers\AuthServiceProvider::class,
App\Providers\EventServiceProvider::class, App\Providers\EventServiceProvider::class,

View file

@ -64,11 +64,6 @@ return [
'driver' => 'eloquent', 'driver' => 'eloquent',
'model' => App\Models\User::class, 'model' => App\Models\User::class,
], ],
// 'users' => [
// 'driver' => 'database',
// 'table' => 'users',
// ],
], ],
/* /*

View file

@ -42,9 +42,8 @@ return [
'encrypted' => true, 'encrypted' => true,
'useTLS' => env('PUSHER_SCHEME', 'https') === 'https', 'useTLS' => env('PUSHER_SCHEME', 'https') === 'https',
], ],
'client_options' => [ // Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html .
// Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html 'client_options' => [],
],
], ],
'ably' => [ 'ably' => [

View file

@ -61,9 +61,7 @@ return [
env('MEMCACHED_USERNAME'), env('MEMCACHED_USERNAME'),
env('MEMCACHED_PASSWORD'), env('MEMCACHED_PASSWORD'),
], ],
'options' => [ 'options' => [],
// Memcached::OPT_CONNECT_TIMEOUT => 2000,
],
'servers' => [ 'servers' => [
[ [
'host' => env('MEMCACHED_HOST', '127.0.0.1'), 'host' => env('MEMCACHED_HOST', '127.0.0.1'),

View file

@ -101,8 +101,8 @@ return [
'charset' => 'utf8', 'charset' => 'utf8',
'prefix' => '', 'prefix' => '',
'prefix_indexes' => true, 'prefix_indexes' => true,
// 'encrypt' => env('DB_ENCRYPT', 'yes'), 'encrypt' => env('DB_ENCRYPT', 'yes'),
// 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'), 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'),
], ],
], ],

69
config/image-resizer.php Normal file
View file

@ -0,0 +1,69 @@
<?php
return [
/**
* The php image driver to use
* this can be 'imagick' or 'gd'
*/
'driver' => 'imagick',
/**
* The cache driver to use as defined
* in config/cache of your app
*/
'cache' => 'file',
/**
* The path where to store image in cache
*/
'cachePath' => 'cache/images',
/**
* Image cache life time
* This is given in minutes
* Here the cache for images is
* one hour
*/
'lifetime' => 60,
/**
* Format can specifiy to which format save the image
* JPEG PNG GIF TIF BMP ICO PSD WebP
* GD ✔️ ✔️ ✔️ - - - - ✔️ *
* Imagick ✔️ ✔️ ✔️ ✔️ ✔️ ✔️ ✔️ ✔️ *
* For WebP support GD driver must be used with PHP 5 >= 5.5.0 or PHP 7
* in order to use imagewebp(). If Imagick is used, it must be compiled
* with libwebp for WebP support.
* resize => will resize the image (boolean)
* fit => Combine cropping and resizing to format image in a smart way (boolean)
* keepRatio => will keep image ratio wile resizing (boolean)
* trim => boolean to trim the image using border color
* inCanvas => to make sure image boundarie is respected
* format => select tha wantd ouput form, yan can just convert images if you want
* example :
* 'small' => [
* 'height' => 500,
* 'width' => 250,
* 'inCanvas' => true,
* 'trim' => ['transparent', null, 10]
* ],
* 'smallWebp' => [
* 'height' => 500,
* 'width' => 250,
* 'inCanvas' => true,
* 'format' => 'webp'
* ],
* 'justconvertImage' => [
* 'format' => 'webp'
* ]
*/
'templates' => [
'productTiny' => [
'height' => 180,
'width' => 180,
'resize' => true,
'keepRatio' => true,
'format' => 'jpg',
],
]
];

View file

@ -51,16 +51,16 @@ return [
'mailgun' => [ 'mailgun' => [
'transport' => 'mailgun', 'transport' => 'mailgun',
// 'client' => [ 'client' => [
// 'timeout' => 5, 'timeout' => 5,
// ], ],
], ],
'postmark' => [ 'postmark' => [
'transport' => 'postmark', 'transport' => 'postmark',
// 'client' => [ 'client' => [
// 'timeout' => 5, 'timeout' => 5,
// ], ],
], ],
'sendmail' => [ 'sendmail' => [

View file

@ -25,7 +25,7 @@ final class BrandFactory extends Factory
public function definition(): array public function definition(): array
{ {
return [ return [
'name' => fake()->word, 'name' => fake()->unique()->word,
'filter' => fake()->word, 'filter' => fake()->word,
]; ];
} }

View file

@ -34,9 +34,15 @@ final class ProductFactory extends Factory
*/ */
public function definition(): array public function definition(): array
{ {
$source = \collect(SourceEnum::toArray())->random()->value; $source = \collect(SourceEnum::toArray())->random()->value;
$fair = $shop = $catalogue = null; $catalogue = null;
if ($source === SourceEnum::showConsumer->value || $source === SourceEnum::showCustomer->value || $source === SourceEnum::showTrade->value) { $fair = null;
$shop = null;
if (
$source === SourceEnum::showConsumer->value ||
$source === SourceEnum::showCustomer->value ||
$source === SourceEnum::showTrade->value
) {
$fair = fake()->word; $fair = fake()->word;
} elseif ($source === SourceEnum::magazin->value) { } elseif ($source === SourceEnum::magazin->value) {
$shop = fake()->word; $shop = fake()->word;
@ -61,8 +67,8 @@ final class ProductFactory extends Factory
'docs' => [], 'docs' => [],
'url' => fake()->url, 'url' => fake()->url,
'certification' => \collect(CertificationEnum::toArray())->random()->value, 'certification' => \collect(CertificationEnum::toArray())->random()->value,
'origins' => [["id" => strval(\collect(OriginEnum::toArray())->random()->value)]], 'origins' => [\collect(OriginEnum::toArray())->random()->value],
'claims' => [["id" => strval($claim)]], 'claims' => [$claim],
'specified_claim' => ($claim == OriginEnum::others->value) ? fake()->text : null, 'specified_claim' => ($claim == OriginEnum::others->value) ? fake()->text : null,
'distribution_network' => \collect(DistributionNetworkEnum::toArray())->random()->value, 'distribution_network' => \collect(DistributionNetworkEnum::toArray())->random()->value,
'ray' => \collect(RayEnum::toArray())->random()->value, 'ray' => \collect(RayEnum::toArray())->random()->value,

View file

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up(): void
{
Schema::table('products', function (Blueprint $table) {
$table->decimal('pvc', 12, 2)->change();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down(): void
{
Schema::table('products', function (Blueprint $table) {
$table->integer('pvc')->change();
});
}
};

View file

@ -9,12 +9,16 @@ use App\Enums\Products\OriginEnum;
use App\Enums\Products\RangeEnum; use App\Enums\Products\RangeEnum;
use App\Enums\Products\RayEnum; use App\Enums\Products\RayEnum;
use App\Enums\Products\SourceEnum; use App\Enums\Products\SourceEnum;
use App\Lib\Helpers\FileStorageHelper;
use App\Models\Brand; use App\Models\Brand;
use App\Models\Keyword; use App\Models\Keyword;
use App\Models\Product; use App\Models\Product;
use App\Models\ProductComment;
use App\Models\User; use App\Models\User;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Console\OutputStyle;
use Illuminate\Contracts\Database\Query\Builder; use Illuminate\Contracts\Database\Query\Builder;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\QueryException; use Illuminate\Database\QueryException;
use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Schema\Blueprint;
@ -22,8 +26,11 @@ use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\ItemNotFoundException;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Symfony\Component\Console\Helper\ProgressBar; use Symfony\Component\Console\Helper\ProgressBar;
use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Console\Output\ConsoleOutput; use Symfony\Component\Console\Output\ConsoleOutput;
return new class extends Migration return new class extends Migration
@ -47,9 +54,18 @@ return new class extends Migration
'VEILLE_PROD', 'VEILLE_PROD_COMM', 'VEILLE_PROD', 'VEILLE_PROD_COMM',
]; ];
/** @var \App\Model\User */ /**
* Console tabulation (margin left)
* @var integer
*/
private $consoleTabulation = 28;
/** @var \App\Models\User */
private $dummyUser; private $dummyUser;
/** @var \App\Models\Brand */
private $dummyBrand;
/** /**
* Run the migrations. * Run the migrations.
* *
@ -58,8 +74,14 @@ return new class extends Migration
*/ */
public function up(): void public function up(): void
{ {
$console = new ConsoleOutput(); $ouput = new ConsoleOutput();
$section = $console->section(); $input = new ArgvInput();
$style = new OutputStyle($input, $ouput);
$section = $ouput->section();
if (!$style->confirm('Do this migration or skip ?', false)) {
return;
}
// * SEED KEYWORDS As they are needed on runtime. // * SEED KEYWORDS As they are needed on runtime.
Schema::disableForeignKeyConstraints(); Schema::disableForeignKeyConstraints();
@ -67,6 +89,9 @@ return new class extends Migration
DB::table('keywordables')->truncate(); DB::table('keywordables')->truncate();
DB::table('keywords')->truncate(); DB::table('keywords')->truncate();
DB::table('products')->truncate(); DB::table('products')->truncate();
DB::table('product_comments')->truncate();
// * Remove previously uploaded files.
File::deleteDirectory(storage_path('app/public/modelfiles/products'));
Schema::enableForeignKeyConstraints(); Schema::enableForeignKeyConstraints();
if (Artisan::call('db:seed', ['--class' => 'KeywordInitSeeder']) !== 0) { if (Artisan::call('db:seed', ['--class' => 'KeywordInitSeeder']) !== 0) {
throw new \RuntimeException('Failed to seed keywords'); throw new \RuntimeException('Failed to seed keywords');
@ -77,6 +102,11 @@ return new class extends Migration
'firstname' => 'Dummy', 'firstname' => 'Dummy',
'lastname' => 'User' 'lastname' => 'User'
]); ]);
// * Seed a dummy brand.
$this->dummyBrand = Brand::factory()->createOne([
'name' => Str::of('Sans Marque')->slug()->studly(),
'filter' => null
]);
// * Apply DB corrections // * Apply DB corrections
$this->setUniqueOnBrandName(); $this->setUniqueOnBrandName();
@ -88,13 +118,13 @@ return new class extends Migration
$this->createAndFillTempTables(); $this->createAndFillTempTables();
// * Migrate PRODUCTS, comment all except the one you want to develop here. // * Migrate PRODUCTS, comment all except the one you want to develop here.
$this->migrateBrands(); $this->migrateBrands($style);
$this->migrateProducts(); $this->migrateProducts($style);
$this->migrateProductsComments($style);
// ! DEV: Add a die here to develop migration and prevent droping temp tables
// dd('toto');
if (!$style->confirm('Continue ?')) {
throw new \RuntimeException('Migration stoped');
}
$this->dropTempTables(); $this->dropTempTables();
$section->clear(); $section->clear();
} }
@ -113,21 +143,91 @@ return new class extends Migration
// - MIGRATION METHODS - // - MIGRATION METHODS -
/** /**
* Migrate products * Migrate products comments
*
* *
* @param \Illuminate\Console\OutputStyle $style
* @return void * @return void
* @throws \Error In case of failure. * @throws \Error In case of failure.
* @phpcs:disable Generic.Metrics.CyclomaticComplexity.MaxExceeded * @phpcs:disable Generic.Metrics.CyclomaticComplexity.MaxExceeded
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.Found
*/ */
private function migrateProducts(): void private function migrateProductsComments(OutputStyle $style): void
{
// phpcs:enable
$index = "NUM_COMM";
$query = $this->getQuery("VEILLE_PROD_COMM")->orderBy($index);
$nbRowsToMigrate = (clone $query)->count();
$totalRowsToMigrate = $nbRowsToMigrate;
$console = new ConsoleOutput();
$console->writeln('');
$progress = new ProgressBar($console, $nbRowsToMigrate);
$progress->setMessage(str_pad('Migrate Products Comments', $this->consoleTabulation, ' '));
$progress->setFormat(' - %message% [%bar%] %percent:3s%% %current%/%max% %elapsed:6s%/%estimated:-6s%');
$skipped = collect();
while ($nbRowsToMigrate > 0) {
$progress->display();
$progress->advance(1);
$row = (clone $query)->skip($totalRowsToMigrate - $nbRowsToMigrate--)->limit(1)->first();
try {
/** @var \App\Models\ProductComment $comment */
$comment = ProductComment::query()->where('id', $row->{$index})->firstOrNew();
$comment->id = $row->NUM_COMM;
/** @var \App\Models\Product $product */
$product = Product::query()->where('id', $row->NUM_PROD)->firstOrFail();
$product->id = $row->NUM_PROD;
$date_saisie = $row->DATE_SAISIE ? $this->tryToParseCarbon($row->DATE_SAISIE, true) : null;
$fill = [
'product_id' => $product->id,
'evaluation' => $row->APPRECIATION ?? '',
'aspect' => $row->ASPECT,
'taste' => $row->GOUT,
'odor' => $row->ODEUR,
'texture' => $row->TEXTURE,
];
if ($date_saisie) {
// ? FIXME: IS this pertinent ?
$fill['created_at'] = $date_saisie;
$fill['updated_at'] = $date_saisie;
}
$comment->fill($fill);
if (!DB::transaction(fn () => $comment->saveQuietly())) {
throw new \Error("Failed to save comment {$row->NUM_COMM}");
}
} catch (ModelNotFoundException $e) {
$model = $e->getModel();
$skipped->push("No {$model} found for '{$row->NUM_PROD}', skip comment '{$row->NUM_COMM}'");
} //end try
} //end while
$progress->finish();
if ($skipped->count() and !$console->writeln('') and $style->confirm('Show errors ?', false)) {
$console->writeln('');
$skipped->each(fn (string $error) => $console->writeln(str_repeat(' ', 8) . $error));
}
}
/**
* Migrate products
*
*
* @param \Illuminate\Console\OutputStyle $style
* @return void
* @throws \Error In case of failure.
* @phpcs:disable Generic.Metrics.CyclomaticComplexity.MaxExceeded
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.Found
*/
private function migrateProducts(OutputStyle $style): void
{ {
// phpcs:enable // phpcs:enable
$otherAllegations = Str::of(File::get(base_path('database/migrations/other_allegations.txt'))) $otherAllegations = Str::of(File::get(base_path('database/migrations/other_allegations.txt')))
->explode(PHP_EOL)->map(fn (string $other_allegation) => Str::of($other_allegation)->squish()) ->explode(PHP_EOL)->map(fn (string $other_allegation) => Str::of($other_allegation)->squish())
->unique(); ->unique();
$MOTKS = Keyword::all(); $MOTKS = Keyword::query()->where('keyword_cat', 'LIKE', 'product_%')->get();
/** @var \Illuminate\Support\Collection<string,string> */ /** @var \Illuminate\Support\Collection<string,string> */
$START_SOURCES = $this->getQuery('SOURCE')->get(); $START_SOURCES = $this->getQuery('SOURCE')->get();
/** @var \Illuminate\Support\Collection<string,string> */ /** @var \Illuminate\Support\Collection<string,string> */
@ -156,7 +256,7 @@ return new class extends Migration
$console = new ConsoleOutput(); $console = new ConsoleOutput();
$console->writeln(''); $console->writeln('');
$progress = new ProgressBar($console, $nbRowsToMigrate); $progress = new ProgressBar($console, $nbRowsToMigrate);
$progress->setMessage(str_pad('Migrate Products', 24, ' ')); $progress->setMessage(str_pad('Migrate Products', $this->consoleTabulation, ' '));
$progress->setFormat(' - %message% [%bar%] %percent:3s%% %current%/%max% %elapsed:6s%/%estimated:-6s%'); $progress->setFormat(' - %message% [%bar%] %percent:3s%% %current%/%max% %elapsed:6s%/%estimated:-6s%');
while ($nbRowsToMigrate > 0) { while ($nbRowsToMigrate > 0) {
@ -211,22 +311,22 @@ return new class extends Migration
// * Uu OMG !!! // * Uu OMG !!!
if ($motk === 'Gain de temps Nomadisme') { if ($motk === 'Gain de temps Nomadisme') {
return $MOTKS->where('label', 'Gain de temps') return $MOTKS->where('label', 'Gain de temps')
->where('keyword_cat', KeywordCategoryEnum::productPracticality)->firstOrFail(); ->where('keyword_cat', KeywordCategoryEnum::productPracticality)->firstOrFail();
} }
if ($motk === 'Vari??t?? des sens') { if ($motk === 'Vari??t?? des sens') {
return $MOTKS->where('label', 'Variété des sens') return $MOTKS->where('label', 'Variété des sens')
->where('keyword_cat', KeywordCategoryEnum::productPleasure)->firstOrFail(); ->where('keyword_cat', KeywordCategoryEnum::productPleasure)->firstOrFail();
} }
if ($motk === 'V??g??tal') { if ($motk === 'V??g??tal') {
return $MOTKS->where('label', 'Végétal') return $MOTKS->where('label', 'Végétal')
->where('keyword_cat', KeywordCategoryEnum::productHealth)->firstOrFail(); ->where('keyword_cat', KeywordCategoryEnum::productHealth)->firstOrFail();
} }
try { try {
return $MOTKS->firstOrFail( return $MOTKS->firstOrFail(
fn (Keyword $keyword) => Str::of($keyword->label)->ascii()->replace(' ', '') fn (Keyword $keyword) => Str::of($keyword->label)->ascii()->replace(' ', '')
== Str::of($motk)->trim()->ascii()->replace(' ', '') == Str::of($motk)->trim()->ascii()->replace(' ', '')
); );
} catch (\Exception) { } catch (ItemNotFoundException) {
dump( dump(
$motk, $motk,
$MOTKS->pluck('label') $MOTKS->pluck('label')
@ -235,28 +335,32 @@ return new class extends Migration
} }
}; };
// FIXME: thread https://lab.s-web.fr/ldc-web/projets/startcoq/-/issues/28#note_9740 . // NOTE: thread https://lab.s-web.fr/ldc-web/projets/startcoq/-/issues/28#note_9740 .
if (empty($row->MARQUE)) { if (is_null($row->MARQUE)) {
continue; continue;
} }
// FIXME: thread https://lab.s-web.fr/ldc-web/projets/startcoq/-/issues/28#note_9750 . if (!strlen(trim($row->MARQUE))) {
$row->MARQUE = $this->dummyBrand->name;
continue;
}
// NOTE: thread https://lab.s-web.fr/ldc-web/projets/startcoq/-/issues/28#note_9750 .
if (empty($row->NUM_RESEAU)) { if (empty($row->NUM_RESEAU)) {
continue; continue;
} }
// FIXME: thread https://lab.s-web.fr/ldc-web/projets/startcoq/-/issues/28#note_9758 . // NOTE: thread https://lab.s-web.fr/ldc-web/projets/startcoq/-/issues/28#note_9758 .
if (empty($row->NUM_SECTEUR)) { if (empty($row->NUM_SECTEUR)) {
continue; continue;
} }
// FIXME: thread https://lab.s-web.fr/ldc-web/projets/startcoq/-/issues/28#note_9760 . // NOTE: thread https://lab.s-web.fr/ldc-web/projets/startcoq/-/issues/28#note_9760 .
if (empty($row->NUM_RAYON)) { if (empty($row->NUM_RAYON)) {
continue; continue;
} }
// FIXME: thread https://lab.s-web.fr/ldc-web/projets/startcoq/-/issues/28#note_9767 . // NOTE: thread https://lab.s-web.fr/ldc-web/projets/startcoq/-/issues/28#note_9767 .
if (empty($row->NUM_FAMILLE)) { if (empty($row->NUM_FAMILLE)) {
continue; continue;
} }
$brandName = Str::of($row->MARQUE)->slug()->studly(); $brandName = Str::of($row->MARQUE)->trim()->title();
/** name is unique on brand */ /** name is unique on brand */
$brand = null; $brand = null;
try { try {
@ -264,9 +368,9 @@ return new class extends Migration
} catch (\Exception) { } catch (\Exception) {
throw new \Error("brand` {$brandName}` not found"); throw new \Error("brand` {$brandName}` not found");
} }
$url = $row->URL ? Str::of(url($row->URL))->lower() : null; $url = $row->URL ? Str::lower(url($row->URL)) : null;
$date_saisie = $row->DATE_SAISIE ? $this->tryToParseCarbon($row->DATE_SAISIE, true) : null; $date_saisie = $row->DATE_SAISIE ? $this->tryToParseCarbon($row->DATE_SAISIE, true) : null;
$is_tasted = Str::of($row->DEGUSTER_O_N)->lower() === 'o' ? true : false; $is_tasted = Str::lower($row->DEGUSTER_O_N) === 'o' ? true : false;
$claimList = collect([ $claimList = collect([
$row->ALLEG0, $row->ALLEG1, $row->ALLEG2 $row->ALLEG0, $row->ALLEG1, $row->ALLEG2
])->filter()->map(fn (string $alleg) => $resolveAllegation($alleg)); ])->filter()->map(fn (string $alleg) => $resolveAllegation($alleg));
@ -319,7 +423,26 @@ return new class extends Migration
/** @var \App\Models\Product $product */ /** @var \App\Models\Product $product */
$product = Product::query()->where('id', $row->{$index})->firstOrNew(); $product = Product::query()->where('id', $row->{$index})->firstOrNew();
$product->id = $row->NUM_VEILLE_PROD; $product->id = $row->NUM_VEILLE_PROD;
$fill = [
// * DOCS
$disk = $this->getMigrationStorage('Produit/' . strval($row->{$index}));
$files = $disk ? collect($disk->files())->mapWithKeys(function (string $file) use ($disk, $product) {
$filePath = FileStorageHelper::storeFile($product, new \SplFileInfo($disk->path($file)));
return [pathinfo($filePath, PATHINFO_BASENAME) => $filePath];
}) : null;
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundBeforeLastUsed
$pictureKey = $files ? $files->map(fn (string $filePath, string $fileName) => $fileName)
->first(function ($fileName) {
return preg_match('/(ref-?[0-9a-z]{0,}.jpg)/', $fileName) === 1;
}) : null;
$picture = $files ? $files->get($pictureKey) : null;
if ($files) {
$files->pull($pictureKey);
$files = $files->values();
}
$fill = [
'brand_id' => $brand->id, 'brand_id' => $brand->id,
'catalogue' => $row->CATALOGUE, 'catalogue' => $row->CATALOGUE,
'certification' => $certificationEnum, 'certification' => $certificationEnum,
@ -341,10 +464,11 @@ return new class extends Migration
'specified_claim' => $claim, 'specified_claim' => $claim,
'tasters_number' => $row->NB_DEGUST, 'tasters_number' => $row->NB_DEGUST,
'origins' => $originList, 'origins' => $originList,
'docs' => 'tttt', 'docs' => $files,
'picture_alt' => 'tttt', 'picture_alt' => "Image de {$row->NOM}",
'picture_title' => 'tttt', 'picture_title' => "Image de {$row->NOM}",
'picture' => 'tttt', 'picture' => $picture,
// NOTE: https://lab.s-web.fr/ldc-web/projets/startcoq/-/issues/28#note_10688 .
'user_id' => $this->dummyUser->id 'user_id' => $this->dummyUser->id
]; ];
if ($date_saisie) { if ($date_saisie) {
@ -369,18 +493,21 @@ return new class extends Migration
/** /**
* Create unique brands using veille products * Create unique brands using veille products
* *
* @param \Illuminate\Console\OutputStyle $style
* @return void * @return void
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.Found
*/ */
private function migrateBrands(): void private function migrateBrands(OutputStyle $style): void
{ {
// phpcs:enable
$query = $this->getQuery("VEILLE_PROD"); $query = $this->getQuery("VEILLE_PROD");
/** @var \Illuminate\Support\Collection<string,string> */ /** @var \Illuminate\Support\Collection<string,string> */
$brands = (clone $query)->pluck('MARQUE')->filter() $brands = (clone $query)->pluck('MARQUE')->filter()
->map(fn (string $brandName) => Str::of($brandName)->slug()->studly())->unique(); ->map(fn (string $brandName) => Str::of($brandName)->trim()->title())->unique();
$console = new ConsoleOutput(); $console = new ConsoleOutput();
$console->writeln(''); $console->writeln('');
$progress = new ProgressBar($console, $brands->count()); $progress = new ProgressBar($console, $brands->count());
$progress->setMessage(str_pad('Migrate Brands', 24, ' ')); $progress->setMessage(str_pad('Migrate Brands', $this->consoleTabulation, ' '));
$progress->setFormat(' - %message% [%bar%] %percent:3s%% %current%/%max% %elapsed:6s%/%estimated:-6s%'); $progress->setFormat(' - %message% [%bar%] %percent:3s%% %current%/%max% %elapsed:6s%/%estimated:-6s%');
// FIXME: Insert brand->filter and find from where it should be fetched. // FIXME: Insert brand->filter and find from where it should be fetched.
$brands->each(function ($brandName) use ($progress) { $brands->each(function ($brandName) use ($progress) {
@ -445,7 +572,7 @@ return new class extends Migration
}); });
Schema::table('keywordables', function (Blueprint $table) { Schema::table('keywordables', function (Blueprint $table) {
$table->foreign(['keyword_id'])->references(['id'])->on('keywords') $table->foreign(['keyword_id'])->references(['id'])->on('keywords')
->restrictOnUpdate()->cascadeOnDelete(); ->restrictOnUpdate()->cascadeOnDelete();
}); });
} }
@ -461,13 +588,42 @@ return new class extends Migration
}); });
Schema::table('processes', function (Blueprint $table) { Schema::table('processes', function (Blueprint $table) {
$table->foreign(['user_id'])->references(['id'])->on('users') $table->foreign(['user_id'])->references(['id'])->on('users')
->restrictOnUpdate()->restrictOnDelete(); ->restrictOnUpdate()->restrictOnDelete();
}); });
} }
// -- END DB CORRECTION -- // -- END DB CORRECTION --
// --- LIB METHODS --- // --- LIB METHODS ---
/**
* Get migration disk
*
* @param string|null $path Get a specific path.
* @return \Illuminate\Filesystem\FilesystemAdapter|null
* @throws \Error If veille folder is missing.
*/
private function getMigrationStorage(string $path = ''): \Illuminate\Filesystem\FilesystemAdapter|null
{
$storagePath = storage_path('app/migration');
$path = "/Veille/{$path}";
if (!File::exists("{$storagePath}{$path}")) {
return null;
}
$diskName = 'migration' . Str::of("{$storagePath}{$path}")->slug();
config()->set("filesystems.disks.{$diskName}", [
'driver' => 'local',
'root' => "{$storagePath}{$path}",
'throw' => false,
]);
/** @var \Illuminate\Filesystem\FilesystemAdapter */
$disk = Storage::disk($diskName);
return $disk;
}
/** /**
* Get Temp DB query builder * Get Temp DB query builder
* *
@ -513,7 +669,7 @@ return new class extends Migration
$tableComment = "{$tableColumn->name} {$tableColumn->type} {$tableColumn->size}"; $tableComment = "{$tableColumn->name} {$tableColumn->type} {$tableColumn->size}";
switch ($tableColumn->type) { switch ($tableColumn->type) {
case 'NUMBER': case 'NUMBER':
$table->bigInteger($tableColumn->name) $table->double($tableColumn->name)
->nullable()->comment($tableComment); ->nullable()->comment($tableComment);
break; break;
case 'VARCHAR2': case 'VARCHAR2':
@ -549,7 +705,7 @@ return new class extends Migration
$this->getTableName($tableName), $this->getTableName($tableName),
$header $header
); );
$message = str_pad("{$this->tablePrefix} {$tableName}", 24, ' '); $message = str_pad("{$this->tablePrefix} {$tableName}", $this->consoleTabulation, ' ');
$section->write(" - {$message} -> Fetching OCI DATA.."); $section->write(" - {$message} -> Fetching OCI DATA..");
$content = $this->fetchOciTableContent($tableName, $header); $content = $this->fetchOciTableContent($tableName, $header);
$section->clear(); $section->clear();
@ -561,7 +717,7 @@ return new class extends Migration
DB::table($this->getTableName($tableName))->insert((array)$row); DB::table($this->getTableName($tableName))->insert((array)$row);
} }
$progress->finish(); $progress->finish();
}//end foreach } //end foreach
} }
/** /**

View file

@ -5,6 +5,7 @@ namespace Database\Seeders;
use App\Enums\KeywordCategoryEnum; use App\Enums\KeywordCategoryEnum;
use App\Models\Keyword; use App\Models\Keyword;
use Illuminate\Database\Seeder; use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Log;
class KeywordInitSeeder extends Seeder class KeywordInitSeeder extends Seeder
{ {
@ -12,186 +13,324 @@ class KeywordInitSeeder extends Seeder
* Run the database seeds. * Run the database seeds.
* *
* @return void * @return void
* @phpcs:disable Generic.Metrics.CyclomaticComplexity.MaxExceeded
*/ */
public function run() public function run()
{ {
// phpcs:enable
/** /**
* * INGREDIENTS * * INGREDIENTS
*/ */
$keyword = new Keyword(); try {
$keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[0]; $keyword = new Keyword();
$keyword->label = "Sophistication"; $keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[0];
$keyword->saveOrFail(); $keyword->label = "Sophistication";
$keyword->saveQuietly();
} catch (\Illuminate\Database\QueryException $e) {
Log::debug($e->getMessage());
}
$keyword = new Keyword(); try {
$keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[0]; $keyword = new Keyword();
$keyword->label = "Exotisme"; $keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[0];
$keyword->saveOrFail(); $keyword->label = "Exotisme";
$keyword->saveQuietly();
} catch (\Illuminate\Database\QueryException $e) {
Log::debug($e->getMessage());
}
$keyword = new Keyword(); try {
$keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[0]; $keyword = new Keyword();
$keyword->label = "Fun"; $keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[0];
$keyword->saveOrFail(); $keyword->label = "Fun";
$keyword->saveQuietly();
} catch (\Illuminate\Database\QueryException $e) {
Log::debug($e->getMessage());
}
$keyword = new Keyword(); try {
$keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[1]; $keyword = new Keyword();
$keyword->label = "Clean label/naturalité"; $keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[1];
$keyword->saveOrFail(); $keyword->label = "Clean label/naturalité";
$keyword->saveQuietly();
} catch (\Illuminate\Database\QueryException $e) {
Log::debug($e->getMessage());
}
$keyword = new Keyword(); try {
$keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[1]; $keyword = new Keyword();
$keyword->label = "Végétarien/vegan"; $keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[1];
$keyword->saveOrFail(); $keyword->label = "Végétarien/vegan";
$keyword->saveQuietly();
} catch (\Illuminate\Database\QueryException $e) {
Log::debug($e->getMessage());
}
$keyword = new Keyword(); try {
$keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[1]; $keyword = new Keyword();
$keyword->label = "Bien être"; $keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[1];
$keyword->saveOrFail(); $keyword->label = "Bien être";
$keyword->saveQuietly();
} catch (\Illuminate\Database\QueryException $e) {
Log::debug($e->getMessage());
}
$keyword = new Keyword(); try {
$keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[2]; $keyword = new Keyword();
$keyword->label = "Solidarité"; $keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[2];
$keyword->saveOrFail(); $keyword->label = "Solidarité";
$keyword->saveQuietly();
} catch (\Illuminate\Database\QueryException $e) {
Log::debug($e->getMessage());
}
$keyword = new Keyword(); try {
$keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[2]; $keyword = new Keyword();
$keyword->label = "Écologie"; $keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[2];
$keyword->saveOrFail(); $keyword->label = "Écologie";
$keyword->saveQuietly();
} catch (\Illuminate\Database\QueryException $e) {
Log::debug($e->getMessage());
}
$keyword = new Keyword(); try {
$keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[3]; $keyword = new Keyword();
$keyword->label = "Emballage individuel"; $keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[3];
$keyword->saveOrFail(); $keyword->label = "Emballage individuel";
$keyword->saveQuietly();
} catch (\Illuminate\Database\QueryException $e) {
Log::debug($e->getMessage());
}
$keyword = new Keyword(); try {
$keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[3]; $keyword = new Keyword();
$keyword->label = "Facilité de mise en oeuvre"; $keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[3];
$keyword->saveOrFail(); $keyword->label = "Facilité de mise en oeuvre";
$keyword->saveQuietly();
} catch (\Illuminate\Database\QueryException $e) {
Log::debug($e->getMessage());
}
/** /**
* * PRODUCTS * * PRODUCTS
*/ */
$keyword = new Keyword(); try {
$keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[4]; $keyword = new Keyword();
$keyword->label = "Sophistication"; $keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[4];
$keyword->saveOrFail(); $keyword->label = "Sophistication";
$keyword->saveQuietly();
} catch (\Illuminate\Database\QueryException $e) {
Log::debug($e->getMessage());
}
$keyword = new Keyword(); try {
$keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[4]; $keyword = new Keyword();
$keyword->label = "Exotisme"; $keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[4];
$keyword->saveOrFail(); $keyword->label = "Exotisme";
$keyword->saveQuietly();
} catch (\Illuminate\Database\QueryException $e) {
Log::debug($e->getMessage());
}
$keyword = new Keyword(); try {
$keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[4]; $keyword = new Keyword();
$keyword->label = "Variété des sens"; $keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[4];
$keyword->saveOrFail(); $keyword->label = "Variété des sens";
$keyword->saveQuietly();
} catch (\Illuminate\Database\QueryException $e) {
Log::debug($e->getMessage());
}
$keyword = new Keyword(); try {
$keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[4]; $keyword = new Keyword();
$keyword->label = "Fun"; $keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[4];
$keyword->saveOrFail(); $keyword->label = "Fun";
$keyword->saveQuietly();
} catch (\Illuminate\Database\QueryException $e) {
Log::debug($e->getMessage());
}
$keyword = new Keyword(); try {
$keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[5]; $keyword = new Keyword();
$keyword->label = "Minceur"; $keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[5];
$keyword->saveOrFail(); $keyword->label = "Minceur";
$keyword->saveQuietly();
} catch (\Illuminate\Database\QueryException $e) {
Log::debug($e->getMessage());
}
$keyword = new Keyword(); try {
$keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[5]; $keyword = new Keyword();
$keyword->label = "Énergie/Bien-être"; $keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[5];
$keyword->saveOrFail(); $keyword->label = "Énergie/Bien-être";
$keyword->saveQuietly();
} catch (\Illuminate\Database\QueryException $e) {
Log::debug($e->getMessage());
}
$keyword = new Keyword(); try {
$keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[5]; $keyword = new Keyword();
$keyword->label = "Cosmétique"; $keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[5];
$keyword->saveOrFail(); $keyword->label = "Cosmétique";
$keyword->saveQuietly();
} catch (\Illuminate\Database\QueryException $e) {
Log::debug($e->getMessage());
}
$keyword = new Keyword(); try {
$keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[6]; $keyword = new Keyword();
$keyword->label = "Manipulation produit"; $keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[6];
$keyword->saveOrFail(); $keyword->label = "Manipulation produit";
$keyword->saveQuietly();
} catch (\Illuminate\Database\QueryException $e) {
Log::debug($e->getMessage());
}
$keyword = new Keyword(); try {
$keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[6]; $keyword = new Keyword();
$keyword->label = "Gain de temps"; $keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[6];
$keyword->saveOrFail(); $keyword->label = "Gain de temps";
$keyword->saveQuietly();
} catch (\Illuminate\Database\QueryException $e) {
Log::debug($e->getMessage());
}
$keyword = new Keyword(); try {
$keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[6]; $keyword = new Keyword();
$keyword->label = "Nomadisme"; $keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[6];
$keyword->saveOrFail(); $keyword->label = "Nomadisme";
$keyword->saveQuietly();
} catch (\Illuminate\Database\QueryException $e) {
Log::debug($e->getMessage());
}
$keyword = new Keyword(); try {
$keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[7]; $keyword = new Keyword();
$keyword->label = "Naturalité"; $keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[7];
$keyword->saveOrFail(); $keyword->label = "Naturalité";
$keyword->saveQuietly();
} catch (\Illuminate\Database\QueryException $e) {
Log::debug($e->getMessage());
}
$keyword = new Keyword(); try {
$keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[7]; $keyword = new Keyword();
$keyword->label = "Végétal"; $keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[7];
$keyword->saveOrFail(); $keyword->label = "Végétal";
$keyword->saveQuietly();
} catch (\Illuminate\Database\QueryException $e) {
Log::debug($e->getMessage());
}
$keyword = new Keyword(); try {
$keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[7]; $keyword = new Keyword();
$keyword->label = "Médical"; $keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[7];
$keyword->saveOrFail(); $keyword->label = "Médical";
$keyword->saveQuietly();
} catch (\Illuminate\Database\QueryException $e) {
Log::debug($e->getMessage());
}
$keyword = new Keyword(); try {
$keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[8]; $keyword = new Keyword();
$keyword->label = "Solidarité"; $keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[8];
$keyword->saveOrFail(); $keyword->label = "Solidarité";
$keyword->saveQuietly();
} catch (\Illuminate\Database\QueryException $e) {
Log::debug($e->getMessage());
}
$keyword = new Keyword(); try {
$keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[8]; $keyword = new Keyword();
$keyword->label = "Écologie"; $keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[8];
$keyword->saveOrFail(); $keyword->label = "Écologie";
$keyword->saveQuietly();
} catch (\Illuminate\Database\QueryException $e) {
Log::debug($e->getMessage());
}
/** /**
* * PACKAGING * * PACKAGING
*/ */
$keyword = new Keyword(); try {
$keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[9]; $keyword = new Keyword();
$keyword->label = "Sophistication"; $keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[9];
$keyword->saveOrFail(); $keyword->label = "Sophistication";
$keyword->saveQuietly();
} catch (\Illuminate\Database\QueryException $e) {
Log::debug($e->getMessage());
}
$keyword = new Keyword(); try {
$keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[9]; $keyword = new Keyword();
$keyword->label = "Exotisme"; $keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[9];
$keyword->saveOrFail(); $keyword->label = "Exotisme";
$keyword->saveQuietly();
} catch (\Illuminate\Database\QueryException $e) {
Log::debug($e->getMessage());
}
$keyword = new Keyword(); try {
$keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[9]; $keyword = new Keyword();
$keyword->label = "Fun"; $keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[9];
$keyword->saveOrFail(); $keyword->label = "Fun";
$keyword->saveQuietly();
} catch (\Illuminate\Database\QueryException $e) {
Log::debug($e->getMessage());
}
$keyword = new Keyword(); try {
$keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[10]; $keyword = new Keyword();
$keyword->label = "Manipulation produit"; $keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[10];
$keyword->saveOrFail(); $keyword->label = "Manipulation produit";
$keyword->saveQuietly();
} catch (\Illuminate\Database\QueryException $e) {
Log::debug($e->getMessage());
}
$keyword = new Keyword(); try {
$keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[10]; $keyword = new Keyword();
$keyword->label = "Gain de temps"; $keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[10];
$keyword->saveOrFail(); $keyword->label = "Gain de temps";
$keyword->saveQuietly();
} catch (\Illuminate\Database\QueryException $e) {
Log::debug($e->getMessage());
}
$keyword = new Keyword(); try {
$keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[10]; $keyword = new Keyword();
$keyword->label = "Nomadisme"; $keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[10];
$keyword->saveOrFail(); $keyword->label = "Nomadisme";
$keyword->saveQuietly();
} catch (\Illuminate\Database\QueryException $e) {
Log::debug($e->getMessage());
}
$keyword = new Keyword(); try {
$keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[11]; $keyword = new Keyword();
$keyword->label = "Décor/forme"; $keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[11];
$keyword->saveOrFail(); $keyword->label = "Décor/forme";
$keyword->saveQuietly();
} catch (\Illuminate\Database\QueryException $e) {
Log::debug($e->getMessage());
}
$keyword = new Keyword(); try {
$keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[11]; $keyword = new Keyword();
$keyword->label = "Matériaux"; $keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[11];
$keyword->saveOrFail(); $keyword->label = "Matériaux";
$keyword->saveQuietly();
} catch (\Illuminate\Database\QueryException $e) {
Log::debug($e->getMessage());
}
$keyword = new Keyword(); try {
$keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[12]; $keyword = new Keyword();
$keyword->label = "Écologie"; $keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[12];
$keyword->saveOrFail(); $keyword->label = "Écologie";
$keyword->saveQuietly();
} catch (\Illuminate\Database\QueryException $e) {
Log::debug($e->getMessage());
}
} }
} }

39
resources/lang/fr.json Normal file
View file

@ -0,0 +1,39 @@
{
"(and :count more error)": "(et :count erreur en plus)",
"(and :count more errors)": "(et :count erreurs en plus)",
"All rights reserved.": "Tous droits réservés.",
"Forbidden": "Interdit",
"Go to page :page": "Aller à la page :page",
"Hello!": "Bonjour !",
"If you did not create an account, no further action is required.": "Si vous n'avez pas créé de compte, vous pouvez ignorer ce message.",
"If you did not request a password reset, no further action is required.": "Si vous n'avez pas demandé de réinitialisation de mot de passe, vous pouvez ignorer ce message.",
"If you're having trouble clicking the \":actionText\" button, copy and paste the URL below\ninto your web browser:": "Si vous avez des difficultés à cliquer sur le bouton \":actionText\", copiez et collez l'URL ci-dessous\ndans votre navigateur Web :",
"Invalid JSON was returned from the route.": "Un JSON non valide a été renvoyé par la route.",
"Login": "Connexion",
"Logout": "Déconnexion",
"Not Found": "Non trouvé",
"of": "de",
"Page Expired": "Page expirée",
"Pagination Navigation": "Pagination",
"Payment Required": "Paiement requis",
"Please click the button below to verify your email address.": "Veuillez cliquer sur le bouton ci-dessous pour vérifier votre adresse e-mail :",
"Regards": "Cordialement",
"Register": "Inscription",
"Reset Password": "Réinitialisation du mot de passe",
"Reset Password Notification": "Notification de réinitialisation du mot de passe",
"results": "résultats",
"Server Error": "Erreur serveur",
"Service Unavailable": "Service indisponible",
"Showing": "Montrant",
"The given data was invalid.": "La donnée renseignée est incorrecte.",
"The response is not a streamed response.": "La réponse n'est pas une réponse diffusée.",
"The response is not a view.": "La réponse n'est pas une vue.",
"This password reset link will expire in :count minutes.": "Ce lien de réinitialisation du mot de passe expirera dans :count minutes.",
"to": "à",
"Toggle navigation": "Afficher / masquer le menu de navigation",
"Too Many Requests": "Trop de requêtes",
"Unauthorized": "Non autorisé",
"Verify Email Address": "Vérifier l'adresse e-mail",
"Whoops!": "Oups !",
"You are receiving this email because we received a password reset request for your account.": "Vous recevez cet e-mail car nous avons reçu une demande de réinitialisation de mot de passe pour votre compte."
}

View file

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
return [
'failed' => 'Ces identifiants ne correspondent pas à nos enregistrements.',
'password' => 'Le mot de passe est incorrect',
'throttle' => 'Tentatives de connexion trop nombreuses. Veuillez essayer de nouveau dans :seconds secondes.',
];

View file

@ -0,0 +1,83 @@
<?php
return [
'actions' => [
'list' => 'Liste',
'view' => 'Voir',
'create' => 'Créer',
'duplicate' => 'Dupliquer',
'update' => 'Éditer',
'delete' => 'Supprimer',
'save' => 'Sauvegarder',
'edit' => 'Mettre à jour',
],
'helpers' => [
'list' => 'Liste',
'list_all' => 'Lister tous les :model',
'create_model' => 'Créer :model',
'view_model' => 'Voir :model',
'update_model' => 'Éditer :model',
'duplicate_model' => 'Dupliquer :model',
'delete_model' => 'Supprimer :model',
'save_model' => 'Sauvegarder :model',
'edit_model' => 'Mettre à jour :model',
'no_results' => 'Aucun résultat pour :model',
'publish' => 'Publié',
'unpublish' => 'Dépublié',
],
'libs' => [
'toolbox' => [
'assertFieldIsUnique' => 'La :valeur du champ :attribut est déjà utilisée',
'assertNoInternalBoUrls' => ':attribute : Il est interdit de coller l\'uri de back-office dans ce champ',
'assertNoInternalUrls' => ':attribute : Il est interdit de coller une url de :appname dans ce champ.',
]
],
'messages' => [
'has_been_created' => ':model a été créé',
'cannot_be_created' => ':model ne peut être créé',
'has_been_updated' => ':model a été mis à jour',
'cannot_be_updated' => ':model ne peut être mis à jour',
'has_been_deleted' => ':model a été supprimé',
'cannot_be_deleted' => ':model ne peut être supprimé',
'order_changed' => 'L\'ordre a été changé !',
'order_not_changed' => 'L\'ordre n\'a pas été changé !',
'publish_status_saved' => 'Changement de l\'état de publication réussit !',
'right' => 'Vous n\'avez pas les droits pour accéder à cette partie !'
],
'minicrud' => [
'visibility_label' => 'Visibilité',
'visibility_helper' => 'Vérifier qu\'il est visible.',
'enum_select' => 'Sélectionnez :enum dans la liste.',
'associated_enum' => 'Associé :enum',
'dimensions_must_be' => 'Doit être :widthpx (largeur) par :heightpx (hauteur).',
'picture_title_helper' => 'Utile pour les utilisateurs, s\'affiche en passant la souris sur l\'image',
'picture_title_label' => 'Attribut du titre de l\'image',
'picture_alt_helper' => 'Doit décrire votre image pour les moteurs de recherche et les aveugles (5 à 6 mots)',
'picture_alt_label' => 'Attribut Alt de l\'image',
'text_warning' => 'Relisez votre contenu avant de le publier',
],
'pagination' => [
"previous" => "Page précédente",
"next" => "Page suivante",
"specific_page" => "Accéder à la page n°:id",
'paginate_list' => 'Nombre d\'éléments par page',
],
'other' => [
'up' => 'Modifier l\'ordre d\'apparition vers le haut',
'down' => 'Modifier l\'ordre d\'apparition vers le bas',
'no_model_found' => 'Aucun :model trouvés',
'publish' => 'Publié',
'unpublish' => 'Dépublié',
'sort_ascending' => 'Trier :name dans l\'ordre croissant',
'sort_descending' => 'Trier :name dans l\'ordre décroissant',
'sort_delete' => 'Supprimer le tri',
'sort_arrow' => 'Ici, les flèches sont utilisées pour trier et non pour filtrer',
'right' => 'Vous n\'avez pas les droits',
]
];

View file

@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
return [
'0' => 'Erreur inconnue',
'100' => 'Continuer',
'101' => 'Protocoles de commutation',
'102' => 'En traitement',
'200' => 'D\'accord',
'201' => 'Créé',
'202' => 'Accepté',
'203' => 'Informations non autorisées',
'204' => 'Pas content',
'205' => 'Réinitialiser le contenu',
'206' => 'Contenu partiel',
'207' => 'Multi-statut',
'208' => 'Déjà rapporté',
'226' => 'J\'ai l\'habitude',
'300' => 'Choix multiples',
'301' => 'Déplacé de façon permanente',
'302' => 'A trouvé',
'303' => 'Voir autre',
'304' => 'Non modifié',
'305' => 'Utiliser un proxy',
'307' => 'Redirection temporaire',
'308' => 'Redirection permanente',
'400' => 'Mauvaise Demande',
'401' => 'Non autorisé',
'402' => 'Paiement Requis',
'403' => 'Interdit',
'404' => 'Page non trouvée',
'405' => 'Méthode Non Autorisée',
'406' => 'Pas acceptable',
'407' => 'Authentification proxy requise',
'408' => 'Délai d\'expiration de la demande',
'409' => 'Conflit',
'410' => 'Disparu',
'411' => 'Longueur requise',
'412' => 'La précondition a échoué',
'413' => 'Charge utile trop grande',
'414' => 'URI trop long',
'415' => 'Type de support non supporté',
'416' => 'Plage non satisfaisante',
'417' => 'Échec de l\'attente',
'418' => 'Je suis une théière',
'419' => 'La session a expiré',
'421' => 'Demande mal dirigée',
'422' => 'Entité non traitable',
'423' => 'Fermé à clef',
'424' => 'Dépendance échouée',
'425' => 'Trop tôt',
'426' => 'Mise à niveau requise',
'428' => 'Condition préalable requise',
'429' => 'Trop de demandes',
'431' => 'Demander des champs d\'en-tête trop grands',
'444' => 'Connexion fermée sans réponse',
'449' => 'Réessayer avec',
'451' => 'Indisponible pour des raisons légales',
'499' => 'Demande fermée du client',
'500' => 'Erreur Interne du Serveur',
'501' => 'Pas mis en œuvre',
'502' => 'Mauvaise passerelle',
'503' => 'Mode de Maintenance',
'504' => 'Délai d\'attente de la passerelle',
'505' => 'Version HTTP non prise en charge',
'506' => 'La variante négocie également',
'507' => 'Espace insuffisant',
'508' => 'Boucle détectée',
'509' => 'Limite de bande passante dépassée',
'510' => 'Non prolongé',
'511' => 'Authentification réseau requise',
'520' => 'Erreur inconnue',
'521' => 'Le serveur Web est en panne',
'522' => 'La connexion a expiré',
'523' => 'L\'origine est inaccessible',
'524' => 'Un dépassement de délai s\'est produit',
'525' => 'Échec de la prise de contact SSL',
'526' => 'Certificat SSL invalide',
'527' => 'Erreur de canon ferroviaire',
'598' => 'Erreur de délai de lecture réseau',
'599' => 'Erreur de délai de connexion réseau',
'unknownError' => 'Erreur inconnue',
];

View file

@ -3,6 +3,7 @@
return [ return [
'classes' => [ 'classes' => [
'products' => 'Produits', 'products' => 'Produits',
'products_monitoring' => 'Veille produit|Veilles produit',
'brands' => 'Marques', 'brands' => 'Marques',
] ]
]; ];

View file

@ -0,0 +1,8 @@
<?php
declare(strict_types=1);
return [
'next' => 'Suivant &raquo;',
'previous' => '&laquo; Précédent',
];

View file

@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
return [
'reset' => 'Votre mot de passe a été réinitialisé !',
'sent' => 'Nous vous avons envoyé par email le lien de réinitialisation du mot de passe !',
'throttled' => 'Veuillez patienter avant de réessayer.',
'token' => 'Ce jeton de réinitialisation du mot de passe n\'est pas valide.',
'user' => 'Aucun utilisateur n\'a été trouvé avec cette adresse email.',
];

View file

@ -1,5 +1,7 @@
<?php <?php
declare(strict_types=1);
return [ return [
'accepted' => 'Le champ :attribute doit être accepté.', 'accepted' => 'Le champ :attribute doit être accepté.',
'accepted_if' => 'Le champ :attribute doit être accepté quand :other a la valeur :value.', 'accepted_if' => 'Le champ :attribute doit être accepté quand :other a la valeur :value.',
@ -10,6 +12,7 @@ return [
'alpha_dash' => 'Le champ :attribute doit contenir uniquement des lettres, des chiffres et des tirets.', 'alpha_dash' => 'Le champ :attribute doit contenir uniquement des lettres, des chiffres et des tirets.',
'alpha_num' => 'Le champ :attribute doit contenir uniquement des chiffres et des lettres.', 'alpha_num' => 'Le champ :attribute doit contenir uniquement des chiffres et des lettres.',
'array' => 'Le champ :attribute doit être un tableau.', 'array' => 'Le champ :attribute doit être un tableau.',
'ascii' => 'Le champ :attribute ne doit contenir que des caractères alphanumériques et des symboles codés sur un octet.',
'before' => 'Le champ :attribute doit être une date antérieure au :date.', 'before' => 'Le champ :attribute doit être une date antérieure au :date.',
'before_or_equal' => 'Le champ :attribute doit être une date antérieure ou égale au :date.', 'before_or_equal' => 'Le champ :attribute doit être une date antérieure ou égale au :date.',
'between' => [ 'between' => [
@ -19,11 +22,18 @@ return [
'string' => 'Le texte :attribute doit contenir entre :min et :max caractères.', 'string' => 'Le texte :attribute doit contenir entre :min et :max caractères.',
], ],
'boolean' => 'Le champ :attribute doit être vrai ou faux.', 'boolean' => 'Le champ :attribute doit être vrai ou faux.',
'can' => 'Le champ :attribute contient une valeur non autorisée.',
'color' => ':attribute doit être un code couleur valide (hexadecimal, RGB, RGBA ou un nom de couleur CSS)',
'color_hex' => ':attribute doit être un code couleur hexadecimal valide',
'color_name' => ':attribute doit être un nom de couleur CSS valide',
'color_rgb' => ':attribute doit être un code couleur RGB valide',
'color_rgba' => ':attribute doit être un code couleur RGBA valide',
'confirmed' => 'Le champ de confirmation :attribute ne correspond pas.', 'confirmed' => 'Le champ de confirmation :attribute ne correspond pas.',
'current_password' => 'Le mot de passe est incorrect.', 'current_password' => 'Le mot de passe est incorrect.',
'date' => 'Le champ :attribute n\'est pas une date valide.', 'date' => 'Le champ :attribute n\'est pas une date valide.',
'date_equals' => 'Le champ :attribute doit être une date égale à :date.', 'date_equals' => 'Le champ :attribute doit être une date égale à :date.',
'date_format' => 'Le champ :attribute ne correspond pas au format :format.', 'date_format' => 'Le champ :attribute ne correspond pas au format :format.',
'decimal' => 'Le champ :attribute doit comporter :decimal décimales.',
'declined' => 'Le champ :attribute doit être décliné.', 'declined' => 'Le champ :attribute doit être décliné.',
'declined_if' => 'Le champ :attribute doit être décliné quand :other a la valeur :value.', 'declined_if' => 'Le champ :attribute doit être décliné quand :other a la valeur :value.',
'different' => 'Les champs :attribute et :other doivent être différents.', 'different' => 'Les champs :attribute et :other doivent être différents.',
@ -31,6 +41,8 @@ return [
'digits_between' => 'Le champ :attribute doit contenir entre :min et :max chiffres.', 'digits_between' => 'Le champ :attribute doit contenir entre :min et :max chiffres.',
'dimensions' => 'La taille de l\'image :attribute n\'est pas conforme.', 'dimensions' => 'La taille de l\'image :attribute n\'est pas conforme.',
'distinct' => 'Le champ :attribute a une valeur en double.', 'distinct' => 'Le champ :attribute a une valeur en double.',
'doesnt_end_with' => 'Le champ :attribute ne doit pas finir avec une des valeurs suivantes : :values.',
'doesnt_start_with' => 'Le champ :attribute ne doit pas commencer avec une des valeurs suivantes : :values.',
'email' => 'Le champ :attribute doit être une adresse e-mail valide.', 'email' => 'Le champ :attribute doit être une adresse e-mail valide.',
'ends_with' => 'Le champ :attribute doit se terminer par une des valeurs suivantes : :values', 'ends_with' => 'Le champ :attribute doit se terminer par une des valeurs suivantes : :values',
'enum' => 'Le champ :attribute sélectionné est invalide.', 'enum' => 'Le champ :attribute sélectionné est invalide.',
@ -57,6 +69,7 @@ return [
'ipv4' => 'Le champ :attribute doit être une adresse IPv4 valide.', 'ipv4' => 'Le champ :attribute doit être une adresse IPv4 valide.',
'ipv6' => 'Le champ :attribute doit être une adresse IPv6 valide.', 'ipv6' => 'Le champ :attribute doit être une adresse IPv6 valide.',
'json' => 'Le champ :attribute doit être un document JSON valide.', 'json' => 'Le champ :attribute doit être un document JSON valide.',
'lowercase' => 'Le champ :attribute doit être en minuscules.',
'lt' => [ 'lt' => [
'array' => 'Le tableau :attribute doit contenir moins de :value éléments.', 'array' => 'Le tableau :attribute doit contenir moins de :value éléments.',
'file' => 'La taille du fichier de :attribute doit être inférieure à :value kilo-octets.', 'file' => 'La taille du fichier de :attribute doit être inférieure à :value kilo-octets.',
@ -71,24 +84,39 @@ return [
], ],
'mac_address' => 'Le champ :attribute doit être une adresse MAC valide.', 'mac_address' => 'Le champ :attribute doit être une adresse MAC valide.',
'max' => [ 'max' => [
'array' => 'Le tableau :attribute ne peut contenir plus de :max éléments.', 'array' => 'Le tableau :attribute ne peut pas contenir plus que :max éléments.',
'file' => 'La taille du fichier de :attribute ne peut pas dépasser :max kilo-octets.', 'file' => 'La taille du fichier de :attribute ne peut pas dépasser :max kilo-octets.',
'numeric' => 'La valeur de :attribute ne peut être supérieure à :max.', 'numeric' => 'La valeur de :attribute ne peut pas être supérieure à :max.',
'string' => 'Le texte de :attribute ne peut contenir plus de :max caractères.', 'string' => 'Le texte de :attribute ne peut pas contenir plus de :max caractères.',
], ],
'max_digits' => 'Le champ :attribute ne doit pas avoir plus de :max chiffres.',
'mimes' => 'Le champ :attribute doit être un fichier de type : :values.', 'mimes' => 'Le champ :attribute doit être un fichier de type : :values.',
'mimetypes' => 'Le champ :attribute doit être un fichier de type : :values.', 'mimetypes' => 'Le champ :attribute doit être un fichier de type : :values.',
'min' => [ 'min' => [
'array' => 'Le tableau :attribute doit contenir au moins :min éléments.', 'array' => 'Le tableau :attribute doit contenir au moins :min éléments.',
'file' => 'La taille du fichier de :attribute doit être supérieure à :min kilo-octets.', 'file' => 'La taille du fichier de :attribute doit être supérieure ou égale à :min kilo-octets.',
'numeric' => 'La valeur de :attribute doit être supérieure ou égale à :min.', 'numeric' => 'La valeur de :attribute doit être supérieure ou égale à :min.',
'string' => 'Le texte :attribute doit contenir au moins :min caractères.', 'string' => 'Le texte de :attribute doit contenir au moins :min caractères.',
], ],
'min_digits' => 'Le champ :attribute doit avoir au moins :min chiffres.',
'missing' => 'Le champ :attribute doit être manquant.',
'missing_if' => 'Le champ :attribute doit être manquant quand :other a la valeur :value.',
'missing_unless' => 'Le champ :attribute doit être manquant sauf si :other a la valeur :value.',
'missing_with' => 'Le champ :attribute doit être manquant quand :values est présent.',
'missing_with_all' => 'Le champ :attribute doit être manquant quand :values sont présents.',
'multiple_of' => 'La valeur de :attribute doit être un multiple de :value', 'multiple_of' => 'La valeur de :attribute doit être un multiple de :value',
'not_in' => 'Le champ :attribute sélectionné n\'est pas valide.', 'not_in' => 'Le champ :attribute sélectionné n\'est pas valide.',
'not_regex' => 'Le format du champ :attribute n\'est pas valide.', 'not_regex' => 'Le format du champ :attribute n\'est pas valide.',
'numeric' => 'Le champ :attribute doit contenir un nombre.', 'numeric' => 'Le champ :attribute doit contenir un nombre.',
'password' => 'Le mot de passe est incorrect', 'password' => [
'letters' => 'Le champ :attribute doit contenir au moins une lettre.',
'mixed' => 'Le champ :attribute doit contenir au moins une majuscule et une minuscule.',
'numbers' => 'Le champ :attribute doit contenir au moins un chiffre.',
'symbols' => 'Le champ :attribute doit contenir au moins un symbole.',
'uncompromised' => 'La valeur du champ :attribute est apparue dans une fuite de données. Veuillez choisir une valeur différente.',
],
'phone' => 'Le champ :attribute n\'est pas un numéro de téléphone valide.',
'postal_code' => 'Le champ :attribute accepte les codes postaux des pays suivants :countries comme par exemple :examples.',
'present' => 'Le champ :attribute doit être présent.', 'present' => 'Le champ :attribute doit être présent.',
'prohibited' => 'Le champ :attribute est interdit.', 'prohibited' => 'Le champ :attribute est interdit.',
'prohibited_if' => 'Le champ :attribute est interdit quand :other a la valeur :value.', 'prohibited_if' => 'Le champ :attribute est interdit quand :other a la valeur :value.',
@ -98,6 +126,7 @@ return [
'required' => 'Le champ :attribute est obligatoire.', 'required' => 'Le champ :attribute est obligatoire.',
'required_array_keys' => 'Le champ :attribute doit contenir des entrées pour : :values.', 'required_array_keys' => 'Le champ :attribute doit contenir des entrées pour : :values.',
'required_if' => 'Le champ :attribute est obligatoire quand la valeur de :other est :value.', 'required_if' => 'Le champ :attribute est obligatoire quand la valeur de :other est :value.',
'required_if_accepted' => 'Le champ :attribute est obligatoire quand le champ :other a été accepté.',
'required_unless' => 'Le champ :attribute est obligatoire sauf si :other est :values.', 'required_unless' => 'Le champ :attribute est obligatoire sauf si :other est :values.',
'required_with' => 'Le champ :attribute est obligatoire quand :values est présent.', 'required_with' => 'Le champ :attribute est obligatoire quand :values est présent.',
'required_with_all' => 'Le champ :attribute est obligatoire quand :values sont présents.', 'required_with_all' => 'Le champ :attribute est obligatoire quand :values sont présents.',
@ -113,16 +142,83 @@ return [
'starts_with' => 'Le champ :attribute doit commencer avec une des valeurs suivantes : :values', 'starts_with' => 'Le champ :attribute doit commencer avec une des valeurs suivantes : :values',
'string' => 'Le champ :attribute doit être une chaîne de caractères.', 'string' => 'Le champ :attribute doit être une chaîne de caractères.',
'timezone' => 'Le champ :attribute doit être un fuseau horaire valide.', 'timezone' => 'Le champ :attribute doit être un fuseau horaire valide.',
'ulid' => 'Le champ :attribute doit être un ULID valide.',
'unique' => 'La valeur du champ :attribute est déjà utilisée.', 'unique' => 'La valeur du champ :attribute est déjà utilisée.',
'uploaded' => 'Le fichier du champ :attribute n\'a pu être téléversé.', 'uploaded' => 'Le fichier du champ :attribute n\'a pu être téléversé.',
'uppercase' => 'Le champ :attribute doit être en majuscules.',
'url' => 'Le format de l\'URL de :attribute n\'est pas valide.', 'url' => 'Le format de l\'URL de :attribute n\'est pas valide.',
'uuid' => 'Le champ :attribute doit être un UUID valide', 'uuid' => 'Le champ :attribute doit être un UUID valide',
'custom' => [], 'attributes' => [
'phone' => 'Le champ :attribute n\'est pas un numéro de téléphone valide.', 'address' => 'adresse',
'color' => ':attribute doit être un code couleur valide (hexadecimal, RGB, RGBA ou un nom de couleur CSS)', 'age' => 'âge',
'color_hex' => ':attribute doit être un code couleur hexadecimal valide', 'amount' => 'montant',
'color_rgb' => ':attribute doit être un code couleur RGB valide', 'area' => 'zone',
'color_rgba' => ':attribute doit être un code couleur RGBA valide', 'available' => 'disponible',
'color_name' => ':attribute doit être un nom de couleur CSS valide', 'birthday' => 'anniversaire',
'postal_code' => 'Le champ :attribute accepte les codes postaux des pays suivants :countries comme par exemple :examples.' 'body' => 'corps',
'city' => 'ville',
'content' => 'contenu',
'country' => 'pays',
'created_at' => 'créé à',
'creator' => 'créateur',
'current_password' => 'mot de passe actuel',
'date' => 'Date',
'date_of_birth' => 'date de naissance',
'day' => 'jour',
'deleted_at' => 'supprimé à',
'description' => 'la description',
'district' => 'quartier',
'duration' => 'durée',
'email' => 'adresse e-mail',
'excerpt' => 'extrait',
'filter' => 'filtre',
'first_name' => 'prénom',
'gender' => 'genre',
'group' => 'groupe',
'hour' => 'heure',
'image' => 'image',
'last_name' => 'nom',
'lesson' => 'leçon',
'line_address_1' => 'ligne d\'adresse 1',
'line_address_2' => 'ligne d\'adresse 2',
'message' => 'message',
'middle_name' => 'deuxième prénom',
'minute' => 'minute',
'mobile' => 'portable',
'month' => 'mois',
'name' => 'nom',
'national_code' => 'code national',
'number' => 'numéro',
'password' => 'mot de passe',
'password_confirmation' => 'confirmation du mot de passe',
'phone' => 'téléphone',
'photo' => 'photo',
'postal_code' => 'code postal',
'price' => 'prix',
'province' => 'région',
'recaptcha_response_field' => 'champ de réponse recaptcha',
'remember' => 'se souvenir',
'restored_at' => 'restauré à',
'result_text_under_image' => 'texte de résultat sous l\'image',
'role' => 'rôle',
'second' => 'seconde',
'sex' => 'sexe',
'short_text' => 'texte court',
'size' => 'taille',
'state' => 'état',
'street' => 'rue',
'student' => 'étudiant',
'subject' => 'sujet',
'teacher' => 'professeur',
'terms' => 'conditions',
'test_description' => 'description test',
'test_locale' => 'localisation test',
'test_name' => 'nom test',
'text' => 'texte',
'time' => 'heure',
'title' => 'titre',
'updated_at' => 'mis à jour à',
'username' => 'nom d\'utilisateur',
'year' => 'année',
],
]; ];

View file

@ -3,7 +3,9 @@
} }
.bo-image-preview { .bo-image-preview {
max-height: 60px; // max-height: 60px;
width: 180px;
height: auto;
} }
.text-ellipsis { .text-ellipsis {
@ -13,9 +15,20 @@
white-space: nowrap; white-space: nowrap;
} }
.btn-md {
min-width: 38px;
height: 38px;
}
// Print fiche commerciale // Print fiche commerciale
@media print { @media print {
#print-id-product { #print-id-product {
display: block; display: block;
} }
} }
.pagination-custom .dropdown-menu {
overflow: auto;
min-width: 5rem;
max-height: 178px;
}

View file

@ -0,0 +1,263 @@
<template>
<div class="belongs-to-dropdown">
<label
:for="name"
class="col-form-label"
>
<b>{{ label }} {{ required ? "*" : "" }}</b>
</label>
<Transition name="fade">
<p
v-if="message"
class="message"
>
{{ message }}
</p>
</Transition>
<div class="d-inline-block tagInput w-100">
<VueTagsInputCls
v-model="item"
:tags="selectedItem"
:autocomplete-items="filteredItems"
:autocomplete-min-length="0"
:max-tags="1"
:placeholder="placeholder"
@before-adding-tag="beforeTagSave"
@tags-changed="changedTags"
/>
</div>
<input
v-if="!selectedItem.length"
:name="name"
type="hidden"
value=""
>
<template
v-for="(sItem, index) in selectedItem"
:key="index"
>
<p class="text-danger m-0">
{{ parseValidationInput(`${name}`, allErrors) }}
</p>
<input
:name="`${name}`"
type="hidden"
:value="sItem.id"
>
</template>
</div>
</template>
<script lang="ts">
import type VueTagsInput from "@sipec/vue3-tags-input";
import { VueTagsInput as VueTagsInputCls } from "@sipec/vue3-tags-input";
import axios from "axios";
import slugify from "slugify";
import { defineComponent } from "vue";
import parseLaravelErrorMessages from "./../modules/parseLaravelErrorMessages";
import route from "./../modules/route";
import trans from "./../modules/trans";
export default defineComponent({
name:"BelongsToDropdown",
mixins: [route, trans, parseLaravelErrorMessages],
components: {
VueTagsInputCls,
},
beforeMount() {
if (!this.$attrs.json) {
throw new Error("this component requires json");
}
},
mounted() {
const json = String(this.$attrs.json ?? "{}"),
data = JSON.parse(json);
this.name = data.name;
this.labelField = data.labelField;
this.label = data.label;
this.required = data.require;
this.allItems = (data.itemsList as CustomTag[]).map(item => {item.text = item[this.labelField] as string; return item;});
this.selectedItem = this.parseTags([data.value]);
this.intStoreRoute = data.storeRoute ?? null;
this.placeholder = data.placeholder;
},
data(): {
selectedItem: CustomTag[];
allItems: CustomTag[];
name: string;
item: string;
message: string | null;
intStoreRoute: string | null;
placeholder: string;
label: string;
required: boolean;
labelField: string;
} {
return {
allItems: [],
selectedItem: [],
item: "",
name: "",
message: null,
intStoreRoute: null,
placeholder: "",
label: "",
required: false,
labelField: "",
};
},
methods: {
parseTags(intSelectedItem: number[]): CustomTag[] {
const out: CustomTag[] = [];
if (intSelectedItem[0] !== null) {
intSelectedItem.forEach((intItem: number) => {
const selectedItem = this.allItems.find(item => String(item.id) === String(intItem));
if (!selectedItem) {
console.debug(intItem);
throw new Error(`Could not find enum ${intItem}`);
}
out.push({
id: String(selectedItem.id),
value: String(selectedItem.id),
text: String(selectedItem[`${this.labelField}`])
} as LaravelModel as CustomTag);
});
}
return out;
},
parseNewlyCreated(newModel: LaravelModel): CustomTag {
let newOut: CustomTag;
newOut = {
id: String(newModel.id),
value: String(newModel.id),
text: String(newModel[`${this.labelField}`])
} as LaravelModel as CustomTag;
return newOut;
},
/**
* Add item
* @param obj
*/
beforeTagSave(obj: VueTagsInput.IAddArgs): void {
if (this.selectedItem.length < 1) {
let customTag = this.tagExists(this.allItems, obj.tag);
if (customTag) {
obj.tag = customTag;
this.selectedItem.push(customTag);
this.item = "";
return;
} else if (this.intStoreRoute !== null) {
const storeRoute = route.methods.route(this.intStoreRoute);
if (!storeRoute) {
throw new Error("Undefined route " + this.intStoreRoute);
}
axios
.post(storeRoute, {
name: obj.tag.text
})
.then((response) => {
if (response.data.status === true) {
this.item = "";
const brandItem = this.parseNewlyCreated(response.data.brand);
this.allItems.push(brandItem);
this.selectedItem.push(brandItem);
}
})
.catch((error: any) => {
console.error(error);
});
}
} else {
this.message = this.__("Nombre d'élements maximal atteint.");
setTimeout(() => (this.message = null), 1500);
}
},
tagExists(
items: Array<CustomTag>,
tagToCheck: VueTagsInput.ITag
): CustomTag | null {
let customTag: CustomTag | null = null;
items.filter((item: CustomTag) => {
if (
item.id === (tagToCheck as CustomTag).id ||
item.slug === slugify(tagToCheck.text)
) {
customTag = item;
}
});
return customTag;
},
/**
* Delete item.
* @param newItems
*/
changedTags(newItems: Array<CustomTag>): void {
this.selectedItem = newItems as Array<CustomTag>;
},
},
computed: {
filteredItems() {
return this.allItems.filter((i) => {
return i.text.toLowerCase().indexOf(this.item.toLowerCase()) !== -1;
});
},
},
});
</script>
<style lang="scss">
.vue-tags-input {
max-width: 100% !important;
.ti- {
&autocomplete {
max-height: 10rem !important;
border-radius: 0.375rem;
box-shadow: 0 0 4px var(--bs-gray-400);
overflow-y: auto !important;
ul {
padding: 0.25rem 0 !important;
}
}
&input {
padding: 0.375rem !important;
border-radius: 0.375rem;
}
&tags {
line-height: inherit !important;
}
&tag,
&new-tag-input-wrapper {
padding: 0.1rem !important;
margin: 0.1rem !important;
}
&new-tag-input-wrapper {
font-size: 1em !important;
input {
min-width: 165px !important;
}
}
&tag {
padding: 0.1rem 0.3rem !important;
background-color: var(--bs-blue) !important;
border-radius: 0.375rem !important;
}
&selected-item {
background-color: var(--bs-gray-300) !important;
color: var(--bs-gray-900) !important;
}
}
.message {
color: #ff0000;
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
}
</style>

View file

@ -63,15 +63,17 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue";
import type VueTagsInput from "@sipec/vue3-tags-input"; import type VueTagsInput from "@sipec/vue3-tags-input";
import { VueTagsInput as VueTagsInputCls } from "@sipec/vue3-tags-input"; import { VueTagsInput as VueTagsInputCls } from "@sipec/vue3-tags-input";
import axios from "axios";
import slugify from "slugify"; import slugify from "slugify";
import { defineComponent } from "vue";
import parseLaravelErrorMessages from "./../modules/parseLaravelErrorMessages";
import route from "./../modules/route"; import route from "./../modules/route";
import trans from "./../modules/trans"; import trans from "./../modules/trans";
import parseLaravelErrorMessages from "./../modules/parseLaravelErrorMessages";
export default defineComponent({ export default defineComponent({
name: "BelongsToManyDropdown",
mixins: [route, trans, parseLaravelErrorMessages], mixins: [route, trans, parseLaravelErrorMessages],
components: { components: {
VueTagsInputCls, VueTagsInputCls,
@ -88,6 +90,7 @@ export default defineComponent({
this.selectedItems = this.parseTags(data.value); this.selectedItems = this.parseTags(data.value);
this.allItems = this.parseTags(data.itemsList); this.allItems = this.parseTags(data.itemsList);
this.placeholder = data.placeholder; this.placeholder = data.placeholder;
this.intStoreRoute = data.storeRoute ?? null;
// * Min and max calc. // * Min and max calc.
this.min = Number.parseInt((data?.itemLimit ?? [])[0] ?? 0); this.min = Number.parseInt((data?.itemLimit ?? [])[0] ?? 0);
this.min = this.min < 0 ? 0 : this.min; this.min = this.min < 0 ? 0 : this.min;
@ -108,6 +111,7 @@ export default defineComponent({
waitingListObjects: Array<VueTagsInput.IAddArgs>; waitingListObjects: Array<VueTagsInput.IAddArgs>;
allErrors: Record<string, Array<string>>; allErrors: Record<string, Array<string>>;
message: string | null; message: string | null;
intStoreRoute: string | null;
} { } {
return { return {
name: "", name: "",
@ -121,6 +125,7 @@ export default defineComponent({
waitingListObjects: [], waitingListObjects: [],
allErrors: {}, allErrors: {},
message: null, message: null,
intStoreRoute: null,
}; };
}, },
methods: { methods: {
@ -143,6 +148,23 @@ export default defineComponent({
this.selectedItems.push(customTag); this.selectedItems.push(customTag);
this.item = ""; this.item = "";
return; return;
} else if (this.intStoreRoute !== null) {
const storeBrandRoute = route.methods.route("brands.store");
if (!storeBrandRoute) {
throw new Error("Undefined route brands.store");
}
axios
.post(storeBrandRoute, {
name: obj.tag.text
})
.then((response) => {
if (response.data.status === true) {
obj.addTag();
}
})
.catch((error: any) => {
console.log(error);
});
} }
} else { } else {
this.message = this.__("Nombre d'élements maximal atteint."); this.message = this.__("Nombre d'élements maximal atteint.");

View file

@ -10,22 +10,27 @@
> >
<template v-if="intIsAdded"> <template v-if="intIsAdded">
<FontAwesomeIcon icon="fa-solid fa-minus" /> <FontAwesomeIcon icon="fa-solid fa-minus" />
&nbsp;
<FontAwesomeIcon icon="fa-solid fa-file-pdf" />
</template> </template>
<template v-else> <template v-else>
<FontAwesomeIcon icon="fa-solid fa-plus" /> <FontAwesomeIcon icon="fa-solid fa-plus" />
&nbsp;
<FontAwesomeIcon icon="fa-solid fa-file-pdf" />
</template> </template>
</button> </button>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue";
import trans from "./../modules/trans";
import route from "./../modules/route";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import Tooltip from "bootstrap/js/dist/tooltip"; import Tooltip from "bootstrap/js/dist/tooltip";
import { defineComponent } from "vue";
import route from "./../modules/route";
import trans from "./../modules/trans";
export default defineComponent({ export default defineComponent({
name: "ButtonMultiPdfComponent",
mixins: [trans, route], mixins: [trans, route],
components: { components: {
FontAwesomeIcon, FontAwesomeIcon,

View file

@ -111,7 +111,7 @@
</label> </label>
<img <img
v-if="intValue" v-if="intValue"
class="img-fluid" class="img-fluid img-preview"
:src="intValue" :src="intValue"
:alt="__('Image placeholder')" :alt="__('Image placeholder')"
:aria-describedby="`PreviewHelp${intId}`" :aria-describedby="`PreviewHelp${intId}`"
@ -400,10 +400,10 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import Cropper from "cropperjs";
import { Tooltip } from "bootstrap"; import { Tooltip } from "bootstrap";
import Cropper from "cropperjs";
import { defineComponent } from "vue";
import trans from "./../modules/trans"; import trans from "./../modules/trans";
const UNITS = [ const UNITS = [
@ -417,6 +417,7 @@ const UNITS = [
const BYTES_PER_KB = 1024; const BYTES_PER_KB = 1024;
export default defineComponent({ export default defineComponent({
name: "ImageInputComponent",
mixins: [trans], mixins: [trans],
emits: ["browseFile"], emits: ["browseFile"],
components: { components: {
@ -952,6 +953,10 @@ export default defineComponent({
transform: translateX(-50%) translateY(-50%); transform: translateX(-50%) translateY(-50%);
} }
} }
.img-preview {
width: 180px;
height: auto;
}
.right-aligned { .right-aligned {
overflow: hidden !important; overflow: hidden !important;
direction: ltr !important; direction: ltr !important;

View file

@ -25,6 +25,7 @@ import WordCounter from "./WordCounterComponent.vue";
const textarea = ref<HTMLTextAreaElement | null>(null); const textarea = ref<HTMLTextAreaElement | null>(null);
export default defineComponent({ export default defineComponent({
name: "InputTextAutoComponent",
inheritAttrs: false, inheritAttrs: false,
mixins: [trans], mixins: [trans],
components: { components: {

View file

@ -38,13 +38,13 @@
type="button" type="button"
data-bs-toggle="collapse" data-bs-toggle="collapse"
:data-bs-target="'#comment' + commentIndex" :data-bs-target="'#comment' + commentIndex"
aria-expanded="false" :aria-expanded="intModel.length > 0 ? true : false"
:aria-controls="'comment' + commentIndex" :aria-controls="'comment' + commentIndex"
> >
{{ __("Commentaire n°" + (commentIndex + 1)) }} {{ __("Commentaire n°" + (commentIndex + 1)) }}
</button> </button>
<div <div
class="border collapse" :class="`border collapse ${intModel.length > 0 ? 'show' : ''}`"
:id="'comment' + commentIndex" :id="'comment' + commentIndex"
> >
<div class="row w-100 mx-auto mb-3"> <div class="row w-100 mx-auto mb-3">
@ -53,14 +53,13 @@
class="col-form-label" class="col-form-label"
for="commentEvaluation" for="commentEvaluation"
> >
<b>{{ __("Évaluation du produit") }} *</b> <b>{{ __("Appréciation globale") }} *</b>
</label> </label>
<InputTextAutoComponent <InputTextAutoComponent
:id="'commentEvaluation' + commentIndex" :id="'commentEvaluation' + commentIndex"
:name="'evaluation[' + commentIndex + ']'" :name="'evaluation[' + commentIndex + ']'"
class="form-control" class="form-control"
:value="comment?.evaluation" :value="comment?.evaluation"
:placeholder="__('Un peu caoutchouteux')"
:ariadescribedby="'productcommentEvaluation' + commentIndex" :ariadescribedby="'productcommentEvaluation' + commentIndex"
/> />
<p class="text-danger m-0"> <p class="text-danger m-0">
@ -77,7 +76,7 @@
> >
{{ {{
__( __(
"Doit détailler l'évaluation du produit (3 caractères minimum, 255 maximum)." "Doit détailler l'appéciation globale (3 caractères minimum, 255 maximum)."
) )
}} }}
</small> </small>
@ -94,7 +93,6 @@
:name="'aspect[' + commentIndex + ']'" :name="'aspect[' + commentIndex + ']'"
class="form-control" class="form-control"
:value="comment?.aspect" :value="comment?.aspect"
:placeholder="__('Un peu caoutchouteux')"
:ariadescribedby="'productcommentAspect' + commentIndex" :ariadescribedby="'productcommentAspect' + commentIndex"
/> />
<p class="text-danger m-0"> <p class="text-danger m-0">
@ -128,7 +126,6 @@
:name="'taste[' + commentIndex + ']'" :name="'taste[' + commentIndex + ']'"
class="form-control" class="form-control"
:value="comment?.taste" :value="comment?.taste"
:placeholder="__('Un peu caoutchouteux')"
:ariadescribedby="'productcommentTaste' + commentIndex" :ariadescribedby="'productcommentTaste' + commentIndex"
/> />
<p class="text-danger m-0"> <p class="text-danger m-0">
@ -162,7 +159,6 @@
:name="'odor[' + commentIndex + ']'" :name="'odor[' + commentIndex + ']'"
class="form-control" class="form-control"
:value="comment?.odor" :value="comment?.odor"
:placeholder="__('Un peu caoutchouteux')"
:ariadescribedby="'productcommentOdor' + commentIndex" :ariadescribedby="'productcommentOdor' + commentIndex"
/> />
<p class="text-danger m-0"> <p class="text-danger m-0">
@ -196,7 +192,6 @@
:name="'texture[' + commentIndex + ']'" :name="'texture[' + commentIndex + ']'"
class="form-control" class="form-control"
:value="comment?.texture" :value="comment?.texture"
:placeholder="__('Un peu caoutchouteux')"
:ariadescribedby="'productCommentTexture' + commentIndex" :ariadescribedby="'productCommentTexture' + commentIndex"
/> />
<p class="text-danger m-0"> <p class="text-danger m-0">
@ -227,11 +222,12 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from "vue";
import InputTextAutoComponent from "./InputTextAutoComponent.vue";
import parseLaravelErrorMessages from "../modules/parseLaravelErrorMessages"; import parseLaravelErrorMessages from "../modules/parseLaravelErrorMessages";
import trans from "../modules/trans"; import trans from "../modules/trans";
import InputTextAutoComponent from "./InputTextAutoComponent.vue";
export default defineComponent({ export default defineComponent({
name: "MultipleCommentsComponent",
components: { components: {
InputTextAutoComponent, InputTextAutoComponent,
}, },

View file

@ -7,11 +7,11 @@
<div class="col-12 d-flex form-group"> <div class="col-12 d-flex form-group">
<div class="input-group"> <div class="input-group">
<button <button
@click.prevent="chooseFiles" @click.prevent="($refs.actualDocuments as HTMLInputElement).click()"
class="btn btn-secondary" class="btn btn-secondary"
type="button" type="button"
:title="__('Modifier les documents sources')" :title="__('Modifier les documents sources')"
data-bs-tooltip="tooltip" data-bs-toggle="tooltip"
:disabled="intValues.length >= intItemLimit[1]" :disabled="intValues.length >= intItemLimit[1]"
> >
<i class="fa-solid fa-folder-open" /> <i class="fa-solid fa-folder-open" />
@ -19,16 +19,16 @@
<input <input
:id="intId" :id="intId"
ref="actualDocuments" ref="actualDocuments"
@change="changedDocuments" @change="loadDocuments([].slice.call(($refs.actualDocuments as HTMLInputElement).files))"
type="file" type="file"
class="d-none" class="d-none"
name="multipleInputDocuments" name="multipleInputDocuments"
accept=".pdf" :accept="intAccepts"
:aria-describedby="`Help${intId}`" :aria-describedby="`Help${intId}`"
multiple multiple
> >
<input <input
@click.prevent="chooseFiles" @click.prevent="($refs.actualDocuments as HTMLInputElement).click()"
type="text" type="text"
:value=" :value="
'Actuellement ' + 'Actuellement ' +
@ -39,31 +39,18 @@
" "
class="form-control right-aligned" class="form-control right-aligned"
:title="__('Modifier les documents sources')" :title="__('Modifier les documents sources')"
data-bs-tooltip="tooltip" data-bs-toggle="tooltip"
:aria-describedby="`Help${intId}`" :aria-describedby="`Help${intId}`"
:disabled="intValues.length >= intItemLimit[1]" :disabled="intValues.length >= intItemLimit[1]"
readonly readonly
> >
<button
class="btn btn-primary btn-collapse collapsed"
type="button"
:title="__('Rouler/Dérouler la liste des documents')"
data-bs-tooltip="tooltip"
data-bs-toggle="collapse"
data-bs-target="#multiple-documents"
aria-expanded="false"
aria-controls="multiple-documents"
:disabled="!intHasDocuments"
>
<i class="fa fa-arrow-down" />
</button>
<button <button
v-if="intValues.length > intItemLimit[0]" v-if="intValues.length > intItemLimit[0]"
@click.prevent="removeFiles" @click.prevent="removeFiles"
class="btn btn-danger" class="btn btn-danger"
type="button" type="button"
:title="__('Supprimer tous les documents')" :title="__('Supprimer tous les documents')"
data-bs-tooltip="tooltip" data-bs-toggle="tooltip"
:disabled="intValues.length <= 0" :disabled="intValues.length <= 0"
> >
<i class="fas fa-eraser" /> <i class="fas fa-eraser" />
@ -72,146 +59,94 @@
</div> </div>
</div> </div>
<!-- End main buttons --> <!-- End main buttons -->
<!-- Content collapse --> <!-- Documents list -->
<div <div
class="collapse" v-for="(value, valueIndex) of intValues"
id="multiple-documents" :key="valueIndex"
class="row w-100 mx-auto my-2"
> >
<div class="row w-100 mx-auto my-2"> <div class="col-12 form-group text-center px-1">
<div <div
v-for="n = 0 in intValues.length" class="d-flex justify-content-between align-items-center pb-1"
:key="n" :class="valueIndex != 1 ? 'border-top' : ''"
class="col-12 form-group text-center px-1"
> >
<div <label
class="d-flex justify-content-between align-items-center pb-1" v-if="value"
:class="n != 1 ? 'border-top' : ''" class="col-form-label d-flex"
> >
<label <span
v-if="intValues[n - 1]" class="text-start text-ellipsis"
class="col-form-label d-flex" v-if="intOriginalFileName[valueIndex]"
> >
<span class="text-start text-ellipsis"> {{ intOriginalFileName[valueIndex].split('.').pop() }}
{{ intOriginalFileName[n - 1] }} &nbsp;
</span> </span>
</label> <span class="text-start text-ellipsis">
<small {{ intOriginalFileName[valueIndex] }}
v-if="intValues[n - 1]" </span>
:id="`PreviewHelp${intId}`" </label>
class="form-text text-muted d-block m-0" <small
> v-if="value"
<span v-if="intOriginalFile[n - 1]"> :id="`PreviewHelp${intId}`"
{{ humanFileSize(intOriginalFile[n - 1].size) }}. class="form-text text-muted d-block m-0"
</span>
</small>
<div class="input-group justify-content-center mt-1 w-auto">
<button
@click.prevent="viewDocumentSource(n - 1)"
class="btn btn-warning"
type="button"
data-bs-toggle="modal"
:data-bs-target="`#modalViewer${intId}`"
:title="__('Afficher le document')"
data-bs-tooltip="tooltip"
>
<i class="fa-solid fa-eye" />
</button>
<button
@click.prevent="chooseAFile(n - 1)"
class="btn btn-secondary"
type="button"
:title="__('Modifier le document source')"
data-bs-tooltip="tooltip"
>
<i class="fa-solid fa-folder-open" />
</button>
<input
:id="intId"
ref="actualDocument"
@change="changedDocument($event, n - 1)"
type="file"
class="d-none"
:name="intName"
accept=".pdf"
:aria-describedby="`Help${intId}`"
>
<button
v-if="intValues.length > intItemLimit[0]"
@click.prevent="removeFile(n - 1)"
class="btn btn-danger"
type="button"
:title="__('Supprimer le document')"
data-bs-tooltip="tooltip"
>
<i class="fas fa-trash" />
</button>
</div>
</div>
<p class="text-danger m-0">
{{
parseValidationInput(
`${intName.replace(/[\[\]']+/g, "")}.${n - 1}`,
intAllErrors
)
}}
</p>
</div>
<!-- Viewer modal -->
<div
:id="`modalViewer${intId}`"
class="modal"
tabindex="-1"
role="dialog"
data-bs-backdrop="static"
data-bs-keyboard="false"
>
<div
class="modal-dialog modal-xl d-flex align-items-center h-100 my-0 py-4"
> >
<div class="modal-content h-100"> <span v-if="intOriginalFile[valueIndex]">
<div class="modal-header"> {{ humanFileSize(intOriginalFile[valueIndex].size) }}.
<h5 class="modal-title"> </span>
{{ __("Prévisualisation du document") }} </small>
</h5> <div class="input-group justify-content-center mt-1 w-auto">
<button <button
@click.prevent="removeDocumentSource" @click.prevent="viewDocumentSource(valueIndex)"
type="button" class="btn btn-warning"
class="btn-close" type="button"
data-bs-dismiss="modal" :title="__('Afficher le document')"
:aria-label="__('Fermer la fenêtre')" data-bs-toggle="tooltip"
:title="__('Fermer la fenêtre')" >
data-bs-tooltip="tooltip" <i class="fa-solid fa-eye" />
/> </button>
</div> <input
<div class="modal-body text-center"> :id="intId"
<div ref="actualDocument"
v-if="intViewerLoadDocument" @change="changedDocument(($refs.actualDocument as HTMLInputElement[])[valueIndex], valueIndex)"
class="text-center w-100 h-100" type="file"
> class="d-none"
<div :name="intName.replace('[]', '') + `[${valueIndex}]`"
class="spinner-border text-secondary" :accept="intAccepts"
role="status" :aria-describedby="`Help${intId}`"
> :disabled="intPreviousFiles.has(intOriginalFileName[valueIndex])"
<span class="visually-hidden"> >
{{ __("Chargement du document") }} <input
</span> @change="changedDocument(($refs.actualDocument as HTMLInputElement[])[valueIndex], valueIndex)"
</div> type="text"
</div> class="d-none"
<object :name="intName.replace('[]', '') + `[${valueIndex}]`"
class="w-100 h-100" :aria-describedby="`Help${intId}`"
:class="intViewerLoadDocument ? 'd-none' : ''" :disabled="!intPreviousFiles.has(intOriginalFileName[valueIndex])"
type="application/pdf" :value="intValues[valueIndex]"
data="" >
ref="documentViewer" <button
:title="__('Document enregistré')" v-if="intValues.length > intItemLimit[0]"
/> @click.prevent="removeFile(valueIndex)"
</div> class="btn btn-danger"
</div> type="button"
:title="__('Supprimer le document')"
data-bs-toggle="tooltip"
>
<i class="fas fa-trash" />
</button>
</div> </div>
</div> </div>
<p class="text-danger m-0">
{{
parseValidationInput(
`${intName.replace(/[\[\]']+/g, "")}.${n - 1}`,
intAllErrors
)
}}
</p>
</div> </div>
</div> </div>
<!-- End content collapse --> <!-- End documents list -->
<small <small
:id="`Help${intId}`" :id="`Help${intId}`"
class="form-text text-muted" class="form-text text-muted"
@ -223,7 +158,7 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from "vue";
import { Tooltip } from "bootstrap"; import bsTooltips from "./../modules/bsTooltips";
import error from "./../modules/parseLaravelErrorMessages"; import error from "./../modules/parseLaravelErrorMessages";
import trans from "./../modules/trans"; import trans from "./../modules/trans";
@ -238,38 +173,37 @@ const UNITS = [
const BYTES_PER_KB = 1024; const BYTES_PER_KB = 1024;
export default defineComponent({ export default defineComponent({
mixins: [error, trans], name: "MultipleDocumentsInputComponent",
mixins: [error, trans, bsTooltips],
components: {
},
data(): { data(): {
/** The file ext accepted */
intAccepts: string;
/** The file ext accepted */
intId: string; intId: string;
intName: string; intName: string;
intHasDocument: boolean[]; intPreviousFiles: Set<string>;
intHasDocuments: boolean;
intOriginalFile: File[]; intOriginalFile: File[];
intOriginalFileName: string[]; intOriginalFileName: string[];
intValues: string[]; intValues: string[];
intHelper: string; intHelper: string;
intTooltipList: HTMLButtonElement[]; intTooltipList: HTMLButtonElement[];
intBtnCollapse: HTMLButtonElement | null;
intLoopLoadDocuments: number;
intViewerLoadDocument: boolean; intViewerLoadDocument: boolean;
intInputDocuments: HTMLInputElement[];
intItemLimit: number[]; intItemLimit: number[];
intAllErrors: Record<string, string[]>; intAllErrors: Record<string, string[]>;
} { } {
return { return {
intAccepts: ".xls,.csv,.xlsx,.png,.jpg,.jpeg,.gif,.doc,.docx,.ppt,.pdf,.txt",
intId: "", intId: "",
intName: "", intName: "",
intHasDocument: [], intPreviousFiles: new Set<string>(),
intHasDocuments: false,
intOriginalFile: [], intOriginalFile: [],
intOriginalFileName: [], intOriginalFileName: [],
intValues: [], intValues: [],
intHelper: "", intHelper: "",
intBtnCollapse: null,
intTooltipList: [], intTooltipList: [],
intLoopLoadDocuments: 0,
intViewerLoadDocument: false, intViewerLoadDocument: false,
intInputDocuments: [],
intItemLimit: [0, 0], intItemLimit: [0, 0],
intAllErrors: {}, intAllErrors: {},
}; };
@ -285,153 +219,87 @@ export default defineComponent({
this.intAllErrors = data.errors ?? {}; this.intAllErrors = data.errors ?? {};
this.$nextTick(() => { this.$nextTick(() => {
this.initComponent(); this.initComponent();
this.updateBootstrapTooltip(); });
this.$nextTick(() => {
this.initTooltips();
}); });
}, },
computed: {
documentInputs(): HTMLInputElement[] {
return this.$refs.actualDocument as HTMLInputElement[];
}
},
methods: { methods: {
/** /** Initialize the component with previously documents registered. */
* Initialise the component with previously documents registered.
*/
initComponent() { initComponent() {
if (this.intValues.length > 0) { if (this.intValues.length > 0) {
this.intBtnCollapse = document.querySelector(
"[data-bs-target=\"#multiple-documents\"]"
);
this.intValues.forEach((element, index) => { this.intValues.forEach((element, index) => {
element.charAt(0) != "/" ? "/".concat(element) : ""; element.charAt(0) != "/" ? "/".concat(element) : "";
this.intValues.splice(index, 1, element.replace(/^\/|^/, "/")); this.intValues.splice(index, 1, element.replace(/^\/|^/, "/"));
}); });
this.intHasDocument = Array(this.intValues.length).fill(false);
this.intHasDocuments = true;
this.initDocumentsSaved(); this.initDocumentsSaved();
} else {
this.intHasDocuments = false;
} }
}, },
/** /** Assign previously documents registered to respective input. */
* Assign previously documents registered to respective input.
*/
initDocumentsSaved() { initDocumentsSaved() {
var actualDocuments = this.$refs.actualDocument as HTMLInputElement[];
this.intValues.forEach((element, index) => { this.intValues.forEach((element, index) => {
const documentName = element.substring(element.lastIndexOf("/") + 1);
this.intPreviousFiles.add(documentName);
this.createFile( this.createFile(
element, element,
"document_" + (index + 1) + ".pdf", `${documentName}`
"application/pdf"
).then((file: File) => { ).then((file: File) => {
const container = new DataTransfer(); const container = new DataTransfer();
container.items.add(file); container.items.add(file);
actualDocuments[index].files = container.files; this.documentInputs[index].files = container.files;
this.intOriginalFile[index] = container.files[0]; this.intOriginalFile[index] = container.files[0];
this.intOriginalFileName[index] = container.files[0].name; this.intOriginalFileName[index] = container.files[0].name;
}); });
}); });
}, },
/** /** Create a file from a path. */
* Create a file from a path. async createFile(path: string, name: string): Promise<File> {
*/ const response = await fetch(
async createFile(path: string, name: string, type: string): Promise<File> {
let response = await fetch(
window.location.protocol + "//" + window.location.host + "/" + path window.location.protocol + "//" + window.location.host + "/" + path
); );
let data = await response.blob(); const data = await response.blob();
let metadata = { const type = await response.headers.get("content-type") ?? "application/pdf";
const metadata = {
type: type, type: type,
}; };
return new File([data], name, metadata); return new File([data], name, metadata);
}, },
/** /** Remove one specific document. */
* Update Bootstrap tooltips.
*/
updateBootstrapTooltip() {
let newTooltipList = [].slice.call(
document.querySelectorAll(
".multiple-documents-input-" +
this.intId +
" [data-bs-tooltip=\"tooltip\"]"
)
) as HTMLButtonElement[];
let tmp = newTooltipList.filter((x) => !this.intTooltipList.includes(x));
tmp.map((tooltip) => {
return new Tooltip(tooltip);
});
this.intTooltipList = newTooltipList;
this.closeBootstrapTooltip();
},
/**
* Close all Bootstrap tooltips.
*/
closeBootstrapTooltip() {
this.intTooltipList.forEach((tooltip) => {
tooltip.blur();
Tooltip.getInstance(tooltip)?.hide();
});
},
/**
* Link button change document to the input file document.
*/
chooseAFile(n: number) {
var actualDocuments = this.$refs.actualDocument as HTMLInputElement[],
actualDocument = actualDocuments[n] as HTMLInputElement;
actualDocument.click();
},
/**
* Link button change documents to the input file documents.
*/
chooseFiles() {
const actualDocuments = this.$refs.actualDocuments as HTMLInputElement;
actualDocuments.click();
},
/**
* Remove one specific document.
*/
removeFile(n: number) { removeFile(n: number) {
if(this.intPreviousFiles.has(this.intOriginalFileName[n])) {
this.intPreviousFiles.delete(this.intOriginalFileName[n]);
}
this.intValues.splice(n, 1); this.intValues.splice(n, 1);
this.intHasDocument.splice(n, 1);
this.intOriginalFile.splice(n, 1); this.intOriginalFile.splice(n, 1);
this.intOriginalFileName.splice(n, 1); this.intOriginalFileName.splice(n, 1);
if (this.intValues.length <= 0) { this.documentInputs.forEach((element, index) => {
this.intHasDocuments = false;
}
this.intInputDocuments = this.$refs.actualDocument as HTMLInputElement[];
this.intInputDocuments.forEach((element, index) => {
if (this.intOriginalFile[index] != undefined) { if (this.intOriginalFile[index] != undefined) {
var dt = new DataTransfer(); const dt = new DataTransfer();
dt.items.add(this.intOriginalFile[index]); dt.items.add(this.intOriginalFile[index]);
element.files = dt.files; element.files = dt.files;
} }
}); });
this.closeBootstrapTooltip(); this.$nextTick(() => this.initTooltips());
}, },
/** /** Remove all documents. */
* Remove all documents.
*/
removeFiles() { removeFiles() {
this.intValues.length = this.intItemLimit[0]; this.intValues.length = this.intItemLimit[0];
this.intHasDocument.length = this.intItemLimit[0];
this.intOriginalFile.length = this.intItemLimit[0]; this.intOriginalFile.length = this.intItemLimit[0];
this.intOriginalFileName.length = this.intItemLimit[0]; this.intOriginalFileName.length = this.intItemLimit[0];
this.intItemLimit[0] > 0
? (this.intHasDocuments = true)
: (this.intHasDocuments = false);
this.intAllErrors = {}; this.intAllErrors = {};
this.closeBootstrapTooltip(); this.$nextTick(() => this.initTooltips());
}, },
/** /** Change one specific document. */
* Change one specific document. changedDocument(el: HTMLInputElement, n: number) {
*/
changedDocument(e: Event, n: number) {
const reader = new FileReader(); const reader = new FileReader();
const el = e.target;
if (!el || !(el instanceof HTMLInputElement)) {
throw new Error(
"confirmDoneJS can only be executed on an exising form"
);
}
if (!el.files) { if (!el.files) {
throw new Error(); throw new Error("No files");
} }
this.intHasDocument[n] = true;
reader.addEventListener( reader.addEventListener(
"load", "load",
() => { () => {
@ -446,82 +314,35 @@ export default defineComponent({
reader.readAsDataURL(el.files[0]); reader.readAsDataURL(el.files[0]);
} }
}, },
/** /** Update inputs file. */
* Add documents to exists document(s).
*/
changedDocuments(e: Event) {
const el = e.target;
if (!el || !(el instanceof HTMLInputElement)) {
throw new Error(
"confirmDoneJS can only be executed on an exising form"
);
}
if (!el.files) {
throw new Error();
}
this.intLoopLoadDocuments = this.intValues.length;
this.loadDocuments([].slice.call(el.files));
this.intHasDocuments = true;
if (this.intBtnCollapse?.classList.contains("collapsed")) {
this.intBtnCollapse?.click();
}
},
/**
* Update inputs file.
*/
loadDocuments(files: Array<File>) { loadDocuments(files: Array<File>) {
if (files.length > 0) { if (files.length > 0) {
if (this.intValues.length + files.length <= this.intItemLimit[1]) { if (this.intValues.length + files.length <= this.intItemLimit[1]) {
let actualFile = files.shift() as File; const actualFile = files.shift() as File;
const reader = new FileReader(); const reader = new FileReader();
reader.readAsDataURL(actualFile); reader.readAsDataURL(actualFile);
reader.onload = () => { reader.onload = () => {
// On convertit l'document en une chaîne de caractères base64. // On convertit l'document en une chaîne de caractères base64.
this.intValues.push(String(reader.result)); this.intValues.push(String(reader.result));
this.intOriginalFileName[this.intValues.length - 1] = "";
}; };
reader.onloadend = () => { reader.onloadend = () => {
this.intInputDocuments = this.$refs const dt = new DataTransfer();
.actualDocument as HTMLInputElement[];
var dt = new DataTransfer();
dt.items.add(actualFile); dt.items.add(actualFile);
this.intInputDocuments[this.intLoopLoadDocuments].files = dt.files; this.documentInputs[this.documentInputs.length - 1].files = dt.files;
this.intOriginalFile[this.intLoopLoadDocuments] = actualFile; this.intOriginalFile[this.documentInputs.length - 1] = actualFile;
this.intHasDocument[this.intValues.length - 1] = true; this.intOriginalFileName[this.documentInputs.length - 1] = actualFile.name;
this.intOriginalFileName[this.intValues.length - 1] =
actualFile.name;
this.intLoopLoadDocuments++;
this.loadDocuments(files); this.loadDocuments(files);
this.initTooltips();
}; };
} }
} else {
this.updateBootstrapTooltip();
this.intLoopLoadDocuments = this.intValues.length;
} }
}, },
/** /** Update document source in the viewer. */
* Update document source in the viewer.
*/
viewDocumentSource(n: number) { viewDocumentSource(n: number) {
const documentViewer = this.$refs.documentViewer as HTMLObjectElement; window.open(URL.createObjectURL(this.intOriginalFile[n]));
this.intViewerLoadDocument = true;
setTimeout(() => {
documentViewer.data = this.intValues[n];
documentViewer.onload = () => {
this.intViewerLoadDocument = false;
};
}, 10);
}, },
/** /** Get size of a file. */
* Remove document source in the viewer.
*/
removeDocumentSource() {
this.intViewerLoadDocument = true;
const documentViewer = this.$refs.documentViewer as HTMLObjectElement;
documentViewer.data = "";
},
/**
* Get size of a file.
*/
humanFileSize(sizeBytes: number | bigint): string { humanFileSize(sizeBytes: number | bigint): string {
let size = Math.abs(Number(sizeBytes)); let size = Math.abs(Number(sizeBytes));
let u = 0; let u = 0;

View file

@ -33,25 +33,15 @@
<p class="text-danger m-0"> <p class="text-danger m-0">
{{ {{
parseValidationInput( parseValidationInput(
`${intName}[${String(index)}][name]`, `${intName}[${String(index)}]`,
intAllErrors intAllErrors
) )
}} }}
</p> </p>
<input <input
:name="`${intName}[${String(index)}][id]`" :name="`${intName}[${String(index)}]`"
type="hidden" type="hidden"
:value="sItem.id" :value="sItem.value"
>
<input
:name="`${intName}[${String(index)}][value]`"
type="hidden"
:value="sItem.id"
>
<input
:name="`${intName}[${String(index)}][label]`"
type="hidden"
:value="sItem.text"
> >
</template> </template>
<small class="form-text text-muted">{{ intHelper }}</small> <small class="form-text text-muted">{{ intHelper }}</small>
@ -82,15 +72,16 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue";
import type VueTagsInput from "@sipec/vue3-tags-input"; import type VueTagsInput from "@sipec/vue3-tags-input";
import { VueTagsInput as VueTagsInputCls } from "@sipec/vue3-tags-input"; import { VueTagsInput as VueTagsInputCls } from "@sipec/vue3-tags-input";
import slugify from "slugify"; import slugify from "slugify";
import { defineComponent } from "vue";
import parseLaravelErrorMessages from "./../modules/parseLaravelErrorMessages";
import route from "./../modules/route"; import route from "./../modules/route";
import trans from "./../modules/trans"; import trans from "./../modules/trans";
import parseLaravelErrorMessages from "./../modules/parseLaravelErrorMessages";
export default defineComponent({ export default defineComponent({
name: "SelectMultipleEnumComponent",
mixins: [route, trans, parseLaravelErrorMessages], mixins: [route, trans, parseLaravelErrorMessages],
components: { components: {
VueTagsInputCls, VueTagsInputCls,
@ -144,7 +135,7 @@ export default defineComponent({
this.intName = data.name; this.intName = data.name;
this.intLabel = data.label; this.intLabel = data.label;
this.intHelper = data.helper; this.intHelper = data.helper;
this.intAllItems = this.parseTags(data.itemsList); this.intAllItems = (data.itemsList as CustomTag[]).map(item => {item.text = item.label; return item;});
this.intSelectedItem = this.parseTags(data.itemsSelected); this.intSelectedItem = this.parseTags(data.itemsSelected);
this.intPlaceholder = data.placeholder; this.intPlaceholder = data.placeholder;
this.intItemLimit = data.itemLimit; this.intItemLimit = data.itemLimit;
@ -155,18 +146,21 @@ export default defineComponent({
this.checkSpecificItem(); this.checkSpecificItem();
}, },
methods: { methods: {
parseTags(intSelectedItem: CustomTag[]): CustomTag[] { parseTags(intSelectedItem: number[]): CustomTag[] {
return intSelectedItem.map((intItem: CustomTag) => { const out: CustomTag[] = [];
if (!intItem.value) intSelectedItem.forEach((intItem: number) => {
intItem = this.intAllItems const selectedItem = this.intAllItems.find(item => String(item.id ?? item.value) === String(intItem));
.filter((intItemEnum) => { if (!selectedItem) {
return intItemEnum.value == intItem.id; console.debug(intItem);
}) throw new Error(`Could not find enum ${intItem}`);
.shift() as CustomTag; }
intItem.id = String(intItem.value); out.push({
intItem.text = String(intItem.label); id: String(selectedItem.id ?? selectedItem.value),
return intItem; value: String(selectedItem.value),
text: String(selectedItem.text)
} as LaravelModel as CustomTag);
}); });
return out;
}, },
/** /**
* Add item * Add item

View file

@ -12,13 +12,17 @@
class="form-select source-select" class="form-select source-select"
:name="intName" :name="intName"
:aria-describedby="`${intId}Help`" :aria-describedby="`${intId}Help`"
@change="setSelectedValue()" @change="setSelectedValue(0)"
> >
<option
:value="null"
:selected="intSelectedValue === 0"
/>
<option <option
v-for="(itemEnum, indexEnum) in intSourceEnum" v-for="(itemEnum, indexEnum) in intSourceEnum"
:key="indexEnum" :key="indexEnum"
:value="itemEnum.value" :value="itemEnum.value"
:selected="itemEnum.value === intModel?.source" :selected="itemEnum.value === intModel?.source || itemEnum.value === intSelectedValue "
> >
{{ itemEnum.label }} {{ itemEnum.label }}
</option> </option>
@ -34,15 +38,15 @@
for="catalogue" for="catalogue"
class="col-form-label" class="col-form-label"
> >
<b>{{ __("Détail de la source Catalogue") }} *</b> <b>{{ __("Détail de la source Catalogue") }}</b>
</label> </label>
<input <input
class="form-control" class="form-control"
name="catalogue" name="catalogue"
type="text" type="text"
placeholder="Nom du catalogue" placeholder=""
list="productsCatalog" list="productsCatalog"
:value="intModel?.catalogue" :value="intModel?.catalogue ?? intOldDetails"
> >
<datalist id="productsCatalog"> <datalist id="productsCatalog">
<option <option
@ -69,15 +73,15 @@
for="shop" for="shop"
class="col-form-label" class="col-form-label"
> >
<b>{{ __("Détail de la source Magasin") }} *</b> <b>{{ __("Détail de la source Magasin") }}</b>
</label> </label>
<input <input
class="form-control" class="form-control"
name="shop" name="shop"
type="text" type="text"
placeholder="Nom du magasin" placeholder=""
list="productsShop" list="productsShop"
:value="intModel?.shop" :value="intModel?.shop ?? intOldDetails"
> >
<datalist id="productsShop"> <datalist id="productsShop">
<option <option
@ -104,15 +108,15 @@
for="salon" for="salon"
class="col-form-label" class="col-form-label"
> >
<b>{{ __("Détail de la source Salon") }} *</b> <b>{{ __("Détail de la source Salon") }}</b>
</label> </label>
<input <input
class="form-control" class="form-control"
name="fair" name="fair"
type="text" type="text"
placeholder="Nom du salon" placeholder=""
list="productsFair" list="productsFair"
:value="intModel?.fair" :value="intModel?.fair ?? intOldDetails"
> >
<datalist id="productsFair"> <datalist id="productsFair">
<option <option
@ -138,6 +142,7 @@ import { defineComponent } from "vue";
import trans from "../modules/trans"; import trans from "../modules/trans";
export default defineComponent({ export default defineComponent({
name: "SourceInputsComponent",
mixins: [trans], mixins: [trans],
mounted() { mounted() {
const json = String(this.$attrs.json ?? "{}"); const json = String(this.$attrs.json ?? "{}");
@ -151,7 +156,9 @@ export default defineComponent({
this.intProductsCatalog = data.productsCatalog; this.intProductsCatalog = data.productsCatalog;
this.intSourceEnum = data.sourceEnum; this.intSourceEnum = data.sourceEnum;
this.intHelper = data.helper; this.intHelper = data.helper;
this.setSelectedValue(); this.intSelectedValue = data.oldValue;
this.intOldDetails = data.oldDetails;
this.setSelectedValue(data.oldValue);
}, },
data(): { data(): {
intId: string; intId: string;
@ -167,6 +174,7 @@ export default defineComponent({
intSourceMagazin: boolean; intSourceMagazin: boolean;
intSourceShow: boolean; intSourceShow: boolean;
intHelper: string; intHelper: string;
intOldDetails: string;
} { } {
return { return {
intId: "", intId: "",
@ -182,18 +190,19 @@ export default defineComponent({
intSourceMagazin: false, intSourceMagazin: false,
intSourceShow: false, intSourceShow: false,
intHelper: "", intHelper: "",
intOldDetails: "",
}; };
}, },
methods: { methods: {
/** /**
* Set selected source variables. * Set selected source variables.
*/ */
setSelectedValue() { setSelectedValue(oldValue: number) {
this.$nextTick(() => { this.$nextTick(() => {
const select = document.querySelector( const select = document.querySelector(
".source-select" ".source-select"
) as HTMLSelectElement; ) as HTMLSelectElement;
this.intSelectedValue = Number(select.value); this.intSelectedValue = isNaN(Number(select.value)) ? Number(oldValue) : Number(select.value) ;
this.updateVue(); this.updateVue();
}); });
}, },

View file

@ -75,10 +75,11 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from "vue";
import MultipleCommentsComponent from "./MultipleCommentsComponent.vue";
import trans from "../modules/trans"; import trans from "../modules/trans";
import MultipleCommentsComponent from "./MultipleCommentsComponent.vue";
export default defineComponent({ export default defineComponent({
name: "TastedProductComponent",
components: { components: {
MultipleCommentsComponent, MultipleCommentsComponent,
}, },
@ -150,6 +151,8 @@ export default defineComponent({
: false : false
) { ) {
this.updateBoolean(); this.updateBoolean();
} else {
this.intIsTasted = data.oldCheck;
} }
this.intCommentModel = this.intCommentModel =
data.commentOldModel.length > 0 data.commentOldModel.length > 0

View file

@ -12,7 +12,7 @@
:name="intName" :name="intName"
type="text" type="text"
:value="intValue" :value="intValue"
:placeholder="__('112233445566')" placeholder=""
class="form-control input-gencode" class="form-control input-gencode"
:class="[ :class="[
intResult != undefined ? [intResult ? 'is-invalid' : 'is-valid'] : '', intResult != undefined ? [intResult ? 'is-invalid' : 'is-valid'] : '',
@ -21,7 +21,7 @@
:maxlength="typeof intMaxlength != 'undefined' ? intMaxlength : ''" :maxlength="typeof intMaxlength != 'undefined' ? intMaxlength : ''"
:required="!!intRequired" :required="!!intRequired"
:disabled="!!intLoader" :disabled="!!intLoader"
@keyup="checkGencode()" @input="checkGencode()"
> >
<a <a
v-if="intResult" v-if="intResult"
@ -43,13 +43,14 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue";
import { Tooltip } from "bootstrap";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { Tooltip } from "bootstrap";
import { defineComponent } from "vue";
import route from "./../modules/route"; import route from "./../modules/route";
import trans from "./../modules/trans"; import trans from "./../modules/trans";
export default defineComponent({ export default defineComponent({
name: "VerificationCodeComponent",
mixins: [route, trans], mixins: [route, trans],
components: { components: {
FontAwesomeIcon, FontAwesomeIcon,
@ -161,7 +162,7 @@ export default defineComponent({
this.intCode = inputCode.value; this.intCode = inputCode.value;
window.axios window.axios
.post(this.getRouteProductGencode(), { .post(this.getRouteProductGencode(), {
GENCODE: this.intCode, GENCOD: this.intCode,
ID: this.intModel?.id, ID: this.intModel?.id,
}) })
.then((reponse) => { .then((reponse) => {

View file

@ -18,6 +18,7 @@ import { defineComponent } from "vue";
import trans from "../modules/trans"; import trans from "../modules/trans";
export default defineComponent({ export default defineComponent({
name: "WordCounterComponent",
mixins: [trans], mixins: [trans],
mounted() { mounted() {
const json = String(this.$attrs.json ?? "{}"); const json = String(this.$attrs.json ?? "{}");

View file

@ -1,7 +1,7 @@
export {}; export { };
import { AxiosStatic } from "axios";
import type VueTagsInput from "@sipec/vue3-tags-input"; import type VueTagsInput from "@sipec/vue3-tags-input";
import { AxiosStatic } from "axios";
// * Recursive List<string> // * Recursive List<string>
type StringList = Record<PropertyKey, string>; type StringList = Record<PropertyKey, string>;
@ -30,7 +30,7 @@ type NestedNumberList = NestedNumberListInterface | NumberList | number;
type ModelList = Array<Model>; type ModelList = Array<Model>;
declare global { declare global {
type CustomTag = LaravelModel & VueTagsInput.ITag; type CustomTag = LaravelModel & VueTagsInput.ITag & {name:string,label:string, value:number|string};
type LaravelModel = Model; type LaravelModel = Model;
type LaravelModelList = ModelList; type LaravelModelList = ModelList;

View file

@ -0,0 +1,27 @@
import { Tooltip } from "bootstrap";
let tooltips: Array<bootstrap.Tooltip> = [];
/** ! Ignore here because there is no good TS vue mixin implementation at this time */
export default {
methods: {
initTooltips() {
// Disable tooltips on reboot.
tooltips.forEach((tooltip) => {
tooltip.disable();
tooltip.hide();
});
tooltips = [].slice
.call(document.querySelectorAll("[data-bs-toggle=\"tooltip\"]"))
.map(function (tooltipTriggerEl) {
return new Tooltip(tooltipTriggerEl, {
customClass: "large-tooltip",
delay: {
show: 1250,
hide: 0,
},
});
});
},
},
};

View file

@ -1,42 +1,49 @@
import { dom, library, config } from "~/@fortawesome/fontawesome-svg-core"; import { config, dom, library } from "~/@fortawesome/fontawesome-svg-core";
import { import {
faEraser, faAnglesRight,
faArrowLeftLong,
faPlus,
faMinus,
faTrash,
faEye,
faFolderOpen,
faCrop,
faQuestionCircle,
faCheck,
faUndoAlt,
faRedoAlt,
faSyncAlt,
faSearchPlus,
faSearchMinus,
faArrowLeft,
faArrowRight,
faArrowUp,
faArrowDown,
faArrowsAltH,
faArrowsAltV,
faWandMagic,
faPencil,
faTimes,
faBackspace,
faSearch,
faArrowRightFromBracket,
faArrowAltCircleDown, faArrowAltCircleDown,
faArrowAltCircleUp, faArrowAltCircleUp,
faQuestion, faArrowDown,
faXmark, faArrowLeft,
faArrowLeftLong,
faArrowRight,
faArrowRightFromBracket,
faArrowUp,
faArrowsAltH,
faArrowsAltV,
faBackspace,
faCheck,
faChevronLeft,
faChevronRight,
faCircleNotch,
faCopy, faCopy,
faFile, faCrop,
faLink,
faPrint,
faImage,
faDownload, faDownload,
faEraser,
faEye,
faFile,
faFilePdf,
faFolderOpen,
faHeading,
faImage,
faLink,
faListUl,
faMinus,
faPencil,
faPlus,
faPrint,
faQuestion,
faQuestionCircle,
faRedoAlt,
faSearch,
faSearchMinus,
faSearchPlus,
faSyncAlt,
faTimes,
faTrash,
faUndoAlt,
faWandMagic,
faXmark,
} from "~/@fortawesome/free-solid-svg-icons"; } from "~/@fortawesome/free-solid-svg-icons";
config.autoReplaceSvg = false; config.autoReplaceSvg = false;
@ -77,5 +84,12 @@ library.add(faLink);
library.add(faPrint); library.add(faPrint);
library.add(faImage); library.add(faImage);
library.add(faDownload); library.add(faDownload);
library.add(faFilePdf);
library.add(faListUl);
library.add(faHeading);
library.add(faAnglesRight);
library.add(faChevronLeft);
library.add(faChevronRight);
library.add(faCircleNotch);
dom.watch(); dom.watch();

View file

@ -5,109 +5,119 @@
{{-- CONTENT --}} {{-- CONTENT --}}
<table width="100%"> <table width="100%">
<tr> <tr>
<td class="table-cell-text-center" width="30%"> @if (isset($product->picture) && !empty($product->picture))
@if (isset($product->picture)) <td class="table-cell-text-center" width="30%">
<img src="{{ public_path($product->picture) }}" alt="{{ $product->picture_alt }}" alt="{{ $product->picture_title }}"> <img src="{{ public_path($product->picture) }}" alt="{{ $product->picture_alt }}" alt="{{ $product->picture_title }}">
@endif </td>
</td> <td width="70%">
<td width="70%"> @else
@if (isset($product->brand_id->name)) <td width="100%">
<h2><strong>{{ __('Marque du produit : ') }}</strong><span>{{ $product->brand_id->name }}</span></h2> @endif
@endif <h2><strong>{{ __('Marque du produit : ') }}</strong><span>{{ $product->brand->name }}</span></h2>
@if (isset($product->name)) <h2><strong>{{ __('Nom du produit : ') }}</strong><span>{{ $product->name }}</span></h2>
<h2><strong>{{ __('Nom du produit : ') }}</strong><span>{{ $product->name }}</span></h2> @if (isset($product->certification))
@endif
@if (isset($product->certification))
<p><strong>{{ __('Certification : ') }}</strong>{{ $product->certification }}</p> <p><strong>{{ __('Certification : ') }}</strong>{{ $product->certification }}</p>
@endif
<p><strong>{{ __('Origines : ') }}</strong>{{ $product->origins }}</p>
<p><strong>{{ __('Allégations : ') }}</strong>
@php
$claims = Str::of($product->claims)->explode(', ');
$claimOthers = \App\Enums\Products\ClaimEnum::others->label();
@endphp
@if ($claims->contains($claimOthers))
@php
$product->claims = $claims->except(array_search('Autres', $claims->toArray()))->implode(', ');
@endphp
{{ $product->claims }}, {{ $claimOthers}}
@if ($product->specified_claim)
: {{ $product->specified_claim }}
@endif
@else
{{ $product->claims }}
@endif @endif
@if (isset($product->origins)) </p>
<p><strong>{{ __('Origines : ') }}</strong>{{ $product->origins }}</p> <table class="table" width="100%">
@endif <tr class="table-header">
@if (isset($product->claims)) @if (isset($product->gencod))
<p><strong>{{ __('Allégations : ') }}</strong>{{ $product->claims }}</p>
@endif
<table class="table" width="100%">
<tr class="table-header">
@if (isset($product->gencod))
<td class="table-cell-text-center"><strong>{{ __('GENCOD') }}</strong></td> <td class="table-cell-text-center"><strong>{{ __('GENCOD') }}</strong></td>
@endif @endif
@if (isset($product->weight)) @if (isset($product->weight))
<td class="table-cell-text-center"><strong>{{ __('Poids UVC') }}</strong></td> <td class="table-cell-text-center"><strong>{{ __('Poids UVC') }}</strong></td>
@endif @endif
@if (isset($product->pvc)) @if (isset($product->pvc))
<td class="table-cell-text-center"><strong>{{ __('PVC') }}</strong></td> <td class="table-cell-text-center"><strong>{{ __('PVC') }}</strong></td>
@endif @endif
<td class="table-cell-text-center"><strong>{{ __('Prix kg') }}</strong></td> <td class="table-cell-text-center"><strong>{{ __('Prix kg') }}</strong></td>
</tr> </tr>
<tr class="table-body"> <tr class="table-body">
@if (isset($product->gencod)) @if (isset($product->gencod))
<td class="table-cell-text-center">{{ $product->gencod }}</td> <td class="table-cell-text-center">{{ $product->gencod }}</td>
@endif @endif
@if (isset($product->weight)) @if (isset($product->weight))
<td class="table-cell-text-center">{{ $product->weight }}</td> <td class="table-cell-text-center">{{ $product->weight }} g</td>
@endif @endif
@if (isset($product->pvc)) @if (isset($product->pvc))
<td class="table-cell-text-center">{{ $product->pvc }}€</td> <td class="table-cell-text-center">{{ $product->pvc }}€</td>
@endif @endif
<td class="table-cell-text-center">--,--€</td> @php
</tr> $priceKg = $product->weight ? (1000 * $product->pvc) / $product->weight : 0;
</table> @endphp
@if (isset($product->productKeywords)) <td class="table-cell-text-center">{{ number_format(round($priceKg, 2), 2, ',', ' ') }}€</td>
<p><strong>{{ __('Tendances identifiées : ') }}</strong>{{ $product->productKeywords }}</p> </tr>
@endif </table>
@if (isset($product->distribution_network)) @if (isset($product->productKeywords))
<p><strong>{{ __('Tendances identifiées : ') }}</strong>{{ Str::replaceLast(', ', '', $product->productKeywords) }}</p>
@endif
@if (isset($product->distribution_network))
<p><strong>{{ __('Réseau de distribution : ') }}</strong>{{ $product->distribution_network }}</p> <p><strong>{{ __('Réseau de distribution : ') }}</strong>{{ $product->distribution_network }}</p>
@endif @endif
@if (isset($product->ray)) @if (isset($product->ray))
<p><strong>{{ __('Rayon : ') }}</strong>{{ $product->ray }}</p> <p><strong>{{ __('Rayon : ') }}</strong>{{ $product->ray }}</p>
@endif @endif
@if (isset($product->product_range)) @if (isset($product->product_range))
<p><strong>{{ __('Famille : ') }}</strong>{{ $product->product_range }}</p> <p><strong>{{ __('Famille : ') }}</strong>{{ $product->product_range }}</p>
@endif @endif
@if (isset($product->geo_area)) @if (isset($product->geo_area))
<p><strong>{{ __('Zone géographique : ') }}</strong>{{ $product->geo_area }}</p> <p><strong>{{ __('Zone géographique : ') }}</strong>{{ $product->geo_area }}</p>
@endif @endif
</td> </td>
</tr> </tr>
</table> </table>
@if (isset($product->freeComment)) @if (isset($product->freeComment))
<p><strong>{{ __('Commentaire libre : ') }}</strong>{{ strip_tags($product->freeComment) }}</p> <p><strong>{{ __('Commentaire libre : ') }}</strong>{{ strip_tags($product->freeComment) }}</p>
@endif @endif
@if(isset($product->productComments))
@foreach ($product->productComments as $productComment) @foreach ($product->productComments as $productComment)
<p><strong>{{ __('Commentaire dégustation du : ') }}</strong>{{ $productComment->date }}</p> <p><strong>{{ __('Commentaire dégustation du : ') }}</strong>{{ $productComment->created_at }}</p>
<table class="table" width="100%"> <table class="table" width="100%">
<tr class="table-header"> <tr class="table-header">
<td class="table-cell-text-center" width="20%"><strong>{{ __('Appréciation') }}</strong></td> <td class="table-cell-text-center" width="20%"><strong>{{ __('Appréciation') }}</strong></td>
<td class="table-cell-text-center" width="20%"><strong>{{ __('Aspect') }}</strong></td> <td class="table-cell-text-center" width="20%"><strong>{{ __('Aspect') }}</strong></td>
<td class="table-cell-text-center" width="20%"><strong>{{ __('Odeur') }}</strong></td> <td class="table-cell-text-center" width="20%"><strong>{{ __('Odeur') }}</strong></td>
<td class="table-cell-text-center" width="20%"><strong>{{ __('Texture') }}</strong></td> <td class="table-cell-text-center" width="20%"><strong>{{ __('Texture') }}</strong></td>
<td class="table-cell-text-center" width="20%"><strong>{{ __('Goût') }}</strong></td> <td class="table-cell-text-center" width="20%"><strong>{{ __('Goût') }}</strong></td>
</tr> </tr>
<tr class="table-body"> <tr class="table-body">
<td class="table-cell-text-center" width="20%">{{ $productComment->evaluation }}</td> <td class="table-cell-text-center" width="20%">{{ $productComment->evaluation }}</td>
<td class="table-cell-text-center" width="20%">{{ $productComment->aspect }}</td> <td class="table-cell-text-center" width="20%">{{ $productComment->aspect }}</td>
<td class="table-cell-text-center" width="20%">{{ $productComment->taste }}</td> <td class="table-cell-text-center" width="20%">{{ $productComment->taste }}</td>
<td class="table-cell-text-center" width="20%">{{ $productComment->odor }}</td> <td class="table-cell-text-center" width="20%">{{ $productComment->odor }}</td>
<td class="table-cell-text-center" width="20%">{{ $productComment->texture }}</td> <td class="table-cell-text-center" width="20%">{{ $productComment->texture }}</td>
</tr> </tr>
</table> </table>
@endforeach @endforeach
@endif <table class="product-saw" width="100%">
<table width="100%" class="product-saw">
<tr> <tr>
@if (isset($product->seen_at))
<td class="table-cell-text-center" width="50%"> <td class="table-cell-text-center" width="50%">
<strong>{{ __('Téléchargez les documents :') }}</strong> <strong>{{ __('Téléchargez les documents :') }}</strong>
<a href="{{ route('products.download', $product->id) }}">{{ __('ICI') }}</a> <a href="{{ route('products.download', $product->id) }}">{{ __('ICI') }}</a>
</td> </td>
@endif
@if (isset($product->source)) @if (isset($product->source))
<td class="table-cell-text-center" width="50%"> <td class="table-cell-text-center" width="50%">
<p><strong>{{ __('Vu le : ') }}</strong>{{ $product->seen_at }}</p> <p><strong>{{ __('Vu le : ') }}</strong>{{ $product->seen_at }}</p>
<p><strong>{{ __('Lieu : ') }}</strong>{{ $product->source }},&nbsp;{{ $product->fair }}{{ $product->shop }}{{ $product->catalogue }}</p> <p><strong>{{ __('Lieu : ') }}</strong>{{ $product->source }},&nbsp;{{ $product->fair }}{{ $product->shop }}{{ $product->catalogue }}
<p><strong>{{ __('Par : ') }}</strong>{{ $product->user_id->firstname }}&nbsp;{{ $product->user_id->lastname }}</p> </p>
</td> <p><strong>{{ __('Par : ') }}</strong>{{ $product->user->firstname }}&nbsp;{{ $product->user->lastname }}</p>
</td>
@endif @endif
</tr> </tr>
</table> </table>

View file

@ -33,7 +33,6 @@
h2 { h2 {
font-weight: lighter; font-weight: lighter;
text-transform: uppercase;
margin-bottom: 1rem; margin-bottom: 1rem;
} }
@ -61,11 +60,11 @@
} }
.table-header { .table-header {
background-color: black; background-color: #FFFFFF;
} }
.table-header td { .table-header td {
color: white; color: #000000;
} }
.table-body td { .table-body td {
@ -75,6 +74,10 @@
.table-cell-text-center { .table-cell-text-center {
text-align: center; text-align: center;
} }
.table-cell-text-start {
text-align: start;
}
</style> </style>
</head> </head>

View file

@ -15,10 +15,10 @@
<li> <li>
<a class="dropdown-item @if(request()->routeIs('products.*')) active @endif" <a class="dropdown-item @if(request()->routeIs('products.*')) active @endif"
href="{{ route('products.index') }}" href="{{ route('products.index') }}"
title="{{ __('Administrer les :model', ['model' => __('models.classes.products')]) }}" title="{{ __('Administrer les :model', ['model' => trans_choice('models.classes.products_monitoring', 2)]) }}"
data-bs="tooltip" data-bs="tooltip"
data-bs-placement="right"> data-bs-placement="right">
{{ __('models.classes.products') }} {{ trans_choice('models.classes.products_monitoring', 2) }}
</a> </a>
</li> </li>
</ul> </ul>

View file

@ -1,60 +1,115 @@
@if ($paginator->hasPages()) {{-- GET ACTUAL PAGINATION --}}
<nav> @php
<ul class="pagination d-flex justify-content-center align-items-center m-0 mt-3"> $pagination = intval(
{{-- Previous Page Link --}} Cache::get(
@if ($paginator->onFirstPage()) 'pagination.' .
<li class="page-item disabled" data-bs="tooltip" data-bs-placement="top" \Illuminate\Support\Str::slug(
title="{{ __('Page précédente') }}" aria-disabled="true" aria-label="@lang('Page précédente')"> request()
<span class="page-link" aria-hidden="true">&lsaquo;</span> ->route()
</li> ->getName(),
@else ),
<li class="page-item"> ),
<a class="page-link" data-bs="tooltip" data-bs-placement="top" );
title="{{ __('Page précédente') }}" href="{{ $paginator->previousPageUrl() }}" @endphp
rel="prev" aria-label="@lang('Page précédente')">&lsaquo;</a> @if ($paginator->items())
</li> <nav class="pagination-custom d-flex justify-content-between align-items-center">
@endif {{-- SELECT ITEMS PER PAGE --}}
<div class="dropup-center d-flex justify-content-center align-items-center input-group w-fit">
{{-- Pagination Elements --}} <span class="btn-md input-group-text" data-bs-toggle="tooltip" data-bs-placement="top"
@foreach ($elements as $element) title="{{ __('crud.pagination.paginate_list') }}">
{{-- "Three Dots" Separator --}} <i class="fa-solid fa-list-ul"></i>
@if (is_string($element)) </span>
<li class="page-item disabled" aria-disabled="true"><span <button class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" type="button" aria-expanded="false">
class="page-link">{{ $element }}</span></li> {{ $pagination }}
@endif </button>
<ul class="dropdown-menu">
{{-- Array Of Links --}} @foreach (\App\Enums\Pagination\ItemsPerPaginationEnum::toArray() as $itemsPerPaginationEnum)
@if (is_array($element)) <li>
@foreach ($element as $page => $url) <a class="dropdown-item user-select-none @if ($pagination === $itemsPerPaginationEnum->value) active @endif text-center"
@if ($page == $paginator->currentPage()) href="{{ request()->fullUrlWithQuery(['page' => 1, 'pagination' => $itemsPerPaginationEnum->value]) }}"
<li class="page-item active" data-bs="tooltip" data-bs-placement="top" role="button">
title="{{ __('Page actuelle') }}" aria-current="page"> {{ $itemsPerPaginationEnum->value }}
<span class="page-link">{{ $page }}</span> </a>
</li> </li>
@else @endforeach
<li class="page-item"> </ul>
<a class="page-link" data-bs="tooltip" data-bs-placement="top" </div>
title="{{ __('Page n°:id', ['id' => $page]) }}" @if ($paginator->hasPages())
href="{{ $url }}">{{ $page }}</a> {{-- PAGINATION --}}
</li> <div class="d-flex justify-content-end align-items-center">
<ul class="pagination m-0">
{{-- PREVIOUS PAGE --}}
<li class="page-item @if ($paginator->onFirstPage()) disabled @endif">
<a class="page-link d-flex align-items-center h-100" data-bs-toggle="tooltip" data-bs-placement="top"
title="{{ __('crud.pagination.previous') }}" aria-label="@lang('crud.pagination.previous')"
@if (!$paginator->onFirstPage()) href="{{ request()->fullUrlWithQuery(['page' => $paginator->currentPage() - 1]) }}" @endif
rel="prev" @if ($paginator->onFirstPage()) aria-hidden="true" disabled @endif>
<i class="fa-solid fa-chevron-left fa-2xs mt-1"></i>
</a>
</li>
{{-- FIRST PAGE --}}
@if ($paginator->currentPage() > 3)
<li class="page-item d-none d-sm-block" data-bs-toggle="tooltip" data-bs-placement="top"
title="{{ __('crud.pagination.specific_page', ['id' => 1]) }}">
<a class="page-link" href="{{ request()->fullUrlWithQuery(['page' => 1]) }}">1</a>
</li>
@endif
{{-- HIDE PAGES TOO FAR --}}
@if ($paginator->currentPage() > 4)
<li class="page-item d-none d-sm-block disabled"><span class="page-link">...</span></li>
@endif
{{-- SHOW ACTUAL PAGES + N-2/N+2 --}}
@foreach (range(1, $paginator->lastPage()) as $i)
@if ($i >= $paginator->currentPage() - 2 && $i <= $paginator->currentPage() + 2)
@if ($i == $paginator->currentPage())
<li class="page-item">
<div class="dropup-center">
<button class="btn btn-primary dropdown-toggle rounded-0" data-bs-toggle="dropdown" type="button"
aria-expanded="false">
{{ $i }}
</button>
<ul class="dropdown-menu">
@for ($i = 1; $i <= $paginator->lastPage(); $i++)
<li>
<a class="dropdown-item user-select-none @if ($paginator->currentPage() === $i) active @endif text-center"
href="{{ request()->fullUrlWithQuery(['page' => $i]) }}" role="button">
{{ $i }}
</a>
</li>
@endfor
</ul>
</div>
</li>
@else
<li class="page-item d-none d-md-block" data-bs-toggle="tooltip" data-bs-placement="top"
title="{{ __('crud.pagination.specific_page', ['id' => $i]) }}">
<a class="page-link" href="{{ request()->fullUrlWithQuery(['page' => $i]) }}">{{ $i }}</a>
</li>
@endif
@endif @endif
@endforeach @endforeach
@endif {{-- HIDE PAGES TOO FAR --}}
@endforeach @if ($paginator->currentPage() < $paginator->lastPage() - 3)
<li class="page-item d-none d-sm-block disabled"><span class="page-link">...</span></li>
{{-- Next Page Link --}} @endif
@if ($paginator->hasMorePages()) {{-- LAST PAGE --}}
<li class="page-item"> @if ($paginator->currentPage() < $paginator->lastPage() - 2)
<a class="page-link" data-bs="tooltip" data-bs-placement="top" title="{{ __('Page suivante') }}" <li class="page-item d-none d-sm-block" data-bs-toggle="tooltip" data-bs-placement="top"
href="{{ $paginator->nextPageUrl() }}" rel="next" title="{{ __('crud.pagination.specific_page', ['id' => $paginator->lastPage()]) }}">
aria-label="@lang('Page suivante')">&rsaquo;</a> <a class="page-link"
</li> href="{{ request()->fullUrlWithQuery(['page' => $paginator->lastPage()]) }}">{{ $paginator->lastPage() }}</a>
@else </li>
<li class="page-item disabled" data-bs="tooltip" data-bs-placement="top" @endif
title="{{ __('Page suivante') }}" aria-disabled="true" aria-label="@lang('Page suivante')"> {{-- NEXT PAGE --}}
<span class="page-link" aria-hidden="true">&rsaquo;</span> <li class="page-item @if (!$paginator->hasMorePages()) disabled @endif">
</li> <a class="page-link d-flex align-items-center h-100" data-bs-toggle="tooltip" data-bs-placement="top"
@endif title="{{ __('crud.pagination.next') }}" aria-label="{{ __('crud.pagination.next') }}"
</ul> @if ($paginator->hasMorePages()) href="{{ request()->fullUrlWithQuery(['page' => $paginator->currentPage() + 1]) }}" @endif
rel="next" @if (!$paginator->hasMorePages()) aria-hidden="true" disabled @endif>
<i class="fa-solid fa-chevron-right fa-2xs mt-1"></i></a>
</li>
</ul>
</div>
@endif
</nav> </nav>
@endif @endif

View file

@ -16,4 +16,7 @@
multipdf: "{{ route('products.multipdf.update', ['id' => 'ID']) }}", multipdf: "{{ route('products.multipdf.update', ['id' => 'ID']) }}",
duplicate: "{{ route('products.duplicate', ['product' => 'PRODUCT']) }}" duplicate: "{{ route('products.duplicate', ['product' => 'PRODUCT']) }}"
}; };
window.__SYSTEM._routes.brands = {
store: "{{ route('brands.store') }}",
};
</script> </script>

View file

@ -12,7 +12,6 @@ $ignore = (isset($ignore) and is_array($ignore)) ? $ignore : [];
@endphp @endphp
{{-- NO Sort for order column --}} {{-- NO Sort for order column --}}
@if($col === 'order' or in_array($col, $ignore)) @if($col === 'order' or in_array($col, $ignore))
@if(!$noOrder or $rst)
@if ($col === 'multi_pdf') @if ($col === 'multi_pdf')
<th scope="col" class="text-center @if(isset($mobileHide) and is_array($mobileHide) and in_array($col, $mobileHide)) d-none d-md-table-cell @endif"> <th scope="col" class="text-center @if(isset($mobileHide) and is_array($mobileHide) and in_array($col, $mobileHide)) d-none d-md-table-cell @endif">
<span>{{ $colname }}</span> <span>{{ $colname }}</span>
@ -35,7 +34,6 @@ $ignore = (isset($ignore) and is_array($ignore)) ? $ignore : [];
<span>{{ $colname }}</span> <span>{{ $colname }}</span>
</th> </th>
@endif @endif
@endif
@continue @continue
@endif @endif
<th scope="col" class="text-center @if(isset($mobileHide) and is_array($mobileHide) and in_array($col, $mobileHide)) d-none d-md-table-cell @endif"> <th scope="col" class="text-center @if(isset($mobileHide) and is_array($mobileHide) and in_array($col, $mobileHide)) d-none d-md-table-cell @endif">

View file

@ -16,14 +16,14 @@
<div class="card-body"> <div class="card-body">
<div class="d-flex flex-column flex-lg-row justify-content-between align-items-center"> <div class="d-flex flex-column flex-lg-row justify-content-between align-items-center">
<div class="text-center text-lg-start pb-2 p-lg-0"> <div class="text-center text-lg-start pb-2 p-lg-0">
<h5 class="card-title">{{ __('models.classes.products') }}</h5> <h5 class="card-title">{{ trans_choice('models.classes.products_monitoring', 2) }}</h5>
<p class="card-text text-muted fst-italic">{{ __('- accéder à l\'ensemble des produits enregistrés') }}</p> <p class="card-text text-muted fst-italic">{{ __('- accéder à l\'ensemble des produits enregistrés') }}</p>
</div> </div>
<a href="{{ route('products.index') }}" <a href="{{ route('products.index') }}"
class="btn btn-primary" class="btn btn-primary"
title="{{ __('Accéder à la liste des :model', ['model' => __('models.classes.products')]) }}" title="{{ __('Accéder à la liste des :model', ['model' => trans_choice('models.classes.products_monitoring', 2)]) }}"
data-bs="tooltip"> data-bs="tooltip">
{{ __('Voir la liste') }} {{ __('Consulter/Insérer une :model', ['model' => trans_choice('models.classes.products_monitoring', 1)]) }}
</a> </a>
</div> </div>
</div> </div>

View file

@ -1,6 +1,6 @@
@extends('layout') @extends('layout')
@section('title', sprintf('%s - %s', __('models.classes.products'), Str::ucfirst(__('création')))) @section('title', sprintf('%s - %s', trans_choice('models.classes.products_monitoring', 1), Str::ucfirst(__('création'))))
@section('description', __('Création d\'un produit')) @section('description', __('Création d\'un produit'))
@section('breadcrumb', 'no') @section('breadcrumb', 'no')
@ -14,17 +14,17 @@
class="btn btn-primary text-decoration-none m-0" class="btn btn-primary text-decoration-none m-0"
data-bs="tooltip" data-bs="tooltip"
data-bs-placement="top" data-bs-placement="top"
title="{{ __('Retourner sur la liste des :model', ['model' => __('models.classes.products')]) }}" title="{{ __('Retourner sur la liste des :model', ['model' => trans_choice('models.classes.products_monitoring', 2)]) }}"
data-bs="tooltip"> data-bs="tooltip">
<i class="fa-solid fa-arrow-left"></i> <i class="fa-solid fa-arrow-left"></i>
</a> </a>
<span class="fw-bold">{{ Str::singular(__('models.classes.products')) }}</span>&nbsp;<small class="text-muted">{{ Str::ucfirst(__('création')) }}</small> <span class="fw-bold">{{ trans_choice('models.classes.products_monitoring', 1) }}</span>&nbsp;<small class="text-muted">{{ Str::ucfirst(__('création')) }}</small>
</h1> </h1>
</div> </div>
<div class="col-12 col-md-4 d-flex justify-content-center justify-content-md-end align-items-center"> <div class="col-12 col-md-4 d-flex justify-content-center justify-content-md-end align-items-center">
<button id="formSubmitClone" <button id="formSubmitClone"
class="btn btn-primary pull-end" class="btn btn-success pull-end"
title="{{ __('Sauvegarder ce :model', ['model' => Str::singular(__('models.classes.products'))]) }}" title="{{ __('Sauvegarder cette :model', ['model' => trans_choice('models.classes.products_monitoring', 1)]) }}"
data-bs="tooltip"> data-bs="tooltip">
{{ __('Sauvegarder') }} {{ __('Sauvegarder') }}
</button> </button>
@ -39,8 +39,8 @@
<div class="col text-center"> <div class="col text-center">
<button id="formSubmit" <button id="formSubmit"
type="submit" type="submit"
class="btn btn-primary" class="btn btn-success"
title="{{ __('Sauvegarder ce :model', ['model' => Str::singular(__('models.classes.products'))]) }}" title="{{ __('Sauvegarder cette :model', ['model' => trans_choice('models.classes.products_monitoring', 1)]) }}"
data-bs="tooltip">{{ __('Sauvegarder') }}</button> data-bs="tooltip">{{ __('Sauvegarder') }}</button>
</div> </div>
</div> </div>

View file

@ -1,6 +1,6 @@
@extends('layout') @extends('layout')
@section('title', sprintf('%s - %s', __('models.classes.products'), Str::ucfirst(__('édition')))) @section('title', sprintf('%s - %s', trans_choice('models.classes.products_monitoring', 1), Str::ucfirst(__('édition'))))
@section('description', __('Édition d\'un produit')) @section('description', __('Édition d\'un produit'))
@section('breadcrumb', 'no') @section('breadcrumb', 'no')
@ -14,11 +14,11 @@
class="btn btn-primary text-decoration-none m-0" class="btn btn-primary text-decoration-none m-0"
data-bs="tooltip" data-bs="tooltip"
data-bs-placement="top" data-bs-placement="top"
title="{{ __('Retourner sur la liste des :model', ['model' => __('models.classes.products')]) }}" title="{{ __('Retourner sur la liste des :model', ['model' => trans_choice('models.classes.products_monitoring', 2)]) }}"
data-bs="tooltip"> data-bs="tooltip">
<i class="fa-solid fa-arrow-left"></i> <i class="fa-solid fa-arrow-left"></i>
</a> </a>
<span class="fw-bold">{{ Str::singular(__('models.classes.products')) }}</span>&nbsp;<small class="text-muted">{{ Str::ucfirst(__('édition')) }}</small> <span class="fw-bold">{{ trans_choice('models.classes.products_monitoring', 1) }}</span>&nbsp;<small class="text-muted">{{ Str::ucfirst(__('édition')) }}</small>
</h1> </h1>
</div> </div>
<div class="col-12 col-md-4 d-flex justify-content-center justify-content-md-end align-items-center"> <div class="col-12 col-md-4 d-flex justify-content-center justify-content-md-end align-items-center">
@ -28,13 +28,13 @@
<div class="btn-group" role="group"> <div class="btn-group" role="group">
<button id="formSubmitClone" <button id="formSubmitClone"
type="button" type="button"
class="btn btn-primary pull-end" class="btn btn-success pull-end"
title="{{ __('Sauvegarder ce :model', ['model' => Str::singular(__('models.classes.products'))]) }}" title="{{ __('Sauvegarder cette :model', ['model' => trans_choice('models.classes.products_monitoring', 1)]) }}"
data-bs="tooltip"> data-bs="tooltip">
{{ __('Sauvegarder') }} {{ __('Sauvegarder') }}
</button> </button>
<button class="btn btn-danger" <button class="btn btn-danger"
title="{{ __('Supprimer ce :model', ['model' => Str::singular(__('models.classes.products'))]) }}" title="{{ __('Supprimer cette :model', ['model' => trans_choice('models.classes.products_monitoring', 1)]) }}"
data-bs="tooltip"> data-bs="tooltip">
<i class="fa-solid fa-trash"></i> <i class="fa-solid fa-trash"></i>
</button> </button>
@ -52,8 +52,8 @@
<div class="col text-center"> <div class="col text-center">
<button id="formSubmit" <button id="formSubmit"
type="submit" type="submit"
class="btn btn-primary" class="btn btn-success"
title="{{ __('Sauvegarder ce :model', ['model' => Str::singular(__('models.classes.products'))]) }}" title="{{ __('Sauvegarder cette :model', ['model' => trans_choice('models.classes.products_monitoring', 1)]) }}"
data-bs="tooltip"> data-bs="tooltip">
{{ __('Sauvegarder') }} {{ __('Sauvegarder') }}
</button> </button>

View file

@ -1,17 +1,18 @@
<div class="row"> <div class="row">
{{-- TITLE --}} {{-- TITLE --}}
<div class="col-12 col-md-6 mb-3"> <div class="col-12 col-md-6 mb-3">
<fieldset class="border h-100 bg-white p-3"> <fieldset class="h-100 border bg-white p-3">
<legend>{{ __('Titre') }}</legend> <legend>{{ __('Nom') }}</legend>
<div class="row"> <div class="row">
<div class="col-12 mb-3"> <div class="col-12 mb-3">
@include('partials.inputs.varchar', [ @include('partials.inputs.varchar', [
'id' => 'productName', 'id' => 'productName',
'label' => __('Titre du :model', ['model' => Str::singular(__('models.classes.products'))]), 'label' => __('Nom du :model', ['model' => Str::singular(__('models.classes.products'))]),
'hasWordCounter' => true, 'hasWordCounter' => true,
'name' => 'name', 'name' => 'name',
'required' => true, 'required' => true,
'model' => $productModel ?? null, 'model' => $productModel ?? null,
'placeholder' => '',
'helper' => __('Doit décrire précisément le produit (3 caractères minimum, 255 maximum).'), 'helper' => __('Doit décrire précisément le produit (3 caractères minimum, 255 maximum).'),
]) ])
</div> </div>
@ -20,17 +21,21 @@
</div> </div>
{{-- BRANDS --}} {{-- BRANDS --}}
<div class="col-12 col-md-6 mb-3"> <div class="col-12 col-md-6 mb-3">
<fieldset class="border h-100 bg-white p-3"> <fieldset class="h-100 border bg-white p-3">
<legend>{{ __('models.classes.brands') }}</legend> <legend>{{ __('models.classes.brands') }}</legend>
<div class="row"> <div class="row">
<div class="col-12 mb-3"> <div class="col-12 mb-3">
@include('partials.inputs.bigint-foreign-id', [ @include('partials.inputs.bigint-belongs-to', [
'id' => 'productBrand',
'label' => trans(':model associé(e)', ['model' => Str::singular(trans('models.classes.brands'))]),
'name' => 'brand_id', 'name' => 'brand_id',
'model' => $productModel ?? null, 'model' => $productModel ?? null,
'associatedModels' => $brands, 'itemLimit' => [0, 1],
'helper' => trans('Selectionnez un(e) :model parmis la liste.', ['model' => Str::singular(trans('models.classes.brands'))]), 'itemsList' => $brands,
'storeRoute' => 'brands.store',
'labelField' => 'name',
'helper' => __('La marque associée au produit.'),
'placeholder' => '',
'require' => true,
'label' => __('Marque associée'),
]) ])
</div> </div>
</div> </div>
@ -38,42 +43,47 @@
</div> </div>
{{-- SOURCE --}} {{-- SOURCE --}}
<div class="col-12 mb-3"> <div class="col-12 mb-3">
<fieldset class="border h-100 bg-white p-3"> <fieldset class="h-100 border bg-white p-3">
<legend>{{ __("Source") }}</legend> <legend>{{ __('Source') }}</legend>
@php @php
$data = [ $data = [
'id' => 'productSource', 'id' => 'productSource',
'label' => __('Source associé(e)'), 'label' => __('Source'),
'name' => 'source', 'name' => 'source',
'model' => $productModel ?? null, 'model' => $productModel ?? null,
'sourceEnum' => \App\Enums\Products\SourceEnum::toArray(), 'sourceEnum' => \App\Enums\Products\SourceEnum::toArray(),
'productsFair' => $productsFair, 'productsFair' => $productsFair,
'productsShop' => $productsShop, 'productsShop' => $productsShop,
'productsCatalog' => $productsCatalog, 'productsCatalog' => $productsCatalog,
'helper' => __('Selectionnez un(e) Source maximum parmis la liste.') 'oldValue' => old('source') ?? ($productModel->source ?? 0),
]; 'oldDetails' => old('shop', old('fair', old('catalogue'))) ?? '',
'helper' => __('Selectionnez un(e) Source maximum parmi la liste.'),
];
@endphp @endphp
<div class="source-inputs" data-json='@json($data)'></div> <div class="source-inputs" data-json='@json($data)'></div>
@include('modules.input-error', ['inputName' => 'source']) @include('modules.input-error', ['inputName' => 'source'])
@include('modules.input-error', ['inputName' => 'catalogue'])
@include('modules.input-error', ['inputName' => 'shop'])
@include('modules.input-error', ['inputName' => 'fair'])
</fieldset> </fieldset>
</div> </div>
{{-- GENCODE --}} {{-- GENCOD --}}
<div class="col-12 col-md-6 mb-3"> <div class="col-12 col-md-6 mb-3">
<fieldset class="border h-100 bg-white p-3"> <fieldset class="h-100 border bg-white p-3">
<legend>{{ __('GENCODE') }}</legend> <legend>{{ __('GENCOD') }}</legend>
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
@php @php
$data = [ $data = [
'id' => 'productGencod', 'id' => 'productGencod',
'label' => __('Code'), 'label' => __('Code'),
'name' => 'gencod', 'name' => 'gencod',
'model' => $productModel ?? null, 'model' => $productModel ?? null,
'value' => old('gencod') ?? $productModel->gencod ?? null, 'value' => old('gencod') ?? ($productModel->gencod ?? null),
'maxlength' => 13, 'maxlength' => 13,
'required' => false, 'required' => false,
'helper' => __('Saisir une suite de 13 chiffres maximum.') 'helper' => __('Saisir une suite de 13 chiffres maximum.'),
]; ];
@endphp @endphp
<div class="verification-code" data-json='@json($data)'></div> <div class="verification-code" data-json='@json($data)'></div>
@include('modules.input-error', ['inputName' => 'gencod']) @include('modules.input-error', ['inputName' => 'gencod'])
@ -83,7 +93,7 @@
</div> </div>
{{-- URL --}} {{-- URL --}}
<div class="col-12 col-md-6 mb-3"> <div class="col-12 col-md-6 mb-3">
<fieldset class="border h-100 bg-white p-3"> <fieldset class="h-100 border bg-white p-3">
<legend>{{ __('URL') }}</legend> <legend>{{ __('URL') }}</legend>
<div class="row"> <div class="row">
<div class="col-12 mb-3"> <div class="col-12 mb-3">
@ -94,7 +104,7 @@
'name' => 'url', 'name' => 'url',
'required' => false, 'required' => false,
'model' => $productModel ?? null, 'model' => $productModel ?? null,
'placeholder' => __('https://google.com'), 'placeholder' => '',
'helper' => __('Doit être une url valide.'), 'helper' => __('Doit être une url valide.'),
]) ])
</div> </div>
@ -103,33 +113,9 @@
</div> </div>
{{-- IMAGE --}} {{-- IMAGE --}}
<div class="col-12 mb-3"> <div class="col-12 mb-3">
<fieldset class="border h-100 bg-white p-3"> <fieldset class="h-100 border bg-white p-3">
<legend>{{ __('Image') }}</legend> <legend>{{ __('Visuel de référence') }}</legend>
<div class="row"> <div class="row">
{{-- PICTURE_ALT --}}
<div class="col-12 col-md-6 mb-3">
@include('partials.inputs.varchar', [
'id' => 'homePart3PictureAlt',
'label' => __('Texte alternatif de :model', ['model' => Str::singular(__('models.classes.products'))]),
'hasWordCounter' => true,
'name' => 'picture_alt',
'required' => true,
'model' => $productModel ?? null,
'helper' => __('Doit décrire votre image pour les moteurs de recherches et les non voyants (5 à 6 mots, minimum 3 caractères).'),
])
</div>
{{-- PICTURE_TITLE --}}
<div class="col-12 col-md-6 mb-3">
@include('partials.inputs.varchar', [
'id' => 'homePart3PictureTitle',
'label' => __('Titre de :model', ['model' => Str::singular(__('models.classes.products'))]),
'hasWordCounter' => true,
'name' => 'picture_title',
'required' => true,
'model' => $productModel ?? null,
'helper' => __('Utile pour les utilisateurs, s\'affiche en passant la souris sur l\'image (minimum 3 caractères).'),
])
</div>
<div class="col-12"> <div class="col-12">
@include('partials.inputs.varchar-picture', [ @include('partials.inputs.varchar-picture', [
'id' => 'productImage', 'id' => 'productImage',
@ -147,19 +133,33 @@
</div> </div>
{{-- DOCUMENTS --}} {{-- DOCUMENTS --}}
<div class="col-12 mb-3"> <div class="col-12 mb-3">
<fieldset class="border h-100 bg-white p-3"> <fieldset class="h-100 border bg-white p-3">
<legend>{{ __('Documents') }}</legend> <legend>{{ __('Documents complémentaires') }}</legend>
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
{{-- application/msword
application/vnd.openxmlformats-officedocument.wordprocessingml.document
image/jpeg
image/png
application/pdf
application/vnd.ms-powerpoint
application/vnd.openxmlformats-officedocument.presentationml.presentation
application/vnd.ms-excel
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
text/csv
image/gif
text/plain pour .txt --}}
@php @php
$data = [ $data = [
'id' => 'productDocs', 'id' => 'productDocs',
'name' => 'docs[]', 'name' => 'docs[]',
'value' => $productModel->docs ?? [], 'value' => $productModel->docs ?? [],
'limit' => [0,5], 'limit' => [0, 5],
'helper' => __('Doit respecter le format :format.', ['format' => '.pdf']), 'helper' => __('Doit respecter le format :format.', [
'errors' => $errors->getBag('default')->getMessages() 'format' => '.xls,.csv,.xlsx,.png,.jpg,.jpeg,.gif,.doc,.docx,.ppt,.pdf,.txt',
]; ]),
'errors' => $errors->getBag('default')->getMessages(),
];
@endphp @endphp
<div class="multiple-documents-input" data-json='@json($data)'></div> <div class="multiple-documents-input" data-json='@json($data)'></div>
</div> </div>
@ -168,7 +168,7 @@
</div> </div>
{{-- CERTIFICATION --}} {{-- CERTIFICATION --}}
<div class="col-12 col-md-6 mb-3"> <div class="col-12 col-md-6 mb-3">
<fieldset class="border h-100 bg-white p-3"> <fieldset class="h-100 border bg-white p-3">
<legend>{{ __('Certification') }}</legend> <legend>{{ __('Certification') }}</legend>
<div class="row"> <div class="row">
<div class="col-12 mb-3"> <div class="col-12 mb-3">
@ -179,7 +179,7 @@
'model' => $productModel ?? null, 'model' => $productModel ?? null,
'require' => false, 'require' => false,
'associatedModels' => \App\Enums\Products\CertificationEnum::toArray(), 'associatedModels' => \App\Enums\Products\CertificationEnum::toArray(),
'helper' => trans('Selectionnez une Certification parmis la liste.'), 'helper' => trans('Selectionnez une Certification parmi la liste.'),
]) ])
</div> </div>
</div> </div>
@ -187,18 +187,18 @@
</div> </div>
{{-- ORIGINS --}} {{-- ORIGINS --}}
<div class="col-12 col-md-6 mb-3"> <div class="col-12 col-md-6 mb-3">
<fieldset class="border h-100 bg-white p-3"> <fieldset class="h-100 border bg-white p-3">
<legend>{{ __('Origines') }}</legend> <legend>{{ __('Origine') }}</legend>
<div class="row"> <div class="row">
@include('partials.inputs.select-multiple-enum', [ @include('partials.inputs.select-multiple-enum', [
'id' => 'productOrigins', 'id' => 'productOrigins',
'label' => __('Origines associées'), 'label' => __('Origine associée'),
'name' => 'origins', 'name' => 'origins',
'itemsSelected' => old('origins', $productModel->origins ?? []), 'itemsSelected' => old('origins', $productModel->origins ?? []),
'itemsList' => collect(\App\Enums\Products\OriginEnum::toArray())->values(), 'itemsList' => collect(\App\Enums\Products\OriginEnum::toArray())->values(),
'itemLimit' => [0, count(\App\Enums\Products\OriginEnum::toArray())], 'itemLimit' => [0, count(\App\Enums\Products\OriginEnum::toArray())],
'placeholder' => __('Recherchez une Origine a associer.'), 'placeholder' => '',
'helper' => __('Les Origines associées au produit.'), 'helper' => __('L\'origine associée au produit.'),
'require' => false, 'require' => false,
]) ])
</div> </div>
@ -206,7 +206,7 @@
</div> </div>
{{-- CLAIMS --}} {{-- CLAIMS --}}
<div class="col-12 mb-3"> <div class="col-12 mb-3">
<fieldset class="border h-100 bg-white p-3"> <fieldset class="h-100 border bg-white p-3">
<legend>{{ __('Allégations') }}</legend> <legend>{{ __('Allégations') }}</legend>
<div class="row"> <div class="row">
@include('partials.inputs.select-multiple-enum', [ @include('partials.inputs.select-multiple-enum', [
@ -219,13 +219,13 @@
'showSpecificItem' => [ 'showSpecificItem' => [
'name' => 'specified_claim', 'name' => 'specified_claim',
'label' => 'Précision de l\'allégation', 'label' => 'Précision de l\'allégation',
'placeholder' => 'Sans sucre ajouté', 'placeholder' => '',
'value' => $productModel->specified_claim ?? null, 'value' => old('specified_claim', $productModel->specified_claim ?? null),
'showId' => 99, 'showId' => 99,
'helper' => __('Doit décrire précisément l\'allégation (3 caractères minimum, 255 maximum).') 'helper' => __('Doit décrire précisément l\'allégation (3 caractères minimum, 255 maximum).'),
], ],
'model' => $productModel ?? null, 'model' => $productModel ?? null,
'placeholder' => __('Recherchez une Allégation a associer.'), 'placeholder' => '',
'require' => false, 'require' => false,
'helper' => __('Les Allégations associées au produit.'), 'helper' => __('Les Allégations associées au produit.'),
]) ])
@ -234,7 +234,7 @@
</div> </div>
{{-- DISTRIBUTION NETWORK --}} {{-- DISTRIBUTION NETWORK --}}
<div class="col-12 col-md-6 mb-3"> <div class="col-12 col-md-6 mb-3">
<fieldset class="border h-100 bg-white p-3"> <fieldset class="h-100 border bg-white p-3">
<legend>{{ __('Réseau de distribution') }}</legend> <legend>{{ __('Réseau de distribution') }}</legend>
<div class="row"> <div class="row">
<div class="col-12 mb-3"> <div class="col-12 mb-3">
@ -243,9 +243,9 @@
'label' => __('Réseau de distribution associé'), 'label' => __('Réseau de distribution associé'),
'name' => 'distribution_network', 'name' => 'distribution_network',
'model' => $productModel ?? null, 'model' => $productModel ?? null,
'require' => true, 'required' => true,
'associatedModels' => \App\Enums\Products\DistributionNetworkEnum::toArray(), 'associatedModels' => \App\Enums\Products\DistributionNetworkEnum::toArray(),
'helper' => trans('Selectionnez un Réseau de distribution parmis la liste.'), 'helper' => trans('Selectionnez un Réseau de distribution parmi la liste.'),
]) ])
</div> </div>
</div> </div>
@ -253,7 +253,7 @@
</div> </div>
{{-- RAY --}} {{-- RAY --}}
<div class="col-12 col-md-6 mb-3"> <div class="col-12 col-md-6 mb-3">
<fieldset class="border h-100 bg-white p-3"> <fieldset class="h-100 border bg-white p-3">
<legend>{{ __('Rayon') }}</legend> <legend>{{ __('Rayon') }}</legend>
<div class="row"> <div class="row">
<div class="col-12 mb-3"> <div class="col-12 mb-3">
@ -262,9 +262,9 @@
'label' => __('Rayon associé'), 'label' => __('Rayon associé'),
'name' => 'ray', 'name' => 'ray',
'model' => $productModel ?? null, 'model' => $productModel ?? null,
'require' => true, 'required' => true,
'associatedModels' => \App\Enums\Products\RayEnum::toArray(), 'associatedModels' => \App\Enums\Products\RayEnum::toArray(),
'helper' => trans('Selectionnez un Rayon parmis la liste.'), 'helper' => trans('Selectionnez un Rayon parmi la liste.'),
]) ])
</div> </div>
</div> </div>
@ -272,7 +272,7 @@
</div> </div>
{{-- RANGE --}} {{-- RANGE --}}
<div class="col-12 col-md-6 mb-3"> <div class="col-12 col-md-6 mb-3">
<fieldset class="border h-100 bg-white p-3"> <fieldset class="h-100 border bg-white p-3">
<legend>{{ __('Famille de produit') }}</legend> <legend>{{ __('Famille de produit') }}</legend>
<div class="row"> <div class="row">
<div class="col-12 mb-3"> <div class="col-12 mb-3">
@ -281,9 +281,9 @@
'label' => __('Famille de produit associée'), 'label' => __('Famille de produit associée'),
'name' => 'product_range', 'name' => 'product_range',
'model' => $productModel ?? null, 'model' => $productModel ?? null,
'require' => true, 'required' => true,
'associatedModels' => \App\Enums\Products\RangeEnum::toArray(), 'associatedModels' => \App\Enums\Products\RangeEnum::toArray(),
'helper' => trans('Selectionnez une Famille de produit parmis la liste.'), 'helper' => trans('Selectionnez une Famille de produit parmi la liste.'),
]) ])
</div> </div>
</div> </div>
@ -291,7 +291,7 @@
</div> </div>
{{-- GEO AREA --}} {{-- GEO AREA --}}
<div class="col-12 col-md-6 mb-3"> <div class="col-12 col-md-6 mb-3">
<fieldset class="border h-100 bg-white p-3"> <fieldset class="h-100 border bg-white p-3">
<legend>{{ __('Zone géographique') }}</legend> <legend>{{ __('Zone géographique') }}</legend>
<div class="row"> <div class="row">
<div class="col-12 mb-3"> <div class="col-12 mb-3">
@ -300,9 +300,9 @@
'label' => __('Zone géographique associée'), 'label' => __('Zone géographique associée'),
'name' => 'geo_area', 'name' => 'geo_area',
'model' => $productModel ?? null, 'model' => $productModel ?? null,
'require' => true, 'required' => true,
'associatedModels' => \App\Enums\Products\GeoAreaEnum::toArray(), 'associatedModels' => \App\Enums\Products\GeoAreaEnum::toArray(),
'helper' => trans('Selectionnez une Zone géographique parmis la liste.'), 'helper' => trans('Selectionnez une Zone géographique parmi la liste.'),
]) ])
</div> </div>
</div> </div>
@ -310,7 +310,7 @@
</div> </div>
{{-- WEIGHT --}} {{-- WEIGHT --}}
<div class="col-12 col-md-6 mb-3"> <div class="col-12 col-md-6 mb-3">
<fieldset class="border h-100 bg-white p-3"> <fieldset class="h-100 border bg-white p-3">
<legend>{{ __('Poids') }}</legend> <legend>{{ __('Poids') }}</legend>
<div class="row"> <div class="row">
<div class="col-12 mb-3"> <div class="col-12 mb-3">
@ -329,16 +329,17 @@
</div> </div>
{{-- PVC --}} {{-- PVC --}}
<div class="col-12 col-md-6 mb-3"> <div class="col-12 col-md-6 mb-3">
<fieldset class="border h-100 bg-white p-3"> <fieldset class="h-100 border bg-white p-3">
<legend>{{ __('PVC') }}</legend> <legend>{{ __('PVC') }}</legend>
<div class="row"> <div class="row">
<div class="col-12 mb-3"> <div class="col-12 mb-3">
@include('partials.inputs.int-number', [ @include('partials.inputs.int-number', [
'id' => 'productWeight', 'id' => 'productPvc',
'label' => __('PVC de l\'UVC'), 'label' => __('PVC de l\'UVC'),
'name' => 'pvc', 'name' => 'pvc',
'model' => $productModel ?? null, 'model' => $productModel ?? null,
'min' => 0, 'min' => 0,
'step' => 0.01,
'required' => false, 'required' => false,
'helper' => trans('Le prix doit être en euro (€).'), 'helper' => trans('Le prix doit être en euro (€).'),
]) ])
@ -346,9 +347,51 @@
</div> </div>
</fieldset> </fieldset>
</div> </div>
{{-- Prix au kilo --}}
<div class="col-12 col-md-6 mb-3">
<fieldset class="h-100 border bg-white p-3">
<legend>{{ __('Prix au kilo') }}</legend>
<div class="row">
<div class="col-12 mb-3">
@php
$priceKg = 0;
if ($productModel && $productModel->weight != 0) {
$priceKg = (1000 * $productModel->pvc) / $productModel->weight;
}
@endphp
@include('partials.inputs.int-number', [
'id' => 'productPriceKg',
'label' => __('Prix au kilo du produit'),
'name' => 'priceKg',
'model' => null,
'required' => false,
'disabled' => true,
'value' => $priceKg,
'helper' => trans('Le prix au kilo est calculé automatiquement.'),
])
</div>
</div>
</fieldset>
</div>
{{-- KEYWORDS --}}
<div class="col-12 mb-3">
<fieldset class="h-100 border bg-white p-3">
<legend>{{ __('Précisez la ou les tendances') }}</legend>
<div class="row">
@include('partials.inputs.pivot-taggable', [
'name' => 'productKeywords',
'model' => $productModel ?? null,
'itemLimit' => [0, count($keywords)],
'itemsList' => $keywords,
'helper' => __('Les Tendances associées au produit.'),
'placeholder' => '',
])
</div>
</fieldset>
</div>
{{-- COMMENT --}} {{-- COMMENT --}}
<div class="col-12 mb-3"> <div class="col-12 mb-3">
<fieldset class="border h-100 bg-white p-3"> <fieldset class="h-100 border bg-white p-3">
<legend>{{ __('Commentaire libre') }}</legend> <legend>{{ __('Commentaire libre') }}</legend>
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
@ -358,63 +401,49 @@
'name' => 'comment', 'name' => 'comment',
'model' => $productModel ?? null, 'model' => $productModel ?? null,
'required' => false, 'required' => false,
'helper' => __(' Doit décrire précisément le contenu du produit, requiert entre 50 et 160 caractères, balise <meta> description.'), 'helper' => __(
'placeholder' => __('Une description du contenu du produit pour les moteurs de recherche.'), ' Doit décrire précisément le contenu du produit, requiert entre 50 et 160 caractères, balise <meta> description.'),
'placeholder' => '',
]) ])
</div> </div>
</div> </div>
</fieldset> </fieldset>
</div> </div>
{{-- KEYWORDS --}}
<div class="col-12 mb-3">
<fieldset class="border h-100 bg-white p-3">
<legend>{{ __('Tendances') }}</legend>
<div class="row">
@include('partials.inputs.pivot-taggable', [
'name' => 'productKeywords',
'model' => $productModel ?? null,
'itemLimit' => [0, count($keywords)],
'itemsList' => $keywords,
'helper' => __('Les Tendances associées au produit.'),
'placeholder' => __('Recherchez une Tendance a associer.'),
])
</div>
</fieldset>
</div>
{{-- TASTED --}} {{-- TASTED --}}
<div class="col-12 mb-3"> <div class="col-12 mb-3">
@php @php
if (session()->getOldInput()['evaluation'] ?? null) { if (session()->getOldInput()['evaluation'] ?? null) {
foreach (session()->getOldInput()['evaluation'] as $key => $value) { foreach (session()->getOldInput()['evaluation'] as $key => $value) {
$commentOldModel[] = [ $commentOldModel[] = [
"evaluation" => session()->getOldInput()['evaluation'][$key] ?? null, 'evaluation' => session()->getOldInput()['evaluation'][$key] ?? null,
"aspect" => session()->getOldInput()['aspect'][$key] ?? null, 'aspect' => session()->getOldInput()['aspect'][$key] ?? null,
"taste" => session()->getOldInput()['taste'][$key] ?? null, 'taste' => session()->getOldInput()['taste'][$key] ?? null,
"odor" => session()->getOldInput()['odor'][$key] ?? null, 'odor' => session()->getOldInput()['odor'][$key] ?? null,
"texture" => session()->getOldInput()['texture'][$key] ?? null, 'texture' => session()->getOldInput()['texture'][$key] ?? null,
]; ];
}
} }
} $data = [
$data = [ 'id' => 'productSource',
'id' => 'productSource', 'title1' => __('Dégustation'),
'title1' => __("Dégustation"), 'label1' => __('Produit dégusté ?'),
'label1' => __('Produit dégusté ?'), 'name1' => 'is_tasted',
'name1' => 'is_tasted', 'oldCheck' => boolval(old('is_tasted', null)),
'model' => $productModel ?? null, 'model' => $productModel ?? null,
'oldModel' => session()->getOldInput(), 'oldModel' => session()->getOldInput(),
'max' => 15, 'max' => 15,
'min' => 1, 'min' => 1,
'helper1' => __('Le produit a été dégusté.'), 'helper1' => __('Le produit a été dégusté.'),
'required1' => false, 'required1' => false,
'title2' => __("Commentaires"), 'title2' => __('Commentaires'),
'name2' => 'tasters_number', 'name2' => 'tasters_number',
'label2' => __('Nombre de dégustation'), 'label2' => __('Nombre de dégustation'),
'helper2' => __('Doit être un chiffre ou un nombre (minimum 1 et maximum 15).'), 'helper2' => __('Doit être un chiffre ou un nombre (minimum 1 et maximum 15).'),
'required2' => false, 'required2' => false,
'commentModel' => $commentModel ?? [], 'commentModel' => $commentModel ?? [],
'commentOldModel' => $commentOldModel ?? [], 'commentOldModel' => $commentOldModel ?? [],
'errors' => $errors->getBag('default')->getMessages() ?? [] 'errors' => $errors->getBag('default')->getMessages() ?? [],
]; ];
@endphp @endphp
<div class="tasted-product" data-json='@json($data)'></div> <div class="tasted-product" data-json='@json($data)'></div>
</div> </div>
@ -429,5 +458,21 @@
event.preventDefault(); event.preventDefault();
submit.click(''); submit.click('');
}) })
const priceKgInput = document.getElementById('productPriceKg');
const priceInput = document.getElementById('productPvc');
const weightInput = document.getElementById('productWeight');
priceInput.addEventListener("change", updatePriceKg);
weightInput.addEventListener("change", updatePriceKg);
function updatePriceKg() {
if (weightInput.value != 0) {
$priceKg = 1000 * priceInput.value / weightInput.value;
priceKgInput.value = $priceKg.toFixed(2);
} else {
priceKgInput.value = 0;
}
}
}); });
</script> </script>

View file

@ -1,6 +1,6 @@
@extends('layout') @extends('layout')
@section('title', sprintf('%s - %s', __('models.classes.products'), Str::ucfirst(__('liste')))) @section('title', sprintf('%s - %s', trans_choice('models.classes.products_monitoring', 2), Str::ucfirst(__('liste'))))
@section('description', __('Liste des produits publiés')) @section('description', __('Liste des produits publiés'))
@section('breadcrumb', 'no') @section('breadcrumb', 'no')
@ -9,16 +9,15 @@
<div class="card-header"> <div class="card-header">
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<h1 class="text-center text-md-start h2"> <h1 class="text-md-start h2 text-center">
<span class="fw-bold">{{ __('models.classes.products') }}</span>&nbsp;<span class="text-muted">{{ Str::ucfirst(__('liste')) }}</span> <span class="fw-bold">{{ trans_choice('models.classes.products_monitoring', 2) }}</span>&nbsp;<span
class="text-muted">{{ Str::ucfirst(__('liste')) }}</span>
</h1> </h1>
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<div class="input-group justify-content-end"> <div class="input-group justify-content-end">
<a href="{{ route('products.create') }}" <a class="btn btn-success" data-bs="tooltip" href="{{ route('products.create') }}"
class="btn btn-primary" title="{{ __('Ajouter une :model', ['model' => trans_choice('models.classes.products_monitoring', 1)]) }}">
title="{{ __('Ajouter un :model', ['model' => Str::singular(__('models.classes.products'))]) }}"
data-bs="tooltip">
<i class="fa-solid fa-plus"></i> <i class="fa-solid fa-plus"></i>
</a> </a>
</div> </div>
@ -28,7 +27,7 @@
<div class="card-body"> <div class="card-body">
@include('pages.products.product-filters') @include('pages.products.product-filters')
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-12 col-md-6 p-0"> <div class="col-12 col-md-6 p-3">
{{ $products->withQueryString()->onEachSide(1)->links() }} {{ $products->withQueryString()->onEachSide(1)->links() }}
</div> </div>
</div> </div>
@ -38,11 +37,10 @@
</div> </div>
</div> </div>
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-12 col-md-6 p-0"> <div class="col-12 col-md-6 p-3">
{{ $products->withQueryString()->onEachSide(1)->links() }} {{ $products->withQueryString()->onEachSide(1)->links() }}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
@endsection @endsection

View file

@ -1,115 +1,103 @@
<div class="table-responsive"> <div class="table-responsive">
<table class="table fix-action m-0 mt-3"> <table class="fix-action m-0 mt-3 table">
<thead> <thead>
@php @php
$cols = [ $cols = [
'name' => __('Nom'), 'name' => __('Nom'),
'brand_id' => __('Marque'), 'brand_id' => __('Marque'),
'picture' => __('Image'), 'picture' => __('Image'),
'updated_at' => __('Mis à jour le'), 'multi_pdf' => __('Générer en pdf la sélection'),
'multi_pdf' => __('Multi .pdf'), ];
]; $routeName = request()
$routeName = request()->route()->getName(); ->route()
$rst = !is_null(request()->rst) || !Session::has("$routeName.sorted"); ->getName();
$noOrder = Session::get("$routeName.sort_col") !== 'order' and (Session::has("$routeName.sort_col") or Session::has("$routeName.sort_way")); $rst = !is_null(request()->rst) || !Session::has("$routeName.sorted");
$noOrder = Session::get("$routeName.sort_col") !== '' and (Session::has("$routeName.sort_col") or Session::has("$routeName.sort_way")); ($noOrder = Session::get("$routeName.sort_col") !== 'order') and (Session::has("$routeName.sort_col") or Session::has("$routeName.sort_way"));
@endphp ($noOrder = Session::get("$routeName.sort_col") !== '') and (Session::has("$routeName.sort_col") or Session::has("$routeName.sort_way"));
@include('modules.table-col-sorter', ['cols' => $cols, 'mobileHide' => [], 'ignore' => ['multi_pdf']]) @endphp
@include('modules.table-col-sorter', ['cols' => $cols, 'mobileHide' => [], 'ignore' => ['multi_pdf']])
</thead> </thead>
<tbody> <tbody>
@if ($products->count()) @if ($products->count())
@foreach ($products as $productModel) @foreach ($products as $productModel)
<tr> <tr>
<td class="align-middle text-center"> <td class="text-center align-middle">
<span>{{ $productModel->name }}</span> <span>{{ $productModel->name }}</span>
</td> </td>
<td class="align-middle text-center"> <td class="text-center align-middle">
@foreach($brands as $key => $brand) @foreach ($brands as $key => $brand)
@if ($brand->id === $productModel->brand_id) @if ($brand->id === $productModel->brand_id)
<p class="m-0">{{ $brand->name }}</p> <p class="m-0">{{ $brand->name }}</p>
@endif @endif
@endforeach @endforeach
</td> </td>
<td class="align-middle text-center"> <td class="text-center align-middle">
@if(isset($productModel->picture)) @if ($productModel->picture)
<img class="bo-image-preview" src="{{ asset($productModel->picture) }}"> <img class="bo-image-preview" src="{{ asset(resize($productModel->picture, 'productTiny')) }}">
@else @else
<p class="text-muted">{{ __('Aucune image enregistée') }}</p> <p class="text-muted">{{ __('Aucune image enregistée') }}</p>
@endif @endif
</td> </td>
<td class="align-middle text-center"> {{-- @if (!$noOrder or $rst) --}}
<span class="badge bg-secondary text-wrap"> <td class="text-center align-middle">
{{ $productModel->updated_at->formatLocalized('%d/%m/%Y à %I:%M') }} @php
</span> $data = [
</td> 'id' => 'productSelectMultiPdf',
@if(!$noOrder or $rst) 'model' => $productModel,
<td class="align-middle text-center"> 'printMultiProduct' => $printMultiProduct,
@php ];
$data = [ @endphp
'id' => 'productSelectMultiPdf', <div class="button-multi-pdf" data-json='@json($data)'></div>
'model' => $productModel, </td>
'printMultiProduct' => $printMultiProduct, {{-- @endif --}}
]; <td class="text-end align-middle">
@endphp <form class="d-inline confirmDeleteJS" action="{{ route('products.destroy', $productModel->id) }}"
<div class="button-multi-pdf" data-json='@json($data)'></div> method="POST">
</td> @method('DELETE')
@endif @csrf
<td class="align-middle text-end"> <div class="btn-group" role="group">
<form action="{{ route('products.destroy', $productModel->id) }}" method="POST" class="d-inline confirmDeleteJS"> <a class="btn btn-sm btn-info glightbox" data-gallery="product{{ $productModel->id }}"
@method('DELETE') data-bs-title="{{ __('Visionner l\'image de la :model', ['model' => trans_choice('models.classes.products_monitoring', 1)]) }}"
@csrf data-bs="tooltip" type="button" href="{{ asset($productModel->picture) }}" target="_blank">
<div class="btn-group" role="group"> <i class="fa-solid fa-image"></i>
<a class="btn btn-sm btn-info glightbox" type="button" </a>
href="{{ asset($productModel->picture) }}" target="_blank" <a class="btn btn-sm btn-warning" data-bs="tooltip" type="button"
data-gallery="product{{ $productModel->id }}" href="{{ route('products.download', $productModel) }}"
data-bs-title="{{ __('Visionner l\'image du :model', ['model' => Str::singular(__('models.classes.products'))]) }}" title="{{ __('Télécharger les annexes de la :model', ['model' => trans_choice('models.classes.products_monitoring', 1)]) }}">
data-bs="tooltip"> <i class="fa-solid fa-download"></i>
<i class="fa-solid fa-image"></i> </a>
</a> <a class="btn btn-sm btn-secondary btn-printing" data-bs="tooltip" data-id="{{ $productModel->id }}"
<a class="btn btn-sm btn-warning" type="button" type="button" href="{{ route('products.pdf.show', $productModel) }}"
href="{{ route('products.download', $productModel) }}" title="{{ __('Fichier .pdf de la :model', ['model' => trans_choice('models.classes.products_monitoring', 1)]) }}"
title="{{ __('Télécharger les annexes du :model', ['model' => Str::singular(__('models.classes.products'))]) }}" target="blank">
data-bs="tooltip"> <i class="fa-solid fa-print"></i>
<i class="fa-solid fa-download"></i> </a>
</a> <a class="btn btn-sm btn-success" data-bs="tooltip"
<a class="btn btn-sm btn-secondary btn-printing" type="button" href="{{ route('products.duplicate', $productModel) }}"
href="{{ route('products.pdf.show', $productModel) }}" title="{{ __('Dupliquer cette :model', ['model' => trans_choice('models.classes.products_monitoring', 1)]) }}">
target="blank" <i class="fa-solid fa-copy"></i>
title="{{ __('Fichier .pdf du :model', ['model' => Str::singular(__('models.classes.products'))]) }}" </a>
data-bs="tooltip" <a class="btn btn-sm btn-primary" data-bs="tooltip" href="{{ route('products.edit', $productModel) }}"
data-id="{{ $productModel->id }}"> title="{{ __('Modifier cette :model', ['model' => trans_choice('models.classes.products_monitoring', 1)]) }}">
<i class="fa-solid fa-print"></i> <i class="fa-solid fa-pencil"></i>
</a> </a>
<a class="btn btn-sm btn-success" <button class="btn btn-sm btn-danger" data-bs="tooltip"
href="{{ route('products.duplicate', $productModel) }}" title="{{ __('Supprimer cette :model', ['model' => trans_choice('models.classes.products_monitoring', 1)]) }}">
title="{{ __('Dupliquer ce :model', ['model' => Str::singular(__('models.classes.products'))]) }}" <i class="fa-solid fa-trash"></i>
data-bs="tooltip"> </button>
<i class="fa-solid fa-copy"></i> </div>
</a> </form>
<a class="btn btn-sm btn-primary" </td>
href="{{ route('products.edit', $productModel) }}" </tr>
title="{{ __('Modifier ce :model', ['model' => Str::singular(__('models.classes.products'))]) }}" @endforeach
data-bs="tooltip">
<i class="fa-solid fa-pencil"></i>
</a>
<button class="btn btn-sm btn-danger"
title="{{ __('Supprimer ce :model', ['model' => Str::singular(__('models.classes.products'))]) }}"
data-bs="tooltip">
<i class="fa-solid fa-trash"></i>
</button>
</div>
</form>
</td>
</tr>
@endforeach
@else @else
<tr> <tr>
<td colspan="{{ count($cols)+1 }}" class="text-center"> <td class="text-center" colspan="{{ count($cols) + 1 }}">
<span <span
class="h3">{{ __('Aucun résultat pour les :models', ['models' => __('models.classes.products')]) }} class="h3">{{ __('Aucun résultat pour les :models', ['models' => trans_choice('models.classes.products_monitoring', 2)]) }}
</span> </span>
</td> </td>
</tr> </tr>
@endif @endif
</tbody> </tbody>
</table> </table>

View file

@ -1,10 +1,10 @@
@php @php
$iArgs = []; $iArgs = [];
if(request()->sort_col) { if (request()->sort_col) {
$iArgs['sort_col'] = request()->sort_col; $iArgs['sort_col'] = request()->sort_col;
} }
if(request()->search) { if (request()->search) {
$iArgs['search'] = request()->sort_way; $iArgs['search'] = request()->sort_way;
} }
@endphp @endphp
@ -12,173 +12,181 @@
<div class="col-12"> <div class="col-12">
<div class="card"> <div class="card">
<div class="card-header border-0 p-0"> <div class="card-header border-0 p-0">
<button class="btn border-0 dropdown-toggle w-100" type="button" data-bs-toggle="collapse" data-bs-target="#collapseProductsFilters" aria-expanded="false" aria-controls="collapseExample"> <button class="btn dropdown-toggle w-100 border-0" data-bs-toggle="collapse" data-bs-target="#collapseProductsFilters"
type="button" aria-expanded="false" aria-controls="collapseExample">
{{ __('Filtres') }} {{ __('Filtres') }}
</button> </button>
</div> </div>
<div class="border-top collapse" id="collapseProductsFilters"> <div class="border-top collapse" id="collapseProductsFilters">
<div class="card-body"> <div class="card-body">
<form id="searchForm" action="{{ request()->url() }}"> <form id="searchForm" action="{{ request()->url() }}">
{{-- ALWAYS RESET SEARCH TO PAGE 1 --}}
<input name="page" type="hidden" value="1">
<!-- GENERAL INFORMATIONS --> <!-- GENERAL INFORMATIONS -->
<div class="row w-100 mx-auto"> <div class="row w-100 mx-auto">
<div class="col-12 col-md-6"> <div class="col-12 col-md-6">
<input name="name" type="text" class="form-control" placeholder="Nom" value="{{ $queryParameters['name'] ?? null }}"> <input class="form-control" name="name" type="text" value="{{ request()->get('name') }}"
placeholder="Nom">
</div> </div>
<div class="col-12 col-md-6 mt-1 m-md-0"> <div class="col-12 col-md-6 m-md-0 mt-1">
<select class="form-select" name="brand_id"> <select class="form-select" name="brand_id">
<option value="0">{{ __('Marque') }}</option> <option value="">{{ __('Marque') }}</option>
@foreach($brands as $key => $brand) @foreach ($brands as $key => $brand)
<option @if($queryParameters['brand_id'] ?? null == $brand->id) selected @endif value="{{ $brand->id }}">{{ $brand->name }}</option> <option value="{{ $brand->id }}" @if (request()->get('brand_id') == $brand->id) selected @endif>
{{ $brand->name }}</option>
@endforeach @endforeach
</select> </select>
</div> </div>
<div class="col-12 col-md-6 mt-1"> <div class="col-12 col-md-6 mt-1">
<input name="gencod" type="text" class="form-control" placeholder="GENCODE" value="{{ $queryParameters['gencod'] ?? null }}"> <input class="form-control" name="gencod" type="text" value="{{ request()->get('gencod') }}"
placeholder="GENCOD">
</div> </div>
</div> </div>
<!-- SOURCE --> <!-- SOURCE -->
<div class="row w-100 pt-3 mt-3 mx-auto border-top"> {{-- FIXME: Pourquoi dans le modèle il est Set une source, mais dans la recherche 3 champs complémentaire
peuvent etre utilisés, dans le modele également => il n'est pas nécessaire de cacher les
champs complémentaires dans le formulaire ? --}}
<div class="row w-100 border-top mx-auto mt-3 pt-3">
<div class="col-12 col-md-6"> <div class="col-12 col-md-6">
<select class="form-select" name="source"> <select class="form-select" name="source">
<option value="0">{{ __('Source') }}</option> <option value="">{{ __('Source') }}</option>
@foreach(\App\Enums\Products\SourceEnum::toArray() as $key => $source) @foreach (\App\Enums\Products\SourceEnum::toArray() as $key => $source)
<option @if(intval($queryParameters['source'] ?? 0) === $source->value) selected @endif value="{{ $source->value }}">{{ $source->label }}</option> <option value="{{ $source->value }}" @if (request()->get('source') == $source->value) selected @endif>
{{ $source->label }}</option>
@endforeach @endforeach
</select> </select>
</div> </div>
<div class="col-12 col-md-6 mt-1 m-md-0"> <div class="col-12 col-md-6 m-md-0 mt-1">
<input name="shop" type="text" class="form-control" placeholder="Magasin" value="{{ $queryParameters['shop'] ?? null }}"> <input class="form-control" name="shop" type="text" value="{{ request()->get('shop') }}"
placeholder="{{ __('Magasin') }}">
</div> </div>
<div class="col-12 col-md-6 mt-1"> <div class="col-12 col-md-6 mt-1">
<input name="fair" type="text" class="form-control" placeholder="Salon" value="{{ $queryParameters['fair'] ?? null }}"> <input class="form-control" name="fair" type="text" value="{{ request()->get('fair') }}"
placeholder="{{ __('Salon') }}">
</div> </div>
<div class="col-12 col-md-6 mt-1"> <div class="col-12 col-md-6 mt-1">
<input name="catalogue" type="text" class="form-control" placeholder="Catalogue" value="{{ $queryParameters['catalogue'] ?? null }}"> <input class="form-control" name="catalogue" type="text" value="{{ request()->get('catalogue') }}"
placeholder="{{ __('Catalogue') }}">
</div> </div>
</div> </div>
<!-- CLAIMS --> <!-- CLAIMS -->
<div class="row w-100 pt-3 mt-3 mx-auto border-top"> <div class="row w-100 border-top mx-auto mt-3 pt-3">
<div class="col-12 col-md-6"> <div class="col-12 col-md-6">
@php
$itemsList = collect(\App\Enums\Products\ClaimEnum::toArray())->values();
$values = collect($queryParameters['claims'] ?? [])->values();
$selectedItemList = collect($itemsList)->filter(fn($claim) =>
$values->filter(fn($value) => intval($value['value']) === $claim->value)->count())->values();
@endphp
@include('partials.inputs.select-multiple-enum', [ @include('partials.inputs.select-multiple-enum', [
'id' => 'productsClaim', 'id' => 'productsClaim',
'name' => 'claims', 'name' => 'claims',
'itemsSelected' => $selectedItemList, 'itemsSelected' => collect(request()->get('claims', []))->filter(fn($value) => is_numeric($value)),
'itemsList' => $itemsList, 'itemsList' => collect(\App\Enums\Products\ClaimEnum::toArray())->values(),
'itemLimit' => [0, count($itemsList)], 'itemLimit' => [0, count(\App\Enums\Products\ClaimEnum::toArray())],
'placeholder' => __('Recherchez une Allégation'), 'placeholder' => __('Recherchez une Allégation'),
'simpleComponent' => true, 'simpleComponent' => true,
]) ])
</div> </div>
<div class="col-12 col-md-6 mt-1 m-md-0"> <div class="col-12 col-md-6 m-md-0 mt-1">
<input name="specified_claim" type="text" class="form-control" placeholder="Allégation complémentaire" value="{{ $queryParameters['specified_claim'] ?? null }}"> <input class="form-control" name="specified_claim" type="text"
value="{{ request()->get('specified_claim') }}" placeholder="Allégation complémentaire">
</div> </div>
</div> </div>
<!-- OTHERS --> <!-- OTHERS -->
<div class="row w-100 pt-3 mt-3 mx-auto border-top"> <div class="row w-100 border-top mx-auto mt-3 pt-3">
<div class="col-12 col-md-6"> <div class="col-12 col-md-6">
@php
$itemsList = collect(\App\Enums\Products\OriginEnum::toArray())->values();
$values = collect($queryParameters['origins'] ?? [])->values();
$selectedItemList = collect($itemsList)->filter(fn($origin) =>
$values->filter(fn($value) => intval($value['value']) === $origin->value)->count())->values();
@endphp
@include('partials.inputs.select-multiple-enum', [ @include('partials.inputs.select-multiple-enum', [
'id' => 'productsOrigin', 'id' => 'productsOrigin',
'name' => 'origins', 'name' => 'origins',
'itemsSelected' => $selectedItemList, 'itemsSelected' => collect(request()->get('origins', []))->filter(
'itemsList' => $itemsList, fn($value) => is_numeric($value)),
'itemLimit' => [0, count($itemsList)], 'itemsList' => collect(\App\Enums\Products\OriginEnum::toArray())->values(),
'itemLimit' => [0, count(\App\Enums\Products\OriginEnum::toArray())],
'placeholder' => __('Recherchez une Origine'), 'placeholder' => __('Recherchez une Origine'),
'simpleComponent' => true, 'simpleComponent' => true,
]) ])
</div> </div>
<div class="col-12 col-md-6 mt-1 m-md-0"> <div class="col-12 col-md-6 m-md-0 mt-1">
<select class="form-select" name="certification"> <select class="form-select" name="certification">
<option value="0">{{ __('Certification') }}</option> <option value="">{{ __('Certification') }}</option>
@foreach(\App\Enums\Products\CertificationEnum::toArray() as $key => $certification) @foreach (\App\Enums\Products\CertificationEnum::toArray() as $key => $certification)
<option @if(intval($queryParameters['certification'] ?? 0) == $certification->value) selected @endif value="{{ $certification->value }}">{{ $certification->label }}</option> <option value="{{ $certification->value }}" @if (request()->get('certification') == $certification->value) selected @endif>
{{ $certification->label }}</option>
@endforeach @endforeach
</select> </select>
</div> </div>
<div class="col-12 col-md-6 mt-1"> <div class="col-12 col-md-6 mt-1">
<select class="form-select" name="distribution_network"> <select class="form-select" name="distribution_network">
<option value="0">{{ __('Réseau de distribution') }}</option> <option value="">{{ __('Réseau de distribution') }}</option>
@foreach(\App\Enums\Products\DistributionNetworkEnum::toArray() as $key => $distribution_network) @foreach (\App\Enums\Products\DistributionNetworkEnum::toArray() as $key => $distribution_network)
<option @if(intval($queryParameters['distribution_network'] ?? 0) == $distribution_network->value) selected @endif value="{{ $distribution_network->value }}">{{ $distribution_network->label }}</option> <option value="{{ $distribution_network->value }}"
@if (request()->get('distribution_network') == $distribution_network->value) selected @endif>{{ $distribution_network->label }}</option>
@endforeach @endforeach
</select> </select>
</div> </div>
<div class="col-12 col-md-6 mt-1"> <div class="col-12 col-md-6 mt-1">
<select class="form-select" name="ray"> <select class="form-select" name="ray">
<option value="0">{{ __('Rayons') }}</option> <option value="">{{ __('Rayons') }}</option>
@foreach(\App\Enums\Products\RayEnum::toArray() as $key => $ray) @foreach (\App\Enums\Products\RayEnum::toArray() as $key => $ray)
<option @if(intval($queryParameters['ray'] ?? 0) == $ray->value) selected @endif value="{{ $ray->value }}">{{ $ray->label }}</option> <option value="{{ $ray->value }}" @if (request()->get('ray') == $ray->value) selected @endif>
{{ $ray->label }}</option>
@endforeach @endforeach
</select> </select>
</div> </div>
<div class="col-12 col-md-6 mt-1"> <div class="col-12 col-md-6 mt-1">
<select class="form-select" name="product_range"> <select class="form-select" name="product_range">
<option value="0">{{ __('Famille de produit') }}</option> <option value="">{{ __('Famille de produit') }}</option>
@foreach(\App\Enums\Products\RangeEnum::toArray() as $key => $product_range) @foreach (\App\Enums\Products\RangeEnum::toArray() as $key => $product_range)
<option @if(intval($queryParameters['product_range'] ?? 0) == $product_range->value) selected @endif value="{{ $product_range->value }}">{{ $product_range->label }}</option> <option value="{{ $product_range->value }}" @if (request()->get('product_range') == $product_range->value) selected @endif>
{{ $product_range->label }}</option>
@endforeach @endforeach
</select> </select>
</div> </div>
<div class="col-12 col-md-6 mt-1"> <div class="col-12 col-md-6 mt-1">
<select class="form-select" name="geo_area"> <select class="form-select" name="geo_area">
<option value="0">{{ __('Secteur') }}</option> <option value="">{{ __('Secteur') }}</option>
@foreach(\App\Enums\Products\GeoAreaEnum::toArray() as $key => $geo_area) @foreach (\App\Enums\Products\GeoAreaEnum::toArray() as $key => $geo_area)
<option @if(intval($queryParameters['geo_area'] ?? 0) == $geo_area->value) selected @endif value="{{ $geo_area->value }}">{{ $geo_area->label }}</option> <option value="{{ $geo_area->value }}" @if (request()->get('product_range') == $geo_area->value) selected @endif>
{{ $geo_area->label }}</option>
@endforeach @endforeach
</select> </select>
</div> </div>
</div> </div>
<!-- KEYWORDS --> <!-- KEYWORDS -->
<div class="row w-100 pt-3 mt-3 mx-auto border-top"> <div class="row w-100 border-top mx-auto mt-3 pt-3">
<div class="col-12 col-md-6"> <div class="col-12 col-md-6">
@php @php
$itemsList = collect($keywords)->map(function ($keyword) { $itemsList = collect($keywords)->map(function ($keyword) {
return ["name" => $keyword->keyword_cat->value, "label" => $keyword->label, "value" => $keyword->id]; return ['name' => $keyword->keyword_cat->value, 'label' => $keyword->label, 'value' => $keyword->id];
})->values(); });
$values = collect($queryParameters['keywords'] ?? [])->values();
$selectedItemList = collect($itemsList)->filter(fn($keyword) =>
$values->filter(fn($value) => intval($value['value']) === $keyword['value'])->count())->values();
@endphp @endphp
@include('partials.inputs.select-multiple-enum', [ @include('partials.inputs.select-multiple-enum', [
'id' => 'productsKeywordsn', 'id' => 'productsKeywords',
'name' => 'keywords', 'name' => 'keywords',
'itemsSelected' => $selectedItemList, 'itemsSelected' => collect(request()->get('keywords', []))->filter(
'itemsList' => $itemsList, fn($value) => is_numeric($value)),
'itemLimit' => [0, count($itemsList)], 'itemsList' => $itemsList->values(),
'placeholder' => __('Recherchez un mot clé'), 'itemLimit' => [0, $itemsList->count()],
'placeholder' => __('Recherchez une tendance'),
'simpleComponent' => true, 'simpleComponent' => true,
]) ])
</div> </div>
</div> </div>
<!-- DATES --> <!-- DATES -->
<div class="row w-100 pt-3 mt-3 mx-auto border-top"> <div class="row w-100 border-top mx-auto mt-3 pt-3">
<div class="col-12 col-md-6"> <div class="col-12 col-md-6 m-md-0 mt-1">
<label class="text-muted fst-italic">{{ __('Saisie après le :') }}</label> <label class="text-muted fst-italic">{{ __('Saisie après le :') }}</label>
<input name="date_before" type="date" class="form-control" value="{{ $queryParameters['date_before'] ?? null }}"> <input class="form-control" name="date_after" type="date"
value="{{ request()->query('date_after') }}">
</div> </div>
<div class="col-12 col-md-6 mt-1 m-md-0"> <div class="col-12 col-md-6">
<label class="text-muted fst-italic">{{ __('Saisie avant le :') }}</label> <label class="text-muted fst-italic">{{ __('Saisie avant le :') }}</label>
<input name="date_after" type="date" class="form-control" value="{{ $queryParameters['date_after'] ?? null }}"> <input class="form-control" name="date_before" type="date"
value="{{ request()->query('date_before') }}">
</div> </div>
</div> </div>
<!-- BUTTONS --> <!-- BUTTONS -->
<div class="d-flex justify-content-center input-group pt-3 mt-3 border-top"> <div class="d-flex justify-content-center input-group border-top mt-3 pt-3">
<button type="submit" class="btn btn-primary" data-bs="tooltip" title="{{ __('Lancer la recherche') }}"> <button class="btn btn-primary" data-bs="tooltip" type="submit" title="{{ __('Lancer la recherche') }}">
{{ __('Rechercher') }} {{ __('Rechercher') }}
</button> </button>
<a class="p-0" href="{{ request()->url() }}"> <a class="p-0" href="{{ request()->url() }}">
<button type="button" data-bs="tooltip" class="btn btn-danger rounded-0 rounded-end" <button class="btn btn-danger rounded-0 rounded-end" data-bs="tooltip" type="button"
title="{{ __('Effacer la recherche') }}"> title="{{ __('Effacer la recherche') }}">
<i class="fas fa-backspace"></i> <i class="fas fa-backspace"></i>
</button> </button>

View file

@ -0,0 +1,18 @@
@php
$value = old($name) ?? $model->{$name} ?? null;
$value = $value ? intval($value) : $value;
$data = [
'name' => $name,
'value' => $value,
'labelField' => $labelField,
'itemLimit' => $itemLimit,
'itemsList' => $itemsList,
'storeRoute' => $storeRoute ?? null,
'placeholder' => $placeholder,
'errors' => $errors->getBag('default')->getMessages(),
'label' => $label,
'require' => $require
];
@endphp
<div class="belongs-to-dropdown" data-json='@json($data)'></div>
@include('modules.input-error', ['helper' => $helper, 'inputName' => $name])

View file

@ -2,8 +2,10 @@
<label for="{{ $name }}" class="col-form-label"><b>{{ $label }} *</b></label> <label for="{{ $name }}" class="col-form-label"><b>{{ $label }} *</b></label>
@endif @endif
<select id="{{ $name }}" class="form-select" name="{{ $name }}" aria-describedby="{{ $id }}Help" @if(!empty($label))required @endif> <select id="{{ $name }}" class="form-select" name="{{ $name }}" aria-describedby="{{ $id }}Help" @if(!empty($label))required @endif>
<option value="0" @if (!old($name, $model->{$name}->id ?? $value ?? null)) selected @endif>
</option>
@foreach ($associatedModels as $associatedModel) @foreach ($associatedModels as $associatedModel)
<option value="{{ $associatedModel->id }}" @if (old($name, $model->{$name} ?? 1) == $associatedModel->id) selected @endif> <option value="{{ $associatedModel->id }}" @if (old($name, $model->{$name} ?? $value ?? null) == $associatedModel->id) selected @endif>
{{ $associatedModel->name }} {{ $associatedModel->name }}
</option> </option>
@endforeach @endforeach

View file

@ -7,10 +7,14 @@
id="{{ $id }}" id="{{ $id }}"
name="{{ $name }}" name="{{ $name }}"
type="number" type="number"
value="{{ old($name, $model->{$name} ?? 0) }}" value="{{ old($name, $model->{$name} ?? $value ?? null) }}"
class="form-control" class="form-control"
aria-describedby="{{ $id }}Help" aria-describedby="{{ $id }}Help"
@if(!empty($step))
step="{{ $step }}"
@else
step="1" step="1"
@endif
@if(!empty($max)) @if(!empty($max))
max="{{ $max }}" max="{{ $max }}"
@else @else
@ -22,6 +26,8 @@
min="{{ !empty($unsigned) ? '0' : '-2147483648' }}" min="{{ !empty($unsigned) ? '0' : '-2147483648' }}"
@endif @endif
@if(!empty($required))required @endif @if(!empty($required))required @endif
@if(!empty($disabled))disabled @endif
@if(!empty($readonly))readonly @endif
> >
@include('modules.input-error', [ @include('modules.input-error', [
'inputName' => $name, 'inputName' => $name,

View file

@ -2,8 +2,10 @@
<label for="{{ $name }}" class="col-form-label"><b>{{ $label }}@if(!empty($required)) * @endif</b></label> <label for="{{ $name }}" class="col-form-label"><b>{{ $label }}@if(!empty($required)) * @endif</b></label>
@endif @endif
<select id="{{ $name }}" class="form-select" name="{{ $name }}" aria-describedby="{{ $id }}Help" @if(!empty($require))required @endif> <select id="{{ $name }}" class="form-select" name="{{ $name }}" aria-describedby="{{ $id }}Help" @if(!empty($require))required @endif>
<option value="0" @if (!old($name, $model->{$name}->value ?? $value ?? null)) selected @endif>
</option>
@foreach ($associatedModels as $associatedModel) @foreach ($associatedModels as $associatedModel)
<option value="{{ $associatedModel->value}}" @if (old($name, $model->{$name}->value ?? $value ?? 1) == $associatedModel->value) selected @endif> <option value="{{ $associatedModel->value}}" @if (old($name, $model->{$name}->value ?? $value ?? null) == $associatedModel->value) selected @endif>
{{ $associatedModel->label }} {{ $associatedModel->label }}
</option> </option>
@endforeach @endforeach

View file

@ -1,5 +1,6 @@
<?php <?php
use App\Http\Controllers\BrandController;
use App\Http\Controllers\DashboardController; use App\Http\Controllers\DashboardController;
use App\Http\Controllers\ProductController; use App\Http\Controllers\ProductController;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
@ -9,6 +10,10 @@ Route::get('/', [DashboardController::class, 'show'])->name('dashboard');
// * PRODUCTS // * PRODUCTS
Route::resource('products', ProductController::class)->except(['show']); Route::resource('products', ProductController::class)->except(['show']);
// * BRANDS
Route::resource('brands', BrandController::class)->only(['store']);
Route::prefix('/product') Route::prefix('/product')
->name('products.') ->name('products.')
->group(function () { ->group(function () {

View file

@ -1,11 +1,11 @@
import legacy from "@vitejs/plugin-legacy";
import vue from "@vitejs/plugin-vue";
import laravel from "laravel-vite-plugin";
import path from "path";
import { defineConfig } from "vite"; import { defineConfig } from "vite";
import checker from "vite-plugin-checker"; import checker from "vite-plugin-checker";
import eslint from "vite-plugin-eslint"; import eslint from "vite-plugin-eslint";
import laravel from "laravel-vite-plugin";
import legacy from "@vitejs/plugin-legacy";
import path from "path";
import stylelint from "vite-plugin-stylelint"; import stylelint from "vite-plugin-stylelint";
import vue from "@vitejs/plugin-vue";
export default defineConfig({ export default defineConfig({
build: { build: {
@ -41,6 +41,7 @@ export default defineConfig({
}, },
plugins: [ plugins: [
checker({ checker({
enableBuild: true,
vueTsc: { vueTsc: {
root: ".", root: ".",
tsconfigPath: "tsconfig.json", tsconfigPath: "tsconfig.json",
@ -74,7 +75,7 @@ export default defineConfig({
"**/vendor/**/*.scss", "**/vendor/**/*.scss",
"**/vendor/**/*.css", "**/vendor/**/*.css",
], ],
lintOnStart: true, lintOnStart: false,
emitError: true, emitError: true,
emitWarning: true, emitWarning: true,
emitErrorAsWarning: false, emitErrorAsWarning: false,
@ -84,7 +85,7 @@ export default defineConfig({
cache: false, cache: false,
extensions: ["js", "ts"], extensions: ["js", "ts"],
include: "**/*.ts", include: "**/*.ts",
lintOnStart: true, lintOnStart: false,
emitWarning: true, emitWarning: true,
emitError: true, emitError: true,
failOnWarning: false, failOnWarning: false,