Added brand creation in product form, fixed missing * when required, cleaning
This commit is contained in:
parent
8ce9d5734b
commit
16fe3773a9
13 changed files with 409 additions and 19 deletions
29
app/Http/Controllers/BrandController.php
Normal file
29
app/Http/Controllers/BrandController.php
Normal 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')]);
|
||||
});
|
||||
}
|
||||
}
|
||||
42
app/Http/Requests/Brands/StoreBrandRequest.php
Normal file
42
app/Http/Requests/Brands/StoreBrandRequest.php
Normal 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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -47,6 +47,7 @@ class StoreProductRequest extends FormRequest
|
|||
'picture_alt' => $this->name,
|
||||
'picture_title' => $this->name,
|
||||
'certification' => $this->certification == 0 ? null : $this->certification,
|
||||
'brand_id' => $this->brand[0]['id'],
|
||||
]);
|
||||
$this->mergePicture('picture');
|
||||
$this->mergeDocuments('docs');
|
||||
|
|
|
|||
272
resources/ts/components/BelongsToDropdown.vue
Normal file
272
resources/ts/components/BelongsToDropdown.vue
Normal file
|
|
@ -0,0 +1,272 @@
|
|||
<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="selectedItems"
|
||||
:autocomplete-items="filteredItems"
|
||||
:autocomplete-min-length="0"
|
||||
:max-tags="1"
|
||||
:placeholder="placeholder"
|
||||
@before-adding-tag="beforeTagSave"
|
||||
@tags-changed="changedTags"
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
v-if="!selectedItems.length"
|
||||
:name="name"
|
||||
type="hidden"
|
||||
value=""
|
||||
>
|
||||
<template
|
||||
v-for="(sItem, index) in selectedItems"
|
||||
:key="index"
|
||||
>
|
||||
<p class="text-danger m-0">
|
||||
{{ parseValidationInput(`${name}[${String(index)}][name]`, allErrors) }}
|
||||
</p>
|
||||
<input
|
||||
:name="`${name}[${String(index)}][id]`"
|
||||
type="hidden"
|
||||
:value="sItem.id"
|
||||
>
|
||||
<input
|
||||
:name="`${name}[${String(index)}][value]`"
|
||||
type="hidden"
|
||||
:value="sItem.id"
|
||||
>
|
||||
<input
|
||||
:name="`${name}[${String(index)}][name]`"
|
||||
type="hidden"
|
||||
:value="sItem.text"
|
||||
>
|
||||
<input
|
||||
:name="`${name}[${String(index)}][label]`"
|
||||
type="hidden"
|
||||
:value="sItem.text"
|
||||
>
|
||||
<input
|
||||
:name="`${name}[${String(index)}][title]`"
|
||||
type="hidden"
|
||||
:value="sItem.text"
|
||||
>
|
||||
</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.label = data.label;
|
||||
this.required = data.require;
|
||||
this.allItems = this.parseTags(data.itemsList);
|
||||
this.selectedItems = this.parseSelected(data.value);
|
||||
this.intStoreRoute = data.storeRoute ?? null;
|
||||
this.placeholder = data.placeholder;
|
||||
},
|
||||
data(): {
|
||||
allItems: Array<CustomTag>;
|
||||
selectedItems: Array<CustomTag>;
|
||||
name: string;
|
||||
item: string;
|
||||
message: string | null;
|
||||
intStoreRoute: string | null;
|
||||
placeholder: string;
|
||||
label: string;
|
||||
required: boolean;
|
||||
} {
|
||||
return {
|
||||
allItems: [],
|
||||
selectedItems: [],
|
||||
item: "",
|
||||
name: "",
|
||||
message: null,
|
||||
intStoreRoute: null,
|
||||
placeholder: "",
|
||||
label: "",
|
||||
required: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
parseTags(itemsToParse: Array<CustomTag>): Array<CustomTag> {
|
||||
return itemsToParse.map((item: CustomTag) => {
|
||||
item.text = String(item.label ?? item.name ?? item.title);
|
||||
item.slug = String(item.slug ?? slugify(item.text));
|
||||
return item;
|
||||
});
|
||||
},
|
||||
parseSelected(itemToParse: CustomTag): Array<CustomTag> {
|
||||
itemToParse.text = String(itemToParse.label ?? itemToParse.name ?? itemToParse.title);
|
||||
itemToParse.slug = String(itemToParse.slug ?? slugify(String(itemToParse.label ?? itemToParse.name ?? itemToParse.title)));
|
||||
return [itemToParse];
|
||||
},
|
||||
parseNewlyCreated(itemToParse: Array<CustomTag>, id: number): Array<CustomTag> {
|
||||
return itemToParse.map((item: CustomTag) => {
|
||||
item.id = id;
|
||||
item.slug = String(item.slug ?? slugify(item.text));
|
||||
return item;
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Add item
|
||||
* @param obj
|
||||
*/
|
||||
beforeTagSave(obj: VueTagsInput.IAddArgs): void {
|
||||
if (this.selectedItems.length < 1) {
|
||||
let customTag = this.tagExists(this.allItems, obj.tag);
|
||||
if (customTag) {
|
||||
obj.tag = customTag;
|
||||
this.selectedItems.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.parseTags([response.data.brand])[0];
|
||||
this.allItems.push(brandItem);
|
||||
this.selectedItems.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.selectedItems = 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>
|
||||
|
|
@ -63,13 +63,14 @@
|
|||
</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",
|
||||
|
|
@ -89,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;
|
||||
|
|
@ -109,6 +111,7 @@ export default defineComponent({
|
|||
waitingListObjects: Array<VueTagsInput.IAddArgs>;
|
||||
allErrors: Record<string, Array<string>>;
|
||||
message: string | null;
|
||||
intStoreRoute: string | null;
|
||||
} {
|
||||
return {
|
||||
name: "",
|
||||
|
|
@ -122,6 +125,7 @@ export default defineComponent({
|
|||
waitingListObjects: [],
|
||||
allErrors: {},
|
||||
message: null,
|
||||
intStoreRoute: null,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
|
|
@ -144,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.");
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@
|
|||
>
|
||||
<template v-if="intIsAdded">
|
||||
<FontAwesomeIcon icon="fa-solid fa-minus" />
|
||||
|
||||
<FontAwesomeIcon icon="fa-solid fa-file-pdf" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<FontAwesomeIcon icon="fa-solid fa-plus" />
|
||||
|
|
|
|||
|
|
@ -203,7 +203,6 @@ export default defineComponent({
|
|||
".source-select"
|
||||
) as HTMLSelectElement;
|
||||
this.intSelectedValue = isNaN(Number(select.value)) ? Number(oldValue) : Number(select.value) ;
|
||||
console.log("là", this.intSelectedValue);
|
||||
this.updateVue();
|
||||
});
|
||||
},
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
<b>{{ intLabel1 }} {{ intRequired1 ? "*" : "" }}</b>
|
||||
</label>
|
||||
<input
|
||||
@click="updateBoolean(null)"
|
||||
@click="updateBoolean()"
|
||||
class="form-check-input"
|
||||
:name="intName1"
|
||||
type="checkbox"
|
||||
|
|
@ -150,7 +150,7 @@ export default defineComponent({
|
|||
? this.intOldModel[this.intName1]
|
||||
: false
|
||||
) {
|
||||
this.updateBoolean(data.oldCheck);
|
||||
this.updateBoolean();
|
||||
} else {
|
||||
this.intIsTasted = data.oldCheck;
|
||||
}
|
||||
|
|
@ -161,8 +161,7 @@ export default defineComponent({
|
|||
this.intAllErrors = data.errors;
|
||||
},
|
||||
methods: {
|
||||
updateBoolean(test:any) {
|
||||
console.log(test);
|
||||
updateBoolean() {
|
||||
this.intIsTasted = this.intIsTasted ? false : true;
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -25,13 +25,16 @@
|
|||
<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'))]),
|
||||
'name' => 'brand_id',
|
||||
@include('partials.inputs.bigint-belongs-to', [
|
||||
'name' => 'brand',
|
||||
'model' => $productModel ?? null,
|
||||
'associatedModels' => $brands,
|
||||
'helper' => trans('Selectionnez un(e) :model parmi la liste.', ['model' => Str::singular(trans('models.classes.brands'))]),
|
||||
'itemLimit' => [0, 1],
|
||||
'itemsList' => $brands,
|
||||
'storeRoute' => 'brands.store',
|
||||
'helper' => __('La marque associée au produit.'),
|
||||
'placeholder' => '',
|
||||
'require' => true,
|
||||
'label' => __('Marque associée')
|
||||
])
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -199,7 +202,7 @@
|
|||
'name' => 'specified_claim',
|
||||
'label' => 'Précision de l\'allégation',
|
||||
'placeholder' => '',
|
||||
'value' => old('specified_claim', $productModel->claims ?? null),
|
||||
'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).')
|
||||
],
|
||||
|
|
@ -222,7 +225,7 @@
|
|||
'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 parmi la liste.'),
|
||||
])
|
||||
|
|
@ -241,7 +244,7 @@
|
|||
'label' => __('Rayon associé'),
|
||||
'name' => 'ray',
|
||||
'model' => $productModel ?? null,
|
||||
'require' => true,
|
||||
'required' => true,
|
||||
'associatedModels' => \App\Enums\Products\RayEnum::toArray(),
|
||||
'helper' => trans('Selectionnez un Rayon parmi la liste.'),
|
||||
])
|
||||
|
|
@ -260,7 +263,7 @@
|
|||
'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 parmi la liste.'),
|
||||
])
|
||||
|
|
@ -279,7 +282,7 @@
|
|||
'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 parmi la liste.'),
|
||||
])
|
||||
|
|
|
|||
15
resources/views/partials/inputs/bigint-belongs-to.blade.php
Normal file
15
resources/views/partials/inputs/bigint-belongs-to.blade.php
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
@php
|
||||
$data = [
|
||||
'name' => $name,
|
||||
'value' => old($name, collect($model->{$name}) ?? []) ?? [],
|
||||
'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])
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
@if(!empty($label))
|
||||
<label for="{{ $name }}" class="col-form-label"><b>{{ $label }} *</b></label>
|
||||
@endif
|
||||
{{-- @dd($model->{$name}) --}}
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -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 () {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue