Compare commits

...

18 Commits

Author SHA1 Message Date
ba137edd1d category qosishsh to'g'irlandi 2026-04-29 15:06:12 +05:00
4297363cea category qosishsh to'g'irlandi 2026-04-29 15:00:28 +05:00
efc648bcd7 category qosishsh to'g'irlandi 2026-04-29 14:04:56 +05:00
a28f552f96 category qosishsh to'g'irlandi 2026-04-29 13:55:41 +05:00
ac2c25bd06 category qosishsh to'g'irlandi 2026-04-29 13:53:09 +05:00
6937deecbe category qosishsh to'g'irlandi 2026-04-29 13:47:19 +05:00
92227fd5f0 category qosishsh to'g'irlandi 2026-04-29 13:44:37 +05:00
e1f79a58ec category qosishsh to'g'irlandi 2026-04-29 13:40:10 +05:00
0254b616a9 minIO permission berildi 2026-04-28 22:31:41 +05:00
b9856f4bf8 storage url o'zgartirildi 2026-04-28 18:06:13 +05:00
a11b9c9100 storage url o'zgartirildi 2026-04-28 18:02:39 +05:00
b8a4efadc2 storage url o'zgartirildi 2026-04-28 17:53:06 +05:00
290dd2dddb storage url o'zgartirildi 2026-04-28 16:47:45 +05:00
79304de97a storage url o'zgartirildi 2026-04-28 16:31:43 +05:00
285be8edea storage url o'zgartirildi 2026-04-28 16:21:25 +05:00
519f97debf storage url o'zgartirildi 2026-04-28 16:17:40 +05:00
9cfa2e1664 storage url o'zgartirildi 2026-04-28 16:13:08 +05:00
0bf99a5e26 storage url o'zgartirildi 2026-04-28 16:00:18 +05:00
30 changed files with 774 additions and 288 deletions

View File

@@ -2,8 +2,8 @@
namespace App\Api; namespace App\Api;
use App\Support\Uploads;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Support\Facades\Storage;
use Intervention\Image\Facades\Image as Imagee; use Intervention\Image\Facades\Image as Imagee;
class ImageResize class ImageResize
@@ -54,8 +54,8 @@ class ImageResize
// 3. Upload thumb to S3/MinIO // 3. Upload thumb to S3/MinIO
$thumbKey = $this->thumbFolder($type) . '/' . $thumbFilename; $thumbKey = $this->thumbFolder($type) . '/' . $thumbFilename;
if (env('FILESYSTEM_DISK') === 's3') { if (config('filesystems.default') === 's3') {
Storage::disk('s3')->put($thumbKey, file_get_contents($tmpThumb)); Uploads::put($thumbKey, file_get_contents($tmpThumb), 's3');
} else { } else {
// Local: move to public/uploads/…/thumbs/… // Local: move to public/uploads/…/thumbs/…
$localDir = public_path(dirname($thumbKey)); $localDir = public_path(dirname($thumbKey));

View File

@@ -0,0 +1,56 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage;
use Throwable;
class StorageHealthCheck extends Command
{
protected $signature = 'storage:health-check
{disk? : Disk name, defaults to filesystems.default}
{--keep : Keep the test file so the public URL can be checked in a browser}';
protected $description = 'Check current storage disk, write/read/delete a test file, and print its public URL.';
public function handle(): int
{
$disk = $this->argument('disk') ?: config('filesystems.default');
$path = 'healthcheck/' . now()->format('YmdHis') . '-' . uniqid() . '.txt';
$this->line('default=' . config('filesystems.default'));
$this->line('disk=' . $disk);
$this->line('endpoint=' . (config("filesystems.disks.{$disk}.endpoint") ?: '-'));
$this->line('url=' . (config("filesystems.disks.{$disk}.url") ?: '-'));
$this->line('bucket=' . (config("filesystems.disks.{$disk}.bucket") ?: '-'));
$this->line('path_style=' . json_encode(config("filesystems.disks.{$disk}.use_path_style_endpoint")));
try {
$stored = Storage::disk($disk)->put($path, 'ok');
if (!$stored) {
$this->error("put=false path={$path}");
return self::FAILURE;
}
$exists = Storage::disk($disk)->exists($path);
$this->line('put=ok');
$this->line('exists=' . ($exists ? 'yes' : 'no'));
$this->line('public_url=' . Storage::disk($disk)->url($path));
if ($this->option('keep')) {
$this->warn('delete=skipped');
$this->warn('Open public_url in browser or curl it from outside the container.');
} else {
Storage::disk($disk)->delete($path);
$this->line('delete=ok');
}
} catch (Throwable $e) {
$this->error(get_class($e) . ': ' . $e->getMessage());
return self::FAILURE;
}
return self::SUCCESS;
}
}

View File

@@ -14,7 +14,9 @@ use App\Jobs\Dashboard\Category\Update as UpdateJob;
use App\Models\Characteristic; use App\Models\Characteristic;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Throwable;
class Controller extends ExController class Controller extends ExController
{ {
@@ -25,17 +27,46 @@ class Controller extends ExController
public function index() public function index()
{ {
$this->authorize('view', 'categories'); $this->authorize('view', 'categories');
$categories = Category::select('id', 'name->ru as category', 'position', 'parent_id', 'image') $categories = $this->categoryTree();
->where('parent_id', null)
->with(['children' => function ($parent) {
return $parent->select('id', 'name->ru as category', 'parent_id', 'position', 'image')->orderBy('position', 'asc')->with(['children' => function ($parent) {
return $parent->select('id', 'name->ru as category', 'parent_id', 'position', 'image')->orderBy('position', 'asc');
}]);
}])->orderBy('position', 'asc')->get();
return view('dashboard.category.index', compact('categories')); return view('dashboard.category.index', compact('categories'));
} }
private function categoryTree($parentId = null, array $visited = [])
{
return Category::select('id', 'name', 'position', 'parent_id', 'image')
->when($parentId === null, function ($query) {
$query->whereNull('parent_id');
}, function ($query) use ($parentId) {
$query->where('parent_id', $parentId);
})
->orderBy('position', 'asc')
->get()
->map(function (Category $category) use ($visited) {
if (in_array($category->id, $visited, true)) {
return [
'id' => $category->id,
'category' => $category->name['ru'] ?? $category->name['uz'] ?? '',
'position' => $category->position,
'parent_id' => $category->parent_id,
'image' => $category->image,
'image_url' => $category->image_url,
'children' => [],
];
}
return [
'id' => $category->id,
'category' => $category->name['ru'] ?? $category->name['uz'] ?? '',
'position' => $category->position,
'parent_id' => $category->parent_id,
'image' => $category->image,
'image_url' => $category->image_url,
'children' => $this->categoryTree($category->id, [...$visited, $category->id]),
];
});
}
/** /**
* @param StoreRequest $request * @param StoreRequest $request
* @return \Illuminate\Contracts\View\Factory|\Illuminate\Http\JsonResponse|\Illuminate\View\View * @return \Illuminate\Contracts\View\Factory|\Illuminate\Http\JsonResponse|\Illuminate\View\View
@@ -55,20 +86,39 @@ class Controller extends ExController
return view('dashboard.category.store', compact('brands', 'parent_categories')); return view('dashboard.category.store', compact('brands', 'parent_categories'));
} }
$category = $this->dispatchSync(new StoreJob($request)); try {
$category = DB::transaction(function () use ($request) {
$category = $this->dispatchSync(new StoreJob($request));
if (!empty($request->char)) { if (!empty($request->char)) {
foreach ($request->char as $char) { foreach ($request->char as $char) {
Characteristic::create([ Characteristic::create([
'name' => $char['name'], 'name' => $char['name'] ?? ['ru' => '', 'uz' => ''],
'type' => $char['type'], 'type' => $char['type'] ?? 'text',
'category_id' => $category->id, 'category_id' => $category->id,
'filter' => $char['filter'] == 'true' ? 1 : 0 'filter' => filter_var($char['filter'] ?? false, FILTER_VALIDATE_BOOLEAN)
]); ]);
} }
}
return $category;
});
} catch (Throwable $exception) {
Log::error('Category store failed', [
'message' => $exception->getMessage(),
'trace' => $exception->getTraceAsString(),
]);
return response()->json([
'status' => false,
'message' => 'Category could not be saved.',
'error' => config('app.debug') ? $exception->getMessage() : null,
], 500);
} }
$this->success(trans('admin.messages.created')); if ($category) {
$this->success(trans('admin.messages.created'));
}
return response()->json([ return response()->json([
'status' => true 'status' => true
@@ -103,39 +153,52 @@ class Controller extends ExController
return view('dashboard.category.update', compact('parent_categories', 'category', 'brands')); return view('dashboard.category.update', compact('parent_categories', 'category', 'brands'));
} }
$image = $request->getImage($category); try {
$image = $request->getImage($category);
$this->dispatchSync(new UpdateJob($category, $request, $image)); DB::transaction(function () use ($category, $request, $image) {
$this->dispatchSync(new UpdateJob($category, $request, $image));
if (!empty($request->char)) { if (!empty($request->char)) {
foreach ($request->char as $char) { foreach ($request->char as $char) {
if ($char['id'] == null || $char['id'] == 'null') { if ($char['id'] == null || $char['id'] == 'null') {
Characteristic::create([ Characteristic::create([
'name' => $char['name'], 'name' => $char['name'] ?? ['ru' => '', 'uz' => ''],
'type' => $char['type'], 'type' => $char['type'] ?? 'text',
'category_id' => $category->id, 'category_id' => $category->id,
'filter' => $char['filter'] == 'true' ? 1 : 0 'filter' => filter_var($char['filter'] ?? false, FILTER_VALIDATE_BOOLEAN)
]); ]);
} else { } else {
Characteristic::where('id', $char['id'])->update([ Characteristic::where('id', $char['id'])->update([
'name' => $char['name'], 'name' => $char['name'] ?? ['ru' => '', 'uz' => ''],
'type' => $char['type'], 'type' => $char['type'] ?? 'text',
'filter' => $char['filter'] == 'true' ? 1 : 0 'filter' => filter_var($char['filter'] ?? false, FILTER_VALIDATE_BOOLEAN)
]); ]);
}
}
} }
}
}
if (!empty($request->deletes['char'])) { if (!empty($request->deletes['char'])) {
$chars = Characteristic::whereIn('id', $request->deletes['char'])->get(); $chars = Characteristic::whereIn('id', $request->deletes['char'])->get();
foreach ($chars as $char) { foreach ($chars as $char) {
$char->values()->detach(); $char->values()->detach();
// foreach ($char->values as $value) { $char->delete();
// $value->delete(); }
// } }
$char->delete(); });
} } catch (Throwable $exception) {
Log::error('Category update failed', [
'category_id' => $category->id,
'message' => $exception->getMessage(),
'trace' => $exception->getTraceAsString(),
]);
return response()->json([
'status' => false,
'message' => 'Category could not be saved.',
'error' => config('app.debug') ? $exception->getMessage() : null,
], 500);
} }
$this->success(trans('admin.messages.updated')); $this->success(trans('admin.messages.updated'));
@@ -145,7 +208,6 @@ class Controller extends ExController
]); ]);
} }
/** /**
* @param Category $category * @param Category $category
* @return \Illuminate\Http\RedirectResponse * @return \Illuminate\Http\RedirectResponse

View File

@@ -50,7 +50,7 @@ class Controller extends ExController
if ($request->isMethod('get')) { if ($request->isMethod('get')) {
$this->authorize('create', 'compilations'); $this->authorize('create', 'compilations');
$categories = $this->categories->whereNull('parent_id')->orderBy('position')->get(); $categories = $this->categoryOptions();
return view('dashboard.compilations.store', compact('categories')); return view('dashboard.compilations.store', compact('categories'));
} }
@@ -86,7 +86,7 @@ class Controller extends ExController
$compilation->setRelation('products', $products); $compilation->setRelation('products', $products);
$categories = $this->categories->whereNull('parent_id')->orderBy('position')->get(); $categories = $this->categoryOptions();
return view('dashboard.compilations.update', compact('compilation', 'categories')); return view('dashboard.compilations.update', compact('compilation', 'categories'));
@@ -115,6 +115,11 @@ class Controller extends ExController
->when($categoryId > 0, function ($builder) use ($categoryId) { ->when($categoryId > 0, function ($builder) use ($categoryId) {
$categoryIds = $this->categoryIdsWithChildren($categoryId); $categoryIds = $this->categoryIdsWithChildren($categoryId);
if (empty($categoryIds)) {
$builder->whereRaw('1 = 0');
return;
}
$builder->whereHas('categories', function ($category) use ($categoryIds) { $builder->whereHas('categories', function ($category) use ($categoryIds) {
$category->whereIn('categories.id', $categoryIds); $category->whereIn('categories.id', $categoryIds);
}); });
@@ -146,25 +151,57 @@ class Controller extends ExController
private function categoryIdsWithChildren(int $categoryId): array private function categoryIdsWithChildren(int $categoryId): array
{ {
$category = Category::with('children.children')->find($categoryId); if (!Category::whereKey($categoryId)->exists()) {
if (!$category) {
return []; return [];
} }
$ids = [$category->id]; return $this->collectCategoryIds($categoryId);
foreach ($category->children as $child) {
$ids[] = $child->id;
foreach ($child->children as $grandChild) {
$ids[] = $grandChild->id;
}
}
return $ids;
} }
private function collectCategoryIds(int $categoryId, array $visited = []): array
{
if (in_array($categoryId, $visited, true)) {
return [];
}
$visited[] = $categoryId;
$ids = [$categoryId];
$children = Category::where('parent_id', $categoryId)
->orderBy('position')
->pluck('id');
foreach ($children as $childId) {
$ids = array_merge($ids, $this->collectCategoryIds((int) $childId, $visited));
}
return array_values(array_unique($ids));
}
private function categoryOptions($parentId = null, string $prefix = '')
{
return Category::select('id', 'name', 'parent_id')
->when($parentId === null, function ($query) {
$query->whereNull('parent_id');
}, function ($query) use ($parentId) {
$query->where('parent_id', $parentId);
})
->orderBy('position')
->get()
->flatMap(function (Category $category) use ($prefix) {
$ru = $category->name['ru'] ?? $category->name['uz'] ?? '';
$uz = $category->name['uz'] ?? $ru;
return collect([[
'id' => $category->id,
'name' => [
'ru' => $prefix . $ru,
'uz' => $prefix . $uz,
],
]])->merge($this->categoryOptions($category->id, $prefix . '- '));
})
->values();
}
/** /**
* @param Compilation $compilation * @param Compilation $compilation

View File

@@ -24,11 +24,12 @@ use App\Jobs\Dashboard\Product\Child as ChildJob;
use App\Jobs\Dashboard\Product\ChildUpdate as ChildUpdateJob; use App\Jobs\Dashboard\Product\ChildUpdate as ChildUpdateJob;
use App\Jobs\Dashboard\Product\Update as UpdateJob; use App\Jobs\Dashboard\Product\Update as UpdateJob;
use App\Jobs\Dashboard\Product\Deletes as DeletesJob; use App\Jobs\Dashboard\Product\Deletes as DeletesJob;
use Exception;
use Illuminate\Support\Facades\Date; use Illuminate\Support\Facades\Date;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Response; use Illuminate\Support\Facades\Response;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Maatwebsite\Excel\Facades\Excel; use Maatwebsite\Excel\Facades\Excel;
use Throwable;
class Controller extends ExController class Controller extends ExController
{ {
@@ -82,17 +83,7 @@ class Controller extends ExController
{ {
if ($request->isMethod('get')) { if ($request->isMethod('get')) {
$this->authorize('create', 'products'); $this->authorize('create', 'products');
$categories = $this->categories->select('id', 'name->ru as category') $categories = $this->categoryTree();
->where('parent_id', null)
->with([
'parents' => function ($parent) {
return $parent->select('id', 'name->ru as category', 'parent_id')->with([
'parents' => function ($parent) {
return $parent->select('id', 'name->ru as category', 'parent_id');
}
]);
}
])->get();
$brands = $this->brands->get(); $brands = $this->brands->get();
$colors = $this->colors->get(); $colors = $this->colors->get();
$measurement = Measurement::query()->get(); $measurement = Measurement::query()->get();
@@ -100,12 +91,26 @@ class Controller extends ExController
return view('dashboard.products.store', compact('categories', 'brands', 'colors', 'measurement')); return view('dashboard.products.store', compact('categories', 'brands', 'colors', 'measurement'));
} }
$product = $this->dispatchSync(StoreJob::fromRequest($request)); try {
$product = $this->dispatchSync(StoreJob::fromRequest($request));
$product->categories()->attach([$request->getCategoryID()]); $product->categories()->attach([$request->getCategoryID()]);
$this->charSync($product, $request->characteristics); $this->charSync($product, $request->characteristics);
$this->dispatchSync(new ChildJob($request, $product)); $this->dispatchSync(new ChildJob($request, $product));
} catch (Throwable $e) {
Log::error('Product store failed', [
'message' => $e->getMessage(),
'category_id' => $request->get('category_id'),
'trace' => $e->getTraceAsString(),
]);
return Response::json([
'status' => false,
'messages' => 'Product could not be saved.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
$this->success(trans('admin.messages.created')); $this->success(trans('admin.messages.created'));
@@ -139,14 +144,52 @@ class Controller extends ExController
} }
} }
private function categoryTree($parentId = null, array $visited = [])
{
return Category::select('id', 'name', 'parent_id')
->when($parentId === null, function ($query) {
$query->whereNull('parent_id');
}, function ($query) use ($parentId) {
$query->where('parent_id', $parentId);
})
->orderBy('position', 'asc')
->get()
->map(function (Category $category) use ($visited) {
$children = in_array($category->id, $visited, true)
? collect()
: $this->categoryTree($category->id, [...$visited, $category->id]);
return [
'id' => $category->id,
'category' => $category->name['ru'] ?? $category->name['uz'] ?? '',
'parent_id' => $category->parent_id,
'parents' => $children,
'$isDisabled' => false,
];
});
}
/** /**
* @param $id * @param $id
* @return array * @return array
*/ */
public function characteristics($id) public function characteristics($id)
{ {
if (!ctype_digit((string) $id)) {
return [
'status' => false,
'characteristics' => []
];
}
$category = Category::find($id); $category = Category::find($id);
if (empty($category)) {
return [
'status' => false,
'characteristics' => []
];
}
if (!empty($category->characteristics) && count($category->characteristics) > 0) { if (!empty($category->characteristics) && count($category->characteristics) > 0) {
$characteristics = $category->characteristics; $characteristics = $category->characteristics;
@@ -187,7 +230,7 @@ class Controller extends ExController
$arr[] = [ $arr[] = [
'id' => $cat['id'], 'id' => $cat['id'],
'category' => $cat['name']['ru'], 'category' => $cat['name']['ru'],
'$isDisabled' => true '$isDisabled' => false
]; ];
foreach ($cat['parents'] as $parent) { foreach ($cat['parents'] as $parent) {
@@ -196,7 +239,7 @@ class Controller extends ExController
$arr[] = [ $arr[] = [
'id' => $parent['id'], 'id' => $parent['id'],
'category' => $parent['name']['ru'], 'category' => $parent['name']['ru'],
'$isDisabled' => true '$isDisabled' => false
]; ];
foreach ($parent['parents'] as $paren) { foreach ($parent['parents'] as $paren) {
@@ -289,17 +332,7 @@ class Controller extends ExController
} }
$categories = $this->categories->select('id', 'name->ru as category') $categories = $this->categoryTree();
->where('parent_id', null)
->with([
'parents' => function ($parent) {
return $parent->select('id', 'name->ru as category', 'parent_id')->with([
'parents' => function ($parent) {
return $parent->select('id', 'name->ru as category', 'parent_id');
}
]);
}
])->get();
$brands = $this->brands->get(); $brands = $this->brands->get();
$measurement = Measurement::query()->get(); $measurement = Measurement::query()->get();
@@ -316,10 +349,18 @@ class Controller extends ExController
$this->dispatchSync(new ChildUpdateJob($request, $product)); $this->dispatchSync(new ChildUpdateJob($request, $product));
$this->charSync($product, $request->characteristics); $this->charSync($product, $request->characteristics);
$this->dispatchSync(new DeletesJob($request)); $this->dispatchSync(new DeletesJob($request));
} catch (Exception $e) { } catch (Throwable $e) {
Log::error('Product update failed', [
'product_id' => $product->id,
'message' => $e->getMessage(),
'category_id' => $request->get('category_id'),
'trace' => $e->getTraceAsString(),
]);
return Response::json([ return Response::json([
'status' => false, 'status' => false,
'messages' => $e->getMessage() 'messages' => 'Product could not be saved.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500); ], 500);
} }
@@ -390,7 +431,7 @@ class Controller extends ExController
$article_number = empty($request->get('article_number')) ? null : $request->get('article_number'); $article_number = empty($request->get('article_number')) ? null : $request->get('article_number');
if ($category) { if ($category) {
$categoryFind = Category::withTrashed()->find($category); $categoryFind = Category::find($category);
if ($categoryFind) { if ($categoryFind) {
list($categoryFind, $category_id) = $this->categoryQuery->getCategoriesAndCategoryMainId($categoryFind); list($categoryFind, $category_id) = $this->categoryQuery->getCategoriesAndCategoryMainId($categoryFind);
} else { } else {

View File

@@ -3,6 +3,7 @@
namespace App\Http\Controllers\Dashboard\Slider; namespace App\Http\Controllers\Dashboard\Slider;
use App\Models\Slider; use App\Models\Slider;
use App\Support\Uploads;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use App\Http\Controllers\Controller as ExController; use App\Http\Controllers\Controller as ExController;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
@@ -53,7 +54,7 @@ class Controller extends ExController
} }
if ($request->hasFile('image')) { if ($request->hasFile('image')) {
$path = $request->file('image')->store('uploads/sliders'); $path = Uploads::store($request->file('image'), 'uploads/sliders');
} else { } else {
$path = $slider->image; $path = $slider->image;
} }
@@ -77,7 +78,7 @@ class Controller extends ExController
if ($request->hasFile('image')) { if ($request->hasFile('image')) {
$path = $request->file('image')->store('uploads/sliders'); $path = Uploads::store($request->file('image'), 'uploads/sliders');
} }
$this->dispatchSync(StoreJob::fromRequest($request, $path)); $this->dispatchSync(StoreJob::fromRequest($request, $path));

View File

@@ -3,11 +3,19 @@
namespace App\Http\Requests\Dashboard\Category; namespace App\Http\Requests\Dashboard\Category;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Storage; use Illuminate\Http\UploadedFile;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use RuntimeException;
class Request extends FormRequest class Request extends FormRequest
{ {
protected function prepareForValidation()
{
if (in_array($this->get('parent_id'), [0, '0', '', 'null', 'NULL'], true)) {
$this->merge(['parent_id' => null]);
}
}
/** /**
* Get the validation rules that apply to the request. * Get the validation rules that apply to the request.
* *
@@ -22,7 +30,7 @@ class Request extends FormRequest
'name' => 'required|array', 'name' => 'required|array',
'name.*' => 'required|string', 'name.*' => 'required|string',
'image' => 'nullable|mimes:jpg,jpeg,png', 'image' => 'nullable|mimes:jpg,jpeg,png',
'parent_id' => 'nullable', 'parent_id' => 'nullable|integer|exists:categories,id',
'brands' => 'nullable|array', 'brands' => 'nullable|array',
'position' => 'nullable|numeric' 'position' => 'nullable|numeric'
]; ];
@@ -41,18 +49,31 @@ class Request extends FormRequest
public function getImage(): string public function getImage(): string
{ {
if ($this->hasFile('image')) { if ($this->hasFile('image')) {
if (env('FILESYSTEM_DISK') == 's3') { return $this->storeCategoryImage($this->file('image'));
$folder = "uploads/categories";
return (string) $this->file('image')->store($folder);
} else {
return $this->file('image')->store('uploads/categories', 'local');
}
} else { } else {
return 'null'; return 'null';
} }
} }
private function storeCategoryImage(UploadedFile $file): string
{
$relativeDirectory = 'uploads/categories';
$directory = public_path($relativeDirectory);
if (!is_dir($directory) && !mkdir($directory, 0775, true) && !is_dir($directory)) {
throw new RuntimeException("Category upload directory could not be created: {$directory}");
}
if (!is_writable($directory)) {
throw new RuntimeException("Category upload directory is not writable: {$directory}");
}
$fileName = $file->hashName();
$file->move($directory, $fileName);
return "{$relativeDirectory}/{$fileName}";
}
public function getPublished() public function getPublished()
{ {
if ($this->get('published') == 'true') { if ($this->get('published') == 'true') {
@@ -82,15 +103,16 @@ class Request extends FormRequest
public function getParentId() public function getParentId()
{ {
if ($this->get('parent_id') > 0) if ((int) $this->get('parent_id') > 0) {
return $this->get('parent_id'); return (int) $this->get('parent_id');
}
return null; return null;
} }
public function getPosition(): int public function getPosition(): int
{ {
return $this->get('position'); return (int) $this->get('position');
} }
public function getPopular(): bool public function getPopular(): bool

View File

@@ -4,11 +4,20 @@ namespace App\Http\Requests\Dashboard\Category;
use App\Models\Category; use App\Models\Category;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use RuntimeException;
class Update extends FormRequest class Update extends FormRequest
{ {
protected function prepareForValidation()
{
if (in_array($this->get('parent_id'), [0, '0', '', 'null', 'NULL'], true)) {
$this->merge(['parent_id' => null]);
}
}
/** /**
* Get the validation rules that apply to the request. * Get the validation rules that apply to the request.
* *
@@ -23,7 +32,7 @@ class Update extends FormRequest
'name' => 'required|array', 'name' => 'required|array',
'name.*' => 'required|string', 'name.*' => 'required|string',
'image' => 'nullable', 'image' => 'nullable',
'parent_id' => 'nullable', 'parent_id' => 'nullable|integer|exists:categories,id',
'popular' => 'nullable', 'popular' => 'nullable',
'brands' => 'nullable|array', 'brands' => 'nullable|array',
'position' => 'nullable|numeric' 'position' => 'nullable|numeric'
@@ -43,13 +52,32 @@ class Update extends FormRequest
public function getImage(Category $category): ?string public function getImage(Category $category): ?string
{ {
if ($this->hasFile('image')) { if ($this->hasFile('image')) {
Storage::delete($category->image); Storage::disk('local')->delete($category->image);
return (string) $this->file('image')->store('uploads/categories'); return $this->storeCategoryImage($this->file('image'));
} }
return $category->image; return $category->image;
} }
private function storeCategoryImage(UploadedFile $file): string
{
$relativeDirectory = 'uploads/categories';
$directory = public_path($relativeDirectory);
if (!is_dir($directory) && !mkdir($directory, 0775, true) && !is_dir($directory)) {
throw new RuntimeException("Category upload directory could not be created: {$directory}");
}
if (!is_writable($directory)) {
throw new RuntimeException("Category upload directory is not writable: {$directory}");
}
$fileName = $file->hashName();
$file->move($directory, $fileName);
return "{$relativeDirectory}/{$fileName}";
}
public function getFilterPower() public function getFilterPower()
{ {
if ($this->get('is_filter_power') == 'true') { if ($this->get('is_filter_power') == 'true') {
@@ -79,15 +107,16 @@ class Update extends FormRequest
public function getParentId() public function getParentId()
{ {
if ($this->get('parent_id') > 0) if ((int) $this->get('parent_id') > 0) {
return $this->get('parent_id'); return (int) $this->get('parent_id');
}
return null; return null;
} }
public function getPosition(): int public function getPosition(): int
{ {
return $this->get('position'); return (int) $this->get('position');
} }
public function getPopular(): bool public function getPopular(): bool

View File

@@ -2,9 +2,9 @@
namespace App\Http\Requests\Dashboard\Product; namespace App\Http\Requests\Dashboard\Product;
use App\Support\Uploads;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Storage;
use App\Api\ImageResize; use App\Api\ImageResize;
class Store extends FormRequest class Store extends FormRequest
@@ -38,7 +38,7 @@ class Store extends FormRequest
'brand_id' => 'required', 'brand_id' => 'required',
'measurement_id' => 'nullable|exists:measurements,id', 'measurement_id' => 'nullable|exists:measurements,id',
'category_id' => 'required', 'category_id' => 'required|integer|exists:categories,id',
'colors' => 'array|required', 'colors' => 'array|required',
'colors.*.color_id' => 'nullable', 'colors.*.color_id' => 'nullable',
'colors.*.sizes' => 'nullable|array', 'colors.*.sizes' => 'nullable|array',
@@ -71,7 +71,7 @@ class Store extends FormRequest
{ {
$folder = "uploads/posters/" . Carbon::now()->format('Y/m/d'); $folder = "uploads/posters/" . Carbon::now()->format('Y/m/d');
return (string) $this->file('poster')->store($folder); return Uploads::store($this->file('poster'), $folder);
} }
/** /**
@@ -81,7 +81,7 @@ class Store extends FormRequest
{ {
if ($this->hasFile('calc')) { if ($this->hasFile('calc')) {
$folder = "uploads/calc/" . Carbon::now()->format('Y/m/d'); $folder = "uploads/calc/" . Carbon::now()->format('Y/m/d');
return (string) $this->file('calc')->store($folder); return Uploads::store($this->file('calc'), $folder);
} }
return null; return null;
} }
@@ -90,7 +90,7 @@ class Store extends FormRequest
{ {
if ($this->hasFile('data_sheet')) { if ($this->hasFile('data_sheet')) {
$folder = "uploads/datasheet/" . Carbon::now()->format('Y/m/d'); $folder = "uploads/datasheet/" . Carbon::now()->format('Y/m/d');
return (string) $this->file('data_sheet')->store($folder); return Uploads::store($this->file('data_sheet'), $folder);
} }
return null; return null;
@@ -102,7 +102,7 @@ class Store extends FormRequest
public function getPosterThumb() public function getPosterThumb()
{ {
$image = $this->file('poster'); $image = $this->file('poster');
$tempPath = $image->store('temp', 'public'); $tempPath = Uploads::store($image, 'temp', 'public');
$resizer = new ImageResize(); $resizer = new ImageResize();
return $resizer->resize($tempPath, 322, 'posters', true); return $resizer->resize($tempPath, 322, 'posters', true);

View File

@@ -2,6 +2,7 @@
namespace App\Http\Requests\Dashboard\Product; namespace App\Http\Requests\Dashboard\Product;
use App\Support\Uploads;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
@@ -27,7 +28,7 @@ class Update extends FormRequest
'price' => 'required|numeric', 'price' => 'required|numeric',
'price_discount' => 'nullable', 'price_discount' => 'nullable',
'brand_id' => 'required', 'brand_id' => 'required',
'category_id' => 'required', 'category_id' => 'required|integer|exists:categories,id',
'popular' => 'nullable', 'popular' => 'nullable',
"calc" => [], "calc" => [],
'leader_of_sales' => 'nullable', 'leader_of_sales' => 'nullable',
@@ -65,7 +66,7 @@ class Update extends FormRequest
if ($this->hasFile('poster')) { if ($this->hasFile('poster')) {
Storage::delete($product->poster); Storage::delete($product->poster);
$folder = "uploads/posters/" . Carbon::now()->format('Y/m/d'); $folder = "uploads/posters/" . Carbon::now()->format('Y/m/d');
return (string) $this->file('poster')->store($folder); return Uploads::store($this->file('poster'), $folder);
} }
return $product->poster; return $product->poster;
@@ -81,7 +82,7 @@ class Update extends FormRequest
Storage::delete($product->calc); Storage::delete($product->calc);
} }
$folder = "uploads/calc/" . Carbon::now()->format('Y/m/d'); $folder = "uploads/calc/" . Carbon::now()->format('Y/m/d');
return (string) $this->file('calc')->store($folder); return Uploads::store($this->file('calc'), $folder);
} }
return $product->calc; return $product->calc;
} }
@@ -93,7 +94,7 @@ class Update extends FormRequest
Storage::delete($product->data_sheet); Storage::delete($product->data_sheet);
} }
$folder = "uploads/datasheet/" . Carbon::now()->format('Y/m/d'); $folder = "uploads/datasheet/" . Carbon::now()->format('Y/m/d');
return (string) $this->file('data_sheet')->store($folder); return Uploads::store($this->file('data_sheet'), $folder);
} }
return $product->data_sheet; return $product->data_sheet;
@@ -106,7 +107,7 @@ class Update extends FormRequest
{ {
if ($this->hasFile('poster')) { if ($this->hasFile('poster')) {
Storage::delete($product->poster_thumb); Storage::delete($product->poster_thumb);
$tempPath = $this->file('poster')->store('temp', 'public'); $tempPath = Uploads::store($this->file('poster'), 'temp', 'public');
$resizer = new ImageResize(); $resizer = new ImageResize();
return $resizer->resize($tempPath, 322, 'posters', true); return $resizer->resize($tempPath, 322, 'posters', true);
} }

View File

@@ -38,12 +38,13 @@ class Store
$category->published = $request->getPublished(); $category->published = $request->getPublished();
$category->is_filter_power = $request->getFilterPower(); $category->is_filter_power = $request->getFilterPower();
$category->credit = $request->getCredit(); $category->credit = $request->getCredit();
$category->keywords = $request->keywords; $category->descriptions = $request->descriptions ?? ['ru' => '', 'uz' => ''];
$category->title_seo = $request->title_seo; $category->keywords = $request->keywords ?? ['ru' => '', 'uz' => ''];
$category->title_seo = $request->title_seo ?? ['ru' => '', 'uz' => ''];
$category->save(); $category->save();
$category->brands()->sync($request->brands, false); $category->brands()->sync($request->brands ?? [], false);
return $category; return $category;
} }

View File

@@ -4,6 +4,7 @@ namespace App\Jobs\Dashboard\Product;
use App\Models\Product; use App\Models\Product;
use App\Models\Screen; use App\Models\Screen;
use App\Support\Uploads;
use Carbon\Carbon; use Carbon\Carbon;
use App\Api\ImageResize; use App\Api\ImageResize;
use App\Http\Requests\Dashboard\Product\Store as StoreRequest; use App\Http\Requests\Dashboard\Product\Store as StoreRequest;
@@ -54,10 +55,10 @@ class Child
$folder = Carbon::now()->format('Y/m/d'); $folder = Carbon::now()->format('Y/m/d');
// Store original // Store original
$path = $screen['image']->store("uploads/screens/{$folder}"); $path = Uploads::store($screen['image'], "uploads/screens/{$folder}");
// Store and resize thumb // Store and resize thumb
$tempPath = $screen['image']->store('temp', 'public'); $tempPath = Uploads::store($screen['image'], 'temp', 'public');
$thumbPath = $this->image->resize($tempPath, 322, 'screens', true); $thumbPath = $this->image->resize($tempPath, 322, 'screens', true);
$this->size = Storage::size($path); $this->size = Storage::size($path);

View File

@@ -4,6 +4,7 @@ namespace App\Jobs\Dashboard\Product;
use App\Models\Product; use App\Models\Product;
use App\Models\Screen; use App\Models\Screen;
use App\Support\Uploads;
use Carbon\Carbon; use Carbon\Carbon;
use App\Api\ImageResize; use App\Api\ImageResize;
@@ -83,10 +84,10 @@ class ChildUpdate
$folder = Carbon::now()->format('Y/m/d'); $folder = Carbon::now()->format('Y/m/d');
if ($screen['image']) { if ($screen['image']) {
// 1. Store original (S3 if enabled) // 1. Store original (S3 if enabled)
$path = $screen['image']->store("uploads/screens/{$folder}"); $path = Uploads::store($screen['image'], "uploads/screens/{$folder}");
// 2. Local temp for resizing // 2. Local temp for resizing
$tempPath = $screen['image']->store('temp', 'public'); $tempPath = Uploads::store($screen['image'], 'temp', 'public');
$thumbPath = $this->image->resize($tempPath, 322, 'screens', true); $thumbPath = $this->image->resize($tempPath, 322, 'screens', true);
Screen::create([ Screen::create([
@@ -94,7 +95,7 @@ class ChildUpdate
'path_thumb' => $thumbPath, 'path_thumb' => $thumbPath,
'name' => basename($path), 'name' => basename($path),
'product_id' => $child_id, 'product_id' => $child_id,
'size' => Storage::disk(env('FILESYSTEM_DISK'))->size($path) 'size' => Storage::size($path)
]); ]);
} }
} }

View File

@@ -2,6 +2,7 @@
namespace App\Jobs\Dashboard\Product; namespace App\Jobs\Dashboard\Product;
use App\Support\Uploads;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use App\Models\Screen as Screens; use App\Models\Screen as Screens;
@@ -38,17 +39,17 @@ class Screen
$folder = Carbon::now()->format('Y/m/d'); $folder = Carbon::now()->format('Y/m/d');
// 1. Store original (S3 if enabled) // 1. Store original (S3 if enabled)
$path = $screen->store("uploads/screens/original/{$folder}"); $path = Uploads::store($screen, "uploads/screens/original/{$folder}");
// 2. Local temp for resizing // 2. Local temp for resizing
$tempPath = $screen->store('temp', 'public'); $tempPath = Uploads::store($screen, 'temp', 'public');
$thumb = $this->img->resize($tempPath, 350, 'screens', true); $thumb = $this->img->resize($tempPath, 350, 'screens', true);
$screens = new Screens(); $screens = new Screens();
$screens->name = basename($path); $screens->name = basename($path);
$screens->path = $path; $screens->path = $path;
$screens->path_thumb = $thumb; $screens->path_thumb = $thumb;
$screens->size = Storage::disk(env('FILESYSTEM_DISK'))->size($path); $screens->size = Storage::size($path);
$screens->product_id = $this->id; $screens->product_id = $this->id;
$screens->save(); $screens->save();
} }

View File

@@ -58,13 +58,6 @@ class Update
private function syncCategories() private function syncCategories()
{ {
$cats = $this->product->categories()->get(); $this->product->categories()->sync([$this->request->getCategoryID()]);
$cats = array_map(function ($cat) {
return $cat['id'];
}, $cats->toArray());
$this->product->categories()->detach($cats);
$this->product->categories()->attach([$this->request->getCategoryID()]);
} }
} }

View File

@@ -147,6 +147,10 @@ class Category extends Model
public function getImage(): string public function getImage(): string
{ {
if (!in_array($this->image, ['null', null])) { if (!in_array($this->image, ['null', null])) {
if (Str::startsWith($this->image, 'uploads/categories/') && file_exists(public_path($this->image))) {
return asset($this->image);
}
return Storage::url($this->image); return Storage::url($this->image);
} }

View File

@@ -236,6 +236,10 @@ class Product extends Model
public function getPoster(): string public function getPoster(): string
{ {
if (!empty($this->poster)) { if (!empty($this->poster)) {
if (str_starts_with($this->poster, 'uploads/') && file_exists(public_path($this->poster))) {
return asset($this->poster);
}
return Storage::url($this->poster); return Storage::url($this->poster);
} }
@@ -245,6 +249,10 @@ class Product extends Model
public function getDataSheet() public function getDataSheet()
{ {
if (!empty($this->data_sheet) and ($this->data_sheet != null and $this->data_sheet != "null")) { if (!empty($this->data_sheet) and ($this->data_sheet != null and $this->data_sheet != "null")) {
if (str_starts_with($this->data_sheet, 'uploads/') && file_exists(public_path($this->data_sheet))) {
return asset($this->data_sheet);
}
return Storage::url($this->data_sheet); return Storage::url($this->data_sheet);
} }
return null; return null;
@@ -253,6 +261,10 @@ class Product extends Model
public function getPosterThumb(): string public function getPosterThumb(): string
{ {
if (!empty($this->poster_thumb)) { if (!empty($this->poster_thumb)) {
if (str_starts_with($this->poster_thumb, 'uploads/') && file_exists(public_path($this->poster_thumb))) {
return asset($this->poster_thumb);
}
return Storage::url($this->poster_thumb); return Storage::url($this->poster_thumb);
} }

View File

@@ -58,6 +58,10 @@ class Screen extends Model
public function getPath(): string public function getPath(): string
{ {
if (!empty($this->path)) { if (!empty($this->path)) {
if (str_starts_with($this->path, 'uploads/') && file_exists(public_path($this->path))) {
return asset($this->path);
}
return Storage::url($this->path); return Storage::url($this->path);
} }
@@ -70,6 +74,10 @@ class Screen extends Model
public function getPathThumb(): string public function getPathThumb(): string
{ {
if (!empty($this->path_thumb)) { if (!empty($this->path_thumb)) {
if (str_starts_with($this->path_thumb, 'uploads/') && file_exists(public_path($this->path_thumb))) {
return asset($this->path_thumb);
}
return Storage::url($this->path_thumb); return Storage::url($this->path_thumb);
} }

42
app/Support/Uploads.php Normal file
View File

@@ -0,0 +1,42 @@
<?php
namespace App\Support;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use RuntimeException;
class Uploads
{
public static function store(UploadedFile $file, string $path, ?string $disk = null): string
{
$diskName = $disk ?: config('filesystems.default');
$storedPath = $disk ? $file->store($path, $disk) : $file->store($path);
if (!$storedPath) {
throw new RuntimeException("File upload failed: {$path}");
}
if (!Storage::disk($diskName)->exists($storedPath)) {
throw new RuntimeException("Uploaded file was not found on disk [{$diskName}]: {$storedPath}");
}
return $storedPath;
}
public static function put(string $path, string $contents, ?string $disk = null): void
{
$stored = $disk
? Storage::disk($disk)->put($path, $contents)
: Storage::put($path, $contents);
if (!$stored) {
throw new RuntimeException("File upload failed: {$path}");
}
$diskName = $disk ?: config('filesystems.default');
if (!Storage::disk($diskName)->exists($path)) {
throw new RuntimeException("Uploaded file was not found on disk [{$diskName}]: {$path}");
}
}
}

View File

@@ -13,7 +13,7 @@ return [
| |
*/ */
'default' => env('FILESYSTEM_DISK', 'local'), 'default' => env('FILESYSTEM_DISK', env('FILESYSTEM_DRIVER', 's3')),
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
@@ -62,10 +62,15 @@ return [
'region' => env('MINIO_REGION', 'us-east-1'), 'region' => env('MINIO_REGION', 'us-east-1'),
'bucket' => env('MINIO_BUCKET'), 'bucket' => env('MINIO_BUCKET'),
'endpoint' => env('MINIO_ENDPOINT'), 'endpoint' => env('MINIO_ENDPOINT'),
'url' => env('MINIO_PUBLIC_URL', env('MINIO_ENDPOINT')), 'url' => env('MINIO_PUBLIC_URL')
? (str_ends_with(rtrim(env('MINIO_PUBLIC_URL'), '/'), '/' . trim(env('MINIO_BUCKET'), '/'))
? rtrim(env('MINIO_PUBLIC_URL'), '/')
: rtrim(env('MINIO_PUBLIC_URL'), '/') . '/' . trim(env('MINIO_BUCKET'), '/'))
: env('MINIO_ENDPOINT'),
'use_path_style_endpoint' => env('MINIO_USE_PATH_STYLE', true), 'use_path_style_endpoint' => env('MINIO_USE_PATH_STYLE', true),
'visibility' => 'public', 'visibility' => 'public',
'signature_version' => 'v4', 'signature_version' => 'v4',
'throw' => true,
], ],

View File

@@ -9,14 +9,21 @@ server {
root /quyoshli/public; root /quyoshli/public;
index index.php; index index.php;
# Final MinIO proxy fix - ^~ is mandatory to override regex blocks location = /quyoshli {
return 301 /quyoshli/;
}
# MinIO bucket public files. The ^~ prefix keeps image/css regex blocks from catching these URLs.
location ^~ /quyoshli/ { location ^~ /quyoshli/ {
proxy_http_version 1.1;
proxy_set_header Host $proxy_host; proxy_set_header Host $proxy_host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://quyoshli-minio:9100; proxy_pass http://minio:9100;
proxy_request_buffering off;
proxy_buffering off; proxy_buffering off;
add_header X-Storage-Proxy minio always;
} }
# try to serve file directly, fallback to start.php # try to serve file directly, fallback to start.php

View File

@@ -49,6 +49,10 @@
} }
} }
menu = document.querySelector('.main-menu-content'); menu = document.querySelector('.main-menu-content');
if (!activeEl || !menu) {
this.obj.update();
return;
}
activeEl = activeEl.getBoundingClientRect().top + menu.scrollTop; activeEl = activeEl.getBoundingClientRect().top + menu.scrollTop;
// If active element's top position is less than 2/3 (66%) of menu height than do not scroll // If active element's top position is less than 2/3 (66%) of menu height than do not scroll
if (activeEl > parseInt((menu.clientHeight * 2) / 3)) { if (activeEl > parseInt((menu.clientHeight * 2) / 3)) {

View File

@@ -50,7 +50,7 @@
<multiselect v-model="compilations" <multiselect v-model="compilations"
placeholder="Искать" placeholder="Искать"
label="name" label="name"
track-by="name" track-by="id"
:options="products" :options="products"
:option-height="104" :option-height="104"
:custom-label="customLabel" :custom-label="customLabel"
@@ -168,7 +168,7 @@
}), }),
mounted() { mounted() {
this.SearchProduct('');
}, },
methods: { methods: {

View File

@@ -50,7 +50,7 @@
<multiselect v-model="compilations" <multiselect v-model="compilations"
placeholder="Искать" placeholder="Искать"
label="name" label="name"
track-by="name" track-by="id"
:options="products" :options="products"
:option-height="104" :option-height="104"
:custom-label="customLabel" :custom-label="customLabel"
@@ -171,7 +171,7 @@
}, },
mounted() { mounted() {
this.SearchProduct('');
}, },
methods: { methods: {

View File

@@ -356,6 +356,7 @@
id="remove_cat" id="remove_cat"
type="button" type="button"
class="btn btn-secondary w-100" class="btn btn-secondary w-100"
@click="category.parent_id = 0"
> >
{{ {{
$t( $t(
@@ -792,7 +793,16 @@ export default {
.catch((error) => { .catch((error) => {
if (error.response) { if (error.response) {
this.error = true; this.error = true;
this.errors = error.response.data.errors; this.errors =
error.response.data.errors ||
error.response.data.details ||
[
[
error.response.data.error ||
error.response.data.message ||
"Server error",
],
];
} }
}); });
}, },

View File

@@ -359,6 +359,7 @@
id="remove_cat" id="remove_cat"
type="button" type="button"
class="btn btn-secondary w-100" class="btn btn-secondary w-100"
@click="category.parent_id = 0"
> >
{{ {{
$t( $t(
@@ -803,7 +804,16 @@ export default {
.catch((error) => { .catch((error) => {
if (error.response) { if (error.response) {
this.error = true; this.error = true;
this.errors = error.response.data.errors; this.errors =
error.response.data.errors ||
error.response.data.details ||
[
[
error.response.data.error ||
error.response.data.message ||
"Server error",
],
];
} }
}); });
}, },

View File

@@ -63,51 +63,28 @@
<multiselect <multiselect
:options="categories" :options="categories"
v-model="category.first" v-model="category.path[0]"
label="category" label="category"
@change=" @input="selectCategoryLevel(0)"
DetectCategory($event) track-by="id"
"
track-by="category"
></multiselect> ></multiselect>
<!-- //@change="getCharacteristics($event)"-->
</div> </div>
</div> </div>
<div <div
class="col-4" class="col-4"
v-if="category.two_view" v-for="level in categoryLevels"
:key="'category-level-' + level"
> >
<div class="form-group"> <div class="form-group">
<label>Суб категория *</label> <label>Суб категория *</label>
<multiselect <multiselect
:options=" :options="categoryOptions(level)"
category.first.parents v-model="category.path[level]"
"
v-model="category.two"
label="category" label="category"
@change="DetectCategoryTwo" @input="selectCategoryLevel(level)"
track-by="category" track-by="id"
></multiselect>
</div>
</div>
<div
class="col-4"
v-if="category.three_view"
>
<div class="form-group">
<label>Под категория *</label>
<multiselect
:options="
category.two.parents
"
v-model="category.three"
label="category"
@change="DetectCategory"
track-by="category"
></multiselect> ></multiselect>
</div> </div>
</div> </div>
@@ -1033,7 +1010,9 @@ export default {
watch: { watch: {
"products.category_id": function (newVal) { "products.category_id": function (newVal) {
this.getCharacteristics(newVal); if (newVal) {
this.getCharacteristics(newVal);
}
}, },
"category.first": function (newVal) { "category.first": function (newVal) {
@@ -1046,13 +1025,15 @@ export default {
"category.two": function (newVal) { "category.two": function (newVal) {
if (this.watch_count.two > 1) { if (this.watch_count.two > 1) {
if (newVal.parents.length > 0) { if (!newVal || !newVal.id) {
return;
}
if (newVal.parents && newVal.parents.length > 0) {
this.DetectCategoryTwo(); this.DetectCategoryTwo();
} else { } else {
if (newVal.id) { this.products.category_id = newVal.id;
this.product.category_id = newVal.id; this.getCharacteristics(newVal.id);
this.getCharacteristics(newVal);
}
} }
} }
@@ -1061,11 +1042,31 @@ export default {
"category.three": function (newVal) { "category.three": function (newVal) {
if (this.watch_count.three > 1) { if (this.watch_count.three > 1) {
if (!newVal || !newVal.id) {
return;
}
this.DetectCategoryThree(); this.DetectCategoryThree();
} }
this.watch_count.three = 2; this.watch_count.three = 2;
}, },
"category.path": {
deep: true,
handler() {
const selected = this.lastSelectedCategory();
if (!selected) {
this.products.category_id = null;
this.characteristics = [];
return;
}
this.products.category_id = selected.id;
this.getCharacteristics(selected.id);
},
},
}, },
data: function () { data: function () {
@@ -1098,6 +1099,7 @@ export default {
three: {}, three: {},
two_view: false, two_view: false,
three_view: false, three_view: false,
path: [],
}, },
watch_count: { watch_count: {
@@ -1128,6 +1130,22 @@ export default {
} }
}, },
computed: {
categoryLevels() {
const levels = [];
let current = this.category.path[0];
let level = 1;
while (current && current.parents && current.parents.length > 0) {
levels.push(level);
current = this.category.path[level];
level++;
}
return levels;
},
},
methods: { methods: {
getPosterPreview() { getPosterPreview() {
if (this.products.poster instanceof File) { if (this.products.poster instanceof File) {
@@ -1137,35 +1155,58 @@ export default {
}, },
setCategory() { setCategory() {
if (this.products.categories[0]) { if (this.products.categories[0]) {
if (this.products.categories[0].parent) { this.products.category_id = this.products.categories[0].id;
if (this.products.categories[0].parent.parent) { this.category.path = this.findCategoryPath(
this.category.two_view = true; this.categories,
this.category.three_view = true; this.products.categories[0].id
this.products.category_id = );
this.products.categories[0].id;
this.category.first =
this.products.categories[0].parent.parent;
this.category.two = this.products.categories[0].parent;
this.category.three = this.products.categories[0];
} else {
this.products.category_id =
this.products.categories[0].id;
this.category.first =
this.products.categories[0].parent;
this.category.two = this.products.categories[0];
this.category.two_view = true;
}
} else {
this.products.category_id = this.products.categories[0].id;
this.category.first = this.products.categories[0];
}
this.getCharacteristics(this.product.categories[0].id); this.getCharacteristics(this.product.categories[0].id);
} }
}, },
findCategoryPath(categories, id, path = []) {
for (const category of categories || []) {
const currentPath = [...path, category];
if (category.id === id) {
return currentPath;
}
const childPath = this.findCategoryPath(
category.parents || [],
id,
currentPath
);
if (childPath.length > 0) {
return childPath;
}
}
return [];
},
categoryOptions(level) {
const parent = this.category.path[level - 1];
return parent && parent.parents ? parent.parents : [];
},
selectCategoryLevel(level) {
this.category.path.splice(level + 1);
},
lastSelectedCategory() {
for (let i = this.category.path.length - 1; i >= 0; i--) {
if (this.category.path[i] && this.category.path[i].id) {
return this.category.path[i];
}
}
return null;
},
DetectCategory() { DetectCategory() {
this.category.two = { this.category.two = {
parents: [], parents: [],
@@ -1174,11 +1215,16 @@ export default {
this.category.three = {}; this.category.three = {};
this.category.three_view = false; this.category.three_view = false;
if (this.category.first.parents.length > 0) { if (!this.category.first || !this.category.first.id) {
this.products.category_id = null;
return;
}
this.products.category_id = this.category.first.id;
this.getCharacteristics(this.category.first.id);
if (this.category.first.parents && this.category.first.parents.length > 0) {
this.category.two_view = true; this.category.two_view = true;
} else {
this.getCharacteristics(this.category.first.id);
this.product.category_id = this.category.first.id;
} }
}, },
@@ -1186,17 +1232,35 @@ export default {
this.category.three = {}; this.category.three = {};
this.category.three_view = false; this.category.three_view = false;
if (this.category.two.parents.length > 0) { if (!this.category.two || !this.category.two.id) {
return;
}
this.products.category_id = this.category.two.id;
this.getCharacteristics(this.category.two.id);
if (this.category.two.parents && this.category.two.parents.length > 0) {
this.category.three_view = true; this.category.three_view = true;
} }
}, },
DetectCategoryThree() { DetectCategoryThree() {
if (!this.category.three || !this.category.three.id) {
return;
}
this.getCharacteristics(this.category.three.id); this.getCharacteristics(this.category.three.id);
this.product.category_id = this.category.three.id; this.products.category_id = this.category.three.id;
}, },
async getCharacteristics(id) { async getCharacteristics(id) {
id = id && id.id ? id.id : id;
if (!id) {
this.characteristics = [];
return;
}
const { data } = await axios.get( const { data } = await axios.get(
"/dashboard/products/characteristics/" + id "/dashboard/products/characteristics/" + id
); );
@@ -1387,7 +1451,8 @@ export default {
this.error = true; this.error = true;
this.errors = error.response.data.errors || { this.errors = error.response.data.errors || {
product: [ product: [
error.response.data.messages || error.response.data.error ||
error.response.data.messages ||
"Ошибка при сохранении", "Ошибка при сохранении",
], ],
}; };

View File

@@ -59,51 +59,28 @@
<multiselect <multiselect
:options="categories" :options="categories"
v-model="category.first" v-model="category.path[0]"
label="category" label="category"
@change=" @input="selectCategoryLevel(0)"
DetectCategory($event) track-by="id"
"
track-by="category"
></multiselect> ></multiselect>
<!-- //@change="getCharacteristics($event)"-->
</div> </div>
</div> </div>
<div <div
class="col-4" class="col-4"
v-if="category.two_view" v-for="level in categoryLevels"
:key="'category-level-' + level"
> >
<div class="form-group"> <div class="form-group">
<label>Суб категория *</label> <label>Суб категория *</label>
<multiselect <multiselect
:options=" :options="categoryOptions(level)"
category.first.parents v-model="category.path[level]"
"
v-model="category.two"
label="category" label="category"
@change="DetectCategoryTwo" @input="selectCategoryLevel(level)"
track-by="category" track-by="id"
></multiselect>
</div>
</div>
<div
class="col-4"
v-if="category.three_view"
>
<div class="form-group">
<label>Под категория *</label>
<multiselect
:options="
category.two.parents
"
v-model="category.three"
label="category"
@change="DetectCategory"
track-by="category"
></multiselect> ></multiselect>
</div> </div>
</div> </div>
@@ -1136,6 +1113,7 @@ export default {
three: {}, three: {},
two_view: false, two_view: false,
three_view: false, three_view: false,
path: [],
}, },
characteristic: false, characteristic: false,
@@ -1149,11 +1127,27 @@ export default {
uploadDisabled() { uploadDisabled() {
return this.files.length === 0; return this.files.length === 0;
}, },
categoryLevels() {
const levels = [];
let current = this.category.path[0];
let level = 1;
while (current && current.parents && current.parents.length > 0) {
levels.push(level);
current = this.category.path[level];
level++;
}
return levels;
},
}, },
watch: { watch: {
"product.category_id": function (newVal) { "product.category_id": function (newVal) {
this.getCharacteristics(newVal); if (newVal) {
this.getCharacteristics(newVal);
}
}, },
"category.first": function (newVal) { "category.first": function (newVal) {
@@ -1161,18 +1155,40 @@ export default {
}, },
"category.two": function (newVal) { "category.two": function (newVal) {
if (newVal.parents.length > 0) { if (!newVal || !newVal.id) {
return;
}
if (newVal.parents && newVal.parents.length > 0) {
this.DetectCategoryTwo(); this.DetectCategoryTwo();
} else { } else {
if (newVal.id) { this.product.category_id = newVal.id;
this.product.category_id = newVal.id;
}
} }
}, },
"category.three": function (newVal) { "category.three": function (newVal) {
if (!newVal || !newVal.id) {
return;
}
this.DetectCategoryThree(); this.DetectCategoryThree();
}, },
"category.path": {
deep: true,
handler() {
const selected = this.lastSelectedCategory();
if (!selected) {
this.product.category_id = null;
this.characteristics = [];
return;
}
this.product.category_id = selected.id;
this.getCharacteristics(selected.id);
},
},
}, },
methods: { methods: {
@@ -1304,11 +1320,38 @@ export default {
.catch((error) => { .catch((error) => {
if (error.response) { if (error.response) {
this.error = true; this.error = true;
this.errors = error.response.data.errors; this.errors = error.response.data.errors || {
product: [
error.response.data.error ||
error.response.data.messages ||
error.response.data.message ||
"Ошибка при сохранении",
],
};
} }
}); });
}, },
categoryOptions(level) {
const parent = this.category.path[level - 1];
return parent && parent.parents ? parent.parents : [];
},
selectCategoryLevel(level) {
this.category.path.splice(level + 1);
},
lastSelectedCategory() {
for (let i = this.category.path.length - 1; i >= 0; i--) {
if (this.category.path[i] && this.category.path[i].id) {
return this.category.path[i];
}
}
return null;
},
remaincharRUCount: function () { remaincharRUCount: function () {
if (this.product.short_body.ru.length > 300) { if (this.product.short_body.ru.length > 300) {
this.short_limit.ru = "Превышен лимит в 300 символов."; this.short_limit.ru = "Превышен лимит в 300 символов.";
@@ -1367,6 +1410,13 @@ export default {
}, },
async getCharacteristics(id) { async getCharacteristics(id) {
id = id && id.id ? id.id : id;
if (!id) {
this.characteristics = [];
return;
}
const { data } = await axios.get( const { data } = await axios.get(
"/dashboard/products/characteristics/" + id "/dashboard/products/characteristics/" + id
); );
@@ -1382,10 +1432,16 @@ export default {
this.category.three = {}; this.category.three = {};
this.category.three_view = false; this.category.three_view = false;
if (this.category.first.parents.length > 0) { if (!this.category.first || !this.category.first.id) {
this.product.category_id = null;
return;
}
this.product.category_id = this.category.first.id;
this.getCharacteristics(this.category.first.id);
if (this.category.first.parents && this.category.first.parents.length > 0) {
this.category.two_view = true; this.category.two_view = true;
} else {
this.product.category_id = this.category.first.id;
} }
}, },
@@ -1393,12 +1449,23 @@ export default {
this.category.three = {}; this.category.three = {};
this.category.three_view = false; this.category.three_view = false;
if (this.category.two.parents.length > 0) { if (!this.category.two || !this.category.two.id) {
return;
}
this.product.category_id = this.category.two.id;
this.getCharacteristics(this.category.two.id);
if (this.category.two.parents && this.category.two.parents.length > 0) {
this.category.three_view = true; this.category.three_view = true;
} }
}, },
DetectCategoryThree() { DetectCategoryThree() {
if (!this.category.three || !this.category.three.id) {
return;
}
this.product.category_id = this.category.three.id; this.product.category_id = this.category.three.id;
}, },

View File

@@ -21,6 +21,23 @@
<!-- JavaScript for delete confirmation --> <!-- JavaScript for delete confirmation -->
<script> <script>
window.PreviewImage = window.PreviewImage || function(event) {
var input = event && event.target
? event.target
: document.activeElement || document.getElementById('uploadImage') || document.getElementById('poster');
var preview = document.getElementById('uploadPreview');
if (!input || !input.files || !input.files[0] || !preview) {
return;
}
var reader = new FileReader();
reader.onload = function(readerEvent) {
preview.src = readerEvent.target.result;
};
reader.readAsDataURL(input.files[0]);
};
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
const deleteForms = document.querySelectorAll('.delete-alert'); const deleteForms = document.querySelectorAll('.delete-alert');

View File

@@ -50,25 +50,14 @@ Route::get('/', function () {
return view('welcome'); return view('welcome');
}); });
Route::middleware('locale')->group(function () { // Public site category controllers were removed. Keep the route names for legacy
// Route::get('/', [SiteMainPageController::class, 'index'])->name('site.main.page'); // link generation, but avoid old string-controller routes that break Laravel 11.
// Route::get('locale/{lang}', [SiteLocaleController::class, 'setLocale'])->name('site.setLocale'); Route::middleware('locale')->prefix('category')->group(function () {
// Route::get('pages/{slug}', [SitePageController::class, 'getPage'])->name('site.default-page'); Route::get('/', fn () => abort(404))->name('categories');
// Route::get('search', [SiteProductController::class, 'search'])->name('search'); Route::get('/{category}', fn () => abort(404))->name('category.view');
Route::get('/{category}/{Category}', fn () => abort(404))->name('category.show');
// Route::middleware('authProfile')->group(function () { Route::get('/{category}/{slug}/{Category}', fn () => abort(404))->name('category.showParent');
// Route::get('favorites', [SiteFavoriteController::class, 'index'])->name('favorites'); Route::post('/filter/{category}', fn () => abort(404))->name('category.filter');
// Route::get('favorites/store/{product}', [SiteFavoriteController::class, 'store']);
// Route::get('favorites/delete/{product}', [SiteFavoriteController::class, 'delete']);
// });
Route::prefix('category')->group(function () {
Route::get('/', 'CategoryController@all')->name('categories');
Route::get('/{category}', 'CategoryController@index')->name('category.view');
Route::get('/{category}/{Category}', 'CategoryController@show')->name('category.show');
Route::get('/{category}/{slug}/{Category}', 'CategoryController@showCatalog')->name('category.showParent');
Route::post('/filter/{category}', 'CategoryController@filter')->name('category.filter');
});
}); });
// Route::get('/sitemap.xml', 'Site\MainPageController@sitemap')->name('sitemap'); // Route::get('/sitemap.xml', 'Site\MainPageController@sitemap')->name('sitemap');