This commit is contained in:
2026-04-06 18:44:02 +05:00
commit d775fbb9ba
431 changed files with 31234 additions and 0 deletions

View File

@@ -0,0 +1,160 @@
@extends('layouts.admin')
@section('content')
<div class="container-fluid">
@if(session('success'))
<div class="alert alert-success alert-dismissible fade show" role="alert">
<i class="bi bi-check-circle-fill me-2"></i> {{ session('success') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
@if(session('error'))
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<i class="bi bi-exclamation-triangle-fill me-2"></i> {{ session('error') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
<div class="brands-table col-12">
<div class="card">
<div class="card-header">
<h5>Бренды</h5>
</div>
<div class="card-body">
<table class="table">
<thead>
<th>Название</th>
<th>Описание</th>
<th>Лого</th>
<th>Сайт</th>
<th>Страна</th>
<th class="text-center">Активен?</th>
<th class="text-center">Действия</th>
</thead>
<tbody>
<tr class="bg-light">
<form action="{{ route('admin.brands.store') }}" method="POST" enctype="multipart/form-data">
@csrf
<td>
<input type="text" name="name"
class="form-control form-control-sm @error('name') is-invalid @enderror"
placeholder="Название" value="{{ old('name') }}">
@error('name')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</td>
<td>
<textarea name="description"
class="form-control form-control-sm @error('description') is-invalid @enderror"
rows="2" placeholder="Описание">{{ old('description') }}</textarea>
@error('description')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</td>
<td>
<input type="file" name="logo"
class="form-control form-control-sm @error('logo') is-invalid @enderror"
accept="image/*">
@error('logo')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</td>
<td>
<input type="url" name="website"
class="form-control form-control-sm @error('website') is-invalid @enderror"
placeholder="https://" value="{{ old('website') }}">
@error('website')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</td>
<td>
<input type="text" name="country"
class="form-control form-control-sm @error('country') is-invalid @enderror"
placeholder="Страна" value="{{ old('country') }}">
@error('country')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</td>
<td class="text-center">
<input type="checkbox" name="is_active" value="1" class="form-check-input"
{{ old('is_active', true) ? 'checked' : '' }}>
</td>
<td class="text-center">
<button type="submit" class="btn btn-dark-green p-1" title="Добавить">
<img src="{{ asset('assets/images/icons/add-folder-svgrepo-com.svg') }}" alt="Добавить" width="20" height="20">
</button>
</td>
</form>
</tr>
@foreach ($brands as $brand)
<tr>
<td>{{ $brand->name }}</td>
<td style="max-width: 200px;">
@if ($brand->description)
{{ Str::limit($brand->description, 50) }}
@else
<span class="text-muted"></span>
@endif
</td>
<td>
@if ($brand->logo)
<img src="{{ asset('assets/images/brands/' . $brand->logo) }}"
alt="{{ $brand->name }}"
style="max-height: 40px;">
@else
<span class="text-muted"></span>
@endif
</td>
<td>
@if ($brand->website)
<a href="{{ $brand->website }}" target="_blank">{{ Str::limit($brand->website, 30) }}</a>
@else
<span class="text-muted"></span>
@endif
</td>
<td>
@if ($brand->country)
{{ $brand->country }}
@else
<span class="text-muted"></span>
@endif
</td>
<td class="text-center">
@if ($brand->is_active)
<i class="bi bi-check-circle text-success"></i>
@else
<i class="bi bi-x-circle text-danger"></i>
@endif
</td>
<td class="text-center">
<div class="d-flex gap-2 justify-content-center">
<a href="{{ route('admin.brands.edit', $brand->id) }}" class="btn btn-orange p-1" title="Редактировать">
<img src="{{ asset('assets/images/icons/edit-svgrepo-com.svg') }}" alt="Редактировать" width="20" height="20">
</a>
<form action="{{ route('admin.brands.destroy', $brand->id) }}" method="post"
onsubmit="return confirm('Удалить бренд «{{ $brand->name }}»?');">
@csrf
@method('DELETE')
<button class="btn btn-danger p-1" type="submit" title="Удалить">
<img src="{{ asset('assets/images/icons/delete-svgrepo-com.svg') }}" alt="Удалить" width="20" height="20">
</button>
</form>
</div>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="ps-2 pe-2">
@if(method_exists($brands, 'hasPages') && $brands->hasPages())
{{ $brands->links() }}
@endif
</div>
@endsection

View File

@@ -0,0 +1,101 @@
@extends('layouts.admin')
@section('content')
<div class="container-fluid">
<div class="card">
<div class="card-header">
<h5>Редактирование бренда: {{ $brand->name }}</h5>
</div>
<div class="card-body">
<form action="{{ route('admin.brands.update', $brand->id) }}" method="POST" enctype="multipart/form-data">
@csrf
@method('PUT')
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Название <span class="text-danger">*</span></label>
<input type="text" name="name"
class="form-control @error('name') is-invalid @enderror"
value="{{ old('name', $brand->name) }}" required>
@error('name')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Страна</label>
<input type="text" name="country"
class="form-control @error('country') is-invalid @enderror"
value="{{ old('country', $brand->country) }}">
@error('country')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
</div>
<div class="mb-3">
<label class="form-label">Описание</label>
<textarea name="description"
class="form-control @error('description') is-invalid @enderror"
rows="4">{{ old('description', $brand->description) }}</textarea>
@error('description')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Сайт</label>
<input type="url" name="website"
class="form-control @error('website') is-invalid @enderror"
value="{{ old('website', $brand->website) }}"
placeholder="https://">
@error('website')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Логотип</label>
@if($brand->logo)
<div class="mb-2">
<img src="{{ asset('assets/images/brands/' . $brand->logo) }}"
alt="{{ $brand->name }}"
style="max-height: 60px;">
</div>
@endif
<input type="file" name="logo"
class="form-control @error('logo') is-invalid @enderror"
accept="image/*">
<small class="text-muted">Оставьте пустым, чтобы не менять</small>
@error('logo')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
</div>
<div class="mb-3">
<div class="form-check">
<input type="checkbox" name="is_active" value="1"
class="form-check-input" id="is_active"
{{ old('is_active', $brand->is_active) ? 'checked' : '' }}>
<label class="form-check-label" for="is_active">
Активен
</label>
</div>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-orange p-2">
<img src="{{ asset('assets/images/icons/save-svgrepo-com.svg') }}" alt="Обновить" width="20" height="20">
</button>
<a href="{{ route('admin.brands') }}" class="btn btn-dark-green p-2 d-flex gap-2 align-items-center">
<img src="{{ asset('assets/images/icons/delete-svgrepo-com.svg') }}" alt="Обновить" width="20" height="20">
Отмена
</a>
</div>
</form>
</div>
</div>
</div>
@endsection

View File

@@ -0,0 +1,245 @@
@extends('layouts.admin')
@section('content')
<div class="container-fluid">
@if(session('success'))
<div class="alert alert-success alert-dismissible fade show" role="alert">
<i class="bi bi-check-circle-fill me-2"></i> {{ session('success') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
@if(session('error'))
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<i class="bi bi-exclamation-triangle-fill me-2"></i> {{ session('error') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
<div class="categories-table col-12 mb-5">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5>Категории</h5>
</div>
<div class="card-body">
<table class="table">
<thead>
<th style="width: 60px">Порядок</th>
<th>Название</th>
<th>Описание</th>
<th>Иконка</th>
<th>Изображение</th>
<th class="text-center">Активна?</th>
<th class="text-center">Действия</th>
</thead>
<tbody>
<tr class="bg-light">
<form action="{{ route('admin.categories.store') }}" method="POST" enctype="multipart/form-data">
@csrf
<td>
<select name="parent_id" class="form-select form-select-sm @error('parent_id') is-invalid @enderror">
<option value="">Корневая категория</option>
@foreach($allCategories as $cat)
<option value="{{ $cat->id }}" {{ old('parent_id') == $cat->id ? 'selected' : '' }}>
@for($i = 0; $i < $cat->level; $i++) @endfor {{ $cat->name }}
</option>
@endforeach
</select>
@error('parent_id')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</td>
<td>
<input type="text" name="name"
class="form-control form-control-sm @error('name') is-invalid @enderror"
placeholder="Название" value="{{ old('name') }}" required>
@error('name')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</td>
<td>
<textarea name="description"
class="form-control form-control-sm @error('description') is-invalid @enderror"
rows="2" placeholder="Описание">{{ old('description') }}</textarea>
@error('description')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</td>
<td>
<input type="file" name="icon"
class="form-control form-control-sm @error('icon') is-invalid @enderror"
accept="image/*">
@error('icon')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</td>
<td>
<input type="file" name="image"
class="form-control form-control-sm @error('image') is-invalid @enderror"
accept="image/*">
@error('image')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</td>
<td class="text-center">
<input type="checkbox" name="is_active" value="1" class="form-check-input"
{{ old('is_active', true) ? 'checked' : '' }}>
@error('is_active')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</td>
<td class="text-center">
<button type="submit" class="btn btn-dark-green p-1" title="Добавить категорию">
<img src="{{ asset('assets/images/icons/add-folder-svgrepo-com.svg') }}" alt="Добавить" width="20" height="20">
</button>
</td>
</form>
</tr>
@foreach ($categories as $index => $category)
<tr class="category-row" data-level="{{ $category->level ?? 0 }}">
<td class="text-nowrap">
<div class="d-flex gap-1">
@php
$canMoveUp = false;
for ($i = 0; $i < $index; $i++) {
if ($categories[$i]->parent_id == $category->parent_id) {
$canMoveUp = true;
break;
}
}
@endphp
@if($canMoveUp)
<form action="{{ route('admin.categories.move-up', $category->id) }}" method="POST" class="d-inline">
@csrf
@method('PATCH')
<button type="submit" class="btn btn-sm btn-link p-0 text-dark" title="Вверх">
<i class="bi bi-arrow-up-short fs-4"></i>
</button>
</form>
@else
<span style="width: 28px"></span>
@endif
@php
$canMoveDown = false;
for ($i = $index + 1; $i < count($categories); $i++) {
if ($categories[$i]->parent_id == $category->parent_id) {
$canMoveDown = true;
break;
}
}
@endphp
@if($canMoveDown)
<form action="{{ route('admin.categories.move-down', $category->id) }}" method="POST" class="d-inline">
@csrf
@method('PATCH')
<button type="submit" class="btn btn-sm btn-link p-0 text-dark" title="Вниз">
<i class="bi bi-arrow-down-short fs-4"></i>
</button>
</form>
@else
<span style="width: 28px"></span>
@endif
</div>
</td>
<td>
<div class="category-name" style="display: flex; align-items: center;">
@for($i = 0; $i < ($category->level ?? 0); $i++)
<span style="width: 24px; display: inline-block;"></span>
@endfor
@if(($category->level ?? 0) > 0)
@php
$hasNext = false;
if (isset($categories[$index + 1])) {
$nextLevel = $categories[$index + 1]->level ?? 0;
$currentLevel = $category->level ?? 0;
if ($nextLevel >= $currentLevel) {
$hasNext = true;
}
}
@endphp
@if($hasNext)
<span class="text-muted" style="font-family: monospace;">├─ </span>
@else
<span class="text-muted" style="font-family: monospace;">└─ </span>
@endif
@endif
@if($category->children->count() > 0)
<i class="bi bi-folder-fill tw-text-orange me-2"></i>
@else
<i class="bi bi-file-earmark me-2 text-secondary"></i>
@endif
<strong>{{ $category->name }}</strong>
@if($category->children->count())
<span class="badge bg-secondary ms-2">{{ $category->children->count() }}</span>
@endif
</div>
</td>
<td style="max-width: 200px;">
@if($category->description)
{{ Str::limit($category->description, 50) }}
@else
<span class="text-muted"></span>
@endif
</td>
<td>
@if ($category->icon)
<img src="{{ asset('assets/images/categories/icons/' . $category->icon) }}" alt="{{ $category->name }}" style="max-height: 30px;">
@else
<span class="text-muted"></span>
@endif
</td>
<td>
@if ($category->image)
<img src="{{ asset('assets/images/categories/images/' . $category->image) }}" alt="{{ $category->name }}" style="max-height: 30px;">
@else
<span class="text-muted"></span>
@endif
</td>
<td class="text-center">
@if ($category->is_active)
<i class="bi bi-check-circle text-success"></i>
@else
<i class="bi bi-x-circle text-danger"></i>
@endif
</td>
<td class="text-center">
<div class="d-flex gap-2 justify-content-center">
<a href="{{ route('admin.categories.edit', $category->id) }}" class="btn btn-orange p-1" title="Редактировать">
<img src="{{ asset('assets/images/icons/edit-svgrepo-com.svg') }}" alt="Редактировать" width="20" height="20">
</a>
<form action="{{ route('admin.categories.destroy', $category->id) }}" method="post"
onsubmit="return confirm('Удалить категорию «{{ $category->name }}»? Все подкатегории также будут удалены.');">
@csrf
@method('DELETE')
<button class="btn btn-danger p-1" type="submit">
<img src="{{ asset('assets/images/icons/delete-svgrepo-com.svg') }}" alt="Удалить" width="20" height="20">
</button>
</form>
</div>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
</div>
@if(method_exists($categories, 'hasPages') && $categories->hasPages())
<div class="ps-2 pe-2">
{{ $categories->links() }}
</div>
@endif
@endsection

View File

@@ -0,0 +1,132 @@
@extends('layouts.admin')
@section('content')
<div class="container-fluid">
<div class="card">
<div class="card-header">
<h5>Редактирование категории: {{ $category->name }}</h5>
</div>
<div class="card-body">
<form action="{{ route('admin.categories.update', $category->id) }}" method="POST" enctype="multipart/form-data">
@csrf
@method('PUT')
<div class="row">
<div class="col-md-6 mb-3">
<div class="col-12 mb-4">
<label class="form-label">Название <span class="text-danger">*</span></label>
<input type="text" name="name"
class="form-control @error('name') is-invalid @enderror"
value="{{ old('name', $category->name) }}">
@error('name')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="col-12">
<select name="parent_id" class="form-select @error('parent_id') is-invalid @enderror">
<option value="">Корневая категория</option>
@foreach($allCategories as $cat)
<option value="{{ $cat->id }}"
{{ old('parent_id', $category->parent_id) == $cat->id ? 'selected' : '' }}
{{ $cat->disabled ? 'disabled' : '' }}>
@for($i = 0; $i < $cat->level; $i++) @endfor {{ $cat->name }}
@if($cat->disabled) (недоступно) @endif
</option>
@endforeach
</select>
@error('parent_id')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Описание</label>
<textarea name="description"
class="form-control @error('description') is-invalid @enderror"
rows="4">{{ old('description', $category->description) }}</textarea>
@error('description')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Иконка</label>
<div class="form-check mb-2">
<input type="checkbox" name="remove_icon" value="1"
class="form-check-input" id="remove_icon"
{{ old('remove_icon') ? 'checked' : '' }}>
<label class="form-check-label" for="remove_icon">
Удалить иконку
</label>
</div>
@if($category->icon)
<div class="mb-2">
<img src="{{ asset('assets/images/categories/icons/' . $category->icon) }}"
alt="{{ $category->name }}"
style="max-height: 60px;">
</div>
@endif
<input type="file" name="icon"
class="form-control @error('icon') is-invalid @enderror"
accept="image/*">
<small class="text-muted">Оставьте пустым, чтобы не менять</small>
@error('icon')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Изображение</label>
<div class="form-check mb-2">
<input type="checkbox" name="remove_image" value="1"
class="form-check-input" id="remove_image"
{{ old('remove_image') ? 'checked' : '' }}>
<label class="form-check-label" for="remove_image">
Удалить изображение
</label>
</div>
@if($category->image)
<div class="mb-2">
<img src="{{ asset('assets/images/categories/images/' . $category->image) }}"
alt="{{ $category->name }}"
style="max-height: 60px;">
</div>
@endif
<input type="file" name="image"
class="form-control @error('image') is-invalid @enderror"
accept="image/*">
<small class="text-muted">Оставьте пустым, чтобы не менять</small>
@error('image')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
</div>
<div class="mb-3">
<div class="form-check">
<input type="checkbox" name="is_active" value="1"
class="form-check-input" id="is_active"
{{ old('is_active', $category->is_active) ? 'checked' : '' }}>
<label class="form-check-label" for="is_active">
Активен
</label>
</div>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-orange p-2">
<img src="{{ asset('assets/images/icons/save-svgrepo-com.svg') }}" alt="Обновить" width="20" height="20">
</button>
<a href="{{ route('admin.categories') }}" class="btn btn-dark-green p-2 d-flex gap-2 align-items-center">
<img src="{{ asset('assets/images/icons/delete-svgrepo-com.svg') }}" alt="Обновить" width="20" height="20">
Отмена
</a>
</div>
</form>
</div>
</div>
</div>
@endsection

View File

@@ -0,0 +1,347 @@
@extends('layouts.admin')
@section('content')
<div class="container-fluid">
@if(session('success'))
<div class="alert alert-success alert-dismissible fade show" role="alert">
<i class="bi bi-check-circle-fill me-2"></i> {{ session('success') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
@if(session('error'))
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<i class="bi bi-exclamation-triangle-fill me-2"></i> {{ session('error') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
<div class="card">
<div class="card-header">
<h5>Редактирование данных сайта</h5>
</div>
<div class="card-body">
<form action="{{ route('admin.contacts.update') }}" method="POST" enctype="multipart/form-data">
@csrf
@method('PUT')
<ul class="nav nav-tabs mb-4" id="contactTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="basic-tab" data-bs-toggle="tab" data-bs-target="#basic" type="button" role="tab">
Основные данные
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="images-tab" data-bs-toggle="tab" data-bs-target="#images" type="button" role="tab">
Логотип и фавикон
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="contacts-tab" data-bs-toggle="tab" data-bs-target="#contacts" type="button" role="tab">
Контакты
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="social-tab" data-bs-toggle="tab" data-bs-target="#social" type="button" role="tab">
Социальные сети
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="seo-tab" data-bs-toggle="tab" data-bs-target="#seo" type="button" role="tab">
SEO
</button>
</li>
</ul>
<div class="tab-content" id="contactTabsContent">
{{-- Основные данные --}}
<div class="tab-pane fade show active" id="basic" role="tabpanel">
<div class="mb-3">
<label class="form-label">Название сайта <span class="text-danger">*</span></label>
<input type="text"
class="form-control @error('name') is-invalid @enderror"
name="name"
value="{{ old('name', $contact->name) }}">
@error('name')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label class="form-label">Описание сайта</label>
<textarea class="form-control @error('description') is-invalid @enderror"
name="description"
rows="3">{{ old('description', $contact->description) }}</textarea>
@error('description')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label class="form-label">Режим работы</label>
<input type="text"
class="form-control @error('work_hours') is-invalid @enderror"
name="work_hours"
value="{{ old('work_hours', $contact->work_hours) }}"
placeholder="Например: Пн-Пт: 10:00 - 18:00, Сб-Вс: выходной">
@error('work_hours')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
</div>
{{-- Логотип и фавикон --}}
<div class="tab-pane fade" id="images" role="tabpanel">
<div class="mb-4">
<label class="form-label">Логотип</label>
<div class="row align-items-end">
<div class="col-md-6">
<input type="file"
class="form-control @error('logo') is-invalid @enderror"
name="logo"
id="logo-input"
accept="image/jpeg,image/png,image/jpg,image/gif,image/svg+xml,image/webp">
@error('logo')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
<small class="text-muted">Рекомендуемый размер: 200x200px. Максимум 2MB</small>
</div>
<div class="col-md-6">
@if($contact->logo)
<div class="current-image">
<p class="mb-2">Текущий логотип:</p>
<img src="{{ asset('assets/images/logo/' . $contact->logo) }}"
alt="Логотип"
id="logo-preview"
style="max-height: 100px; border: 1px solid #ddd; padding: 5px; border-radius: 4px;">
</div>
@else
<div class="current-image">
<p class="mb-2 text-muted">Логотип не загружен</p>
<img src=""
id="logo-preview"
style="max-height: 100px; display: none;">
</div>
@endif
</div>
</div>
</div>
<div class="mb-3">
<label class="form-label">Фавикон (иконка вкладки браузера)</label>
<div class="row align-items-end">
<div class="col-md-6">
<input type="file"
class="form-control @error('favicon') is-invalid @enderror"
name="favicon"
id="favicon-input"
accept="image/x-icon,image/png,image/svg+xml">
@error('favicon')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
<small class="text-muted">Рекомендуемый размер: 16x16px или 32x32px. Максимум 1MB</small>
</div>
<div class="col-md-6">
@if($contact->favicon)
<div class="current-image">
<p class="mb-2">Текущий фавикон:</p>
<img src="{{ asset('assets/images/logo/' . $contact->favicon) }}"
alt="Фавикон"
id="favicon-preview"
style="max-height: 32px; border: 1px solid #ddd; padding: 2px; border-radius: 4px;">
</div>
@else
<div class="current-image">
<p class="mb-2 text-muted">Фавикон не загружен</p>
<img src=""
id="favicon-preview"
style="max-height: 32px; display: none;">
</div>
@endif
</div>
</div>
</div>
</div>
{{-- Контакты --}}
<div class="tab-pane fade" id="contacts" role="tabpanel">
<div class="mb-3">
<label class="form-label">Телефон</label>
<input type="text"
class="form-control @error('phone') is-invalid @enderror"
name="phone"
value="{{ old('phone', $contact->phone) }}"
placeholder="+7(000)-000-00-00">
@error('phone')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label class="form-label">Email</label>
<input type="email"
class="form-control @error('email') is-invalid @enderror"
name="email"
value="{{ old('email', $contact->email) }}">
@error('email')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label class="form-label">Адрес</label>
<input type="text"
class="form-control @error('address') is-invalid @enderror"
name="address"
value="{{ old('address', $contact->address) }}">
@error('address')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
</div>
{{-- Социальные сети --}}
<div class="tab-pane fade" id="social" role="tabpanel">
<div class="mb-3">
<label class="form-label">Telegram</label>
<div class="input-group">
<span class="input-group-text">t.me/</span>
<input type="text"
class="form-control @error('telegram') is-invalid @enderror"
name="telegram"
value="{{ old('telegram', $contact->telegram) }}"
placeholder="username">
</div>
@error('telegram')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label class="form-label">WhatsApp</label>
<div class="input-group">
<span class="input-group-text">wa.me/</span>
<input type="text"
class="form-control @error('whatsapp') is-invalid @enderror"
name="whatsapp"
value="{{ old('whatsapp', $contact->whatsapp) }}"
placeholder="79991234567">
</div>
@error('whatsapp')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label class="form-label">ВКонтакте</label>
<div class="input-group">
<span class="input-group-text">vk.com/</span>
<input type="text"
class="form-control @error('vkontakte') is-invalid @enderror"
name="vkontakte"
value="{{ old('vkontakte', $contact->vkontakte) }}"
placeholder="club00000000">
</div>
@error('vkontakte')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
</div>
{{-- SEO --}}
<div class="tab-pane fade" id="seo" role="tabpanel">
<div class="mb-3">
<label class="form-label">Meta Title (заголовок страницы)</label>
<input type="text"
class="form-control @error('meta_title') is-invalid @enderror"
name="meta_title"
value="{{ old('meta_title', $contact->meta_title) }}">
<small class="text-muted">Рекомендуемая длина: 50-60 символов</small>
@error('meta_title')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label class="form-label">Meta Description (описание страницы)</label>
<textarea class="form-control @error('meta_description') is-invalid @enderror"
name="meta_description"
rows="3">{{ old('meta_description', $contact->meta_description) }}</textarea>
<small class="text-muted">Рекомендуемая длина: 150-160 символов</small>
@error('meta_description')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label class="form-label">Meta Keywords (ключевые слова)</label>
<input type="text"
class="form-control @error('meta_keywords') is-invalid @enderror"
name="meta_keywords"
value="{{ old('meta_keywords', $contact->meta_keywords) }}"
placeholder="магазин, зоотовары, корм, игрушки">
<small class="text-muted">Ключевые слова через запятую</small>
@error('meta_keywords')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
</div>
</div>
<div class="d-flex gap-2 mt-4">
<button type="submit" class="btn btn-dark-green p-2 d-flex gap-1 align-items-center justify-content-center">
<img src="{{ asset('assets/images/icons/edit-svgrepo-com.svg') }}" alt="Сохранить" width="20" height="20">
Сохранить изменения
</button>
</div>
</form>
</div>
</div>
</div>
@endsection
@section('script')
<script>
document.addEventListener('DOMContentLoaded', function() {
const logoInput = document.getElementById('logo-input');
const logoPreview = document.getElementById('logo-preview');
if (logoInput) {
logoInput.addEventListener('change', function(e) {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = function(event) {
logoPreview.src = event.target.result;
logoPreview.style.display = 'block';
if (logoPreview.closest('.current-image')) {
logoPreview.closest('.current-image').querySelector('.text-muted')?.remove();
}
};
reader.readAsDataURL(file);
}
});
}
const faviconInput = document.getElementById('favicon-input');
const faviconPreview = document.getElementById('favicon-preview');
if (faviconInput) {
faviconInput.addEventListener('change', function(e) {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = function(event) {
faviconPreview.src = event.target.result;
faviconPreview.style.display = 'block';
if (faviconPreview.closest('.current-image')) {
faviconPreview.closest('.current-image').querySelector('.text-muted')?.remove();
}
};
reader.readAsDataURL(file);
}
});
}
});
</script>
@endsection

View File

@@ -0,0 +1,113 @@
@extends('layouts.admin')
@section('content')
<div class="container-fluid">
<div class="d-none d-lg-block">
<h1 class="h2 mb-4">Панель управления</h1>
<div class="row g-4 mb-4">
<div class="col-md-3">
<div class="card tw-bg-dark-green tw-text-light-gray border-0">
<div class="card-body">
<h5 class="card-title">Пользователи</h5>
<p class="display-5">{{ $totalUsers }}</p>
<a href="" class="text-white">Управление </a>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card tw-bg-orange tw-text-light-gray border-0">
<div class="card-body">
<h5 class="card-title">Товары</h5>
<p class="display-5">{{ $totalProducts }}</p>
<a href="" class="text-white">Управление </a>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card tw-bg-dark-green tw-text-light-gray border-0">
<div class="card-body">
<h5 class="card-title">Заказы</h5>
<p class="display-5">{{ $totalOrders }}</p>
<a href="" class="text-white">Управление </a>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card tw-bg-orange tw-text-light-gray border-0">
<div class="card-body">
<h5 class="card-title">Выручка</h5>
<p class="display-5">{{ $monthlyRevenue }} </p>
<span class="text-white">За текущий месяц</span>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-4">
<div class="card">
<div class="card-header">
<h5>Последние заказы</h5>
</div>
<div class="card-body">
<table class="table">
<thead>
<tr>
<th></th>
<th>Клиент</th>
<th>Сумма</th>
<th>Статус</th>
</tr>
</thead>
<tbody>
@foreach ($recentOrders as $ord)
<tr>
<td>{{ $ord->order_number }}</td>
<td>{{ $ord->customer_name }}</td>
<td>{{ $ord->total }}</td>
<td>
<span class="badge bg-{{ $ord->delivery_status_color }}">
{{ $ord->delivery_status_name }}
</span>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
<div class="col-md-6 mb-4">
<div class="card">
<div class="card-header">
<h5>Популярные товары</h5>
</div>
<div class="card-body">
<table class="table">
<thead>
<tr>
<th>Товар</th>
<th>Кол-во продаж</th>
</tr>
</thead>
<tbody>
@foreach ($popularProducts as $product)
<tr>
<td>{{ $product->variation_name }}</td>
<td class="text-center">{{ $product->total_quantity }}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
@endsection

View File

@@ -0,0 +1,82 @@
@extends('layouts.admin')
@section('title', 'Заказы')
@section('content')
<div class="container-fluid">
@if(session('success'))
<div class="alert alert-success alert-dismissible fade show" role="alert">
<i class="bi bi-check-circle-fill me-2"></i> {{ session('success') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center flex-wrap gap-2">
<h5>Заказы</h5>
<form method="GET" action="{{ route('admin.orders') }}" class="d-flex gap-2">
<input type="text" name="search" class="form-control form-control-sm" placeholder="Поиск по номеру..." value="{{ request('search') }}" style="width: 200px;">
<select name="status" class="form-select form-select-sm" style="width: 150px;">
<option value="">Все статусы</option>
<option value="pending" {{ request('status') == 'pending' ? 'selected' : '' }}>Ожидает</option>
<option value="processing" {{ request('status') == 'processing' ? 'selected' : '' }}>В обработке</option>
<option value="shipped" {{ request('status') == 'shipped' ? 'selected' : '' }}>Отправлен</option>
<option value="delivered" {{ request('status') == 'delivered' ? 'selected' : '' }}>Доставлен</option>
<option value="cancelled" {{ request('status') == 'cancelled' ? 'selected' : '' }}>Отменён</option>
</select>
<button type="submit" class="btn btn-sm btn-dark-green">Фильтр</button>
@if(request('search') || request('status'))
<a href="{{ route('admin.orders') }}" class="btn btn-sm btn-secondary">Сбросить</a>
@endif
</form>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-bordered">
<thead>
<tr>
<th> заказа</th>
<th>Клиент</th>
<th>Сумма</th>
<th>Статус доставки</th>
<th>Статус оплаты</th>
<th>Дата</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
@forelse($orders as $order)
<tr>
<td>{{ $order->order_number }}</td>
<td>{{ $order->customer_name }}<br><small class="text-muted">{{ $order->customer_email }}</small></td>
<td>{{ number_format($order->total, 0, '.', ' ') }} </td>
<td>
<span class="badge bg-{{ $order->delivery_status_color }}">
{{ $order->delivery_status_name }}
</span>
</td>
<td>
<span class="badge bg-{{ $order->payment_status === 'paid' ? 'success' : 'warning' }}">
{{ $order->payment_status_name }}
</span>
</td>
<td>{{ $order->created_at->format('d.m.Y H:i') }}</td>
<td>
<a href="{{ route('admin.orders.show', $order->id) }}" class="btn btn-sm btn-dark-green">
<i class="bi bi-eye"></i>
</a>
</td>
</tr>
@empty
<tr>
<td colspan="7" class="text-center text-muted">Заказов не найдено</td>
</tr>
@endforelse
</tbody>
</table>
</div>
{{ $orders->links() }}
</div>
</div>
</div>
@endsection

View File

@@ -0,0 +1,109 @@
@extends('layouts.admin')
@section('title', 'Заказ ' . $order->order_number)
@section('content')
<div class="container-fluid">
<div class="d-flex justify-content-between align-items-center mb-4">
<h4>Заказ #{{ $order->order_number }}</h4>
<a href="{{ route('admin.orders') }}" class="btn btn-secondary btn-sm"> Назад к списку</a>
</div>
<div class="row">
<div class="col-md-6">
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">Информация о заказе</h5>
</div>
<div class="card-body">
<p><strong>Дата:</strong> {{ $order->created_at->format('d.m.Y H:i') }}</p>
<p><strong>Клиент:</strong> {{ $order->customer_name }}</p>
<p><strong>Email:</strong> {{ $order->customer_email }}</p>
<p><strong>Телефон:</strong> {{ $order->customer_phone }}</p>
@if($order->shipping_address)
<p><strong>Адрес доставки:</strong> {{ $order->shipping_address }}</p>
@endif
@if($order->comment)
<p><strong>Комментарий:</strong> {{ $order->comment }}</p>
@endif
</div>
</div>
</div>
<div class="col-md-6">
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">Обновить статус</h5>
</div>
<div class="card-body">
<form action="{{ route('admin.orders.update-status', $order->id) }}" method="POST">
@csrf
@method('PUT')
<div class="mb-3">
<label class="form-label">Статус доставки</label>
<select name="delivery_status" class="form-select">
@foreach($order::getDeliveryStatuses() as $value => $label)
<option value="{{ $value }}" {{ $order->delivery_status == $value ? 'selected' : '' }}>
{{ $label }}
</option>
@endforeach
</select>
</div>
<div class="mb-3">
<label class="form-label">Статус оплаты</label>
<select name="payment_status" class="form-select">
@foreach($order::getPaymentStatuses() as $value => $label)
<option value="{{ $value }}" {{ $order->payment_status == $value ? 'selected' : '' }}>
{{ $label }}
</option>
@endforeach
</select>
</div>
<button type="submit" class="btn btn-dark-green">Обновить статус</button>
</form>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h5 class="mb-0">Товары в заказе</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-bordered">
<thead>
<tr>
<th>Товар</th>
<th>Вариация</th>
<th>SKU</th>
<th>Кол-во</th>
<th>Цена</th>
<th>Сумма</th>
</tr>
</thead>
<tbody>
@foreach($order->items as $item)
<tr>
<td>{{ $item->product_name }}</td>
<td>{{ $item->variation_name ?? '—' }}</td>
<td>{{ $item->sku ?? '—' }}</td>
<td>{{ $item->quantity }}</td>
<td>{{ number_format($item->price, 0, '.', ' ') }} </td>
<td>{{ number_format($item->total, 0, '.', ' ') }} </td>
</tr>
@endforeach
</tbody>
<tfoot>
<tr>
<td colspan="5" class="text-end fw-bold">Итого:</td>
<td class="fw-bold">{{ number_format($order->total, 0, '.', ' ') }} </td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
</div>
@endsection

View File

@@ -0,0 +1,5 @@
<div class="container-fluid">
<div class="row g-2 flex-wrap">
<h1 class="h2">{{ auth()->user()->name }} | {{ auth()->user()->role->name }}</h1>
</div>
</div>

View File

@@ -0,0 +1,97 @@
<nav class="navbar navbar-dark tw-bg-dark-green">
<div class="container-fluid">
<a class="navbar-brand d-flex align-items-center gap-2" href="/">
<img src="{{ asset('assets/images/logo/' . $menuContacts->logo) }}" width="35" height="35">
Админ-панель
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#mobileMenu">
<span class="navbar-toggler-icon"></span>
</button>
</div>
</nav>
<div class="collapse" id="mobileMenu">
<div class="tw-bg-dark-green p-3">
<nav class="nav flex-column">
@can('admin_access')
<a href="{{ route('admin.dashboard') }}"
class="nav-link tw-text-light-gray {{ request()->routeIs('admin.dashboard') ? 'tw-bg-orange' : '' }} rounded mb-1">
<i class="bi bi-speedometer2 me-2"></i> Панель управления
</a>
@endcan
@can('view_orders')
<a href=""
class="nav-link tw-text-light-gray {{ request()->routeIs('admin.orders*') ? 'tw-bg-orange' : '' }} rounded mb-1">
<i class="bi bi-bag me-2"></i> Заказы
</a>
@endcan
@can('view_products')
<a href=""
class="nav-link tw-text-light-gray {{ request()->routeIs('admin.products*') ? 'tw-bg-orange' : '' }} rounded mb-1">
<i class="bi bi-box me-2"></i> Товары
</a>
@endcan
@can('manage_categories')
<a href=""
class="nav-link tw-text-light-gray {{ request()->routeIs('admin.categories*') ? 'tw-bg-orange' : '' }} rounded mb-1">
<i class="bi bi-folder me-2"></i> Категории
</a>
@endcan
@can('manage_brands')
<a href="{{ route('admin.brands') }}"
class="nav-link tw-text-light-gray {{ request()->routeIs('admin.brands*') ? 'tw-bg-orange' : '' }} rounded mb-1">
<i class="bi bi-tag me-2"></i> Бренды
</a>
@endcan
@can('view_users')
<a href=""
class="nav-link tw-text-light-gray {{ request()->routeIs('admin.users*') ? 'tw-bg-orange' : '' }} rounded mb-1">
<i class="bi bi-people me-2"></i> Пользователи
</a>
@endcan
<hr class="tw-border-light-gray my-2">
@can('manage_roles')
<a href=""
class="nav-link tw-text-light-gray {{ request()->routeIs('admin.roles*') ? 'tw-bg-orange' : '' }} rounded mb-1">
<i class="bi bi-shield me-2"></i> Роли
</a>
@endcan
@can('manage_permissions')
<a href=""
class="nav-link tw-text-light-gray {{ request()->routeIs('admin.permissions*') ? 'tw-bg-orange' : '' }} rounded mb-1">
<i class="bi bi-key me-2"></i> Права
</a>
@endcan
@can('edit_shop_settings')
<a href=""
class="nav-link tw-text-light-gray {{ request()->routeIs('admin.settings') ? 'tw-bg-orange' : '' }} rounded mb-1">
<i class="bi bi-gear me-2"></i> Настройки
</a>
@endcan
<hr class="tw-border-light-gray my-2">
<a href="/" class="nav-link tw-text-light-gray rounded mb-1">
<i class="bi bi-house me-2"></i> На сайт
</a>
<div class="mt-3 pt-2">
<form method="POST" action="{{ route('logout') }}">
@csrf
<button type="submit" class="btn btn-orange w-100">
<i class="bi bi-box-arrow-right me-1"></i> Выйти
</button>
</form>
</div>
</nav>
</div>
</div>

View File

@@ -0,0 +1,105 @@
<aside class="tw-bg-dark-green tw-text-light-gray">
<div class="p-4">
<a href="/" class="d-flex align-items-center gap-2 text-decoration-none mb-4">
<img src="{{ asset('assets/images/logo/' . $menuContacts->logo) }}" alt="{{ $menuContacts->name }}" width="50" height="50">
<span class="tw-text-light-gray fw-bold fs-4">Админ-панель</span>
</a>
{{-- Меню --}}
<nav class="nav flex-column">
{{-- Дашборд (доступен всем с admin_access) --}}
@can('admin_access')
<a href="{{ route('admin.dashboard') }}"
class="nav-link tw-text-light-gray {{ request()->is('admin/dashboard') ? 'tw-bg-orange' : '' }} rounded mb-1">
<i class="bi bi-speedometer2 me-2"></i>
Панель управления
</a>
@endcan
{{-- Заказы --}}
@can('view_orders')
<a href="{{ route('admin.orders') }}"
class="nav-link tw-text-light-gray {{ request()->is('admin/orders*') ? 'tw-bg-orange' : '' }} rounded mb-1">
<i class="bi bi-bag me-2"></i>
Заказы
</a>
@endcan
{{-- Товары --}}
@can('view_products')
<a href="/admin/products"
class="nav-link tw-text-light-gray {{ request()->is('admin/products*') ? 'tw-bg-orange' : '' }} rounded mb-1">
<i class="bi bi-box me-2"></i>
Товары
</a>
@endcan
{{-- Категории --}}
@can('manage_categories')
<a href="/admin/categories"
class="nav-link tw-text-light-gray {{ request()->is('admin/categories*') ? 'tw-bg-orange' : '' }} rounded mb-1">
<i class="bi bi-folder me-2"></i>
Категории
</a>
@endcan
{{-- Бренды --}}
@can('manage_brands')
<a href="{{ route('admin.brands') }}"
class="nav-link tw-text-light-gray {{ request()->is('admin/brands*') ? 'tw-bg-orange' : '' }} rounded mb-1">
<i class="bi bi-tag me-2"></i>
Бренды
</a>
@endcan
{{-- Пользователи --}}
@can('view_users')
<a href="/admin/users"
class="nav-link tw-text-light-gray {{ request()->is('admin/users*') ? 'tw-bg-orange' : '' }} rounded mb-1">
<i class="bi bi-people me-2"></i>
Юзеры
</a>
@endcan
@canany(['manage_roles', 'manage_permissions', 'edit_shop_settings'])
<hr class="tw-border-light-gray my-3">
@endcanany
{{-- Роли --}}
@can('manage_roles')
<a href="{{ route('admin.roles') }}"
class="nav-link tw-text-light-gray {{ request()->is('admin/roles*') ? 'tw-bg-orange' : '' }} rounded mb-1">
<i class="bi bi-shield me-2"></i>
Роли
</a>
@endcan
{{-- Права --}}
@can('manage_permissions')
<a href="{{ route('admin.permissions') }}"
class="nav-link tw-text-light-gray {{ request()->is('admin/permissions*') ? 'tw-bg-orange' : '' }} rounded mb-1">
<i class="bi bi-key me-2"></i>
Права
</a>
@endcan
{{-- Настройки магазина --}}
@can('edit_shop_settings')
<a href="{{ route('admin.contacts.edit') }}"
class="nav-link tw-text-light-gray {{ request()->is('admin/settings') ? 'tw-bg-orange' : '' }} rounded mb-1">
<i class="bi bi-gear me-2"></i>
Настройки
</a>
@endcan
{{-- Выход на сайт (для всех) --}}
<hr class="tw-border-light-gray my-3">
<a href="/"
class="nav-link tw-text-light-gray rounded mb-1">
<i class="bi bi-house me-2"></i>
На сайт
</a>
</nav>
</div>
</aside>

View File

@@ -0,0 +1,74 @@
@extends('layouts.admin')
@section('content')
<div class="container-fluid">
<div class="card">
<div class="card-header">
<h5>Редактирование права: {{ $permission->name }}</h5>
</div>
<div class="card-body">
<form action="{{ route('admin.permissions.update', $permission->id) }}" method="POST">
@csrf
@method('PUT')
<div class="mb-3">
<label class="form-label">Название <span class="text-danger">*</span></label>
<input type="text"
class="form-control @error('name') is-invalid @enderror"
name="name"
value="{{ old('name', $permission->name) }}">
@error('name')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label class="form-label">Slug <span class="text-danger">*</span></label>
<input type="text"
class="form-control @error('slug') is-invalid @enderror"
name="slug"
value="{{ old('slug', $permission->slug) }}">
<small class="text-muted">Уникальный идентификатор (например: manage_cart, admin_access)</small>
@error('slug')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label class="form-label">Группа</label>
<input type="text"
class="form-control @error('group') is-invalid @enderror"
name="group"
value="{{ old('group', $permission->group) }}"
placeholder="Например: cart, orders, products, admin">
<small class="text-muted">Для группировки прав в интерфейсе</small>
@error('group')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label class="form-label">Описание</label>
<textarea class="form-control @error('description') is-invalid @enderror"
name="description"
rows="3">{{ old('description', $permission->description) }}</textarea>
@error('description')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="d-flex gap-2 mb-4">
<button type="submit" class="btn btn-dark-green p-2 d-flex gap-1 align-items-center justify-content-center" title="Сохранить изменения">
<img src="{{ asset('assets/images/icons/edit-svgrepo-com.svg') }}" alt="Сохранить" width="20" height="20">
Сохранить
</button>
<a href="{{ route('admin.permissions') }}" class="btn btn-orange p-2 d-flex gap-1 align-items-center">
<img src="{{ asset('assets/images/icons/delete-svgrepo-com.svg') }}" alt="Отмена" width="20" height="20">
Отмена
</a>
</div>
</form>
</div>
</div>
</div>
@endsection

View File

@@ -0,0 +1,118 @@
@extends('layouts.admin')
@section('content')
<div class="container-fluid">
@if(session('success'))
<div class="alert alert-success alert-dismissible fade show" role="alert">
<i class="bi bi-check-circle-fill me-2"></i> {{ session('success') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
@if(session('error'))
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<i class="bi bi-exclamation-triangle-fill me-2"></i> {{ session('error') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
<div class="permissions-table col-12">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5>Права доступа</h5>
</div>
<div class="card-body">
<table class="table table-bordered">
<thead>
<tr>
<th style="width: 25%">Название</th>
<th style="width: 15%">Slug</th>
<th style="width: 15%">Группа</th>
<th style="width: 35%">Описание</th>
<th style="width: 10%">Действия</th>
</tr>
</thead>
<tbody>
<tr class="bg-light">
<form action="{{ route('admin.permissions.store') }}" method="POST">
@csrf
<td>
<input type="text" name="name"
class="form-control form-control-sm @error('name') is-invalid @enderror"
placeholder="Название" value="{{ old('name') }}">
@error('name')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</td>
<td>
<input type="text" name="slug"
class="form-control form-control-sm @error('slug') is-invalid @enderror"
placeholder="slug" value="{{ old('slug') }}">
@error('slug')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</td>
<td>
<input type="text" name="group"
class="form-control form-control-sm @error('group') is-invalid @enderror"
placeholder="Группа" value="{{ old('group') }}">
@error('group')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</td>
<td>
<input type="text" name="description"
class="form-control form-control-sm @error('description') is-invalid @enderror"
placeholder="Описание" value="{{ old('description') }}">
@error('description')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</td>
<td class="text-center">
<button type="submit" class="btn btn-dark-green p-1" title="Добавить">
<img src="{{ asset('assets/images/icons/add-folder-svgrepo-com.svg') }}" alt="Добавить" width="20" height="20">
</button>
</td>
</form>
</tr>
@foreach ($permissions as $permission)
<tr>
<td>{{ $permission->name }}</td>
<td><code>{{ $permission->slug }}</code></td>
<td>
@if ($permission->group)
<span class="badge bg-secondary">{{ $permission->group }}</span>
@else
<span class="text-muted"></span>
@endif
</td>
<td>{{ $permission->description ?? '—' }}</td>
<td class="text-center">
<div class="d-flex gap-2 justify-content-center">
<a href="{{ route('admin.permissions.edit', $permission->id) }}" class="btn btn-orange p-1" title="Редактировать">
<img src="{{ asset('assets/images/icons/edit-svgrepo-com.svg') }}" alt="Редактировать" width="20" height="20">
</a>
<form action="{{ route('admin.permissions.destroy', $permission->id) }}" method="POST"
onsubmit="return confirm('Удалить право «{{ $permission->name }}»?');">
@csrf
@method('DELETE')
<button class="btn btn-danger p-1" type="submit" title="Удалить">
<img src="{{ asset('assets/images/icons/delete-svgrepo-com.svg') }}" alt="Удалить" width="20" height="20">
</button>
</form>
</div>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="ps-2 pe-2">
{{ $permissions->links() }}
</div>
@endsection

View File

@@ -0,0 +1,650 @@
@extends('layouts.admin')
@section('title', 'Добавить товар')
@section('content')
<div class="container-fluid">
@if(session('success'))
<div class="alert alert-success alert-dismissible fade show" role="alert">
<i class="bi bi-check-circle-fill me-2"></i> {{ session('success') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
@if(session('error'))
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<i class="bi bi-exclamation-triangle-fill me-2"></i> {{ session('error') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
<form action="{{ route('admin.products.store') }}" method="POST" enctype="multipart/form-data" id="productForm">
@csrf
<div class="row">
<div class="col-md-8">
<div class="card mb-3">
<div class="card-header">
<h5 class="mb-0">Основная информация</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-12 mb-3">
<label class="form-label">Название <span class="text-danger">*</span></label>
<input type="text" name="name" class="form-control @error('name') is-invalid @enderror" value="{{ old('name') }}">
@error('name')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Бренд</label>
<select name="brand_id" class="form-select">
<option value="">Выберите бренд</option>
@foreach($brands as $brand)
<option value="{{ $brand->id }}" {{ old('brand_id') == $brand->id ? 'selected' : '' }}>
{{ $brand->name }}
</option>
@endforeach
</select>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Категория <span class="text-danger">*</span></label>
<select name="category_id" class="form-select @error('category_id') is-invalid @enderror">
<option value="">Выберите категорию</option>
@foreach($categories as $cat)
<option value="{{ $cat->id }}" {{ old('category_id') == $cat->id ? 'selected' : '' }}>
@for($i = 0; $i < $cat->level; $i++) @endfor {{ $cat->name }}
</option>
@endforeach
</select>
@error('category_id')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
</div>
<div class="mb-3">
<label class="form-label">Полное описание</label>
<textarea name="description" class="form-control" rows="5">{{ old('description') }}</textarea>
</div>
<div class="row">
<div class="col-12">
<div class="form-check">
<input type="checkbox" name="is_active" value="1"
class="form-check-input" id="is_active"
{{ old('is_active', true) ? 'checked' : '' }}>
<label class="form-check-label" for="is_active">Активен</label>
</div>
</div>
</div>
</div>
</div>
<div class="card mb-3">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">Характеристики товара</h5>
<button type="button" class="btn btn-sm btn-dark-green" id="addSpec">
<i class="bi bi-plus-lg"></i> Добавить характеристику
</button>
</div>
<div class="card-body">
<div id="specs-container">
<div class="row spec-item mb-2">
<div class="col-md-5">
<input type="text" name="attributes[0][key]" class="form-control form-control-sm"
placeholder="Название (например, Состав)">
</div>
<div class="col-md-6">
<input type="text" name="attributes[0][value]" class="form-control form-control-sm"
placeholder="Значение (например, индейка 30%)">
</div>
<div class="col-md-1">
<button type="button" class="btn btn-sm btn-danger remove-spec w-100">
<img src="{{ asset('assets/images/icons/delete-svgrepo-com.svg') }}" alt="Удалить" width="20" height="20" class="mx-auto">
</button>
</div>
</div>
</div>
</div>
</div>
<div class="card mb-3">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">Вариации (размеры, вес, цвет)</h5>
<button type="button" class="btn btn-sm btn-dark-green" id="addVariation">
<i class="bi bi-plus-lg"></i> Добавить вариацию
</button>
</div>
<div class="card-body">
<div id="variations-container">
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card mb-3">
<div class="card-header">
<h5 class="mb-0">SEO</h5>
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label">Meta Title</label>
<input type="text" name="meta_title" class="form-control"
value="{{ old('meta_title') }}">
<small class="text-muted">До 60 символов</small>
</div>
<div class="mb-3">
<label class="form-label">Meta Description</label>
<textarea name="meta_description" class="form-control" rows="3">{{ old('meta_description') }}</textarea>
<small class="text-muted">До 160 символов</small>
</div>
<div class="mb-3">
<label class="form-label">Meta Keywords</label>
<input type="text" name="meta_keywords" class="form-control"
value="{{ old('meta_keywords') }}">
<small class="text-muted">Ключевые слова через запятую</small>
</div>
</div>
</div>
</div>
</div>
<div class="d-flex gap-2 mb-4">
<input type="hidden" id="variationCount" value="{{ old('variations') ? count(old('variations')) : 0 }}">
<input type="hidden" id="specCount" value="{{ old('attributes') ? count(old('attributes')) : 1 }}">
<button type="submit" class="btn btn-dark-green p-2 d-flex gap-1 align-items-center justify-content-center" title="Добавить товар">
<img src="{{ asset('assets/images/icons/add-folder-svgrepo-com.svg') }}" alt="Добавить" width="20" height="20">
</button>
<a href="{{ route('admin.products') }}" class="btn btn-orange p-2 d-flex gap-1 align-items-center">
<img src="{{ asset('assets/images/icons/delete-svgrepo-com.svg') }}" alt="Отмена" width="20" height="20">
Отмена
</a>
</div>
</form>
</div>
@endsection
@section('script')
<script>
let errors = <?php echo json_encode($errors->getMessages()); ?>;
let oldInput = <?php echo json_encode(old()); ?>;
let variationIndex = parseInt(document.getElementById('variationCount').value);
let specIndex = parseInt(document.getElementById('specCount').value);
function escapeHtml(text) {
if (!text) return '';
return String(text).replace(/[&<>]/g, function(m) {
if (m === '&') return '&amp;';
if (m === '<') return '&lt;';
if (m === '>') return '&gt;';
return m;
});
}
function getVariationTemplate(index) {
let nameError = errors[`variations.${index}.name`] ? errors[`variations.${index}.name`][0] : null;
let skuError = errors[`variations.${index}.sku`] ? errors[`variations.${index}.sku`][0] : null;
let priceError = errors[`variations.${index}.price`] ? errors[`variations.${index}.price`][0] : null;
let stockError = errors[`variations.${index}.stock`] ? errors[`variations.${index}.stock`][0] : null;
let nameOld = oldInput.variations?.[index]?.name || '';
let skuOld = oldInput.variations?.[index]?.sku || '';
let priceOld = oldInput.variations?.[index]?.price || '';
let stockOld = oldInput.variations?.[index]?.stock || 0;
let oldPriceOld = oldInput.variations?.[index]?.old_price || '';
let weightOld = oldInput.variations?.[index]?.attributes?.weight || '';
const isDefaultChecked = (oldInput.variations?.[index]?.is_default !== undefined) ?
oldInput.variations?.[index]?.is_default :
(index === 0);
return `
<div class="variation-item border p-3 mb-3 rounded bg-light">
<div class="row g-2">
<div class="col-md-4">
<label class="form-label small fw-bold">Название вариации <span class="text-danger">*</span></label>
<input type="text" name="variations[${index}][name]"
class="form-control form-control-sm ${nameError ? 'is-invalid' : ''}"
value="${escapeHtml(nameOld)}">
${nameError ? `<div class="invalid-feedback">${escapeHtml(nameError)}</div>` : ''}
</div>
<div class="col-md-4">
<label class="form-label small fw-bold">Артикул (SKU) <span class="text-danger">*</span></label>
<input type="text" name="variations[${index}][sku]"
class="form-control form-control-sm ${skuError ? 'is-invalid' : ''}"
value="${escapeHtml(skuOld)}">
${skuError ? `<div class="invalid-feedback">${escapeHtml(skuError)}</div>` : ''}
</div>
<div class="col-md-4">
<label class="form-label small fw-bold">Цена () <span class="text-danger">*</span></label>
<input type="number" step="0.01" name="variations[${index}][price]"
class="form-control form-control-sm ${priceError ? 'is-invalid' : ''}"
value="${priceOld}">
${priceError ? `<div class="invalid-feedback">${escapeHtml(priceError)}</div>` : ''}
</div>
</div>
<div class="row g-2 mt-2">
<div class="col-md-4">
<label class="form-label small fw-bold">Старая цена</label>
<input type="number" step="0.01" name="variations[${index}][old_price]"
class="form-control form-control-sm"
value="${oldPriceOld}">
</div>
<div class="col-md-4">
<label class="form-label small fw-bold">Количество <span class="text-danger">*</span></label>
<input type="number" name="variations[${index}][stock]"
class="form-control form-control-sm ${stockError ? 'is-invalid' : ''}"
value="${stockOld}">
${stockError ? `<div class="invalid-feedback">${escapeHtml(stockError)}</div>` : ''}
</div>
<div class="col-md-4">
<label class="form-label small fw-bold">Вес (кг)</label>
<input type="number" step="0.01" name="variations[${index}][attributes][weight]"
class="form-control form-control-sm"
value="${weightOld}">
</div>
</div>
<div class="row g-2 mt-2">
<div class="col-12">
<label class="form-label small fw-bold">Фото для этой вариации</label>
<div id="sortable-${index}" class="sortable-container d-flex flex-wrap gap-2 mb-2"></div>
<input type="file" name="variations[${index}][images][]"
class="form-control form-control-sm variation-images-input"
data-index="${index}"
multiple accept="image/*">
<small class="text-muted">Можно выбрать несколько фото. Первое будет главным. Перетаскивайте для сортировки. Максимальный размер 2 МБ.</small>
</div>
</div>
<div class="row g-2 mt-2">
<div class="col-md-4">
<label class="form-label small">Вкус</label>
<input type="text" name="variations[${index}][attributes][flavor]"
class="form-control form-control-sm"
value="${escapeHtml(oldInput.variations?.[index]?.attributes?.flavor || '')}">
</div>
<div class="col-md-4">
<label class="form-label small">Цвет</label>
<input type="text" name="variations[${index}][attributes][color]"
class="form-control form-control-sm"
value="${escapeHtml(oldInput.variations?.[index]?.attributes?.color || '')}">
</div>
<div class="col-md-4">
<label class="form-label small">Размер</label>
<input type="text" name="variations[${index}][attributes][size]"
class="form-control form-control-sm"
value="${escapeHtml(oldInput.variations?.[index]?.attributes?.size || '')}">
</div>
</div>
<div class="col-12">
<div class="form-check mt-3">
<input type="checkbox" name="variations[${index}][is_default]" class="form-check-input" value="1" id="default_${index}"
${isDefaultChecked ? 'checked' : ''}>
<label class="form-check-label small" for="default_${index}">
Основная вариация
</label>
</div>
</div>
<div class="mt-3">
<button type="button" class="btn btn-sm btn-danger remove-variation d-flex gap-1 align-items-center justify-content-center">
<img src="{{ asset('assets/images/icons/delete-svgrepo-com.svg') }}" alt="Удалить" width="20" height="20"> Удалить
</button>
</div>
</div>
`;
}
function getSpecTemplate(index) {
return `
<div class="row spec-item mb-2">
<div class="col-md-5">
<input type="text" name="attributes[${index}][key]" class="form-control form-control-sm"
placeholder="Название характеристики">
</div>
<div class="col-md-6">
<input type="text" name="attributes[${index}][value]" class="form-control form-control-sm"
placeholder="Значение">
</div>
<div class="col-md-1">
<button type="button" class="btn btn-sm btn-danger remove-spec w-100">
<img src="{{ asset('assets/images/icons/delete-svgrepo-com.svg') }}" alt="Удалить" width="20" height="20" class="mx-auto">
</button>
</div>
</div>
`;
}
function initSortable(container, fileInput) {
let filesArray = [];
new Sortable(container, {
onEnd: function() {
const newOrder = [];
container.querySelectorAll('.file-item').forEach(item => {
const idx = parseInt(item.dataset.index);
newOrder.push(filesArray[idx]);
});
filesArray = newOrder;
updateFileInput(fileInput, filesArray);
renderPreviews(container, filesArray);
}
});
fileInput.addEventListener('change', function(e) {
filesArray = Array.from(e.target.files);
renderPreviews(container, filesArray);
updateFileInput(fileInput, filesArray);
});
}
function renderPreviews(container, files) {
container.innerHTML = files.map((file, idx) => `
<div class="file-item position-relative" data-index="${idx}" style="cursor: move;">
<img src="${URL.createObjectURL(file)}" style="max-height: 60px; width: auto;" class="border rounded">
<span class="badge bg-dark position-absolute top-0 start-0">${idx + 1}</span>
</div>
`).join('');
}
function updateFileInput(fileInput, files) {
const dataTransfer = new DataTransfer();
files.forEach(file => dataTransfer.items.add(file));
fileInput.files = dataTransfer.files;
}
// ========== УПРАВЛЕНИЕ ОСНОВНОЙ ВАРИАЦИЕЙ ==========
function initDefaultVariation() {
const defaultCheckboxes = document.querySelectorAll('input[name$="[is_default]"]');
defaultCheckboxes.forEach((checkbox, index) => {
checkbox.addEventListener('change', function() {
if (this.checked) {
defaultCheckboxes.forEach((cb, i) => {
if (i !== index) {
cb.checked = false;
}
});
}
});
});
}
// ========== ПРОВЕРКА SKU ==========
function checkSkuOnInput(input, variationIndex) {
const sku = input.value.trim();
if (!sku) return;
const checkUrl = document.querySelector('meta[name="sku-check-url"]').getAttribute('content');
fetch(checkUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
},
body: JSON.stringify({
sku: sku,
variation_id: null,
product_id: null
})
})
.then(response => response.json())
.then(data => {
if (!data.unique) {
input.classList.add('is-invalid');
let errorDiv = input.parentElement.querySelector('.invalid-feedback');
if (!errorDiv) {
errorDiv = document.createElement('div');
errorDiv.className = 'invalid-feedback';
input.parentElement.appendChild(errorDiv);
}
errorDiv.textContent = data.message;
} else {
input.classList.remove('is-invalid');
const errorDiv = input.parentElement.querySelector('.invalid-feedback');
if (errorDiv) errorDiv.remove();
}
});
}
function initSkuValidation() {
document.querySelectorAll('input[name$="[sku]"]').forEach((input, index) => {
let timeout;
input.addEventListener('input', function() {
clearTimeout(timeout);
timeout = setTimeout(() => {
checkSkuOnInput(this, index);
}, 500);
});
});
}
// ========== ОТОБРАЖЕНИЕ ОШИБОК ==========
function displayValidationErrors() {
for (let i = 0; i < variationIndex; i++) {
if (errors[`variations.${i}.name`]) {
const input = document.querySelector(`input[name="variations[${i}][name]"]`);
if (input && !input.classList.contains('is-invalid')) {
input.classList.add('is-invalid');
let errorDiv = input.parentElement.querySelector('.invalid-feedback');
if (!errorDiv) {
errorDiv = document.createElement('div');
errorDiv.className = 'invalid-feedback';
input.parentElement.appendChild(errorDiv);
}
errorDiv.textContent = errors[`variations.${i}.name`][0];
}
}
if (errors[`variations.${i}.sku`]) {
const input = document.querySelector(`input[name="variations[${i}][sku]"]`);
if (input && !input.classList.contains('is-invalid')) {
input.classList.add('is-invalid');
let errorDiv = input.parentElement.querySelector('.invalid-feedback');
if (!errorDiv) {
errorDiv = document.createElement('div');
errorDiv.className = 'invalid-feedback';
input.parentElement.appendChild(errorDiv);
}
errorDiv.textContent = errors[`variations.${i}.sku`][0];
}
}
if (errors[`variations.${i}.price`]) {
const input = document.querySelector(`input[name="variations[${i}][price]"]`);
if (input && !input.classList.contains('is-invalid')) {
input.classList.add('is-invalid');
let errorDiv = input.parentElement.querySelector('.invalid-feedback');
if (!errorDiv) {
errorDiv = document.createElement('div');
errorDiv.className = 'invalid-feedback';
input.parentElement.appendChild(errorDiv);
}
errorDiv.textContent = errors[`variations.${i}.price`][0];
}
}
if (errors[`variations.${i}.stock`]) {
const input = document.querySelector(`input[name="variations[${i}][stock]"]`);
if (input && !input.classList.contains('is-invalid')) {
input.classList.add('is-invalid');
let errorDiv = input.parentElement.querySelector('.invalid-feedback');
if (!errorDiv) {
errorDiv = document.createElement('div');
errorDiv.className = 'invalid-feedback';
input.parentElement.appendChild(errorDiv);
}
errorDiv.textContent = errors[`variations.${i}.stock`][0];
}
}
}
}
// ========== ПРОВЕРКА ВСЕХ SKU ПЕРЕД ОТПРАВКОЙ ==========
function validateAllSkus() {
const skuInputs = document.querySelectorAll('input[name$="[sku]"]');
let hasError = false;
const skuValues = [];
skuInputs.forEach((input) => {
const sku = input.value.trim();
if (sku) {
if (skuValues.includes(sku)) {
input.classList.add('is-invalid');
let errorDiv = input.parentElement.querySelector('.invalid-feedback');
if (!errorDiv) {
errorDiv = document.createElement('div');
errorDiv.className = 'invalid-feedback';
input.parentElement.appendChild(errorDiv);
}
errorDiv.textContent = 'Артикул не может дублироваться в одной форме';
hasError = true;
} else {
skuValues.push(sku);
}
}
});
if (hasError) {
alert('Пожалуйста, исправьте ошибки в артикулах');
}
return !hasError;
}
// ========== ИНИЦИАЛИЗАЦИЯ ==========
document.addEventListener('DOMContentLoaded', function() {
const variationsContainer = document.getElementById('variations-container');
const addVariationBtn = document.getElementById('addVariation');
const specsContainer = document.getElementById('specs-container');
const addSpecBtn = document.getElementById('addSpec');
// Заполняем вариации
if (oldInput.variations && Object.keys(oldInput.variations).length > 0) {
variationsContainer.innerHTML = '';
for (let i = 0; i < Object.keys(oldInput.variations).length; i++) {
variationsContainer.insertAdjacentHTML('beforeend', getVariationTemplate(i));
if (i >= variationIndex) variationIndex = i + 1;
}
} else if (variationIndex === 0) {
variationsContainer.insertAdjacentHTML('beforeend', getVariationTemplate(0));
variationIndex = 1;
} else {
for (let i = 0; i < variationIndex; i++) {
variationsContainer.insertAdjacentHTML('beforeend', getVariationTemplate(i));
}
}
// Инициализация Sortable для всех вариаций
setTimeout(() => {
document.querySelectorAll('.sortable-container').forEach(container => {
const variationIdx = container.id.split('-')[1];
const fileInput = document.querySelector(`.variation-images-input[data-index="${variationIdx}"]`);
if (fileInput) {
initSortable(container, fileInput);
}
});
}, 100);
// Добавление вариации
if (addVariationBtn) {
addVariationBtn.addEventListener('click', function() {
const newIndex = variationIndex; // Сохраняем текущий индекс
variationsContainer.insertAdjacentHTML('beforeend', getVariationTemplate(newIndex));
// Увеличиваем индекс ПОСЛЕ вставки HTML
variationIndex++;
setTimeout(() => {
const newContainer = document.getElementById(`sortable-${newIndex}`);
const newFileInput = document.querySelector(`.variation-images-input[data-index="${newIndex}"]`);
console.log('Adding new variation:', newIndex, newContainer, newFileInput);
if (newContainer && newFileInput) {
initSortable(newContainer, newFileInput);
}
initDefaultVariation();
initSkuValidation();
}, 100);
});
}
// Удаление вариаций и характеристик
document.addEventListener('click', function(e) {
if (e.target.classList.contains('remove-variation') ||
e.target.parentElement?.classList.contains('remove-variation')) {
const btn = e.target.classList.contains('remove-variation') ? e.target : e.target.parentElement;
const item = btn.closest('.variation-item');
if (item) item.remove();
}
if (e.target.classList.contains('remove-spec') ||
e.target.parentElement?.classList.contains('remove-spec')) {
const btn = e.target.classList.contains('remove-spec') ? e.target : e.target.parentElement;
const item = btn.closest('.spec-item');
if (item) item.remove();
}
});
// Заполняем характеристики
if (oldInput.attributes && Object.keys(oldInput.attributes).length > 0) {
specsContainer.innerHTML = '';
for (let i = 0; i < Object.keys(oldInput.attributes).length; i++) {
const attr = oldInput.attributes[i];
const specHtml = `
<div class="row spec-item mb-2">
<div class="col-md-5">
<input type="text" name="attributes[${i}][key]" class="form-control form-control-sm"
placeholder="Название характеристики" value="${escapeHtml(attr.key || '')}">
</div>
<div class="col-md-6">
<input type="text" name="attributes[${i}][value]" class="form-control form-control-sm"
placeholder="Значение" value="${escapeHtml(attr.value || '')}">
</div>
<div class="col-md-1">
<button type="button" class="btn btn-sm btn-danger remove-spec w-100">
<img src="{{ asset('assets/images/icons/delete-svgrepo-com.svg') }}" alt="Удалить" width="20" height="20" class="mx-auto">
</button>
</div>
</div>
`;
specsContainer.insertAdjacentHTML('beforeend', specHtml);
if (i >= specIndex) specIndex = i + 1;
}
}
// Добавление характеристики
if (addSpecBtn) {
addSpecBtn.addEventListener('click', function() {
specsContainer.insertAdjacentHTML('beforeend', getSpecTemplate(specIndex));
specIndex++;
});
}
// Инициализация всех обработчиков
initDefaultVariation();
initSkuValidation();
displayValidationErrors();
// Блокировка отправки формы при ошибках
const productForm = document.getElementById('productForm');
if (productForm) {
productForm.addEventListener('submit', function(e) {
if (!validateAllSkus()) {
e.preventDefault();
}
});
}
});
</script>
@endsection

View File

@@ -0,0 +1,832 @@
@extends('layouts.admin')
@section('title', 'Редактировать товар')
@section('content')
<div class="container-fluid">
@if(session('success'))
<div class="alert alert-success alert-dismissible fade show" role="alert">
<i class="bi bi-check-circle-fill me-2"></i> {{ session('success') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
@if(session('error'))
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<i class="bi bi-exclamation-triangle-fill me-2"></i> {{ session('error') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
<form action="{{ route('admin.products.update', $product->id) }}" method="POST" enctype="multipart/form-data" id="productForm">
@csrf
@method('PUT')
@php
$variationCount = old('variations') !== null ? count(old('variations')) : $product->variations->count();
$specCount = old('attributes') !== null ? count(old('attributes')) : $product->attributes->count();
@endphp
<input type="hidden" id="variationCount" value="{{ $variationCount }}">
<input type="hidden" id="specCount" value="{{ $specCount ?: 1 }}">
<div class="row">
<div class="col-md-8">
<div class="card mb-3">
<div class="card-header">
<h5 class="mb-0">Основная информация</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-12 mb-3">
<label class="form-label">Название <span class="text-danger">*</span></label>
<input type="text" name="name"
class="form-control @error('name') is-invalid @enderror"
value="{{ old('name', $product->name) }}">
@error('name')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Бренд</label>
<select name="brand_id" class="form-select">
<option value="">Выберите бренд</option>
@foreach($brands as $brand)
<option value="{{ $brand->id }}"
{{ old('brand_id', $product->brand_id) == $brand->id ? 'selected' : '' }}>
{{ $brand->name }}
</option>
@endforeach
</select>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Категория <span class="text-danger">*</span></label>
<select name="category_id" class="form-select @error('category_id') is-invalid @enderror">
<option value="">Выберите категорию</option>
@foreach($categories as $cat)
<option value="{{ $cat->id }}"
{{ old('category_id', $product->category_id) == $cat->id ? 'selected' : '' }}>
@for($i = 0; $i < $cat->level; $i++) @endfor {{ $cat->name }}
</option>
@endforeach
</select>
@error('category_id')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
</div>
<div class="mb-3">
<label class="form-label">Описание</label>
<textarea name="description" class="form-control" rows="5">{{ old('description', $product->description) }}</textarea>
</div>
<div class="row">
<div class="col-12">
<div class="form-check">
<input type="checkbox" name="is_active" value="1"
class="form-check-input" id="is_active"
{{ old('is_active', $product->is_active) ? 'checked' : '' }}>
<label class="form-check-label" for="is_active">Активен</label>
</div>
</div>
</div>
</div>
</div>
<div class="card mb-3">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">Характеристики товара</h5>
<button type="button" class="btn btn-sm btn-dark-green" id="addSpec">
<i class="bi bi-plus-lg"></i> Добавить характеристику
</button>
</div>
<div class="card-body">
<div id="specs-container">
@php
$productAttributes = old('attributes', $product->attributes->toArray());
$attrIndex = 0;
@endphp
@foreach($productAttributes as $key => $value)
<div class="row spec-item mb-2">
<div class="col-md-5">
<input type="text" name="attributes[{{ $attrIndex }}][key]"
class="form-control form-control-sm"
value="{{ is_array($value) ? ($value['key'] ?? '') : $key }}" placeholder="Название">
</div>
<div class="col-md-6">
<input type="text" name="attributes[{{ $attrIndex }}][value]"
class="form-control form-control-sm"
value="{{ is_array($value) ? ($value['value'] ?? '') : $value }}" placeholder="Значение">
</div>
<div class="col-md-1">
<button type="button" class="btn btn-sm btn-danger remove-spec w-100">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
@php $attrIndex++; @endphp
@endforeach
</div>
</div>
</div>
<div class="card mb-3">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">Вариации (размеры, вес, цвет)</h5>
<button type="button" class="btn btn-sm btn-dark-green" id="addVariation">
<i class="bi bi-plus-lg"></i> Добавить вариацию
</button>
</div>
<div class="card-body">
<div id="variations-container">
@foreach(old('variations', $product->variations) as $index => $variation)
@include('admin.products.partials.variation-form', [
'index' => $index,
'variation' => $variation,
'product' => $product
])
@endforeach
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card mb-3">
<div class="card-header">
<h5 class="mb-0">SEO</h5>
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label">Meta Title</label>
<input type="text" name="meta_title" class="form-control"
value="{{ old('meta_title', $product->meta_title) }}">
<small class="text-muted">До 60 символов</small>
</div>
<div class="mb-3">
<label class="form-label">Meta Description</label>
<textarea name="meta_description" class="form-control" rows="3">{{ old('meta_description', $product->meta_description) }}</textarea>
<small class="text-muted">До 160 символов</small>
</div>
<div class="mb-3">
<label class="form-label">Meta Keywords</label>
<input type="text" name="meta_keywords" class="form-control"
value="{{ old('meta_keywords', $product->meta_keywords) }}">
<small class="text-muted">Ключевые слова через запятую</small>
</div>
</div>
</div>
</div>
</div>
<div class="d-flex gap-2 mb-4">
<button type="submit" class="btn btn-dark-green">
<i class="bi bi-save"></i> Сохранить
</button>
<a href="{{ route('admin.products') }}" class="btn btn-secondary">
Отмена
</a>
</div>
</form>
</div>
@endsection
@section('script')
<script>
let variationIndex = parseInt(document.getElementById('variationCount').value);
let specIndex = parseInt(document.getElementById('specCount').value);
let errors = <?php echo json_encode($errors->getMessages()); ?>;
let oldInput = <?php echo json_encode(old()); ?>;
let deletedSkus = [];
function escapeHtml(text) {
if (!text) return '';
return String(text).replace(/[&<>]/g, function(m) {
if (m === '&') return '&amp;';
if (m === '<') return '&lt;';
if (m === '>') return '&gt;';
return m;
});
}
function showNotification(type, message) {
const alertClass = type === 'success' ? 'alert-success' : (type === 'warning' ? 'alert-warning' : 'alert-danger');
const icon = type === 'success' ? 'bi-check-circle-fill' : (type === 'warning' ? 'bi-exclamation-triangle-fill' : 'bi-x-circle-fill');
const alertHtml = `
<div class="alert ${alertClass} alert-dismissible fade show position-fixed top-0 end-0 m-3" style="z-index: 9999; min-width: 350px;" role="alert">
<i class="bi ${icon} me-2"></i> ${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
`;
document.body.insertAdjacentHTML('beforeend', alertHtml);
setTimeout(() => {
const alert = document.querySelector('.alert-dismissible');
if (alert) alert.remove();
}, 5000);
}
function handleDefaultVariation(clickedIndex) {
const allCheckboxes = document.querySelectorAll('input[name$="[is_default]"]');
allCheckboxes.forEach((checkbox, index) => {
checkbox.checked = (index === clickedIndex);
});
}
function initDefaultVariationHandlers() {
document.querySelectorAll('input[name$="[is_default]"]').forEach((checkbox, index) => {
const newCheckbox = checkbox.cloneNode(true);
checkbox.parentNode.replaceChild(newCheckbox, checkbox);
newCheckbox.addEventListener('change', function() {
if (this.checked) {
handleDefaultVariation(index);
}
});
});
}
function checkSkuOnInput(input, variationIndex, variationId = null) {
const sku = input.value.trim();
if (!sku) {
input.classList.remove('is-invalid');
const errorDiv = input.parentElement.querySelector('.invalid-feedback');
if (errorDiv) errorDiv.remove();
return;
}
if (deletedSkus.includes(sku)) {
input.classList.add('is-invalid');
let errorDiv = input.parentElement.querySelector('.invalid-feedback');
if (!errorDiv) {
errorDiv = document.createElement('div');
errorDiv.className = 'invalid-feedback';
input.parentElement.appendChild(errorDiv);
}
errorDiv.textContent = 'Этот артикул будет удален после сохранения. Сохраните изменения и попробуйте снова.';
return;
}
const checkUrl = document.querySelector('meta[name="sku-check-url"]').getAttribute('content');
const productId = document.querySelector('meta[name="product-id"]')?.getAttribute('content') || null;
fetch(checkUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
},
body: JSON.stringify({
sku: sku,
variation_id: variationId,
product_id: productId
})
})
.then(response => response.json())
.then(data => {
if (input.value.trim() !== sku) return;
if (!data.unique) {
input.classList.add('is-invalid');
let errorDiv = input.parentElement.querySelector('.invalid-feedback');
if (!errorDiv) {
errorDiv = document.createElement('div');
errorDiv.className = 'invalid-feedback';
input.parentElement.appendChild(errorDiv);
}
errorDiv.textContent = data.message;
} else {
input.classList.remove('is-invalid');
const errorDiv = input.parentElement.querySelector('.invalid-feedback');
if (errorDiv) errorDiv.remove();
}
})
.catch(error => {
console.error('Error checking SKU:', error);
});
}
function initSkuValidation() {
document.querySelectorAll('input[name$="[sku]"]').forEach((input, index) => {
const variationItem = input.closest('.variation-item');
const variationIdInput = variationItem?.querySelector('input[name$="[id]"]');
const variationId = variationIdInput ? variationIdInput.value : null;
let timeout;
input.addEventListener('input', function() {
clearTimeout(timeout);
timeout = setTimeout(() => {
checkSkuOnInput(this, index, variationId);
}, 500);
});
});
}
function validateAllSkus() {
const skuInputs = document.querySelectorAll('input[name$="[sku]"]');
let hasError = false;
const skuValues = [];
skuInputs.forEach((input) => {
const sku = input.value.trim();
if (sku) {
if (skuValues.includes(sku)) {
input.classList.add('is-invalid');
let errorDiv = input.parentElement.querySelector('.invalid-feedback');
if (!errorDiv) {
errorDiv = document.createElement('div');
errorDiv.className = 'invalid-feedback';
input.parentElement.appendChild(errorDiv);
}
errorDiv.textContent = 'Артикул не может дублироваться в одной форме';
hasError = true;
} else {
skuValues.push(sku);
}
}
});
skuInputs.forEach((input) => {
const sku = input.value.trim();
if (sku && deletedSkus.includes(sku)) {
input.classList.add('is-invalid');
let errorDiv = input.parentElement.querySelector('.invalid-feedback');
if (!errorDiv) {
errorDiv = document.createElement('div');
errorDiv.className = 'invalid-feedback';
input.parentElement.appendChild(errorDiv);
}
errorDiv.textContent = 'Этот артикул будет удален после сохранения. Сохраните изменения и попробуйте снова.';
hasError = true;
}
});
if (hasError) {
showNotification('warning', 'Пожалуйста, исправьте ошибки в артикулах');
}
return !hasError;
}
function markVariationAsDeleted(item, sku) {
item.style.opacity = '0.6';
item.style.backgroundColor = '#f8d7da';
let deleteMark = item.querySelector('.deletion-mark');
if (!deleteMark) {
deleteMark = document.createElement('div');
deleteMark.className = 'deletion-mark text-danger small mt-2';
deleteMark.innerHTML = '<i class="bi bi-trash"></i> Будет удалена после сохранения';
item.appendChild(deleteMark);
}
const inputs = item.querySelectorAll('input, textarea, select');
inputs.forEach(input => {
input.setAttribute('data-disabled', 'true');
input.style.pointerEvents = 'none';
input.style.opacity = '0.6';
});
showNotification('warning', `Артикул "${sku}" будет удален после сохранения. Не используйте его для новых вариаций до сохранения.`);
}
function updateVariationIndices() {
const variationItems = document.querySelectorAll('.variation-item');
variationItems.forEach((item, newIndex) => {
item.setAttribute('data-variation-index', newIndex);
const inputs = item.querySelectorAll('input, textarea, select');
inputs.forEach(input => {
const name = input.getAttribute('name');
if (name) {
const newName = name.replace(/variations\[\d+\]/, `variations[${newIndex}]`);
if (newName !== name) {
input.setAttribute('name', newName);
}
}
});
const elementsWithId = item.querySelectorAll('[id]');
elementsWithId.forEach(el => {
const oldId = el.id;
if (oldId.includes('existing-images-') || oldId.includes('sortable-') || oldId.includes('default_')) {
const newId = oldId.replace(/\d+/, newIndex);
el.id = newId;
}
});
const fileInputs = item.querySelectorAll('.variation-images-input');
fileInputs.forEach(input => {
input.setAttribute('data-index', newIndex);
});
});
variationIndex = variationItems.length;
}
function getNewVariationTemplate(index) {
return `
<div class="variation-item border p-3 mb-3 rounded bg-light" data-variation-index="${index}">
<input type="hidden" name="variations[${index}][id]" value="">
<div class="row g-2">
<div class="col-md-4">
<label class="form-label small fw-bold">Название вариации <span class="text-danger">*</span></label>
<input type="text" name="variations[${index}][name]" class="form-control form-control-sm" placeholder="Например: 250г, Красный, Мятный">
</div>
<div class="col-md-4">
<label class="form-label small fw-bold">Артикул (SKU) <span class="text-danger">*</span></label>
<input type="text" name="variations[${index}][sku]" class="form-control form-control-sm" placeholder="Уникальный артикул">
</div>
<div class="col-md-4">
<label class="form-label small fw-bold">Цена () <span class="text-danger">*</span></label>
<input type="number" step="0.01" name="variations[${index}][price]" class="form-control form-control-sm">
</div>
</div>
<div class="row g-2 mt-2">
<div class="col-md-4">
<label class="form-label small fw-bold">Старая цена</label>
<input type="number" step="0.01" name="variations[${index}][old_price]" class="form-control form-control-sm">
</div>
<div class="col-md-4">
<label class="form-label small fw-bold">Количество <span class="text-danger">*</span></label>
<input type="number" name="variations[${index}][stock]" class="form-control form-control-sm" value="0">
</div>
<div class="col-md-4">
<label class="form-label small fw-bold">Вес (кг)</label>
<input type="number" step="0.01" name="variations[${index}][attributes][weight]" class="form-control form-control-sm">
</div>
</div>
<div class="row g-2 mt-2">
<div class="col-12">
<label class="form-label small fw-bold">Фото для этой вариации</label>
<div id="existing-images-${index}" class="existing-images-container d-flex flex-wrap gap-2 mb-2"></div>
<div id="sortable-${index}" class="sortable-container d-flex flex-wrap gap-2 mb-2"></div>
<input type="file" name="variations[${index}][images][]" class="form-control form-control-sm variation-images-input" data-index="${index}" multiple accept="image/jpeg,image/png,image/webp">
<small class="text-muted">Можно выбрать несколько фото. Первое будет главным. Перетаскивайте для сортировки.</small>
</div>
</div>
<div class="row g-2 mt-3">
<div class="col-md-4">
<label class="form-label small">Вкус</label>
<input type="text" name="variations[${index}][attributes][flavor]" class="form-control form-control-sm">
</div>
<div class="col-md-4">
<label class="form-label small">Цвет</label>
<input type="text" name="variations[${index}][attributes][color]" class="form-control form-control-sm">
</div>
<div class="col-md-4">
<label class="form-label small">Размер</label>
<input type="text" name="variations[${index}][attributes][size]" class="form-control form-control-sm">
</div>
</div>
<div class="row mt-3">
<div class="col-12">
<div class="form-check">
<input type="checkbox" name="variations[${index}][is_default]" class="form-check-input" value="1" id="default_${index}">
<label class="form-check-label small" for="default_${index}">Основная вариация</label>
</div>
</div>
</div>
<div class="mt-3">
<button type="button" class="btn btn-sm btn-danger remove-variation">
<i class="bi bi-trash"></i> Удалить вариацию
</button>
</div>
</div>
`;
}
function getSpecTemplate(index) {
return `
<div class="row spec-item mb-2">
<div class="col-md-5">
<input type="text" name="attributes[${index}][key]" class="form-control form-control-sm" placeholder="Название">
</div>
<div class="col-md-6">
<input type="text" name="attributes[${index}][value]" class="form-control form-control-sm" placeholder="Значение">
</div>
<div class="col-md-1">
<button type="button" class="btn btn-sm btn-danger remove-spec w-100">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
`;
}
function initSortable(container, fileInput) {
let filesArray = [];
new Sortable(container, {
onEnd: function() {
const newOrder = [];
container.querySelectorAll('.file-item').forEach(item => {
const idx = parseInt(item.dataset.index);
newOrder.push(filesArray[idx]);
});
filesArray = newOrder;
updateFileInput(fileInput, filesArray);
renderNewPreviews(container, filesArray);
}
});
fileInput.addEventListener('change', function(e) {
filesArray = Array.from(e.target.files);
renderNewPreviews(container, filesArray);
updateFileInput(fileInput, filesArray);
});
}
function initExistingSortable(container) {
if (!container || container.children.length === 0) return;
new Sortable(container, {
animation: 150,
handle: '.existing-image-item',
onEnd: function() {
const newOrder = [];
container.querySelectorAll('.existing-image-item').forEach(item => {
newOrder.push(item.dataset.image);
});
updateExistingImagesOrder(container.id, newOrder);
updateMainBadge(container);
}
});
}
function renderNewPreviews(container, files) {
if (!container) return;
container.innerHTML = files.map((file, idx) => `
<div class="file-item position-relative" data-index="${idx}" style="cursor: move;">
<img src="${URL.createObjectURL(file)}" style="max-height: 60px; width: auto;" class="border rounded">
<span class="badge bg-dark position-absolute top-0 start-0">${idx + 1}</span>
</div>
`).join('');
}
function updateFileInput(fileInput, files) {
const dataTransfer = new DataTransfer();
files.forEach(file => dataTransfer.items.add(file));
fileInput.files = dataTransfer.files;
}
function updateExistingImagesOrder(containerId, newOrder) {
const container = document.getElementById(containerId);
if (!container) return;
const parts = containerId.split('-');
const variationIdx = parts[parts.length - 1];
container.querySelectorAll('input[name$="[existing_images][]"]').forEach(input => input.remove());
newOrder.forEach(imagePath => {
const input = document.createElement('input');
input.type = 'hidden';
input.name = `variations[${variationIdx}][existing_images][]`;
input.value = imagePath;
container.appendChild(input);
});
}
function updateMainBadge(container) {
const items = container.querySelectorAll('.existing-image-item');
items.forEach((item, idx) => {
const oldBadge = item.querySelector('.badge.bg-success');
if (oldBadge) oldBadge.remove();
if (idx === 0) {
const badge = document.createElement('span');
badge.className = 'badge bg-success position-absolute top-0 start-0';
badge.style.fontSize = '10px';
badge.textContent = 'Главное';
item.style.position = 'relative';
item.appendChild(badge);
}
});
}
window.removeVariationImage = function(button) {
let variationIdx = parseInt(button.dataset.variationIndex);
let imagePath = button.dataset.image;
let removedContainer = document.getElementById(`removed-images-${variationIdx}`);
if (!removedContainer) {
removedContainer = document.createElement('div');
removedContainer.id = `removed-images-${variationIdx}`;
removedContainer.style.display = 'none';
let existingContainer = document.getElementById(`existing-images-${variationIdx}`);
if (existingContainer) {
existingContainer.appendChild(removedContainer);
}
}
let removedInput = document.createElement('input');
removedInput.type = 'hidden';
removedInput.name = `variations[${variationIdx}][removed_images][]`;
removedInput.value = imagePath;
removedContainer.appendChild(removedInput);
let imageDiv = button.closest('.existing-image-item');
if (imageDiv) {
imageDiv.remove();
}
updateOrderAfterRemoval(variationIdx);
}
function updateOrderAfterRemoval(variationIdx) {
const container = document.getElementById(`existing-images-${variationIdx}`);
if (!container) return;
const items = container.querySelectorAll('.existing-image-item');
const newOrder = Array.from(items).map(item => item.dataset.image);
updateExistingImagesOrder(container.id, newOrder);
updateMainBadge(container);
}
function initDeleteVariationHandler() {
document.addEventListener('click', function(e) {
if (e.target.classList.contains('remove-variation') || e.target.closest('.remove-variation')) {
const btn = e.target.classList.contains('remove-variation') ? e.target : e.target.closest('.remove-variation');
const item = btn.closest('.variation-item');
if (item && confirm('Удалить эту вариацию?')) {
const skuInput = item.querySelector('input[name$="[sku]"]');
const sku = skuInput ? skuInput.value.trim() : null;
if (sku) {
deletedSkus.push(sku);
markVariationAsDeleted(item, sku);
}
item.remove();
updateVariationIndices();
if (sku) {
showNotification('warning', `Артикул "${sku}" будет удален после сохранения. Не используйте его для новых вариаций до сохранения.`);
}
}
}
});
}
document.addEventListener('DOMContentLoaded', function() {
deletedSkus = [];
setTimeout(function() {
document.querySelectorAll('.existing-images-container').forEach(container => {
if (container.children.length > 0) {
initExistingSortable(container);
}
});
document.querySelectorAll('.sortable-container').forEach(container => {
const variationIdx = container.id.split('-')[1];
const fileInput = document.querySelector(`.variation-images-input[data-index="${variationIdx}"]`);
if (fileInput) {
initSortable(container, fileInput);
}
});
}, 500);
initSkuValidation();
initDefaultVariationHandlers();
initDeleteVariationHandler();
document.getElementById('addVariation')?.addEventListener('click', function() {
const container = document.getElementById('variations-container');
container.insertAdjacentHTML('beforeend', getNewVariationTemplate(variationIndex));
setTimeout(() => {
updateVariationIndices();
initSkuValidation();
initDefaultVariationHandlers();
document.querySelectorAll('.sortable-container').forEach(container => {
const variationIdx = container.id.split('-')[1];
const fileInput = document.querySelector(`.variation-images-input[data-index="${variationIdx}"]`);
if (fileInput && !container.hasAttribute('data-initialized')) {
initSortable(container, fileInput);
container.setAttribute('data-initialized', 'true');
}
});
}, 100);
});
document.getElementById('addSpec')?.addEventListener('click', function() {
const container = document.getElementById('specs-container');
container.insertAdjacentHTML('beforeend', getSpecTemplate(specIndex));
specIndex++;
});
document.addEventListener('click', function(e) {
if (e.target.classList.contains('remove-spec') || e.target.closest('.remove-spec')) {
const btn = e.target.classList.contains('remove-spec') ? e.target : e.target.closest('.remove-spec');
const item = btn.closest('.spec-item');
if (item) item.remove();
}
});
const productForm = document.getElementById('productForm');
if (productForm) {
productForm.addEventListener('submit', function(e) {
if (!validateAllSkus()) {
e.preventDefault();
}
});
}
});
function displayValidationErrors() {
for (let i = 0; i < variationIndex; i++) {
if (errors[`variations.${i}.name`]) {
const nameInput = document.querySelector(`input[name="variations[${i}][name]"]`);
if (nameInput && !nameInput.classList.contains('is-invalid')) {
nameInput.classList.add('is-invalid');
let errorDiv = nameInput.parentElement.querySelector('.invalid-feedback');
if (!errorDiv) {
errorDiv = document.createElement('div');
errorDiv.className = 'invalid-feedback';
nameInput.parentElement.appendChild(errorDiv);
}
errorDiv.textContent = errors[`variations.${i}.name`][0];
}
}
if (errors[`variations.${i}.sku`]) {
const skuInput = document.querySelector(`input[name="variations[${i}][sku]"]`);
if (skuInput && !skuInput.classList.contains('is-invalid')) {
skuInput.classList.add('is-invalid');
let errorDiv = skuInput.parentElement.querySelector('.invalid-feedback');
if (!errorDiv) {
errorDiv = document.createElement('div');
errorDiv.className = 'invalid-feedback';
skuInput.parentElement.appendChild(errorDiv);
}
errorDiv.textContent = errors[`variations.${i}.sku`][0];
}
}
if (errors[`variations.${i}.price`]) {
const priceInput = document.querySelector(`input[name="variations[${i}][price]"]`);
if (priceInput && !priceInput.classList.contains('is-invalid')) {
priceInput.classList.add('is-invalid');
let errorDiv = priceInput.parentElement.querySelector('.invalid-feedback');
if (!errorDiv) {
errorDiv = document.createElement('div');
errorDiv.className = 'invalid-feedback';
priceInput.parentElement.appendChild(errorDiv);
}
errorDiv.textContent = errors[`variations.${i}.price`][0];
}
}
if (errors[`variations.${i}.stock`]) {
const stockInput = document.querySelector(`input[name="variations[${i}][stock]"]`);
if (stockInput && !stockInput.classList.contains('is-invalid')) {
stockInput.classList.add('is-invalid');
let errorDiv = stockInput.parentElement.querySelector('.invalid-feedback');
if (!errorDiv) {
errorDiv = document.createElement('div');
errorDiv.className = 'invalid-feedback';
stockInput.parentElement.appendChild(errorDiv);
}
errorDiv.textContent = errors[`variations.${i}.stock`][0];
}
}
const imageErrors = Object.keys(errors).filter(key => key.startsWith(`variations.${i}.images.`));
if (imageErrors.length > 0) {
const fileInput = document.querySelector(`.variation-images-input[data-index="${i}"]`);
if (fileInput && !fileInput.classList.contains('is-invalid')) {
fileInput.classList.add('is-invalid');
let errorDiv = fileInput.parentElement.querySelector('.invalid-feedback');
if (!errorDiv) {
errorDiv = document.createElement('div');
errorDiv.className = 'invalid-feedback';
fileInput.parentElement.appendChild(errorDiv);
}
errorDiv.textContent = errors[imageErrors[0]][0];
}
}
}
}
document.addEventListener('DOMContentLoaded', function() {
setTimeout(displayValidationErrors, 100);
});
const addVariationBtn = document.getElementById('addVariation');
if (addVariationBtn) {
addVariationBtn.addEventListener('click', function() {
setTimeout(displayValidationErrors, 200);
});
}
</script>
@endsection

View File

@@ -0,0 +1,180 @@
<div class="variation-item border p-3 mb-3 rounded bg-light" data-variation-index="{{ $index }}">
<input type="hidden" name="variations[{{ $index }}][id]" value="{{ is_array($variation) ? ($variation['id'] ?? '') : ($variation->id ?? '') }}">
<div class="row g-2">
<div class="col-md-4">
<label class="form-label small fw-bold">Название вариации <span class="text-danger">*</span></label>
<input type="text" name="variations[{{ $index }}][name]"
class="form-control form-control-sm @error(" variations.$index.name") is-invalid @enderror"
value="{{ old("variations.$index.name", is_array($variation) ? ($variation['name'] ?? '') : ($variation->name ?? '')) }}">
@error("variations.$index.name")
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="col-md-4">
<label class="form-label small fw-bold">Артикул (SKU) <span class="text-danger">*</span></label>
<input type="text" name="variations[{{ $index }}][sku]"
class="form-control form-control-sm @error(" variations.$index.sku") is-invalid @enderror"
value="{{ old("variations.$index.sku", is_array($variation) ? ($variation['sku'] ?? '') : ($variation->sku ?? '')) }}">
@error("variations.$index.sku")
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="col-md-4">
<label class="form-label small fw-bold">Цена () <span class="text-danger">*</span></label>
<input type="number" step="0.01" name="variations[{{ $index }}][price]"
class="form-control form-control-sm @error(" variations.$index.price") is-invalid @enderror"
value="{{ old("variations.$index.price", is_array($variation) ? ($variation['price'] ?? '') : ($variation->price ?? '')) }}">
@error("variations.$index.price")
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
</div>
<div class="row g-2 mt-2">
<div class="col-md-4">
<label class="form-label small fw-bold">Старая цена</label>
<input type="number" step="0.01" name="variations[{{ $index }}][old_price]"
class="form-control form-control-sm"
value="{{ old("variations.$index.old_price", is_array($variation) ? ($variation['old_price'] ?? '') : ($variation->old_price ?? '')) }}">
</div>
<div class="col-md-4">
<label class="form-label small fw-bold">Количество <span class="text-danger">*</span></label>
<input type="number" name="variations[{{ $index }}][stock]"
class="form-control form-control-sm @error(" variations.$index.stock") is-invalid @enderror"
value="{{ old("variations.$index.stock", is_array($variation) ? ($variation['stock'] ?? 0) : ($variation->stock ?? 0)) }}">
@error("variations.$index.stock")
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="col-md-4">
<label class="form-label small fw-bold">Вес (кг)</label>
@php
$weightValue = '';
if (is_array($variation)) {
$weightValue = $variation['attributes']['weight'] ?? '';
} else {
$weightValue = $variation->attributes->where('key', 'weight')->first()?->value ?? '';
}
@endphp
<input type="number" step="0.01" name="variations[{{ $index }}][attributes][weight]"
class="form-control form-control-sm"
value="{{ old("variations.$index.attributes.weight", $weightValue) }}">
</div>
</div>
<div class="row g-2 mt-2">
<div class="col-12">
<label class="form-label small fw-bold">Фото для этой вариации</label>
<div id="existing-images-{{ $index }}" class="existing-images-container d-flex flex-wrap gap-2 mb-2">
@php
$imagesArray = [];
if (!is_array($variation)) {
$imagesArray = $variation->images->pluck('path')->toArray();
} else {
$variationId = $variation['id'] ?? null;
if ($variationId && isset($product) && $product->variations) {
$originalVariation = $product->variations->where('id', $variationId)->first();
if ($originalVariation) {
$imagesArray = $originalVariation->images->pluck('path')->toArray();
}
}
}
@endphp
@foreach($imagesArray as $idx => $imagePath)
<div class="existing-image-item position-relative" data-image="{{ $imagePath }}" data-index="{{ $idx }}" style="cursor: move;">
<img src="{{ asset($imagePath) }}" style="max-height: 60px; width: auto;" class="border rounded">
@if($idx === 0)
<span class="badge bg-success position-absolute top-0 start-0" style="font-size: 10px;">Главное</span>
@endif
<button type="button"
class="btn btn-sm btn-danger position-absolute top-0 end-0 p-0"
style="font-size: 14px; line-height: 1; width: 20px; height: 20px; border-radius: 50%;"
data-variation-index="{{ $index }}"
data-image="{{ $imagePath }}"
onclick="removeVariationImage(this)">
</button>
</div>
@endforeach
</div>
<div id="sortable-{{ $index }}" class="sortable-container d-flex flex-wrap gap-2 mb-2"></div>
<input type="file" name="variations[{{ $index }}][images][]"
class="form-control form-control-sm variation-images-input @error(" variations.{$index}.images.*") is-invalid @enderror"
data-index="{{ $index }}"
multiple accept="image/*">
@error("variations.{$index}.images.*")
<div class="invalid-feedback d-block">{{ $message }}</div>
@enderror
<small class="text-muted">Можно выбрать несколько фото. Первое будет главным. Перетаскивайте для сортировки.</small>
</div>
</div>
<div class="row g-2 mt-2">
<div class="col-md-4">
<label class="form-label small">Вкус</label>
@php
$flavorValue = '';
if (is_array($variation)) {
$flavorValue = $variation['attributes']['flavor'] ?? '';
} else {
$flavorValue = $variation->attributes->where('key', 'flavor')->first()?->value ?? '';
}
@endphp
<input type="text" name="variations[{{ $index }}][attributes][flavor]"
class="form-control form-control-sm"
value="{{ old("variations.$index.attributes.flavor", $flavorValue) }}">
</div>
<div class="col-md-4">
<label class="form-label small">Цвет</label>
@php
$colorValue = '';
if (is_array($variation)) {
$colorValue = $variation['attributes']['color'] ?? '';
} else {
$colorValue = $variation->attributes->where('key', 'color')->first()?->value ?? '';
}
@endphp
<input type="text" name="variations[{{ $index }}][attributes][color]"
class="form-control form-control-sm"
value="{{ old("variations.$index.attributes.color", $colorValue) }}">
</div>
<div class="col-md-4">
<label class="form-label small">Размер</label>
@php
$sizeValue = '';
if (is_array($variation)) {
$sizeValue = $variation['attributes']['size'] ?? '';
} else {
$sizeValue = $variation->attributes->where('key', 'size')->first()?->value ?? '';
}
@endphp
<input type="text" name="variations[{{ $index }}][attributes][size]"
class="form-control form-control-sm"
value="{{ old("variations.$index.attributes.size", $sizeValue) }}">
</div>
</div>
<div class="col-12">
<div class="form-check mt-3">
<input type="checkbox" name="variations[{{ $index }}][is_default]"
class="form-check-input" value="1" id="default_{{ $index }}"
{{ old("variations.$index.is_default", is_array($variation) ? ($variation['is_default'] ?? false) : ($variation->is_default ?? false)) ? 'checked' : '' }}>
<label class="form-check-label small" for="default_{{ $index }}">
Основная вариация
</label>
</div>
</div>
<div class="mt-3">
<button type="button" class="btn btn-sm btn-danger remove-variation">
<i class="bi bi-trash"></i> Удалить вариацию
</button>
</div>
</div>

View File

@@ -0,0 +1,164 @@
@extends('layouts.admin')
@section('title', 'Товары')
@section('content')
<div class="container-fluid">
@if(session('success'))
<div class="alert alert-success alert-dismissible fade show" role="alert">
<i class="bi bi-check-circle-fill me-2"></i> {{ session('success') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
@if(session('error'))
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<i class="bi bi-exclamation-triangle-fill me-2"></i> {{ session('error') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
<div class="products-table col-12 mb-5">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5>Товары</h5>
<a href="{{ route('admin.products.create') }}" class="btn btn-sm btn-dark-green">
<i class="bi bi-plus-lg"></i> Добавить товар
</a>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Фото</th>
<th>Название</th>
<th>Категория</th>
<th>Бренд</th>
<th>Вариации</th>
<th>Цена</th>
<th>Остаток</th>
<th>Статус</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
@forelse($products as $product)
<tr>
<td>
<img src="{{ $product->main_image }}"
alt="{{ $product->name }}"
style="width: 50px; height: 50px; object-fit: cover;"
class="rounded">
</td>
<td>
<strong>{{ $product->name }}</strong>
</td>
<td>{{ $product->category->name ?? '-' }}</td>
<td>{{ $product->brand->name ?? '-' }}</td>
<td style="min-width: 180px;">
<div class="variations-table">
@foreach($product->variations as $index => $variation)
<div class="d-flex justify-content-between align-items-center small mb-1 tw-bg-orange rounded p-1 tw-text-light-gray
{{ $index >= 3 ? 'd-none variations-more' : '' }}">
<span class="text-truncate" style="max-width: 100px;"
title="{{ $variation->name }}">
{{ Str::limit($variation->name, 15) }}
</span>
<span class="text-nowrap">
<span>{{ number_format($variation->price) }} </span>
<span class="ms-1">×{{ $variation->stock }}</span>
</span>
</div>
@endforeach
@if($product->variations->count() > 3)
<a href="#" class="small toggle-variations" data-target="{{ $product->id }}">
Показать ещё {{ $product->variations->count() - 3 }}
</a>
@endif
</div>
</td>
<td>{{ $product->price_range }}</td>
<td>
@if($product->total_stock > 0)
<span class="badge bg-success">{{ $product->total_stock }} шт.</span>
@else
<span class="badge bg-danger">Нет</span>
@endif
</td>
<td>
@if($product->is_active)
<span class="badge bg-success">Активен</span>
@else
<span class="badge bg-secondary">Неактивен</span>
@endif
</td>
<td class="text-nowrap">
<div class="d-flex gap-2 justify-content-center">
<a href="{{ route('admin.products.edit', $product->id) }}"
class="btn btn-orange p-1 d-inline-flex align-items-center justify-content-center"
style="width: 32px; height: 32px;"
title="Редактировать">
<img src="{{ asset('assets/images/icons/edit-svgrepo-com.svg') }}" alt="Редактировать" width="20" height="20">
</a>
<form action="{{ route('admin.products.destroy', $product->id) }}"
method="POST"
onsubmit="return confirm('Удалить товар «{{ $product->name }}»? Все вариации также будут удалены.');">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-danger p-1 d-inline-flex align-items-center justify-content-center"
style="width: 32px; height: 32px;"
title="Удалить">
<img src="{{ asset('assets/images/icons/delete-svgrepo-com.svg') }}" alt="Удалить" width="20" height="20">
</button>
</form>
<form action="{{ route('admin.products.duplicate', $product->id) }}"
method="POST">
@csrf
<button type="submit" class="btn btn-dark-green p-1 d-inline-flex align-items-center justify-content-center"
style="width: 32px; height: 32px;"
title="Копировать">
<img src="{{ asset('assets/images/icons/copy-svgrepo-com.svg') }}" alt="Копировать" width="20" height="20">
</button>
</form>
</div>
</td>
</tr>
@empty
<tr>
<td colspan="10" class="text-center text-muted py-4">
<i class="bi bi-box fs-1 d-block mb-2"></i>
Нет товаров.
<a href="{{ route('admin.products.create') }}">Добавить первый товар</a>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
<div class="ps-2 pe-2">
@if(method_exists($products, 'hasPages') && $products->hasPages())
{{ $products->links() }}
@endif
</div>
</div>
</div>
</div>
</div>
@endsection
@section('script')
<script>
document.querySelectorAll('.toggle-variations').forEach(btn => {
btn.addEventListener('click', function(e) {
e.preventDefault();
const container = this.closest('.variations-table');
const hidden = container.querySelectorAll('.variations-more');
hidden.forEach(el => el.classList.toggle('d-none'));
this.textContent = this.textContent.includes('ещё') ? 'Свернуть' : 'Показать ещё';
});
});
</script>
@endsection

View File

@@ -0,0 +1,79 @@
@extends('layouts.admin')
@section('content')
<div class="container-fluid">
<div class="card">
<div class="card-header">
<h5>Создание роли</h5>
</div>
<div class="card-body">
<form action="{{ route('admin.roles.store') }}" method="POST">
@csrf
<div class="mb-3">
<label class="form-label">Название роли <span class="text-danger">*</span></label>
<input type="text"
class="form-control @error('name') is-invalid @enderror"
name="name"
value="{{ old('name') }}">
@error('name')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label class="form-label">Slug <span class="text-danger">*</span></label>
<input type="text"
class="form-control @error('slug') is-invalid @enderror"
name="slug"
value="{{ old('slug') }}">
<small class="text-muted">Уникальный идентификатор роли (например: admin, manager)</small>
@error('slug')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label class="form-label">Описание</label>
<textarea class="form-control @error('description') is-invalid @enderror"
name="description"
rows="3">{{ old('description') }}</textarea>
@error('description')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label class="form-label">Права доступа</label>
<div class="row">
@foreach($permissions as $permission)
<div class="col-md-3">
<div class="form-check">
<input class="form-check-input"
type="checkbox"
name="permissions[]"
value="{{ $permission->id }}"
id="perm_{{ $permission->id }}">
<label class="form-check-label" for="perm_{{ $permission->id }}">
{{ $permission->name }}
</label>
</div>
</div>
@endforeach
</div>
</div>
<div class="d-flex gap-2 mb-4">
<button type="submit" class="btn btn-dark-green p-2 d-flex gap-1 align-items-center justify-content-center" title="Добавить роль">
<img src="{{ asset('assets/images/icons/add-folder-svgrepo-com.svg') }}" alt="Добавить" width="20" height="20">
</button>
<a href="{{ route('admin.roles') }}" class="btn btn-orange p-2 d-flex gap-1 align-items-center">
<img src="{{ asset('assets/images/icons/delete-svgrepo-com.svg') }}" alt="Отмена" width="20" height="20">
Отмена
</a>
</div>
</form>
</div>
</div>
</div>
@endsection

View File

@@ -0,0 +1,91 @@
@extends('layouts.admin')
@section('content')
<div class="container-fluid">
<div class="card">
<div class="card-header">
<h5>Редактирование роли: {{ $role->name }}</h5>
</div>
<div class="card-body">
<form action="{{ route('admin.roles.update', $role->id) }}" method="POST">
@csrf
@method('PUT')
<div class="mb-3">
<label class="form-label">Название роли <span class="text-danger">*</span></label>
<input type="text"
class="form-control @error('name') is-invalid @enderror"
name="name"
value="{{ old('name', $role->name) }}">
@error('name')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label class="form-label">Slug <span class="text-danger">*</span></label>
<input type="text"
class="form-control @error('slug') is-invalid @enderror"
name="slug"
value="{{ old('slug', $role->slug) }}">
<small class="text-muted">Уникальный идентификатор роли (например: admin, manager)</small>
@error('slug')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label class="form-label">Описание</label>
<textarea class="form-control @error('description') is-invalid @enderror"
name="description"
rows="3">{{ old('description', $role->description) }}</textarea>
@error('description')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label class="form-label">Права доступа</label>
<div class="row">
@foreach($permissions as $permission)
@php
$checked = false;
if (old('permissions')) {
$checked = in_array($permission->id, old('permissions'));
}
else {
$checked = $role->permissions->contains($permission->id);
}
@endphp
<div class="col-md-3">
<div class="form-check">
<input class="form-check-input"
type="checkbox"
name="permissions[]"
value="{{ $permission->id }}"
id="perm_{{ $permission->id }}"
{{ $checked ? 'checked' : '' }}>
<label class="form-check-label" for="perm_{{ $permission->id }}">
{{ $permission->name }}
</label>
</div>
</div>
@endforeach
</div>
</div>
<div class="d-flex gap-2 mb-4">
<button type="submit" class="btn btn-dark-green p-2 d-flex gap-1 align-items-center justify-content-center" title="Сохранить изменения">
<img src="{{ asset('assets/images/icons/edit-svgrepo-com.svg') }}" alt="Сохранить" width="20" height="20">
Сохранить
</button>
<a href="{{ route('admin.roles') }}" class="btn btn-orange p-2 d-flex gap-1 align-items-center">
<img src="{{ asset('assets/images/icons/delete-svgrepo-com.svg') }}" alt="Отмена" width="20" height="20">
Отмена
</a>
</div>
</form>
</div>
</div>
</div>
@endsection

View File

@@ -0,0 +1,90 @@
@extends('layouts.admin')
@section('content')
<div class="container-fluid">
@if(session('success'))
<div class="alert alert-success alert-dismissible fade show" role="alert">
<i class="bi bi-check-circle-fill me-2"></i> {{ session('success') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
@if(session('error'))
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<i class="bi bi-exclamation-triangle-fill me-2"></i> {{ session('error') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
<div class="roles-table col-12">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5>Роли пользователей</h5>
<a href="{{ route('admin.roles.create') }}" class="btn btn-sm btn-dark-green">
<i class="bi bi-plus-lg"></i> Добавить роль
</a>
</div>
<div class="card-body">
<table class="table table-bordered">
<thead>
<tr>
<th>Название</th>
<th>Slug</th>
<th>Описание</th>
<th>Права</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
@foreach ($roles as $role)
<tr>
<td>{{ $role->name }}</td>
<td>
<code>{{ $role->slug }}</code>
</td>
<td>
@if ($role->description)
{{ Str::limit($role->description, 100) }}
@else
<span class="text-muted"></span>
@endif
</td>
<td>
@if($role->permissions->count())
@foreach ($role->permissions as $permission)
<span class="badge bg-secondary mb-1">{{ $permission->name }}</span>
@endforeach
@elseif($role->slug === 'super_admin')
<span class="text-muted">Все права</span>
@else
<span class="text-muted">Нет прав</span>
@endif
</td>
<td class="text-center">
<div class="d-flex gap-2 justify-content-center">
<a href="{{ route('admin.roles.edit', $role->id) }}" class="btn btn-orange p-1" title="Редактировать">
<img src="{{ asset('assets/images/icons/edit-svgrepo-com.svg') }}" alt="Редактировать" width="20" height="20">
</a>
<form action="{{ route('admin.roles.destroy', $role->id) }}" method="POST"
onsubmit="return confirm('Удалить роль «{{ $role->name }}»?');">
@csrf
@method('DELETE')
<button class="btn btn-danger p-1" type="submit" title="Удалить">
<img src="{{ asset('assets/images/icons/delete-svgrepo-com.svg') }}" alt="Удалить" width="20" height="20">
</button>
</form>
</div>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="ps-2 pe-2">
{{ $roles->links() }}
</div>
@endsection

View File

@@ -0,0 +1,100 @@
@extends('layouts.admin')
@section('content')
<div class="container-fluid">
<div class="card">
<div class="card-header">
<h5>Создание пользователя</h5>
</div>
<div class="card-body">
<form action="{{ route('admin.users.store') }}" method="POST">
@csrf
<div class="mb-3">
<label class="form-label">Имя <span class="text-danger">*</span></label>
<input type="text"
class="form-control @error('name') is-invalid @enderror"
name="name"
value="{{ old('name') }}">
@error('name')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label class="form-label">Email <span class="text-danger">*</span></label>
<input type="email"
class="form-control @error('email') is-invalid @enderror"
name="email"
value="{{ old('email') }}">
@error('email')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label class="form-label">Телефон</label>
<input type="text"
class="form-control @error('phone') is-invalid @enderror"
name="phone" id="phone-mask" placeholder="+7(___)-___-__-__"
value="{{ old('phone') }}">
@error('phone')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label class="form-label">Пароль <span class="text-danger">*</span></label>
<input type="password"
class="form-control @error('password') is-invalid @enderror"
name="password">
<small class="text-muted">Минимум 8 символов</small>
@error('password')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label class="form-label">Подтверждение пароля <span class="text-danger">*</span></label>
<input type="password"
class="form-control @error('password_confirmation') is-invalid @enderror"
name="password_confirmation">
@error('password_confirmation')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label class="form-label">Роль <span class="text-danger">*</span></label>
<select class="form-select @error('role_id') is-invalid @enderror" name="role_id">
<option value="">Выберите роль</option>
@foreach($roles as $role)
<option value="{{ $role->id }}" {{ old('role_id') == $role->id ? 'selected' : '' }}>
{{ $role->name }}
</option>
@endforeach
</select>
@error('role_id')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="d-flex gap-2 mb-4">
<button type="submit" class="btn btn-dark-green p-2 d-flex gap-1 align-items-center justify-content-center" title="Добавить пользователя">
<img src="{{ asset('assets/images/icons/add-folder-svgrepo-com.svg') }}" alt="Добавить" width="20" height="20">
Добавить
</button>
<a href="{{ route('admin.users') }}" class="btn btn-orange p-2 d-flex gap-1 align-items-center">
<img src="{{ asset('assets/images/icons/delete-svgrepo-com.svg') }}" alt="Отмена" width="20" height="20">
Отмена
</a>
</div>
</form>
</div>
</div>
</div>
@endsection
@section('script')
<script src="{{ asset('assets/js/phone_mask.js') }}"></script>
@endsection

View File

@@ -0,0 +1,98 @@
@extends('layouts.admin')
@section('content')
<div class="container-fluid">
<div class="card">
<div class="card-header">
<h5>Редактирование пользователя: {{ $user->name }}</h5>
</div>
<div class="card-body">
<form action="{{ route('admin.users.update', $user->id) }}" method="POST">
@csrf
@method('PUT')
<div class="mb-3">
<label class="form-label">Имя <span class="text-danger">*</span></label>
<input type="text"
class="form-control @error('name') is-invalid @enderror"
name="name"
value="{{ old('name', $user->name) }}">
@error('name')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label class="form-label">Email <span class="text-danger">*</span></label>
<input type="email"
class="form-control @error('email') is-invalid @enderror"
name="email"
value="{{ old('email', $user->email) }}">
@error('email')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label class="form-label">Телефон</label>
<input type="text"
class="form-control @error('phone') is-invalid @enderror"
name="phone" id="phone-mask" placeholder="+7(___)-___-__-__"
value="{{ old('phone', $user->phone) }}">
@error('phone')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label class="form-label">Новый пароль</label>
<input type="password"
class="form-control @error('password') is-invalid @enderror"
name="password">
<small class="text-muted">Оставьте пустым, если не хотите менять пароль. Минимум 8 символов</small>
@error('password')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label class="form-label">Подтверждение пароля</label>
<input type="password"
class="form-control @error('password_confirmation') is-invalid @enderror"
name="password_confirmation">
</div>
<div class="mb-3">
<label class="form-label">Роль <span class="text-danger">*</span></label>
<select class="form-select @error('role_id') is-invalid @enderror" name="role_id">
<option value="">Выберите роль</option>
@foreach($roles as $role)
<option value="{{ $role->id }}" {{ old('role_id', $user->role_id) == $role->id ? 'selected' : '' }}>
{{ $role->name }}
</option>
@endforeach
</select>
@error('role_id')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="d-flex gap-2 mb-4">
<button type="submit" class="btn btn-dark-green p-2 d-flex gap-1 align-items-center justify-content-center" title="Сохранить изменения">
<img src="{{ asset('assets/images/icons/edit-svgrepo-com.svg') }}" alt="Сохранить" width="20" height="20">
Сохранить
</button>
<a href="{{ route('admin.users') }}" class="btn btn-orange p-2 d-flex gap-1 align-items-center">
<img src="{{ asset('assets/images/icons/delete-svgrepo-com.svg') }}" alt="Отмена" width="20" height="20">
Отмена
</a>
</div>
</form>
</div>
</div>
</div>
@endsection
@section('script')
<script src="{{ asset('assets/js/phone_mask.js') }}"></script>
@endsection

View File

@@ -0,0 +1,104 @@
@extends('layouts.admin')
@section('content')
<div class="container-fluid">
@if(session('success'))
<div class="alert alert-success alert-dismissible fade show" role="alert">
<i class="bi bi-check-circle-fill me-2"></i> {{ session('success') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
@if(session('error'))
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<i class="bi bi-exclamation-triangle-fill me-2"></i> {{ session('error') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
<div class="users-table col-12">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center flex-wrap gap-2">
<h5>Пользователи</h5>
<div class="d-flex gap-2">
<form method="GET" action="{{ route('admin.users') }}" class="d-flex gap-2">
<input type="text" id="search"
name="search"
class="form-control form-control-sm"
placeholder="Поиск по имени или email..."
value="{{ request('search') }}"
style="width: 250px;">
<button type="submit" class="btn btn-sm btn-dark-green">
<i class="bi bi-search"></i> Найти
</button>
@if(request('search'))
<a href="{{ route('admin.users') }}" class="btn btn-sm btn-orange">
<i class="bi bi-x-lg"></i> Сбросить
</a>
@endif
</form>
<a href="{{ route('admin.users.create') }}" class="btn btn-sm btn-dark-green">
<i class="bi bi-plus-lg"></i> Добавить пользователя
</a>
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-bordered">
<thead>
<tr>
<th>Имя</th>
<th>Email</th>
<th>Телефон</th>
<th>Роль</th>
<th>Дата регистрации</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
@forelse ($users as $user)
<tr>
<td>{{ $user->name }}</td>
<td>{{ $user->email }}</td>
<td>{{ $user->phone ?? '—' }}</td>
<td>
<span class="badge bg-{{ $user->role?->slug === 'super_admin' ? 'danger' : ($user->role?->slug === 'shop_admin' ? 'warning' : 'secondary') }}">
{{ $user->role?->name ?? 'Нет роли' }}
</span>
</td>
<td>{{ $user->created_at->format('d.m.Y H:i') }}</td>
<td class="text-center">
<div class="d-flex gap-2 justify-content-center">
<a href="{{ route('admin.users.edit', $user->id) }}" class="btn btn-orange p-1" title="Редактировать">
<img src="{{ asset('assets/images/icons/edit-svgrepo-com.svg') }}" alt="Редактировать" width="20" height="20">
</a>
@if($user->id !== auth()->id())
<form action="{{ route('admin.users.destroy', $user->id) }}" method="POST"
onsubmit="return confirm('Удалить пользователя «{{ $user->name }}»?');">
@csrf
@method('DELETE')
<button class="btn btn-danger p-1" type="submit" title="Удалить">
<img src="{{ asset('assets/images/icons/delete-svgrepo-com.svg') }}" alt="Удалить" width="20" height="20">
</button>
</form>
@endif
</div>
</td>
</tr>
@empty
<tr>
<td colspan="7" class="text-center text-muted">Пользователи не найдены</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<div class="ps-2 pe-2">
{{ $users->links() }}
</div>
@endsection