This commit is contained in:
219
resources/views/admin/products/_form.blade.php
Normal file
219
resources/views/admin/products/_form.blade.php
Normal file
@@ -0,0 +1,219 @@
|
||||
@php
|
||||
$currentProduct = $product ?? null;
|
||||
$selectedCategoryId = (int) old('category_id', $currentProduct->category_id ?? 0);
|
||||
$specValues = old('specs', $currentProduct->specs ?? []);
|
||||
$selectedGalleryPathsForRemoval = collect(old('remove_gallery_paths', []))
|
||||
->filter(fn ($path) => is_string($path) && trim($path) !== '')
|
||||
->map(fn (string $path) => trim($path))
|
||||
->unique()
|
||||
->values()
|
||||
->all();
|
||||
$galleryItems = collect((array) ($currentProduct->gallery_paths ?? []))
|
||||
->filter(fn ($path) => is_string($path) && trim($path) !== '')
|
||||
->map(fn (string $path) => trim($path))
|
||||
->unique()
|
||||
->filter(fn (string $path) => $path !== ($currentProduct->image_path ?? null))
|
||||
->map(function (string $path) {
|
||||
$url = $path;
|
||||
|
||||
if (!\Illuminate\Support\Str::startsWith($path, ['http://', 'https://', '/'])) {
|
||||
$url = \Illuminate\Support\Str::startsWith($path, 'uploads/')
|
||||
? asset($path)
|
||||
: '/storage/' . ltrim($path, '/');
|
||||
}
|
||||
|
||||
return [
|
||||
'path' => $path,
|
||||
'url' => $url,
|
||||
];
|
||||
})
|
||||
->values()
|
||||
->all();
|
||||
@endphp
|
||||
|
||||
<label>
|
||||
Категория
|
||||
<select id="product-category-select" name="category_id" required>
|
||||
<option value="">Выберите категорию</option>
|
||||
@foreach ($categories as $categoryOption)
|
||||
<option value="{{ $categoryOption->id }}" @selected((int) old('category_id', $currentProduct->category_id ?? 0) === $categoryOption->id)>
|
||||
{{ $categoryOption->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Название
|
||||
<input type="text" name="name" value="{{ old('name', $currentProduct->name ?? '') }}" required>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Slug
|
||||
<input type="text" name="slug" value="{{ old('slug', $currentProduct->slug ?? '') }}" placeholder="Можно оставить пустым">
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Артикул
|
||||
<input type="text" name="sku" value="{{ old('sku', $currentProduct->sku ?? '') }}">
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Изображение товара (jpg, png, webp)
|
||||
<input class="pc-file-input" type="file" name="image" accept=".jpg,.jpeg,.png,.webp,image/jpeg,image/png,image/webp">
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Дополнительные изображения (до 12 файлов)
|
||||
<input
|
||||
class="pc-file-input"
|
||||
type="file"
|
||||
name="gallery_images[]"
|
||||
accept=".jpg,.jpeg,.png,.webp,image/jpeg,image/png,image/webp"
|
||||
multiple
|
||||
>
|
||||
</label>
|
||||
|
||||
<div class="pc-grid pc-grid-2">
|
||||
<label>
|
||||
Цена
|
||||
<input type="number" step="0.01" min="0" name="price" value="{{ old('price', $currentProduct->price ?? '') }}" required>
|
||||
</label>
|
||||
<label>
|
||||
Старая цена
|
||||
<input type="number" step="0.01" min="0" name="old_price" value="{{ old('old_price', $currentProduct->old_price ?? '') }}">
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label>
|
||||
Остаток
|
||||
<input type="number" min="0" name="stock" value="{{ old('stock', $currentProduct->stock ?? 0) }}" required>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Короткое описание
|
||||
<input type="text" name="short_description" value="{{ old('short_description', $currentProduct->short_description ?? '') }}">
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Описание
|
||||
<textarea name="description">{{ old('description', $currentProduct->description ?? '') }}</textarea>
|
||||
</label>
|
||||
|
||||
@if (!empty($currentProduct?->image_url))
|
||||
<div class="pc-admin-product-image-current">
|
||||
<img class="pc-admin-product-image-preview" src="{{ $currentProduct->image_url }}" alt="{{ $currentProduct->name }}" loading="lazy">
|
||||
<span class="pc-muted">Текущее изображение. Загрузите новое, чтобы заменить.</span>
|
||||
<label class="pc-checkbox">
|
||||
<input type="checkbox" name="remove_image" value="1" @checked(old('remove_image'))>
|
||||
<span>Удалить текущее изображение</span>
|
||||
</label>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($galleryItems !== [])
|
||||
<div class="pc-admin-product-gallery-current">
|
||||
<span class="pc-muted">Отметьте изображения, которые нужно удалить. При новой загрузке текущая галерея будет заменена.</span>
|
||||
<div class="pc-admin-product-gallery-grid">
|
||||
@foreach ($galleryItems as $galleryItem)
|
||||
<label class="pc-admin-product-gallery-item">
|
||||
<img class="pc-admin-product-gallery-preview" src="{{ $galleryItem['url'] }}" alt="Дополнительное изображение товара" loading="lazy">
|
||||
<span class="pc-checkbox">
|
||||
<input type="checkbox" name="remove_gallery_paths[]" value="{{ $galleryItem['path'] }}" @checked(in_array($galleryItem['path'], $selectedGalleryPathsForRemoval, true))>
|
||||
<span>Удалить</span>
|
||||
</span>
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="pc-spec-fields-wrap">
|
||||
<h3 class="pc-spec-fields-title">Характеристики категории</h3>
|
||||
<div id="pc-spec-fields" class="pc-grid pc-grid-2"></div>
|
||||
</div>
|
||||
|
||||
<label class="pc-checkbox">
|
||||
<input type="checkbox" name="is_active" value="1" @checked(old('is_active', $currentProduct->is_active ?? true))>
|
||||
<span>Товар активен</span>
|
||||
</label>
|
||||
|
||||
<button class="pc-btn primary" type="submit">{{ $submitLabel ?? 'Сохранить' }}</button>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
const categorySelect = document.getElementById('product-category-select');
|
||||
const specsContainer = document.getElementById('pc-spec-fields');
|
||||
if (!categorySelect || !specsContainer) {
|
||||
return;
|
||||
}
|
||||
|
||||
const specsByCategory = @json($categorySpecsById ?? []);
|
||||
const specValues = @json($specValues);
|
||||
|
||||
const renderSpecFields = () => {
|
||||
const selectedCategoryId = categorySelect.value;
|
||||
const fields = specsByCategory[selectedCategoryId] || [];
|
||||
specsContainer.innerHTML = '';
|
||||
|
||||
if (fields.length === 0) {
|
||||
const muted = document.createElement('p');
|
||||
muted.className = 'pc-muted';
|
||||
muted.textContent = 'Для выбранной категории нет дополнительных полей.';
|
||||
specsContainer.appendChild(muted);
|
||||
return;
|
||||
}
|
||||
|
||||
fields.forEach((field) => {
|
||||
const label = document.createElement('label');
|
||||
|
||||
const title = document.createElement('span');
|
||||
title.textContent = field.label;
|
||||
|
||||
label.appendChild(title);
|
||||
|
||||
const input = document.createElement('input');
|
||||
input.type = 'text';
|
||||
input.name = `specs[${field.key}]`;
|
||||
input.value = typeof specValues[field.key] === 'string' ? specValues[field.key] : '';
|
||||
|
||||
const options = Array.isArray(field.options)
|
||||
? [...new Set(field.options.map((value) => String(value).trim()).filter(Boolean))]
|
||||
: [];
|
||||
|
||||
if (options.length > 0) {
|
||||
const listId = `pc-spec-options-${field.key}`;
|
||||
input.setAttribute('list', listId);
|
||||
input.setAttribute('autocomplete', 'off');
|
||||
|
||||
const datalist = document.createElement('datalist');
|
||||
datalist.id = listId;
|
||||
|
||||
options.forEach((optionValue) => {
|
||||
const option = document.createElement('option');
|
||||
option.value = optionValue;
|
||||
datalist.appendChild(option);
|
||||
});
|
||||
|
||||
const hint = document.createElement('small');
|
||||
hint.className = 'pc-field-hint';
|
||||
hint.textContent = 'Выберите из списка или введите свое значение';
|
||||
|
||||
label.appendChild(input);
|
||||
label.appendChild(datalist);
|
||||
label.appendChild(hint);
|
||||
} else {
|
||||
label.appendChild(input);
|
||||
}
|
||||
|
||||
specsContainer.appendChild(label);
|
||||
});
|
||||
};
|
||||
|
||||
categorySelect.addEventListener('change', renderSpecFields);
|
||||
if (categorySelect.value === '' && '{{ $selectedCategoryId }}' !== '0') {
|
||||
categorySelect.value = '{{ $selectedCategoryId }}';
|
||||
}
|
||||
renderSpecFields();
|
||||
})();
|
||||
</script>
|
||||
22
resources/views/admin/products/create.blade.php
Normal file
22
resources/views/admin/products/create.blade.php
Normal file
@@ -0,0 +1,22 @@
|
||||
@extends('layouts.shop')
|
||||
|
||||
@section('content')
|
||||
@include('partials.breadcrumbs', [
|
||||
'items' => [
|
||||
['label' => 'Админка', 'url' => route('admin.dashboard')],
|
||||
['label' => 'Товары', 'url' => route('admin.products.index')],
|
||||
['label' => 'Новый товар', 'url' => null],
|
||||
],
|
||||
])
|
||||
|
||||
<section class="pc-section">
|
||||
<div class="pc-section-title">
|
||||
<h2>Новый товар</h2>
|
||||
</div>
|
||||
|
||||
<form class="pc-card pc-form" method="post" action="{{ route('admin.products.store') }}" enctype="multipart/form-data">
|
||||
@csrf
|
||||
@include('admin.products._form', ['submitLabel' => 'Создать'])
|
||||
</form>
|
||||
</section>
|
||||
@endsection
|
||||
23
resources/views/admin/products/edit.blade.php
Normal file
23
resources/views/admin/products/edit.blade.php
Normal file
@@ -0,0 +1,23 @@
|
||||
@extends('layouts.shop')
|
||||
|
||||
@section('content')
|
||||
@include('partials.breadcrumbs', [
|
||||
'items' => [
|
||||
['label' => 'Админка', 'url' => route('admin.dashboard')],
|
||||
['label' => 'Товары', 'url' => route('admin.products.index')],
|
||||
['label' => 'Редактирование', 'url' => null],
|
||||
],
|
||||
])
|
||||
|
||||
<section class="pc-section">
|
||||
<div class="pc-section-title">
|
||||
<h2>Редактирование товара</h2>
|
||||
</div>
|
||||
|
||||
<form class="pc-card pc-form" method="post" action="{{ route('admin.products.update', $product) }}" enctype="multipart/form-data">
|
||||
@csrf
|
||||
@method('put')
|
||||
@include('admin.products._form', ['product' => $product, 'submitLabel' => 'Сохранить'])
|
||||
</form>
|
||||
</section>
|
||||
@endsection
|
||||
58
resources/views/admin/products/index.blade.php
Normal file
58
resources/views/admin/products/index.blade.php
Normal file
@@ -0,0 +1,58 @@
|
||||
@extends('layouts.shop')
|
||||
|
||||
@section('content')
|
||||
@include('partials.breadcrumbs', [
|
||||
'items' => [
|
||||
['label' => 'Админка', 'url' => route('admin.dashboard')],
|
||||
['label' => 'Товары', 'url' => null],
|
||||
],
|
||||
])
|
||||
|
||||
<section class="pc-section">
|
||||
<div class="pc-category-toolbar">
|
||||
<div class="pc-section-title">
|
||||
<h2>Товары</h2>
|
||||
</div>
|
||||
<div class="pc-product-actions">
|
||||
<a class="pc-btn primary" href="{{ route('admin.products.create') }}">Добавить товар</a>
|
||||
<a class="pc-btn ghost" href="{{ route('admin.products.export') }}">Экспорт CSV</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pc-card">
|
||||
<h3>Импорт из CSV</h3>
|
||||
<p class="pc-muted">Загрузите CSV файл (UTF-8). Поддерживаются колонки category_slug/category_name, name, price, stock и дополнительные характеристики.</p>
|
||||
<form class="pc-product-actions" method="post" action="{{ route('admin.products.import') }}" enctype="multipart/form-data">
|
||||
@csrf
|
||||
<input class="pc-file-input" type="file" name="csv_file" accept=".csv,text/csv" required>
|
||||
<button class="pc-btn ghost" type="submit">Импорт CSV</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="pc-card">
|
||||
@if ($products->isEmpty())
|
||||
<p>Товары пока не созданы.</p>
|
||||
@else
|
||||
<div class="pc-account-orders">
|
||||
@foreach ($products as $product)
|
||||
<div class="pc-account-order">
|
||||
<span>{{ $product->name }} ({{ $product->category?->name ?? 'Без категории' }})</span>
|
||||
<div class="pc-product-actions">
|
||||
<a class="pc-btn ghost" href="{{ route('admin.products.edit', $product) }}">Редактировать</a>
|
||||
<form method="post" action="{{ route('admin.products.destroy', $product) }}">
|
||||
@csrf
|
||||
@method('delete')
|
||||
<button class="pc-btn ghost" type="submit">Удалить</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="pc-pagination">
|
||||
{{ $products->links() }}
|
||||
</div>
|
||||
</section>
|
||||
@endsection
|
||||
Reference in New Issue
Block a user