tsett
This commit is contained in:
160
resources/views/admin/brands/brands.blade.php
Normal file
160
resources/views/admin/brands/brands.blade.php
Normal 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
|
||||
101
resources/views/admin/brands/edit.blade.php
Normal file
101
resources/views/admin/brands/edit.blade.php
Normal 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
|
||||
245
resources/views/admin/categories/categories.blade.php
Normal file
245
resources/views/admin/categories/categories.blade.php
Normal 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
|
||||
132
resources/views/admin/categories/edit.blade.php
Normal file
132
resources/views/admin/categories/edit.blade.php
Normal 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
|
||||
347
resources/views/admin/contacts/edit.blade.php
Normal file
347
resources/views/admin/contacts/edit.blade.php
Normal 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
|
||||
113
resources/views/admin/dashboard.blade.php
Normal file
113
resources/views/admin/dashboard.blade.php
Normal 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
|
||||
82
resources/views/admin/orders/orders.blade.php
Normal file
82
resources/views/admin/orders/orders.blade.php
Normal 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
|
||||
109
resources/views/admin/orders/show.blade.php
Normal file
109
resources/views/admin/orders/show.blade.php
Normal 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
|
||||
5
resources/views/admin/partials/header.blade.php
Normal file
5
resources/views/admin/partials/header.blade.php
Normal 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>
|
||||
97
resources/views/admin/partials/mobile-nav.blade.php
Normal file
97
resources/views/admin/partials/mobile-nav.blade.php
Normal 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>
|
||||
105
resources/views/admin/partials/sidebar.blade.php
Normal file
105
resources/views/admin/partials/sidebar.blade.php
Normal 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>
|
||||
74
resources/views/admin/permissions/edit.blade.php
Normal file
74
resources/views/admin/permissions/edit.blade.php
Normal 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
|
||||
118
resources/views/admin/permissions/permissions.blade.php
Normal file
118
resources/views/admin/permissions/permissions.blade.php
Normal 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
|
||||
650
resources/views/admin/products/create.blade.php
Normal file
650
resources/views/admin/products/create.blade.php
Normal 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 '&';
|
||||
if (m === '<') return '<';
|
||||
if (m === '>') return '>';
|
||||
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
|
||||
832
resources/views/admin/products/edit.blade.php
Normal file
832
resources/views/admin/products/edit.blade.php
Normal 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 '&';
|
||||
if (m === '<') return '<';
|
||||
if (m === '>') return '>';
|
||||
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
|
||||
180
resources/views/admin/products/partials/variation-form.blade.php
Normal file
180
resources/views/admin/products/partials/variation-form.blade.php
Normal 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>
|
||||
164
resources/views/admin/products/products.blade.php
Normal file
164
resources/views/admin/products/products.blade.php
Normal 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
|
||||
79
resources/views/admin/roles/create.blade.php
Normal file
79
resources/views/admin/roles/create.blade.php
Normal 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
|
||||
91
resources/views/admin/roles/edit.blade.php
Normal file
91
resources/views/admin/roles/edit.blade.php
Normal 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
|
||||
90
resources/views/admin/roles/roles.blade.php
Normal file
90
resources/views/admin/roles/roles.blade.php
Normal 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
|
||||
100
resources/views/admin/users/create.blade.php
Normal file
100
resources/views/admin/users/create.blade.php
Normal 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
|
||||
98
resources/views/admin/users/edit.blade.php
Normal file
98
resources/views/admin/users/edit.blade.php
Normal 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
|
||||
104
resources/views/admin/users/users.blade.php
Normal file
104
resources/views/admin/users/users.blade.php
Normal 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
|
||||
Reference in New Issue
Block a user