Initial commit
Some checks failed
Deploy / deploy (push) Has been cancelled

This commit is contained in:
ssww23
2026-03-10 00:55:37 +03:00
parent fc0f28d830
commit 93a655235a
155 changed files with 24768 additions and 0 deletions

View File

@@ -0,0 +1,31 @@
@extends('layouts.shop')
@section('content')
<section class="pc-section">
<div class="pc-section-title">
<h2>Вход в админку</h2>
<p>Доступ только для администраторов.</p>
</div>
<form class="pc-card pc-form pc-auth-form" method="post" action="{{ route('admin.login.attempt') }}">
@csrf
<label>
Email
<input type="email" name="email" value="{{ old('email') }}" required>
</label>
<label>
Пароль
<input type="password" name="password" required>
</label>
<label>
Капча: решите пример {{ $captchaQuestion }}
<input type="text" name="captcha" inputmode="numeric" autocomplete="off" required>
</label>
<label class="pc-checkbox">
<input type="checkbox" name="remember" value="1" @checked(old('remember'))>
<span>Запомнить меня</span>
</label>
<button class="pc-btn primary" type="submit">Войти</button>
</form>
</section>
@endsection

View File

@@ -0,0 +1,21 @@
<label>
Название
<input type="text" name="name" value="{{ old('name', $category->name ?? '') }}" required>
</label>
<label>
Slug
<input type="text" name="slug" value="{{ old('slug', $category->slug ?? '') }}" placeholder="Можно оставить пустым">
</label>
<label>
Описание
<textarea name="description">{{ old('description', $category->description ?? '') }}</textarea>
</label>
<label class="pc-checkbox">
<input type="checkbox" name="is_active" value="1" @checked(old('is_active', $category->is_active ?? true))>
<span>Категория активна</span>
</label>
<button class="pc-btn primary" type="submit">{{ $submitLabel ?? 'Сохранить' }}</button>

View File

@@ -0,0 +1,22 @@
@extends('layouts.shop')
@section('content')
@include('partials.breadcrumbs', [
'items' => [
['label' => 'Админка', 'url' => route('admin.dashboard')],
['label' => 'Категории', 'url' => route('admin.categories.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.categories.store') }}">
@csrf
@include('admin.categories._form', ['submitLabel' => 'Создать'])
</form>
</section>
@endsection

View File

@@ -0,0 +1,23 @@
@extends('layouts.shop')
@section('content')
@include('partials.breadcrumbs', [
'items' => [
['label' => 'Админка', 'url' => route('admin.dashboard')],
['label' => 'Категории', 'url' => route('admin.categories.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.categories.update', $category) }}">
@csrf
@method('put')
@include('admin.categories._form', ['category' => $category, 'submitLabel' => 'Сохранить'])
</form>
</section>
@endsection

View File

@@ -0,0 +1,45 @@
@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>
<a class="pc-btn primary" href="{{ route('admin.categories.create') }}">Добавить категорию</a>
</div>
<div class="pc-card">
@if ($categories->isEmpty())
<p>Категории пока не созданы.</p>
@else
<div class="pc-account-orders">
@foreach ($categories as $category)
<div class="pc-account-order">
<span>{{ $category->name }} ({{ $category->slug }})</span>
<div class="pc-product-actions">
<a class="pc-btn ghost" href="{{ route('admin.categories.edit', $category) }}">Редактировать</a>
<form method="post" action="{{ route('admin.categories.destroy', $category) }}">
@csrf
@method('delete')
<button class="pc-btn ghost" type="submit">Удалить</button>
</form>
</div>
</div>
@endforeach
</div>
@endif
</div>
<div class="pc-pagination">
{{ $categories->links() }}
</div>
</section>
@endsection

View File

@@ -0,0 +1,205 @@
@extends('layouts.shop')
@section('meta_title', 'Чаты с клиентами')
@section('content')
@include('partials.breadcrumbs', [
'items' => [
['label' => 'Админка', 'url' => route('admin.dashboard')],
['label' => 'Чаты', 'url' => null],
],
])
<section class="pc-section">
<div class="pc-section-title">
<h2>Чаты с клиентами</h2>
<p>Отвечайте клиентам прямо из админки.</p>
</div>
@if ($conversations->isEmpty())
<div class="pc-card">
<p>Пока нет сообщений от клиентов.</p>
</div>
@else
<div class="pc-admin-chat-layout">
<aside class="pc-card pc-admin-chat-list">
@foreach ($conversations as $conversation)
@php
$isActive = $selectedConversation && $selectedConversation->id === $conversation->id;
$subtitle = $conversation->user?->email ?: ('Токен: ' . \Illuminate\Support\Str::limit($conversation->visitor_token, 14, '...'));
$preview = $conversation->latestMessage?->body ? \Illuminate\Support\Str::limit($conversation->latestMessage->body, 80) : 'Нет сообщений';
@endphp
<a class="pc-admin-chat-item {{ $isActive ? 'is-active' : '' }}" href="{{ route('admin.chats.index', ['conversation' => $conversation->id]) }}">
<div class="pc-admin-chat-item-top">
<strong>{{ $conversation->display_name }}</strong>
@if ($conversation->unread_count > 0)
<span class="pc-admin-chat-badge">{{ $conversation->unread_count }}</span>
@endif
</div>
<span class="pc-muted">{{ $subtitle }}</span>
@if ($conversation->status === \App\Models\ChatConversation::STATUS_CLOSED)
<span class="pc-muted">Чат закрыт</span>
@endif
<span class="pc-muted">{{ $preview }}</span>
</a>
@endforeach
</aside>
<div class="pc-card pc-admin-chat-panel">
@if ($selectedConversation)
@php
$isClosed = $selectedConversation->status === \App\Models\ChatConversation::STATUS_CLOSED;
@endphp
<div
id="pc-admin-chat"
data-fetch-url="{{ route('admin.chats.messages', $selectedConversation) }}"
data-send-url="{{ route('admin.chats.messages.store', $selectedConversation) }}"
data-csrf="{{ csrf_token() }}"
>
<div class="pc-admin-chat-head">
<div>
<h3>{{ $selectedConversation->display_name }}</h3>
<p class="pc-muted">{{ $selectedConversation->user?->email ?: 'Гость' }}</p>
<p class="pc-muted">{{ $isClosed ? 'Чат закрыт' : 'Чат открыт' }}</p>
</div>
<div class="pc-admin-chat-actions">
<form method="post" action="{{ route('admin.chats.status', $selectedConversation) }}">
@csrf
@method('patch')
<input type="hidden" name="status" value="{{ $isClosed ? \App\Models\ChatConversation::STATUS_OPEN : \App\Models\ChatConversation::STATUS_CLOSED }}">
<button class="pc-btn ghost" type="submit">{{ $isClosed ? 'Открыть чат' : 'Закрыть чат' }}</button>
</form>
<form method="post" action="{{ route('admin.chats.destroy', $selectedConversation) }}" onsubmit="return confirm('Удалить чат и все сообщения?')">
@csrf
@method('delete')
<button class="pc-btn ghost" type="submit">Удалить чат</button>
</form>
</div>
</div>
<div class="pc-admin-chat-messages" id="pc-admin-chat-messages"></div>
<form class="pc-admin-chat-form" id="pc-admin-chat-form">
<textarea name="message" rows="3" maxlength="2000" placeholder="{{ $isClosed ? 'Чат закрыт. Сначала откройте его.' : 'Введите ответ клиенту...' }}" required @disabled($isClosed)></textarea>
<button class="pc-btn primary" type="submit" @disabled($isClosed)>{{ $isClosed ? 'Чат закрыт' : 'Отправить' }}</button>
</form>
</div>
@else
<p>Выберите чат слева.</p>
@endif
</div>
</div>
<div class="pc-pagination">
{{ $conversations->links() }}
</div>
@endif
</section>
@endsection
@if ($selectedConversation)
@push('scripts')
<script>
(() => {
const root = document.getElementById('pc-admin-chat');
const list = document.getElementById('pc-admin-chat-messages');
const form = document.getElementById('pc-admin-chat-form');
if (!root || !list || !form) {
return;
}
const fetchUrl = root.dataset.fetchUrl;
const sendUrl = root.dataset.sendUrl;
const csrf = root.dataset.csrf;
const initialMessages = @json($initialMessages);
const renderMessages = (messages) => {
if (!Array.isArray(messages)) {
return;
}
list.innerHTML = '';
messages.forEach((message) => {
const item = document.createElement('div');
item.className = `pc-admin-chat-message ${message.sender === 'admin' ? 'is-admin' : 'is-customer'}`;
const bubble = document.createElement('div');
bubble.className = 'pc-admin-chat-bubble';
bubble.textContent = message.body || '';
const meta = document.createElement('span');
meta.className = 'pc-muted';
meta.textContent = message.time || '';
item.appendChild(bubble);
item.appendChild(meta);
list.appendChild(item);
});
list.scrollTop = list.scrollHeight;
};
const fetchMessages = async () => {
try {
const response = await fetch(fetchUrl, {
method: 'GET',
headers: {
'Accept': 'application/json',
},
});
if (!response.ok) {
return;
}
const payload = await response.json();
renderMessages(payload.messages || []);
} catch (error) {
// Ignore temporary connection issues and continue polling.
}
};
form.addEventListener('submit', async (event) => {
event.preventDefault();
const textarea = form.querySelector('textarea[name="message"]');
const button = form.querySelector('button[type="submit"]');
if (!textarea || !button) {
return;
}
const message = textarea.value.trim();
if (message === '') {
return;
}
button.disabled = true;
try {
const response = await fetch(sendUrl, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrf,
},
body: JSON.stringify({ message }),
});
if (!response.ok) {
return;
}
textarea.value = '';
await fetchMessages();
} finally {
button.disabled = false;
}
});
renderMessages(initialMessages);
fetchMessages();
window.setInterval(fetchMessages, 4000);
})();
</script>
@endpush
@endif

View File

@@ -0,0 +1,63 @@
@extends('layouts.shop')
@section('content')
@include('partials.breadcrumbs', [
'items' => [
['label' => 'Админка', 'url' => null],
],
])
<section class="pc-section">
<div class="pc-section-title">
<h2>Панель администратора</h2>
<p>Управление товарами, категориями, заказами, чатами и слайдерами.</p>
</div>
<div class="pc-grid pc-grid-4">
<div class="pc-card">
<h3>Категории</h3>
<p>{{ $stats['categories'] }}</p>
</div>
<div class="pc-card">
<h3>Товары</h3>
<p>{{ $stats['products'] }}</p>
</div>
<div class="pc-card">
<h3>Заказы</h3>
<p>{{ $stats['orders'] }}</p>
</div>
<div class="pc-card">
<h3>Выручка</h3>
<p>{{ number_format($stats['revenue'], 0, '.', ' ') }} {{ config('shop.currency_symbol', '₽') }}</p>
</div>
</div>
<div class="pc-product-actions">
<a class="pc-btn primary" href="{{ route('admin.products.index') }}">Товары</a>
<a class="pc-btn ghost" href="{{ route('admin.categories.index') }}">Категории</a>
<a class="pc-btn ghost" href="{{ route('admin.orders.index') }}">Заказы</a>
<a class="pc-btn ghost" href="{{ route('admin.home-slides.index') }}">Слайдеры</a>
<a class="pc-btn ghost" href="{{ route('admin.chats.index') }}">Чаты</a>
<form method="post" action="{{ route('admin.logout') }}">
@csrf
<button class="pc-btn ghost" type="submit">Выйти</button>
</form>
</div>
<div class="pc-card">
<h3>Последние заказы</h3>
@if ($recentOrders->isEmpty())
<p>Пока нет заказов.</p>
@else
<div class="pc-account-orders">
@foreach ($recentOrders as $order)
<a class="pc-account-order" href="{{ route('admin.orders.show', $order) }}">
<span>Заказ #{{ $order->id }} ({{ $order->status }})</span>
<strong>{{ number_format($order->total, 0, '.', ' ') }} {{ config('shop.currency_symbol', '₽') }}</strong>
</a>
@endforeach
</div>
@endif
</div>
</section>
@endsection

View File

@@ -0,0 +1,71 @@
<label>
Блок на главной
<select name="zone" required>
@foreach ($zoneLabels as $zoneKey => $zoneLabel)
<option value="{{ $zoneKey }}" @selected(old('zone', $slide->zone ?? 'left') === $zoneKey)>{{ $zoneLabel }}</option>
@endforeach
</select>
</label>
<label>
Заголовок
<input type="text" name="title" value="{{ old('title', $slide->title ?? '') }}" maxlength="160" placeholder="Например: Скидки на видеокарты">
</label>
<label>
Подзаголовок
<textarea name="subtitle" rows="3" maxlength="1500" placeholder="Короткий текст на баннере">{{ old('subtitle', $slide->subtitle ?? '') }}</textarea>
</label>
<div class="pc-grid pc-grid-2">
<label>
Текст кнопки
<input type="text" name="button_text" value="{{ old('button_text', $slide->button_text ?? '') }}" maxlength="60" placeholder="Подробнее">
</label>
<label>
Ссылка кнопки
<input type="text" name="button_url" value="{{ old('button_url', $slide->button_url ?? '') }}" maxlength="255" placeholder="/catalog или https://...">
</label>
</div>
<div class="pc-grid pc-grid-3">
<label class="pc-checkbox">
<input type="checkbox" name="show_title" value="1" @checked(old('show_title', $slide->show_title ?? true))>
<span>Показывать заголовок</span>
</label>
<label class="pc-checkbox">
<input type="checkbox" name="show_subtitle" value="1" @checked(old('show_subtitle', $slide->show_subtitle ?? true))>
<span>Показывать описание</span>
</label>
<label class="pc-checkbox">
<input type="checkbox" name="show_button" value="1" @checked(old('show_button', $slide->show_button ?? true))>
<span>Показывать кнопку</span>
</label>
</div>
<div class="pc-grid pc-grid-2">
<label>
Порядок
<input type="number" min="0" max="9999" name="sort_order" value="{{ old('sort_order', $slide->sort_order ?? 100) }}" required>
</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" @required(empty($slide?->id))>
</label>
</div>
@if (!empty($slide?->image_url))
<div class="pc-admin-slide-current">
<div class="pc-admin-slide-current-preview" style="background-image: url('{{ $slide->image_url }}')"></div>
<span class="pc-muted">Текущее изображение. Загрузите новое, если хотите заменить.</span>
</div>
@endif
<label class="pc-checkbox">
<input type="checkbox" name="is_active" value="1" @checked(old('is_active', $slide->is_active ?? true))>
<span>Слайд активен</span>
</label>
<button class="pc-btn primary" type="submit">{{ $submitLabel ?? 'Сохранить' }}</button>

View File

@@ -0,0 +1,22 @@
@extends('layouts.shop')
@section('content')
@include('partials.breadcrumbs', [
'items' => [
['label' => 'Админка', 'url' => route('admin.dashboard')],
['label' => 'Слайдеры главной', 'url' => route('admin.home-slides.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.home-slides.store') }}" enctype="multipart/form-data">
@csrf
@include('admin.home-slides._form', ['submitLabel' => 'Создать'])
</form>
</section>
@endsection

View File

@@ -0,0 +1,23 @@
@extends('layouts.shop')
@section('content')
@include('partials.breadcrumbs', [
'items' => [
['label' => 'Админка', 'url' => route('admin.dashboard')],
['label' => 'Слайдеры главной', 'url' => route('admin.home-slides.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.home-slides.update', $slide) }}" enctype="multipart/form-data">
@csrf
@method('put')
@include('admin.home-slides._form', ['slide' => $slide, 'submitLabel' => 'Сохранить'])
</form>
</section>
@endsection

View File

@@ -0,0 +1,57 @@
@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>
<p>Загружайте баннеры для блока 2/3 и 1/3 на первом экране.</p>
</div>
<div class="pc-product-actions">
<a class="pc-btn primary" href="{{ route('admin.home-slides.create', ['zone' => 'left']) }}">Добавить в 2/3</a>
<a class="pc-btn ghost" href="{{ route('admin.home-slides.create', ['zone' => 'right']) }}">Добавить в 1/3</a>
</div>
</div>
<div class="pc-grid pc-grid-2">
@foreach ($zoneLabels as $zone => $zoneLabel)
@php
$zoneSlides = $slides->get($zone, collect());
@endphp
<div class="pc-card">
<h3>{{ $zoneLabel }}</h3>
@if ($zoneSlides->isEmpty())
<p>Слайды пока не добавлены.</p>
@else
<div class="pc-account-orders">
@foreach ($zoneSlides as $slide)
<div class="pc-admin-slide-item">
<div class="pc-admin-slide-preview" style="background-image: url('{{ $slide->image_url }}')"></div>
<div class="pc-admin-slide-main">
<strong>{{ $slide->title ?: 'Без заголовка' }}</strong>
<span class="pc-muted">Порядок: {{ $slide->sort_order }} · {{ $slide->is_active ? 'Активен' : 'Скрыт' }}</span>
</div>
<div class="pc-product-actions">
<a class="pc-btn ghost" href="{{ route('admin.home-slides.edit', $slide) }}">Редактировать</a>
<form method="post" action="{{ route('admin.home-slides.destroy', $slide) }}">
@csrf
@method('delete')
<button class="pc-btn ghost" type="submit">Удалить</button>
</form>
</div>
</div>
@endforeach
</div>
@endif
</div>
@endforeach
</div>
</section>
@endsection

View File

@@ -0,0 +1,35 @@
@extends('layouts.shop')
@section('content')
@include('partials.breadcrumbs', [
'items' => [
['label' => 'Админка', 'url' => route('admin.dashboard')],
['label' => 'Заказы', 'url' => null],
],
])
<section class="pc-section">
<div class="pc-section-title">
<h2>Заказы</h2>
</div>
<div class="pc-card">
@if ($orders->isEmpty())
<p>Заказов пока нет.</p>
@else
<div class="pc-account-orders">
@foreach ($orders as $order)
<a class="pc-account-order" href="{{ route('admin.orders.show', $order) }}">
<span>Заказ #{{ $order->id }} ({{ $order->status }})</span>
<strong>{{ number_format($order->total, 0, '.', ' ') }} {{ config('shop.currency_symbol', '₽') }}</strong>
</a>
@endforeach
</div>
@endif
</div>
<div class="pc-pagination">
{{ $orders->links() }}
</div>
</section>
@endsection

View File

@@ -0,0 +1,68 @@
@extends('layouts.shop')
@section('content')
@include('partials.breadcrumbs', [
'items' => [
['label' => 'Админка', 'url' => route('admin.dashboard')],
['label' => 'Заказы', 'url' => route('admin.orders.index')],
['label' => 'Заказ #' . $order->id, 'url' => null],
],
])
<section class="pc-section">
<div class="pc-section-title">
<h2>Заказ #{{ $order->id }}</h2>
</div>
<div class="pc-grid pc-grid-2">
<div class="pc-card">
<h3>Статус</h3>
<form class="pc-form" method="post" action="{{ route('admin.orders.update', $order) }}">
@csrf
@method('put')
<label>
Текущий статус
<select name="status">
@foreach (['new' => 'Новый', 'processing' => 'В обработке', 'paid' => 'Оплачен', 'shipped' => 'Отправлен', 'completed' => 'Завершен', 'cancelled' => 'Отменен'] as $value => $label)
<option value="{{ $value }}" @selected($order->status === $value)>{{ $label }}</option>
@endforeach
</select>
</label>
<button class="pc-btn primary" type="submit">Обновить статус</button>
</form>
</div>
<div class="pc-card">
<h3>Покупатель</h3>
<p><strong>{{ $order->customer_name }}</strong></p>
<p>{{ $order->email }}</p>
<p>Оплата: {{ $order->payment_method_label }}</p>
@if ($order->phone)
<p>{{ $order->phone }}</p>
@endif
@if ($order->address)
<p>{{ $order->address }}</p>
@endif
@if ($order->comment)
<p>Комментарий: {{ $order->comment }}</p>
@endif
</div>
</div>
<div class="pc-card">
<h3>Состав заказа</h3>
<div class="pc-account-orders">
@foreach ($order->items as $item)
<div class="pc-account-order">
<span>{{ $item->name }} × {{ $item->quantity }}</span>
<strong>{{ number_format($item->subtotal, 0, '.', ' ') }} {{ config('shop.currency_symbol', '₽') }}</strong>
</div>
@endforeach
</div>
<div class="pc-cart-summary-row">
<span>Итого</span>
<strong>{{ number_format($order->total, 0, '.', ' ') }} {{ config('shop.currency_symbol', '₽') }}</strong>
</div>
</div>
</section>
@endsection

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

View 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

View 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

View 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