¨4.0.1¨

This commit is contained in:
¨NW¨
2023-12-03 14:07:47 +00:00
parent c08b36d1b6
commit f35052522d
1112 changed files with 43019 additions and 24987 deletions

View File

@@ -0,0 +1,25 @@
<?php
namespace Modules\Variation\Admin;
use Modules\Admin\Ui\AdminTable;
use Yajra\DataTables\EloquentDataTable;
use Yajra\DataTables\Exceptions\Exception;
class VariationTable extends AdminTable
{
/**
* Make table response for the resource.
*
* @return EloquentDataTable
* @throws Exception
*/
public function make(): EloquentDataTable
{
return $this->newTable()
->editColumn('type', function ($variation) {
return trans("variation::variations.form.variation_types.{$variation->type}");
})
->removeColumn('values');
}
}

View File

@@ -0,0 +1,10 @@
<?php
return [
'admin.variations' => [
'index' => 'variation::permissions.variations.index',
'create' => 'variation::permissions.variations.create',
'edit' => 'variation::permissions.variations.edit',
'destroy' => 'variation::permissions.variations.destroy',
],
];

View File

@@ -0,0 +1,37 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateVariationsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up(): void
{
Schema::create('variations', function (Blueprint $table) {
$table->increments('id');
$table->string('uid')->unique();
$table->string('type');
$table->boolean('is_global')->default(true);
$table->integer('position')->unsigned()->nullable();
$table->softDeletes();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down(): void
{
Schema::dropIfExists('variations');
}
}

View File

@@ -0,0 +1,37 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateVariationTranslationsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up(): void
{
Schema::create('variation_translations', function (Blueprint $table) {
$table->increments('id');
$table->integer('variation_id')->unsigned();
$table->string('locale');
$table->string('name');
$table->unique(['variation_id', 'locale']);
$table->foreign('variation_id')->references('id')->on('variations')->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('variation_translations');
}
}

View File

@@ -0,0 +1,38 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateVariationValuesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('variation_values', function (Blueprint $table) {
$table->increments('id');
$table->string('uid')->unique();
$table->integer('variation_id')->unsigned()->index();
$table->string('value')->nullable();
$table->integer('position')->unsigned()->nullable();
$table->timestamps();
$table->foreign('variation_id')->references('id')->on('variations')->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('variation_values');
}
}

View File

@@ -0,0 +1,37 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateVariationValueTranslationsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up(): void
{
Schema::create('variation_value_translations', function (Blueprint $table) {
$table->increments('id');
$table->integer('variation_value_id')->unsigned();
$table->string('locale');
$table->string('label');
$table->unique(['variation_value_id', 'locale']);
$table->foreign('variation_value_id')->references('id')->on('variation_values')->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down(): void
{
Schema::dropIfExists('variation_value_translations');
}
}

View File

@@ -0,0 +1,174 @@
<?php
namespace Modules\Variation\Entities;
use Illuminate\Support\Collection;
use Modules\Support\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;
use Modules\Support\Eloquent\Translatable;
use Modules\Variation\Admin\VariationTable;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Variation extends Model
{
use Translatable, SoftDeletes;
/**
* Available variation types.
*
* @var array
*/
const TYPES = ['text', 'color', 'image'];
/**
* The relations to eager load on every query.
*
* @var array
*/
protected $with = ['translations', 'values'];
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = ['uid', 'type', 'is_global', 'position'];
/**
* The attributes that should be cast to native types.
*
* @var array
*/
protected $casts = [
'is_global' => 'boolean',
];
/**
* The attributes that should be mutated to dates.
*
* @var array
*/
protected $dates = ['deleted_at'];
/**
* The attributes that are translatable.
*
* @var array
*/
protected array $translatedAttributes = ['name'];
/**
* Perform any actions required after the model boots.
*
* @return void
*/
protected static function booted(): void
{
static::saved(function ($variation) {
if (request()->routeIs('admin.variations.*')) {
$variation->saveValuesForGlobal();
}
if (request()->routeIs('admin.products.*')) {
$variation->saveValuesForLocal();
}
});
}
/**
* Save values for the variation.
*
* @param array $values
*
* @return void
*/
public function saveValues(array $values = []): void
{
$ids = $this->getDeleteCandidates($values);
if ($ids->isNotEmpty()) {
$this->values()
->whereIn('id', $ids)
->delete();
}
$counter = 0;
foreach (array_reset_index($values) as $attributes) {
$attributes += ['position' => ++$counter];
$attributes += ['value' => $attributes['color'] ?? ''];
$this->values()->updateOrCreate(
[
'id' => array_get($attributes, 'id'),
],
$attributes,
);
}
}
/**
* Get the values for the variation.
*
* @return HasMany
*/
public function values(): HasMany
{
return $this->hasMany(VariationValue::class);
}
/**
* Scope a query to only include global variations.
*
* @param Builder $query
*
* @return Builder
*/
public function scopeGlobals(Builder $query): Builder
{
return $query->where('is_global', true);
}
/**
* Get table data for the resource
*
* @return VariationTable
*/
public function table(): VariationTable
{
return new VariationTable($this->newQuery()->globals());
}
protected function saveValuesForGlobal()
{
$this->saveValues(request('values', []));
}
protected function saveValuesForLocal()
{
$this->saveValues(
request('variations.' . $this->uid . '.values', [])
);
}
/**
* @param $values
*
* @return Collection
*/
private function getDeleteCandidates($values): Collection
{
return $this->values()
->pluck('id')
->diff(array_pluck($values, 'id'));
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace Modules\Variation\Entities;
use Modules\Support\Eloquent\TranslationModel;
class VariationTranslation extends TranslationModel
{
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = ['name'];
}

View File

@@ -0,0 +1,99 @@
<?php
namespace Modules\Variation\Entities;
use Modules\Support\Eloquent\Model;
use Modules\Media\Eloquent\HasMedia;
use Modules\Support\Eloquent\Translatable;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class VariationValue extends Model
{
use Translatable, HasMedia;
/**
* The relations to eager load on every query.
*
* @var array
*/
protected $with = ['translations'];
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = ['uid', 'value', 'position'];
/**
* The attributes that are translatable.
*
* @var array
*/
protected array $translatedAttributes = ['label'];
/**
* @var string[]
*/
protected $appends = ['color', 'image'];
/**
* @return mixed|null
*/
public function getColorAttribute(): mixed
{
return $this->value ?? null;
}
/**
* @return mixed|null
*/
public function getImageAttribute(): mixed
{
return $this->files()->first() ?? null;
}
/**
* @return BelongsTo
*/
public function variation(): BelongsTo
{
return $this->belongsTo(Variation::class);
}
protected function extractMediaFromRequest()
{
if (request()->routeIs('admin.variations.*')) {
return $this->extractMediaForGlobal();
}
if (request()->routeIs('admin.products.*')) {
return $this->extractMediaForLocal();
}
}
protected function extractMediaForGlobal()
{
if (request('type') === 'image') {
return [
'media' => [request('values.' . $this->uid . '.image')],
];
}
}
protected function extractMediaForLocal()
{
if ($this->variation->type === 'image') {
return [
'media' => [request('variations.' . $this->variation->uid . '.values.' . $this->uid . '.image')],
];
}
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace Modules\Variation\Entities;
use Modules\Support\Eloquent\TranslationModel;
class VariationValueTranslation extends TranslationModel
{
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = ['label'];
}

View File

@@ -0,0 +1,73 @@
<?php
namespace Modules\Variation\Http\Controllers\Admin;
use Illuminate\Contracts\View\View;
use Illuminate\Contracts\View\Factory;
use Modules\Admin\Traits\HasCrudActions;
use Modules\Variation\Entities\Variation;
use Illuminate\Contracts\Foundation\Application;
use Modules\Variation\Transformers\VariationResource;
use Modules\Variation\Http\Requests\SaveVariationRequest;
class VariationController
{
use HasCrudActions;
/**
* Model for the resource.
*
* @var string
*/
protected string $model = Variation::class;
/**
* Label of the resource.
*
* @var string
*/
protected string $label = 'variation::variations.variation';
/**
* View path of the resource.
*
* @var string
*/
protected string $viewPath = 'variation::admin.variations';
/**
* Form requests for the resource.
*
* @var array|string
*/
protected string|array $validation = SaveVariationRequest::class;
public function show($id): VariationResource
{
$entity = $this->getEntity($id);
return new VariationResource($entity);
}
/**
* Show the form for editing the specified resource.
*
* @param int $id
*
* @return Application|Factory|View
*/
public function edit($id): View|Factory|Application
{
$entity = $this->getEntity($id);
$variationResource = new VariationResource($entity);
return view("{$this->viewPath}.edit",
[
'variation' => $entity,
'variation_resource' => $variationResource->response()->content(),
]
);
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace Modules\Variation\Http\Requests;
use Illuminate\Validation\Rule;
use Modules\Core\Http\Requests\Request;
use Modules\Variation\Entities\Variation;
class SaveVariationRequest extends Request
{
/**
* Available attributes.
*
* @var string
*/
protected $availableAttributes = 'variation::attributes';
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'name' => 'required',
'type' => ['required', Rule::in(Variation::TYPES)],
'values' => 'array|min:1',
'values.*.label' => 'required|distinct',
'values.*.color' => 'required_if:type,color|regex:/^#(?:[0-9a-fA-F]{3}){1,2}$/',
'values.*.image' => 'required_if:type,image',
];
}
public function __validationData(): array
{
return request()
->merge([
'values' => $this->filter($this->values ?? []),
])
->all();
}
private function filter($values = [])
{
return array_filter($values, function ($value) {
if (!array_has($value, 'label')) {
return true;
}
return !is_null($value['label']);
});
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace Modules\Variation\Providers;
use Illuminate\Support\ServiceProvider;
use Illuminate\Http\Resources\Json\JsonResource;
class VariationServiceProvider extends ServiceProvider
{
/**
* Bootstrap the application services.
*
* @return void
*/
public function boot(): void
{
JsonResource::withoutWrapping();
}
}

View File

@@ -0,0 +1,68 @@
import Vue from "vue";
import VariationMixin from "./mixins/VariationMixin";
import { toaster } from "@admin/js/Toaster";
new Vue({
el: "#app",
mixins: [VariationMixin],
created() {
this.setFormDefaultData();
},
mounted() {
this.focusInitialField();
},
methods: {
setFormDefaultData() {
this.form = {
uid: this.uid(),
type: "",
values: [
{
uid: this.uid(),
image: {
id: null,
path: null,
},
},
],
};
},
focusInitialField() {
this.$nextTick(() => {
$("#name").trigger("focus");
});
},
submit() {
this.formSubmitting = true;
$.ajax({
type: "POST",
url: route("admin.variations.store"),
data: this.transformData(this.form),
dataType: "json",
success: (response) => {
toaster(response.message, {
type: "success",
});
this.resetForm();
this.errors.reset();
},
})
.catch((error) => {
this.errors.reset();
this.errors.record(error.responseJSON.errors);
this.scrollToFirstErrorField(this.$refs.form.elements);
})
.always(() => {
this.formSubmitting = false;
});
},
},
});

View File

@@ -0,0 +1,55 @@
import Vue from "vue";
import VariationMixin from "./mixins/VariationMixin";
import { toaster } from "@admin/js/Toaster";
new Vue({
el: "#app",
mixins: [VariationMixin],
created() {
this.form = this.prepareFormData(FleetCart.data["variation"]);
},
mounted() {
this.initColorPicker();
},
methods: {
prepareFormData(formData) {
formData.uid = this.uid();
formData.values.forEach((value) => {
value.uid = this.uid();
});
return formData;
},
submit() {
this.formSubmitting = true;
$.ajax({
type: "PUT",
url: route("admin.variations.update", this.form.id),
data: this.transformData(this.form),
dataType: "json",
success: (response) => {
toaster(response.message, {
type: "success",
});
this.errors.reset();
},
})
.catch((error) => {
this.errors.reset();
this.errors.record(error.responseJSON.errors);
this.scrollToFirstErrorField(this.$refs.form.elements);
})
.always(() => {
this.formSubmitting = false;
});
},
},
});

View File

@@ -0,0 +1,242 @@
import draggable from "vuedraggable";
import Errors from "@admin/js/Errors";
import Coloris from "@melloware/coloris";
export default {
components: {
draggable,
},
data: {
formSubmitting: false,
form: {},
errors: new Errors(),
},
computed: {
isEmptyVariationType() {
return this.form.type === "";
},
},
mounted() {
this.hideColorPicker();
},
methods: {
uid() {
return Math.random().toString(36).slice(3);
},
changeVariationType(value) {
const values = this.form.values;
if (value !== "" && values.length === 1) {
this.$nextTick(() => {
$(`#values-${values[0].uid}-label`).trigger("focus");
});
}
if (value === "text") {
values.forEach((value) => {
this.errors.clear(`values.${value.uid}.color`);
this.errors.clear(`values.${value.uid}.image`);
});
} else if (value === "color") {
values.forEach((value) => {
this.errors.clear(`values.${value.uid}.image`);
});
this.$nextTick(() => {
this.initColorPicker();
});
} else if (value === "image") {
values.forEach((value, index) => {
if (!value.image) {
this.$set(values[index], "image", {
id: null,
path: null,
});
}
});
values.forEach((value) => {
this.errors.clear(`values.${value.uid}.color`);
});
} else {
this.clearValueErrors();
}
},
addRow() {
const values = this.form.values;
const uid = this.uid();
values.push({
uid,
image: {
id: null,
path: null,
},
});
this.$nextTick(() => {
$(`#values-${uid}-label`).trigger("focus");
if (this.form.type === "color") {
this.initColorPicker();
}
});
},
addRowOnPressEnter(event, index) {
const values = this.form.values;
if (event.target.value === "") return;
if (values.length - 1 === index) {
this.addRow();
return;
}
if (index < values.length - 1) {
$(`#values-${values[index + 1].uid}-label`).trigger("focus");
}
},
deleteRow(index, uid) {
const values = this.form.values;
values.splice(index, 1);
if (values.length === 0) {
this.addRow();
}
this.clearValueRowErrors(uid);
this.updateColorThumbnails();
},
updateColorThumbnails() {
if (this.form.type !== "color") return;
const elements = document.querySelectorAll(".clr-field");
this.form.values.forEach((value, index) => {
elements[index].style.color = value.color || "";
});
},
initColorPicker() {
Coloris.init();
Coloris({
el: ".color-picker",
alpha: false,
rtl: FleetCart.rtl,
theme: "large",
wrap: true,
format: "hex",
selectInput: true,
swatches: [
"#D01C1F",
"#3AA845",
"#118257",
"#0A33AE",
"#0D46A0",
"#000000",
"#5F4C3A",
"#726E6E",
"#F6D100",
"#C0E506",
"#FF540A",
"#C5A996",
"#4B80BE",
"#A1C3DA",
"#C8BFC2",
"#A9A270",
],
});
},
hideColorPicker() {
$(document).on("click", "#clr-swatches button", (e) => {
$(e.currentTarget)
.parents("#clr-picker")
.removeClass("clr-open");
});
},
chooseImage(index, uid) {
let picker = new MediaPicker({ type: "image" });
picker.on("select", ({ id, path }) => {
this.errors.clear(`values.${uid}.image`);
this.form.values[index].image = {
id: +id,
path,
};
});
},
resetForm() {
this.setFormDefaultData();
this.focusInitialField();
},
clearValueErrors() {
Object.keys(this.errors.errors).forEach((key) => {
if (key.startsWith("values")) {
this.errors.clear(key);
}
});
},
clearValueRowErrors(uid) {
Object.keys(this.errors.errors).forEach((key) => {
if (key.startsWith(`values.${uid}`)) {
this.errors.clear(key);
}
});
},
scrollToFirstErrorField(elements) {
this.$nextTick(() => {
[...elements]
.find(
(el) => el.name === Object.keys(this.errors.errors)[0]
)
.focus();
});
},
transformData(data) {
const formData = JSON.parse(JSON.stringify(data));
const PATHS = {
text: ["id", "uid", "label"],
color: ["id", "uid", "label", "color"],
image: ["id", "uid", "label", "image"],
};
if (formData.type === "") {
formData.values = [];
return formData;
}
formData.values = formData.values.reduce((accumulator, value) => {
value = _.pick(value, PATHS[formData.type]);
if (formData.type === "image") {
value.image = value.image.id;
}
return { ...accumulator, [value.uid]: value };
}, {});
return formData;
},
},
};

View File

@@ -0,0 +1,127 @@
/*rtl:begin:ignore*/
@import "@melloware/coloris/dist/coloris.css";
/*rtl:end:ignore*/
.form {
.has-variation-type {
margin-bottom: 10px;
}
}
.clr-field {
width: 100%;
button {
top: 4px;
right: 4px;
bottom: 4px;
height: auto;
width: 28px;
border-radius: 3px;
transform: none;
}
input {
padding-right: 40px;
}
}
.variations-group {
.variation-values {
margin-top: 15px;
}
}
.variation-values {
.table-responsive {
margin-bottom: 15px;
}
.options {
&.type-text {
.option-row {
td {
&:nth-child(2) {
width: 100%;
min-width: 180px;
}
}
}
}
}
.options {
&.type-color,
&.type-image {
.option-row {
td {
&:nth-child(2) {
width: 75%;
min-width: 160px;
}
&:nth-child(3) {
width: 25%;
min-width: 135px;
.image-holder {
height: 36px;
width: 36px;
margin: 0;
> i {
font-size: 20px;
}
}
}
}
}
}
}
}
@media screen and (min-width: 1200px) {
.variation-values {
.options {
&.type-color {
.option-row {
td:nth-child(3) {
min-width: 200px;
}
}
}
}
}
}
@media screen and (max-width: 767px) {
.box-body {
padding: 15px;
}
.form {
.has-variation-type {
margin-bottom: 5px;
}
> .row {
> div {
&:first-child {
margin-bottom: 10px;
}
}
&:last-child {
> div {
margin-bottom: 0;
}
}
}
}
.variations-group {
.variation-values {
margin-top: 20px;
}
}
}

View File

@@ -0,0 +1,10 @@
<?php
return [
'name' => 'Name',
'type' => 'Type',
'values' => 'Values',
'values.*.label' => 'Label',
'values.*.color' => 'Color',
'values.*.image' => 'Image',
];

View File

@@ -0,0 +1,10 @@
<?php
return [
'variations' => [
'index' => 'Index Variations',
'create' => 'Create Variations',
'edit' => 'Edit Variations',
'destroy' => 'Delete Variations',
],
];

View File

@@ -0,0 +1,5 @@
<?php
return [
'variations' => 'Variations',
];

View File

@@ -0,0 +1,32 @@
<?php
return [
'variation' => 'Variation',
'variations' => 'Variations',
'table' => [
'name' => 'Name',
'type' => 'Type',
],
'group' => [
'general' => 'General',
'values' => 'Values',
],
'form' => [
'name' => 'Name',
'type' => 'Type',
'values' => 'Values',
'label' => 'Label',
'color' => 'Color',
'image' => 'Image',
'variation_types' => [
'please_select' => 'Please Select',
'text' => 'Text',
'color' => 'Color',
'image' => 'Image',
],
'add_row' => 'Add Row',
],
];

View File

@@ -0,0 +1,38 @@
@extends('admin::layout')
@component('admin::components.page.header')
@slot('title', trans('admin::resource.create', ['resource' => trans('variation::variations.variation')]))
<li><a href="{{ route('admin.variations.index') }}">{{ trans('variation::variations.variations') }}</a></li>
<li class="active">{{ trans('admin::resource.create', ['resource' => trans('variation::variations.variation')]) }}</li>
@endcomponent
@section('content')
<div class="box">
<div class="box-body">
<div id="app">
<form
class="form"
@input="errors.clear($event.target.name)"
@submit.prevent
ref="form"
>
@include('variation::admin.variations.partials.general')
@include('variation::admin.variations.partials.values')
@include('variation::admin.variations.partials.submit')
</form>
</div>
</div>
</div>
@endsection
@include('variation::admin.variations.partials.scripts')
@push('globals')
@vite([
'Modules/Variation/Resources/assets/admin/sass/main.scss',
'Modules/Variation/Resources/assets/admin/js/create.js',
'Modules/Media/Resources/assets/admin/sass/main.scss',
'Modules/Media/Resources/assets/admin/js/main.js',
])
@endpush

View File

@@ -0,0 +1,43 @@
@extends('admin::layout')
@component('admin::components.page.header')
@slot('title', trans('admin::resource.edit', ['resource' => trans('variation::variations.variation')]))
@slot('subtitle', $variation->name)
<li><a href="{{ route('admin.variations.index') }}">{{ trans('variation::variations.variations') }}</a></li>
<li class="active">{{ trans('admin::resource.edit', ['resource' => trans('variation::variations.variation')]) }}</li>
@endcomponent
@section('content')
<div class="box">
<div class="box-body">
<div id="app">
<form
class="form"
@input="errors.clear($event.target.name)"
@submit.prevent
ref="form"
>
@include('variation::admin.variations.partials.general')
@include('variation::admin.variations.partials.values')
@include('variation::admin.variations.partials.submit')
</form>
</div>
</div>
</div>
@endsection
@include('variation::admin.variations.partials.scripts')
@push('globals')
<script type="module">
FleetCart.data['variation'] = {!! $variation_resource !!};
</script>
@vite([
'Modules/Variation/Resources/assets/admin/sass/main.scss',
'Modules/Variation/Resources/assets/admin/js/edit.js',
'Modules/Media/Resources/assets/admin/sass/main.scss',
'Modules/Media/Resources/assets/admin/js/main.js',
])
@endpush

View File

@@ -0,0 +1,38 @@
@extends('admin::layout')
@component('admin::components.page.header')
@slot('title', trans('variation::variations.variations'))
<li class="active">{{ trans('variation::variations.variations') }}</li>
@endcomponent
@component('admin::components.page.index_table')
@slot('buttons', ['create'])
@slot('resource', 'variations')
@slot('name', trans('variation::variations.variation'))
@slot('thead')
<tr>
@include('admin::partials.table.select_all')
<th>{{ trans('admin::admin.table.id') }}</th>
<th>{{ trans('variation::variations.table.name') }}</th>
<th>{{ trans('variation::variations.table.type') }}</th>
<th data-sort>{{ trans('admin::admin.table.updated') }}</th>
</tr>
@endslot
@endcomponent
@push('scripts')
<script type="module">
new DataTable('#variations-table .table', {
columns: [
{ data: 'checkbox', orderable: false, searchable: false, width: '3%' },
{ data: 'id', width: '5%' },
{ data: 'name', name: 'translations.name', orderable: false, defaultContent: '' },
{ data: 'type', name: 'type' },
{ data: 'updated', name: 'updated_at' },
],
});
</script>
@endpush

View File

@@ -0,0 +1,57 @@
<div class="row" :class="{ 'has-variation-type': !isEmptyVariationType }">
<div class="col-lg-2 col-sm-2">
<h5>{{ trans('variation::variations.group.general') }}</h5>
</div>
<div class="col-lg-7 col-sm-10">
<div class="row">
<div class="col-sm-6">
<div class="form-group">
<label for="name">
{{ trans('variation::attributes.name') }}
<span class="text-red">*</span>
</label>
<input type="text" name="name" id="name" class="form-control" v-model="form.name">
<span class="help-block text-red" v-if="errors.has('name')" v-text="errors.get('name')"></span>
</div>
</div>
<div class="col-sm-6">
<div class="form-group">
<label for="type">
{{ trans('variation::attributes.type') }}
<span class="text-red">*</span>
</label>
<select
name="type"
id="type"
class="form-control custom-select-black"
@change="changeVariationType($event.target.value)"
v-model="form.type"
>
<option value="">
{{ trans('variation::variations.form.variation_types.please_select') }}
</option>
<option value="text">
{{ trans('variation::variations.form.variation_types.text') }}
</option>
<option value="color">
{{ trans('variation::variations.form.variation_types.color') }}
</option>
<option value="image">
{{ trans('variation::variations.form.variation_types.image') }}
</option>
</select>
<span class="help-block text-red" v-if="errors.has('type')" v-text="errors.get('type')"></span>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,15 @@
@push('shortcuts')
<dl class="dl-horizontal">
<dt><code>b</code></dt>
<dd>{{ trans('admin::admin.shortcuts.back_to_index', ['name' => trans('variation::variations.variation')]) }}</dd>
</dl>
@endpush
@push('scripts')
<script type="module">
keypressAction([{
key: 'b',
route: "{{ route('admin.variations.index') }}"
}, ]);
</script>
@endpush

View File

@@ -0,0 +1,15 @@
<div class="row">
<div class="col-lg-7 col-lg-offset-2 col-md-12 text-right">
<button
type="button"
class="btn btn-primary"
:class="{
'btn-loading': formSubmitting
}"
:disabled="formSubmitting"
@click="submit"
>
{{ trans('admin::admin.buttons.save') }}
</button>
</div>
</div>

View File

@@ -0,0 +1,123 @@
<div v-cloak class="row" v-if="!isEmptyVariationType">
<div class="col-lg-2 col-sm-2">
<h5>{{ trans('variation::variations.group.values') }}</h5>
</div>
<div class="col-lg-7 col-sm-10">
<div class="variation-values clearfix">
<div class="table-responsive">
<table
class="options table table-bordered table-striped"
:class="form.type !== '' ? `type-${form.type}` : ''"
>
<thead>
<tr>
<th></th>
<th>
{{ trans('variation::variations.form.label') }}
<span class="text-red">*</span>
</th>
<th v-if="form.type === 'color'">
{{ trans('variation::variations.form.color') }}
<span class="text-red">*</span>
</th>
<th v-else-if="form.type === 'image'">
{{ trans('variation::variations.form.image') }}
<span class="text-red">*</span>
</th>
<th></th>
</tr>
</thead>
<tbody
is="draggable"
tag="tbody"
handle=".drag-handle"
animation="150"
:list="form.values"
@end="updateColorThumbnails"
>
<tr v-for="(value, index) in form.values" class="option-row" :key="index">
<td class="text-center">
<span class="drag-handle">
<i class="fa">&#xf142;</i>
<i class="fa">&#xf142;</i>
</span>
</td>
<td>
<input
type="text"
:name="`values.${value.uid}.label`"
:id="`values-${value.uid}-label`"
class="form-control"
@keyup.enter="addRowOnPressEnter($event, index)"
v-model="value.label"
>
<span
class="help-block text-red"
v-if="errors.has(`values.${value.uid}.label`)"
v-text="errors.get(`values.${value.uid}.label`)"
>
</span>
</td>
<td v-if="form.type === 'color'">
<div>
<input
type="text"
:name="`values.${value.uid}.color`"
:id="`values-${value.uid}-color`"
class="form-control color-picker"
v-model="value.color"
>
</div>
<span
class="help-block text-red"
v-if="errors.has(`values.${value.uid}.color`)"
v-text="errors.get(`values.${value.uid}.color`)"
>
</span>
</td>
<td v-else-if="form.type === 'image'">
<div class="d-flex">
<div
class="image-holder"
@click="chooseImage(index, value.uid)"
>
<template v-if="value.image.id">
<img :src="value.image.path" alt="variation image">
</template>
<img v-else src="{{ asset('build/assets/placeholder_image.png') }}" class="placeholder-image" alt="Placeholder image">
</div>
</div>
<span
class="help-block text-red"
v-if="errors.has(`values.${value.uid}.image`)"
v-text="errors.get(`values.${value.uid}.image`)"
>
</span>
</td>
<td class="text-center">
<button
type="button"
tabindex="-1"
class="btn btn-default delete-row"
@click="deleteRow(index, value.uid)"
>
<i class="fa fa-trash"></i>
</button>
</td>
</tr>
</tbody>
</table>
</div>
<button type="button" class="btn btn-default" @click="addRow">
{{ trans('variation::variations.form.add_row') }}
</button>
</div>
</div>
</div>

View File

@@ -0,0 +1,51 @@
<?php
use Illuminate\Support\Facades\Route;
Route::get('variations', [
'as' => 'admin.variations.index',
'uses' => 'VariationController@index',
'middleware' => 'can:admin.variations.index',
]);
Route::get('variations/create', [
'as' => 'admin.variations.create',
'uses' => 'VariationController@create',
'middleware' => 'can:admin.variations.create',
]);
Route::post('variations', [
'as' => 'admin.variations.store',
'uses' => 'VariationController@store',
'middleware' => 'can:admin.variations.create',
]);
Route::get('variations/{id}', [
'as' => 'admin.variations.show',
'uses' => 'VariationController@show',
'middleware' => 'can:admin.variations.index',
]);
Route::get('variations/{id}/edit', [
'as' => 'admin.variations.edit',
'uses' => 'VariationController@edit',
'middleware' => 'can:admin.variations.edit',
]);
Route::put('variations/{id}', [
'as' => 'admin.variations.update',
'uses' => 'VariationController@update',
'middleware' => 'can:admin.variations.edit',
]);
Route::delete('variations/{ids}', [
'as' => 'admin.variations.destroy',
'uses' => 'VariationController@destroy',
'middleware' => 'can:admin.variations.destroy',
]);
Route::get('variations/index/table', [
'as' => 'admin.variations.table',
'uses' => 'VariationController@table',
'middleware' => 'can:admin.variations.index',
]);

View File

@@ -0,0 +1,26 @@
<?php
namespace Modules\Variation\Sidebar;
use Maatwebsite\Sidebar\Item;
use Maatwebsite\Sidebar\Menu;
use Maatwebsite\Sidebar\Group;
use Modules\Admin\Sidebar\BaseSidebarExtender;
class SidebarExtender extends BaseSidebarExtender
{
public function extend(Menu $menu)
{
$menu->group(trans('admin::sidebar.content'), function (Group $group) {
$group->item(trans('product::sidebar.products'), function (Item $item) {
$item->item(trans('variation::sidebar.variations'), function (Item $item) {
$item->weight(25);
$item->route('admin.variations.index');
$item->authorize(
$this->auth->hasAccess('admin.variations.index')
);
});
});
});
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace Modules\Variation\Transformers;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class VariationResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param Request $request
*
* @return array
*/
public function toArray($request): array
{
return [
'id' => $this->id,
'uid' => $this->uid,
'name' => $this->name,
'type' => $this->type,
'is_global' => $this->is_global,
'values' => VariationValueResource::collection($this->values->sortBy('position')),
];
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace Modules\Variation\Transformers;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class VariationValueResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param Request $request
*
* @return array
*/
public function toArray($request): array
{
return [
'id' => $this->id,
'uid' => $this->uid,
'label' => $this->label,
'image' => $this->when(
condition: $this->variation->type === 'image',
value: fn () => [
'id' => $this->image?->id,
'path' => $this->image?->path,
]
),
'color' => $this->when(
condition: $this->variation->type === 'color',
value: $this->color
),
];
}
}

View File

@@ -0,0 +1,27 @@
{
"name": "fleetcart/variation",
"description": "The FleetCart Variation Module.",
"authors": [
{
"name": "Envay Soft",
"email": "envaysoft@gmail.com"
}
],
"require": {
"php": "^8.0.2"
},
"autoload": {
"psr-4": {
"Modules\\Variation\\": ""
}
},
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"config": {
"sort-packages": true
},
"minimum-stability": "dev"
}

View File

@@ -0,0 +1,9 @@
{
"name": "Variation",
"alias": "variation",
"description": "The FleetCart Variation Module.",
"priority": 90,
"providers": [
"Modules\\Variation\\Providers\\VariationServiceProvider"
]
}

View File

@@ -0,0 +1,8 @@
{
"name": "variation-module",
"private": true,
"dependencies": {
"@melloware/coloris": "^0.21.1",
"vuedraggable": "^2.24.3"
}
}