first upload all files

This commit is contained in:
NW
2023-06-11 13:14:03 +01:00
parent f14dbc52b5
commit c08b36d1b6
1705 changed files with 106852 additions and 0 deletions

View File

@@ -0,0 +1,23 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Define which assets will be available through the asset manager
|--------------------------------------------------------------------------
| These assets are registered on the asset manager
*/
'all_assets' => [
'admin.category.css' => ['module' => 'category:admin/css/category.css'],
'admin.category.js' => ['module' => 'category:admin/js/category.js'],
'admin.jstree.js' => ['module' => 'category:admin/js/jstree.js'],
],
/*
|--------------------------------------------------------------------------
| Define which default assets will always be included in your pages
| through the asset pipeline
|--------------------------------------------------------------------------
*/
'required_assets' => [],
];

View File

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

View File

@@ -0,0 +1,12 @@
<?php
use Modules\Category\Entities\Category;
$factory->define(Category::class, function (Faker\Generator $faker) {
return [
'name' => $faker->word(),
'slug' => $faker->slug(),
'on_navigation' => $faker->boolean(),
'is_active' => $faker->boolean(),
];
});

View File

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

View File

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

View File

@@ -0,0 +1,19 @@
<?php
namespace Modules\Category\Database\Seeders;
use Illuminate\Database\Seeder;
use Modules\Category\Entities\Category;
class CategoryDatabaseSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
factory(Category::class, 20)->create();
}
}

View File

@@ -0,0 +1,161 @@
<?php
namespace Modules\Category\Entities;
use TypiCMS\NestableTrait;
use Modules\Media\Entities\File;
use Modules\Support\Eloquent\Model;
use Modules\Media\Eloquent\HasMedia;
use Illuminate\Support\Facades\Cache;
use Modules\Product\Entities\Product;
use Modules\Support\Eloquent\Sluggable;
use Modules\Support\Eloquent\Translatable;
class Category extends Model
{
use Translatable, Sluggable, HasMedia, NestableTrait;
/**
* The relations to eager load on every query.
*
* @var array
*/
protected $with = ['translations'];
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = ['parent_id', 'slug', 'position', 'is_searchable', 'is_active'];
/**
* The attributes that should be hidden for serialization.
*
* @var array
*/
protected $hidden = ['translations'];
/**
* The attributes that should be cast to native types.
*
* @var array
*/
protected $casts = [
'is_searchable' => 'boolean',
'is_active' => 'boolean',
];
/**
* The attributes that are translatable.
*
* @var array
*/
protected $translatedAttributes = ['name'];
/**
* The attribute that will be slugged.
*
* @var string
*/
protected $slugAttribute = 'name';
/**
* Perform any actions required after the model boots.
*
* @return void
*/
protected static function booted()
{
static::addActiveGlobalScope();
}
public static function findBySlug($slug)
{
return static::with('files')->where('slug', $slug)->firstOrNew([]);
}
public function isRoot()
{
return $this->exists && is_null($this->parent_id);
}
public function url()
{
return route('categories.products.index', ['category' => $this->slug]);
}
public static function tree()
{
return Cache::tags('categories')
->rememberForever(md5('categories.tree:' . locale()), function () {
return static::with('files')
->orderByRaw('-position DESC')
->get()
->nest();
});
}
public static function treeList()
{
return Cache::tags('categories')->rememberForever(md5('categories.tree_list:' . locale()), function () {
return static::orderByRaw('-position DESC')
->get()
->nest()
->setIndent('¦–– ')
->listsFlattened('name');
});
}
public static function searchable()
{
return Cache::tags('categories')
->rememberForever(md5('categories.searchable:' . locale()), function () {
return static::where('is_searchable', true)
->get()
->map(function ($category) {
return [
'slug' => $category->slug,
'name' => $category->name,
];
});
});
}
public function products()
{
return $this->belongsToMany(Product::class, 'product_categories');
}
public function getLogoAttribute()
{
return $this->files->where('pivot.zone', 'logo')->first() ?: new File;
}
public function getBannerAttribute()
{
return $this->files->where('pivot.zone', 'banner')->first() ?: new File;
}
public function toArray()
{
$attributes = parent::toArray();
if ($this->relationLoaded('files')) {
$attributes += [
'logo' => [
'id' => $this->logo->id,
'path' => $this->logo->path,
'exists' => $this->logo->exists,
],
'banner' => [
'id' => $this->banner->id,
'path' => $this->banner->path,
'exists' => $this->banner->exists,
],
];
}
return $attributes;
}
}

View File

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

View File

@@ -0,0 +1,66 @@
<?php
namespace Modules\Category\Http\Controllers\Admin;
use Modules\Category\Entities\Category;
use Modules\Admin\Traits\HasCrudActions;
use Modules\Category\Http\Requests\SaveCategoryRequest;
class CategoryController
{
use HasCrudActions;
/**
* Model for the resource.
*
* @var string
*/
protected $model = Category::class;
/**
* Label of the resource.
*
* @var string
*/
protected $label = 'category::categories.category';
/**
* View path of the resource.
*
* @var string
*/
protected $viewPath = 'category::admin.categories';
/**
* Form requests for the resource.
*
* @var array|string
*/
protected $validation = SaveCategoryRequest::class;
/**
* Display the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function show($id)
{
return Category::with('files')->withoutGlobalScope('active')->find($id);
}
/**
* Destroy resources by given ids.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function destroy($id)
{
Category::withoutGlobalScope('active')
->findOrFail($id)
->delete();
return back()->withSuccess(trans('admin::messages.resource_deleted', ['resource' => $this->getLabel()]));
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace Modules\Category\Http\Controllers\Admin;
use Modules\Category\Entities\Category;
use Modules\Category\Services\CategoryTreeUpdater;
use Modules\Category\Http\Responses\CategoryTreeResponse;
class CategoryTreeController
{
/**
* Display category tree in json.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
$categories = Category::withoutGlobalScope('active')
->orderByRaw('-position DESC')
->get();
return new CategoryTreeResponse($categories);
}
/**
* Update category tree in storage.
*
* @return \Illuminate\Http\Response
*/
public function update()
{
CategoryTreeUpdater::update(request('category_tree'));
return trans('category::messages.category_order_saved');
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace Modules\Category\Http\Controllers;
use Modules\Category\Entities\Category;
class CategoryController
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
return view('public.categories.index', [
'categories' => Category::all()->nest(),
]);
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace Modules\Category\Http\Controllers;
use Modules\Product\Entities\Product;
use Modules\Category\Entities\Category;
use Modules\Product\Filters\ProductFilter;
use Modules\Product\Http\Controllers\ProductSearch;
class CategoryProductController
{
use ProductSearch;
/**
* Display a listing of the resource.
*
* @param string $slug
* @param \Modules\Product\Entities\Product $model
* @param \Modules\Product\Filters\ProductFilter $productFilter
* @return \Illuminate\Http\Response
*/
public function index($slug, Product $model, ProductFilter $productFilter)
{
request()->merge(['category' => $slug]);
if (request()->expectsJson()) {
return $this->searchProducts($model, $productFilter);
}
$category = Category::findBySlug($slug);
return view('public.products.index', [
'categoryName' => $category->name,
'categoryBanner' => $category->banner->path,
]);
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace Modules\Category\Http\Requests;
use Illuminate\Validation\Rule;
use Modules\Category\Entities\Category;
use Modules\Core\Http\Requests\Request;
class SaveCategoryRequest extends Request
{
/**
* Available attributes.
*
* @var string
*/
protected $availableAttributes = 'category::attributes';
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'name' => 'required',
'slug' => $this->getSlugRules(),
'is_active' => 'required|boolean',
'is_active' => 'required|boolean',
];
}
private function getSlugRules()
{
$rules = $this->route()->getName() === 'admin.categories.update'
? ['required']
: ['nullable'];
$slug = Category::withoutGlobalScope('active')->where('id', $this->id)->value('slug');
$rules[] = Rule::unique('categories', 'slug')->ignore($slug, 'slug');
return $rules;
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace Modules\Category\Http\Responses;
use Illuminate\Contracts\Support\Responsable;
class CategoryTreeResponse implements Responsable
{
/**
* Categories collection.
*
* @var \Illuminate\Database\Eloquent\Collection
*/
private $categories;
/**
* Create a new instance.
*
* @param \Illuminate\Database\Eloquent\Collection $categories
*/
public function __construct($categories)
{
$this->categories = $categories;
}
/**
* Create an HTTP response that represents the object.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function toResponse($request)
{
return response()->json($this->transform());
}
/**
* Transform the categories.
*
* @return \Illuminate\Support\Collection
*/
private function transform()
{
return $this->categories->map(function ($category) {
return [
'id' => $category->id,
'parent' => $category->parent_id ?: '#',
'text' => $category->name,
'data' => [
'position' => $category->position,
],
];
});
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Modules\Category\Providers;
use Modules\Support\Traits\AddsAsset;
use Illuminate\Support\ServiceProvider;
class CategoryServiceProvider extends ServiceProvider
{
use AddsAsset;
/**
* Bootstrap the application services.
*
* @return void
*/
public function boot()
{
$this->addAdminAssets('admin.categories.index', [
'admin.category.css', 'admin.jstree.js', 'admin.category.js',
'admin.media.css', 'admin.media.js',
]);
}
}

View File

@@ -0,0 +1,180 @@
import CategoryTree from './CategoryTree';
export default class {
constructor() {
let tree = $('.category-tree');
new CategoryTree(this, tree);
this.collapseAll(tree);
this.expandAll(tree);
this.addRootCategory();
this.addSubCategory();
$('#category-form').on('submit', this.submit);
window.admin.removeSubmitButtonOffsetOn('#image', '.category-details-tab li > a');
}
collapseAll(tree) {
$('.collapse-all').on('click', (e) => {
e.preventDefault();
tree.jstree('close_all');
});
}
expandAll(tree) {
$('.expand-all').on('click', (e) => {
e.preventDefault();
tree.jstree('open_all');
});
}
addRootCategory() {
$('.add-root-category').on('click', () => {
this.loading(true);
$('.add-sub-category').addClass('disabled');
$('.category-tree').jstree('deselect_all');
this.clear();
// Intentionally delay 150ms so that user feel new form is loaded.
setTimeout(this.loading, 150, false);
});
}
addSubCategory() {
$('.add-sub-category').on('click', () => {
let selectedId = $('.category-tree').jstree('get_selected')[0];
if (selectedId === undefined) {
return;
}
this.clear();
this.loading(true);
window.form.appendHiddenInput('#category-form', 'parent_id', selectedId);
// Intentionally delay 150ms so that user feel new form is loaded.
setTimeout(this.loading, 150, false);
});
}
fetchCategory(id) {
this.loading(true);
$('.add-sub-category').removeClass('disabled');
$.ajax({
type: 'GET',
url: route('admin.categories.show', id),
success: (category) => {
this.update(category);
this.loading(false);
},
error: (xhr) => {
error(xhr.responseJSON.message);
this.loading(false);
},
});
}
update(category) {
window.form.removeErrors();
$('.btn-delete').removeClass('hide');
$('.form-group .help-block').remove();
$('#confirmation-form').attr('action', route('admin.categories.destroy', category.id));
$('#id-field').removeClass('hide');
$('#id').val(category.id);
$('#name').val(category.name);
$('#slug').val(category.slug);
$('#slug-field').removeClass('hide');
$('.category-details-tab .seo-tab').removeClass('hide');
$('#is_searchable').prop('checked', category.is_searchable);
$('#is_active').prop('checked', category.is_active);
$('.logo .image-holder-wrapper').html(this.categoryImage('logo', category.logo));
$('.banner .image-holder-wrapper').html(this.categoryImage('banner', category.banner));
$('#category-form input[name="parent_id"]').remove();
}
categoryImage(fieldName, file) {
if (! file.exists) {
return this.imagePlaceholder();
}
return `
<div class="image-holder">
<img src="${file.path}">
<button type="button" class="btn remove-image" data-input-name="files[${fieldName}]"></button>
<input type="hidden" name="files[${fieldName}]" value="${file.id}">
</div>
`;
}
clear() {
$('#id-field').addClass('hide');
$('#id').val('');
$('#name').val('');
$('#slug').val('');
$('#slug-field').addClass('hide');
$('.category-details-tab .seo-tab').addClass('hide');
$('#is_searchable').prop('checked', false);
$('#is_active').prop('checked', false);
$('.logo .image-holder-wrapper').html(this.imagePlaceholder());
$('.banner .image-holder-wrapper').html(this.imagePlaceholder());
$('.btn-delete').addClass('hide');
$('.form-group .help-block').remove();
$('#category-form input[name="parent_id"]').remove();
$('.general-information-tab a').click();
}
imagePlaceholder() {
return `
<div class="image-holder placeholder">
<i class="fa fa-picture-o"></i>
</div>
`;
}
loading(state) {
if (state === true) {
$('.overlay.loader').removeClass('hide');
} else {
$('.overlay.loader').addClass('hide');
}
}
submit(e) {
let selectedId = $('.category-tree').jstree('get_selected')[0];
if (! $('#slug-field').hasClass('hide')) {
window.form.appendHiddenInput('#category-form', '_method', 'put');
$('#category-form').attr('action', route('admin.categories.update', selectedId));
}
e.currentTarget.submit();
}
}

View File

@@ -0,0 +1,73 @@
export default class {
constructor(categoryForm, selector) {
this.selector = selector;
$.jstree.defaults.dnd.touch = true;
$.jstree.defaults.dnd.copy = false;
this.fetchCategoryTree();
// On selecting a category.
selector.on('select_node.jstree', (e, node) => categoryForm.fetchCategory(node.selected[0]));
// Expand categories when jstree is loaded.
selector.on('loaded.jstree', () => selector.jstree('open_all'));
// On updating category tree.
this.selector.on('move_node.jstree', (e, data) => {
this.updateCategoryTree(data);
});
}
fetchCategoryTree() {
this.selector.jstree({
core: {
data: { url: route('admin.categories.tree') },
check_callback: true,
},
plugins: ['dnd'],
});
}
updateCategoryTree(data) {
this.loading(data.node, true);
$.ajax({
type: 'PUT',
url: route('admin.categories.tree.update'),
data: { category_tree: this.getCategoryTree() },
success: (message) => {
success(message);
this.loading(data.node, false);
},
error: (xhr) => {
error(xhr.responseJSON.message);
this.loading(data.node, false);
},
});
}
getCategoryTree() {
let categories = this.selector.jstree(true).get_json('#', { flat: true });
return categories.reduce((formatted, category) => {
return formatted.concat({
id: category.id,
parent_id: (category.parent === '#') ? null : category.parent,
position: category.data.position,
});
}, []);
}
loading(node, state) {
let nodeElement = this.selector.jstree().get_node(node, true);
if (state) {
$(nodeElement).addClass('jstree-loading');
} else {
$(nodeElement).removeClass('jstree-loading');
}
}
}

View File

@@ -0,0 +1,3 @@
import CategoryForm from './CategoryForm';
new CategoryForm();

View File

@@ -0,0 +1,37 @@
@import '~jstree/dist/themes/default/style';
.category-tree {
margin-left: -10px;
}
.add-root-category {
display: table;
}
.add-sub-category {
display: table;
margin: 5px 0 10px;
}
.rtl {
.jstree-default {
&.jstree-rtl .jstree-node {
background-position: 0% 1px;
}
}
}
#category-form .loading-spinner {
margin-top: 15px;
margin-bottom: 20px;
}
.category-details-tab {
margin: 0 -15px 0 15px;
}
@media screen and (max-width: 991px) {
.category-details-tab {
margin: 30px 0 0 -15px;
}
}

View File

@@ -0,0 +1,9 @@
<?php
return [
'id' => 'ID',
'name' => 'Name',
'slug' => 'URL',
'is_searchable' => 'Searchable',
'is_active' => 'Status',
];

View File

@@ -0,0 +1,23 @@
<?php
return [
'category' => 'Category',
'categories' => 'Categories',
'tree' => [
'add_root_category' => 'Add Root Category',
'add_sub_category' => 'Add Subcategory',
'collapse_all' => 'Collapse All',
'expand_all' => 'Expand All',
],
'tabs' => [
'general' => 'General',
'image' => 'Image',
'seo' => 'SEO',
],
'form' => [
'show_this_category_in_search_box' => 'Show this category in search box category list',
'enable_the_category' => 'Enable the category',
'logo' => 'Logo',
'banner' => 'Banner',
],
];

View File

@@ -0,0 +1,5 @@
<?php
return [
'category_order_saved' => 'Category order has been saved.',
];

View File

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

View File

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

View File

@@ -0,0 +1,107 @@
@extends('admin::layout')
@component('admin::components.page.header')
@slot('title', trans('category::categories.categories'))
<li class="active">{{ trans('category::categories.categories') }}</li>
@endcomponent
@section('content')
<div class="box box-default">
<div class="box-body clearfix">
<div class="col-lg-2 col-md-3">
<div class="row">
<button class="btn btn-default add-root-category">{{ trans('category::categories.tree.add_root_category') }}</button>
<button class="btn btn-default add-sub-category disabled">{{ trans('category::categories.tree.add_sub_category') }}</button>
<div class="m-b-10">
<a href="#" class="collapse-all">{{ trans('category::categories.tree.collapse_all') }}</a> |
<a href="#" class="expand-all">{{ trans('category::categories.tree.expand_all') }}</a>
</div>
<div class="category-tree"></div>
</div>
</div>
<div class="col-lg-10 col-md-9">
<div class="tab-wrapper category-details-tab">
<ul class="nav nav-tabs">
<li class="general-information-tab active"><a data-toggle="tab" href="#general-information">{{ trans('category::categories.tabs.general') }}</a></li>
@hasAccess('admin.media.index')
<li class="image-tab"><a data-toggle="tab" href="#image">{{ trans('category::categories.tabs.image') }}</a></li>
@endHasAccess
<li class="seo-tab hide"><a data-toggle="tab" href="#seo">{{ trans('category::categories.tabs.seo') }}</a></li>
</ul>
<form method="POST" action="{{ route('admin.categories.store') }}" class="form-horizontal" id="category-form" novalidate>
{{ csrf_field() }}
<div class="tab-content">
<div id="general-information" class="tab-pane fade in active">
<div class="row">
<div class="col-md-8">
<div id="id-field" class="hide">
{{ Form::text('id', trans('category::attributes.id'), $errors, null, ['disabled' => true]) }}
</div>
{{ Form::text('name', trans('category::attributes.name'), $errors, null, ['required' => true]) }}
{{ Form::checkbox('is_searchable', trans('category::attributes.is_searchable'), trans('category::categories.form.show_this_category_in_search_box'), $errors) }}
{{ Form::checkbox('is_active', trans('category::attributes.is_active'), trans('category::categories.form.enable_the_category'), $errors) }}
</div>
</div>
</div>
@if (auth()->user()->hasAccess('admin.media.index'))
<div id="image" class="tab-pane fade">
<div class="logo">
@include('media::admin.image_picker.single', [
'title' => trans('category::categories.form.logo'),
'inputName' => 'files[logo]',
'file' => (object) ['exists' => false],
])
</div>
<div class="banner">
@include('media::admin.image_picker.single', [
'title' => trans('category::categories.form.banner'),
'inputName' => 'files[banner]',
'file' => (object) ['exists' => false],
])
</div>
</div>
@endif
<div id="seo" class="tab-pane fade">
<div class="row">
<div class="col-md-8">
<div class="hide" id="slug-field">
{{ Form::text('slug', trans('category::attributes.slug'), $errors) }}
</div>
</div>
</div>
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<button type="submit" class="btn btn-primary" data-loading>
{{ trans('admin::admin.buttons.save') }}
</button>
<button type="button" class="btn btn-link text-red btn-delete p-l-0 hide" data-confirm>
{{ trans('admin::admin.buttons.delete') }}
</button>
</div>
</div>
</form>
</div>
</div>
</div>
<div class="overlay loader hide">
<i class="fa fa-refresh fa-spin"></i>
</div>
</div>
@endsection

View File

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

View File

@@ -0,0 +1,7 @@
<?php
use Illuminate\Support\Facades\Route;
Route::get('categories', 'CategoryController@index')->name('categories.index');
Route::get('categories/{category}/products', 'CategoryProductController@index')->name('categories.products.index');

View File

@@ -0,0 +1,95 @@
<?php
namespace Modules\Category\Services;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Cache;
use Modules\Category\Entities\Category;
class CategoryTreeUpdater
{
/**
* Update category tree order.
*
* @param array $tree
* @return void
*/
public static function update(array $tree)
{
list($ids, $parentIdCases, $positionCases, $params) = static::getDataForQuery($tree);
$sql = "UPDATE `categories` SET
`parent_id` = CASE `id` {$parentIdCases} END,
`position` = CASE `id` {$positionCases} END,
`updated_at` = ?
WHERE `id` IN ({$ids})";
DB::update($sql, $params);
Cache::tags('categories')->flush();
}
/**
* Get data for update query.
*
* @param array $tree
* @return array
*/
private static function getDataForQuery(array $tree)
{
$params = [];
$ids = [];
foreach (static::getAttributesList($tree) as $id => $values) {
foreach ($values as $column => $value) {
$cases[$column][] = "WHEN {$id} THEN ?";
$params[$column][] = $value;
}
$ids[] = $id;
}
return static::prepareData($ids, $cases, $params);
}
/**
* Get attributes list from given tree.
*
* @param array $tree
* @return array
*/
private static function getAttributesList(array $tree)
{
$attributes = [];
foreach ($tree as $position => $category) {
$attributes[$category['id']] = [
'parent_id' => $category['parent_id'],
'position' => $position,
];
}
return $attributes;
}
/**
* Prepare data for update query.
*
* @param array $ids
* @param array $cases
* @param array $params
* @return array
*/
private static function prepareData(array $ids, array $cases, array $params)
{
$ids = implode(',', $ids);
$parentIdCases = implode(' ', $cases['parent_id']);
$positionCases = implode(' ', $cases['position']);
$params = array_flatten($params);
$params[] = Carbon::now();
return [$ids, $parentIdCases, $positionCases, $params];
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace Modules\Category\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('category::sidebar.categories'), function (Item $item) {
$item->weight(10);
$item->route('admin.categories.index');
$item->authorize(
$this->auth->hasAccess('admin.categories.index')
);
});
});
});
}
}

View File

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

View File

@@ -0,0 +1,9 @@
{
"name": "Category",
"alias": "category",
"description": "The category module is responsible for managing categories.",
"priority": 70,
"providers": [
"Modules\\Category\\Providers\\CategoryServiceProvider"
]
}

View File

@@ -0,0 +1,7 @@
{
"name": "category-module",
"private": true,
"devDependencies": {
"jstree": "^3.3.9"
}
}

View File

@@ -0,0 +1,12 @@
let mix = require('laravel-mix');
let execSync = require('child_process').execSync;
mix.js(`${__dirname}/Resources/assets/admin/js/main.js`, `${__dirname}/Assets/admin/js/category.js`)
.scripts(`${__dirname}/node_modules/jstree/dist/jstree.js`, `${__dirname}/Assets/admin/js/jstree.js`)
.sass(`${__dirname}/Resources/assets/admin/sass/main.scss`, `${__dirname}/Assets/admin/css/category.css`)
.copy(`${__dirname}/node_modules/jstree/dist/themes/default/32px.png`, `${__dirname}/Assets/admin/css`)
.copy(`${__dirname}/node_modules/jstree/dist/themes/default/40px.png`, `${__dirname}/Assets/admin/css`)
.copy(`${__dirname}/node_modules/jstree/dist/themes/default/throbber.gif`, `${__dirname}/Assets/admin/css`)
.then(() => {
execSync(`npm run rtlcss ${__dirname}/Assets/admin/css/category.css ${__dirname}/Assets/admin/css/category.rtl.css`);
});

View File

@@ -0,0 +1,14 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
jquery@>=1.9.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.2.1.tgz#5c4d9de652af6cd0a770154a631bba12b015c787"
jstree@^3.3.9:
version "3.3.9"
resolved "https://registry.yarnpkg.com/jstree/-/jstree-3.3.9.tgz#62b47cad3c4fda390d021e5c4f98ee5b3365198a"
integrity sha512-jRIbhg+BHrIs1Wm6oiJt3oKTVBE6sWS0PCp2/RlkIUqsLUPWUYgV3q8LfKoi1/E+YMzGtP6BuK4okk+0mwfmhQ==
dependencies:
jquery ">=1.9.1"