?php
namespace App\Filament\Admin\Resources;
use App\Filament\Admin\Resources\ProductResource\Pages;
use App\Filament\Admin\Resources\ProductResource\RelationManagers\CategoriesRelationManager;
use App\Services\SeoGeneratorService;
use App\Models\Product;
use App\Models\Format;
use App\Models\Color;
use App\Models\Occasion;
use App\Models\Flower;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
use Filament\Tables\Columns\ToggleColumn;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Filters\SelectFilter;
use Illuminate\Support\Str;
use Filament\Notifications\Notification;
use App\Services\ImageProcessor;
use Illuminate\Support\Facades\Storage;
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\Actions;
use Filament\Forms\Components\Actions\Action;
class ProductResource extends Resource
{
protected static ?string $model = Product::class;
protected static ?string $navigationIcon = 'heroicon-o-shopping-bag';
protected static ?string $navigationGroup = 'Управление товарами';
protected static ?int $navigationSort = 1;
protected static ?string $modelLabel = 'Товар';
protected static ?string $pluralModelLabel = 'Товары';
public static function form(Form $form): Form
{
return $form->schema([
Forms\Components\Section::make('Основная информация')
->schema([
Forms\Components\TextInput::make('name')
->required()
->maxLength(255)
->live(onBlur: true)
->afterStateUpdated(function (callable $set, $state) {
$set('slug', Str::slug($state));
})
->label('Название товара'),
Forms\Components\TextInput::make('slug')
->required()
->maxLength(255)
->unique(ignoreRecord: true)
->label('URL-адрес'),
Forms\Components\Textarea::make('description')
->columnSpanFull()
->label('Описание'),
Forms\Components\FileUpload::make('main_image_upload')
->label('Главное изображение')
->disk('public')
->directory('tmp')
->image()
->required(fn ($livewire) => $livewire instanceof \Filament\Resources\Pages\CreateRecord)
->columnSpanFull()
->preserveFilenames()
->imageEditor()
->openable()
->downloadable()
->afterStateUpdated(function ($state, callable $set) {
if (!$state) {
$set('main_image_webp', null);
$set('main_image', null);
$set('main_image_webp_medium', null);
$set('main_image_jpg_medium', null);
$set('main_image_webp_small', null);
$set('main_image_jpg_small', null);
return;
}
try {
$tempPath = $state->storeAs('tmp', $state->getClientOriginalName(), 'public');
$fullPath = storage_path('app/public/' . $tempPath);
$paths = ImageProcessor::processFromPath($fullPath, 'products/main', true);
$set('main_image_webp', $paths['webp'] ?? null);
$set('main_image', $paths['jpg'] ?? null);
$set('main_image_webp_medium', $paths['webp_medium'] ?? null);
$set('main_image_jpg_medium', $paths['jpg_medium'] ?? null);
$set('main_image_webp_small', $paths['webp_small'] ?? null);
$set('main_image_jpg_small', $paths['jpg_small'] ?? null);
Storage::disk('public')->delete($tempPath);
} catch (\Throwable $e) {
\Log::error('Ошибка при обработке изображения: ' . $e->getMessage());
Notification::make()
->title('Ошибка при обработке изображения')
->body($e->getMessage())
->danger()
->send();
}
})
->dehydrated(false)
->dehydrateStateUsing(fn ($state) => null),
Forms\Components\ViewField::make('current_main_image')
->label('Текущее изображение')
->view('filament.components.product-image-preview')
->hidden(fn ($record) => !$record?->main_image)
->columnSpanFull()
->extraAttributes([
'x-data' => "{
confirmDelete() {
if (confirm('Вы уверены, что хотите удалить главное изображение?')) {
\$wire.dispatch('delete-main-image');
}
}
}",
'x-on:delete-main-image.window' => "confirmDelete()",
]),
Forms\Components\ViewField::make('current_gallery')
->label('Текущие изображения галереи')
->view('filament.components.product-gallery-preview')
->hidden(fn ($record) => empty($record?->gallery))
->columnSpanFull()
->extraAttributes([
'x-data' => "{
confirmDelete(index) {
if (confirm('Вы уверены, что хотите удалить это изображение из галереи?')) {
\$wire.dispatch('delete-gallery-image', { index: index });
}
}
}",
'x-on:delete-gallery-image.window' => 'confirmDelete($event.detail.index)',
]),
Hidden::make('main_image_webp'),
Hidden::make('main_image'),
Hidden::make('main_image_webp_medium'),
Hidden::make('main_image_jpg_medium'),
Hidden::make('main_image_webp_small'),
Hidden::make('main_image_jpg_small'),
// Новое поле для загрузки видео
Forms\Components\FileUpload::make('video_file')
->label('Видеофайл (mp4, webm)')
->disk('public')
->directory('products/video')
->acceptedFileTypes(['video/mp4', 'video/webm'])
->maxSize(102400) // 100 MB
->visibility('public')
->nullable(),
Forms\Components\TextInput::make('video_url')
->label('Ссылка на видео (YouTube/Vimeo)')
->url()
->nullable(),
Forms\Components\Toggle::make('is_active')
->label('Активен')
->default(true),
Forms\Components\Toggle::make('is_bestseller')
->label('Хит продаж')
->default(false),
])
->columns(['md' => 2, 'lg' => 3]),
Forms\Components\Section::make('Галерея изображений')
->schema([
Forms\Components\FileUpload::make('gallery_upload')
->label('Галерея изображений')
->disk('public')
->directory('tmp')
->image()
->multiple()
->reorderable()
->appendFiles()
->preserveFilenames()
->imageEditor()
->openable()
->downloadable()
->columnSpanFull()
->afterStateUpdated(function (callable $get, callable $set, $state) {
$gallery = $get('gallery') ?? [];
if (!$state) {
$set('gallery', []);
return;
}
$newImages = [];
foreach ($state as $file) {
if ($file instanceof TemporaryUploadedFile) {
try {
$tempPath = $file->storeAs('tmp', $file->getClientOriginalName(), 'public');
$fullPath = storage_path('app/public/' . $tempPath);
$paths = ImageProcessor::processFromPath($fullPath, 'products/gallery', false);
$newImages[] = [
'webp' => $paths['webp'] ?? null,
'jpg' => $paths['jpg'] ?? null
];
Storage::disk('public')->delete($tempPath);
} catch (\Throwable $e) {
\Log::error('Ошибка при обработке изображения галереи: ' . $e->getMessage());
Notification::make()
->title('Ошибка при обработке изображения галереи')
->body($e->getMessage())
->danger()
->send();
}
}
}
$set('gallery', array_merge($gallery, $newImages));
})
->dehydrated(false)
->dehydrateStateUsing(fn ($state) => null),
Hidden::make('gallery'),
]),
Forms\Components\Section::make('Цены и наличие')
->schema([
Forms\Components\TextInput::make('price')
->required()
->numeric()
->prefix('₽')
->label('Цена'),
Forms\Components\TextInput::make('old_price')
->numeric()
->prefix('₽')
->label('Старая цена (если есть скидка)'),
Forms\Components\Toggle::make('in_stock')
->required()
->label('В наличии')
->default(true),
Forms\Components\TextInput::make('stock_count')
->numeric()
->label('Количество на складе')
->helperText('Оставьте 0 для бесконечного количества')
->default(0),
])
->columns(['md' => 2]),
Forms\Components\Section::make('Характеристики')
->schema([
Forms\Components\TextInput::make('height_cm')
->required()
->numeric()
->suffix('см')
->label('Высота'),
Forms\Components\TextInput::make('width_cm')
->required()
->numeric()
->suffix('см')
->label('Ширина'),
Forms\Components\Select::make('format_id')
->label('Формат')
->relationship('format', 'name')
->required()
->searchable()
->preload(),
Forms\Components\Select::make('colors')
->label('Цвета')
->relationship('colors', 'name')
->multiple()
->searchable()
->preload()
->columnSpanFull(),
Forms\Components\Repeater::make('flower_product_composition')
->relationship('flowerProductComposition')
->schema([
Forms\Components\Select::make('flower_id')
->label('Цветок')
->options(fn () => Flower::pluck('name', 'id'))
->required()
->searchable(),
Forms\Components\TextInput::make('count')
->label('Количество')
->numeric()
->required()
->default(1),
])
->columns(['md' => 2])
->collapsible()
->label('Состав букета'),
Forms\Components\Select::make('occasions')
->label('Поводы')
->relationship('occasions', 'name')
->multiple()
->searchable()
->preload()
->columnSpanFull(),
])
->columns(['md' => 2]),
Forms\Components\Section::make('SEO настройки')
->schema([
Forms\Components\TextInput::make('seo_title')
->maxLength(60)
->label('SEO Title')
->helperText('Максимум 60 символов'),
Forms\Components\Textarea::make('seo_description')
->maxLength(160)
->label('SEO Description')
->helperText('Максимум 160 символов'),
Forms\Components\TextInput::make('seo_keywords')
->label('SEO Keywords')
->helperText('Через запятую'),
Actions::make([
Action::make('generate_seo')
->label('Сгенерировать SEO автоматически')
->color('success')
->icon('heroicon-o-sparkles')
->action(function (callable $set, callable $get) {
$name = $get('name');
if (empty($name)) {
Notification::make()
->title('Сначала заполните название товара')
->danger()
->send();
return;
}
self::generateSeoFields($set, $get, $name);
Notification::make()
->title('SEO успешно сгенерировано')
->success()
->send();
}),
]),
])
->columns(['md' => 2]),
]);
}
private static function generateSeoFields(callable $set, callable $get, string $productName): void
{
$composition = $get('flower_product_composition') ?? [];
$occasionIds = $get('occasions') ?? [];
$colorIds = $get('colors') ?? [];
$format = $get('format_id');
$width = $get('width_cm') ?? '';
$height = $get('height_cm') ?? '';
$price = $get('price') ?? '';
$occasionNames = [];
if (!empty($occasionIds)) {
$occasionNames = is_array($occasionIds)
? Occasion::whereIn('id', $occasionIds)->pluck('name')->toArray()
: [Occasion::find($occasionIds)?->name];
}
$colorNames = [];
if (is_array($colorIds)) {
$colorNames = Color::whereIn('id', $colorIds)->pluck('name')->toArray();
} elseif (!empty($colorIds)) {
$colorNames[] = Color::find($colorIds)?->name;
}
$seoData = SeoGeneratorService::generate([
'name' => $productName,
'composition' => $composition,
'occasion' => $occasionNames,
'color' => implode(', ', $colorNames),
'format' => $format,
'width_cm' => $width,
'height_cm' => $height,
'price' => $price,
]);
$set('seo_title', $seoData['seo_title'] ?? '');
$set('seo_description', $seoData['seo_description'] ?? '');
$set('seo_keywords', $seoData['seo_keywords'] ?? '');
$set('description', $seoData['description'] ?? '');
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\ImageColumn::make('main_image_jpg_small')
->label('Фото')
->disk('public')
->rounded()
->height(80)
->width(80)
->defaultImageUrl(function ($record) {
if ($record->main_image_jpg_medium && Storage::disk('public')->exists($record->main_image_jpg_medium)) {
return Storage::disk('public')->url($record->main_image_jpg_medium);
}
if ($record->main_image && Storage::disk('public')->exists($record->main_image)) {
return Storage::disk('public')->url($record->main_image);
}
return null;
}),
Tables\Columns\TextColumn::make('name')
->label('Название')
->searchable()
->sortable()
->wrap(),
// НОВАЯ КОЛОНКА ИНДИКАТОРА СОСТАВА
IconColumn::make('has_composition')
->label('Состав')
->boolean()
->getStateUsing(fn ($record) => $record->flowers()->exists()) // Проверяет, есть ли привязанные цветы
->trueIcon('heroicon-o-check-circle') // Иконка, если состав есть
->falseIcon('heroicon-o-x-circle') // Иконка, если состава нет
->trueColor('success') // Зеленый цвет
->falseColor('danger') // Красный цвет
->sortable(query: function ($query, string $direction) {
return $query->withCount('flowers')->orderBy('flowers_count', $direction);
}),
Tables\Columns\TextColumn::make('price')
->label('Цена')
->sortable(),
ToggleColumn::make('in_stock')
->label('В наличии')
->sortable(),
ToggleColumn::make('is_active')
->label('Активен')
->sortable(),
Tables\Columns\TextColumn::make('created_at')
->label('Создан')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->filters([
SelectFilter::make('in_stock')
->label('Наличие')
->options([
true => 'В наличии',
false => 'Нет в наличии',
]),
SelectFilter::make('is_active')
->label('Активность')
->options([
true => 'Активен',
false => 'Неактивен',
]),
])
->actions([
Tables\Actions\EditAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
Tables\Actions\BulkAction::make('setInStock')
->label('Отметить "В наличии"')
->icon('heroicon-o-check-circle')
->action(function ($records) {
foreach ($records as $record) {
$record->update(['in_stock' => true]);
}
})
->deselectRecordsAfterCompletion(),
Tables\Actions\BulkAction::make('setOutOfStock')
->label('Отметить "Нет в наличии"')
->icon('heroicon-o-x-circle')
->action(function ($records) {
foreach ($records as $record) {
$record->update(['in_stock' => false]);
}
})
->deselectRecordsAfterCompletion(),
Tables\Actions\BulkAction::make('activate')
->label('Активировать')
->icon('heroicon-o-check')
->action(function ($records) {
foreach ($records as $record) {
$record->update(['is_active' => true]);
}
})
->deselectRecordsAfterCompletion(),
Tables\Actions\BulkAction::make('deactivate')
->label('Деактивировать')
->icon('heroicon-o-x-mark')
->action(function ($records) {
foreach ($records as $record) {
$record->update(['is_active' => false]);
}
})
->deselectRecordsAfterCompletion(),
]),
])
->defaultSort('created_at', 'desc');
}
public static function getRelations(): array
{
return [
CategoriesRelationManager::class,
];
}
public static function getPages(): array
{
return [
'index' => \App\Filament\Admin\Resources\Pages\ListProducts::route('/'),
'create' => \App\Filament\Admin\Resources\Pages\CreateProduct::route('/create'),
'edit' => \App\Filament\Admin\Resources\Pages\EditProduct::route('/{record}/edit'),
];
}
}
Букет из 7 белых роз, доставка по Казани (Арт. 441)
Главная
/
Каталог
/
Букет из 7 белых роз
Букет из 7 белых роз
3280.00 ₽
Хочу добавить вазу в свой заказ
1 500 ₽
Подробнее
Прозрачная ваза подходящая для вашего букета
Прозрачная ваза, идеально подходящая для вашего букета. Этот стильный аксессуар станет прекрасным до...
Хочу добавить переноску и аквабокс
200 ₽
Подробнее
Переноска и аквабокс
Переноска и аквабокс для букета среднего размера. Обеспечивают надежную транспортировку и поддержани...
Букет из 7 белых роз — это большой и красивый букет, который станет идеальным подарком на День матери, День рождения, День учителя или 1 сентября. Розы отличаются нежным и благородным цветом, который символизирует чистоту и искренность чувств. Доставка по Казани.
✓ Ваза добавлена к заказу
✓ Переноска и аквабокс добавлены к заказу
Пожалуйста, обратите внимание: каждый цветок уникален, как и сама природа. Живые цветы не растут точь-в-точь по шаблону, поэтому оттенок бутона, степень его раскрытия или форма сборки могут немного отличаться от представленных на фотографии. Но мы обещаем, что наши флористы соберут для вас максимально похожий и по-настоящему красивый букет, сохранив его настроение и цветовую гамму!
Инструкция по свежести
1. Подрежьте стебли под углом 45 градусов перед тем, как поставить цветы в воду
2. Используйте чистую вазу и свежую воду комнатной температуры
3. Добавьте в воду подкормку для цветов (идет в комплекте)
4. Меняйте воду каждые 2 дня и подрезайте стебли
5. Держите букет в прохладном месте, избегая прямых солнечных лучей
Отзывы
Отзывов пока нет. Будьте первыми!
Главная
Каталог
Корзина
0
Избранное
0
Войти
🎁
Дарим 500 рублей
на бонусный счет при регистрации
Мы используем файлы cookie и сервис веб-аналитики
Яндекс Метрика для обеспечения корректной работы сайта и анализа пользовательского опыта.
Продолжая использовать сайт, вы даете согласие на обработку ваших данных в соответствии с
Политикой конфиденциальности .
Только необходимые
Согласен