Compare commits

...
Sign in to create a new pull request.

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
95 changed files with 4926 additions and 1514 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.
*
* @param \Illuminate\Console\Scheduling\Schedule $schedule
* @return void
*/
protected function schedule(Schedule $schedule): void
{
// $schedule->command('inspire')->hourly();
}
/**
* Register the commands for the application.
*
* @return 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

@ -12,8 +12,8 @@ enum CertificationEnum : int
case labelRouge = 2;
case certifiedCQ = 3;
case standard = 4;
case freeRange = 5;
case others = 6;
case freeRange = 6;
case others = 5;
/**
* Custom added functions definition

View file

@ -18,9 +18,9 @@ enum RangeEnum : int
case rawMeat = 8;
case cookedMeats = 9;
case farcis = 10;
case plant = 11;
case frozenSpecific = 12;
case others = 13;
case plant = 12;
case frozenSpecific = 13;
case others = 11;
/**
* Custom added functions definition

View file

@ -13,10 +13,10 @@ enum RayEnum : int
case freshlyPackaged = 3;
case roastery = 4;
case caterer = 5;
case creamery = 6;
case groceryStore = 7;
case frozen = 8;
case others = 9;
case creamery = 7;
case groceryStore = 8;
case frozen = 9;
case others = 10;
/**
* Custom added functions definition

View file

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

View file

@ -12,16 +12,14 @@ class Handler extends ExceptionHandler
*
* @var array<class-string<\Throwable>, \Psr\Log\LogLevel::*>
*/
protected $levels = [
];
protected $levels = [];
/**
* A list of the exception types that are not reported.
*
* @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.
@ -36,6 +34,8 @@ class Handler extends ExceptionHandler
/**
* Register the exception handling callbacks for the application.
*
* @return 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;
use App\Enums\Pagination\ItemsPerPaginationEnum;
use App\Lib\Helpers\ToolboxHelper;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Str;
class Controller extends BaseController
{
@ -122,4 +127,26 @@ class Controller extends BaseController
}
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;
use App\Http\Controllers\Controller;
use App\Http\Requests\Products\IndexFilterRequest;
use App\Http\Requests\Products\StoreProductRequest;
use App\Http\Requests\Products\UpdateProductRequest;
use App\Jobs\ExportProduct;
@ -12,6 +13,7 @@ use App\Models\Product;
use App\Traits\Controllers\HasDocuments;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
@ -22,99 +24,54 @@ class ProductController extends Controller
/**
* Display a listing of the resource.
*
* @param \Illuminate\Http\Request $request
* @param \App\Http\Requests\Products\IndexFilterRequest $request
* @return \Illuminate\Contracts\View\View
*/
public function index(Request $request)
public function index(IndexFilterRequest $request)
{
/** @var \Illuminate\Database\Eloquent\Builder $brands */
$brands = Brand::get();
$brandsQuery = Brand::query();
/** @var \Illuminate\Database\Eloquent\Builder $products */
$products = Product::query();
/** @var \Illuminate\Database\Eloquent\Builder $productsQuery */
$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 */
$printMultiProduct = session('printMultiProduct') ?? [];
/** @var \Illuminate\Database\Eloquent\Builder $brands */
$brands = Brand::get();
$brands = $brandsQuery->get();
/** @var \Illuminate\Database\Eloquent\Builder $keyword */
$keywords = Keyword::where('keyword_cat', 'LIKE', 'product_%')->get();
$keywords = Keyword::query()->where('keyword_cat', 'LIKE', 'product_%')->get();
return view('pages.products.index', [
'products' => $products,
'brands' => $brands,
'keywords' => $keywords,
'printMultiProduct' => $printMultiProduct,
'queryParameters' => $request->all(),
'printMultiProduct' => $printMultiProduct
]);
}
/**
* 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.
*
* @param App\Models\Product $productModel
* @param Illuminate\Support\Collection $commentModel
* @return \Illuminate\Contracts\View\View
* 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
$brands = Brand::get();
$productsFair = Product::pluck('fair');
$productsShop = Product::pluck('shop');
$productsCatalog = Product::pluck('catalogue');
$keywords = Keyword::where('keyword_cat', 'product_pleasure')
$brands = Brand::all();
$productsFair = Product::query()->pluck('fair');
$productsShop = Product::query()->pluck('shop');
$productsCatalog = Product::query()->pluck('catalogue');
$keywords = Keyword::query()->where('keyword_cat', 'product_pleasure')
->orWhere('keyword_cat', 'product_shape')
->orWhere('keyword_cat', 'product_practicality')
->orWhere('keyword_cat', 'product_health')
@ -127,7 +84,8 @@ class ProductController extends Controller
'productsFair',
'productsShop',
'productsCatalog',
'keywords'
'keywords',
'commentModel'
));
}
@ -170,12 +128,12 @@ class ProductController extends Controller
*/
public function edit(Product $product)
{
$brands = Brand::get();
$productsFair = Product::pluck('fair');
$productsShop = Product::pluck('shop');
$productsCatalog = Product::pluck('catalogue');
$brands = Brand::all();
$productsFair = Product::query()->pluck('fair');
$productsShop = Product::query()->pluck('shop');
$productsCatalog = Product::query()->pluck('catalogue');
$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_practicality')
->orWhere('keyword_cat', 'product_health')
@ -300,7 +258,11 @@ class ProductController extends Controller
*/
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)
{
if (
isset($request->GENCODE) &&
$request->GENCODE !== null &&
ctype_digit($request->GENCODE) &&
strlen($request->GENCODE) > 0
isset($request->GENCOD) &&
$request->GENCOD !== null &&
ctype_digit($request->GENCOD) &&
strlen($request->GENCOD) > 0
) {
if (
isset($request->ID) &&
$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;
} else {
$result = in_array(intval($request->GENCODE), Product::pluck('gencod')->toArray()) ?
Product::orderBy('created_at', 'desc')->where('gencod', intval($request->GENCODE))->first() : null;
$result = in_array(intval($request->GENCOD), Product::pluck('gencod')->toArray()) ?
Product::orderBy('created_at', 'desc')->where('gencod', intval($request->GENCOD))->first() : null;
}
} else {
$result = null;
@ -363,6 +325,7 @@ class ProductController extends Controller
*/
public function printMultiProductSheet()
{
// FIXME: This should not use session.
/** @var array printMultiProduct */
$printMultiProduct = session('printMultiProduct') ?? [];
asort($printMultiProduct);
@ -427,4 +390,62 @@ class ProductController extends Controller
{
\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>
*/
protected $middleware = [
// \App\Http\Middleware\TrustHosts::class,
\App\Http\Middleware\TrustHosts::class,
\App\Http\Middleware\TrustProxies::class,
\Illuminate\Http\Middleware\HandleCors::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.
*
* @param \Illuminate\Http\Request $request
* @return string|null
*/
protected function redirectTo(Request $request): ?string
{

View file

@ -3,7 +3,6 @@
namespace App\Http\Middleware;
use App\Providers\RouteServiceProvider;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;
@ -13,10 +12,15 @@ class RedirectIfAuthenticated
/**
* Handle an incoming request.
*
* @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;
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.
*
* @var array<int, string>
*
* 'fbclid',
* 'utm_campaign',
* 'utm_content',
* 'utm_medium',
* 'utm_source',
* 'utm_term',
*/
protected $except = [
// 'fbclid',
// 'utm_campaign',
// 'utm_content',
// 'utm_medium',
// 'utm_source',
// 'utm_term',
];
protected $except = [];
}

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\SourceEnum;
use App\Lib\Helpers\ToolboxHelper;
use App\Traits\Requests\HasDocument;
use App\Traits\Requests\HasDocumentAndPictureList;
use App\Traits\Requests\HasPicture;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rules\Enum;
@ -20,7 +20,7 @@ use Illuminate\Validation\ValidationException;
class StoreProductRequest extends FormRequest
{
use HasPicture;
use HasDocument;
use HasDocumentAndPictureList;
/**
* 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,
'origins' => $this->origins ? $this->origins : [],
'claims' => $this->claims ? $this->claims : [],
'picture_alt' => $this->name,
'picture_title' => $this->name,
'certification' => $this->certification == 0 ? null : $this->certification,
]);
$this->mergePicture('picture');
$this->mergeDocuments('docs');
@ -60,9 +63,9 @@ class StoreProductRequest extends FormRequest
'name' => 'required|string|min:3|max:255',
'brand_id' => 'required|exists:brands,id',
'source' => [new Enum(SourceEnum::class)],
'fair' => 'nullable|string|min:3|max:255|required_if:source,' . SourceEnum::showConsumer->value . ',' . SourceEnum::showCustomer->value . ',' . SourceEnum::showTrade->value,
'shop' => 'nullable|string|min:3|max:255|required_if:source,' . SourceEnum::magazin->value,
'catalogue' => 'nullable|string|min:3|max:255|required_if:source,' . SourceEnum::catalogArgumentary->value,
'fair' => 'nullable|string|min:3|max:255',
'shop' => 'nullable|string|min:3|max:255',
'catalogue' => 'nullable|string|min:3|max:255',
'gencod' => 'nullable|numeric|min:0',
'picture_alt' => '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)],
'origins' => 'nullable|array',
'origins.*' => 'present|array',
'origins.*.id' => [new Enum(OriginEnum::class)],
'claims' => 'nullable|array',
'claims.*' => 'present|array',
'claims.*.id' => [new Enum(ClaimEnum::class)],
'origins' => 'present|array',
'origins.*' => [new Enum(OriginEnum::class)],
'claims' => 'present|array',
'claims.*' => [new Enum(ClaimEnum::class)],
'specified_claim' => 'string|min:3|max:255|required_if:claims,' . ClaimEnum::others->value,
'distribution_network' => ['required', new Enum(DistributionNetworkEnum::class)],
'ray' => ['required', new Enum(RayEnum::class)],
'product_range' => ['required', new Enum(RangeEnum::class)],
'geo_area' => ['required', new Enum(GeoAreaEnum::class)],
'weight' => 'nullable|integer',
'pvc' => 'nullable|integer',
'pvc' => 'nullable|decimal:0,2',
'comment' => 'nullable|string|min:15',
'is_tasted' => 'required|boolean',
'tasters_number' => 'nullable|integer',
@ -106,7 +107,7 @@ class StoreProductRequest extends FormRequest
return \array_merge(
$rules,
$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;
use App\Enums\Products\CertificationEnum;
use App\Enums\Products\ClaimEnum;
use App\Enums\Products\OriginEnum;
use App\Models\Brand;
use App\Models\Keyword;
use App\Models\Product;
use App\Models\User;
use Carbon\Carbon;
use App\Models\ProductComment;
use Illuminate\Http\Resources\Json\JsonResource;
class ProductResource extends JsonResource
@ -19,10 +17,10 @@ class ProductResource extends JsonResource
/** @var array */
private static $skeleton = [
'id' => null,
'user_id' => null,
'brand_id' => null,
'user' => null,
'brand' => null,
'name' => null,
'saw_at' => null,
'seen_at' => null,
'source' => null,
'fair' => null,
'shop' => null,
@ -36,6 +34,7 @@ class ProductResource extends JsonResource
'certification' => null,
'origins' => null,
'claims' => null,
'specified_claim' => null,
'distribution_network' => null,
'ray' => null,
'product_range' => null,
@ -65,54 +64,13 @@ class ProductResource extends JsonResource
*/
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(
[
'id' => $this->product->id,
'user_id' => User::where('id', $this->product->user_id)->firstOrFail(),
'brand_id' => Brand::where('id', $this->product->brand_id)->firstOrFail(),
'user' => (object)$this->product->user->toArray(),
'brand' => (object)$this->product->brand->toArray(),
'name' => $this->product->name,
'saw_at' => Carbon::createFromFormat('Y-m-d H:i:s', $this->product->saw_at)->format('d/m/Y'),
'seen_at' => $this->product->seen_at->isoFormat('DD\/MM\/YYYY'),
'source' => $this->product->source->label(),
'fair' => $this->product->fair,
'shop' => $this->product->shop,
@ -123,16 +81,25 @@ class ProductResource extends JsonResource
'picture' => $this->product->picture,
'picture_alt' => $this->product->picture_alt,
'picture_title' => $this->product->picture_title,
'certification' => $certification,
'origins' => $origins ?? null,
'claims' => $claims ?? null,
'certification' => \optional($this->product->certification)->label(),
'origins' => $this->product->origins_enum->map(fn (OriginEnum $enum) => $enum->label())->implode(', '),
'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(),
'ray' => $this->product->ray->label(),
'product_range' => $this->product->product_range->label(),
'geo_area' => $this->product->geo_area->label(),
'freeComment' => $this->product->comment,
'productComments' => $this->product->productComments,
'productKeywords' => $productKeywords ?? null,
// FIXME: Use dedicaced resources for relations.
'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()
);

View file

@ -22,6 +22,7 @@ class ExportProduct implements ShouldQueue
/**
* Execute the job.
*
* @param integer $productId
* @return void
* @throws \Error If file storage config is wrong.
* @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.");
}
$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)) {
throw new \RuntimeException("The folder cannot be created inside the ZIP.");

View file

@ -2,13 +2,12 @@
namespace App\Lib\Helpers;
use Illuminate\Database\Eloquent\Model;
use App\Enums\Pagination\ItemsPerPaginationEnum;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\Enum;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
@ -140,75 +139,15 @@ class ToolboxHelper
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
* @param mixed $search
* @param mixed $replace
* @param string|array $search
* @param string|array $replace
* @param mixed $subject
* @param integer $count
* @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)) {
return false;
@ -253,7 +192,7 @@ class ToolboxHelper
public static function assertFieldIsUnique(
string $table,
string $field,
$value,
mixed $value,
?int $id = null,
?callable $query = null
) {
@ -288,4 +227,22 @@ class ToolboxHelper
{
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 string $brand
* @property string $name
* @property string $filter
* @property string $created_at
* @property string $updated_at

View file

@ -8,10 +8,13 @@ use Illuminate\Database\Eloquent\Model;
/**
* @property integer $id
* @property string $label
* @property string $keyword_cat
* @property string $created_at
* @property string $updated_at
* @property Keywordable[] $keywordables
* @property \App\Enums\KeywordCategoryEnum $keyword_cat
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
*
* * 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
{
@ -34,12 +37,14 @@ class Keyword extends Model
'keyword_cat' => KeywordCategoryEnum::class
];
// * RELATIONSHIPS
/**
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function keywordables()
{
return $this->hasMany('App\Models\Keywordable');
return $this->hasMany(Keywordable::class);
}
/**

View file

@ -3,8 +3,10 @@
namespace App\Models;
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;
@ -13,43 +15,53 @@ use App\Lib\Helpers\FileStorageHelper;
use App\Lib\Helpers\ToolboxHelper;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Storage;
use Mccarlosen\LaravelMpdf\LaravelMpdf;
use Symfony\Component\HttpFoundation\StreamedResponse;
/**
* FIXME: using migration, do not set nullable props without a real need.
*
* @property integer $id
* @property integer $user_id
* @property integer $brand_id
* @property string $name
* @property string $brand
* @property string $saw_at
* @property enum $source
* @property string $fair
* @property string $shop
* @property string $catalogue
* @property string $gencod
* @property string $picture
* @property array $docs
* @property string $url
* @property enum $certification
* @property object $origins
* @property object $claims
* @property string $specified_claim
* @property enum $distribution_network
* @property enum $ray
* @property enum $product_range
* @property enum $geo area
* @property integer $weight
* @property integer $pvc
* @property string $comment
* @property \Carbon\Carbon $seen_at
* @property \App\Enums\Products\SourceEnum $source
* @property string|null $fair
* @property string|null $shop
* @property string|null $catalogue
* @property string|null $gencod
* @property string|null $picture
* @property string $picture_alt
* @property string $picture_title
* @property array|null $docs
* @property string|null $url
* @property \App\Enums\Products\CertificationEnum|null $certification
* @property array<integer>|null $origins
* @property \Illuminate\Support\Collection<\App\Enums\Products\OriginEnum> $origins_enum
* @property array<integer>|null $claims
* @property \Illuminate\Support\Collection<\App\Enums\Products\OriginEnum> $claims_enum
* @property string|null $specified_claim
* @property \App\Enums\Products\DistributionNetworkEnum $distribution_network
* @property \App\Enums\Products\RayEnum $ray
* @property \App\Enums\Products\RangeEnum $product_range
* @property \App\Enums\Products\GeoAreaEnum $geo area
* @property integer|null $weight
* @property integer|null $pvc
* @property string|null $comment
* @property boolean $is_tasted
* @property integer $tasters_number
* @property string $created_at
* @property string $updated_at
* @property string $deleted_at
* @property ProductComment[] $productComments
* @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
{
@ -62,8 +74,9 @@ class Product extends Model
'user_id',
'brand_id',
'name',
// ? FIXME: ?why is there brand here, column is not in DB.
'brand',
'saw_at',
'seen_at',
'source',
'fair',
'shop',
@ -108,10 +121,10 @@ class Product extends Model
'product_range' => RangeEnum::class,
'geo_area' => GeoAreaEnum::class,
'is_tasted' => 'boolean',
'saw_at' => 'datetime:Y-m-d',
'created_at' => 'datetime:Y-m-d',
'updated_at' => 'datetime:Y-m-d',
'deleted_at' => 'datetime:Y-m-d',
'seen_at' => 'datetime',
'created_at' => 'datetime',
'updated_at' => 'datetime',
'deleted_at' => 'datetime',
];
// * EVENTS
@ -131,7 +144,7 @@ class Product extends Model
$model->user_id = 1;
});
static::updating(function (self $model) {
self::deleteOldDocuments($model);
self::deleteOldDocuments($model, true);
$model->picture = FileStorageHelper::storeFile($model, $model->picture, true);
self::updateSource($model);
self::updateSpecifiedClaim($model);
@ -170,8 +183,11 @@ class Product extends Model
*/
private static function updateSpecifiedClaim(Product $model)
{
foreach ($model->claims as $claim) {
$check = (intval($claim['id']) === \App\Enums\Products\ClaimEnum::others->value) ? true : false;
$check = 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;
}
@ -208,14 +224,22 @@ class Product extends Model
* Delete old documents.
*
* @param self $model
* @param boolean $onlyIfUpdated
* @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), '/');
if (\strpos($originalPath, 'storage') !== 0) {
return;
continue;
}
$originalPath = ToolboxHelper::mbReplace('storage/', '', $originalPath);
if (Storage::disk('public')->exists($originalPath)) {
@ -282,26 +306,27 @@ class Product extends Model
/**
* Generate a pdf with multiple products.
*
* @param array $printMultiProduct
* @param array<integer> $printMultiProduct
* @param string $title
* @return \Mccarlosen\LaravelMpdf\LaravelMpdf
*/
public static function generateMultipleProductPdf(array $printMultiProduct, string $title)
{
/** @var \Mccarlosen\LaravelMpdf\LaravelMpdf */
/** @var \Mccarlosen\LaravelMpdf\LaravelMpdf|null */
$pdf = null;
// Loop all views.
// * Loop all Model ids.
foreach ($printMultiProduct as $productId) {
$product = Product::where('id', $productId)->firstOrFail();
// If not yet defined, set the first page.
/** @var \App\Models\Product */
$product = Product::query()->where('id', $productId)->firstOrFail();
// * If not yet defined, set the first page.
if (is_null($pdf)) {
$pdf = Product::generateProductPdf($product, $title);
} 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()->WriteHTML((string)view(
'documents.product-body',
['product' => \json_decode(\json_encode(new ProductResource($product)))],
['product' => (object)(new ProductResource($product))->toArray()],
));
}
}
@ -339,14 +364,64 @@ class Product extends Model
])->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
*/
public function productComments()
{
return $this->hasMany('App\Models\ProductComment');
return $this->hasMany(ProductComment::class);
}
/**
@ -354,7 +429,7 @@ class Product extends Model
*/
public function user()
{
return $this->belongsTo('App\Models\User');
return $this->belongsTo(User::class);
}
/**
@ -362,7 +437,7 @@ class Product extends Model
*/
public function brand()
{
return $this->belongsTo('App\Models\Brand');
return $this->belongsTo(Brand::class);
}
/**

View file

@ -9,13 +9,15 @@ use Illuminate\Database\Eloquent\Model;
* @property integer $id
* @property integer $product_id
* @property string $evaluation
* @property string $aspect
* @property string $taste
* @property string $odor
* @property string $texture
* @property string $created_at
* @property string $updated_at
* @property Product $product
* @property string|null $aspect
* @property string|null $taste
* @property string|null $odor
* @property string|null $texture
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
*
* * RELATIONSHIPS
* @property-read \App\Models\Product $product
*/
class ProductComment extends Model
{
@ -35,11 +37,23 @@ class ProductComment extends Model
'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
*/
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.
*
* @return void
*/
public function boot(): void
{

View file

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

View file

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

View file

@ -17,7 +17,6 @@ trait HasDocuments
*/
private function storeDocuments(Request $request, Model $model): void
{
if (is_array($request->docs)) {
$model->docs = [];
$model->docs = collect($request->docs)
->map(function ($document) use ($model) {
@ -25,4 +24,3 @@ trait HasDocuments
})->all();
}
}
}

View file

@ -4,24 +4,43 @@ namespace App\Traits\Requests;
use Illuminate\Support\Facades\File;
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
*
* @param string $name
* @param boolean $required
* @return array
*/
public function documentsRules(
string $name = 'documents',
?bool $required = null
string $name = 'docs',
): array {
return [
"{$name}" => ($required ? 'required' : 'nullable') . '|array',
"{$name}" => 'present|array',
"{$name}.*" => [
$required ? 'required' : 'nullable',
'required',
function (string $attribute, $value, callable $fail) {
return $this->validateDocument($attribute, $value, $fail);
}
@ -38,32 +57,36 @@ trait HasDocument
* @return void
* @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)) {
$fail(trans(":attribute n'existe pas dans le système de fichier"));
return;
}
if (
!\in_array($this->getUploadedFileMimeType($value, $useStorage), [
'application/pdf'
])
) {
$fail(trans(":attribute doit être une image de type 'application/pdf'"));
if (!\in_array($this->getUploadedFileMimeType($value, $useStorage), $this->autorisedMimTypes)) {
$fail(trans(':attribute doit être un document de type :mimetypes', [
'mimetypes' => Str::of($this->autorisedMimTypes)->implode(' ou ')
]));
}
$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
* @return void
*/
protected function mergeDocuments(string $name = 'documents'): void
protected function mergeDocuments(string $name = 'docs'): void
{
$documents = $this->{$name};
if (is_array($documents)) {
$this->merge([$name => $this->{$name} ?? []]);
$this->merge(["{$name}Validate" => []]);
if (is_array($documents)) {
foreach ($documents as $key => $documentPath) {
// ! Empty document.
if ($documentPath === 'false') {

View file

@ -123,7 +123,7 @@ trait HasPicture
* @return void
* @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)) {
$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'"));
}
$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])) {
$fail(trans(":attribute impossible de valider les dimensions"));
} else {
@ -239,7 +244,7 @@ trait HasPicture
*/
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",
"diglactic/laravel-breadcrumbs": "^8.0",
"guzzlehttp/guzzle": "^7.2",
"kwaadpepper/image-resizer": "^2.1",
"laravel/framework": "^10.0",
"laravel/tinker": "^2.8"
},
@ -19,6 +20,7 @@
"fakerphp/faker": "^1.9.1",
"kitloong/laravel-migrations-generator": "^6.8",
"krlove/eloquent-model-generator": "dev-l10-compatibility",
"laravel-lang/common": "^4.0",
"laravel/pint": "^1.0",
"laravel/sail": "^1.18",
"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' => [
'driver' => 'file',
// 'store' => 'redis',
// 'store' 'redis'
],
/*
@ -159,6 +159,7 @@ return [
/*
* Laravel Framework Service Providers...
*/
Illuminate\Auth\AuthServiceProvider::class,
Illuminate\Broadcasting\BroadcastServiceProvider::class,
Illuminate\Bus\BusServiceProvider::class,
@ -185,11 +186,13 @@ return [
/*
* Package Service Providers...
*/
Krlove\EloquentModelGenerator\Provider\GeneratorServiceProvider::class,
/*
* Application Service Providers...
*/
App\Providers\AppServiceProvider::class,
App\Providers\AuthServiceProvider::class,
App\Providers\EventServiceProvider::class,

View file

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

View file

@ -42,9 +42,8 @@ return [
'encrypted' => true,
'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' => [

View file

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

View file

@ -63,6 +63,18 @@ return [
]) : [],
],
'oracle' => [
'driver' => 'oracle',
'host' => env('DB_OCI_HOST', ''),
'port' => env('DB_OCI_PORT', '1521'),
'database' => env('DB_OCI_DATABASE', ''),
'username' => env('DB_OCI_USERNAME', ''),
'password' => env('DB_OCI_PASSWORD', ''),
'charset' => env('DB_OCI_CHARSET', 'AL32UTF8'),
'prefix' => env('DB_OCI_PREFIX', ''),
'prefix_schema' => env('DB_OCI_SCHEMA_PREFIX', ''),
],
'pgsql' => [
'driver' => 'pgsql',
'url' => env('DATABASE_URL'),
@ -89,8 +101,8 @@ return [
'charset' => 'utf8',
'prefix' => '',
'prefix_indexes' => true,
// 'encrypt' => env('DB_ENCRYPT', 'yes'),
// 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'),
'encrypt' => env('DB_ENCRYPT', 'yes'),
'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' => [
'transport' => 'mailgun',
// 'client' => [
// 'timeout' => 5,
// ],
'client' => [
'timeout' => 5,
],
],
'postmark' => [
'transport' => 'postmark',
// 'client' => [
// 'timeout' => 5,
// ],
'client' => [
'timeout' => 5,
],
],
'sendmail' => [

View file

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

View file

@ -35,8 +35,14 @@ final class ProductFactory extends Factory
public function definition(): array
{
$source = \collect(SourceEnum::toArray())->random()->value;
$fair = $shop = $catalogue = null;
if ($source === SourceEnum::showConsumer->value || $source === SourceEnum::showCustomer->value || $source === SourceEnum::showTrade->value) {
$catalogue = null;
$fair = null;
$shop = null;
if (
$source === SourceEnum::showConsumer->value ||
$source === SourceEnum::showCustomer->value ||
$source === SourceEnum::showTrade->value
) {
$fair = fake()->word;
} elseif ($source === SourceEnum::magazin->value) {
$shop = fake()->word;
@ -50,7 +56,7 @@ final class ProductFactory extends Factory
'user_id' => fake()->randomElement(User::pluck('id')),
'brand_id' => fake()->randomElement(Brand::pluck('id')),
'name' => fake()->unique()->word . " " . fake()->unique()->word,
'saw_at' => fake()->dateTime(),
'seen_at' => fake()->dateTime(),
'source' => $source,
'fair' => $fair,
'shop' => $shop,
@ -61,8 +67,8 @@ final class ProductFactory extends Factory
'docs' => [],
'url' => fake()->url,
'certification' => \collect(CertificationEnum::toArray())->random()->value,
'origins' => [["id" => strval(\collect(OriginEnum::toArray())->random()->value)]],
'claims' => [["id" => strval($claim)]],
'origins' => [\collect(OriginEnum::toArray())->random()->value],
'claims' => [$claim],
'specified_claim' => ($claim == OriginEnum::others->value) ? fake()->text : null,
'distribution_network' => \collect(DistributionNetworkEnum::toArray())->random()->value,
'ray' => \collect(RayEnum::toArray())->random()->value,

View file

@ -19,7 +19,7 @@ return new class extends Migration
$table->bigInteger('user_id')->index();
$table->bigInteger('brand_id')->index()->comment('Marque - Required - Texte libre avec auto-suggestion sur la table elle même');
$table->string('name')->comment('Nom du produit - Required');
$table->timestamp('saw_at')->useCurrentOnUpdate()->useCurrent()->comment('Produit vue le (champs date)');
$table->timestamp('seen_at')->useCurrentOnUpdate()->useCurrent()->comment('Produit vue le (champs date)');
$table->unsignedTinyInteger('source')->comment('Source - Required - (valeur de l\'enum SourceEnum) -> champ SELECT');
$table->string('fair')->nullable()->comment('Salon - Affiché en front si SourceEnum = Salon Professionnel ou Salon Clients ou Salon Grand Public - Texte libre avec auto-suggestion sur la table elle même');
$table->string('shop')->nullable()->comment('Magasin - Affiché en front si SourceEnum = Magasin - Texte libre avec auto-suggestion sur la table elle même');

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

@ -0,0 +1,876 @@
<?php
use App\Enums\KeywordCategoryEnum;
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 App\Lib\Helpers\FileStorageHelper;
use App\Models\Brand;
use App\Models\Keyword;
use App\Models\Product;
use App\Models\ProductComment;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Console\OutputStyle;
use Illuminate\Contracts\Database\Query\Builder;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\QueryException;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\ItemNotFoundException;
use Illuminate\Support\Str;
use Symfony\Component\Console\Helper\ProgressBar;
use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Console\Output\ConsoleOutput;
return new class extends Migration
{
/**
* StartC table prefix
*
* @var string
*/
private $tablePrefix = 'STARTC';
/**
* StartC table to migrate
*
* @var string[]
*/
private $tableList = [
'FAMILLE', 'CERTIFICATION',
'RAYON', 'RESEAU',
'SECTEUR', 'SOURCE',
'VEILLE_PROD', 'VEILLE_PROD_COMM',
];
/**
* Console tabulation (margin left)
* @var integer
*/
private $consoleTabulation = 28;
/** @var \App\Models\User */
private $dummyUser;
/** @var \App\Models\Brand */
private $dummyBrand;
/**
* Run the migrations.
*
* @return void
* @throws \RuntimeException In case of failure.
*/
public function up(): void
{
$ouput = new ConsoleOutput();
$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.
Schema::disableForeignKeyConstraints();
DB::table('brands')->truncate();
DB::table('keywordables')->truncate();
DB::table('keywords')->truncate();
DB::table('products')->truncate();
DB::table('product_comments')->truncate();
// * Remove previously uploaded files.
File::deleteDirectory(storage_path('app/public/modelfiles/products'));
Schema::enableForeignKeyConstraints();
if (Artisan::call('db:seed', ['--class' => 'KeywordInitSeeder']) !== 0) {
throw new \RuntimeException('Failed to seed keywords');
}
// * Seed a dummy user.
$this->dummyUser = User::factory()->createOne([
'firstname' => 'Dummy',
'lastname' => 'User'
]);
// * Seed a dummy brand.
$this->dummyBrand = Brand::factory()->createOne([
'name' => Str::of('Sans Marque')->slug()->studly(),
'filter' => null
]);
// * Apply DB corrections
$this->setUniqueOnBrandName();
$this->setJsonColumnsAsJsonOnProducts();
$this->fixForeignKeyActionOnKeywordables();
$this->fixForeignKeyActionOnProcesses();
// ! DEV: Comment this after running once, to develop a migration without remigrating every temp table.
$this->createAndFillTempTables();
// * Migrate PRODUCTS, comment all except the one you want to develop here.
$this->migrateBrands($style);
$this->migrateProducts($style);
$this->migrateProductsComments($style);
if (!$style->confirm('Continue ?')) {
throw new \RuntimeException('Migration stoped');
}
$this->dropTempTables();
$section->clear();
}
/**
* Reverse the migrations.
*
* @return void
* @throws \Error Wont rollback.
*/
public function down(): void
{
throw new \Error('Wont rollback');
}
// - MIGRATION METHODS -
/**
* Migrate products comments
*
* @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 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
$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())
->unique();
$MOTKS = Keyword::query()->where('keyword_cat', 'LIKE', 'product_%')->get();
/** @var \Illuminate\Support\Collection<string,string> */
$START_SOURCES = $this->getQuery('SOURCE')->get();
/** @var \Illuminate\Support\Collection<string,string> */
$START_CERTIFICATIONS = $this->getQuery('CERTIFICATION')->get();
/** @var \Illuminate\Support\Collection<string,string> */
$START_FAMILLES = $this->getQuery('FAMILLE')->get();
/** @var \Illuminate\Support\Collection<string,string> */
$START_SECTEURS = $this->getQuery('SECTEUR')->get();
/** @var \Illuminate\Support\Collection<string,string> */
$START_RAYONS = $this->getQuery('RAYON')->get();
// No need to paginate here, column name is unique.
$brands = Brand::all();
$sources = collect(SourceEnum::cases());
$certifications = collect(CertificationEnum::cases());
$claims = collect(ClaimEnum::cases());
$ranges = collect(RangeEnum::cases());
$geo_areas = collect(GeoAreaEnum::cases());
$origins = collect(OriginEnum::cases());
$index = "NUM_VEILLE_PROD";
$query = $this->getQuery("VEILLE_PROD")->orderBy($index);
$nbRowsToMigrate = (clone $query)->count();
$totalRowsToMigrate = $nbRowsToMigrate;
$console = new ConsoleOutput();
$console->writeln('');
$progress = new ProgressBar($console, $nbRowsToMigrate);
$progress->setMessage(str_pad('Migrate Products', $this->consoleTabulation, ' '));
$progress->setFormat(' - %message% [%bar%] %percent:3s%% %current%/%max% %elapsed:6s%/%estimated:-6s%');
while ($nbRowsToMigrate > 0) {
$progress->display();
$progress->advance(1);
$row = (clone $query)->skip($totalRowsToMigrate - $nbRowsToMigrate--)->limit(1)->first();
$resolveAllegation = function (string $alleg) use ($claims, $otherAllegations) {
// phpcs:ignore Generic.Files.LineLength.TooLong
if ($alleg === 'Sans additif, sans colorant, sans conservateur,100% simple, sans polyphosphate,sans sel nutrite') {
return ClaimEnum::withoutAdditives;
}
if (in_array(Str::of($alleg)->squish(), $otherAllegations->all())) {
return ClaimEnum::others;
}
try {
return $claims->firstOrFail(
fn (ClaimEnum $claim) => Str::of($claim->label())->ascii()->replace(' ', '')
== Str::of($alleg)->trim()->ascii()->replace(' ', '')
);
} catch (\Exception) {
dump(
$alleg,
$claims->map(fn (ClaimEnum $claim) => $claim->label())
);
throw new \Error("allegation `{$alleg}` no found");
}
};
$resolveOrigin = function (string $ori) use ($origins) {
if (
in_array(Str::of($ori)->squish(), [
'N??,??lev?? et pr??par?? en France',
'N???,???lev??? et pr???par??? en France'
])
) {
return ClaimEnum::others;
}
try {
return $origins->firstOrFail(
fn (OriginEnum $origin) => Str::of($origin->label())->ascii()->replace(' ', '')
== Str::of($ori)->trim()->ascii()->replace(' ', '')
);
} catch (\Exception) {
dump(
$ori,
$origins->map(fn (OriginEnum $origin) => $origin->label())
);
throw new \Error("origin `{$ori}` no found");
}
};
$resolveMotk = function (string $motk) use ($MOTKS) {
// * Uu OMG !!!
if ($motk === 'Gain de temps Nomadisme') {
return $MOTKS->where('label', 'Gain de temps')
->where('keyword_cat', KeywordCategoryEnum::productPracticality)->firstOrFail();
}
if ($motk === 'Vari??t?? des sens') {
return $MOTKS->where('label', 'Variété des sens')
->where('keyword_cat', KeywordCategoryEnum::productPleasure)->firstOrFail();
}
if ($motk === 'V??g??tal') {
return $MOTKS->where('label', 'Végétal')
->where('keyword_cat', KeywordCategoryEnum::productHealth)->firstOrFail();
}
try {
return $MOTKS->firstOrFail(
fn (Keyword $keyword) => Str::of($keyword->label)->ascii()->replace(' ', '')
== Str::of($motk)->trim()->ascii()->replace(' ', '')
);
} catch (ItemNotFoundException) {
dump(
$motk,
$MOTKS->pluck('label')
);
throw new \Error("keyword `{$motk}` no found");
}
};
// NOTE: thread https://lab.s-web.fr/ldc-web/projets/startcoq/-/issues/28#note_9740 .
if (is_null($row->MARQUE)) {
continue;
}
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)) {
continue;
}
// NOTE: thread https://lab.s-web.fr/ldc-web/projets/startcoq/-/issues/28#note_9758 .
if (empty($row->NUM_SECTEUR)) {
continue;
}
// NOTE: thread https://lab.s-web.fr/ldc-web/projets/startcoq/-/issues/28#note_9760 .
if (empty($row->NUM_RAYON)) {
continue;
}
// NOTE: thread https://lab.s-web.fr/ldc-web/projets/startcoq/-/issues/28#note_9767 .
if (empty($row->NUM_FAMILLE)) {
continue;
}
$brandName = Str::of($row->MARQUE)->trim()->title();
/** name is unique on brand */
$brand = null;
try {
$brand = $brands->where('name', $brandName)->firstOrFail();
} catch (\Exception) {
throw new \Error("brand` {$brandName}` not found");
}
$url = $row->URL ? Str::lower(url($row->URL)) : null;
$date_saisie = $row->DATE_SAISIE ? $this->tryToParseCarbon($row->DATE_SAISIE, true) : null;
$is_tasted = Str::lower($row->DEGUSTER_O_N) === 'o' ? true : false;
$claimList = collect([
$row->ALLEG0, $row->ALLEG1, $row->ALLEG2
])->filter()->map(fn (string $alleg) => $resolveAllegation($alleg));
$originList = collect([
$row->ORI0, $row->ORI1, $row->ORI2
])->filter()->map(fn (string $origin) => $resolveOrigin($origin));
$keywordsList = collect([
$row->MOTK0, $row->MOTK1, $row->MOTK2, $row->MOTK3, $row->MOTK4, $row->MOTK5
])->filter()->map(fn (string $motk) => $resolveMotk($motk));
$sourceEnum = null;
if ($row->NUM_SOURCE) {
$sourceEnum = $sources->firstOrFail(fn (SourceEnum $source) => $source->value() == $row->NUM_SOURCE);
}
$certificationEnum = null;
if ($row->CERTIFICATION) {
$certificationEnum = $certifications->firstOrFail(
fn (CertificationEnum $certification) => $certification->value() == $row->CERTIFICATION
);
}
$distributionNetwork = $row->NUM_RESEAU ? DistributionNetworkEnum::from($row->NUM_RESEAU) : null;
$ray = $row->NUM_RAYON ? RayEnum::from($row->NUM_RAYON) : null;
$product_range = null;
if ($row->NUM_FAMILLE) {
$product_range = $ranges->firstOrFail(
fn (RangeEnum $range) => $range->value() == $row->NUM_FAMILLE
);
}
$geo_area = null;
if ($row->NUM_SECTEUR) {
$geo_area = $geo_areas->firstOrFail(
fn (GeoAreaEnum $geoArea) => $geoArea->value() == $row->NUM_SECTEUR
);
}
$claim = null;
if ($row->COMP_ALLEG) {
try {
if (!$resolveAllegation($row->COMP_ALLEG)->equals(ClaimEnum::others)) {
throw new \Error("`{$row->COMP_ALLEG}` is not resolved as `other`");
}
$claim = $row->COMP_ALLEG;
} catch (\Exception) {
throw new \Error("`{$row->COMP_ALLEG}` not found");
}
}
/** @var \App\Models\Product $product */
$product = Product::query()->where('id', $row->{$index})->firstOrNew();
$product->id = $row->NUM_VEILLE_PROD;
// * 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,
'catalogue' => $row->CATALOGUE,
'certification' => $certificationEnum,
'comment' => $row->COMM_LIBRE,
'distribution_network' => $distributionNetwork,
'fair' => $row->SALON,
'geo_area' => $geo_area,
'name' => $row->NOM,
'product_range' => $product_range,
'ray' => $ray,
'shop' => $row->MAGASIN,
'source' => $sourceEnum,
'pvc' => $row->PVC,
'weight' => $row->POIDS,
'is_tasted' => $is_tasted,
'gencod' => $row->GENCOD,
'url' => $url,
'claims' => $claimList,
'specified_claim' => $claim,
'tasters_number' => $row->NB_DEGUST,
'origins' => $originList,
'docs' => $files,
'picture_alt' => "Image de {$row->NOM}",
'picture_title' => "Image de {$row->NOM}",
'picture' => $picture,
// NOTE: https://lab.s-web.fr/ldc-web/projets/startcoq/-/issues/28#note_10688 .
'user_id' => $this->dummyUser->id
];
if ($date_saisie) {
// ? FIXME: IS this pertinent ?
$fill['seen_at'] = $date_saisie;
$fill['created_at'] = $date_saisie;
$fill['updated_at'] = $date_saisie;
}
if (Str::of($row->SUPPRIMER_O_N)->lower() === 'o') {
$fill['deleted_at'] = $date_saisie;
}
$product->fill($fill);
if (!DB::transaction(fn () => $product->saveQuietly())) {
throw new \Error("Failed to save product {$row->NUM_VEILLE_PROD}");
}
$product->productKeywords()->sync($keywordsList->pluck('id'));
} //end while
$progress->finish();
}
/**
* Create unique brands using veille products
*
* @param \Illuminate\Console\OutputStyle $style
* @return void
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.Found
*/
private function migrateBrands(OutputStyle $style): void
{
// phpcs:enable
$query = $this->getQuery("VEILLE_PROD");
/** @var \Illuminate\Support\Collection<string,string> */
$brands = (clone $query)->pluck('MARQUE')->filter()
->map(fn (string $brandName) => Str::of($brandName)->trim()->title())->unique();
$console = new ConsoleOutput();
$console->writeln('');
$progress = new ProgressBar($console, $brands->count());
$progress->setMessage(str_pad('Migrate Brands', $this->consoleTabulation, ' '));
$progress->setFormat(' - %message% [%bar%] %percent:3s%% %current%/%max% %elapsed:6s%/%estimated:-6s%');
// FIXME: Insert brand->filter and find from where it should be fetched.
$brands->each(function ($brandName) use ($progress) {
Brand::query()->where('name', $brandName)->firstOrNew()->fill([
'name' => $brandName
])->saveQuietly();
$progress->advance();
});
$progress->finish();
}
// - END MIGRATION METHODS -
// -- DB CORRECTIONS --
/**
* Set unique on brands.name
*
* @return void
*/
private function setUniqueOnBrandName(): void
{
try {
Schema::table('brands', function (Blueprint $table) {
$table->dropUnique(['name']);
});
// phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
} catch (QueryException) {
// * Do nothing
} finally {
Schema::table('brands', function (Blueprint $table) {
$table->unique(['name']);
});
}
}
/**
* Correct some columns to effectively json
*
* @return void
*/
private function setJsonColumnsAsJsonOnProducts(): void
{
Schema::table('products', function (Blueprint $table) {
$table->json('origins')->nullable()->change()
->comment('Origine - Tableau d\'Enum (entier) JSON (valeur de l\'enum OriginEnum)');
$table->json('claims')->nullable()->change()
->comment('Autres allégations - Tableau d\'Enum (entier) JSON (valeur de l\'enum ClaimEnum)');
$table->json('docs')->nullable()->change()
->comment('Docs complémentaires - Tableau de path - 0 a 5 docs PDF');
});
}
/**
* Restrict foreign keys on keywordables
*
* @return void
*/
private function fixForeignKeyActionOnKeywordables(): void
{
Schema::table('keywordables', function (Blueprint $table) {
$table->dropForeign(['keyword_id']);
});
Schema::table('keywordables', function (Blueprint $table) {
$table->foreign(['keyword_id'])->references(['id'])->on('keywords')
->restrictOnUpdate()->cascadeOnDelete();
});
}
/**
* Restrict foreign keys on processes
*
* @return void
*/
private function fixForeignKeyActionOnProcesses(): void
{
Schema::table('processes', function (Blueprint $table) {
$table->dropForeign(['user_id']);
});
Schema::table('processes', function (Blueprint $table) {
$table->foreign(['user_id'])->references(['id'])->on('users')
->restrictOnUpdate()->restrictOnDelete();
});
}
// -- END DB CORRECTION --
// --- 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
*
* @param string $tableName
* @return \Illuminate\Contracts\Database\Query\Builder
* @throws \Error If $tableName does not exists.
*/
private function getQuery(string $tableName): Builder
{
if (!DB::table($this->getTableName($tableName))->exists()) {
throw new \Error("table {$tableName} does not exists");
}
return DB::table($this->getTableName($tableName));
}
/**
* Get a table if it exists
*
* @param string $tableName
* @return string
* @throws \Error If table is unkown.
*/
private function getTableName(string $tableName): string
{
if (!in_array($tableName, $this->tableList)) {
throw new Error("Unknown table {$tableName}");
}
return "{$this->tablePrefix}_{$tableName}";
}
/**
* Create table in SQL
*
* @param string $tableName
* @param array<object> $tableColumns With name, type, size.
* @return void
*/
private function createTable(string $tableName, array $tableColumns)
{
Schema::dropIfExists($tableName);
Schema::create($tableName, function (Blueprint $table) use ($tableColumns) {
foreach ($tableColumns as $tableColumn) {
$tableComment = "{$tableColumn->name} {$tableColumn->type} {$tableColumn->size}";
switch ($tableColumn->type) {
case 'NUMBER':
$table->double($tableColumn->name)
->nullable()->comment($tableComment);
break;
case 'VARCHAR2':
case 'CHAR':
$table->string($tableColumn->name, $tableColumn->size)
->nullable()->comment($tableComment);
break;
case 'DATE':
$table->dateTime($tableColumn->name)
->nullable()->comment($tableComment);
break;
default:
throw new Error("Unhandled type {$tableColumn->type}");
}
}
});
}
/**
* Create temp tables and fill them
*
* @return void
* @throws \Error In case of failure.
*/
private function createAndFillTempTables(): void
{
$console = new ConsoleOutput();
$console->writeln('');
foreach ($this->tableList as $tableName) {
$section = $console->section();
$header = $this->fetchOciTableHeader($tableName);
$this->createTable(
$this->getTableName($tableName),
$header
);
$message = str_pad("{$this->tablePrefix} {$tableName}", $this->consoleTabulation, ' ');
$section->write(" - {$message} -> Fetching OCI DATA..");
$content = $this->fetchOciTableContent($tableName, $header);
$section->clear();
$progress = new ProgressBar($section, count($content));
$progress->setMessage($message);
$progress->setFormat(' - %message% [%bar%] %percent:3s%% %current%/%max% %elapsed:6s%/%estimated:-6s%');
foreach ($content as $row) {
$progress->advance();
DB::table($this->getTableName($tableName))->insert((array)$row);
}
$progress->finish();
} //end foreach
}
/**
* Create temp tables and fill them
*
* @return void
* @throws \Error In case of failure.
*/
private function dropTempTables(): void
{
foreach ($this->tableList as $tableName) {
Schema::drop($this->getTableName($tableName));
}
}
/**
* Oci table header
*
* @param string $tableName
* @return array<object> with name, type, size
* @throws \Error If connection or query fails.
*/
private function fetchOciTableHeader(string $tableName): array
{
$tableName = $this->getTableName($tableName);
$conn = $this->getOciConnection();
// Prepare the statement.
$stid = oci_parse($conn, "SELECT * FROM {$tableName}");
if (!$stid) {
$e = oci_error($conn);
throw new \Error(htmlentities($e['message'], ENT_QUOTES));
}
// Use OCI_DESCRIBE_ONLY if not fetching rows.
oci_execute($stid, OCI_DESCRIBE_ONLY);
if (!$stid) {
$e = oci_error($conn);
throw new \Error(htmlentities($e['message'], ENT_QUOTES));
}
$ncols = oci_num_fields($stid);
$out = [];
for ($i = 1; $i <= $ncols; $i++) {
$column_name = oci_field_name($stid, $i);
$column_type = oci_field_type($stid, $i);
$column_size = oci_field_size($stid, $i);
$out[] = (object)[
'name' => $column_name,
'type' => $column_type,
'size' => $column_size
];
}
oci_free_statement($stid);
oci_close($conn);
return $out;
}
/**
* Oci table content
*
* @param string $tableName Table name.
* @param array<object> $header With name, type,size.
* @return array<object> With DB content.
* @throws \Error If connection or query fails.
*/
private function fetchOciTableContent(string $tableName, array $header): array
{
$out = [];
$tableName = $this->getTableName($tableName);
$conn = $this->getOciConnection();
// Prepare the statement.
$stid = oci_parse($conn, "SELECT * FROM {$tableName}");
if (!$stid) {
$e = oci_error($conn);
throw new \Error(htmlentities($e['message'], ENT_QUOTES));
}
$r = oci_execute($stid);
if (!$stid) {
$e = oci_error($conn);
throw new \Error(htmlentities($e['message'], ENT_QUOTES));
}
while ($row = oci_fetch_array($stid, OCI_ASSOC + OCI_RETURN_NULLS)) {
$outRow = [];
foreach ($row as $k => $item) {
/** @var string */
$type = collect($header)->where('name', $k)->firstOrFail()->type;
if ($type === 'DATE' and $item !== null) {
$item = $this->tryToParseCarbon($item);
}
$outRow[$k] = $item;
}
$out[] = (object)$outRow;
}
oci_free_statement($stid);
oci_close($conn);
return $out;
}
/**
* get Oci connection
*
* @return resource oci8 connection resource
* @throws \Error If connection fails.
*/
private function getOciConnection()
{
$conn = oci_connect('system', 'secret', 'localhost/XE');
if (!$conn) {
$e = oci_error();
throw new \Error(htmlentities($e['message'], ENT_QUOTES));
}
return $conn;
}
/**
* Try to parse freaking Start coq dates
*
* @param string $date The string date to parse.
* @param boolean $asTimestamp To force the return of a valid TIMESTAMP (in epoch).
* @return \Carbon\Carbon
* @throws \Error If the date cannot fit in a timestamp.
*/
private function tryToParseCarbon(string $date, bool $asTimestamp = false): Carbon
{
$utcTimeZone = new DateTimeZone('UTC');
$cDate = Carbon::parse($date, $utcTimeZone);
if (
$asTimestamp and
$cDate->lt(Carbon::createFromTimestamp(0, $utcTimeZone)) or
$cDate->gt(Carbon::createFromTimestamp(
(new DateTime("99999/12/31 00:00:00"))->getTimestamp(),
$utcTimeZone
))
) {
throw new \Error("The date {$date} is cannot fit in a Timestamp");
}
if ($cDate->timestamp === 0) {
// * Prevent Invalid datetime format: 1292 Incorrect datetime value: '1970-01-01 00:00:00'
$cDate->addSecond();
}
return $cDate;
}
};

View file

@ -0,0 +1,185 @@
# EXPORT THIS FROM DB USING
# `COMP_ALLEG` NOT IN(
# 'Sans additif, sans colorant, sans conservateur,100% simple, sans polyphosphate,sans sel nutrite',
# 'Sans additif, sans colorant, sans conservateur, 100% simple, sans polyphosphate, sans sel nitrité',
# 'Produit de l\'année',
# 'Bleu Blanc Coeur, source d\'Omega 3',
# 'Sans OGM',
# 'Sans gluten',
# 'Sans Antibiotique',
# 'Vegan',
# 'Allégé',
# 'Halal, Casher',
# 'Riche en protéines, fibres, calcium…',
# 'Autres'
# )
Agri ethique france commerce equitable
Commerce equitable
eleveurs engages, alimentation 100% vegetaux, mineraux vitamines, cereales d'origines locales, collecte de proximite, respect du bien-etre animal
Emballage produit en Vendee
Faible en MG / pauvre en sel
producteurs partenaires
Recette Vegetarienne
Sans colorant
Sans pesticides
Saumon garanti jamais congele
saumon garanti jamais congele
Creme fraiche origine france et chablis AOC
Elabore a la main, petits pois origine france
sans colorant ni conservateur , sans huile de palme
SANS COLORANT SANS HUILE DE PALME SANS EXHAUSTEUR DE GOUT
sans colorant, conservateur et huile de palme
sans colorant, conservatuer et huile de palme
VEGETAL
vegetarien
Antioxydant/ Vitamine C
equitable
Filiere cacao equitable
Ferme engagee
Filiere courte des eleveurs aux consommateurs ;
meilleur remuneration des producteurs
-25% sel
-60% de plastique
100% filets
Barquette en bois issue de cultures durables
bien eleve et nourri
COLLECTION FESTIVE
filet de poulet
fume au bois de hetre
Origine France
Sans additif, Sans conservateur
sans conservateur
sans conservateur - vegetarien
sans conservateur, colorant, arome ajoute,
sans conservateur, sans colorant, sans huile de palme, sans exhausteur de gout, sans huile hydrogenee
sans huile de palem , sans exhausteur de gout
Sans huile de palme, Sans exhausteur de gout
Sans sel nitrite ajoute
source de proteines vegetales et sources de fibres
viande 100% Filet
Eleve en plein air
certifie
Cooperative engagee
Faible en matiere grasse
Le porc Francais
Mache cultivee en Pays de la Loire
Melusines Bieres Artisanales
Pauvre en sel
Porc francais
Porc Francais, produit de Bretagne
Poulet fermier
Poulet Francais, sans conservateur ni colorant
SANS ADDITIF
Sans colorant, avec du poulet eleve au sol et bien nourri
Sans huile de palme / sans exhausteur de gout
Sans sel nitrite
Source de proteines
+ de 40 eleveurs de votre region
100 % local
100% Alimentation vegetale, minerale er vitamines
100% local,
100% local, issu de votre region
100% viande de volaille
5 min a la poele
AB
Agri cnfiance
Alimentation 100% vegetale
Alimentation 100% vegetale CLASSE A
alimentation 100% vegetale minerale et vitaminee
alimentation 100% vegetale minerale et vitamines
alimentation 100% vegetale, 50% mais
Alimentation 100% vegetale, minerale et vitamines
Alimentation 100% vegetale, Minerale et vitamines
Alimentation cereales 100% francaises
Alimentation cereales entieres 100% francaises
alimente avec 100% vegetaux mineraux et vitamines
Avec de la dinde bien nourrie
avec du poulet bien nourri
avec du poulet bien nourri, sans colorant
barquette avec 60% de plastique recycle
Barquette avec 60% recycle
Barquette cuisson
Barquettes recyclables a 60 % en carton
Bien etre animal
Bien-etre animal
Bq 100% recyclable issu de 85% de plastique recycle
Cereales entieres 100% francaises
Cereales produites localement; preparation et abattage local; j'achete local
Chair plus gouteuse
Clean label
Clean Label
Confit de France
Contribue a l'economie locale, revenu des eleveurs protege, + de 5 900 emplois soutenus en regions
cuit a l'etouffee
Eleve en France
Eleve en plein air et Emballage eco-concu -45% de plastique
Eleve en plein air, Emballage eco-concu
Elevee en plein air, Emballage eco-concu
Emballage eco-concu -45% de plastique
Engagee pour la vie de nos eleveurs, Barquette avec 60% de plastique recycle
Ethik food
Faconne traditionnellement a la main
Fait main
Faite main
Ficele a la main
Fromage AOP
fume au bois de hetre et de chene
Garantie volaille bien elevee et bien nourrie
Garantie Volaille bien elevee et bien nourrie et bien
Issu de souche rustique
J'aime manger local
Juste remuneration des eleveurs
La moins calorique
La viande du quotidien la moins calorique
moins de 2% de mG
montees a la main
Montees a la main
montees main
naturellement tendres
ne et eleve en France abattue en Allemagne
NIVEAU BIEN ETRE ANIMAL
nourri aux cereales locales
Nourri avec 100% de vegetaux, mineraux et vitamines
Nourri avec 100% vegetaux, mineraux et vitamines
Nourri avec des cereales 100% francaises
Nourrie sans OGM (<0,9%) cereales francaises, elevage respectueux, barquette 100% recyclable
Origine certifie sud ouest
Planete positive
PRET A CONSOMMER
sans antibio
sans huile de palme
Sans Huile de palme
sans huile de palme sans colorant
Sans lactose
Sans lactose, Sources de proteines
sans traitement antibiotique , aux aromes naturels, sans conservateur
Sortant a l'exterieur
Sources de proteines
Viande francaise
Volaille bien nourie
volaille francaise
volaille nourrie 100% vegetaux mineraux et vitamines
Barquette Cuisson
Cuisson facile
Elabores par nos bouchers
Eleve en large enlos
En direct de nos ateliers
extra tendre
Faite mains
Filiere responsable engagee pour vous
gigot d'agneau
les eleveurs cooperateurs; sans colorant ni conservateur
Montees main, pic inox, qualite superieur
Omega 3
Porc Francais, fabrication savoie
qualite superieur
recette traditionnelle, sans colorant ni conservateur
Sans additif
sans os
Savoir faire boucher francais
Selectionne par nos Bouchers
Terres d'Eure et Loir
Tous engages : nourris aux cereales francaise sans OGM
Viande de cheval fraiche
100% pur beure
100% vegetal

View file

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

View file

@ -16,186 +16,7 @@ class KeywordSeeder extends Seeder
*/
public function run()
{
$id = 0;
/**
* * INGREDIENTS
*/
$keyword = new Keyword();
$keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[0];
$keyword->label = "Sophistication";
$keyword->save();
$keyword = new Keyword();
$keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[0];
$keyword->label = "Exotisme";
$keyword->save();
$keyword = new Keyword();
$keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[0];
$keyword->label = "Fun";
$keyword->save();
$keyword = new Keyword();
$keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[1];
$keyword->label = "Clean label/naturalité";
$keyword->save();
$keyword = new Keyword();
$keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[1];
$keyword->label = "Végétarien/vegan";
$keyword->save();
$keyword = new Keyword();
$keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[1];
$keyword->label = "Bien être";
$keyword->save();
$keyword = new Keyword();
$keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[2];
$keyword->label = "Solidarité";
$keyword->save();
$keyword = new Keyword();
$keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[2];
$keyword->label = "Écologie";
$keyword->save();
$keyword = new Keyword();
$keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[3];
$keyword->label = "Emballage individuel";
$keyword->save();
$keyword = new Keyword();
$keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[3];
$keyword->label = "Facilité de mise en oeuvre";
$keyword->save();
/**
* * PRODUCTS
*/
$keyword = new Keyword();
$keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[4];
$keyword->label = "Sophistication";
$keyword->save();
$keyword = new Keyword();
$keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[4];
$keyword->label = "Exotisme";
$keyword->save();
$keyword = new Keyword();
$keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[4];
$keyword->label = "Variété des sens";
$keyword->save();
$keyword = new Keyword();
$keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[4];
$keyword->label = "Fun";
$keyword->save();
$keyword = new Keyword();
$keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[5];
$keyword->label = "Minceur";
$keyword->save();
$keyword = new Keyword();
$keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[5];
$keyword->label = "Énergie/Bien-être";
$keyword->save();
$keyword = new Keyword();
$keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[5];
$keyword->label = "Cosmétique";
$keyword->save();
$keyword = new Keyword();
$keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[6];
$keyword->label = "Manipulation produit";
$keyword->save();
$keyword = new Keyword();
$keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[6];
$keyword->label = "Gain de temps";
$keyword->save();
$keyword = new Keyword();
$keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[6];
$keyword->label = "Nomadisme";
$keyword->save();
$keyword = new Keyword();
$keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[7];
$keyword->label = "Naturalité";
$keyword->save();
$keyword = new Keyword();
$keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[7];
$keyword->label = "Végétal";
$keyword->save();
$keyword = new Keyword();
$keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[7];
$keyword->label = "Médical";
$keyword->save();
$keyword = new Keyword();
$keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[8];
$keyword->label = "Solidarité";
$keyword->save();
$keyword = new Keyword();
$keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[8];
$keyword->label = "Écologie";
$keyword->save();
/**
* * PACKAGING
*/
$keyword = new Keyword();
$keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[9];
$keyword->label = "Sophistication";
$keyword->save();
$keyword = new Keyword();
$keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[9];
$keyword->label = "Exotisme";
$keyword->save();
$keyword = new Keyword();
$keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[9];
$keyword->label = "Fun";
$keyword->save();
$keyword = new Keyword();
$keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[10];
$keyword->label = "Manipulation produit";
$keyword->save();
$keyword = new Keyword();
$keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[10];
$keyword->label = "Gain de temps";
$keyword->save();
$keyword = new Keyword();
$keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[10];
$keyword->label = "Nomadisme";
$keyword->save();
$keyword = new Keyword();
$keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[11];
$keyword->label = "Décor/forme";
$keyword->save();
$keyword = new Keyword();
$keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[11];
$keyword->label = "Matériaux";
$keyword->save();
$keyword = new Keyword();
$keyword->keyword_cat = \collect(KeywordCategoryEnum::cases())[12];
$keyword->label = "Écologie";
$keyword->save();
$this->call(KeywordInitSeeder::class);
// Seeder for keywordables table.
Keyword::query()->get()->each(function (Keyword $keyword) {
Keywordable::factory(mt_rand(1, 3))->make()->each(function (Keywordable $keywordable) use ($keyword) {
@ -226,7 +47,7 @@ class KeywordSeeder extends Seeder
$keywordable->keyword()->associate($keyword);
$keywordable->keywordable_type = 'App\Models\Packaging';
$keywordable->saveOrFail();
}
}//end if
});
});
}

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 [
'classes' => [
'products' => 'Produits',
'products_monitoring' => 'Veille produit|Veilles produit',
'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
declare(strict_types=1);
return [
'accepted' => 'Le champ :attribute doit être accepté.',
'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_num' => 'Le champ :attribute doit contenir uniquement des chiffres et des lettres.',
'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_or_equal' => 'Le champ :attribute doit être une date antérieure ou égale au :date.',
'between' => [
@ -19,11 +22,18 @@ return [
'string' => 'Le texte :attribute doit contenir entre :min et :max caractères.',
],
'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.',
'current_password' => 'Le mot de passe est incorrect.',
'date' => 'Le champ :attribute n\'est pas une date valide.',
'date_equals' => 'Le champ :attribute doit être une date égale à :date.',
'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_if' => 'Le champ :attribute doit être décliné quand :other a la valeur :value.',
'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.',
'dimensions' => 'La taille de l\'image :attribute n\'est pas conforme.',
'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.',
'ends_with' => 'Le champ :attribute doit se terminer par une des valeurs suivantes : :values',
'enum' => 'Le champ :attribute sélectionné est invalide.',
@ -57,6 +69,7 @@ return [
'ipv4' => 'Le champ :attribute doit être une adresse IPv4 valide.',
'ipv6' => 'Le champ :attribute doit être une adresse IPv6 valide.',
'json' => 'Le champ :attribute doit être un document JSON valide.',
'lowercase' => 'Le champ :attribute doit être en minuscules.',
'lt' => [
'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.',
@ -71,24 +84,39 @@ return [
],
'mac_address' => 'Le champ :attribute doit être une adresse MAC valide.',
'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.',
'numeric' => 'La valeur de :attribute ne peut être supérieure à :max.',
'string' => 'Le texte de :attribute ne peut contenir plus de :max caractères.',
'numeric' => 'La valeur de :attribute ne peut pas être supérieure à :max.',
'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.',
'mimetypes' => 'Le champ :attribute doit être un fichier de type : :values.',
'min' => [
'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.',
'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',
'not_in' => 'Le champ :attribute sélectionné n\'est pas valide.',
'not_regex' => 'Le format du champ :attribute n\'est pas valide.',
'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.',
'prohibited' => 'Le champ :attribute est interdit.',
'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_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_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_with' => 'Le champ :attribute est obligatoire quand :values est présent.',
'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',
'string' => 'Le champ :attribute doit être une chaîne de caractères.',
'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.',
'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.',
'uuid' => 'Le champ :attribute doit être un UUID valide',
'custom' => [],
'phone' => 'Le champ :attribute n\'est pas un numéro de téléphone valide.',
'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_rgb' => ':attribute doit être un code couleur RGB valide',
'color_rgba' => ':attribute doit être un code couleur RGBA valide',
'color_name' => ':attribute doit être un nom de couleur CSS valide',
'postal_code' => 'Le champ :attribute accepte les codes postaux des pays suivants :countries comme par exemple :examples.'
'attributes' => [
'address' => 'adresse',
'age' => 'âge',
'amount' => 'montant',
'area' => 'zone',
'available' => 'disponible',
'birthday' => 'anniversaire',
'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 {
max-height: 60px;
// max-height: 60px;
width: 180px;
height: auto;
}
.text-ellipsis {
@ -13,9 +15,20 @@
white-space: nowrap;
}
.btn-md {
min-width: 38px;
height: 38px;
}
// Print fiche commerciale
@media print {
#print-id-product {
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>
<script lang="ts">
import { defineComponent } from "vue";
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";
import parseLaravelErrorMessages from "./../modules/parseLaravelErrorMessages";
export default defineComponent({
name: "BelongsToManyDropdown",
mixins: [route, trans, parseLaravelErrorMessages],
components: {
VueTagsInputCls,
@ -88,6 +90,7 @@ export default defineComponent({
this.selectedItems = this.parseTags(data.value);
this.allItems = this.parseTags(data.itemsList);
this.placeholder = data.placeholder;
this.intStoreRoute = data.storeRoute ?? null;
// * Min and max calc.
this.min = Number.parseInt((data?.itemLimit ?? [])[0] ?? 0);
this.min = this.min < 0 ? 0 : this.min;
@ -108,6 +111,7 @@ export default defineComponent({
waitingListObjects: Array<VueTagsInput.IAddArgs>;
allErrors: Record<string, Array<string>>;
message: string | null;
intStoreRoute: string | null;
} {
return {
name: "",
@ -121,6 +125,7 @@ export default defineComponent({
waitingListObjects: [],
allErrors: {},
message: null,
intStoreRoute: null,
};
},
methods: {
@ -143,6 +148,23 @@ export default defineComponent({
this.selectedItems.push(customTag);
this.item = "";
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 {
this.message = this.__("Nombre d'élements maximal atteint.");

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
export { };
import { AxiosStatic } from "axios";
import type VueTagsInput from "@sipec/vue3-tags-input";
import { AxiosStatic } from "axios";
// * Recursive List<string>
type StringList = Record<PropertyKey, string>;
@ -30,7 +30,7 @@ type NestedNumberList = NestedNumberListInterface | NumberList | number;
type ModelList = Array<Model>;
declare global {
type CustomTag = LaravelModel & VueTagsInput.ITag;
type CustomTag = LaravelModel & VueTagsInput.ITag & {name:string,label:string, value:number|string};
type LaravelModel = Model;
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 {
faEraser,
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,
faAnglesRight,
faArrowAltCircleDown,
faArrowAltCircleUp,
faQuestion,
faXmark,
faArrowDown,
faArrowLeft,
faArrowLeftLong,
faArrowRight,
faArrowRightFromBracket,
faArrowUp,
faArrowsAltH,
faArrowsAltV,
faBackspace,
faCheck,
faChevronLeft,
faChevronRight,
faCircleNotch,
faCopy,
faFile,
faLink,
faPrint,
faImage,
faCrop,
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";
config.autoReplaceSvg = false;
@ -77,5 +84,12 @@ library.add(faLink);
library.add(faPrint);
library.add(faImage);
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();

View file

@ -5,27 +5,37 @@
{{-- CONTENT --}}
<table width="100%">
<tr>
@if (isset($product->picture) && !empty($product->picture))
<td class="table-cell-text-center" width="30%">
@if (isset($product->picture))
<img src="{{ public_path($product->picture) }}" alt="{{ $product->picture_alt }}" alt="{{ $product->picture_title }}">
@endif
</td>
<td width="70%">
@if (isset($product->brand_id->name))
<h2><strong>{{ __('Marque du produit : ') }}</strong><span>{{ $product->brand_id->name }}</span></h2>
@else
<td width="100%">
@endif
@if (isset($product->name))
<h2><strong>{{ __('Marque du produit : ') }}</strong><span>{{ $product->brand->name }}</span></h2>
<h2><strong>{{ __('Nom du produit : ') }}</strong><span>{{ $product->name }}</span></h2>
@endif
@if (isset($product->certification))
<p><strong>{{ __('Certification : ') }}</strong>{{ $product->certification }}</p>
@endif
@if (isset($product->origins))
<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
@if (isset($product->claims))
<p><strong>{{ __('Allégations : ') }}</strong>{{ $product->claims }}</p>
@else
{{ $product->claims }}
@endif
</p>
<table class="table" width="100%">
<tr class="table-header">
@if (isset($product->gencod))
@ -44,16 +54,19 @@
<td class="table-cell-text-center">{{ $product->gencod }}</td>
@endif
@if (isset($product->weight))
<td class="table-cell-text-center">{{ $product->weight }}</td>
<td class="table-cell-text-center">{{ $product->weight }} g</td>
@endif
@if (isset($product->pvc))
<td class="table-cell-text-center">{{ $product->pvc }}€</td>
@endif
<td class="table-cell-text-center">--,--€</td>
@php
$priceKg = $product->weight ? (1000 * $product->pvc) / $product->weight : 0;
@endphp
<td class="table-cell-text-center">{{ number_format(round($priceKg, 2), 2, ',', ' ') }}€</td>
</tr>
</table>
@if (isset($product->productKeywords))
<p><strong>{{ __('Tendances identifiées : ') }}</strong>{{ $product->productKeywords }}</p>
<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>
@ -73,9 +86,8 @@
@if (isset($product->freeComment))
<p><strong>{{ __('Commentaire libre : ') }}</strong>{{ strip_tags($product->freeComment) }}</p>
@endif
@if(isset($product->productComments))
@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%">
<tr class="table-header">
<td class="table-cell-text-center" width="20%"><strong>{{ __('Appréciation') }}</strong></td>
@ -93,20 +105,18 @@
</tr>
</table>
@endforeach
@endif
<table width="100%" class="product-saw">
<table class="product-saw" width="100%">
<tr>
@if (isset($product->saw_at))
<td class="table-cell-text-center" width="50%">
<strong>{{ __('Téléchargez les documents :') }}</strong>
<a href="{{ route('products.download', $product->id) }}">{{ __('ICI') }}</a>
</td>
@endif
@if (isset($product->source))
<td class="table-cell-text-center" width="50%">
<p><strong>{{ __('Vu le : ') }}</strong>{{ $product->saw_at }}</p>
<p><strong>{{ __('Lieu : ') }}</strong>{{ $product->source }},&nbsp;{{ $product->fair }}{{ $product->shop }}{{ $product->catalogue }}</p>
<p><strong>{{ __('Par : ') }}</strong>{{ $product->user_id->firstname }}&nbsp;{{ $product->user_id->lastname }}</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>{{ __('Par : ') }}</strong>{{ $product->user->firstname }}&nbsp;{{ $product->user->lastname }}</p>
</td>
@endif
</tr>

View file

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

View file

@ -15,10 +15,10 @@
<li>
<a class="dropdown-item @if(request()->routeIs('products.*')) active @endif"
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-placement="right">
{{ __('models.classes.products') }}
{{ trans_choice('models.classes.products_monitoring', 2) }}
</a>
</li>
</ul>

View file

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

View file

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

View file

@ -12,7 +12,6 @@ $ignore = (isset($ignore) and is_array($ignore)) ? $ignore : [];
@endphp
{{-- NO Sort for order column --}}
@if($col === 'order' or in_array($col, $ignore))
@if(!$noOrder or $rst)
@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">
<span>{{ $colname }}</span>
@ -35,7 +34,6 @@ $ignore = (isset($ignore) and is_array($ignore)) ? $ignore : [];
<span>{{ $colname }}</span>
</th>
@endif
@endif
@continue
@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="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">
<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>
</div>
<a href="{{ route('products.index') }}"
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">
{{ __('Voir la liste') }}
{{ __('Consulter/Insérer une :model', ['model' => trans_choice('models.classes.products_monitoring', 1)]) }}
</a>
</div>
</div>

View file

@ -1,6 +1,6 @@
@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('breadcrumb', 'no')
@ -14,17 +14,17 @@
class="btn btn-primary text-decoration-none m-0"
data-bs="tooltip"
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">
<i class="fa-solid fa-arrow-left"></i>
</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>
</div>
<div class="col-12 col-md-4 d-flex justify-content-center justify-content-md-end align-items-center">
<button id="formSubmitClone"
class="btn btn-primary pull-end"
title="{{ __('Sauvegarder ce :model', ['model' => Str::singular(__('models.classes.products'))]) }}"
class="btn btn-success pull-end"
title="{{ __('Sauvegarder cette :model', ['model' => trans_choice('models.classes.products_monitoring', 1)]) }}"
data-bs="tooltip">
{{ __('Sauvegarder') }}
</button>
@ -39,8 +39,8 @@
<div class="col text-center">
<button id="formSubmit"
type="submit"
class="btn btn-primary"
title="{{ __('Sauvegarder ce :model', ['model' => Str::singular(__('models.classes.products'))]) }}"
class="btn btn-success"
title="{{ __('Sauvegarder cette :model', ['model' => trans_choice('models.classes.products_monitoring', 1)]) }}"
data-bs="tooltip">{{ __('Sauvegarder') }}</button>
</div>
</div>

View file

@ -1,6 +1,6 @@
@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('breadcrumb', 'no')
@ -14,11 +14,11 @@
class="btn btn-primary text-decoration-none m-0"
data-bs="tooltip"
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">
<i class="fa-solid fa-arrow-left"></i>
</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>
</div>
<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">
<button id="formSubmitClone"
type="button"
class="btn btn-primary pull-end"
title="{{ __('Sauvegarder ce :model', ['model' => Str::singular(__('models.classes.products'))]) }}"
class="btn btn-success pull-end"
title="{{ __('Sauvegarder cette :model', ['model' => trans_choice('models.classes.products_monitoring', 1)]) }}"
data-bs="tooltip">
{{ __('Sauvegarder') }}
</button>
<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">
<i class="fa-solid fa-trash"></i>
</button>
@ -52,8 +52,8 @@
<div class="col text-center">
<button id="formSubmit"
type="submit"
class="btn btn-primary"
title="{{ __('Sauvegarder ce :model', ['model' => Str::singular(__('models.classes.products'))]) }}"
class="btn btn-success"
title="{{ __('Sauvegarder cette :model', ['model' => trans_choice('models.classes.products_monitoring', 1)]) }}"
data-bs="tooltip">
{{ __('Sauvegarder') }}
</button>

View file

@ -1,17 +1,18 @@
<div class="row">
{{-- TITLE --}}
<div class="col-12 col-md-6 mb-3">
<fieldset class="border h-100 bg-white p-3">
<legend>{{ __('Titre') }}</legend>
<fieldset class="h-100 border bg-white p-3">
<legend>{{ __('Nom') }}</legend>
<div class="row">
<div class="col-12 mb-3">
@include('partials.inputs.varchar', [
'id' => 'productName',
'label' => __('Titre du :model', ['model' => Str::singular(__('models.classes.products'))]),
'label' => __('Nom du :model', ['model' => Str::singular(__('models.classes.products'))]),
'hasWordCounter' => true,
'name' => 'name',
'required' => true,
'model' => $productModel ?? null,
'placeholder' => '',
'helper' => __('Doit décrire précisément le produit (3 caractères minimum, 255 maximum).'),
])
</div>
@ -20,17 +21,21 @@
</div>
{{-- BRANDS --}}
<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>
<div class="row">
<div class="col-12 mb-3">
@include('partials.inputs.bigint-foreign-id', [
'id' => 'productBrand',
'label' => trans(':model associé(e)', ['model' => Str::singular(trans('models.classes.brands'))]),
@include('partials.inputs.bigint-belongs-to', [
'name' => 'brand_id',
'model' => $productModel ?? null,
'associatedModels' => $brands,
'helper' => trans('Selectionnez un(e) :model parmis la liste.', ['model' => Str::singular(trans('models.classes.brands'))]),
'itemLimit' => [0, 1],
'itemsList' => $brands,
'storeRoute' => 'brands.store',
'labelField' => 'name',
'helper' => __('La marque associée au produit.'),
'placeholder' => '',
'require' => true,
'label' => __('Marque associée'),
])
</div>
</div>
@ -38,29 +43,34 @@
</div>
{{-- SOURCE --}}
<div class="col-12 mb-3">
<fieldset class="border h-100 bg-white p-3">
<legend>{{ __("Source") }}</legend>
<fieldset class="h-100 border bg-white p-3">
<legend>{{ __('Source') }}</legend>
@php
$data = [
'id' => 'productSource',
'label' => __('Source associé(e)'),
'label' => __('Source'),
'name' => 'source',
'model' => $productModel ?? null,
'sourceEnum' => \App\Enums\Products\SourceEnum::toArray(),
'productsFair' => $productsFair,
'productsShop' => $productsShop,
'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
<div class="source-inputs" data-json='@json($data)'></div>
@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>
</div>
{{-- GENCODE --}}
{{-- GENCOD --}}
<div class="col-12 col-md-6 mb-3">
<fieldset class="border h-100 bg-white p-3">
<legend>{{ __('GENCODE') }}</legend>
<fieldset class="h-100 border bg-white p-3">
<legend>{{ __('GENCOD') }}</legend>
<div class="row">
<div class="col-12">
@php
@ -69,10 +79,10 @@
'label' => __('Code'),
'name' => 'gencod',
'model' => $productModel ?? null,
'value' => old('gencod') ?? $productModel->gencod ?? null,
'value' => old('gencod') ?? ($productModel->gencod ?? null),
'maxlength' => 13,
'required' => false,
'helper' => __('Saisir une suite de 13 chiffres maximum.')
'helper' => __('Saisir une suite de 13 chiffres maximum.'),
];
@endphp
<div class="verification-code" data-json='@json($data)'></div>
@ -83,7 +93,7 @@
</div>
{{-- URL --}}
<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>
<div class="row">
<div class="col-12 mb-3">
@ -94,7 +104,7 @@
'name' => 'url',
'required' => false,
'model' => $productModel ?? null,
'placeholder' => __('https://google.com'),
'placeholder' => '',
'helper' => __('Doit être une url valide.'),
])
</div>
@ -103,33 +113,9 @@
</div>
{{-- IMAGE --}}
<div class="col-12 mb-3">
<fieldset class="border h-100 bg-white p-3">
<legend>{{ __('Image') }}</legend>
<fieldset class="h-100 border bg-white p-3">
<legend>{{ __('Visuel de référence') }}</legend>
<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">
@include('partials.inputs.varchar-picture', [
'id' => 'productImage',
@ -147,18 +133,32 @@
</div>
{{-- DOCUMENTS --}}
<div class="col-12 mb-3">
<fieldset class="border h-100 bg-white p-3">
<legend>{{ __('Documents') }}</legend>
<fieldset class="h-100 border bg-white p-3">
<legend>{{ __('Documents complémentaires') }}</legend>
<div class="row">
<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
$data = [
'id' => 'productDocs',
'name' => 'docs[]',
'value' => $productModel->docs ?? [],
'limit' => [0, 5],
'helper' => __('Doit respecter le format :format.', ['format' => '.pdf']),
'errors' => $errors->getBag('default')->getMessages()
'helper' => __('Doit respecter le format :format.', [
'format' => '.xls,.csv,.xlsx,.png,.jpg,.jpeg,.gif,.doc,.docx,.ppt,.pdf,.txt',
]),
'errors' => $errors->getBag('default')->getMessages(),
];
@endphp
<div class="multiple-documents-input" data-json='@json($data)'></div>
@ -168,7 +168,7 @@
</div>
{{-- CERTIFICATION --}}
<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>
<div class="row">
<div class="col-12 mb-3">
@ -179,7 +179,7 @@
'model' => $productModel ?? null,
'require' => false,
'associatedModels' => \App\Enums\Products\CertificationEnum::toArray(),
'helper' => trans('Selectionnez une Certification parmis la liste.'),
'helper' => trans('Selectionnez une Certification parmi la liste.'),
])
</div>
</div>
@ -187,18 +187,18 @@
</div>
{{-- ORIGINS --}}
<div class="col-12 col-md-6 mb-3">
<fieldset class="border h-100 bg-white p-3">
<legend>{{ __('Origines') }}</legend>
<fieldset class="h-100 border bg-white p-3">
<legend>{{ __('Origine') }}</legend>
<div class="row">
@include('partials.inputs.select-multiple-enum', [
'id' => 'productOrigins',
'label' => __('Origines associées'),
'label' => __('Origine associée'),
'name' => 'origins',
'itemsSelected' => old('origins', $productModel->origins ?? []),
'itemsList' => collect(\App\Enums\Products\OriginEnum::toArray())->values(),
'itemLimit' => [0, count(\App\Enums\Products\OriginEnum::toArray())],
'placeholder' => __('Recherchez une Origine a associer.'),
'helper' => __('Les Origines associées au produit.'),
'placeholder' => '',
'helper' => __('L\'origine associée au produit.'),
'require' => false,
])
</div>
@ -206,7 +206,7 @@
</div>
{{-- CLAIMS --}}
<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>
<div class="row">
@include('partials.inputs.select-multiple-enum', [
@ -219,13 +219,13 @@
'showSpecificItem' => [
'name' => 'specified_claim',
'label' => 'Précision de l\'allégation',
'placeholder' => 'Sans sucre ajouté',
'value' => $productModel->specified_claim ?? null,
'placeholder' => '',
'value' => old('specified_claim', $productModel->specified_claim ?? null),
'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,
'placeholder' => __('Recherchez une Allégation a associer.'),
'placeholder' => '',
'require' => false,
'helper' => __('Les Allégations associées au produit.'),
])
@ -234,7 +234,7 @@
</div>
{{-- DISTRIBUTION NETWORK --}}
<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>
<div class="row">
<div class="col-12 mb-3">
@ -243,9 +243,9 @@
'label' => __('Réseau de distribution associé'),
'name' => 'distribution_network',
'model' => $productModel ?? null,
'require' => true,
'required' => true,
'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>
@ -253,7 +253,7 @@
</div>
{{-- RAY --}}
<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>
<div class="row">
<div class="col-12 mb-3">
@ -262,9 +262,9 @@
'label' => __('Rayon associé'),
'name' => 'ray',
'model' => $productModel ?? null,
'require' => true,
'required' => true,
'associatedModels' => \App\Enums\Products\RayEnum::toArray(),
'helper' => trans('Selectionnez un Rayon parmis la liste.'),
'helper' => trans('Selectionnez un Rayon parmi la liste.'),
])
</div>
</div>
@ -272,7 +272,7 @@
</div>
{{-- RANGE --}}
<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>
<div class="row">
<div class="col-12 mb-3">
@ -281,9 +281,9 @@
'label' => __('Famille de produit associée'),
'name' => 'product_range',
'model' => $productModel ?? null,
'require' => true,
'required' => true,
'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>
@ -291,7 +291,7 @@
</div>
{{-- GEO AREA --}}
<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>
<div class="row">
<div class="col-12 mb-3">
@ -300,9 +300,9 @@
'label' => __('Zone géographique associée'),
'name' => 'geo_area',
'model' => $productModel ?? null,
'require' => true,
'required' => true,
'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>
@ -310,7 +310,7 @@
</div>
{{-- WEIGHT --}}
<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>
<div class="row">
<div class="col-12 mb-3">
@ -329,16 +329,17 @@
</div>
{{-- PVC --}}
<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>
<div class="row">
<div class="col-12 mb-3">
@include('partials.inputs.int-number', [
'id' => 'productWeight',
'id' => 'productPvc',
'label' => __('PVC de l\'UVC'),
'name' => 'pvc',
'model' => $productModel ?? null,
'min' => 0,
'step' => 0.01,
'required' => false,
'helper' => trans('Le prix doit être en euro (€).'),
])
@ -346,9 +347,51 @@
</div>
</fieldset>
</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 --}}
<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>
<div class="row">
<div class="col-12">
@ -358,62 +401,48 @@
'name' => 'comment',
'model' => $productModel ?? null,
'required' => false,
'helper' => __(' Doit décrire précisément le contenu du produit, requiert entre 50 et 160 caractères, balise <meta> description.'),
'placeholder' => __('Une description du contenu du produit pour les moteurs de recherche.'),
'helper' => __(
' Doit décrire précisément le contenu du produit, requiert entre 50 et 160 caractères, balise <meta> description.'),
'placeholder' => '',
])
</div>
</div>
</fieldset>
</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 --}}
<div class="col-12 mb-3">
@php
if (session()->getOldInput()['evaluation'] ?? null) {
foreach (session()->getOldInput()['evaluation'] as $key => $value) {
$commentOldModel[] = [
"evaluation" => session()->getOldInput()['evaluation'][$key] ?? null,
"aspect" => session()->getOldInput()['aspect'][$key] ?? null,
"taste" => session()->getOldInput()['taste'][$key] ?? null,
"odor" => session()->getOldInput()['odor'][$key] ?? null,
"texture" => session()->getOldInput()['texture'][$key] ?? null,
'evaluation' => session()->getOldInput()['evaluation'][$key] ?? null,
'aspect' => session()->getOldInput()['aspect'][$key] ?? null,
'taste' => session()->getOldInput()['taste'][$key] ?? null,
'odor' => session()->getOldInput()['odor'][$key] ?? null,
'texture' => session()->getOldInput()['texture'][$key] ?? null,
];
}
}
$data = [
'id' => 'productSource',
'title1' => __("Dégustation"),
'title1' => __('Dégustation'),
'label1' => __('Produit dégusté ?'),
'name1' => 'is_tasted',
'oldCheck' => boolval(old('is_tasted', null)),
'model' => $productModel ?? null,
'oldModel' => session()->getOldInput(),
'max' => 15,
'min' => 1,
'helper1' => __('Le produit a été dégusté.'),
'required1' => false,
'title2' => __("Commentaires"),
'title2' => __('Commentaires'),
'name2' => 'tasters_number',
'label2' => __('Nombre de dégustation'),
'helper2' => __('Doit être un chiffre ou un nombre (minimum 1 et maximum 15).'),
'required2' => false,
'commentModel' => $commentModel ?? [],
'commentOldModel' => $commentOldModel ?? [],
'errors' => $errors->getBag('default')->getMessages() ?? []
'errors' => $errors->getBag('default')->getMessages() ?? [],
];
@endphp
<div class="tasted-product" data-json='@json($data)'></div>
@ -429,5 +458,21 @@
event.preventDefault();
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>

View file

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

View file

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

View file

@ -12,173 +12,181 @@
<div class="col-12">
<div class="card">
<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') }}
</button>
</div>
<div class="border-top collapse" id="collapseProductsFilters">
<div class="card-body">
<form id="searchForm" action="{{ request()->url() }}">
{{-- ALWAYS RESET SEARCH TO PAGE 1 --}}
<input name="page" type="hidden" value="1">
<!-- GENERAL INFORMATIONS -->
<div class="row w-100 mx-auto">
<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 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">
<option value="0">{{ __('Marque') }}</option>
<option value="">{{ __('Marque') }}</option>
@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
</select>
</div>
<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>
<!-- 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">
<select class="form-select" name="source">
<option value="0">{{ __('Source') }}</option>
<option value="">{{ __('Source') }}</option>
@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
</select>
</div>
<div class="col-12 col-md-6 mt-1 m-md-0">
<input name="shop" type="text" class="form-control" placeholder="Magasin" value="{{ $queryParameters['shop'] ?? null }}">
<div class="col-12 col-md-6 m-md-0 mt-1">
<input class="form-control" name="shop" type="text" value="{{ request()->get('shop') }}"
placeholder="{{ __('Magasin') }}">
</div>
<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 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>
<!-- 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">
@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', [
'id' => 'productsClaim',
'name' => 'claims',
'itemsSelected' => $selectedItemList,
'itemsList' => $itemsList,
'itemLimit' => [0, count($itemsList)],
'itemsSelected' => collect(request()->get('claims', []))->filter(fn($value) => is_numeric($value)),
'itemsList' => collect(\App\Enums\Products\ClaimEnum::toArray())->values(),
'itemLimit' => [0, count(\App\Enums\Products\ClaimEnum::toArray())],
'placeholder' => __('Recherchez une Allégation'),
'simpleComponent' => true,
])
</div>
<div class="col-12 col-md-6 mt-1 m-md-0">
<input name="specified_claim" type="text" class="form-control" placeholder="Allégation complémentaire" value="{{ $queryParameters['specified_claim'] ?? null }}">
<div class="col-12 col-md-6 m-md-0 mt-1">
<input class="form-control" name="specified_claim" type="text"
value="{{ request()->get('specified_claim') }}" placeholder="Allégation complémentaire">
</div>
</div>
<!-- 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">
@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', [
'id' => 'productsOrigin',
'name' => 'origins',
'itemsSelected' => $selectedItemList,
'itemsList' => $itemsList,
'itemLimit' => [0, count($itemsList)],
'itemsSelected' => collect(request()->get('origins', []))->filter(
fn($value) => is_numeric($value)),
'itemsList' => collect(\App\Enums\Products\OriginEnum::toArray())->values(),
'itemLimit' => [0, count(\App\Enums\Products\OriginEnum::toArray())],
'placeholder' => __('Recherchez une Origine'),
'simpleComponent' => true,
])
</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">
<option value="0">{{ __('Certification') }}</option>
<option value="">{{ __('Certification') }}</option>
@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
</select>
</div>
<div class="col-12 col-md-6 mt-1">
<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)
<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
</select>
</div>
<div class="col-12 col-md-6 mt-1">
<select class="form-select" name="ray">
<option value="0">{{ __('Rayons') }}</option>
<option value="">{{ __('Rayons') }}</option>
@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
</select>
</div>
<div class="col-12 col-md-6 mt-1">
<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)
<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
</select>
</div>
<div class="col-12 col-md-6 mt-1">
<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)
<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
</select>
</div>
</div>
<!-- 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">
@php
$itemsList = collect($keywords)->map(function ($keyword) {
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();
return ['name' => $keyword->keyword_cat->value, 'label' => $keyword->label, 'value' => $keyword->id];
});
@endphp
@include('partials.inputs.select-multiple-enum', [
'id' => 'productsKeywordsn',
'id' => 'productsKeywords',
'name' => 'keywords',
'itemsSelected' => $selectedItemList,
'itemsList' => $itemsList,
'itemLimit' => [0, count($itemsList)],
'placeholder' => __('Recherchez un mot clé'),
'itemsSelected' => collect(request()->get('keywords', []))->filter(
fn($value) => is_numeric($value)),
'itemsList' => $itemsList->values(),
'itemLimit' => [0, $itemsList->count()],
'placeholder' => __('Recherchez une tendance'),
'simpleComponent' => true,
])
</div>
</div>
<!-- DATES -->
<div class="row w-100 pt-3 mt-3 mx-auto border-top">
<div class="col-12 col-md-6">
<div class="row w-100 border-top mx-auto mt-3 pt-3">
<div class="col-12 col-md-6 m-md-0 mt-1">
<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 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>
<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>
<!-- BUTTONS -->
<div class="d-flex justify-content-center input-group pt-3 mt-3 border-top">
<button type="submit" class="btn btn-primary" data-bs="tooltip" title="{{ __('Lancer la recherche') }}">
<div class="d-flex justify-content-center input-group border-top mt-3 pt-3">
<button class="btn btn-primary" data-bs="tooltip" type="submit" title="{{ __('Lancer la recherche') }}">
{{ __('Rechercher') }}
</button>
<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') }}">
<i class="fas fa-backspace"></i>
</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>
@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)
<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 }}
</option>
@endforeach

View file

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

View file

@ -2,8 +2,10 @@
<label for="{{ $name }}" class="col-form-label"><b>{{ $label }}@if(!empty($required)) * @endif</b></label>
@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)
<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 }}
</option>
@endforeach

View file

@ -1,5 +1,6 @@
<?php
use App\Http\Controllers\BrandController;
use App\Http\Controllers\DashboardController;
use App\Http\Controllers\ProductController;
use Illuminate\Support\Facades\Route;
@ -9,6 +10,10 @@ Route::get('/', [DashboardController::class, 'show'])->name('dashboard');
// * PRODUCTS
Route::resource('products', ProductController::class)->except(['show']);
// * BRANDS
Route::resource('brands', BrandController::class)->only(['store']);
Route::prefix('/product')
->name('products.')
->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 checker from "vite-plugin-checker";
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 vue from "@vitejs/plugin-vue";
export default defineConfig({
build: {
@ -41,6 +41,7 @@ export default defineConfig({
},
plugins: [
checker({
enableBuild: true,
vueTsc: {
root: ".",
tsconfigPath: "tsconfig.json",
@ -74,7 +75,7 @@ export default defineConfig({
"**/vendor/**/*.scss",
"**/vendor/**/*.css",
],
lintOnStart: true,
lintOnStart: false,
emitError: true,
emitWarning: true,
emitErrorAsWarning: false,
@ -84,7 +85,7 @@ export default defineConfig({
cache: false,
extensions: ["js", "ts"],
include: "**/*.ts",
lintOnStart: true,
lintOnStart: false,
emitWarning: true,
emitError: true,
failOnWarning: false,