Added brand creation in product form, fixed missing * when required, cleaning

This commit is contained in:
Noah Lancien 2023-09-06 16:03:31 +02:00 • committed by Jérémy Munsch
parent 8ce9d5734b
commit 16fe3773a9
No known key found for this signature in database
GPG key ID: 18361AF3BFEC0FEC
13 changed files with 409 additions and 19 deletions

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

@ -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

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

View 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>

View file

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

View file

@ -10,6 +10,8 @@
>
<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" />

View file

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

View file

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

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

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

View 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])

View file

@ -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>

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 () {