Rework admin panel and other

This commit is contained in:
F4ilji
2025-04-02 18:37:20 +05:00
parent 49072e50c7
commit 5cd6ff11b5
198 changed files with 9715 additions and 6664 deletions
+6
View File
@@ -9,12 +9,18 @@ server {
try_files $uri /index.php?$args; # Обработка запросов try_files $uri /index.php?$args; # Обработка запросов
} }
location /sveden/ { location /sveden/ {
alias /var/www/public/sveden/; alias /var/www/public/sveden/;
index index.html; index index.html;
try_files $uri $uri/ /sveden/index.html; # Обработка статических файлов try_files $uri $uri/ /sveden/index.html; # Обработка статических файлов
} }
location = /sveden {
return 301 /sveden/;
}
location ~ \.php$ { location ~ \.php$ {
try_files $uri =404; # Если файл не найден, возвращаем 404 try_files $uri =404; # Если файл не найден, возвращаем 404
fastcgi_split_path_info ^(.+\.php)(/.+)$; # Разделение пути fastcgi_split_path_info ^(.+\.php)(/.+)$; # Разделение пути
+64
View File
@@ -0,0 +1,64 @@
<?php
namespace App\Containers\Post\Models;
use App\Enums\PostStatus;
use App\Models\Category;
use App\Models\MainSlider;
use App\Models\Seo;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\MorphOne;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
use Illuminate\Support\Facades\Cache;
use Spatie\Tags\HasTags;
class Post extends Model
{
use HasFactory, HasTags;
protected $guarded = false;
protected static function booted()
{
static::saved(function ($post) {
Cache::forget('post_' . $post->id);
Cache::forget('posts_' . $post->category_id . '_*'); // Очистка кеша для всех постов в категории
});
static::deleted(function ($post) {
Cache::forget('post_' . $post->id);
Cache::forget('posts_' . $post->category_id . '_*'); // Очистка кеша для всех постов в категории
});
}
public function category() : BelongsTo
{
return $this->belongsTo(Category::class);
}
public function author() : BelongsTo
{
return $this->belongsTo(User::class, 'user_id');
}
public function seo()
{
return $this->morphOne(Seo::class, 'seoable');
}
public function mainSlider()
{
return $this->morphOne(MainSlider::class, 'slidable');
}
protected $casts = [
'content' => 'array',
'authors' => 'array',
'status' => PostStatus::class,
'images' => 'array'
];
}
+3
View File
@@ -17,6 +17,7 @@ class MainSliderDTO
public ?Carbon $start_time, public ?Carbon $start_time,
public ?Carbon $end_time, public ?Carbon $end_time,
public ?int $sort, public ?int $sort,
public int $slider_id,
) {} ) {}
// Опционально: метод для создания DTO из массива // Опционально: метод для создания DTO из массива
@@ -33,6 +34,7 @@ class MainSliderDTO
start_time: isset($data['start_time']) ? Carbon::parse($data['start_time']) : null, start_time: isset($data['start_time']) ? Carbon::parse($data['start_time']) : null,
end_time: isset($data['end_time']) ? Carbon::parse($data['end_time']) : null, end_time: isset($data['end_time']) ? Carbon::parse($data['end_time']) : null,
sort: $data['sort'] ?? null, sort: $data['sort'] ?? null,
slider_id: $data['slider_id'],
); );
} }
@@ -50,6 +52,7 @@ class MainSliderDTO
'start_time' => $this->start_time?->toDateTimeString(), 'start_time' => $this->start_time?->toDateTimeString(),
'end_time' => $this->end_time?->toDateTimeString(), 'end_time' => $this->end_time?->toDateTimeString(),
'sort' => $this->sort, 'sort' => $this->sort,
'slider_id' => $this->slider_id,
]; ];
} }
} }
+31
View File
@@ -0,0 +1,31 @@
<?php
namespace App\Enums;
use Filament\Support\Contracts\HasLabel;
use Filament\Support\Contracts\HasColor;
enum AdmissionCampaignStatus: int implements HasLabel, HasColor
{
case ACTIVE = 1;
case ARCHIVED = 2;
case HIDDEN = 3;
public function getLabel(): ?string
{
return match ($this) {
self::ACTIVE => 'Активная',
self::ARCHIVED => 'Архивная',
self::HIDDEN => 'Скрытая',
};
}
public function getColor(): string|array|null
{
return match ($this) {
self::ACTIVE => 'success',
self::ARCHIVED => 'gray',
self::HIDDEN => 'danger',
};
}
}
+57
View File
@@ -0,0 +1,57 @@
<?php
namespace App\Enums;
enum CacheKeys: string {
case USER_PREFIX = 'person_';
case POST_PREFIX = 'post_';
case RECENT_POSTS_PREFIX = 'recent_posts_';
case POSTS_PREFIX = 'posts_';
case EDUCATION_PROGRAM_PREFIX = 'education_program_';
case EDUCATION_PROGRAMS_PREFIX = 'education_programs_';
case ADDITIONAL_EDUCATIONAL_PROGRAM_PREFIX = 'additional_education_program_';
case ADDITIONAL_EDUCATIONAL_PROGRAMS_PREFIX = 'additional_education_programs_';
case ACADEMIC_JOURNAL_PREFIX = 'academic_journal_';
case ACADEMIC_JOURNALS_PREFIX = 'academic_journals_';
case CATEGORIES_PREFIX = 'categories_';
case CONTACT_WIDGET_PREFIX = 'contact_widget_';
case CONTACT_WIDGETS_PREFIX = 'contact_widgets_';
// Department
case DEPARTMENT_PREFIX = 'department_';
case DEPARTMENTS_PREFIX = 'departments_';
// Division
case DIVISION_PREFIX = 'division_';
case DIVISIONS_PREFIX = 'divisions_';
// Event
case EVENT_PREFIX = 'event_';
case EVENTS_PREFIX = 'events_';
// Faculty
case FACULTY_PREFIX = 'faculty_';
case FACULTIES_PREFIX = 'faculties_';
// Navigation
case NAVIGATION_PREFIX = 'navigation';
// Page
case PAGE_PREFIX = 'page_';
case PAGE_DATA_PREFIX = 'page_data_';
case PAGE_REFERENCE_LIST_PREFIX = 'page_reference_list_';
case PAGE_REFERENCE_LISTS_PREFIX = 'page_reference_lists_';
// Schedule
case SCHEDULE_PREFIX = 'schedule_';
case SCHEDULES_PREFIX = 'schedules_';
// Slider
case SLIDER_PREFIX = 'slider_';
// Tag
case TAG_IDS_PREFIX = 'tag_ids_';
case TAGS_PREFIX = 'tags_';
case TAG_CONTENT_PREFIX = 'tag_content_';
}
+156 -27
View File
@@ -13,6 +13,7 @@ use App\Models\Page;
use App\Models\Post; use App\Models\Post;
use Filament\Forms; use Filament\Forms;
use Filament\Forms\Components\Builder; use Filament\Forms\Components\Builder;
use Filament\Forms\Components\DateTimePicker;
use Filament\Forms\Components\FileUpload; use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Hidden; use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\RichEditor; use Filament\Forms\Components\RichEditor;
@@ -36,52 +37,180 @@ class CustomFormForm
{ {
return $form return $form
->schema([ ->schema([
Section::make() Section::make('Настройки формы')
->description('Конфигурация пользовательской формы')
->collapsible()
->schema([ ->schema([
Tabs::make('Tabs') Tabs::make('Конфигурация формы')
->persistTabInQueryString()
->columnSpanFull()
->tabs([ ->tabs([
Tabs\Tab::make('Основная информация') Tabs\Tab::make('Основная информация')
->icon('heroicon-o-information-circle')
->schema([ ->schema([
Forms\Components\Grid::make(2)->schema([ Forms\Components\Grid::make(2)
TextInput::make('title')->label('Заголовок')->required() ->schema([
->live(onBlur: true) TextInput::make('title')
->afterStateUpdated(function (string $operation, string|null $state, Forms\Set $set) { ->label('Название формы')
$set('form_id', Str::slug($state) . Carbon::now()->timestamp); ->placeholder('Введите название формы')
}), ->helperText('Это название будет видно пользователям')
TextInput::make('form_id')->label('ID формы')->unique(ignoreRecord: true)->required(), ->required()
]), ->maxLength(255)
Forms\Components\Textarea::make('description')->label('Описание формы')->required(), ->live(onBlur: true)
Select::make('status')->label('Статус формы')->required() ->afterStateUpdated(function (string $operation, string|null $state, Forms\Set $set) {
$set('form_id', Str::slug($state) . Carbon::now()->timestamp);
}),
TextInput::make('form_id')
->label('Уникальный ID формы')
->helperText('Автоматически генерируется из названия')
->required()
->unique(ignoreRecord: true)
->maxLength(255),
]),
Forms\Components\Textarea::make('description')
->label('Описание формы')
->placeholder('Опишите назначение этой формы')
->helperText('Это описание будет видно пользователям')
->required()
->maxLength(2000)
->columnSpanFull(),
Select::make('status')
->label('Статус формы')
->options(CustomFormStatus::class) ->options(CustomFormStatus::class)
->required()
->native(false)
->helperText('Определяет видимость формы на сайте')
->columnSpanFull(),
]), ]),
Tabs\Tab::make('Колонки')
Tabs\Tab::make('Поля формы')
->icon('heroicon-o-view-columns')
->schema([ ->schema([
FormBuilderItem::getItem(), FormBuilderItem::getItem()
->columnSpanFull(),
]), ]),
Tabs\Tab::make('Кнопка отправки') Tabs\Tab::make('Кнопка отправки')
->icon('heroicon-o-paper-airplane')
->schema([ ->schema([
TextInput::make('button')->label('Текст кнопки отправки')->required(), TextInput::make('button')
Forms\Components\Textarea::make('send_message')->label('Текст после отправления письма')->required(), ->label('Текст кнопки отправки')
->placeholder('Например: Отправить заявку')
->helperText('Текст, который будет отображаться на кнопке отправки формы')
->required()
->maxLength(255),
Forms\Components\Textarea::make('send_message')
->label('Сообщение после отправки')
->placeholder('Спасибо! Ваша заявка принята.')
->helperText('Это сообщение увидят пользователи после успешной отправки формы')
->required()
->maxLength(1000)
->columnSpanFull(),
]), ]),
Tabs\Tab::make('Настройка интеграции с почтой')
Tabs\Tab::make('Настройки')
->icon('heroicon-o-cog')
->schema([
Toggle::make('settings.personal_data')
->label('Согласие на обработку данных')
->helperText('Показывать checkbox для согласия на обработку персональных данных')
->inline(false)
->onColor('success')
->offColor('gray'),
Toggle::make('settings.captcha')
->label('Защита CAPTCHA')
->helperText('Включить защиту от спама с помощью CAPTCHA')
->inline(false)
->onColor('success')
->offColor('gray'),
Section::make('Ограничение по времени')
->collapsible()
->schema([
Toggle::make('is_time_period')
->label('Ограничить период работы формы')
->helperText('Форма будет активна только в указанный период')
->dehydrated(false)
->live(true)
->inline(false),
Forms\Components\Grid::make(2)
->schema([
DateTimePicker::make('settings.period.start_time')
->label('Дата начала')
->native(false)
->displayFormat('d/m/Y H:i')
->seconds(false)
->helperText('Когда форма станет доступна')
->default(Carbon::now())
->minDate(Carbon::now()),
DateTimePicker::make('settings.period.end_time')
->label('Дата окончания')
->native(false)
->displayFormat('d/m/Y H:i')
->seconds(false)
->helperText('Когда форма перестанет быть доступна')
->default(Carbon::now()->addWeeks(2))
->minDate(Carbon::now()),
])
->hidden(fn(Forms\Get $get): bool => $get('is_time_period') !== true),
]),
]),
Tabs\Tab::make('Настройки почты')
->icon('heroicon-o-envelope')
->schema([ ->schema([
Forms\Components\Repeater::make('mail_settings') Forms\Components\Repeater::make('mail_settings')
->label('') ->label('Настройки уведомлений')
->addActionLabel('Добавить получателя') ->addActionLabel('Добавить получателя')
->helperText('Укажите, кому и какие уведомления отправлять')
->collapsed()
->itemLabel(fn (array $state): ?string => $state['target'] ?? 'Новый получатель')
->schema([ ->schema([
TextInput::make('target')->label('Кому')->email()->required(), TextInput::make('target')
TextInput::make('topic')->label('Тема')->required(), ->label('Email получателя')
Builder::make('data')->schema([ ->placeholder('email@example.com')
Builder\Block::make('text')->schema([ ->email()
RichEditor::make('content')->required(), ->required()
->maxLength(255),
TextInput::make('topic')
->label('Тема письма')
->placeholder('Новая заявка с формы')
->required()
->maxLength(255),
Builder::make('data')
->label('Содержимое письма')
->blockNumbers(false)
->collapsible()
->schema([
Builder\Block::make('text')
->label('Текст письма')
->schema([
RichEditor::make('content')
->label('')
->required()
->toolbarButtons([
'bold', 'italic', 'link',
'orderedList', 'bulletList'
]),
]),
Builder\Block::make('answers')
->label('Ответы формы')
->schema([]),
]), ]),
Builder\Block::make('answers')->schema([]),
])->required(),
]) ])
->collapsed(), ->grid(2),
]), ]),
]), ]),
]) ]),
]); ]);
} }
} }
@@ -0,0 +1,8 @@
<?php
namespace App\Filament\Components\Forms\ItemForm\Blocks;
interface BlockSchema
{
public static function schema(): array;
}
@@ -0,0 +1,31 @@
<?php
namespace App\Filament\Components\Forms\ItemForm\Blocks;
use App\Filament\Components\Forms\ItemForm\Blocks\BlockSchema;
use App\Helpers\ByteConverter;
use App\Models\ContactWidget;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Illuminate\Support\Carbon;
use Illuminate\Support\Str;
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
class ContactBlock implements BlockSchema
{
public static function schema(): array
{
return [
Select::make('contact')
->label('Виджет контактов')
->options(ContactWidget::query()->where('is_active', true)->pluck('title', 'slug'))
->searchable()
->required()
->helperText('Выберите активный виджет контактов'),
];
}
}
@@ -0,0 +1,28 @@
<?php
namespace App\Filament\Components\Forms\ItemForm\Blocks;
use App\Enums\CustomFormStatus;
use App\Models\CustomForm;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Toggle;
class CustomFormBlock implements BlockSchema
{
public static function schema(): array
{
return [
Select::make('form')
->label('Форма')
->options(CustomForm::query()->where('status', CustomFormStatus::PUBLISHED)->pluck('title', 'form_id'))
->searchable()
->required()
->helperText('Выберите опубликованную форму'),
Section::make()->schema([
Toggle::make('settings.in_modal')->label('Открывать в модальном окне')->default(false),
]),
];
}
}
@@ -0,0 +1,66 @@
<?php
namespace App\Filament\Components\Forms\ItemForm\Blocks;
use App\Filament\Components\Forms\ItemForm\Blocks\BlockSchema;
use App\Helpers\ByteConverter;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\TextInput;
use Illuminate\Support\Carbon;
use Illuminate\Support\Str;
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
class FilesBlock implements BlockSchema
{
public static function schema(): array
{
return [
Repeater::make('file')
->label('Файлы')
->helperText('Загрузите один или несколько файлов')
->schema([
Hidden::make('expansion')->required(),
Hidden::make('size')->required(),
TextInput::make('title')
->label('Название файла')
->placeholder('Введите название файла')
->required()
->maxLength(255)
->autofocus()
->helperText('Это название будет отображаться пользователям'),
FileUpload::make('path')
->label('Файл')
->required()
->helperText('Поддерживаются PDF, Word, Excel, PowerPoint и ZIP файлы (макс. 500KB)')
->getUploadedFileNameForStorageUsing(
fn (TemporaryUploadedFile $file): string =>
str(Str::slug(pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME) . '-' . Carbon::now()->timestamp) . '.' . $file->getClientOriginalExtension())
)
->acceptedFileTypes([
'application/pdf',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'application/zip'
])
->maxSize(512000)
->disk('public')
->directory('files')
->downloadable()
->afterStateUpdated(function ($set, $state) {
$set('expansion', $state?->getClientOriginalExtension());
$set('size', ByteConverter::bytesToHuman($state?->getSize()));
})
->visibility('public')
->preserveFilenames()
])
->itemLabel(fn (array $state): ?string => $state['title'] ?? null)
->collapsible()
->cloneable()
->grid(2),
];
}
}
@@ -0,0 +1,27 @@
<?php
namespace App\Filament\Components\Forms\ItemForm\Blocks;
use App\Filament\Components\Forms\ItemForm\Blocks\BlockSchema;
use Filament\Forms\Components\TextInput;
class HeadingBlock implements BlockSchema
{
public static function schema(): array
{
return [
TextInput::make('id')
->hidden()
->integer()
->default(rand(2335235, 324634264263426)),
TextInput::make('content')
->label('Текст заголовка')
->placeholder('Введите текст заголовка')
->helperText('Основной заголовок раздела')
->live(onBlur: true)
->required()
->maxLength(255),
];
}
}
@@ -0,0 +1,38 @@
<?php
namespace App\Filament\Components\Forms\ItemForm\Blocks;
use App\Filament\Components\Forms\ItemForm\Blocks\BlockSchema;
use App\Helpers\ByteConverter;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\TextInput;
use Illuminate\Support\Carbon;
use Illuminate\Support\Str;
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
class ImageBlock implements BlockSchema
{
public static function schema(): array
{
return [
FileUpload::make('url')
->label('Изображение')
->image()
->multiple()
->reorderable()
->maxFiles(5)
->disk('public')
->directory('images')
->imageEditor()
->required()
->helperText('Можно загрузить до 5 изображений'),
TextInput::make('alt')
->label('Альтернативный текст')
->placeholder('Необязательно')
->helperText('Описание изображения для SEO'),
];
}
}
@@ -0,0 +1,38 @@
<?php
namespace App\Filament\Components\Forms\ItemForm\Blocks;
use App\Filament\Components\Forms\ItemForm\Blocks\BlockSchema;
use App\Helpers\ByteConverter;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\TextInput;
use Illuminate\Support\Carbon;
use Illuminate\Support\Str;
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
class ImagesBlock implements BlockSchema
{
public static function schema(): array
{
return [
FileUpload::make('url')
->label('Изображения')
->image()
->multiple()
->reorderable()
->maxFiles(5)
->disk('public')
->directory('images')
->imageEditor()
->required()
->helperText('Максимум 5 изображений. Можно перетаскивать для изменения порядка'),
TextInput::make('alt')
->label('Описание изображений')
->placeholder('Необязательно')
->helperText('Используется для SEO и доступности'),
];
}
}
@@ -0,0 +1,31 @@
<?php
namespace App\Filament\Components\Forms\ItemForm\Blocks;
use App\Filament\Components\Forms\ItemForm\Blocks\BlockSchema;
use App\Helpers\ByteConverter;
use App\Models\Page;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Illuminate\Support\Carbon;
use Illuminate\Support\Str;
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
class PageItemBlock implements BlockSchema
{
public static function schema(): array
{
return [
Select::make('page')
->label('Страница')
->options(Page::query()->where('title', '!=', null)->where('is_visible', true)->pluck('title', 'id'))
->searchable()
->required()
->helperText('Выберите видимую страницу'),
];
}
}
@@ -0,0 +1,31 @@
<?php
namespace App\Filament\Components\Forms\ItemForm\Blocks;
use App\Filament\Components\Forms\ItemForm\Blocks\BlockSchema;
use App\Helpers\ByteConverter;
use App\Models\PageReferenceList;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Illuminate\Support\Carbon;
use Illuminate\Support\Str;
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
class PageResourceListBlock implements BlockSchema
{
public static function schema(): array
{
return [
Select::make('resource')
->label('Ресурс')
->options(PageReferenceList::query()->where('is_active', true)->pluck('title', 'slug'))
->searchable()
->required()
->helperText('Выберите активный ресурс'),
];
}
}
@@ -0,0 +1,51 @@
<?php
namespace App\Filament\Components\Forms\ItemForm\Blocks;
use App\Filament\Components\Forms\ItemForm\Blocks\BlockSchema;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Get;
use Mohamedsabil83\FilamentFormsTinyeditor\Components\TinyEditor;
class ParagraphBlock implements BlockSchema
{
public static function schema(): array
{
return [
Toggle::make('seo_active')
->label('Использовать блок как SEO-текст')
->helperText('Этот текст будет использоваться для SEO-оптимизации')
->live(onBlur: true)
->required()
->disabled(function ($state, Get $get) {
$data = $get('../../');
return self::findSeoActive($data) && !$state;
})
->dehydrated(),
TinyEditor::make('content')
->label('Текст')
->placeholder('Начните вводить текст...')
->profile('test')
->required()
->helperText('Основное текстовое содержимое блока'),
];
}
private static function findSeoActive(array $data) : bool
{
$bool = false;
foreach ($data as $item) {
if ($item['type'] !== 'paragraph') {
continue;
}
if ($item['data']['seo_active'] === true) {
$bool = true;
break;
}
}
return $bool;
}
}
@@ -0,0 +1,63 @@
<?php
namespace App\Filament\Components\Forms\ItemForm\Blocks;
use App\Filament\Components\Forms\ItemForm\Blocks\BlockSchema;
use App\Helpers\ByteConverter;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Illuminate\Support\Carbon;
use Illuminate\Support\Str;
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
class PersonBlock implements BlockSchema
{
public static function schema(): array
{
return [
TextInput::make('name')
->label('Имя персоны')
->placeholder('Введите имя')
->required()
->maxLength(255)
->helperText('Полное имя персоны'),
FileUpload::make('photo')
->label('Фотография')
->image()
->helperText('Рекомендуемый формат: WebP')
->optimize('webp')
->resize(50)
->disk('public')
->directory('images')
->imageEditor()
->required()
->downloadable()
->openable(),
Repeater::make('info')
->label('Дополнительная информация')
->helperText('Добавьте характеристики персоны')
->schema([
TextInput::make('column')
->label('Название характеристики')
->placeholder('Например: Должность')
->required()
->maxLength(255),
Textarea::make('content')
->label('Значение')
->placeholder('Например: Главный инженер')
->required()
->maxLength(1000)
->columnSpanFull(),
])
->minItems(1)
->grid(2)
->collapsible()
->cloneable()
->itemLabel(fn (array $state): ?string => $state['column'] ?? null),
];
}
}
@@ -0,0 +1,23 @@
<?php
namespace App\Filament\Components\Forms\ItemForm\Blocks;
use App\Enums\PostStatus;
use App\Models\Post;
use Filament\Forms\Components\Select;
class PostItemBlock implements BlockSchema
{
public static function schema(): array
{
return [
Select::make('post')
->label('Новость')
->options(Post::query()->where('status', PostStatus::PUBLISHED)->pluck('title', 'id'))
->searchable()
->required()
->helperText('Выберите опубликованную новость'),
];
}
}
@@ -0,0 +1,41 @@
<?php
namespace App\Filament\Components\Forms\ItemForm\Blocks;
use App\Filament\Components\Forms\ItemForm\Blocks\BlockSchema;
use App\Helpers\ByteConverter;
use App\Models\Category;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Grid;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Illuminate\Support\Carbon;
use Illuminate\Support\Str;
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
class PostListBlock implements BlockSchema
{
public static function schema(): array
{
return [
Grid::make(2)
->schema([
TextInput::make('count')
->label('Количество записей')
->integer()
->minValue(1)
->maxValue(20)
->default(5)
->helperText('От 1 до 20 записей'),
Select::make('category')
->label('Категория')
->options(Category::all()->pluck('title', 'id'))
->searchable()
->helperText('Выберите категорию или оставьте пустым для всех'),
]),
];
}
}
@@ -0,0 +1,31 @@
<?php
namespace App\Filament\Components\Forms\ItemForm\Blocks;
use App\Filament\Components\Forms\ItemForm\Blocks\BlockSchema;
use App\Helpers\ByteConverter;
use App\Models\Slider;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Illuminate\Support\Carbon;
use Illuminate\Support\Str;
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
class SliderBlock implements BlockSchema
{
public static function schema(): array
{
return [
Select::make('slider')
->label('Слайдер')
->options(Slider::query()->whereHas('slides')->where('is_active', true)->pluck('title', 'slug'))
->searchable()
->required()
->helperText('Выберите активный слайдер с изображениями'),
];
}
}
@@ -0,0 +1,55 @@
<?php
namespace App\Filament\Components\Forms\ItemForm\Blocks;
use App\Filament\Components\Forms\ItemForm\Blocks\BlockSchema;
use App\Helpers\ByteConverter;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\RichEditor;
use Filament\Forms\Components\TextInput;
use Illuminate\Support\Carbon;
use Illuminate\Support\Str;
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
class StepperBlock implements BlockSchema
{
public static function schema(): array
{
return [
TextInput::make('step_name')
->label('Название процесса')
->placeholder('Например: Процесс оформления')
->required()
->maxLength(255)
->helperText('Общее название для всех шагов'),
Repeater::make('steps')
->label('Шаги')
->helperText('Добавьте шаги процесса')
->schema([
TextInput::make('title')
->label('Название шага')
->placeholder('Например: Шаг 1')
->required()
->maxLength(255)
->columnSpanFull(),
RichEditor::make('content')
->label('Описание шага')
->required()
->toolbarButtons([
'bold',
'italic',
'link',
'orderedList',
'bulletList',
]),
])
->minItems(1)
->collapsible()
->cloneable()
->itemLabel(fn (array $state): ?string => $state['title'] ?? null),
];
}
}
@@ -0,0 +1,128 @@
<?php
namespace App\Filament\Components\Forms\ItemForm\Blocks;
use App\Filament\Components\Forms\ItemForm\Blocks\BlockSchema;
use App\Filament\Components\Forms\ItemForm\Defaults\TabBuilderItem;
use App\Helpers\ByteConverter;
use Filament\Forms\Components\Builder;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\TextInput;
use Illuminate\Support\Carbon;
use Illuminate\Support\Str;
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
class TabBlock implements BlockSchema
{
public static function schema(): array
{
return [
Repeater::make('tab')
->label('Вкладки')
->helperText('Добавьте вкладки с контентом')
->schema([
TextInput::make('title')
->label('Название вкладки')
->placeholder('Введите название вкладки')
->required()
->maxLength(255)
->columnSpanFull()
->helperText('Это название будет отображаться в табе'),
Builder::make('content')
->label('')
->blocks([
Builder\Block::make('heading')
->label('Заголовок')
->icon('heroicon-o-hashtag')
->schema(HeadingBlock::schema()),
Builder\Block::make('paragraph')
->label('Текст')
->icon('heroicon-o-document-text')
->schema(ParagraphBlock::schema()),
Builder\Block::make('files')
->label('Файлы')
->icon('heroicon-o-paper-clip')
->schema(FilesBlock::schema()),
Builder\Block::make('person')
->label('Персона')
->icon('heroicon-o-user')
->schema(PersonBlock::schema()),
Builder\Block::make('stepper')
->label('Этапы')
->icon('heroicon-o-list-bullet')
->schema(StepperBlock::schema()),
Builder\Block::make('images')
->label('Слайдер изображений')
->icon('heroicon-o-photo')
->schema(ImagesBlock::schema()),
Builder\Block::make('image')
->label('Изображение')
->icon('heroicon-o-photo')
->schema(ImagesBlock::schema()),
Builder\Block::make('video')
->label('Видео')
->icon('heroicon-o-film')
->schema(VideoBlock::schema()),
Builder\Block::make('postsList')
->label('Список новостей')
->icon('heroicon-o-newspaper')
->schema(PostListBlock::schema()),
Builder\Block::make('postItem')
->label('Конкретная новость')
->icon('heroicon-o-document-text')
->schema(PostItemBlock::schema()),
Builder\Block::make('pageItem')
->label('Конкретная страница')
->icon('heroicon-o-document')
->schema(PageItemBlock::schema()),
Builder\Block::make('customForm')
->label('Пользовательская форма')
->icon('heroicon-o-clipboard-document-list')
->schema(CustomFormBlock::schema()),
Builder\Block::make('pageResourceList')
->label('Ресурсы')
->icon('heroicon-o-archive-box')
->schema(PageResourceListBlock::schema()),
Builder\Block::make('contact')
->label('Контакты')
->icon('heroicon-o-phone')
->schema(ContactBlock::schema()),
Builder\Block::make('slider')
->label('Слайдер')
->icon('heroicon-o-presentation-chart-line')
->schema(SliderBlock::schema()),
])
->collapsed()
->blockNumbers(false)
->collapsible()
->blockPickerColumns(3)
->blockPickerWidth('2xl')
->addActionLabel('Добавить новый блок')
->cloneable()
->reorderableWithButtons(),
])
->minItems(1)
->collapsible()
->cloneable()
->itemLabel(fn (array $state): ?string => $state['title'] ?? null),
];
}
}
@@ -0,0 +1,53 @@
<?php
namespace App\Filament\Components\Forms\ItemForm\Blocks;
use App\Filament\Components\Forms\ItemForm\Blocks\BlockSchema;
use App\Helpers\ByteConverter;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\TextInput;
use Illuminate\Support\Carbon;
use Illuminate\Support\Str;
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
class VideoBlock implements BlockSchema
{
public static function schema(): array
{
return [
TextInput::make('mime')
->label('Тип видео')
->readOnly()
->helperText('Определяется автоматически'),
TextInput::make('title')
->label('Название видео')
->placeholder('Введите название видео')
->required()
->maxLength(255)
->autofocus()
->helperText('Это название будет отображаться перед видео'),
FileUpload::make('path')
->label('Видеофайл')
->required()
->acceptedFileTypes([
'video/mp4',
'video/quicktime',
'video/x-msvideo',
'video/x-ms-wmv',
'video/avi',
'video/webm',
'video/ogg',
'video/3gpp',
'video/3gpp2',
'video/x-m4v',
])
->disk('public')
->directory('videos')
->helperText('Поддерживаются популярные видеоформаты (MP4, MOV, AVI и др.)')
->afterStateUpdated(fn (callable $set, $state) => $set('mime', $state?->getMimeType())),
];
}
}
@@ -16,9 +16,11 @@ use Filament\Forms;
use Filament\Forms\Components\Builder; use Filament\Forms\Components\Builder;
use Filament\Forms\Components\FileUpload; use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Hidden; use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\RichEditor; use Filament\Forms\Components\RichEditor;
use Filament\Forms\Components\Section; use Filament\Forms\Components\Section;
use Filament\Forms\Components\Select; use Filament\Forms\Components\Select;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle; use Filament\Forms\Components\Toggle;
use Filament\Forms\Form; use Filament\Forms\Form;
@@ -29,23 +31,44 @@ use Mohamedsabil83\FilamentFormsTinyeditor\Components\TinyEditor;
class FormBuilderItem class FormBuilderItem
{ {
public static function getItem() public static function getItem(): Builder
{ {
return Builder::make('columns') return Builder::make('columns')
->label('Конструктор полей формы')
->addActionLabel('Добавить новое поле')
->blockPickerColumns(3)
->blockPickerWidth('2xl')
->collapsed()
->collapsible()
->cloneable()
->schema([ ->schema([
// Email поле
Builder\Block::make('email') Builder\Block::make('email')
->label('Почта') ->icon('heroicon-o-envelope')
->label('Поле Email')
->schema([ ->schema([
TextInput::make('title_field') TextInput::make('title_field')
->label('Заголовок поля') ->label('Название поля')
->placeholder('Например: Ваш Email')
->helperText('Это название будет отображаться пользователям')
->required()
->maxLength(255)
->live(onBlur: true) ->live(onBlur: true)
->afterStateUpdated(function (string $operation, string $state, Forms\Set $set) { ->afterStateUpdated(function (string $operation, string $state, Forms\Set $set) {
$set('name_field', Str::slug($state) . Carbon::now()->timestamp ); $set('name_field', Str::slug($state) . Carbon::now()->timestamp);
}), }),
Forms\Components\Hidden::make('name_field')->required(),
Forms\Components\Textarea::make('description')->label('Описание поля (опционально)'),
Section::make('Настройка') Hidden::make('name_field')->required(),
Textarea::make('description')
->label('Подсказка для поля')
->placeholder('Например: Введите действующий email')
->helperText('Необязательное пояснение для пользователей')
->maxLength(500)
->columnSpanFull(),
Section::make('Дополнительные настройки')
->collapsible()
->collapsed() ->collapsed()
->statePath('rules') ->statePath('rules')
->schema([ ->schema([
@@ -54,18 +77,34 @@ class FormBuilderItem
RuleLengthLimitComponent::getComponent(), RuleLengthLimitComponent::getComponent(),
]), ]),
]), ]),
// Phone поле
Builder\Block::make('phone') Builder\Block::make('phone')
->label('Телефон') ->icon('heroicon-o-phone')
->label('Поле Телефона')
->schema([ ->schema([
TextInput::make('title_field') TextInput::make('title_field')
->label('Заголовок поля') ->label('Название поля')
->placeholder('Например: Ваш телефон')
->helperText('Укажите контактный номер для связи')
->required()
->maxLength(255)
->live(onBlur: true) ->live(onBlur: true)
->afterStateUpdated(function (string $operation, string $state, Forms\Set $set) { ->afterStateUpdated(function (string $operation, string $state, Forms\Set $set) {
$set('name_field', Str::slug($state) . Carbon::now()->timestamp ); $set('name_field', Str::slug($state) . Carbon::now()->timestamp);
}), }),
Forms\Components\Hidden::make('name_field')->required(),
Forms\Components\Textarea::make('description')->label('Описание поля (опционально)'), Hidden::make('name_field')->required(),
Section::make('Настройка')
Textarea::make('description')
->label('Подсказка для поля')
->placeholder('Например: +7 (XXX) XXX-XX-XX')
->maxLength(500)
->columnSpanFull(),
Section::make('Дополнительные настройки')
->collapsible()
->collapsed()
->statePath('rules') ->statePath('rules')
->schema([ ->schema([
RuleRequiredComponent::getComponent(), RuleRequiredComponent::getComponent(),
@@ -73,73 +112,135 @@ class FormBuilderItem
RuleLengthLimitComponent::getComponent(), RuleLengthLimitComponent::getComponent(),
]), ]),
]), ]),
// Короткий текст
Builder\Block::make('text') Builder\Block::make('text')
->icon('heroicon-o-pencil')
->label('Короткий текст') ->label('Короткий текст')
->schema([ ->schema([
TextInput::make('title_field') TextInput::make('title_field')
->label('Заголовок поля') ->label('Название поля')
->placeholder('Например: Ваше имя')
->helperText('Краткий текст (до 255 символов)')
->required()
->maxLength(255)
->live(onBlur: true) ->live(onBlur: true)
->afterStateUpdated(function (string $operation, string $state, Forms\Set $set) { ->afterStateUpdated(function (string $operation, string $state, Forms\Set $set) {
$set('name_field', Str::slug($state) . Carbon::now()->timestamp ); $set('name_field', Str::slug($state) . Carbon::now()->timestamp);
}), }),
Forms\Components\Hidden::make('name_field')->required(),
Forms\Components\Textarea::make('description')->label('Описание поля (опционально)'),
Section::make('Настройка') Hidden::make('name_field')->required(),
->statePath('rules')
->schema([ Textarea::make('description')
RuleRequiredComponent::getComponent(), ->label('Подсказка для поля')
RuleLengthLimitComponent::getComponent(), ->placeholder('Например: Введите ваше полное имя')
]), ->maxLength(500)
]), ->columnSpanFull(),
Builder\Block::make('textarea')
->label('Длинный текст текст') Section::make('Дополнительные настройки')
->schema([ ->collapsible()
TextInput::make('title_field') ->collapsed()
->label('Заголовок поля')
->live(onBlur: true)
->afterStateUpdated(function (string $operation, string $state, Forms\Set $set) {
$set('name_field', Str::slug($state) . Carbon::now()->timestamp );
}),
Forms\Components\Hidden::make('name_field')->required(),
Forms\Components\Textarea::make('description')->label('Описание поля (опционально)'),
Section::make('Настройка')
->statePath('rules') ->statePath('rules')
->schema([ ->schema([
RuleRequiredComponent::getComponent(), RuleRequiredComponent::getComponent(),
RuleLengthLimitComponent::getComponent(), RuleLengthLimitComponent::getComponent(),
]), ]),
]), ]),
// Длинный текст
Builder\Block::make('textarea')
->icon('heroicon-o-document-text')
->label('Длинный текст')
->schema([
TextInput::make('title_field')
->label('Название поля')
->placeholder('Например: Ваш комментарий')
->helperText('Расширенный текст (до 5000 символов)')
->required()
->maxLength(255)
->live(onBlur: true)
->afterStateUpdated(function (string $operation, string $state, Forms\Set $set) {
$set('name_field', Str::slug($state) . Carbon::now()->timestamp);
}),
Hidden::make('name_field')->required(),
Textarea::make('description')
->label('Подсказка для поля')
->placeholder('Например: Опишите вашу проблему подробно')
->maxLength(500)
->columnSpanFull(),
Section::make('Дополнительные настройки')
->collapsible()
->collapsed()
->statePath('rules')
->schema([
RuleRequiredComponent::getComponent(),
RuleLengthLimitComponent::getComponent(),
]),
]),
// Дата
Builder\Block::make('date') Builder\Block::make('date')
->icon('heroicon-o-calendar')
->label('Дата') ->label('Дата')
->schema([ ->schema([
TextInput::make('title_field') TextInput::make('title_field')
->label('Заголовок поля') ->label('Название поля')
->placeholder('Например: Дата рождения')
->helperText('Выбор даты из календаря')
->required()
->maxLength(255)
->live(onBlur: true) ->live(onBlur: true)
->afterStateUpdated(function (string $operation, string $state, Forms\Set $set) { ->afterStateUpdated(function (string $operation, string $state, Forms\Set $set) {
$set('name_field', Str::slug($state) . Carbon::now()->timestamp ); $set('name_field', Str::slug($state) . Carbon::now()->timestamp);
}), }),
Forms\Components\Hidden::make('name_field')->required(),
Forms\Components\Textarea::make('description')->label('Описание поля (опционально)'), Hidden::make('name_field')->required(),
Section::make('Настройка')
Textarea::make('description')
->label('Подсказка для поля')
->placeholder('Например: Укажите вашу дату рождения')
->maxLength(500)
->columnSpanFull(),
Section::make('Дополнительные настройки')
->collapsible()
->collapsed()
->statePath('rules') ->statePath('rules')
->schema([ ->schema([
RuleRequiredComponent::getComponent(), RuleRequiredComponent::getComponent(),
]), ]),
]), ]),
// Ссылка
Builder\Block::make('url') Builder\Block::make('url')
->icon('heroicon-o-link')
->label('Ссылка') ->label('Ссылка')
->schema([ ->schema([
TextInput::make('title_field') TextInput::make('title_field')
->label('Заголовок поля') ->label('Название поля')
->placeholder('Например: Ваш сайт')
->helperText('Введите корректный URL адрес')
->required()
->maxLength(255)
->live(onBlur: true) ->live(onBlur: true)
->afterStateUpdated(function (string $operation, string $state, Forms\Set $set) { ->afterStateUpdated(function (string $operation, string $state, Forms\Set $set) {
$set('name_field', Str::slug($state) . Carbon::now()->timestamp ); $set('name_field', Str::slug($state) . Carbon::now()->timestamp);
}), }),
Forms\Components\Hidden::make('name_field')->required(),
Forms\Components\Textarea::make('description')->label('Описание поля (опционально)'), Hidden::make('name_field')->required(),
Section::make('Настройка')
Textarea::make('description')
->label('Подсказка для поля')
->placeholder('Например: https://example.com')
->maxLength(500)
->columnSpanFull(),
Section::make('Дополнительные настройки')
->collapsible()
->collapsed()
->statePath('rules') ->statePath('rules')
->schema([ ->schema([
RuleRequiredComponent::getComponent(), RuleRequiredComponent::getComponent(),
@@ -147,140 +248,178 @@ class FormBuilderItem
RuleLengthLimitComponent::getComponent(), RuleLengthLimitComponent::getComponent(),
]), ]),
]), ]),
// Множественный выбор
Builder\Block::make('multiple_choice') Builder\Block::make('multiple_choice')
->label('Несколько вариантов') ->icon('heroicon-o-check-circle')
->label('Множественный выбор')
->schema([ ->schema([
TextInput::make('title_field') TextInput::make('title_field')
->label('Заголовок поля') ->label('Название группы')
->placeholder('Например: Ваши интересы')
->helperText('Несколько вариантов с возможностью выбора')
->required()
->maxLength(255)
->live(onBlur: true) ->live(onBlur: true)
->afterStateUpdated(function (string $operation, string $state, Forms\Set $set) { ->afterStateUpdated(function (string $operation, string $state, Forms\Set $set) {
$set('name_field', Str::slug($state) . Carbon::now()->timestamp ); $set('name_field', Str::slug($state) . Carbon::now()->timestamp);
}), }),
Forms\Components\Hidden::make('name_field')->required(), Forms\Components\Repeater::make('columns')->schema([
TextInput::make('title_field')
->live(onBlur: true)
->afterStateUpdated(function (string $operation, string $state, Forms\Set $set) {
$set('name_field', Str::slug($state) . Carbon::now()->timestamp );
}),
Forms\Components\Hidden::make('name_field')->required(),
Forms\Components\Textarea::make('description')->label('Описание поля (опционально)'),
])->collapsed(),
Section::make('Настройка') Hidden::make('name_field')->required(),
Repeater::make('columns')
->label('Варианты выбора')
->addActionLabel('Добавить вариант')
->collapsible()
->cloneable()
->itemLabel(fn (array $state): ?string => $state['title_field'] ?? 'Новый вариант')
->schema([
TextInput::make('title_field')
->label('Текст варианта')
->placeholder('Например: Спорт')
->required()
->maxLength(255)
->live(onBlur: true)
->afterStateUpdated(function (string $operation, string $state, Forms\Set $set) {
$set('name_field', Str::slug($state) . Carbon::now()->timestamp);
}),
Hidden::make('name_field')->required(),
Textarea::make('description')
->label('Описание варианта')
->placeholder('Необязательное описание')
->maxLength(500),
]),
Section::make('Дополнительные настройки')
->collapsible()
->collapsed()
->statePath('rules') ->statePath('rules')
->schema([ ->schema([
RuleRequiredComponent::getComponent(), RuleRequiredComponent::getComponent(),
]), ]),
]), ]),
// Одиночный выбор
Builder\Block::make('single_choice') Builder\Block::make('single_choice')
->label('Один вариант') ->icon('heroicon-o-radio')
->label('Одиночный выбор')
->schema([ ->schema([
TextInput::make('title_field') TextInput::make('title_field')
->label('Заголовок поля') ->label('Название группы')
->placeholder('Например: Ваш пол')
->helperText('Один вариант из предложенных')
->required()
->maxLength(255)
->live(onBlur: true) ->live(onBlur: true)
->afterStateUpdated(function (string $operation, string $state, Forms\Set $set) { ->afterStateUpdated(function (string $operation, string $state, Forms\Set $set) {
$set('name_field', Str::slug($state) . Carbon::now()->timestamp ); $set('name_field', Str::slug($state) . Carbon::now()->timestamp);
}), }),
Forms\Components\Hidden::make('name_field')->required(), Forms\Components\Repeater::make('columns')->schema([
TextInput::make('title_field')
->live(onBlur: true)
->afterStateUpdated(function (string $operation, string $state, Forms\Set $set) {
$set('name_field', Str::slug($state) . Carbon::now()->timestamp );
}),
Forms\Components\Hidden::make('name_field')->required(),
Forms\Components\Textarea::make('description')->label('Описание поля (опционально)'),
])->collapsed(),
Section::make('Настройка') Hidden::make('name_field')->required(),
Repeater::make('columns')
->label('Варианты выбора')
->addActionLabel('Добавить вариант')
->collapsible()
->cloneable()
->itemLabel(fn (array $state): ?string => $state['title_field'] ?? 'Новый вариант')
->schema([
TextInput::make('title_field')
->label('Текст варианта')
->placeholder('Например: Мужской')
->required()
->maxLength(255)
->live(onBlur: true)
->afterStateUpdated(function (string $operation, string $state, Forms\Set $set) {
$set('name_field', Str::slug($state) . Carbon::now()->timestamp);
}),
Hidden::make('name_field')->required(),
Textarea::make('description')
->label('Описание варианта')
->placeholder('Необязательное описание')
->maxLength(500),
]),
Section::make('Дополнительные настройки')
->collapsible()
->collapsed()
->statePath('rules') ->statePath('rules')
->schema([ ->schema([
RuleRequiredComponent::getComponent(), RuleRequiredComponent::getComponent(),
]), ]),
]), ]),
// Дополнительное образование
Builder\Block::make('additional_education_choice') Builder\Block::make('additional_education_choice')
->label('Выбрать дополнительное образование') ->icon('heroicon-o-academic-cap')
->label('Доп. образование')
->schema([ ->schema([
TextInput::make('title_field') TextInput::make('title_field')
->label('Заголовок поля') ->label('Название поля')
->placeholder('Например: Дополнительное образование')
->helperText('Выбор из списка доп. образования')
->required()
->maxLength(255)
->live(onBlur: true) ->live(onBlur: true)
->afterStateUpdated(function (string $operation, string $state, Forms\Set $set) { ->afterStateUpdated(function (string $operation, string $state, Forms\Set $set) {
$set('name_field', Str::slug($state) . Carbon::now()->timestamp ); $set('name_field', Str::slug($state) . Carbon::now()->timestamp);
}), }),
Forms\Components\Hidden::make('name_field')->required(),
Forms\Components\Textarea::make('description')->label('Описание поля (опционально)'),
Section::make('Настройка') Hidden::make('name_field')->required(),
Textarea::make('description')
->label('Подсказка для поля')
->placeholder('Например: Выберите интересующую программу')
->maxLength(500)
->columnSpanFull(),
Section::make('Дополнительные настройки')
->collapsible()
->collapsed()
->statePath('rules') ->statePath('rules')
->schema([ ->schema([]),
// RuleRequiredComponent::getComponent(),
// RuleLengthLimitComponent::getComponent(),
]),
]) ])
->maxItems(1), ->maxItems(1),
// Образовательная программа
Builder\Block::make('educational_program_choice') Builder\Block::make('educational_program_choice')
->label('Выбрать Образовательную программу') ->icon('heroicon-o-book-open')
->label('Образовательная программа')
->schema([ ->schema([
TextInput::make('title_field') TextInput::make('title_field')
->label('Заголовок поля') ->label('Название поля')
->placeholder('Например: Основная программа')
->helperText('Выбор из списка образовательных программ')
->required()
->maxLength(255)
->live(onBlur: true) ->live(onBlur: true)
->afterStateUpdated(function (string $operation, string $state, Forms\Set $set) { ->afterStateUpdated(function (string $operation, string $state, Forms\Set $set) {
$set('name_field', Str::slug($state) . Carbon::now()->timestamp ); $set('name_field', Str::slug($state) . Carbon::now()->timestamp);
}), }),
Forms\Components\Hidden::make('name_field')->required(),
Forms\Components\Textarea::make('description')->label('Описание поля (опционально)'),
Section::make('Настройка') Hidden::make('name_field')->required(),
Textarea::make('description')
->label('Подсказка для поля')
->placeholder('Например: Выберите основную программу обучения')
->maxLength(500)
->columnSpanFull(),
Section::make('Дополнительные настройки')
->collapsible()
->collapsed()
->statePath('rules') ->statePath('rules')
->schema([ ->schema([
RuleRequiredComponent::getComponent(), RuleRequiredComponent::getComponent(),
RuleLengthLimitComponent::getComponent(), RuleLengthLimitComponent::getComponent(),
]), ]),
]) ])
->maxItems(1), ->maxItems(1)
Builder\Block::make('captcha') ]);
->label('reCaptcha')
->schema([
TextInput::make('title_field')
->label('Заголовок поля')
->live(onBlur: true)
->default('Капча')
->disabled(true)
->dehydrated(true),
Forms\Components\Hidden::make('name_field')->required()->default(Str::slug('reCaptcha') . Carbon::now()->timestamp),
Section::make('Настройка')
->collapsed()
->statePath('rules')
->schema([
RuleRequiredComponent::getComponent()->default(true),
]),
]),
Builder\Block::make('personal_data')
->label('Соглашение на обработку персональных данных')
->schema([
TextInput::make('title_field')
->label('Заголовок поля')
->live(onBlur: true)
->default('Соглашение на обработку персональных данных')
->afterStateUpdated(function (string $operation, string $state, Forms\Set $set) {
$set('name_field', Str::slug($state) . Carbon::now()->timestamp );
}),
Forms\Components\Hidden::make('name_field')->required()->default(Str::slug('Соглашение на обработку персональных данных') . Carbon::now()->timestamp),
Section::make('Настройка')
->collapsed()
->statePath('rules')
->schema([
RuleRequiredComponent::getComponent()->default(true),
]),
]),
])
->label('')
->addActionLabel('Добавить поле')
->blockPickerColumns(3)
->blockPickerWidth('2xl')
->collapsed();
} }
} }
@@ -13,10 +13,13 @@ use App\Models\Post;
use Filament\Forms; use Filament\Forms;
use Filament\Forms\Components\Builder; use Filament\Forms\Components\Builder;
use Filament\Forms\Components\FileUpload; use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Grid;
use Filament\Forms\Components\Hidden; use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\RichEditor; use Filament\Forms\Components\RichEditor;
use Filament\Forms\Components\Section; use Filament\Forms\Components\Section;
use Filament\Forms\Components\Select; use Filament\Forms\Components\Select;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle; use Filament\Forms\Components\Toggle;
use Filament\Forms\Form; use Filament\Forms\Form;
@@ -29,202 +32,347 @@ class ContentBuilderItem
{ {
public static function getItem(string $name) public static function getItem(string $name)
{ {
return return Builder::make($name)
Builder::make($name)->label('')->blocks([ ->label('Конструктор содержимого')
Builder\Block::make('heading')->label('Заголовок') ->blocks([
// Заголовок
Builder\Block::make('heading')
->icon('heroicon-o-title')
->label('Заголовок')
->schema([ ->schema([
TextInput::make('id')->hidden()->integer()->default(rand(2335235,324634264263426)), TextInput::make('id')
->hidden()
->integer()
->default(rand(2335235, 324634264263426)),
TextInput::make('content') TextInput::make('content')
->label('') ->label('Текст заголовка')
->live(onBlur: true) ->placeholder('Введите заголовок H2-H4')
->afterStateUpdated(function (string $operation, string $state, Forms\Set $set, $get) { ->hint('Рекомендуется 50-80 символов')
}), ->helperText('Используйте для семантической структуры')
->minLength(10)
->maxLength(120)
->required()
->live(onBlur: true),
]), ]),
// Текстовый блок
Builder\Block::make('paragraph') Builder\Block::make('paragraph')
->icon('heroicon-o-document-text')
->label('Текстовый блок')
->schema([ ->schema([
TinyEditor::make('content') TinyEditor::make('content')
->label('') ->label('Содержимое')
->profile('test') ->placeholder('Введите текст...')
->required(), ->hint('Поддерживается форматирование')
])->label('Текст'), ->helperText('Для заголовков используйте стили H3-H4')
Builder\Block::make('files') ->required()
->label('Файл(-ы)') ->columnSpanFull(),
->schema([
Forms\Components\Repeater::make('file')->schema([
Hidden::make('expansion')->required(),
Hidden::make('size')->required(),
TextInput::make('title')
->required()
->maxLength(255)
->autofocus(),
FileUpload::make('path')
->required()
->getUploadedFileNameForStorageUsing(
fn (TemporaryUploadedFile $file): string =>
str(Str::slug(pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME) . '-' . Carbon::now()->timestamp ) . '.' . $file->getClientOriginalExtension())
)
->acceptedFileTypes([
'application/pdf',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'application/zip'
])
->maxSize(512000)
->disk('public')
->directory('files')
->downloadable()
->afterStateUpdated(function ($set, $state) {
$set('expansion', $state?->getClientOriginalExtension());
$set('size', ByteConverter::bytesToHuman($state?->getSize()));
})
->visibility('public')
]),
]), ]),
// Файлы
Builder\Block::make('files')
->icon('heroicon-o-paper-clip')
->label('Файлы для скачивания')
->schema([
Repeater::make('file')
->label('')
->hint('Максимум 10 файлов')
->schema([
Hidden::make('expansion')->required(),
Hidden::make('size')->required(),
TextInput::make('title')
->label('Название файла')
->placeholder('Годовой отчет 2023.pdf')
->required()
->maxLength(255),
FileUpload::make('path')
->label('Выберите файл')
->helperText('Допустимы: PDF, DOCX, XLSX, PPTX, ZIP')
->hint('Макс. размер 500KB')
->required()
->acceptedFileTypes([
'application/pdf',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'application/zip'
])
->maxSize(512000)
->disk('public')
->directory('files')
->downloadable()
->preserveFilenames()
->afterStateUpdated(function ($set, $state) {
$set('expansion', $state?->getClientOriginalExtension());
$set('size', ByteConverter::bytesToHuman($state?->getSize()));
}),
])
->maxItems(10)
->collapsible()
->itemLabel(fn (array $state): string => $state['title'] ?? 'Новый файл'),
]),
// Карточка персоны
Builder\Block::make('person') Builder\Block::make('person')
->label('Персона') ->icon('heroicon-o-user')
->label('Карточка сотрудника')
->schema([ ->schema([
TextInput::make('name') TextInput::make('name')
->label('Имя') ->label('ФИО')
->placeholder('Иванов Иван Иванович')
->required() ->required()
->maxLength(255), ->maxLength(255),
FileUpload::make('photo') FileUpload::make('photo')
->label('Фотография') ->label('Фотография')
->hint('Оптимальный размер 500x500px')
->helperText('Автоматическая конвертация в WebP')
->image() ->image()
->optimize('webp') ->optimize('webp')
->resize(50) ->resize(50)
->disk('public') ->disk('public')
->directory('images') ->directory('personnel')
->imageEditor(), ->imageEditor()
Forms\Components\Repeater::make('info')->schema([ ->required(),
Repeater::make('info')
->label('Характеристики')
->hint('Добавьте 3-5 ключевых пунктов')
->schema([
TextInput::make('column') TextInput::make('column')
->label('Название колонки') ->label('Параметр')
->placeholder('Стаж работы')
->required() ->required()
->maxLength(255), ->maxLength(100),
Forms\Components\Textarea::make('content')
->label('Содержание') Textarea::make('content')
->label('Значение')
->placeholder('10 лет')
->required() ->required()
->maxLength(1000), ->maxLength(500),
])->minItems(1)->label('Информация о персоне'), ])
->minItems(1)
->maxItems(10)
->collapsible()
->itemLabel(fn (array $state): string => $state['column'] ?? 'Новый параметр'),
]), ]),
// Этапы
Builder\Block::make('stepper') Builder\Block::make('stepper')
->label('Строитель этапов') ->icon('heroicon-o-list-bullet')
->label('Пошаговый процесс')
->schema([ ->schema([
TextInput::make('step_name') TextInput::make('step_name')
->label('Название шага') ->label('Название процесса')
->placeholder('Процесс согласования')
->required()
->maxLength(100),
Repeater::make('steps')
->label('Этапы')
->hint('Добавьте последовательные шаги')
->schema([
TextInput::make('title')
->label('Шаг')
->placeholder('1. Подготовка документов')
->required()
->maxLength(100),
RichEditor::make('content')
->label('Описание')
->required()
->maxLength(2000),
])
->minItems(2)
->collapsible()
->itemLabel(fn (array $state): string => $state['title'] ?? 'Новый этап'),
]),
// Табы
Builder\Block::make('tabs')
->icon('heroicon-o-rectangle-stack')
->label('Табы')
->schema([
Repeater::make('tabs')
->label('')
->hint('Оптимально 3-5 вкладок')
->schema([
TextInput::make('title')
->label('Название вкладки')
->placeholder('Характеристики')
->required()
->maxLength(50),
RichEditor::make('content')
->label('Содержимое')
->required(),
])
->minItems(2)
->maxItems(8)
->collapsible()
->itemLabel(fn (array $state): string => $state['title'] ?? 'Новая вкладка'),
]),
// Слайдер изображений
Builder\Block::make('images')
->icon('heroicon-o-photo')
->label('Галерея изображений')
->schema([
FileUpload::make('url')
->label('Изображения')
->hint('Оптимально 3-5 изображений')
->helperText('Поддерживаются JPG, PNG, WEBP')
->image()
->multiple()
->reorderable()
->minFiles(1)
->maxFiles(10)
->disk('public')
->directory('gallery')
->imageEditor()
->required(),
TextInput::make('alt')
->label('Описание для SEO')
->placeholder('Наш офис в Москве')
->hint('Краткое описание изображения')
->maxLength(255),
]),
// Одиночное изображение
Builder\Block::make('image')
->icon('heroicon-o-photo')
->label('Изображение')
->schema([
FileUpload::make('url')
->label('Выберите изображение')
->helperText('Рекомендуемое соотношение 16:9')
->image()
->disk('public')
->directory('images')
->imageEditor()
->required(),
TextInput::make('alt')
->label('ALT-текст')
->placeholder('Описание изображения')
->required() ->required()
->maxLength(255), ->maxLength(255),
Forms\Components\Repeater::make('steps')->schema([
TextInput::make('title')
->required()
->maxLength(255)->columnSpanFull(),
RichEditor::make('content')->required(),
])->minItems(1),
]), ]),
TabBuilderItem::getItem(),
Builder\Block::make('images') // Видео
->schema([
FileUpload::make('url')
->label('Изображение(-я)')
->image()
->multiple()
->reorderable()
->maxFiles(5)
->disk('public')
->directory('images')
->imageEditor()
->required(),
TextInput::make('alt')
->label('Описание')
->placeholder('Необязяательно')
])->label('Слайдер изображений'),
Builder\Block::make('image')
->schema([
FileUpload::make('url')
->label('Изображение(-я)')
->image()
->multiple()
->reorderable()
->maxFiles(5)
->disk('public')
->directory('images')
->imageEditor()
->required(),
TextInput::make('alt')
->label('Описание')
->placeholder('Необязяательно')
])->label('Изображение'),
Builder\Block::make('video') Builder\Block::make('video')
->label('Видео (Не стабильно)') ->icon('heroicon-o-film')
->label('Видео')
->schema([ ->schema([
TextInput::make('mime')->readOnly(), TextInput::make('mime')
->label('Формат')
->readOnly(),
TextInput::make('title') TextInput::make('title')
->label('Название видео')
->placeholder('Обзор продукта')
->required() ->required()
->maxLength(255) ->maxLength(255),
->autofocus(),
FileUpload::make('path') FileUpload::make('path')
->label('Видеофайл')
->hint('MP4, WebM, до 50MB')
->helperText('Рекомендуемое разрешение 1080p')
->required() ->required()
->acceptedFileTypes([ ->acceptedFileTypes([
'video/mp4', 'video/mp4',
'video/quicktime',
'video/x-msvideo',
'video/x-ms-wmv',
'video/avi',
'video/webm', 'video/webm',
'video/ogg', 'video/ogg',
'video/3gpp',
'video/3gpp2',
'video/x-m4v',
]) ])
->maxSize(51200)
->disk('public') ->disk('public')
->directory('videos') ->directory('videos'),
->afterStateUpdated(fn (callable $set, $state) => $set('mime', $state?->getMimeType())),
]), ]),
// Список новостей
Builder\Block::make('postsList') Builder\Block::make('postsList')
->icon('heroicon-o-newspaper')
->label('Лента новостей')
->schema([ ->schema([
Forms\Components\Grid::make(2)->schema([ Grid::make(2)
TextInput::make('count') ->schema([
->label('Количество запией') TextInput::make('count')
->integer(), ->label('Количество')
Select::make('category') ->numeric()
->options(Category::all()->pluck('title', 'id')) ->minValue(1)
]), ->maxValue(20)
])->label('Список новостей'), ->default(5)
->required(),
Select::make('category')
->label('Категория')
->options(Category::all()->pluck('title', 'id'))
->searchable()
->placeholder('Все категории'),
]),
]),
// Отдельная новость
Builder\Block::make('postItem') Builder\Block::make('postItem')
->icon('heroicon-o-document-text')
->label('Конкретная новость')
->schema([ ->schema([
Select::make('post') Select::make('post')
->options(Post::query()->where('status', PostStatus::PUBLISHED)->pluck('title', 'id')) ->label('Выберите новость')
->options(Post::published()->pluck('title', 'id'))
->searchable() ->searchable()
->required(), ->required()
])->label('Новость'), ->placeholder('Начните вводить название'),
]),
// Страница
Builder\Block::make('pageItem') Builder\Block::make('pageItem')
->icon('heroicon-o-document')
->label('Ссылка на страницу')
->schema([ ->schema([
Select::make('page') Select::make('page')
->options(Page::query()->where('title', '!=' , null)->where('is_visible', true)->pluck('title', 'id')) ->label('Страница')
->options(Page::visible()->pluck('title', 'id'))
->searchable() ->searchable()
->required(), ->required(),
])->label('Страница'), ]),
// Форма
Builder\Block::make('customForm') Builder\Block::make('customForm')
->icon('heroicon-o-clipboard-document')
->label('Форма')
->schema([ ->schema([
Select::make('form') Select::make('form')
->options(CustomForm::query()->where('status', CustomFormStatus::PUBLISHED)->pluck('title', 'form_id')) ->label('Выберите форму')
->options(CustomForm::published()->pluck('title', 'form_id'))
->searchable() ->searchable()
->required(), ->required(),
])->label('Форма'), ]),
// Ресурсы
Builder\Block::make('pageResourceList') Builder\Block::make('pageResourceList')
->icon('heroicon-o-archive-box')
->label('Список ресурсов')
->schema([ ->schema([
Select::make('resource') Select::make('resource')
->options(PageReferenceList::query()->where('is_active', true)->pluck('title', 'slug')) ->label('Ресурс')
->options(PageReferenceList::active()->pluck('title', 'slug'))
->searchable() ->searchable()
->required(), ->required(),
])->label('Ресурсы'), ]),
]) ])
->collapsed() ->collapsed()
->blockNumbers(false) ->blockNumbers(false)
->collapsible() ->collapsible()
->blockPickerColumns(3) ->blockPickerColumns(3)
->blockPickerWidth('2xl') ->blockPickerWidth('2xl')
->addActionLabel('Добавить новый блок'); ->addActionLabel('Добавить блок')
->addBetweenActionLabel('Вставить блок между')
->cloneActionLabel('Клонировать блок');
} }
@@ -6,10 +6,12 @@ use App\Enums\CustomFormStatus;
use App\Enums\PostStatus; use App\Enums\PostStatus;
use App\Helpers\ByteConverter; use App\Helpers\ByteConverter;
use App\Models\Category; use App\Models\Category;
use App\Models\ContactWidget;
use App\Models\CustomForm; use App\Models\CustomForm;
use App\Models\Page; use App\Models\Page;
use App\Models\PageReferenceList; use App\Models\PageReferenceList;
use App\Models\Post; use App\Models\Post;
use App\Models\Slider;
use Filament\Forms; use Filament\Forms;
use Filament\Forms\Components\Builder; use Filament\Forms\Components\Builder;
use Filament\Forms\Components\FileUpload; use Filament\Forms\Components\FileUpload;
@@ -29,210 +31,356 @@ class TabBuilderItem
{ {
public static function getItem() public static function getItem()
{ {
return return Builder::make('content')
Builder\Block::make('tabs') ->label('Содержимое вкладки')
->label('Вкладки') ->blocks([
->schema([ Builder\Block::make('heading')
Forms\Components\Repeater::make('tab')->schema([ ->label('Заголовок')
TextInput::make('title') ->icon('heroicon-o-hashtag')
->schema([
TextInput::make('id')
->hidden()
->integer()
->default(rand(2335235, 324634264263426)),
TextInput::make('content')
->label('Текст заголовка')
->placeholder('Введите текст заголовка')
->helperText('Основной заголовок раздела')
->live(onBlur: true)
->required() ->required()
->maxLength(255)->columnSpanFull(), ->maxLength(255),
Builder::make('content')->label('')->blocks([ ]),
Builder\Block::make('heading')->label('Заголовок')
->schema([ Builder\Block::make('paragraph')
TextInput::make('id')->hidden()->integer()->default(rand(2335235,324634264263426)), ->label('Текст')
TextInput::make('content') ->icon('heroicon-o-document-text')
->label('') ->schema([
->live(onBlur: true) Toggle::make('seo_active')
->afterStateUpdated(function (string $operation, string $state, Forms\Set $set, $get) { ->label('Использовать блок как SEO-текст')
}), ->helperText('Этот текст будет использоваться для SEO-оптимизации')
]), ->live(onBlur: true)
Builder\Block::make('paragraph') ->required()
->schema([ ->disabled(function ($state, Forms\Get $get) {
TinyEditor::make('content') $data = $get('../../');
->label('') return self::findSeoActive($data) && !$state;
->profile('test') })
->required(), ->dehydrated(),
])->label('Текст'), TinyEditor::make('content')
Builder\Block::make('files') ->label('Текст')
->label('Файл(-ы)') ->placeholder('Начните вводить текст...')
->schema([ ->profile('test')
Forms\Components\Repeater::make('file')->schema([ ->required()
Hidden::make('expansion')->required(), ->helperText('Основное текстовое содержимое блока'),
Hidden::make('size')->required(), ]),
TextInput::make('title')
->required() Builder\Block::make('files')
->maxLength(255) ->label('Файлы')
->autofocus(), ->icon('heroicon-o-paper-clip')
FileUpload::make('path') ->schema([
->required() Forms\Components\Repeater::make('file')
->getUploadedFileNameForStorageUsing( ->label('Файлы')
fn (TemporaryUploadedFile $file): string => ->helperText('Загрузите один или несколько файлов')
str(Str::slug(pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME) . '-' . Carbon::now()->timestamp ) . '.' . $file->getClientOriginalExtension()) ->schema([
) Hidden::make('expansion')->required(),
->acceptedFileTypes([ Hidden::make('size')->required(),
'application/pdf', TextInput::make('title')
'application/vnd.openxmlformats-officedocument.wordprocessingml.document', ->label('Название файла')
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', ->placeholder('Введите название файла')
'application/vnd.openxmlformats-officedocument.presentationml.presentation', ->required()
'application/zip' ->maxLength(255)
]) ->autofocus()
->maxSize(512000) ->helperText('Это название будет отображаться пользователям'),
->disk('public') FileUpload::make('path')
->directory('files') ->label('Файл')
->downloadable() ->required()
->afterStateUpdated(function ($set, $state) { ->helperText('Поддерживаются PDF, Word, Excel, PowerPoint и ZIP файлы (макс. 500KB)')
$set('expansion', $state?->getClientOriginalExtension()); ->getUploadedFileNameForStorageUsing(
$set('size', ByteConverter::bytesToHuman($state?->getSize())); fn (TemporaryUploadedFile $file): string =>
}) str(Str::slug(pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME) . '-' . Carbon::now()->timestamp) . '.' . $file->getClientOriginalExtension())
->visibility('public') )
]), ->acceptedFileTypes([
]), 'application/pdf',
Builder\Block::make('person') 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
->label('Персона') 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
->schema([ 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
TextInput::make('name') 'application/zip'
->label('Имя') ])
->required() ->maxSize(512000)
->maxLength(255), ->disk('public')
FileUpload::make('photo') ->directory('files')
->label('Фотография') ->downloadable()
->image() ->afterStateUpdated(function ($set, $state) {
->optimize('webp') $set('expansion', $state?->getClientOriginalExtension());
->resize(50) $set('size', ByteConverter::bytesToHuman($state?->getSize()));
->disk('public') })
->directory('images') ->visibility('public')
->imageEditor(), ->preserveFilenames()
Forms\Components\Repeater::make('info')->schema([ ])
TextInput::make('column') ->itemLabel(fn (array $state): ?string => $state['title'] ?? null)
->label('Название колонки')
->required()
->maxLength(255),
Forms\Components\Textarea::make('content')
->label('Содержание')
->required()
->maxLength(1000),
])->minItems(1)->label('Информация о персоне'),
]),
Builder\Block::make('stepper')
->label('Строитель этапов')
->schema([
TextInput::make('step_name')
->label('Название шага')
->required()
->maxLength(255),
Forms\Components\Repeater::make('steps')->schema([
TextInput::make('title')
->required()
->maxLength(255)->columnSpanFull(),
RichEditor::make('content')->required(),
])->minItems(1),
]),
Builder\Block::make('images')
->schema([
FileUpload::make('url')
->label('Изображение(-я)')
->image()
->multiple()
->reorderable()
->maxFiles(5)
->disk('public')
->directory('images')
->imageEditor()
->required(),
TextInput::make('alt')
->label('Описание')
->placeholder('Необязяательно')
])->label('Слайдер изображений'),
Builder\Block::make('image')
->schema([
FileUpload::make('url')
->label('Изображение(-я)')
->image()
->multiple()
->reorderable()
->maxFiles(5)
->disk('public')
->directory('images')
->imageEditor()
->required(),
TextInput::make('alt')
->label('Описание')
->placeholder('Необязяательно')
])->label('Изображение'),
Builder\Block::make('video')
->label('Видео (Не стабильно)')
->schema([
TextInput::make('mime')->readOnly(),
TextInput::make('title')
->required()
->maxLength(255)
->autofocus(),
FileUpload::make('path')
->required()
->acceptedFileTypes([
'video/mp4',
'video/quicktime',
'video/x-msvideo',
'video/x-ms-wmv',
'video/avi',
'video/webm',
'video/ogg',
'video/3gpp',
'video/3gpp2',
'video/x-m4v',
])
->disk('public')
->directory('videos')
->afterStateUpdated(fn (callable $set, $state) => $set('mime', $state?->getMimeType())),
]),
Builder\Block::make('postsList')
->schema([
Forms\Components\Grid::make(2)->schema([
TextInput::make('count')
->label('Количество запией')
->integer(),
Select::make('category')
->options(Category::all()->pluck('title', 'id'))
]),
])->label('Список новостей'),
Builder\Block::make('postItem')
->schema([
Select::make('post')
->options(Post::query()->where('status', PostStatus::PUBLISHED)->pluck('title', 'id'))
->searchable()
->required(),
])->label('Новость'),
Builder\Block::make('pageItem')
->schema([
Select::make('page')
->options(Page::query()->where('title', '!=' , null)->where('is_visible', true)->pluck('title', 'id'))
->searchable()
->required(),
])->label('Страница'),
Builder\Block::make('customForm')
->schema([
Select::make('form')
->options(CustomForm::query()->where('status', CustomFormStatus::PUBLISHED)->pluck('title', 'form_id'))
->searchable()
->required(),
])->label('Форма'),
Builder\Block::make('pageResourceList')
->schema([
Select::make('resource')
->options(PageReferenceList::query()->where('is_active', true)->pluck('title', 'slug'))
->searchable()
->required(),
])->label('Ресурсы'),
])
->collapsed()
->blockNumbers(false)
->collapsible() ->collapsible()
->blockPickerColumns(3) ->cloneable()
->blockPickerWidth('2xl') ->grid(2),
->addActionLabel('Добавить новый блок'), ]),
])->minItems(1),
]); Builder\Block::make('person')
->label('Персона')
->icon('heroicon-o-user')
->schema([
TextInput::make('name')
->label('Имя персоны')
->placeholder('Введите имя')
->required()
->maxLength(255)
->helperText('Полное имя персоны'),
FileUpload::make('photo')
->label('Фотография')
->image()
->helperText('Рекомендуемый формат: WebP')
->optimize('webp')
->resize(50)
->disk('public')
->directory('images')
->imageEditor()
->required()
->downloadable()
->openable(),
Forms\Components\Repeater::make('info')
->label('Дополнительная информация')
->helperText('Добавьте характеристики персоны')
->schema([
TextInput::make('column')
->label('Название характеристики')
->placeholder('Например: Должность')
->required()
->maxLength(255),
Forms\Components\Textarea::make('content')
->label('Значение')
->placeholder('Например: Главный инженер')
->required()
->maxLength(1000)
->columnSpanFull(),
])
->minItems(1)
->grid(2)
->collapsible()
->cloneable()
->itemLabel(fn (array $state): ?string => $state['column'] ?? null),
]),
Builder\Block::make('stepper')
->label('Этапы')
->icon('heroicon-o-list-bullet')
->schema([
TextInput::make('step_name')
->label('Название процесса')
->placeholder('Например: Процесс оформления')
->required()
->maxLength(255)
->helperText('Общее название для всех шагов'),
Forms\Components\Repeater::make('steps')
->label('Шаги')
->helperText('Добавьте шаги процесса')
->schema([
TextInput::make('title')
->label('Название шага')
->placeholder('Например: Шаг 1')
->required()
->maxLength(255)
->columnSpanFull(),
RichEditor::make('content')
->label('Описание шага')
->required()
->toolbarButtons([
'bold',
'italic',
'link',
'orderedList',
'bulletList',
]),
])
->minItems(1)
->collapsible()
->cloneable()
->itemLabel(fn (array $state): ?string => $state['title'] ?? null),
]),
Builder\Block::make('images')
->label('Слайдер изображений')
->icon('heroicon-o-photo')
->schema([
FileUpload::make('url')
->label('Изображения')
->image()
->multiple()
->reorderable()
->maxFiles(5)
->disk('public')
->directory('images')
->imageEditor()
->required()
->helperText('Максимум 5 изображений. Можно перетаскивать для изменения порядка'),
TextInput::make('alt')
->label('Описание изображений')
->placeholder('Необязательно')
->helperText('Используется для SEO и доступности'),
]),
Builder\Block::make('image')
->label('Изображение')
->icon('heroicon-o-photo')
->schema([
FileUpload::make('url')
->label('Изображение')
->image()
->multiple()
->reorderable()
->maxFiles(5)
->disk('public')
->directory('images')
->imageEditor()
->required()
->helperText('Можно загрузить до 5 изображений'),
TextInput::make('alt')
->label('Альтернативный текст')
->placeholder('Необязательно')
->helperText('Описание изображения для SEO'),
]),
Builder\Block::make('video')
->label('Видео')
->icon('heroicon-o-film')
->schema([
TextInput::make('mime')
->label('Тип видео')
->readOnly()
->helperText('Определяется автоматически'),
TextInput::make('title')
->label('Название видео')
->placeholder('Введите название видео')
->required()
->maxLength(255)
->autofocus()
->helperText('Это название будет отображаться перед видео'),
FileUpload::make('path')
->label('Видеофайл')
->required()
->acceptedFileTypes([
'video/mp4',
'video/quicktime',
'video/x-msvideo',
'video/x-ms-wmv',
'video/avi',
'video/webm',
'video/ogg',
'video/3gpp',
'video/3gpp2',
'video/x-m4v',
])
->disk('public')
->directory('videos')
->helperText('Поддерживаются популярные видеоформаты (MP4, MOV, AVI и др.)')
->afterStateUpdated(fn (callable $set, $state) => $set('mime', $state?->getMimeType())),
]),
Builder\Block::make('postsList')
->label('Список новостей')
->icon('heroicon-o-newspaper')
->schema([
Forms\Components\Grid::make(2)
->schema([
TextInput::make('count')
->label('Количество записей')
->integer()
->minValue(1)
->maxValue(20)
->default(5)
->helperText('От 1 до 20 записей'),
Select::make('category')
->label('Категория')
->options(Category::all()->pluck('title', 'id'))
->searchable()
->helperText('Выберите категорию или оставьте пустым для всех'),
]),
]),
Builder\Block::make('postItem')
->label('Конкретная новость')
->icon('heroicon-o-document-text')
->schema([
Select::make('post')
->label('Новость')
->options(Post::query()->where('status', PostStatus::PUBLISHED)->pluck('title', 'id'))
->searchable()
->required()
->helperText('Выберите опубликованную новость'),
]),
Builder\Block::make('pageItem')
->label('Конкретная страница')
->icon('heroicon-o-document')
->schema([
Select::make('page')
->label('Страница')
->options(Page::query()->where('title', '!=', null)->where('is_visible', true)->pluck('title', 'id'))
->searchable()
->required()
->helperText('Выберите видимую страницу'),
]),
Builder\Block::make('customForm')
->label('Пользовательская форма')
->icon('heroicon-o-clipboard-document-list')
->schema([
Select::make('form')
->label('Форма')
->options(CustomForm::query()->where('status', CustomFormStatus::PUBLISHED)->pluck('title', 'form_id'))
->searchable()
->required()
->helperText('Выберите опубликованную форму'),
]),
Builder\Block::make('pageResourceList')
->label('Ресурсы')
->icon('heroicon-o-archive-box')
->schema([
Select::make('resource')
->label('Ресурс')
->options(PageReferenceList::query()->where('is_active', true)->pluck('title', 'slug'))
->searchable()
->required()
->helperText('Выберите активный ресурс'),
]),
Builder\Block::make('contact')
->label('Контакты')
->icon('heroicon-o-phone')
->schema([
Select::make('contact')
->label('Виджет контактов')
->options(ContactWidget::query()->where('is_active', true)->pluck('title', 'slug'))
->searchable()
->required()
->helperText('Выберите активный виджет контактов'),
]),
Builder\Block::make('slider')
->label('Слайдер')
->icon('heroicon-o-presentation-chart-line')
->schema([
Select::make('slider')
->label('Слайдер')
->options(Slider::query()->whereHas('slides')->where('is_active', true)->pluck('title', 'slug'))
->searchable()
->required()
->helperText('Выберите активный слайдер с изображениями'),
]),
])
->collapsed()
->blockPickerColumns(3)
->blockPickerWidth('2xl')
->blockNumbers(false)
->collapsible()
->addActionLabel('Добавить блок в вкладку');
} }
@@ -4,6 +4,22 @@ namespace App\Filament\Components\Forms\ItemForm\Pages;
use App\Enums\CustomFormStatus; use App\Enums\CustomFormStatus;
use App\Enums\PostStatus; use App\Enums\PostStatus;
use App\Filament\Components\Forms\ItemForm\Blocks\ContactBlock;
use App\Filament\Components\Forms\ItemForm\Blocks\CustomFormBlock;
use App\Filament\Components\Forms\ItemForm\Blocks\FilesBlock;
use App\Filament\Components\Forms\ItemForm\Blocks\HeadingBlock;
use App\Filament\Components\Forms\ItemForm\Blocks\ImagesBlock;
use App\Filament\Components\Forms\ItemForm\Blocks\PageItemBlock;
use App\Filament\Components\Forms\ItemForm\Blocks\PageResourceListBlock;
use App\Filament\Components\Forms\ItemForm\Blocks\ParagraphBlock;
use App\Filament\Components\Forms\ItemForm\Blocks\PersonBlock;
use App\Filament\Components\Forms\ItemForm\Blocks\PostItemBlock;
use App\Filament\Components\Forms\ItemForm\Blocks\PostListBlock;
use App\Filament\Components\Forms\ItemForm\Blocks\SliderBlock;
use App\Filament\Components\Forms\ItemForm\Blocks\StepperBlock;
use App\Filament\Components\Forms\ItemForm\Blocks\TabBlock;
use App\Filament\Components\Forms\ItemForm\Blocks\VideoBlock;
use App\Filament\Components\Forms\ItemForm\Defaults\TabBuilderItem;
use App\Helpers\ByteConverter; use App\Helpers\ByteConverter;
use App\Models\Category; use App\Models\Category;
use App\Models\ContactWidget; use App\Models\ContactWidget;
@@ -14,6 +30,7 @@ use App\Models\Post;
use App\Models\Slider; use App\Models\Slider;
use Filament\Forms; use Filament\Forms;
use Filament\Forms\Components\Builder; use Filament\Forms\Components\Builder;
use Filament\Forms\Components\Fieldset;
use Filament\Forms\Components\FileUpload; use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Hidden; use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\RichEditor; use Filament\Forms\Components\RichEditor;
@@ -29,407 +46,99 @@ use Mohamedsabil83\FilamentFormsTinyeditor\Components\TinyEditor;
class ContentBuilderItem class ContentBuilderItem
{ {
private static function findSeoActive(array $data) : bool public static function getItem(string $name): Builder
{ {
$bool = false; return Builder::make($name)
->label('')
->blocks([
Builder\Block::make('heading')
->label('Заголовок')
->icon('heroicon-o-hashtag')
->schema(HeadingBlock::schema()),
foreach ($data as $item) {
if ($item['type'] !== 'paragraph') {
continue;
}
if ($item['data']['seo_active'] === true) {
$bool = true;
break;
}
}
return $bool;
}
public static function getItem(string $name)
{
return
Builder::make($name)->label('')->blocks([
Builder\Block::make('heading')->label('Заголовок')
->schema([
TextInput::make('id')->hidden()->integer()->default(rand(2335235,324634264263426)),
TextInput::make('content')
->label('')
->live(onBlur: true)
->afterStateUpdated(function (string $operation, string $state, Forms\Set $set, $get) {
}),
]),
Builder\Block::make('paragraph') Builder\Block::make('paragraph')
->schema([ ->label('Текст')
Toggle::make('seo_active')->label('Использовать блок как seo') ->icon('heroicon-o-document-text')
->live(onBlur: true) ->schema(ParagraphBlock::schema()),
->required()
->disabled(function ($state, Forms\Get $get) {
$data = $get('../../');
return self::findSeoActive($data) && !$state;
})
->dehydrated(),
TinyEditor::make('content')
->label('')
->profile('test')
->required(),
])->label('Текст'),
Builder\Block::make('files') Builder\Block::make('files')
->label('Файл(-ы)') ->label('Файлы')
->schema([ ->icon('heroicon-o-paper-clip')
Forms\Components\Repeater::make('file')->schema([ ->schema(FilesBlock::schema()),
Hidden::make('expansion')->required(),
Hidden::make('size')->required(),
TextInput::make('title')
->required()
->maxLength(255)
->autofocus(),
FileUpload::make('path')
->required()
->getUploadedFileNameForStorageUsing(
fn (TemporaryUploadedFile $file): string =>
str(Str::slug(pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME) . '-' . Carbon::now()->timestamp ) . '.' . $file->getClientOriginalExtension())
)
->acceptedFileTypes([
'application/pdf',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'application/zip'
])
->maxSize(512000)
->disk('public')
->directory('files')
->downloadable()
->afterStateUpdated(function ($set, $state) {
$set('expansion', $state?->getClientOriginalExtension());
$set('size', ByteConverter::bytesToHuman($state?->getSize()));
})
->visibility('public')
]),
]),
Builder\Block::make('person') Builder\Block::make('person')
->label('Персона') ->label('Персона')
->schema([ ->icon('heroicon-o-user')
TextInput::make('name') ->schema(PersonBlock::schema()),
->label('Имя')
->required()
->maxLength(255),
FileUpload::make('photo')
->label('Фотография')
->image()
->optimize('webp')
->resize(50)
->disk('public')
->directory('images')
->imageEditor(),
Forms\Components\Repeater::make('info')->schema([
TextInput::make('column')
->label('Название колонки')
->required()
->maxLength(255),
Forms\Components\Textarea::make('content')
->label('Содержание')
->required()
->maxLength(1000),
])->minItems(1)->label('Информация о персоне'),
]),
Builder\Block::make('stepper') Builder\Block::make('stepper')
->label('Строитель этапов') ->label('Этапы')
->schema([ ->icon('heroicon-o-list-bullet')
TextInput::make('step_name') ->schema(StepperBlock::schema()),
->label('Название шага')
->required()
->maxLength(255),
Forms\Components\Repeater::make('steps')->schema([
TextInput::make('title')
->required()
->maxLength(255)->columnSpanFull(),
RichEditor::make('content')->required(),
])->minItems(1),
]),
Builder\Block::make('tabs') Builder\Block::make('tabs')
->label('Вкладки') ->label('Вкладки')
->schema([ ->icon('heroicon-o-rectangle-stack')
Forms\Components\Repeater::make('tab')->schema([ ->schema(TabBlock::schema()),
TextInput::make('title')
->required()
->maxLength(255)->columnSpanFull(),
\Filament\Forms\Components\Builder::make('content')->label('')->blocks([
Builder\Block::make('heading')->label('Заголовок')
->schema([
TextInput::make('id')->hidden()->integer()->default(rand(2335235,324634264263426)),
TextInput::make('content')
->label('')
->live(onBlur: true)
->afterStateUpdated(function (string $operation, string $state, Forms\Set $set, $get) {
}),
]),
Builder\Block::make('paragraph')
->schema([
RichEditor::make('content')
->toolbarButtons([
'blockquote',
'bold',
'bulletList',
'italic',
'link',
'orderedList',
'redo',
'strike',
'underline',
'undo',
])
->label(''),
])->label('Текст'),
Builder\Block::make('files')
->schema([
Forms\Components\Repeater::make('file')->schema([
TextInput::make('title')
->required()
->maxLength(255)
->autofocus(),
FileUpload::make('path')
->required()
->acceptedFileTypes([
'application/pdf',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'application/zip'
])
->maxSize(512000)
->disk('public')
->directory('files')
->downloadable()
->visibility('public')
]),
]),
Builder\Block::make('person')
->schema([
TextInput::make('name')
->label('Имя')
->required()
->maxLength(255),
FileUpload::make('photo')
->label('Фотография')
->image()
->disk('public')
->directory('images')
->imageEditor(),
Forms\Components\Repeater::make('info')->schema([
Forms\Components\Grid::make(2)->schema([
TextInput::make('column')
->required()
->maxLength(255),
TextInput::make('content')
->required()
->maxLength(255),
]),
])->minItems(1),
]),
Builder\Block::make('stepper')
->schema([
TextInput::make('step_name')
->label('Название шага')
->required()
->maxLength(255),
Forms\Components\Repeater::make('steps')->schema([
TextInput::make('title')
->required()
->maxLength(255)->columnSpanFull(),
RichEditor::make('content')->required(),
])->minItems(1),
]),
Builder\Block::make('images')
->schema([
FileUpload::make('url')
->label('Изображение(-я)')
->image()
->multiple()
->reorderable()
->maxFiles(5)
->disk('public')
->directory('images')
->imageEditor()
->required(),
TextInput::make('alt')
->label('Описание')
->placeholder('Необязяательно')
])->label('Слайдер изображений'),
Builder\Block::make('image')
->schema([
FileUpload::make('url')
->label('Изображение(-я)')
->image()
->multiple()
->reorderable()
->maxFiles(5)
->disk('public')
->directory('images')
->imageEditor()
->required(),
TextInput::make('alt')
->label('Описание')
->placeholder('Необязяательно')
])->label('Изображение'),
Builder\Block::make('video')
->schema([
TextInput::make('mime')->readOnly(),
TextInput::make('title')
->required()
->maxLength(255)
->autofocus(),
FileUpload::make('path')
->required()
->acceptedFileTypes([
'video/mp4',
'video/quicktime',
'video/x-msvideo',
'video/x-ms-wmv',
'video/avi',
'video/webm',
'video/ogg',
'video/3gpp',
'video/3gpp2',
'video/x-m4v',
])
->disk('public')
->directory('videos')
->afterStateUpdated(fn (callable $set, $state) => $set('mime', $state?->getMimeType())),
]),
Builder\Block::make('postsList')
->schema([
Forms\Components\Grid::make(2)->schema([
TextInput::make('count')
->label('Количество запией')
->integer(),
Select::make('category')
->options(Category::all()->pluck('title', 'id'))
]),
])->label('Список новостей'),
])
->collapsed()
->blockNumbers(false)
->collapsible()
->addActionLabel('Добавить новый блок'),
])->minItems(1),
]),
Builder\Block::make('images') Builder\Block::make('images')
->schema([ ->label('Слайдер изображений')
FileUpload::make('url') ->icon('heroicon-o-photo')
->label('Изображение(-я)') ->schema(ImagesBlock::schema()),
->image()
->multiple()
->reorderable()
->maxFiles(5)
->disk('public')
->directory('images')
->imageEditor()
->required(),
TextInput::make('alt')
->label('Описание')
->placeholder('Необязяательно')
])->label('Слайдер изображений'),
Builder\Block::make('image') Builder\Block::make('image')
->schema([ ->label('Изображение')
FileUpload::make('url') ->icon('heroicon-o-photo')
->label('Изображение(-я)') ->schema(ImagesBlock::schema()),
->image()
->multiple()
->reorderable()
->maxFiles(5)
->disk('public')
->directory('images')
->imageEditor()
->required(),
TextInput::make('alt')
->label('Описание')
->placeholder('Необязяательно')
])->label('Изображение'),
Builder\Block::make('video') Builder\Block::make('video')
->label('Видео (Не стабильно)') ->label('Видео')
->schema([ ->icon('heroicon-o-film')
TextInput::make('mime')->readOnly(), ->schema(VideoBlock::schema()),
TextInput::make('title')
->required()
->maxLength(255)
->autofocus(),
FileUpload::make('path')
->required()
->acceptedFileTypes([
'video/mp4',
'video/quicktime',
'video/x-msvideo',
'video/x-ms-wmv',
'video/avi',
'video/webm',
'video/ogg',
'video/3gpp',
'video/3gpp2',
'video/x-m4v',
])
->disk('public')
->directory('videos')
->afterStateUpdated(fn (callable $set, $state) => $set('mime', $state?->getMimeType())),
]),
Builder\Block::make('postsList') Builder\Block::make('postsList')
->schema([ ->label('Список новостей')
Forms\Components\Grid::make(2)->schema([ ->icon('heroicon-o-newspaper')
TextInput::make('count') ->schema(PostListBlock::schema()),
->label('Количество запией')
->integer(),
Select::make('category')
->options(Category::all()->pluck('title', 'id'))
]),
])->label('Список новостей'),
Builder\Block::make('postItem') Builder\Block::make('postItem')
->schema([ ->label('Конкретная новость')
Select::make('post') ->icon('heroicon-o-document-text')
->options(Post::query()->where('status', PostStatus::PUBLISHED)->pluck('title', 'id')) ->schema(PostItemBlock::schema()),
->searchable()
->required(),
])->label('Новость'),
Builder\Block::make('pageItem') Builder\Block::make('pageItem')
->schema([ ->label('Конкретная страница')
Select::make('page') ->icon('heroicon-o-document')
->options(Page::query()->where('title', '!=' , null)->where('is_visible', true)->pluck('title', 'id')) ->schema(PageItemBlock::schema()),
->searchable()
->required(),
])->label('Страница'),
Builder\Block::make('customForm') Builder\Block::make('customForm')
->schema([ ->label('Пользовательская форма')
Select::make('form') ->icon('heroicon-o-clipboard-document-list')
->options(CustomForm::query()->where('status', CustomFormStatus::PUBLISHED)->pluck('title', 'form_id')) ->schema(CustomFormBlock::schema()),
->searchable()
->required(),
])->label('Форма'),
Builder\Block::make('pageResourceList') Builder\Block::make('pageResourceList')
->schema([ ->label('Ресурсы')
Select::make('resource') ->icon('heroicon-o-archive-box')
->options(PageReferenceList::query()->where('is_active', true)->pluck('title', 'slug')) ->schema(PageResourceListBlock::schema()),
->searchable()
->required(),
])->label('Ресурсы'),
Builder\Block::make('contact') Builder\Block::make('contact')
->schema([ ->label('Контакты')
Select::make('contact') ->icon('heroicon-o-phone')
->options(ContactWidget::query()->where('is_active', true)->pluck('title', 'slug')) ->schema(ContactBlock::schema()),
->searchable()
->required(),
])->label('Контакты'),
Builder\Block::make('slider') Builder\Block::make('slider')
->schema([ ->label('Слайдер')
Select::make('slider') ->icon('heroicon-o-presentation-chart-line')
->options(Slider::query()->whereHas('slides')->where('is_active', true)->pluck('title', 'slug')) ->schema(SliderBlock::schema()),
->searchable()
->required(),
])->label('Слайдеры'),
]) ])
->collapsed() ->collapsed()
->blockNumbers(false) ->blockNumbers(false)
->collapsible() ->collapsible()
->blockPickerColumns(3) ->blockPickerColumns(3)
->blockPickerWidth('2xl') ->blockPickerWidth('2xl')
->addActionLabel('Добавить новый блок'); ->addActionLabel('Добавить новый блок')
->cloneable()
->reorderableWithButtons();
} }
+122 -61
View File
@@ -6,6 +6,7 @@ use App\Enums\CustomFormStatus;
use App\Enums\PostStatus; use App\Enums\PostStatus;
use App\Filament\Components\Forms\ItemForm\Pages\ContentBuilderItem; use App\Filament\Components\Forms\ItemForm\Pages\ContentBuilderItem;
use Filament\Forms; use Filament\Forms;
use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Components\Section; use Filament\Forms\Components\Section;
use Filament\Forms\Components\Select; use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
@@ -22,69 +23,129 @@ class PageForm
->schema([ ->schema([
Section::make() Section::make()
->schema([ ->schema([
Forms\Components\Tabs::make('')->schema([ Forms\Components\Tabs::make('Настройки страницы')
Forms\Components\Tabs\Tab::make('Основная информация')->schema([ ->persistTabInQueryString()
Forms\Components\Grid::make(2)->schema([ ->columnSpanFull()
TextInput::make('title')->label('Заголовок')->required() ->tabs([
->live(onBlur: true) Forms\Components\Tabs\Tab::make('Основная информация')
->afterStateUpdated(function (string $operation, string|null $state, Forms\Set $set) { ->icon('heroicon-o-information-circle')
$set('slug', Str::slug($state)); ->schema([
$set('path', Str::slug($state)); Forms\Components\Grid::make(2)
}), ->schema([
TextInput::make('slug')->label('Текстовый идентификатор страницы')->unique(ignoreRecord: true)->required() TextInput::make('title')
->live(onBlur: true) ->label('Заголовок страницы')
->afterStateUpdated(function (string $operation, string $state, Forms\Set $set, $get) { ->required()
$set('path', Str::slug($state)); ->maxLength(255)
}), ->placeholder('Введите название страницы')
]), ->helperText('Этот заголовок будет отображаться в заголовке страницы и в навигации')
Select::make('sub_section_id')->label('Подраздел') ->live(onBlur: true)
->relationship('section', 'title') ->afterStateUpdated(function (string $operation, string|null $state, Forms\Set $set) {
->createOptionForm([ $set('slug', Str::slug($state));
Forms\Components\TextInput::make('title')->label('Название подраздела')->required() $set('path', Str::slug($state));
->live(onBlur: true) }),
->afterStateUpdated(function (string $operation, string $state, Forms\Set $set) { TextInput::make('slug')
$set('slug', Str::slug($state)); ->label('URL-адрес страницы')
}), ->required()
TextInput::make('slug')->label('Slug')->unique(ignoreRecord: true)->readOnly()->required(), ->unique(ignoreRecord: true)
->maxLength(255)
->helperText('Человеко-понятный URL для страницы')
->placeholder('example-page')
// ->prefix(fn ($record) => url('/') . '/' . substr($record->path, 0, strrpos($record->path, '/')))
->live(onBlur: true)
->afterStateUpdated(function (string $operation, string $state, Forms\Set $set, $get) {
$set('path', Str::slug($state));
})
->suffixAction(
Action::make('copy')
->icon('heroicon-s-clipboard-document-check')
->action(function ($livewire, $state, $record) {
$livewire->js(
'window.navigator.clipboard.writeText("'. url('/') . '/' . substr($record->path, 0, strrpos($record->path, '/')) . '/' . $state.'");
$tooltip("'.__('Copied to clipboard').'", { timeout: 1500 });'
);
})),
]),
Select::make('sub_section_id')
->label('Родительский подраздел')
->relationship('section', 'title')
->preload()
->searchable()
->placeholder('Выберите подраздел')
->helperText('Выберите раздел, к которому принадлежит эта страница')
->createOptionForm([
Forms\Components\Grid::make(2)
->schema([
Forms\Components\TextInput::make('title')
->label('Название подраздела')
->required()
->maxLength(255)
->placeholder('Введите название подраздела')
->live(onBlur: true)
->afterStateUpdated(function (string $operation, string $state, Forms\Set $set) {
$set('slug', Str::slug($state));
}),
TextInput::make('slug')
->label('URL подраздела')
->unique(ignoreRecord: true)
->readOnly()
->required()
->maxLength(255)
->helperText('Автоматически генерируется из названия'),
]),
]),
Select::make('code')
->label('HTTP статус страницы')
->options([
'200' => 'Обычная страница (200 OK)',
'404' => 'Страница не найдена (404 Not Found)',
'500' => 'Технические работы (500 Server Error)',
])
->required()
->default('200')
->helperText('Выберите HTTP статус, с которым будет отдаваться страница'),
Toggle::make('searchable')
->label('Индексировать в поиске')
->default(true)
->inline(false)
->helperText('Разрешить локальному поиску индексировать страницу'),
IconPicker::make('icon')
->label('Иконка страницы')
->default('heroicon-o-academic-cap')
->helperText('Выберите иконку для отображения в навигации')
->columns(6),
TextInput::make('search_data')
->hidden(),
]),
Forms\Components\Tabs\Tab::make('Содержание')
->icon('heroicon-o-document-text')
->schema([
ContentBuilderItem::getItem('content')
->helperText('Создайте содержимое страницы используя конструктор')
]),
Forms\Components\Tabs\Tab::make('Дополнительные настройки')
->icon('heroicon-o-cog')
->schema([
Section::make('Отображение элементов')
->description('Управление видимостью элементов на странице')
->collapsible()
->schema([
Toggle::make('settings.hide_page_sub_section_links')
->label('Скрыть боковую панель с ссылками на страницы раздела')
->helperText('Скрывает список страниц текущего раздела')
->columnSpan(1),
Toggle::make('settings.hide_page_navigate_links')
->label('Скрыть навигацию по странице')
->helperText('Скрывает навигацию по заголовкам')
->columnSpan(1),
Toggle::make('settings.hide_breadcrumbs')
->label('Скрыть хлебные крошки')
->helperText('Скрывает навигационную цепочку вверху страницы')
->columnSpan(1),
])
->columns(2),
]), ]),
Select::make('code')->options([
'200' => 'Открытая страница',
'404' => 'Не найдено',
'500' => 'Ведутся технические работы',
])->label('Статус')->required()->default('200'),
Toggle::make('searchable')->default(true)->label('Индексируется поиском')->inline(false),
IconPicker::make('icon')
->default('heroicon-o-academic-cap')
->label('Icon'),
TextInput::make('search_data')->hidden(),
]), ]),
Forms\Components\Tabs\Tab::make('Контент')->schema([
ContentBuilderItem::getItem('content')
]),
Forms\Components\Tabs\Tab::make('Настройки')->schema([
Section::make()->schema([
Toggle::make('settings.hide_page_sub_section_links')
->label('Скрыть сайдбар смежных страниц')
->columnSpan(1),
Toggle::make('settings.hide_page_navigate_links')
->label('Скрыть навигацию по страницу')
->columnSpan(1),
Toggle::make('settings.hide_breadcrumbs')
->label('Скрыть хлебные крошки')
->columnSpan(1),
//
// Toggle::make('settings.full_width_page')
// ->label('Страница на всю ширину')
// ->columnSpan(1)->default(true),
]),
]),
]),
]) ])
]); ]);
} }
+210 -102
View File
@@ -11,6 +11,7 @@ use App\Models\CustomForm;
use App\Models\Page; use App\Models\Page;
use App\Models\PageReferenceList; use App\Models\PageReferenceList;
use App\Models\Post; use App\Models\Post;
use App\Models\Slider;
use Filament\Forms; use Filament\Forms;
use Filament\Forms\Components\Builder; use Filament\Forms\Components\Builder;
use Filament\Forms\Components\ColorPicker; use Filament\Forms\Components\ColorPicker;
@@ -38,8 +39,6 @@ use Symfony\Component\Finder\Finder;
class PostForm class PostForm
{ {
public static function getForm(Form $form): Form public static function getForm(Form $form): Form
{ {
return $form return $form
@@ -49,155 +48,264 @@ class PostForm
Tabs::make('Tabs') Tabs::make('Tabs')
->tabs([ ->tabs([
Tabs\Tab::make('Основная информация') Tabs\Tab::make('Основная информация')
->icon('heroicon-o-information-circle')
->schema([ ->schema([
Forms\Components\Grid::make(2)->schema([ Grid::make(2)
TextInput::make('title')->label('Заголовок')->required() ->schema([
->live(onBlur: true) TextInput::make('title')
->afterStateUpdated(function (string $operation, string|null $state, Forms\Set $set) { ->label('Заголовок')
$set('slug', Str::slug($state)); ->required()
$set('seo.title', $state); ->maxLength(255)
}), ->placeholder('Введите заголовок новости')
TextInput::make('slug')->label('Slug')->unique(ignoreRecord: true)->readOnly()->required(), ->helperText('Этот заголовок будет отображаться на сайте')
]), ->live(onBlur: true)
Select::make('status')->options(PostStatus::class) ->afterStateUpdated(function (string $operation, string|null $state, Forms\Set $set) {
->label('Статус')->required() $set('slug', Str::slug($state));
$set('seo.title', $state);
}),
TextInput::make('slug')
->label('URL-адрес')
->unique(ignoreRecord: true)
->readOnly()
->required()
->helperText('Этот URL будет использоваться для страницы новости')
->maxLength(255),
]),
Select::make('status')
->label('Статус публикации')
->options(PostStatus::class)
->required()
->default(PostStatus::VERIFICATION)
->helperText('Выберите статус публикации новости')
->disableOptionWhen(fn (string $value): bool => ->disableOptionWhen(fn (string $value): bool =>
$value == PostStatus::PUBLISHED->value && !auth()->user()->can('publish_post') $value == PostStatus::PUBLISHED->value && !auth()->user()->can('publish_post')
) ),
->default(PostStatus::VERIFICATION),
Select::make('category_id') Select::make('category_id')
->label('Категория')
->options(Category::all()->pluck('title', 'id')) ->options(Category::all()->pluck('title', 'id'))
->searchable()
->preload() ->preload()
->label('Категория'), ->placeholder('Выберите категорию')
SpatieTagsInput::make('tags')->label('Тэги'), ->helperText('Выберите категорию для новости'),
SpatieTagsInput::make('tags')
->label('Теги')
->placeholder('Добавьте теги')
->helperText('Добавьте теги для лучшей классификации'),
Forms\Components\TagsInput::make('authors') Forms\Components\TagsInput::make('authors')
->label('Авторы')->placeholder('Добавить автора'), ->label('Авторы')
Section::make('Отложенная публикация')->schema([ ->placeholder('Добавить автора')
Grid::make(2)->schema([ ->helperText('Укажите авторов новости')
Toggle::make('publish_setting.publish_after') ->suggestions([
->label('Включить') 'Редакция',
->inline(false) 'Администратор',
->default(false)
->live(),
DateTimePicker::make('publish_setting.publish_at')
->label('Дата публикации')
->native()
->displayFormat('d/m/Y')
->required(fn (Forms\Get $get) => $get('publish_setting.publish_after'))
->disabled(fn (Forms\Get $get) => !$get('publish_setting.publish_after'))
->minDate(Carbon::now()->subWeek())
->maxDate(Carbon::now()->addMonth()),
]), ]),
]), Section::make('Отложенная публикация')
Section::make('Публикация в сервисах')->schema([ ->description('Настройте автоматическую публикацию новости в указанное время')
Forms\Components\Grid::make()->schema([ ->collapsible()
Toggle::make('publication.vk')->label('Публикация в VK')->default(true), ->schema([
Toggle::make('publication.telegram')->label('Публикация в Telegram')->default(true), Grid::make(2)
->schema([
Toggle::make('publish_setting.publish_after')
->label('Включить отложенную публикацию')
->inline(false)
->default(false)
->live()
->helperText('Активируйте для публикации в указанное время'),
DateTimePicker::make('publish_setting.publish_at')
->label('Дата и время публикации')
->native(false)
->displayFormat('d/m/Y H:i')
->seconds(false)
->minutesStep(15)
->helperText('Выберите дату и время публикации')
->required(fn (Forms\Get $get) => $get('publish_setting.publish_after'))
->disabled(fn (Forms\Get $get) => !$get('publish_setting.publish_after'))
->minDate(now())
->maxDate(now()->addMonth()),
]),
]),
Section::make('Публикация в соцсетях')
->description('Управление автоматической публикацией в социальных сетях')
->collapsible()
->schema([
Grid::make()
->schema([
Toggle::make('publication.vk')
->label('Опубликовать в VK')
->default(true)
->helperText('Новость будет автоматически опубликована в VK'),
Toggle::make('publication.telegram')
->label('Опубликовать в Telegram')
->default(true)
->helperText('Новость будет автоматически опубликована в Telegram'),
]),
]), ]),
]),
]), ]),
Tabs\Tab::make('Содержание новости') Tabs\Tab::make('Содержание')
->icon('heroicon-o-document-text')
->schema([ ->schema([
ContentBuilderItem::getItem('content')->required(), ContentBuilderItem::getItem('content')
->required()
->helperText('Создайте содержимое новости используя конструктор'),
]), ]),
Tabs\Tab::make('Изображения') Tabs\Tab::make('Медиа')
->icon('heroicon-o-photo')
->schema([ ->schema([
FileUpload::make('preview')->label('Превью новости') FileUpload::make('preview')
->label('Главное изображение')
->image() ->image()
->directory('posts/previews')
->optimize('webp') ->optimize('webp')
->resize(50) ->resize(50)
->imageEditor() ->imageEditor()
->directory('images'), ->helperText('Загрузите главное изображение для новости')
FileUpload::make('images')->label('Альбом') ->maxSize(2048)
->acceptedFileTypes(['image/jpeg', 'image/png', 'image/webp'])
->imagePreviewHeight('150')
->panelLayout('integrated'),
FileUpload::make('images')
->label('Галерея изображений')
->image() ->image()
->directory('posts/gallery')
->optimize('jpg') ->optimize('jpg')
->resize(30) ->resize(30)
->imageEditor() ->imageEditor()
->panelLayout('grid')
->reorderable()
->imageEditor()
->multiple() ->multiple()
->directory('images'), ->reorderable()
->panelLayout('grid')
->helperText('Загрузите дополнительные изображения для галереи')
->maxFiles(10)
->maxSize(2048)
->acceptedFileTypes(['image/jpeg', 'image/png'])
->imagePreviewHeight('150'),
]), ]),
Tabs\Tab::make('Добавление новости в слайдер') Tabs\Tab::make('Слайдер')
->icon('heroicon-o-view-columns')
->schema([ ->schema([
Toggle::make('is_slider_enabled') Toggle::make('is_slider_enabled')
->label('Добавить новый слайд') ->label('Добавить в слайдер')
->live() ->live()
->hidden(fn (string $context): bool => $context === 'edit') ->helperText('Активируйте для добавления новости в слайдер')
->hidden(function (Forms\Get $get, string $context) {
if ($context === 'edit') {
return true;
}
return false;
})
->dehydrated(false) ->dehydrated(false)
->default(false), ->default(false),
Section::make() Section::make('Настройки слайда')
->description('Настройте отображение новости в слайдере')
->collapsible()
->collapsed()
->schema([ ->schema([
Forms\Components\Section::make('Информация слайда')->schema([ Select::make('slide.slider_id')
Forms\Components\TextInput::make('slide.title') ->label('Выберите слайдер')
->label('Заголовок слайда'), ->options(Slider::where('is_active', true)->pluck('title', 'id'))
Forms\Components\Textarea::make('slide.content') ->required()
->label('Текст слайда'), ->helperText('Выберите слайдер для размещения'),
Forms\Components\Grid::make()->schema([ TextInput::make('slide.title')
->label('Заголовок слайда')
->maxLength(100)
->helperText('Короткий заголовок для слайда')
->placeholder('Введите заголовок'),
Forms\Components\Textarea::make('slide.content')
->label('Текст слайда')
->maxLength(255)
->helperText('Краткое описание для слайда')
->placeholder('Введите текст слайда'),
Grid::make()
->schema([
ColorPicker::make('slide.color_theme') ColorPicker::make('slide.color_theme')
->label('Цвет текста') ->label('Цвет текста')
->default('#ffffff') ->default('#ffffff')
->required(), ->required()
Forms\Components\ToggleButtons::make('slide.settings.text_position') ->helperText('Выберите цвет текста на слайде'),
ToggleButtons::make('slide.settings.text_position')
->label('Позиция текста')
->options([ ->options([
'left' => 'Текст слева', 'left' => 'Слева',
'center' => 'Текст по середине', 'center' => 'По центру',
'right' => 'Текст справа' 'right' => 'Справа',
]) ])
->inline()->default('left')->grouped() ->inline()
->label('Позиция текста на слайде'), ->grouped()
->default('left')
->helperText('Выберите расположение текста на слайде'),
]), ]),
Forms\Components\Grid::make()->schema([ Grid::make()
->schema([
Toggle::make('active_button') Toggle::make('active_button')
->label('Использовать кнопку для ссылки (Ссылка будет открываться при нажатии на слайд)') ->label('Добавить кнопку')
->inline(false) ->inline(false)
->live() ->live()
->helperText('Добавить кнопку со ссылкой на новость')
->afterStateHydrated(function (Toggle $component, $state, $get) { ->afterStateHydrated(function (Toggle $component, $state, $get) {
$component->state(true); $component->state(true);
}) })
->dehydrated(false), ->dehydrated(false),
Forms\Components\TextInput::make('slide.settings.link_text') TextInput::make('slide.settings.link_text')
->default('Читать')
->label('Текст кнопки') ->label('Текст кнопки')
->default('Читать')
->maxLength(20)
->disabled(fn (Forms\Get $get) => !$get('active_button')) ->disabled(fn (Forms\Get $get) => !$get('active_button'))
->helperText('Текст для кнопки перехода'),
]), ]),
]), Section::make('Изображение слайда')
Forms\Components\Section::make('Изображение')->schema([ ->schema([
FileUpload::make('slide.image.url') FileUpload::make('slide.image.url')
->label('Изображение') ->label('Фоновое изображение')
->image() ->image()
->optimize('webp') ->directory('sliders')
->resize(50) ->optimize('webp')
->disk('public') ->resize(50)
->directory('images') ->imageEditor()
->imageEditor() ->required()
->required(), ->maxSize(2048)
ToggleButtons::make('slide.image.shading')->inline()->grouped()->label('Уровень затемнения изображения')->options([ ->helperText('Загрузите фоновое изображение для слайда')
'1' => 'Без затемнения', ->acceptedFileTypes(['image/jpeg', 'image/png', 'image/webp']),
'0.7' => 'Слабое затемнение', ToggleButtons::make('slide.image.shading')
'0.5' => 'Среднее затемнение', ->label('Затемнение фона')
'0.3' => 'Сильное затемнение', ->inline()
->grouped()
->options([
'1' => 'Нет',
'0.7' => 'Слабое',
'0.5' => 'Среднее',
'0.3' => 'Сильное',
])
->helperText('Выберите уровень затемнения фона'),
]), ]),
]), Section::make('Время показа')
Forms\Components\Section::make('Общая часть')->schema([ ->schema([
Forms\Components\Grid::make()->schema([
DateTimePicker::make('slide.end_time') DateTimePicker::make('slide.end_time')
->label('Слайд действует до') ->label('Дата окончания показа')
->native() ->native(false)
->displayFormat('d/m/Y') ->displayFormat('d/m/Y H:i')
->minDate(Carbon::now()) ->minDate(now())
->maxDate(Carbon::now()->addMonth()), ->maxDate(now()->addMonth())
->helperText('Укажите до какого времени слайд будет активен'),
]), ]),
]),
]) ])
->hidden(fn(Forms\Get $get) => !$get('is_slider_enabled')) ->hidden(function (Forms\Get $get) {
if ($get('is_slider_enabled') === true) {
return false;
]), }
]), if ($get('slide')['slider_id'] !== null) {
]) return false;
}
return true;
}),
])
->hidden(function (Forms\Get $get, string $context) {
if ($context === 'edit' && $get('slide')['slider_id'] === null) {
return true;
}
return false;
}),
])
->persistTabInQueryString(),
]),
]); ]);
} }
} }
+1 -1
View File
@@ -24,7 +24,7 @@ class Backups extends BaseBackups
public static function getNavigationGroup(): ?string public static function getNavigationGroup(): ?string
{ {
return 'Settings'; return 'Настройки приложения';
} }
public static function getNavigationLabel(): string public static function getNavigationLabel(): string
@@ -15,6 +15,7 @@ class CheckpointSettingsPage extends SettingsPage
{ {
protected static ?string $slug = 'checkpoint/settings'; protected static ?string $slug = 'checkpoint/settings';
protected static ?string $navigationIcon = 'heroicon-o-adjustments-horizontal'; protected static ?string $navigationIcon = 'heroicon-o-adjustments-horizontal';
protected static string $settings = CheckpointSettings::class; protected static string $settings = CheckpointSettings::class;
@@ -36,7 +37,7 @@ class CheckpointSettingsPage extends SettingsPage
public static function getNavigationGroup(): ?string public static function getNavigationGroup(): ?string
{ {
return 'Settings'; // Группа навигации return 'Настройки приложения'; // Группа навигации
} }
public function form(Form $form): Form public function form(Form $form): Form
+146 -759
View File
@@ -4,6 +4,7 @@ namespace App\Filament\Resources;
use App\Enums\CustomFormStatus; use App\Enums\CustomFormStatus;
use App\Enums\PostStatus; use App\Enums\PostStatus;
use App\Filament\Components\Forms\ItemForm\Pages\ContentBuilderItem;
use App\Filament\Resources\AcademicJournalResource\Pages; use App\Filament\Resources\AcademicJournalResource\Pages;
use App\Filament\Resources\AcademicJournalResource\RelationManagers; use App\Filament\Resources\AcademicJournalResource\RelationManagers;
use App\Filament\Resources\AcademicJournalResource\RelationManagers\JournalsRelationManager; use App\Filament\Resources\AcademicJournalResource\RelationManagers\JournalsRelationManager;
@@ -14,10 +15,13 @@ use App\Models\CustomForm;
use App\Models\Page; use App\Models\Page;
use App\Models\Post; use App\Models\Post;
use Filament\Forms; use Filament\Forms;
use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Components\Builder; use Filament\Forms\Components\Builder;
use Filament\Forms\Components\FileUpload; use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Grid;
use Filament\Forms\Components\Hidden; use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\RichEditor; use Filament\Forms\Components\RichEditor;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\Select; use Filament\Forms\Components\Select;
use Filament\Forms\Components\Tabs; use Filament\Forms\Components\Tabs;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
@@ -33,780 +37,141 @@ use Mohamedsabil83\FilamentFormsTinyeditor\Components\TinyEditor;
class AcademicJournalResource extends Resource class AcademicJournalResource extends Resource
{ {
protected static ?string $navigationGroup = 'Наука'; protected static ?string $navigationGroup = 'Наука';
public static ?string $label = 'Журнал'; public static ?string $label = 'Журнал';
protected static ?string $pluralLabel = 'Научные журналы'; protected static ?string $pluralLabel = 'Научные журналы';
protected static ?string $model = AcademicJournal::class; protected static ?string $model = AcademicJournal::class;
protected static ?string $navigationIcon = 'heroicon-o-beaker'; protected static ?string $navigationIcon = 'heroicon-o-beaker';
public static function form(Form $form): Form public static function form(Form $form): Form
{ {
return $form return $form
->schema([ ->schema([
Forms\Components\Section::make()->schema([ Section::make('Основные данные')
Forms\Components\Grid::make(2)->schema([ ->description('Основная информация о научном журнале')
TextInput::make('title')->label('Заголовок')->required() ->schema([
->live(onBlur: true) Grid::make(2)
->afterStateUpdated(function (string $operation, string|null $state, Forms\Set $set) { ->schema([
$set('slug', Str::slug($state)); TextInput::make('title')
}), ->label('Название журнала')
TextInput::make('slug')->label('Slug')->unique(ignoreRecord: true)->readOnly()->required(), ->required()
->maxLength(255)
->placeholder('Введите полное название журнала')
->helperText('Официальное название журнала как в регистрационных документах')
->live(onBlur: true)
->afterStateUpdated(function (string $operation, string|null $state, Forms\Set $set) {
$set('slug', Str::slug($state));
}),
TextInput::make('slug')
->label('URL-адрес')
->unique(ignoreRecord: true)
->required()
->readOnly()
->helperText('Формируется автоматически из названия')
->prefix(fn () => route('client.academicJournals.index') . '/')
->suffixAction(
Action::make('copy')
->icon('heroicon-s-clipboard-document-check')
->action(function ($livewire, $state) {
$livewire->js(
'window.navigator.clipboard.writeText("'. route('client.academicJournals.index') . '/' . $state.'");
$tooltip("'.__('Copied to clipboard').'", { timeout: 1500 });'
);
})),
]),
]), ]),
]),
Tabs::make('Tabs') Tabs::make('Настройки журнала')
->persistTabInQueryString()
->columnSpanFull()
->tabs([ ->tabs([
Tabs\Tab::make('Основная информация журнала') Tabs\Tab::make('Основная информация')
->icon('heroicon-o-information-circle')
->schema([ ->schema([
Builder::make('main_info')->label('')->blocks([ ContentBuilderItem::getItem('main_info')
Builder\Block::make('heading')->label('Заголовок') ->label('Описание журнала')
->schema([ ->helperText('Добавьте полное описание журнала, его историю и основные направления'),
TextInput::make('id')->hidden()->integer()->default(rand(2335235,324634264263426)), ]),
TextInput::make('content')
->label('') Tabs\Tab::make('Редакционная коллегия')
->live(onBlur: true) ->icon('heroicon-o-user-group')
->afterStateUpdated(function (string $operation, string $state, Forms\Set $set, $get) { ->schema([
}), Section::make('Главный редактор')
]), ->description('Информация о главном редакторе журнала')
Builder\Block::make('paragraph') ->collapsible()
->schema([ ->schema([
RichEditor::make('content') Forms\Components\Repeater::make('chief_editor')->label('')
->toolbarButtons([ ->schema([
'blockquote', TextInput::make('name')
'bold', ->label('ФИО')
'bulletList', ->required()
'italic', ->maxLength(100)
'link', ->placeholder('Иванов Иван Иванович'),
'orderedList', TextInput::make('academicTitle')
'redo', ->label('Учёная степень')
'strike', ->required()
'underline', ->maxLength(50)
'undo', ->placeholder('д.т.н., профессор'),
]) TextInput::make('position')
->label(''), ->label('Должность')
])->label('Текст'), ->required()
Builder\Block::make('files') ->maxLength(100)
->schema([ ->placeholder('Главный научный сотрудник'),
Forms\Components\Repeater::make('file')->schema([ TextInput::make('institution')
Hidden::make('expansion')->required(), ->label('Учреждение')
Hidden::make('size')->required(),
TextInput::make('title')
->required() ->required()
->maxLength(255) ->maxLength(255)
->autofocus(), ->placeholder('МГУ имени М.В. Ломоносова'),
FileUpload::make('path') ])
->required() ->maxItems(1)
->getUploadedFileNameForStorageUsing( ->reorderable(false)
fn (TemporaryUploadedFile $file): string => ->helperText('Укажите данные главного редактора журнала'),
str(Str::slug(pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME) . '-' . Carbon::now()->timestamp ) . '.' . $file->getClientOriginalExtension()) ]),
)
->acceptedFileTypes([
'application/pdf',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'application/zip'
])
->maxSize(512000)
->disk('public')
->directory('files')
->downloadable()
->afterStateUpdated(function ($set, $state) {
$set('expansion', $state?->getClientOriginalExtension());
$set('size', ByteConverter::bytesToHuman($state?->getSize()));
})
->visibility('public')
]),
]),
Builder\Block::make('person')
->schema([
TextInput::make('name')
->label('Имя')
->required()
->maxLength(255),
FileUpload::make('photo')
->label('Фотография')
->image()
->disk('public')
->directory('images')
->imageEditor(),
Forms\Components\Repeater::make('info')->schema([
Forms\Components\Grid::make(2)->schema([
TextInput::make('column')
->required()
->maxLength(255),
TextInput::make('content')
->required()
->maxLength(255),
]),
])->minItems(1),
]),
Builder\Block::make('stepper')
->schema([
TextInput::make('step_name')
->label('Название шага')
->required()
->maxLength(255),
Forms\Components\Repeater::make('steps')->schema([
TextInput::make('title')
->required()
->maxLength(255)->columnSpanFull(),
RichEditor::make('content')->required(),
])->minItems(1),
]),
Builder\Block::make('tabs')
->schema([
Forms\Components\Repeater::make('tab')->schema([
TextInput::make('title')
->required()
->maxLength(255)->columnSpanFull(),
\Filament\Forms\Components\Builder::make('content')->label('')->blocks([
Builder\Block::make('heading')->label('Заголовок')
->schema([
TextInput::make('id')->hidden()->integer()->default(rand(2335235,324634264263426)),
TextInput::make('content')
->label('')
->live(onBlur: true)
->afterStateUpdated(function (string $operation, string $state, Forms\Set $set, $get) {
}),
]),
Builder\Block::make('paragraph')
->schema([
RichEditor::make('content')
->toolbarButtons([
'blockquote',
'bold',
'bulletList',
'italic',
'link',
'orderedList',
'redo',
'strike',
'underline',
'undo',
])
->label(''),
])->label('Текст'),
Builder\Block::make('files')
->schema([
Forms\Components\Repeater::make('file')->schema([
TextInput::make('title')
->required()
->maxLength(255)
->autofocus(),
FileUpload::make('path')
->required()
->acceptedFileTypes([
'application/pdf',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'application/zip'
])
->maxSize(512000)
->disk('public')
->directory('files')
->downloadable()
->visibility('public')
]),
]),
Builder\Block::make('person')
->schema([
TextInput::make('name')
->label('Имя')
->required()
->maxLength(255),
FileUpload::make('photo')
->label('Фотография')
->image()
->disk('public')
->directory('images')
->imageEditor(),
Forms\Components\Repeater::make('info')->schema([
Forms\Components\Grid::make(2)->schema([
TextInput::make('column')
->required()
->maxLength(255),
TextInput::make('content')
->required()
->maxLength(255),
]),
])->minItems(1),
]),
Builder\Block::make('stepper')
->schema([
TextInput::make('step_name')
->label('Название шага')
->required()
->maxLength(255),
Forms\Components\Repeater::make('steps')->schema([
TextInput::make('title')
->required()
->maxLength(255)->columnSpanFull(),
RichEditor::make('content')->required(),
])->minItems(1),
]),
Builder\Block::make('images')
->schema([
FileUpload::make('url')
->label('Изображение(-я)')
->image()
->multiple()
->reorderable()
->maxFiles(5)
->disk('public')
->directory('images')
->imageEditor()
->required(),
TextInput::make('alt')
->label('Описание')
->placeholder('Необязяательно')
])->label('Слайдер изображений'),
Builder\Block::make('image')
->schema([
FileUpload::make('url')
->label('Изображение(-я)')
->image()
->multiple()
->reorderable()
->maxFiles(5)
->disk('public')
->directory('images')
->imageEditor()
->required(),
TextInput::make('alt')
->label('Описание')
->placeholder('Необязяательно')
])->label('Изображение'),
Builder\Block::make('video')
->schema([
TextInput::make('mime')->readOnly(),
TextInput::make('title')
->required()
->maxLength(255)
->autofocus(),
FileUpload::make('path')
->required()
->acceptedFileTypes([
'video/mp4',
'video/quicktime',
'video/x-msvideo',
'video/x-ms-wmv',
'video/avi',
'video/webm',
'video/ogg',
'video/3gpp',
'video/3gpp2',
'video/x-m4v',
])
->disk('public')
->directory('videos')
->afterStateUpdated(fn (callable $set, $state) => $set('mime', $state?->getMimeType())),
]),
Builder\Block::make('postsList')
->schema([
Forms\Components\Grid::make(2)->schema([
TextInput::make('count')
->label('Количество запией')
->integer(),
Select::make('category')
->options(Category::all()->pluck('title', 'id'))
]),
])->label('Список новостей'),
])
->collapsed()
->blockNumbers(false)
->collapsible()
->addActionLabel('Добавить новый блок'),
])->minItems(1),
]),
Builder\Block::make('images')
->schema([
FileUpload::make('url')
->label('Изображение(-я)')
->image()
->multiple()
->reorderable()
->maxFiles(5)
->disk('public')
->directory('images')
->imageEditor()
->required(),
TextInput::make('alt')
->label('Описание')
->placeholder('Необязяательно')
])->label('Слайдер изображений'),
Builder\Block::make('image')
->schema([
FileUpload::make('url')
->label('Изображение(-я)')
->image()
->multiple()
->reorderable()
->maxFiles(5)
->disk('public')
->directory('images')
->imageEditor()
->required(),
TextInput::make('alt')
->label('Описание')
->placeholder('Необязяательно')
])->label('Изображение'),
Builder\Block::make('video')
->schema([
TextInput::make('mime')->readOnly(),
TextInput::make('title')
->required()
->maxLength(255)
->autofocus(),
FileUpload::make('path')
->required()
->acceptedFileTypes([
'video/mp4',
'video/quicktime',
'video/x-msvideo',
'video/x-ms-wmv',
'video/avi',
'video/webm',
'video/ogg',
'video/3gpp',
'video/3gpp2',
'video/x-m4v',
])
->disk('public')
->directory('videos')
->afterStateUpdated(fn (callable $set, $state) => $set('mime', $state?->getMimeType())),
]),
Builder\Block::make('postsList')
->schema([
Forms\Components\Grid::make(2)->schema([
TextInput::make('count')
->label('Количество запией')
->integer(),
Select::make('category')
->options(Category::all()->pluck('title', 'id'))
]),
])->label('Список новостей'),
Builder\Block::make('postItem')
->schema([
Select::make('post')
->options(Post::query()->where('status', PostStatus::PUBLISHED)->pluck('title', 'id'))
->searchable()
->required(),
])->label('Новость'),
Builder\Block::make('pageItem')
->schema([
Select::make('page')
->options(Page::query()->where('title', '!=' , null)->where('is_visible', true)->pluck('title', 'id'))
->searchable()
->required(),
])->label('Страница'),
Builder\Block::make('customForm')
->schema([
Select::make('form')
->options(CustomForm::query()->where('status', CustomFormStatus::PUBLISHED)->pluck('title', 'form_id'))
->searchable()
->required(),
])->label('Форма'),
])
->collapsed()
->blockNumbers(false)
->collapsible()
->blockPickerColumns(3)
->blockPickerWidth('2xl')
->addActionLabel('Добавить новый блок'),
]),
Tabs\Tab::make('Редакция')
->schema([
Forms\Components\Section::make('Главный редактор')->schema([
Forms\Components\Repeater::make('chief_editor')->label('')->schema([
TextInput::make('name')->label('Имя'),
TextInput::make('academicTitle')->label('Ученная степень'),
TextInput::make('position')->label('Должность'),
TextInput::make('institution')->label('Учереждение'),
])->maxItems(1)->reorderable(false),
]),
Forms\Components\Section::make('Редакторы')->schema([
Forms\Components\Repeater::make('editors')->label('')->schema([
TextInput::make('name')->label('Имя'),
TextInput::make('academicTitle')->label('Ученная степень'),
TextInput::make('position')->label('Должность'),
TextInput::make('institution')->label('Учереждение'),
])
->collapsed()
->collapsible()
->label('')
->addActionLabel('Добавить редактора'),
]), Section::make('Редакционная коллегия')
]), ->description('Состав редакционной коллегии журнала')
Tabs\Tab::make('Информация для авторов') ->collapsible()
->schema([ ->schema([
Builder::make('for_authors')->label('')->blocks([ Forms\Components\Repeater::make('editors')->label('')
Builder\Block::make('heading')->label('Заголовок') ->schema([
->schema([ TextInput::make('name')
TextInput::make('id')->hidden()->integer()->default(rand(2335235,324634264263426)), ->label('ФИО')
TextInput::make('content') ->required()
->label('') ->maxLength(100)
->live(onBlur: true) ->placeholder('Петров Петр Петрович'),
->afterStateUpdated(function (string $operation, string $state, Forms\Set $set, $get) { TextInput::make('academicTitle')
}), ->label('Учёная степень')
]), ->required()
Builder\Block::make('paragraph') ->maxLength(50)
->schema([ ->placeholder('к.ф.-м.н., доцент'),
RichEditor::make('content') TextInput::make('position')
->toolbarButtons([ ->label('Должность')
'blockquote', ->required()
'bold', ->maxLength(100)
'bulletList', ->placeholder('Доцент кафедры'),
'italic', TextInput::make('institution')
'link', ->label('Учреждение')
'orderedList',
'redo',
'strike',
'underline',
'undo',
])
->label(''),
])->label('Текст'),
Builder\Block::make('files')
->schema([
Forms\Components\Repeater::make('file')->schema([
Hidden::make('expansion')->required(),
Hidden::make('size')->required(),
TextInput::make('title')
->required() ->required()
->maxLength(255) ->maxLength(255)
->autofocus(), ->placeholder('СПбГУ'),
FileUpload::make('path') ])
->required() ->collapsed()
->getUploadedFileNameForStorageUsing( ->collapsible()
fn (TemporaryUploadedFile $file): string => ->addActionLabel('Добавить редактора')
str(Str::slug(pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME) . '-' . Carbon::now()->timestamp ) . '.' . $file->getClientOriginalExtension()) ->reorderable(true)
) ->itemLabel(fn (array $state): ?string => $state['name'] ?? null)
->acceptedFileTypes([ ->helperText('Добавьте членов редакционной коллегии журнала'),
'application/pdf', ]),
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'application/zip'
])
->maxSize(512000)
->disk('public')
->directory('files')
->downloadable()
->afterStateUpdated(function ($set, $state) {
$set('expansion', $state?->getClientOriginalExtension());
$set('size', ByteConverter::bytesToHuman($state?->getSize()));
})
->visibility('public')
]),
]),
Builder\Block::make('person')
->schema([
TextInput::make('name')
->label('Имя')
->required()
->maxLength(255),
FileUpload::make('photo')
->label('Фотография')
->image()
->disk('public')
->directory('images')
->imageEditor(),
Forms\Components\Repeater::make('info')->schema([
Forms\Components\Grid::make(2)->schema([
TextInput::make('column')
->required()
->maxLength(255),
TextInput::make('content')
->required()
->maxLength(255),
]),
])->minItems(1),
]),
Builder\Block::make('stepper')
->schema([
TextInput::make('step_name')
->label('Название шага')
->required()
->maxLength(255),
Forms\Components\Repeater::make('steps')->schema([
TextInput::make('title')
->required()
->maxLength(255)->columnSpanFull(),
RichEditor::make('content')->required(),
])->minItems(1),
]),
Builder\Block::make('tabs')
->schema([
Forms\Components\Repeater::make('tab')->schema([
TextInput::make('title')
->required()
->maxLength(255)->columnSpanFull(),
\Filament\Forms\Components\Builder::make('content')->label('')->blocks([
Builder\Block::make('heading')->label('Заголовок')
->schema([
TextInput::make('id')->hidden()->integer()->default(rand(2335235,324634264263426)),
TextInput::make('content')
->label('')
->live(onBlur: true)
->afterStateUpdated(function (string $operation, string $state, Forms\Set $set, $get) {
}),
]),
Builder\Block::make('paragraph')
->schema([
RichEditor::make('content')
->toolbarButtons([
'blockquote',
'bold',
'bulletList',
'italic',
'link',
'orderedList',
'redo',
'strike',
'underline',
'undo',
])
->label(''),
])->label('Текст'),
Builder\Block::make('files')
->schema([
Forms\Components\Repeater::make('file')->schema([
TextInput::make('title')
->required()
->maxLength(255)
->autofocus(),
FileUpload::make('path')
->required()
->acceptedFileTypes([
'application/pdf',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'application/zip'
])
->maxSize(512000)
->disk('public')
->directory('files')
->downloadable()
->visibility('public')
]),
]),
Builder\Block::make('person')
->schema([
TextInput::make('name')
->label('Имя')
->required()
->maxLength(255),
FileUpload::make('photo')
->label('Фотография')
->image()
->disk('public')
->directory('images')
->imageEditor(),
Forms\Components\Repeater::make('info')->schema([
Forms\Components\Grid::make(2)->schema([
TextInput::make('column')
->required()
->maxLength(255),
TextInput::make('content')
->required()
->maxLength(255),
]),
])->minItems(1),
]),
Builder\Block::make('stepper')
->schema([
TextInput::make('step_name')
->label('Название шага')
->required()
->maxLength(255),
Forms\Components\Repeater::make('steps')->schema([
TextInput::make('title')
->required()
->maxLength(255)->columnSpanFull(),
RichEditor::make('content')->required(),
])->minItems(1),
]),
Builder\Block::make('images')
->schema([
FileUpload::make('url')
->label('Изображение(-я)')
->image()
->multiple()
->reorderable()
->maxFiles(5)
->disk('public')
->directory('images')
->imageEditor()
->required(),
TextInput::make('alt')
->label('Описание')
->placeholder('Необязяательно')
])->label('Слайдер изображений'),
Builder\Block::make('image')
->schema([
FileUpload::make('url')
->label('Изображение(-я)')
->image()
->multiple()
->reorderable()
->maxFiles(5)
->disk('public')
->directory('images')
->imageEditor()
->required(),
TextInput::make('alt')
->label('Описание')
->placeholder('Необязяательно')
])->label('Изображение'),
Builder\Block::make('video')
->schema([
TextInput::make('mime')->readOnly(),
TextInput::make('title')
->required()
->maxLength(255)
->autofocus(),
FileUpload::make('path')
->required()
->acceptedFileTypes([
'video/mp4',
'video/quicktime',
'video/x-msvideo',
'video/x-ms-wmv',
'video/avi',
'video/webm',
'video/ogg',
'video/3gpp',
'video/3gpp2',
'video/x-m4v',
])
->disk('public')
->directory('videos')
->afterStateUpdated(fn (callable $set, $state) => $set('mime', $state?->getMimeType())),
]),
Builder\Block::make('postsList')
->schema([
Forms\Components\Grid::make(2)->schema([
TextInput::make('count')
->label('Количество запией')
->integer(),
Select::make('category')
->options(Category::all()->pluck('title', 'id'))
]),
])->label('Список новостей'),
])
->collapsed()
->blockNumbers(false)
->collapsible()
->addActionLabel('Добавить новый блок'),
])->minItems(1),
]),
Builder\Block::make('images')
->schema([
FileUpload::make('url')
->label('Изображение(-я)')
->image()
->multiple()
->reorderable()
->maxFiles(5)
->disk('public')
->directory('images')
->imageEditor()
->required(),
TextInput::make('alt')
->label('Описание')
->placeholder('Необязяательно')
])->label('Слайдер изображений'),
Builder\Block::make('image')
->schema([
FileUpload::make('url')
->label('Изображение(-я)')
->image()
->multiple()
->reorderable()
->maxFiles(5)
->disk('public')
->directory('images')
->imageEditor()
->required(),
TextInput::make('alt')
->label('Описание')
->placeholder('Необязяательно')
])->label('Изображение'),
Builder\Block::make('video')
->schema([
TextInput::make('mime')->readOnly(),
TextInput::make('title')
->required()
->maxLength(255)
->autofocus(),
FileUpload::make('path')
->required()
->acceptedFileTypes([
'video/mp4',
'video/quicktime',
'video/x-msvideo',
'video/x-ms-wmv',
'video/avi',
'video/webm',
'video/ogg',
'video/3gpp',
'video/3gpp2',
'video/x-m4v',
])
->disk('public')
->directory('videos')
->afterStateUpdated(fn (callable $set, $state) => $set('mime', $state?->getMimeType())),
]),
Builder\Block::make('postsList')
->schema([
Forms\Components\Grid::make(2)->schema([
TextInput::make('count')
->label('Количество запией')
->integer(),
Select::make('category')
->options(Category::all()->pluck('title', 'id'))
]),
])->label('Список новостей'),
Builder\Block::make('postItem')
->schema([
Select::make('post')
->options(Post::query()->where('status', PostStatus::PUBLISHED)->pluck('title', 'id'))
->searchable()
->required(),
])->label('Новость'),
Builder\Block::make('pageItem')
->schema([
Select::make('page')
->options(Page::query()->where('title', '!=' , null)->where('is_visible', true)->pluck('title', 'id'))
->searchable()
->required(),
])->label('Страница'),
Builder\Block::make('customForm')
->schema([
Select::make('form')
->options(CustomForm::query()->where('status', CustomFormStatus::PUBLISHED)->pluck('title', 'form_id'))
->searchable()
->required(),
])->label('Форма'),
])
->collapsed()
->blockNumbers(false)
->collapsible()
->blockPickerColumns(3)
->blockPickerWidth('2xl')
->addActionLabel('Добавить новый блок'),
]), ]),
])->columnSpanFull()
Tabs\Tab::make('Для авторов')
->icon('heroicon-o-pencil')
->schema([
ContentBuilderItem::getItem('for_authors')
->label('Информация для авторов')
->helperText('Разместите требования к статьям, правила оформления и сроки подачи'),
]),
])
]); ]);
} }
@@ -814,18 +179,39 @@ class AcademicJournalResource extends Resource
{ {
return $table return $table
->columns([ ->columns([
// Tables\Columns\TextColumn::make('title')
->label('Название журнала')
->searchable()
->sortable(),
Tables\Columns\TextColumn::make('created_at')
->label('Дата создания')
->dateTime('d.m.Y')
->sortable(),
]) ])
->filters([ ->filters([
//
]) ])
->actions([ ->actions([
Tables\Actions\EditAction::make(), Tables\Actions\EditAction::make()
->iconButton()
->tooltip('Редактировать'),
Tables\Actions\DeleteAction::make()
->iconButton()
->tooltip('Удалить'),
Tables\Actions\RestoreAction::make()
->iconButton()
->tooltip('Восстановить'),
]) ])
->bulkActions([ ->bulkActions([
Tables\Actions\BulkActionGroup::make([ Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(), Tables\Actions\DeleteBulkAction::make()
->label('Удалить выбранное'),
Tables\Actions\RestoreBulkAction::make()
->label('Восстановить выбранное'),
]), ]),
])
->emptyStateActions([
Tables\Actions\CreateAction::make()
->label('Добавить журнал'),
]); ]);
} }
@@ -844,4 +230,5 @@ class AcademicJournalResource extends Resource
'edit' => Pages\EditAcademicJournal::route('/{record}/edit'), 'edit' => Pages\EditAcademicJournal::route('/{record}/edit'),
]; ];
} }
}
}
@@ -3,42 +3,27 @@
namespace App\Filament\Resources\AcademicJournalResource\Pages; namespace App\Filament\Resources\AcademicJournalResource\Pages;
use App\Filament\Resources\AcademicJournalResource; use App\Filament\Resources\AcademicJournalResource;
use App\Services\Filament\Traits\SeoGenerate;
use Filament\Actions; use Filament\Actions;
use Filament\Resources\Pages\CreateRecord; use Filament\Resources\Pages\CreateRecord;
use Illuminate\Support\Str; use Illuminate\Support\Str;
class CreateAcademicJournal extends CreateRecord class CreateAcademicJournal extends CreateRecord
{ {
protected static string $resource = AcademicJournalResource::class; use SeoGenerate;
protected array $seoData; protected static string $resource = AcademicJournalResource::class;
protected function mutateFormDataBeforeCreate(array $data): array protected function mutateFormDataBeforeCreate(array $data): array
{ {
$this->seoData = $this->generateSeo($data);
$data['search_data'] = $this->generateSearchData($data['main_info']); $data['search_data'] = $this->generateSearchData($data['main_info']);
return $data; return $data;
} }
protected function afterCreate(): void protected function afterCreate(): void
{ {
$this->record->seo()->create($this->seoData); $this->createSeo($this->record);
}
private function generateSeo(array $data) : array
{
$title = $data['title'];
$rowData = $this->getFirstBlockByName('paragraph', $data['main_info']);
if ($rowData !== null) {
$description = strip_tags($rowData['data']['content']);
} else {
$description = null;
}
return [
'title' => $title,
'description' => Str::limit(htmlspecialchars($description, ENT_QUOTES, 'UTF-8'), 160),
];
} }
private function generateSearchData(array $data) : string private function generateSearchData(array $data) : string
@@ -3,20 +3,21 @@
namespace App\Filament\Resources\AcademicJournalResource\Pages; namespace App\Filament\Resources\AcademicJournalResource\Pages;
use App\Filament\Resources\AcademicJournalResource; use App\Filament\Resources\AcademicJournalResource;
use App\Services\Filament\Traits\SeoGenerate;
use Filament\Actions; use Filament\Actions;
use Filament\Resources\Pages\EditRecord; use Filament\Resources\Pages\EditRecord;
use Illuminate\Support\Str; use Illuminate\Support\Str;
class EditAcademicJournal extends EditRecord class EditAcademicJournal extends EditRecord
{ {
use SeoGenerate;
protected static string $resource = AcademicJournalResource::class; protected static string $resource = AcademicJournalResource::class;
protected array $seoData;
protected function mutateFormDataBeforeSave(array $data): array protected function mutateFormDataBeforeSave(array $data): array
{ {
$this->seoData = $this->generateSeo($data);
$data['search_data'] = $this->generateSearchData($data['main_info']); $data['search_data'] = $this->generateSearchData($data['main_info']);
return $data; return $data;
@@ -24,24 +25,7 @@ class EditAcademicJournal extends EditRecord
protected function afterSave(): void protected function afterSave(): void
{ {
$this->record->seo()->update($this->seoData); $this->updateSeo($this->record);
}
private function generateSeo(array $data) : array
{
$title = $data['title'];
$rowData = $this->getFirstBlockByName('paragraph', $data['main_info']);
if ($rowData !== null) {
$description = strip_tags($rowData['data']['content']);
} else {
$description = null;
}
return [
'title' => $title,
'description' => Str::limit(htmlspecialchars($description, ENT_QUOTES, 'UTF-8'), 160),
];
} }
private function getDataFromBlocks($block) : string private function getDataFromBlocks($block) : string
@@ -94,15 +78,6 @@ class EditAcademicJournal extends EditRecord
return strtolower($result); return strtolower($result);
} }
private function getFirstBlockByName(string $name, array $content) : array|null
{
$data = null;
foreach ($content as $block) {
$data = ($block['type'] === $name) ? $block : null;
break;
}
return $data;
}
protected function getHeaderActions(): array protected function getHeaderActions(): array
{ {
@@ -4,7 +4,9 @@ namespace App\Filament\Resources\AcademicJournalResource\RelationManagers;
use App\Models\AcademicJournal; use App\Models\AcademicJournal;
use Filament\Forms; use Filament\Forms;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\FileUpload; use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle; use Filament\Forms\Components\Toggle;
use Filament\Forms\Form; use Filament\Forms\Form;
use Filament\Resources\RelationManagers\RelationManager; use Filament\Resources\RelationManagers\RelationManager;
@@ -17,27 +19,54 @@ class JournalsRelationManager extends RelationManager
{ {
protected static string $relationship = 'journals'; protected static string $relationship = 'journals';
protected static ?string $modelLabel = 'выпуск';
protected static ?string $pluralModelLabel = 'выпуски';
public function form(Form $form): Form public function form(Form $form): Form
{ {
return $form return $form
->schema([ ->schema([
Forms\Components\TextInput::make('title')->required(), TextInput::make('title')
->label('Название выпуска')
->required()
->maxLength(255)
->placeholder('Введите название выпуска журнала')
->helperText('Например: "Том 15, №3 (2023)" или специальное название выпуска'),
FileUpload::make('path_file') FileUpload::make('path_file')
->label('Файл выпуска')
->required() ->required()
->acceptedFileTypes([ ->acceptedFileTypes([
'application/pdf', 'application/pdf' => 'PDF',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'DOCX',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'XLSX',
'application/vnd.openxmlformats-officedocument.presentationml.presentation', 'application/vnd.openxmlformats-officedocument.presentationml.presentation' => 'PPTX',
'application/zip' 'application/zip' => 'ZIP',
]) ])
->maxSize(512000) ->maxSize(512000)
->disk('public') ->disk('public')
->directory('files') ->directory('journals/files')
->downloadable() ->downloadable()
->visibility('public'), ->visibility('public')
Forms\Components\TextInput::make('year_publication')->integer(), ->helperText('Максимальный размер файла: 512MB. Допустимые форматы: PDF, DOCX, XLSX, PPTX, ZIP')
Toggle::make('is_active')->default(true)->label('Активный выпуск')->inline(false), ->openable()
->previewable(false),
TextInput::make('year_publication')
->label('Год публикации')
->required()
->numeric()
->minValue(1900)
->maxValue(now()->year + 1)
->placeholder('Укажите год выпуска')
->helperText('Год должен быть в диапазоне от 1900 до '.(now()->year + 1)),
Toggle::make('is_active')
->label('Активный выпуск')
->default(true)
->inline(false)
->helperText('Активные выпуски отображаются на сайте'),
]); ]);
} }
@@ -49,22 +78,79 @@ class JournalsRelationManager extends RelationManager
->defaultSort('sort') ->defaultSort('sort')
->recordTitleAttribute('title') ->recordTitleAttribute('title')
->columns([ ->columns([
Tables\Columns\TextColumn::make('title'), Tables\Columns\TextColumn::make('title')
->label('Название выпуска')
->searchable()
->sortable()
->description(fn ($record) => $record->year_publication),
Tables\Columns\IconColumn::make('is_active')
->label('Статус')
->boolean()
->trueIcon('heroicon-o-check-circle')
->falseIcon('heroicon-o-x-circle')
->trueColor('success')
->falseColor('danger'),
Tables\Columns\TextColumn::make('created_at')
->label('Дата добавления')
->dateTime('d.m.Y H:i')
->sortable(),
]) ])
->filters([ ->filters([
// Tables\Filters\SelectFilter::make('year_publication')
->label('Год выпуска')
->options(
fn () => $this->getOwnerRecord()
->journals()
->select('year_publication')
->distinct()
->orderBy('year_publication', 'desc')
->pluck('year_publication', 'year_publication')
->toArray()
),
Tables\Filters\TernaryFilter::make('is_active')
->label('Только активные')
->trueLabel('Активные')
->falseLabel('Неактивные')
->queries(
true: fn (Builder $query) => $query->where('is_active', true),
false: fn (Builder $query) => $query->where('is_active', false),
),
]) ])
->headerActions([ ->headerActions([
Tables\Actions\CreateAction::make(), Tables\Actions\CreateAction::make()
->label('Добавить выпуск')
->modalHeading('Добавление нового выпуска'),
]) ])
->actions([ ->actions([
Tables\Actions\EditAction::make(), Tables\Actions\EditAction::make()
Tables\Actions\DetachAction::make(), ->iconButton()
->tooltip('Редактировать'),
Tables\Actions\DeleteAction::make()
->iconButton()
->tooltip('Удалить')
->modalHeading('Удаление выпуска')
->modalDescription('Вы уверены, что хотите удалить этот выпуск?'),
]) ])
->bulkActions([ ->bulkActions([
Tables\Actions\BulkActionGroup::make([ Tables\Actions\BulkActionGroup::make([
Tables\Actions\DetachBulkAction::make(), Tables\Actions\DeleteBulkAction::make()
->label('Удалить выбранные')
->modalHeading('Удаление выпусков')
->modalDescription('Вы уверены, что хотите удалить выбранные выпуски?'),
]), ]),
])
->emptyStateActions([
Tables\Actions\CreateAction::make()
->label('Добавить выпуск'),
])
->groups([
Tables\Grouping\Group::make('year_publication')
->label('Год публикации')
->collapsible(),
]); ]);
} }
} }
@@ -3,18 +3,21 @@
namespace App\Filament\Resources; namespace App\Filament\Resources;
use App\Filament\Resources\AdditionalEducationCategoryResource\Pages; use App\Filament\Resources\AdditionalEducationCategoryResource\Pages;
use App\Filament\Resources\AdditionalEducationCategoryResource\RelationManagers;
use App\Models\AdditionalEducationCategory; use App\Models\AdditionalEducationCategory;
use App\Models\DirectionAdditionalEducation; use App\Models\DirectionAdditionalEducation;
use Filament\Forms; use Filament\Forms;
use Filament\Forms\Components\Grid;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Form; use Filament\Forms\Form;
use Filament\Resources\Resource; use Filament\Resources\Resource;
use Filament\Tables; use Filament\Tables;
use Filament\Tables\Columns\BadgeColumn;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn; use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table; use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletingScope;
use Illuminate\Support\Str; use Illuminate\Support\Str;
class AdditionalEducationCategoryResource extends Resource class AdditionalEducationCategoryResource extends Resource
@@ -27,27 +30,57 @@ class AdditionalEducationCategoryResource extends Resource
protected static ?string $pluralLabel = 'Категории дополнительного образования'; protected static ?string $pluralLabel = 'Категории дополнительного образования';
protected static ?string $navigationParentItem = 'Дополнительное Образование'; protected static ?string $navigationParentItem = 'Дополнительное Образование';
protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack'; protected static ?string $navigationIcon = 'heroicon-o-tag';
public static function form(Form $form): Form public static function form(Form $form): Form
{ {
return $form return $form
->schema([ ->schema([
Forms\Components\Section::make()->schema([ Section::make('Основная информация')
Forms\Components\Grid::make('2')->schema([ ->description('Заполните данные о категории программ ДПО')
TextInput::make('title')->label('Заголовок')->required() ->collapsible()
->live(onBlur: true) ->schema([
->afterStateUpdated(function (string $operation, string|null $state, Forms\Set $set) { Grid::make(2)
$set('slug', Str::slug($state)); ->schema([
$set('seo.title', $state); TextInput::make('title')
}), ->label('Название категории')
TextInput::make('slug')->label('Slug')->unique(ignoreRecord: true)->readOnly()->required(), ->required()
Forms\Components\Select::make('dir_addit_educat_id')->required()->label('Направление доп. образования') ->maxLength(255)
->preload() ->placeholder('Например: "Профессиональная переподготовка"')
->options(DirectionAdditionalEducation::where('is_active', true)->pluck('title', 'id')) ->live(onBlur: true)
->afterStateUpdated(function (string $operation, ?string $state, Forms\Set $set) {
$set('slug', Str::slug($state));
})
->helperText('Укажите понятное название категории'),
TextInput::make('slug')
->label('URL-идентификатор')
->required()
->maxLength(255)
->unique(ignoreRecord: true)
->helperText('Человеко-понятный URL для категории'),
Select::make('dir_addit_educat_id')
->label('Направление ДПО')
->options(
DirectionAdditionalEducation::where('is_active', true)
->orderBy('title')
->pluck('title', 'id')
)
->required()
->preload()
->searchable()
->placeholder('Выберите направление')
->helperText('К какому направлению относится категория'),
]),
Toggle::make('is_active')
->label('Активная категория')
->inline(false)
->default(true)
->helperText('Отображать ли категорию на сайте')
->columnSpanFull(),
]), ]),
Forms\Components\Toggle::make('is_active')->label('Активно')->columnSpanFull()->inline(false)->default(true),
]),
]); ]);
} }
@@ -55,30 +88,77 @@ class AdditionalEducationCategoryResource extends Resource
{ {
return $table return $table
->columns([ ->columns([
TextColumn::make('id')->label('ID')->sortable(), TextColumn::make('title')
TextColumn::make('title')->label('Название')->sortable()->searchable(), ->label('Название')
TextColumn::make('created_at')->label('Дата создания')->sortable(), ->searchable()
Tables\Columns\BadgeColumn::make('direction.title')->label('Направление')->sortable(), ->sortable()
Tables\Columns\ToggleColumn::make('is_active')->label('Активно')->sortable(), ->description(fn ($record) => $record->direction->title ?? '')
->limit(50),
BadgeColumn::make('direction.title')
->label('Направление')
->sortable()
->searchable()
->color('primary'),
IconColumn::make('is_active')
->label('Активна')
->boolean()
->trueIcon('heroicon-o-check-circle')
->falseIcon('heroicon-o-x-circle')
->trueColor('success')
->falseColor('danger')
->sortable(),
TextColumn::make('updated_at')
->label('Обновлено')
->dateTime('d.m.Y H:i')
->sortable()
->toggleable(isToggledHiddenByDefault: true),
]) ])
->filters([ ->filters([
// Tables\Filters\SelectFilter::make('dir_addit_educat_id')
->label('Направление ДПО')
->options(
DirectionAdditionalEducation::where('is_active', true)
->orderBy('title')
->pluck('title', 'id')
)
->searchable(),
Tables\Filters\TernaryFilter::make('is_active')
->label('Только активные')
->placeholder('Все')
->trueLabel('Активные')
->falseLabel('Неактивные'),
]) ])
->actions([ ->actions([
Tables\Actions\EditAction::make(), Tables\Actions\EditAction::make()
->iconButton()
->tooltip('Редактировать'),
Tables\Actions\ViewAction::make()
->iconButton()
->tooltip('Просмотреть'),
]) ])
->bulkActions([ ->bulkActions([
Tables\Actions\BulkActionGroup::make([ Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(), Tables\Actions\DeleteBulkAction::make()
->label('Удалить выбранные')
->modalHeading('Удаление категорий')
->modalDescription('Вы уверены, что хотите удалить выбранные категории ДПО?'),
]), ]),
]); ])
->emptyStateActions([
Tables\Actions\CreateAction::make()
->label('Добавить категорию'),
])
->defaultSort('title');
} }
public static function getRelations(): array public static function getRelations(): array
{ {
return [ return [];
//
];
} }
public static function getPages(): array public static function getPages(): array
@@ -89,4 +169,4 @@ class AdditionalEducationCategoryResource extends Resource
'edit' => Pages\EditAdditionalEducationCategory::route('/{record}/edit'), 'edit' => Pages\EditAdditionalEducationCategory::route('/{record}/edit'),
]; ];
} }
} }
@@ -2,41 +2,25 @@
namespace App\Filament\Resources; namespace App\Filament\Resources;
use App\Enums\CustomFormStatus;
use App\Enums\FormEducation; use App\Enums\FormEducation;
use App\Enums\LevelEducational; use App\Filament\Components\Forms\ItemForm\Pages\ContentBuilderItem;
use App\Enums\PostStatus;
use App\Filament\Resources\AdditionalEducationResource\Pages; use App\Filament\Resources\AdditionalEducationResource\Pages;
use App\Filament\Resources\AdditionalEducationResource\RelationManagers;
use App\Helpers\ByteConverter;
use App\Models\AdditionalEducation; use App\Models\AdditionalEducation;
use App\Models\AdditionalEducationCategory; use App\Models\AdditionalEducationCategory;
use App\Models\Category;
use App\Models\CustomForm;
use App\Models\DirectionAdditionalEducation;
use App\Models\Page;
use App\Models\Post;
use Filament\Forms; use Filament\Forms;
use Filament\Forms\Components\Builder; use Filament\Forms\Components\Grid;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\RichEditor;
use Filament\Forms\Components\Section; use Filament\Forms\Components\Section;
use Filament\Forms\Components\Select; use Filament\Forms\Components\Select;
use Filament\Forms\Components\SpatieTagsInput;
use Filament\Forms\Components\Tabs;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle; use Filament\Forms\Components\Toggle;
use Filament\Forms\Form; use Filament\Forms\Form;
use Filament\Resources\Resource; use Filament\Resources\Resource;
use Filament\Tables; use Filament\Tables;
use Filament\Tables\Columns\BadgeColumn;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn; use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table; use Filament\Tables\Table;
use Illuminate\Database\Eloquent\SoftDeletingScope;
use Illuminate\Support\Carbon;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
use Mohamedsabil83\FilamentFormsTinyeditor\Components\TinyEditor;
class AdditionalEducationResource extends Resource class AdditionalEducationResource extends Resource
{ {
@@ -45,401 +29,117 @@ class AdditionalEducationResource extends Resource
protected static ?string $navigationGroup = 'Образование'; protected static ?string $navigationGroup = 'Образование';
public static ?string $label = 'Дополнительное образование'; public static ?string $label = 'Дополнительное образование';
protected static ?string $pluralLabel = 'Дополнительное образование';
protected static ?string $navigationIcon = 'heroicon-o-academic-cap'; protected static ?string $pluralLabel = 'Дополнительное образование';
protected static ?string $navigationIcon = 'heroicon-o-book-open';
public static function form(Form $form): Form public static function form(Form $form): Form
{ {
return $form return $form
->schema([ ->schema([
Section::make()->schema([ Forms\Components\Tabs::make('Программа ДПО')
Tabs::make('Tabs') ->persistTabInQueryString()
->tabs([ ->columnSpanFull()
Tabs\Tab::make('Основная информация') ->tabs([
->schema([ Forms\Components\Tabs\Tab::make('Основные данные')
Forms\Components\Grid::make('2')->schema([ ->icon('heroicon-o-information-circle')
TextInput::make('title')->label('Заголовок')->required() ->schema([
->live(onBlur: true) Section::make('Общая информация')
->afterStateUpdated(function (string $operation, string|null $state, Forms\Set $set) { ->description('Основные сведения о программе')
$set('slug', Str::slug($state)); ->schema([
}), Grid::make(2)
TextInput::make('slug')->label('Slug')->unique(ignoreRecord: true)->readOnly()->required(),
Forms\Components\Select::make('category_id')->required()->label('Категория')
->options(AdditionalEducationCategory::where('is_active', true)->pluck('title', 'id'))->preload()->searchable()
]),
Forms\Components\TextInput::make('target_group')->required()->columnSpanFull()->label('Целевая аудитория'),
Forms\Components\TextInput::make('qualification')->required()->columnSpanFull()->label('Присваиваемая квалификация'),
Forms\Components\Grid::make('2')->schema([
Forms\Components\TextInput::make('price')->required()->integer()->label('Стоимость'),
Forms\Components\TextInput::make('learning_time')->required()->integer()->label('Объем обучения'),
]),
Forms\Components\Select::make('form_education')->label('Форма обучения')->required()->options(FormEducation::class),
Forms\Components\Toggle::make('is_active')->label('Активно')->columnSpanFull()->inline(false)->default(true),
]),
Tabs\Tab::make('Контент')
->schema([
Builder::make('content')->label('')->blocks([
Builder\Block::make('heading')->label('Заголовок')
->schema([ ->schema([
TextInput::make('id')->hidden()->integer()->default(rand(2335235,324634264263426)),
TextInput::make('content')
->label('')
->live(onBlur: true)
->afterStateUpdated(function (string $operation, string $state, Forms\Set $set, $get) {
}),
]),
Builder\Block::make('paragraph')
->schema([
RichEditor::make('content')
->toolbarButtons([
'blockquote',
'bold',
'bulletList',
'italic',
'link',
'orderedList',
'redo',
'strike',
'underline',
'undo',
])
->label(''),
])->label('Текст'),
Builder\Block::make('files')
->schema([
Forms\Components\Repeater::make('file')->schema([
Hidden::make('expansion')->required(),
Hidden::make('size')->required(),
TextInput::make('title')
->required()
->maxLength(255)
->autofocus(),
FileUpload::make('path')
->required()
->getUploadedFileNameForStorageUsing(
fn (TemporaryUploadedFile $file): string =>
str(Str::slug(pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME) . '-' . Carbon::now()->timestamp ) . '.' . $file->getClientOriginalExtension())
)
->acceptedFileTypes([
'application/pdf',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'application/zip'
])
->maxSize(512000)
->disk('public')
->directory('files')
->downloadable()
->afterStateUpdated(function ($set, $state) {
$set('expansion', $state?->getClientOriginalExtension());
$set('size', ByteConverter::bytesToHuman($state?->getSize()));
})
->visibility('public')
]),
]),
Builder\Block::make('person')
->schema([
TextInput::make('name')
->label('Имя')
->required()
->maxLength(255),
FileUpload::make('photo')
->label('Фотография')
->image()
->disk('public')
->directory('images')
->imageEditor(),
Forms\Components\Repeater::make('info')->schema([
Forms\Components\Grid::make(2)->schema([
TextInput::make('column')
->required()
->maxLength(255),
TextInput::make('content')
->required()
->maxLength(255),
]),
])->minItems(1),
]),
Builder\Block::make('stepper')
->schema([
TextInput::make('step_name')
->label('Название шага')
->required()
->maxLength(255),
Forms\Components\Repeater::make('steps')->schema([
TextInput::make('title')
->required()
->maxLength(255)->columnSpanFull(),
RichEditor::make('content')->required(),
])->minItems(1),
]),
Builder\Block::make('tabs')
->schema([
Forms\Components\Repeater::make('tab')->schema([
TextInput::make('title')
->required()
->maxLength(255)->columnSpanFull(),
\Filament\Forms\Components\Builder::make('content')->label('')->blocks([
Builder\Block::make('heading')->label('Заголовок')
->schema([
TextInput::make('id')->hidden()->integer()->default(rand(2335235,324634264263426)),
TextInput::make('content')
->label('')
->live(onBlur: true)
->afterStateUpdated(function (string $operation, string $state, Forms\Set $set, $get) {
}),
]),
Builder\Block::make('paragraph')
->schema([
RichEditor::make('content')
->toolbarButtons([
'blockquote',
'bold',
'bulletList',
'italic',
'link',
'orderedList',
'redo',
'strike',
'underline',
'undo',
])
->label(''),
])->label('Текст'),
Builder\Block::make('files')
->schema([
Forms\Components\Repeater::make('file')->schema([
TextInput::make('title')
->required()
->maxLength(255)
->autofocus(),
FileUpload::make('path')
->required()
->acceptedFileTypes([
'application/pdf',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'application/zip'
])
->maxSize(512000)
->disk('public')
->directory('files')
->downloadable()
->visibility('public')
]),
]),
Builder\Block::make('person')
->schema([
TextInput::make('name')
->label('Имя')
->required()
->maxLength(255),
FileUpload::make('photo')
->label('Фотография')
->image()
->disk('public')
->directory('images')
->imageEditor(),
Forms\Components\Repeater::make('info')->schema([
Forms\Components\Grid::make(2)->schema([
TextInput::make('column')
->required()
->maxLength(255),
TextInput::make('content')
->required()
->maxLength(255),
]),
])->minItems(1),
]),
Builder\Block::make('stepper')
->schema([
TextInput::make('step_name')
->label('Название шага')
->required()
->maxLength(255),
Forms\Components\Repeater::make('steps')->schema([
TextInput::make('title')
->required()
->maxLength(255)->columnSpanFull(),
RichEditor::make('content')->required(),
])->minItems(1),
]),
Builder\Block::make('images')
->schema([
FileUpload::make('url')
->label('Изображение(-я)')
->image()
->multiple()
->reorderable()
->maxFiles(5)
->disk('public')
->directory('images')
->imageEditor()
->required(),
TextInput::make('alt')
->label('Описание')
->placeholder('Необязяательно')
])->label('Слайдер изображений'),
Builder\Block::make('image')
->schema([
FileUpload::make('url')
->label('Изображение(-я)')
->image()
->multiple()
->reorderable()
->maxFiles(5)
->disk('public')
->directory('images')
->imageEditor()
->required(),
TextInput::make('alt')
->label('Описание')
->placeholder('Необязяательно')
])->label('Изображение'),
Builder\Block::make('video')
->schema([
TextInput::make('mime')->readOnly(),
TextInput::make('title')
->required()
->maxLength(255)
->autofocus(),
FileUpload::make('path')
->required()
->acceptedFileTypes([
'video/mp4',
'video/quicktime',
'video/x-msvideo',
'video/x-ms-wmv',
'video/avi',
'video/webm',
'video/ogg',
'video/3gpp',
'video/3gpp2',
'video/x-m4v',
])
->disk('public')
->directory('videos')
->afterStateUpdated(fn (callable $set, $state) => $set('mime', $state?->getMimeType())),
]),
Builder\Block::make('postsList')
->schema([
Forms\Components\Grid::make(2)->schema([
TextInput::make('count')
->label('Количество запией')
->integer(),
Select::make('category')
->options(Category::all()->pluck('title', 'id'))
]),
])->label('Список новостей'),
])
->collapsed()
->blockNumbers(false)
->collapsible()
->addActionLabel('Добавить новый блок'),
])->minItems(1),
]),
Builder\Block::make('images')
->schema([
FileUpload::make('url')
->label('Изображение(-я)')
->image()
->multiple()
->reorderable()
->maxFiles(5)
->disk('public')
->directory('images')
->imageEditor()
->required(),
TextInput::make('alt')
->label('Описание')
->placeholder('Необязяательно')
])->label('Слайдер изображений'),
Builder\Block::make('image')
->schema([
FileUpload::make('url')
->label('Изображение(-я)')
->image()
->multiple()
->reorderable()
->maxFiles(5)
->disk('public')
->directory('images')
->imageEditor()
->required(),
TextInput::make('alt')
->label('Описание')
->placeholder('Необязяательно')
])->label('Изображение'),
Builder\Block::make('video')
->schema([
TextInput::make('mime')->readOnly(),
TextInput::make('title') TextInput::make('title')
->label('Название программы')
->required() ->required()
->maxLength(255) ->maxLength(255)
->autofocus(), ->placeholder('Например: "Цифровые технологии в управлении"')
FileUpload::make('path') ->live(onBlur: true)
->afterStateUpdated(function (string $operation, ?string $state, Forms\Set $set) {
$set('slug', Str::slug($state));
})
->helperText('Полное официальное название программы'),
TextInput::make('slug')
->label('URL-адрес')
->required() ->required()
->acceptedFileTypes([ ->readonly()
'video/mp4', ->maxLength(255)
'video/quicktime', ->unique(ignoreRecord: true)
'video/x-msvideo', ->helperText('Человеко-понятный URL для страницы программы'),
'video/x-ms-wmv',
'video/avi',
'video/webm',
'video/ogg',
'video/3gpp',
'video/3gpp2',
'video/x-m4v',
])
->disk('public')
->directory('videos')
->afterStateUpdated(fn (callable $set, $state) => $set('mime', $state?->getMimeType())),
]), ]),
Builder\Block::make('postsList')
Select::make('category_id')
->label('Категория')
->options(AdditionalEducationCategory::where('is_active', true)->pluck('title', 'id'))
->required()
->preload()
->searchable()
->placeholder('Выберите категорию')
->helperText('К какой категории относится программа'),
TextInput::make('target_group')
->label('Целевая аудитория')
->required()
->maxLength(255)
->placeholder('Например: "Руководители среднего звена"')
->columnSpanFull()
->helperText('Для кого предназначена эта программа'),
TextInput::make('qualification')
->label('Выдаваемый документ')
->required()
->maxLength(255)
->placeholder('Например: "Удостоверение о повышении квалификации"')
->columnSpanFull()
->helperText('Какой документ получат слушатели'),
]),
Section::make('Параметры обучения')
->schema([
Grid::make(2)
->schema([ ->schema([
Forms\Components\Grid::make(2)->schema([ TextInput::make('price')
TextInput::make('count') ->label('Стоимость (руб)')
->label('Количество запией') ->required()
->integer(), ->numeric()
Select::make('category') ->minValue(0)
->options(Category::all()->pluck('title', 'id')) ->placeholder('Укажите стоимость')
]), ->helperText('Полная стоимость программы'),
])->label('Список новостей'),
Builder\Block::make('postItem') TextInput::make('learning_time')
->schema([ ->label('Объем (часов)')
Select::make('post') ->required()
->options(Post::query()->where('status', PostStatus::PUBLISHED)->pluck('title', 'id')) ->numeric()
->searchable() ->minValue(1)
->required(), ->placeholder('Укажите количество часов')
])->label('Новость'), ->helperText('Общий объем программы в академических часах'),
Builder\Block::make('pageItem')
->schema([ Select::make('form_education')
Select::make('page') ->label('Форма обучения')
->options(Page::query()->where('title', '!=' , null)->where('is_visible', true)->pluck('title', 'id')) ->options(FormEducation::class)
->searchable() ->required()
->required(), ->native(false)
])->label('Страница'), ->placeholder('Выберите форму')
Builder\Block::make('customForm') ->helperText('Основная форма проведения занятий'),
->schema([
Select::make('form') Toggle::make('is_active')
->options(CustomForm::query()->where('status', CustomFormStatus::PUBLISHED)->pluck('title', 'form_id')) ->label('Активна для записи')
->searchable() ->inline(false)
->required(), ->default(true)
])->label('Форма'), ->helperText('Отображать ли программу на сайте'),
]) ]),
->collapsed() ]),
->blockNumbers(false) ]),
->collapsible()
->blockPickerColumns(3) Forms\Components\Tabs\Tab::make('Содержание программы')
->blockPickerWidth('2xl') ->icon('heroicon-o-document-text')
->addActionLabel('Добавить новый блок'), ->schema([
]), ContentBuilderItem::getItem('content')
]), ->label('Описание программы')
]), ->helperText('Создайте подробное описание программы с помощью конструктора')
]),
]),
]); ]);
} }
@@ -447,30 +147,94 @@ class AdditionalEducationResource extends Resource
{ {
return $table return $table
->columns([ ->columns([
TextColumn::make('id')->label('ID')->sortable(), TextColumn::make('title')
TextColumn::make('title')->label('Название')->sortable()->searchable(), ->label('Название')
TextColumn::make('created_at')->label('Дата создания')->sortable(), ->searchable()
Tables\Columns\BadgeColumn::make('category.title')->label('Категория')->sortable(), ->sortable()
Tables\Columns\ToggleColumn::make('is_active')->label('Активно')->sortable(), ->description(fn ($record) => $record->target_group)
->limit(50),
BadgeColumn::make('category.title')
->label('Категория')
->sortable()
->searchable()
->color('primary'),
TextColumn::make('price')
->label('Стоимость')
->sortable()
->money('RUB')
->alignEnd(),
TextColumn::make('learning_time')
->label('Часов')
->sortable()
->alignCenter(),
BadgeColumn::make('form_education')
->label('Форма')
->formatStateUsing(fn ($state) => FormEducation::tryFrom($state->value)?->getLabel())
->color(fn ($state) => FormEducation::tryFrom($state->value)?->getColor())
->sortable(),
IconColumn::make('is_active')
->label('Активна')
->boolean()
->trueIcon('heroicon-o-check-circle')
->falseIcon('heroicon-o-x-circle')
->trueColor('success')
->falseColor('danger')
->sortable(),
TextColumn::make('updated_at')
->label('Обновлено')
->dateTime('d.m.Y H:i')
->sortable()
->toggleable(isToggledHiddenByDefault: true),
]) ])
->filters([ ->filters([
// Tables\Filters\SelectFilter::make('category_id')
->label('Категория')
->options(AdditionalEducationCategory::where('is_active', true)->pluck('title', 'id'))
->searchable(),
Tables\Filters\SelectFilter::make('form_education')
->label('Форма обучения')
->options(FormEducation::class),
Tables\Filters\TernaryFilter::make('is_active')
->label('Только активные')
->placeholder('Все')
->trueLabel('Активные')
->falseLabel('Неактивные'),
]) ])
->actions([ ->actions([
Tables\Actions\EditAction::make(), Tables\Actions\EditAction::make()
->iconButton()
->tooltip('Редактировать'),
Tables\Actions\ViewAction::make()
->iconButton()
->tooltip('Просмотреть'),
]) ])
->bulkActions([ ->bulkActions([
Tables\Actions\BulkActionGroup::make([ Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(), Tables\Actions\DeleteBulkAction::make()
->label('Удалить выбранные')
->modalHeading('Удаление программ')
->modalDescription('Вы уверены, что хотите удалить выбранные программы ДПО?'),
]), ]),
]); ])
->emptyStateActions([
Tables\Actions\CreateAction::make()
->label('Добавить программу'),
])
->defaultSort('title');
} }
public static function getRelations(): array public static function getRelations(): array
{ {
return [ return [];
//
];
} }
public static function getPages(): array public static function getPages(): array
@@ -481,4 +245,4 @@ class AdditionalEducationResource extends Resource
'edit' => Pages\EditAdditionalEducation::route('/{record}/edit'), 'edit' => Pages\EditAdditionalEducation::route('/{record}/edit'),
]; ];
} }
} }
@@ -3,20 +3,20 @@
namespace App\Filament\Resources\AdditionalEducationResource\Pages; namespace App\Filament\Resources\AdditionalEducationResource\Pages;
use App\Filament\Resources\AdditionalEducationResource; use App\Filament\Resources\AdditionalEducationResource;
use App\Services\Filament\Traits\SeoGenerate;
use Filament\Actions; use Filament\Actions;
use Filament\Resources\Pages\CreateRecord; use Filament\Resources\Pages\CreateRecord;
use Illuminate\Support\Str; use Illuminate\Support\Str;
class CreateAdditionalEducation extends CreateRecord class CreateAdditionalEducation extends CreateRecord
{ {
use SeoGenerate;
protected static string $resource = AdditionalEducationResource::class; protected static string $resource = AdditionalEducationResource::class;
protected array $seoData;
protected function mutateFormDataBeforeCreate(array $data): array protected function mutateFormDataBeforeCreate(array $data): array
{ {
$this->seoData = $this->generateSeo($data);
$data['search_data'] = $this->generateSearchData($data['content']); $data['search_data'] = $this->generateSearchData($data['content']);
return $data; return $data;
@@ -24,32 +24,9 @@ class CreateAdditionalEducation extends CreateRecord
protected function afterCreate(): void protected function afterCreate(): void
{ {
$this->record->seo()->create($this->seoData); $this->createSeo($this->record);
} }
private function generateSeo(array $data) : array
{
$title = $data['title'];
$rowData = $this->getFirstBlockByName('paragraph', $data['content']);
if ($rowData !== null) {
$description = strip_tags($rowData['data']['content']);
} else {
$description = null;
}
return [
'title' => $title,
'description' => Str::limit(htmlspecialchars($description, ENT_QUOTES, 'UTF-8'), 160),
];
}
private function getFirstBlockByName(string $name, array $content) : array|null
{
$data = null;
foreach ($content as $block) {
$data = ($block['type'] === $name) ? $block : null;
break;
}
return $data;
}
private function generateSearchData(array $data) : string private function generateSearchData(array $data) : string
{ {
@@ -3,22 +3,22 @@
namespace App\Filament\Resources\AdditionalEducationResource\Pages; namespace App\Filament\Resources\AdditionalEducationResource\Pages;
use App\Filament\Resources\AdditionalEducationResource; use App\Filament\Resources\AdditionalEducationResource;
use App\Services\Filament\Traits\SeoGenerate;
use Filament\Actions; use Filament\Actions;
use Filament\Resources\Pages\EditRecord; use Filament\Resources\Pages\EditRecord;
use Illuminate\Support\Str; use Illuminate\Support\Str;
class EditAdditionalEducation extends EditRecord class EditAdditionalEducation extends EditRecord
{ {
use SeoGenerate;
protected static string $resource = AdditionalEducationResource::class; protected static string $resource = AdditionalEducationResource::class;
protected array $seoData;
protected function mutateFormDataBeforeSave(array $data): array protected function mutateFormDataBeforeSave(array $data): array
{ {
$this->seoData = $this->generateSeo($data);
$data['search_data'] = $this->generateSearchData($data['content']); $data['search_data'] = $this->generateSearchData($data['content']);
return $data; return $data;
@@ -26,25 +26,9 @@ class EditAdditionalEducation extends EditRecord
protected function afterSave(): void protected function afterSave(): void
{ {
$this->record->seo()->update($this->seoData); $this->updateSeo($this->record);
} }
private function generateSeo(array $data) : array
{
$title = $data['title'];
$rowData = $this->getFirstBlockByName('paragraph', $data['content']);
if ($rowData !== null) {
$description = strip_tags($rowData['data']['content']);
} else {
$description = null;
}
return [
'title' => $title,
'description' => Str::limit(htmlspecialchars($description, ENT_QUOTES, 'UTF-8'), 160),
];
}
private function getFirstBlockByName(string $name, array $content) : array|null private function getFirstBlockByName(string $name, array $content) : array|null
{ {
$data = null; $data = null;
@@ -2,60 +2,138 @@
namespace App\Filament\Resources; namespace App\Filament\Resources;
use App\Enums\FormEducation; use App\Enums\AdmissionCampaignStatus;
use App\Enums\LevelEducational; use App\Enums\LevelEducational;
use App\Filament\Resources\AdmissionCampaignResource\Pages; use App\Filament\Resources\AdmissionCampaignResource\Pages;
use App\Filament\Resources\AdmissionCampaignResource\RelationManagers;
use App\Models\AdmissionCampaign; use App\Models\AdmissionCampaign;
use Filament\Forms; use Filament\Forms;
use Filament\Forms\Components\Grid;
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Forms\Form; use Filament\Forms\Form;
use Filament\Resources\Resource; use Filament\Resources\Resource;
use Filament\Tables; use Filament\Tables;
use Filament\Tables\Columns\BadgeColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table; use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletingScope;
class AdmissionCampaignResource extends Resource class AdmissionCampaignResource extends Resource
{ {
protected static ?string $model = AdmissionCampaign::class; protected static ?string $model = AdmissionCampaign::class;
protected static ?string $navigationGroup = 'Образование'; protected static ?string $navigationGroup = 'Образование';
protected static ?string $navigationIcon = 'heroicon-o-clipboard-document-check'; protected static ?string $navigationIcon = 'heroicon-o-clipboard-document-check';
protected static ?string $pluralLabel = 'Приемная-компания'; protected static ?string $pluralLabel = 'Приемная-компания';
protected static ?string $modelLabel = 'Приемная кампания';
public static function form(Form $form): Form public static function form(Form $form): Form
{ {
return $form return $form
->schema([ ->schema([
Forms\Components\Section::make()->schema([ Section::make('Основные настройки')
TextInput::make('name')->label('Название')->required()->columnSpanFull(), ->description('Общая информация о приемной кампании')
Forms\Components\Grid::make()->schema([ ->collapsible()
Forms\Components\Select::make('academic_year')->label('Академический год')->required() ->schema([
->options(self::generateAcademicYears()), TextInput::make('name')
Forms\Components\Select::make('status')->label('Статус')->required() ->label('Название кампании')
->options(['1' => 'Активный', '2' => 'Архивный', '3' => 'Скрыт']), ->required()
->maxLength(255)
->placeholder('Например: "Приемная кампания 2024"')
->columnSpanFull()
->helperText('Укажите понятное название для идентификации кампании'),
Grid::make(2)
->schema([
Select::make('academic_year')
->label('Академический год')
->required()
->options(self::generateAcademicYears())
->searchable()
->placeholder('Выберите учебный год')
->helperText('Выберите учебный год, к которому относится кампания'),
Select::make('status')
->label('Статус кампании')
->required()
->options(AdmissionCampaignStatus::class)
->native(false)
->placeholder('Выберите статус')
->helperText('Определяет видимость и доступность кампании'),
]),
]),
Section::make('Информация о наборе')
->description('Данные о программах и местах для разных уровней образования')
->collapsible()
->schema([
Repeater::make('info')
->label('')
->addActionLabel('Добавить уровень образования')
->schema([
Select::make('edu_name')
->label('Уровень образования')
->options(LevelEducational::class)
->required()
->native(false)
->placeholder('Выберите уровень образования')
->helperText('Выберите уровень образовательной программы'),
Grid::make(2)
->schema([
Section::make('Программы')
->schema([
TextInput::make('total_programs')
->label('Количество программ')
->required()
->numeric()
->minValue(0)
->placeholder('Укажите количество')
->helperText('Общее количество программ по набору'),
]),
Section::make('Распределение мест')
->schema([
TextInput::make('och_count')
->label('Очная форма')
->required()
->numeric()
->minValue(0)
->placeholder('Укажите количество')
->helperText('Количество мест на очной форме'),
TextInput::make('zaoch_count')
->label('Заочная форма')
->required()
->numeric()
->minValue(0)
->placeholder('Укажите количество')
->helperText('Количество мест на заочной форме'),
TextInput::make('budget_places')
->label('Бюджетные места')
->required()
->numeric()
->minValue(0)
->placeholder('Укажите количество')
->helperText('Количество бюджетных мест'),
TextInput::make('non_budget_places')
->label('Платные места')
->required()
->numeric()
->minValue(0)
->placeholder('Укажите количество')
->helperText('Количество платных мест'),
]),
]),
])
->itemLabel(fn (array $state): ?string =>
LevelEducational::tryFrom($state['edu_name'] ?? '')?->getLabel() ?? 'Новый уровень')
->collapsible()
->cloneable()
->columnSpanFull(),
]), ]),
]),
Forms\Components\Section::make()->schema([
Forms\Components\Repeater::make('info')->schema([
Forms\Components\Select::make('edu_name')->options(LevelEducational::class),
Forms\Components\Grid::make(2)->schema([
Forms\Components\Section::make()->schema([
TextInput::make('total_programs')->label('Количество программ по набору')->integer()->required(),
]),
Forms\Components\Section::make('Места')->schema([
TextInput::make('och_count')->label('Количество мест (Очная форма)')->integer()->required(),
TextInput::make('zaoch_count')->label('Количество мест (Заочная форма)')->integer()->required(),
TextInput::make('budget_places')->label('Количество бюджетных мест')->integer()->required(),
TextInput::make('non_budget_places')->label('Количество платных мест')->integer()->required(),
]),
]),
])->columnSpanFull(),
]),
]); ]);
} }
@@ -63,28 +141,65 @@ class AdmissionCampaignResource extends Resource
{ {
return $table return $table
->columns([ ->columns([
Tables\Columns\TextColumn::make('name'), TextColumn::make('name')
Tables\Columns\TextColumn::make('academic_year'), ->label('Название')
Tables\Columns\TextColumn::make('status'), ->searchable()
->sortable()
->description(fn ($record) => $record->academic_year),
BadgeColumn::make('status')
->label('Статус')
->formatStateUsing(fn ($state) => AdmissionCampaignStatus::tryFrom($state)?->getLabel())
->color(fn ($state) => AdmissionCampaignStatus::tryFrom($state)?->getColor())
->sortable(),
TextColumn::make('info_count')
->label('Программ')
->getStateUsing(fn ($record) => count($record->info ?? []))
->badge(),
TextColumn::make('updated_at')
->label('Обновлено')
->dateTime('d.m.Y H:i')
->sortable()
->toggleable(isToggledHiddenByDefault: true),
]) ])
->filters([ ->filters([
// Tables\Filters\SelectFilter::make('status')
->label('Статус')
->options(AdmissionCampaignStatus::class),
Tables\Filters\SelectFilter::make('academic_year')
->label('Учебный год')
->options(self::generateAcademicYears()),
]) ])
->actions([ ->actions([
Tables\Actions\EditAction::make(), Tables\Actions\EditAction::make()
->iconButton()
->tooltip('Редактировать'),
Tables\Actions\ViewAction::make()
->iconButton()
->tooltip('Просмотреть'),
]) ])
->bulkActions([ ->bulkActions([
Tables\Actions\BulkActionGroup::make([ Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(), Tables\Actions\DeleteBulkAction::make()
->label('Удалить выбранные')
->modalHeading('Удаление приемных кампаний')
->modalDescription('Вы уверены, что хотите удалить выбранные кампании? Это действие нельзя отменить.'),
]), ]),
]); ])
->emptyStateActions([
Tables\Actions\CreateAction::make()
->label('Создать кампанию'),
])
->defaultSort('academic_year', 'desc');
} }
public static function getRelations(): array public static function getRelations(): array
{ {
return [ return [];
//
];
} }
public static function getPages(): array public static function getPages(): array
@@ -99,15 +214,15 @@ class AdmissionCampaignResource extends Resource
protected static function generateAcademicYears(): array protected static function generateAcademicYears(): array
{ {
$currentYear = (int) date('Y') - 5; $currentYear = (int) date('Y') - 5;
$yearsAhead = 10; // Количество лет вперед $yearsAhead = 10;
$academicYears = []; $academicYears = [];
for ($i = 0; $i < $yearsAhead; $i++) { for ($i = 0; $i < $yearsAhead; $i++) {
$startYear = $currentYear + $i; $startYear = $currentYear + $i;
$endYear = $startYear + 1; $endYear = $startYear + 1;
$academicYears[$startYear] = "{$startYear}/{$endYear}"; $academicYears["{$startYear}/{$endYear}"] = "{$startYear}/{$endYear}";
} }
return $academicYears; return $academicYears;
} }
} }
@@ -8,5 +8,6 @@ use Filament\Resources\Pages\CreateRecord;
class CreateAdmissionCampaign extends CreateRecord class CreateAdmissionCampaign extends CreateRecord
{ {
protected static string $resource = AdmissionCampaignResource::class; protected static string $resource = AdmissionCampaignResource::class;
} }
+116 -32
View File
@@ -15,7 +15,9 @@ use Filament\Forms;
use Filament\Forms\Components\Builder; use Filament\Forms\Components\Builder;
use Filament\Forms\Components\FileUpload; use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Hidden; use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\Section; use Filament\Forms\Components\Section;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Forms\Form; use Filament\Forms\Form;
use Filament\Forms\Get; use Filament\Forms\Get;
@@ -42,45 +44,127 @@ class AdmissionPlanResource extends Resource
return $form return $form
->schema([ ->schema([
Section::make()->schema([ Section::make()->schema([
Forms\Components\Select::make('educational_programs_id') Select::make('educational_programs_id')
->label('Приемная кампания')
->required()
->columnSpanFull()
->options(EducationalProgram::whereIn('status', [EducationalProgramStatus::PUBLISHED, EducationalProgramStatus::IN_PROGRESS])->pluck('name', 'id'))
->searchable() ->searchable()
->label('Образовательная программа') ->preload()
->options(EducationalProgram::whereIn('status', [EducationalProgramStatus::PUBLISHED, EducationalProgramStatus::IN_PROGRESS])->pluck('name', 'id')), ->placeholder('Выберите образовательную программу'),
Forms\Components\Select::make('admission_campaigns_id')
->label('Приемная компания')
->options(AdmissionCampaign::all()->pluck('name', 'id')),
]),
Section::make('План приема')->schema([
Forms\Components\Repeater::make('exams')->label('Вступительные испытания')->schema([
TextInput::make('title')->label('Название-предмета'),
Forms\Components\Select::make('type_exam')->label('Тип-ВИ')
->options(['ege' => 'ЕГЭ', 'internal_test' => 'ВИ, проводимое организацией самостоятельно']),
TextInput::make('min_score')->label('Минимальный-балл')->integer()
])->live()->maxItems(2)->collapsed()->addActionLabel('Добавить вступительное испытание')->columns(3) ->itemLabel(function (Get $get) {
static $count = 0;
$maxCount = count($get('exams'));
$count = ($count++ <= $maxCount) ? $count : 1;
return "Вступительное испытание #" . $count;
}),
Forms\Components\Repeater::make('contests')->label('Условия поступления')->schema([
Forms\Components\Select::make('form_education')->label('Форма обучения')
->options(FormEducation::class),
Forms\Components\Select::make('financing_source')->label('Источник финансирования')
->options(BudgetEducation::class),
TextInput::make('position_count')->label('Количество мест на прием')->integer(),
])->live()->maxItems(2)->collapsed()->addActionLabel('Добавить группу')->columns(3)
->itemLabel(function (Get $get) {
static $count = 0;
$maxCount = count($get('contests'));
$count = ($count++ <= $maxCount) ? $count : 1;
return "Группа #" . $count;
}),
Select::make('admission_campaigns_id')
->label('Приемная кампания')
->required()
->columnSpanFull()
->options(
AdmissionCampaign::query()
->orderBy('name')
->pluck('name', 'id')
)
->searchable()
->preload()
->placeholder('Выберите приемную кампанию')
->helperText('Выберите связанную приемную кампанию'),
]), ]),
Section::make('План приема')
->description('Настройка вступительных испытаний и условий поступления')
->collapsible()
->schema([
self::getExamsRepeater(),
self::getContestsRepeater(),
]),
]); ]);
} }
protected static function getExamsRepeater(): Repeater
{
return Repeater::make('exams')
->label('Вступительные испытания')
->schema([
TextInput::make('title')
->label('Название предмета')
->required()
->maxLength(100)
->placeholder('Например: Математика')
->helperText('Название вступительного испытания'),
Select::make('type_exam')
->label('Тип испытания')
->required()
->options([
'ege' => 'ЕГЭ',
'internal_test' => 'Внутреннее испытание',
])
->native(false)
->placeholder('Выберите тип')
->helperText('Тип вступительного испытания'),
TextInput::make('min_score')
->label('Минимальный балл')
->required()
->numeric()
->minValue(0)
->maxValue(100)
->placeholder('Укажите минимальный балл')
->helperText('Минимальный проходной балл'),
])
->columns(3)
->maxItems(10)
->collapsible()
->collapsed()
->addActionLabel('Добавить испытание')
->itemLabel(fn (array $state): ?string => $state['title'] ?? 'Новое испытание')
->helperText('Добавьте все необходимые вступительные испытания');
}
protected static function getContestsRepeater(): Repeater
{
return Repeater::make('contests')
->label('Условия поступления')
->schema([
Select::make('form_education')
->label('Форма обучения')
->options(FormEducation::class)
->required()
->native(false)
->placeholder('Выберите форму')
->columnSpanFull()
->helperText('Форма обучения для данной группы'),
Repeater::make('places')
->label('Места')
->schema([
Select::make('form_budget')
->label('Форма финансирования')
->options(BudgetEducation::class)
->required()
->native(false)
->placeholder('Выберите тип')
->helperText('Бюджетные или платные места'),
TextInput::make('count')
->label('Количество мест')
->required()
->numeric()
->minValue(0)
->placeholder('Укажите количество')
->helperText('Количество доступных мест'),
])
->columnSpanFull()
->maxItems(2)
->addActionLabel('Добавить тип мест')
])
->columns(2)
->maxItems(3)
->collapsible()
->collapsed()
->addActionLabel('Добавить группу')
->helperText('Добавьте группы с условиями поступления');
}
public static function table(Table $table): Table public static function table(Table $table): Table
{ {
return $table return $table
+115 -33
View File
@@ -24,46 +24,128 @@ class ContactWidgetResource extends Resource
{ {
protected static ?string $model = ContactWidget::class; protected static ?string $model = ContactWidget::class;
protected static ?string $pluralLabel = 'Контактная информация';
protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack'; protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack';
protected static ?string $navigationGroup = 'Виджеты';
public static function form(Form $form): Form public static function form(Form $form): Form
{ {
return $form return $form
->schema([ ->schema([
Forms\Components\Section::make('')->schema([ Forms\Components\Section::make('Ресурс')
Tabs::make('Tabs') ->description('Настройка контактных ресурсов')
->tabs([ ->collapsible()
Tabs\Tab::make('Основная информация') ->schema([
->schema([ Tabs::make('Настройки ресурса')
Forms\Components\Grid::make(2)->schema([ ->persistTabInQueryString()
TextInput::make('title')->label('Название ресурса')->required() ->columnSpanFull()
->live(onBlur: true) ->tabs([
->afterStateUpdated(function (string $operation, string|null $state, Forms\Set $set) { Tabs\Tab::make('Основная информация')
$set('slug', Str::slug($state)); ->icon('heroicon-o-information-circle')
$set('seo.title', $state); ->schema([
}), Forms\Components\Grid::make(2)
TextInput::make('slug')->label('Slug')->unique(ignoreRecord: true)->readOnly()->required(), ->schema([
Toggle::make('is_active')->default(true)->label('Активный ресурс')->inline(false), TextInput::make('title')
]) ->label('Название ресурса')
]), ->placeholder('Введите название ресурса')
Tabs\Tab::make('Содержание ресурса') ->helperText('Это название будет отображаться в интерфейсе')
->schema([ ->required()
Repeater::make('content')->label('Ресурсы')->schema([ ->maxLength(255)
TextInput::make('title')->label('Главный заголовок столбца')->required(), ->live(onBlur: true)
Repeater::make('items')->label('Контакты')->schema([ ->afterStateUpdated(function (string $operation, string|null $state, Forms\Set $set) {
TextInput::make('header')->label('Заголовок')->required(), $set('slug', Str::slug($state));
Repeater::make('details')->label('Компонент контакта')->schema([ $set('seo.title', $state);
Forms\Components\Grid::make(2)->schema([ })
TextInput::make('content')->label('содержание')->required(), ->columnSpan(1),
TextInput::make('url')->label('Ссылка(Необязательно)'),
]),
]),
]),
])->collapsed()->required(),
]),
]), TextInput::make('slug')
]), ->label('URL-адрес (Slug)')
->helperText('Автоматически генерируется из названия')
->hintIcon('heroicon-o-information-circle', tooltip: 'Изменить можно только вручную')
->unique(ignoreRecord: true)
->readOnly()
->required()
->columnSpan(1),
Toggle::make('is_active')
->label('Активность ресурса')
->helperText('Отключите, чтобы скрыть ресурс')
->default(true)
->inline(false)
->onColor('success')
->offColor('danger')
->columnSpanFull(),
])
]),
Tabs\Tab::make('Содержание ресурса')
->icon('heroicon-o-document-text')
->schema([
Repeater::make('content')
->label('Структура ресурса')
->helperText('Добавьте столбцы с контактной информацией')
->addActionLabel('Добавить столбец')
->itemLabel(fn (array $state): ?string => $state['title'] ?? 'Новый столбец')
->collapsible()
->cloneable()
->grid(2)
->schema([
TextInput::make('title')
->label('Заголовок столбца')
->placeholder('Например: Контакты')
->helperText('Основной заголовок для группы контактов')
->required()
->maxLength(255),
Repeater::make('items')
->label('Контактные блоки')
->helperText('Добавьте контактные блоки в этот столбец')
->addActionLabel('Добавить контактный блок')
->itemLabel(fn (array $state): ?string => $state['header'] ?? 'Новый контакт')
->collapsible()
->cloneable()
->schema([
TextInput::make('header')
->label('Заголовок контакта')
->placeholder('Например: Телефон')
->helperText('Название контактной информации')
->required()
->maxLength(255),
Repeater::make('details')
->label('Детали контакта')
->helperText('Добавьте контактные данные')
->addActionLabel('Добавить деталь')
->collapsible()
->cloneable()
->schema([
Forms\Components\Grid::make(2)
->schema([
TextInput::make('content')
->label('Значение')
->placeholder('Например: +7 (123) 456-78-90')
->helperText('Основная контактная информация')
->columnSpanFull()
->required(),
TextInput::make('url')
->label('Ссылка')
->placeholder('https://example.com')
->helperText('Необязательная ссылка, связанная с контактом')
->url()
->columnSpanFull(),
])
])
])
])
->required(),
]),
]),
]),
]); ]);
} }
@@ -40,6 +40,9 @@ class CustomFormResource extends Resource
public static ?string $label = 'Форма'; public static ?string $label = 'Форма';
protected static ?string $pluralLabel = 'Пользовательские формы'; protected static ?string $pluralLabel = 'Пользовательские формы';
protected static ?string $navigationGroup = 'Виджеты';
protected static ?string $model = CustomForm::class; protected static ?string $model = CustomForm::class;
+104 -425
View File
@@ -2,476 +2,155 @@
namespace App\Filament\Resources; namespace App\Filament\Resources;
use App\Enums\CustomFormStatus; use App\Filament\Components\Forms\ItemForm\Pages\ContentBuilderItem;
use App\Enums\PostStatus;
use App\Filament\Resources\DepartmentResource\Pages; use App\Filament\Resources\DepartmentResource\Pages;
use App\Filament\Resources\DepartmentResource\RelationManagers; use App\Filament\Resources\DepartmentResource\RelationManagers;
use App\Helpers\ByteConverter;
use App\Models\Category;
use App\Models\CustomForm;
use App\Models\Department; use App\Models\Department;
use App\Models\Faculty; use App\Models\Faculty;
use App\Models\Page;
use App\Models\PageReferenceList;
use App\Models\Post;
use Filament\Forms; use Filament\Forms;
use Filament\Forms\Components\Builder;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\RichEditor;
use Filament\Forms\Components\Section; use Filament\Forms\Components\Section;
use Filament\Forms\Components\Select; use Filament\Forms\Components\Select;
use Filament\Forms\Components\Tabs;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle; use Filament\Forms\Components\Toggle;
use Filament\Forms\Form; use Filament\Forms\Form;
use Filament\Resources\Resource; use Filament\Resources\Resource;
use Filament\Tables; use Filament\Tables;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn; use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table; use Filament\Tables\Table;
use Illuminate\Database\Eloquent\SoftDeletingScope;
use Illuminate\Support\Carbon;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
class DepartmentResource extends Resource class DepartmentResource extends Resource
{ {
protected static ?string $model = Department::class; protected static ?string $model = Department::class;
protected static ?string $navigationGroup = 'Структура института'; protected static ?string $navigationGroup = 'Структура института';
protected static ?string $navigationIcon = 'heroicon-o-building-office';
protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack';
protected static ?string $pluralLabel = 'Кафедры'; protected static ?string $pluralLabel = 'Кафедры';
protected static ?string $modelLabel = 'кафедра';
public static ?string $label = 'Кафедра';
protected static ?string $navigationParentItem = 'Факультеты'; protected static ?string $navigationParentItem = 'Факультеты';
public static function form(Form $form): Form public static function form(Form $form): Form
{ {
return $form return $form
->schema([ ->schema([
Section::make() Forms\Components\Tabs::make('Настройки кафедры')
->schema([ ->persistTabInQueryString()
Tabs::make('Tabs') ->columnSpanFull()
->tabs([ ->tabs([
Tabs\Tab::make('Основная информация') Forms\Components\Tabs\Tab::make('Основные данные')
->icon('heroicon-o-information-circle')
->schema([
Section::make('Идентификация')
->description('Основная информация о кафедре')
->schema([ ->schema([
Forms\Components\Grid::make()->schema([ TextInput::make('title')
TextInput::make('title')->label('Название факультета')->required() ->label('Полное название')
->live(onBlur: true)
->afterStateUpdated(function (string $operation, string|null $state, Forms\Set $set) {
$set('slug', Str::slug($state));
$set('seo.title', $state);
}),
TextInput::make('slug')->label('Slug')->unique(ignoreRecord: true)->readOnly()->required(),
]),
Toggle::make('is_active')->default(true)->label('Активный факультет')->inline(false),
Forms\Components\Select::make('faculty_id')
->options(Faculty::all()->pluck('title', 'id'))
->label('Факультет')
->required(),
]),
Tabs\Tab::make('Описание факультета')
->schema([
Builder::make('content')->label('')->blocks([
Builder\Block::make('heading')->label('Заголовок')
->schema([
TextInput::make('id')->hidden()->integer()->default(rand(2335235,324634264263426)),
TextInput::make('content')
->label('')
->live(onBlur: true)
->afterStateUpdated(function (string $operation, string $state, Forms\Set $set, $get) {
}),
]),
Builder\Block::make('paragraph')
->schema([
RichEditor::make('content')
->toolbarButtons([
'blockquote',
'bold',
'bulletList',
'italic',
'link',
'orderedList',
'redo',
'strike',
'underline',
'undo',
])
->label(''),
])->label('Текст'),
Builder\Block::make('files')
->schema([
Forms\Components\Repeater::make('file')->schema([
Hidden::make('expansion')->required(),
Hidden::make('size')->required(),
TextInput::make('title')
->required()
->maxLength(255)
->autofocus(),
FileUpload::make('path')
->required()
->getUploadedFileNameForStorageUsing(
fn (TemporaryUploadedFile $file): string =>
str(Str::slug(pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME) . '-' . Carbon::now()->timestamp ) . '.' . $file->getClientOriginalExtension())
)
->acceptedFileTypes([
'application/pdf',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'application/zip'
])
->maxSize(512000)
->disk('public')
->directory('files')
->downloadable()
->afterStateUpdated(function ($set, $state) {
$set('expansion', $state?->getClientOriginalExtension());
$set('size', ByteConverter::bytesToHuman($state?->getSize()));
})
->visibility('public')
]),
]),
Builder\Block::make('person')
->schema([
TextInput::make('name')
->label('Имя')
->required()
->maxLength(255),
FileUpload::make('photo')
->label('Фотография')
->image()
->disk('public')
->directory('images')
->imageEditor(),
Forms\Components\Repeater::make('info')->schema([
Forms\Components\Grid::make(2)->schema([
TextInput::make('column')
->required()
->maxLength(255),
TextInput::make('content')
->required()
->maxLength(255),
]),
])->minItems(1),
]),
Builder\Block::make('stepper')
->schema([
TextInput::make('step_name')
->label('Название шага')
->required()
->maxLength(255),
Forms\Components\Repeater::make('steps')->schema([
TextInput::make('title')
->required()
->maxLength(255)->columnSpanFull(),
RichEditor::make('content')->required(),
])->minItems(1),
]),
Builder\Block::make('tabs')
->schema([
Forms\Components\Repeater::make('tab')->schema([
TextInput::make('title')
->required()
->maxLength(255)->columnSpanFull(),
\Filament\Forms\Components\Builder::make('content')->label('')->blocks([
Builder\Block::make('heading')->label('Заголовок')
->schema([
TextInput::make('id')->hidden()->integer()->default(rand(2335235,324634264263426)),
TextInput::make('content')
->label('')
->live(onBlur: true)
->afterStateUpdated(function (string $operation, string $state, Forms\Set $set, $get) {
}),
]),
Builder\Block::make('paragraph')
->schema([
RichEditor::make('content')
->toolbarButtons([
'blockquote',
'bold',
'bulletList',
'italic',
'link',
'orderedList',
'redo',
'strike',
'underline',
'undo',
])
->label(''),
])->label('Текст'),
Builder\Block::make('files')
->schema([
Forms\Components\Repeater::make('file')->schema([
TextInput::make('title')
->required()
->maxLength(255)
->autofocus(),
FileUpload::make('path')
->required()
->acceptedFileTypes([
'application/pdf',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'application/zip'
])
->maxSize(512000)
->disk('public')
->directory('files')
->downloadable()
->visibility('public')
]),
]),
Builder\Block::make('person')
->schema([
TextInput::make('name')
->label('Имя')
->required()
->maxLength(255),
FileUpload::make('photo')
->label('Фотография')
->image()
->disk('public')
->directory('images')
->imageEditor(),
Forms\Components\Repeater::make('info')->schema([
Forms\Components\Grid::make(2)->schema([
TextInput::make('column')
->required()
->maxLength(255),
TextInput::make('content')
->required()
->maxLength(255),
]),
])->minItems(1),
]),
Builder\Block::make('stepper')
->schema([
TextInput::make('step_name')
->label('Название шага')
->required()
->maxLength(255),
Forms\Components\Repeater::make('steps')->schema([
TextInput::make('title')
->required()
->maxLength(255)->columnSpanFull(),
RichEditor::make('content')->required(),
])->minItems(1),
]),
Builder\Block::make('images')
->schema([
FileUpload::make('url')
->label('Изображение(-я)')
->image()
->multiple()
->reorderable()
->maxFiles(5)
->disk('public')
->directory('images')
->imageEditor()
->required(),
TextInput::make('alt')
->label('Описание')
->placeholder('Необязяательно')
])->label('Слайдер изображений'),
Builder\Block::make('image')
->schema([
FileUpload::make('url')
->label('Изображение(-я)')
->image()
->multiple()
->reorderable()
->maxFiles(5)
->disk('public')
->directory('images')
->imageEditor()
->required(),
TextInput::make('alt')
->label('Описание')
->placeholder('Необязяательно')
])->label('Изображение'),
Builder\Block::make('video')
->schema([
TextInput::make('mime')->readOnly(),
TextInput::make('title')
->required()
->maxLength(255)
->autofocus(),
FileUpload::make('path')
->required()
->acceptedFileTypes([
'video/mp4',
'video/quicktime',
'video/x-msvideo',
'video/x-ms-wmv',
'video/avi',
'video/webm',
'video/ogg',
'video/3gpp',
'video/3gpp2',
'video/x-m4v',
])
->disk('public')
->directory('videos')
->afterStateUpdated(fn (callable $set, $state) => $set('mime', $state?->getMimeType())),
]),
Builder\Block::make('postsList')
->schema([
Forms\Components\Grid::make(2)->schema([
TextInput::make('count')
->label('Количество запией')
->integer(),
Select::make('category')
->options(Category::all()->pluck('title', 'id'))
]),
])->label('Список новостей'),
])
->collapsed()
->blockNumbers(false)
->collapsible()
->addActionLabel('Добавить новый блок'),
])->minItems(1),
]),
Builder\Block::make('images')
->schema([
FileUpload::make('url')
->label('Изображение(-я)')
->image()
->multiple()
->reorderable()
->maxFiles(5)
->disk('public')
->directory('images')
->imageEditor()
->required(),
TextInput::make('alt')
->label('Описание')
->placeholder('Необязяательно')
])->label('Слайдер изображений'),
Builder\Block::make('image')
->schema([
FileUpload::make('url')
->label('Изображение(-я)')
->image()
->multiple()
->reorderable()
->maxFiles(5)
->disk('public')
->directory('images')
->imageEditor()
->required(),
TextInput::make('alt')
->label('Описание')
->placeholder('Необязяательно')
])->label('Изображение'),
Builder\Block::make('video')
->schema([
TextInput::make('mime')->readOnly(),
TextInput::make('title')
->required()
->maxLength(255)
->autofocus(),
FileUpload::make('path')
->required()
->acceptedFileTypes([
'video/mp4',
'video/quicktime',
'video/x-msvideo',
'video/x-ms-wmv',
'video/avi',
'video/webm',
'video/ogg',
'video/3gpp',
'video/3gpp2',
'video/x-m4v',
])
->disk('public')
->directory('videos')
->afterStateUpdated(fn (callable $set, $state) => $set('mime', $state?->getMimeType())),
]),
Builder\Block::make('postsList')
->schema([
Forms\Components\Grid::make(2)->schema([
TextInput::make('count')
->label('Количество запией')
->integer(),
Select::make('category')
->options(Category::all()->pluck('title', 'id'))
]),
])->label('Список новостей'),
Builder\Block::make('postItem')
->schema([
Select::make('post')
->options(Post::query()->where('status', PostStatus::PUBLISHED)->pluck('title', 'id'))
->searchable()
->required(),
])->label('Новость'),
Builder\Block::make('pageItem')
->schema([
Select::make('page')
->options(Page::query()->where('title', '!=' , null)->where('is_visible', true)->pluck('title', 'id'))
->searchable()
->required(),
])->label('Страница'),
Builder\Block::make('customForm')
->schema([
Select::make('form')
->options(CustomForm::query()->where('status', CustomFormStatus::PUBLISHED)->pluck('title', 'form_id'))
->searchable()
->required(),
])->label('Форма'),
Builder\Block::make('pageResourceList')
->schema([
Select::make('resource')
->options(PageReferenceList::query()->where('is_active', true)->pluck('title', 'slug'))
->searchable()
->required(),
])->label('Ресурсы'),
])
->collapsed()
->blockNumbers(false)
->collapsible()
->required() ->required()
->blockPickerColumns(3) ->maxLength(255)
->blockPickerWidth('2xl') ->placeholder('Например: Кафедра программной инженерии')
->addActionLabel('Добавить новый блок'), ->live(onBlur: true)
->afterStateUpdated(function (string $operation, ?string $state, Forms\Set $set) {
$set('slug', Str::slug($state));
})
->helperText('Официальное название кафедры'),
TextInput::make('slug')
->label('URL-идентификатор')
->required()
->maxLength(255)
->unique(ignoreRecord: true)
->helperText('Человеко-понятный URL для страницы кафедры'),
Select::make('faculty_id')
->label('Факультет')
->options(Faculty::query()->orderBy('title')->pluck('title', 'id'))
->searchable()
->preload()
->required()
->native(false)
->helperText('К какому факультету относится кафедра'),
Toggle::make('is_active')
->label('Активная кафедра')
->inline(false)
->default(true)
->helperText('Отображать ли кафедру на сайте'),
]), ]),
]), ]),
])
Forms\Components\Tabs\Tab::make('Контент')
->icon('heroicon-o-document-text')
->schema([
ContentBuilderItem::getItem('content')
]),
]),
]); ]);
} }
public static function table(Table $table): Table public static function table(Table $table): Table
{ {
return $table return $table
->columns([ ->columns([
TextColumn::make('id')->label('ID')->sortable(), TextColumn::make('title')
TextColumn::make('title')->label('Название')->sortable()->searchable(), ->label('Название')
Tables\Columns\TextColumn::make('faculty.title')->label('Факультет')->words(2), ->searchable()
TextColumn::make('created_at')->label('Дата создания')->sortable(), ->sortable()
Tables\Columns\ToggleColumn::make('is_active')->label('Активно')->sortable(), ->description(fn ($record) => $record->faculty->abbreviation ?? ''),
TextColumn::make('faculty.title')
->label('Факультет')
->sortable()
->toggleable(isToggledHiddenByDefault: false),
IconColumn::make('is_active')
->label('Статус')
->boolean()
->trueIcon('heroicon-o-check-circle')
->falseIcon('heroicon-o-x-circle')
->trueColor('success')
->falseColor('danger')
->sortable(),
TextColumn::make('updated_at')
->label('Обновлено')
->dateTime('d.m.Y H:i')
->sortable()
->toggleable(isToggledHiddenByDefault: true),
]) ])
->filters([ ->filters([
// Tables\Filters\SelectFilter::make('faculty_id')
->label('Факультет')
->options(Faculty::query()->orderBy('title')->pluck('title', 'id'))
->searchable(),
Tables\Filters\TernaryFilter::make('is_active')
->label('Только активные')
->placeholder('Все')
->trueLabel('Активные')
->falseLabel('Неактивные'),
]) ])
->actions([ ->actions([
Tables\Actions\EditAction::make(), Tables\Actions\EditAction::make()
->iconButton()
->tooltip('Редактировать'),
Tables\Actions\ViewAction::make()
->iconButton()
->tooltip('Просмотреть'),
]) ])
->bulkActions([ ->bulkActions([
Tables\Actions\BulkActionGroup::make([ Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(), Tables\Actions\DeleteBulkAction::make()
->label('Удалить выбранные')
->modalHeading('Удаление кафедр')
->modalDescription('Вы уверены, что хотите удалить выбранные кафедры?'),
]), ]),
]); ])
->emptyStateActions([
Tables\Actions\CreateAction::make()
->label('Добавить кафедру'),
])
->defaultSort('title');
} }
public static function getRelations(): array public static function getRelations(): array
@@ -491,4 +170,4 @@ class DepartmentResource extends Resource
'edit' => Pages\EditDepartment::route('/{record}/edit'), 'edit' => Pages\EditDepartment::route('/{record}/edit'),
]; ];
} }
} }
@@ -3,41 +3,27 @@
namespace App\Filament\Resources\DepartmentResource\Pages; namespace App\Filament\Resources\DepartmentResource\Pages;
use App\Filament\Resources\DepartmentResource; use App\Filament\Resources\DepartmentResource;
use App\Services\Filament\Traits\SeoGenerate;
use Filament\Actions; use Filament\Actions;
use Filament\Resources\Pages\CreateRecord; use Filament\Resources\Pages\CreateRecord;
use Illuminate\Support\Str; use Illuminate\Support\Str;
class CreateDepartment extends CreateRecord class CreateDepartment extends CreateRecord
{ {
use SeoGenerate;
protected static string $resource = DepartmentResource::class; protected static string $resource = DepartmentResource::class;
protected array $seoData;
protected function mutateFormDataBeforeCreate(array $data): array protected function mutateFormDataBeforeCreate(array $data): array
{ {
$this->seoData = $this->generateSeo($data);
$data['search_data'] = $this->generateSearchData($data['content']); $data['search_data'] = $this->generateSearchData($data['content']);
return $data; return $data;
} }
protected function afterCreate(): void protected function afterCreate(): void
{ {
$this->record->seo()->create($this->seoData); $this->createSeo($this->record);
}
private function generateSeo(array $data) : array
{
$title = $data['title'];
$rowData = $this->getFirstBlockByName('paragraph', $data['content']);
if ($rowData !== null) {
$description = strip_tags($rowData['data']['content']);
} else {
$description = null;
}
return [
'title' => $title,
'description' => Str::limit(htmlspecialchars($description, ENT_QUOTES, 'UTF-8'), 160),
];
} }
private function generateSearchData(array $data) : string private function generateSearchData(array $data) : string
@@ -52,15 +38,6 @@ class CreateDepartment extends CreateRecord
return strtolower($result); return strtolower($result);
} }
private function getFirstBlockByName(string $name, array $content) : array|null
{
$data = null;
foreach ($content as $block) {
$data = ($block['type'] === $name) ? $block : null;
break;
}
return $data;
}
private function getDataFromBlocks($block) : string private function getDataFromBlocks($block) : string
@@ -3,20 +3,20 @@
namespace App\Filament\Resources\DepartmentResource\Pages; namespace App\Filament\Resources\DepartmentResource\Pages;
use App\Filament\Resources\DepartmentResource; use App\Filament\Resources\DepartmentResource;
use App\Services\Filament\Traits\SeoGenerate;
use Filament\Actions; use Filament\Actions;
use Filament\Resources\Pages\EditRecord; use Filament\Resources\Pages\EditRecord;
use Illuminate\Support\Str; use Illuminate\Support\Str;
class EditDepartment extends EditRecord class EditDepartment extends EditRecord
{ {
protected static string $resource = DepartmentResource::class; use SeoGenerate;
protected array $seoData; protected static string $resource = DepartmentResource::class;
protected function mutateFormDataBeforeSave(array $data): array protected function mutateFormDataBeforeSave(array $data): array
{ {
$this->seoData = $this->generateSeo($data);
$data['search_data'] = $this->generateSearchData($data['content']); $data['search_data'] = $this->generateSearchData($data['content']);
return $data; return $data;
@@ -24,24 +24,7 @@ class EditDepartment extends EditRecord
protected function afterSave(): void protected function afterSave(): void
{ {
$this->record->seo()->update($this->seoData); $this->updateSeo($this->record);
}
private function generateSeo(array $data) : array
{
$title = $data['title'];
$rowData = $this->getFirstBlockByName('paragraph', $data['content']);
if ($rowData !== null) {
$description = strip_tags($rowData['data']['content']);
} else {
$description = null;
}
return [
'title' => $title,
'description' => Str::limit(htmlspecialchars($description, ENT_QUOTES, 'UTF-8'), 160),
];
} }
private function getDataFromBlocks($block) : string private function getDataFromBlocks($block) : string
@@ -15,14 +15,22 @@ use Illuminate\Database\Eloquent\SoftDeletingScope;
class ProgramsRelationManager extends RelationManager class ProgramsRelationManager extends RelationManager
{ {
protected static string $relationship = 'programs'; protected static string $relationship = 'programs';
protected static ?string $title = 'Образовательные программы кафедры';
public function form(Form $form): Form public function form(Form $form): Form
{ {
return $form return $form
->schema([ ->schema([
Forms\Components\TextInput::make('name') Forms\Components\Section::make('Основная информация')
->required() ->description('Связь образовательной программы с кафедрой')
->maxLength(255), ->schema([
Forms\Components\TextInput::make('name')
->label('Название программы')
->required()
->maxLength(255)
->placeholder('Например: Информатика и вычислительная техника')
->helperText('Полное название образовательной программы'),
])
]); ]);
} }
@@ -31,24 +39,65 @@ class ProgramsRelationManager extends RelationManager
return $table return $table
->recordTitleAttribute('name') ->recordTitleAttribute('name')
->columns([ ->columns([
Tables\Columns\TextColumn::make('name'), Tables\Columns\TextColumn::make('name')
->label('Название программы')
->searchable()
->sortable()
->wrap(),
Tables\Columns\TextColumn::make('status')
->label('Статус')
->badge()
->formatStateUsing(fn($state): string => EducationalProgramStatus::tryFrom($state)->getLabel())
->color(fn($state): string => EducationalProgramStatus::tryFrom($state)->getColor())
->sortable(),
]) ])
->filters([ ->filters([
// Tables\Filters\SelectFilter::make('status')
->label('Статус программы')
->options(EducationalProgramStatus::class)
->default(EducationalProgramStatus::PUBLISHED->value),
]) ])
->headerActions([ ->headerActions([
AttachAction::make() AttachAction::make()
->recordSelectOptionsQuery(fn (Builder $query) => $query->where('status', EducationalProgramStatus::PUBLISHED)), ->label('Добавить программу')
->modalHeading('Добавление программы к кафедре')
->modalSubmitActionLabel('Добавить')
->preloadRecordSelect()
->recordSelectOptionsQuery(fn(Builder $query) => $query->where('status', EducationalProgramStatus::PUBLISHED))
->recordSelect(
fn(Forms\Components\Select $select) => $select
->placeholder('Выберите программу')
->label('Образовательная программа')
->helperText('Только опубликованные программы')
->searchable()
->columnSpanFull()
)
]) ])
->actions([ ->actions([
Tables\Actions\EditAction::make(),
Tables\Actions\DetachAction::make(), Tables\Actions\DetachAction::make()
->iconButton()
->tooltip('Открепить программу')
->modalHeading('Открепление программы')
->modalSubmitActionLabel('Открепить')
->modalDescription('Вы уверены, что хотите открепить эту программу от кафедры?'),
]) ])
->bulkActions([ ->bulkActions([
Tables\Actions\BulkActionGroup::make([ Tables\Actions\BulkActionGroup::make([
Tables\Actions\DetachBulkAction::make(), Tables\Actions\DetachBulkAction::make()
->label('Открепить выбранные')
->modalHeading('Открепление программ')
->modalSubmitActionLabel('Открепить')
->modalDescription('Вы уверены, что хотите открепить выбранные программы от кафедры?'),
]), ]),
]); ])
->emptyStateActions([
AttachAction::make()
->label('Добавить программу'),
])
->defaultSort('name')
->deferLoading()
->persistFiltersInSession();
} }
} }
@@ -14,21 +14,48 @@ use Illuminate\Database\Eloquent\SoftDeletingScope;
class TeachersRelationManager extends RelationManager class TeachersRelationManager extends RelationManager
{ {
protected static string $relationship = 'teachers'; protected static string $relationship = 'teachers';
protected static ?string $inverseRelationship = 'departments_teach'; protected static ?string $inverseRelationship = 'departments_teach';
protected static ?string $title = 'Преподаватели кафедры'; protected static ?string $title = 'Преподаватели кафедры';
public function form(Form $form): Form public function form(Form $form): Form
{ {
return $form return $form
->schema([ ->schema([
Forms\Components\TextInput::make('teaching_position')->label('Преподавательская должность')->required(), Forms\Components\Section::make('Информация о преподавателе')
Forms\Components\TextInput::make('service_email')->label('Служебная почта'), ->description('Основные данные о работе преподавателя на кафедре')
Forms\Components\TextInput::make('service_phone')->label('Служебный телефон'), ->schema([
Forms\Components\TextInput::make('cabinet')->label('Кабинет'), Forms\Components\TextInput::make('teaching_position')
->label('Преподавательская должность')
->required()
->maxLength(255)
->placeholder('Например: Профессор')
->helperText('Официальная преподавательская должность'),
Forms\Components\TextInput::make('service_email')
->label('Служебная почта')
->email()
->maxLength(255)
->placeholder('example@university.edu')
->helperText('Корпоративная электронная почта'),
Forms\Components\TextInput::make('service_phone')
->label('Служебный телефон')
->tel()
->maxLength(20)
->placeholder('+7 (XXX) XXX-XX-XX')
->helperText('Формат: +7 (XXX) XXX-XX-XX')
->regex('/^\+?[0-9\s\-\(\)]{7,}$/')
->validationMessages([
'regex' => 'Пожалуйста, введите корректный номер телефона. Допустимые форматы: +7 (XXX) XXX-XX-XX или XXX-XX-XX',
]),
Forms\Components\TextInput::make('cabinet')
->label('Кабинет')
->maxLength(10)
->placeholder('Например: 305а')
->helperText('Номер кабинета преподавателя'),
])
->columns(2),
]); ]);
} }
@@ -37,29 +64,105 @@ class TeachersRelationManager extends RelationManager
return $table return $table
->recordTitleAttribute('name') ->recordTitleAttribute('name')
->columns([ ->columns([
Tables\Columns\TextColumn::make('name'), Tables\Columns\TextColumn::make('name')
->label('ФИО')
->searchable()
->sortable(),
Tables\Columns\TextColumn::make('teaching_position')
->label('Должность')
->searchable()
->sortable()
->wrap(),
Tables\Columns\TextColumn::make('service_email')
->label('Почта')
->searchable()
->toggleable(isToggledHiddenByDefault: true),
Tables\Columns\TextColumn::make('cabinet')
->label('Кабинет')
->sortable()
->toggleable(),
]) ])
->filters([ ->filters([
//
]) ])
->headerActions([ ->headerActions([
AttachAction::make() AttachAction::make()
->preloadRecordSelect()
->recordSelectOptionsQuery(fn (Builder $query) => $query->has('userDetail'))
->form(fn (AttachAction $action): array => [ ->form(fn (AttachAction $action): array => [
$action->getRecordSelect(), Forms\Components\Section::make('')
Forms\Components\TextInput::make('teaching_position')->label('Преподавательская должность')->required(), ->schema([
Forms\Components\TextInput::make('service_email')->label('Служебная почта'), $action->getRecordSelect()
Forms\Components\TextInput::make('service_phone')->label('Служебный телефон'), ->placeholder('Выбрать преподавателя')
Forms\Components\TextInput::make('cabinet')->label('Кабинет'), ->columnSpanFull()
->searchable()
->preload()
->helperText('Выберите преподавателя')
->required(),
Forms\Components\TextInput::make('teaching_position')
->label('Преподавательская должность')
->required()
->maxLength(255)
->placeholder('Например: Профессор')
->helperText('Официальная преподавательская должность'),
Forms\Components\TextInput::make('service_email')
->label('Служебная почта')
->email()
->maxLength(255)
->placeholder('example@university.edu')
->helperText('Корпоративная электронная почта'),
Forms\Components\TextInput::make('service_phone')
->label('Служебный телефон')
->tel()
->maxLength(20)
->placeholder('+7 (XXX) XXX-XX-XX')
->helperText('Формат: +7 (XXX) XXX-XX-XX')
->regex('/^\+?[0-9\s\-\(\)]{7,}$/')
->validationMessages([
'regex' => 'Пожалуйста, введите корректный номер телефона. Допустимые форматы: +7 (XXX) XXX-XX-XX или XXX-XX-XX',
]),
Forms\Components\TextInput::make('cabinet')
->label('Кабинет')
->maxLength(10)
->placeholder('Например: 305а')
->helperText('Номер кабинета преподавателя'),
])
->columns(1),
]) ])
->modalSubmitActionLabel('Добавить')
]) ])
->actions([ ->actions([
Tables\Actions\EditAction::make(), Tables\Actions\EditAction::make()
Tables\Actions\DetachAction::make(), ->iconButton()
->tooltip('Редактировать'),
Tables\Actions\DetachAction::make()
->iconButton()
->tooltip('Убрать с кафедры')
->modalHeading('Удаление связи')
->modalSubmitActionLabel('Убрать')
->modalDescription('Вы уверены, что хотите убрать этого преподавателя с кафедры?'),
]) ])
->bulkActions([ ->bulkActions([
Tables\Actions\BulkActionGroup::make([ Tables\Actions\BulkActionGroup::make([
Tables\Actions\DetachBulkAction::make(), Tables\Actions\DetachBulkAction::make()
->label('Убрать выбранных')
->modalHeading('Удаление связей')
->modalSubmitActionLabel('Убрать')
->modalDescription('Вы уверены, что хотите убрать выбранных преподавателей с кафедры?'),
]), ]),
]); ])
->emptyStateActions([
AttachAction::make()
->label('Добавить преподавателя'),
])
->defaultSort('name')
->deferLoading();
} }
} }
@@ -14,20 +14,48 @@ use Illuminate\Database\Eloquent\SoftDeletingScope;
class WorkersRelationManager extends RelationManager class WorkersRelationManager extends RelationManager
{ {
protected static string $relationship = 'workers'; protected static string $relationship = 'workers';
protected static ?string $inverseRelationship = 'departments_work'; protected static ?string $inverseRelationship = 'departments_work';
protected static ?string $title = 'Сотрудники кафедры'; protected static ?string $title = 'Сотрудники кафедры';
public function form(Form $form): Form public function form(Form $form): Form
{ {
return $form return $form
->schema([ ->schema([
Forms\Components\TextInput::make('position')->label(олжность')->required(), Forms\Components\Section::make('Информация о должности')
Forms\Components\TextInput::make('service_email')->label('Служебная почта'), ->description('Основные данные о работе сотрудника на кафедре')
Forms\Components\TextInput::make('service_phone')->label('Служебный телефон'), ->schema([
Forms\Components\TextInput::make('cabinet')->label('Кабинет'), Forms\Components\TextInput::make('position')
->label('Должность')
->required()
->maxLength(255)
->placeholder('Например: Заведующий кафедрой')
->helperText('Официальная должность сотрудника на кафедре'),
Forms\Components\TextInput::make('service_email')
->label('Служебная почта')
->email()
->maxLength(255)
->placeholder('example@university.edu')
->helperText('Корпоративная электронная почта'),
Forms\Components\TextInput::make('service_phone')
->label('Служебный телефон')
->tel()
->maxLength(20)
->placeholder('+7 (XXX) XXX-XX-XX')
->helperText('Формат: +7 (XXX) XXX-XX-XX')
->regex('/^\+?[0-9\s\-\(\)]{7,}$/') // Разрешаем +, цифры, пробелы, дефисы, скобки
->validationMessages([
'regex' => 'Пожалуйста, введите корректный номер телефона. Допустимые форматы: +7 (XXX) XXX-XX-XX или XXX-XX-XX',
]),
Forms\Components\TextInput::make('cabinet')
->label('Кабинет')
->maxLength(10)
->placeholder('Например: 305а')
->helperText('Номер кабинета сотрудника'),
])
->columns(2),
]); ]);
} }
@@ -36,30 +64,105 @@ class WorkersRelationManager extends RelationManager
return $table return $table
->recordTitleAttribute('name') ->recordTitleAttribute('name')
->columns([ ->columns([
Tables\Columns\TextColumn::make('name'), Tables\Columns\TextColumn::make('name')
Tables\Columns\TextColumn::make('position'), ->label('ФИО')
->searchable()
->sortable(),
Tables\Columns\TextColumn::make('position')
->label('Должность')
->searchable()
->sortable()
->wrap(),
Tables\Columns\TextColumn::make('service_email')
->label('Почта')
->searchable()
->toggleable(isToggledHiddenByDefault: true),
Tables\Columns\TextColumn::make('cabinet')
->label('Кабинет')
->sortable()
->toggleable(),
]) ])
->filters([ ->filters([
//
]) ])
->headerActions([ ->headerActions([
AttachAction::make() AttachAction::make()
->preloadRecordSelect()
->recordSelectOptionsQuery(fn (Builder $query) => $query->has('userDetail'))
->form(fn (AttachAction $action): array => [ ->form(fn (AttachAction $action): array => [
$action->getRecordSelect(), Forms\Components\Section::make('')
Forms\Components\TextInput::make('position')->label('Должность')->required(), ->schema([
Forms\Components\TextInput::make('service_email')->label('Служебная почта'), $action->getRecordSelect()
Forms\Components\TextInput::make('service_phone')->label('Служебный телефон'), ->placeholder('Выбрать сотрудника')
Forms\Components\TextInput::make('cabinet')->label('Кабинет'), ->columnSpanFull()
->searchable()
->preload()
->helperText('Выберите сотрудника')
->required(),
Forms\Components\TextInput::make('position')
->label('Должность')
->required()
->maxLength(255)
->placeholder('Например: Заведующий кафедрой')
->helperText('Официальная должность сотрудника на кафедре'),
Forms\Components\TextInput::make('service_email')
->label('Служебная почта')
->email()
->maxLength(255)
->placeholder('example@university.edu')
->helperText('Корпоративная электронная почта'),
Forms\Components\TextInput::make('service_phone')
->label('Служебный телефон')
->tel()
->maxLength(20)
->placeholder('+7 (XXX) XXX-XX-XX')
->helperText('Формат: +7 (XXX) XXX-XX-XX')
->regex('/^\+?[0-9\s\-\(\)]{7,}$/') // Разрешаем +, цифры, пробелы, дефисы, скобки
->validationMessages([
'regex' => 'Пожалуйста, введите корректный номер телефона. Допустимые форматы: +7 (XXX) XXX-XX-XX или XXX-XX-XX',
]),
Forms\Components\TextInput::make('cabinet')
->label('Кабинет')
->maxLength(10)
->placeholder('Например: 305а')
->helperText('Номер кабинета сотрудника'),
])
->columns(2),
]) ])
->modalSubmitActionLabel('Добавить')
]) ])
->actions([ ->actions([
Tables\Actions\EditAction::make(), Tables\Actions\EditAction::make()
Tables\Actions\DetachAction::make(), ->iconButton()
->tooltip('Редактировать'),
Tables\Actions\DetachAction::make()
->iconButton()
->tooltip('Убрать с кафедры')
->modalHeading('Удаление связи')
->modalSubmitActionLabel('Убрать')
->modalDescription('Вы уверены, что хотите убрать этого сотрудника с кафедры?'),
]) ])
->bulkActions([ ->bulkActions([
Tables\Actions\BulkActionGroup::make([ Tables\Actions\BulkActionGroup::make([
Tables\Actions\DetachBulkAction::make(), Tables\Actions\DetachBulkAction::make()
->label('Убрать выбранных')
->modalHeading('Удаление связей')
->modalSubmitActionLabel('Убрать')
->modalDescription('Вы уверены, что хотите убрать выбранных сотрудников с кафедры?'),
]), ]),
]); ])
->emptyStateActions([
AttachAction::make()
->label('Добавить сотрудника'),
])
->defaultSort('name')
->deferLoading();
} }
} }
@@ -3,17 +3,18 @@
namespace App\Filament\Resources; namespace App\Filament\Resources;
use App\Filament\Resources\DirectionAdditionalEducationResource\Pages; use App\Filament\Resources\DirectionAdditionalEducationResource\Pages;
use App\Filament\Resources\DirectionAdditionalEducationResource\RelationManagers;
use App\Models\DirectionAdditionalEducation; use App\Models\DirectionAdditionalEducation;
use Filament\Forms; use Filament\Forms;
use Filament\Forms\Components\Grid;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Form; use Filament\Forms\Form;
use Filament\Resources\Resource; use Filament\Resources\Resource;
use Filament\Tables; use Filament\Tables;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn; use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table; use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletingScope;
use Illuminate\Support\Str; use Illuminate\Support\Str;
class DirectionAdditionalEducationResource extends Resource class DirectionAdditionalEducationResource extends Resource
@@ -25,25 +26,44 @@ class DirectionAdditionalEducationResource extends Resource
public static ?string $label = 'Направление'; public static ?string $label = 'Направление';
protected static ?string $pluralLabel = 'Направления дополнительного образования'; protected static ?string $pluralLabel = 'Направления дополнительного образования';
protected static ?string $navigationParentItem = 'Дополнительное Образование'; protected static ?string $navigationParentItem = 'Дополнительное Образование';
protected static ?string $navigationIcon = 'heroicon-o-arrow-trending-up';
protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack';
public static function form(Form $form): Form public static function form(Form $form): Form
{ {
return $form return $form
->schema([ ->schema([
Forms\Components\Section::make()->schema([ Section::make('Основная информация')
Forms\Components\Grid::make()->schema([ ->description('Заполните данные о направлении дополнительного образования')
TextInput::make('title')->label('Заголовок')->required() ->collapsible()
->live(onBlur: true) ->schema([
->afterStateUpdated(function (string $operation, string|null $state, Forms\Set $set) { Grid::make(2)
$set('slug', Str::slug($state)); ->schema([
$set('seo.title', $state); TextInput::make('title')
}), ->label('Название направления')
TextInput::make('slug')->label('Slug')->unique(ignoreRecord: true)->readOnly()->required(), ->required()
->maxLength(255)
->placeholder('Например: "Информационные технологии"')
->live(onBlur: true)
->afterStateUpdated(function (string $operation, ?string $state, Forms\Set $set) {
$set('slug', Str::slug($state));
})
->helperText('Укажите понятное название направления'),
TextInput::make('slug')
->label('URL-идентификатор')
->required()
->maxLength(255)
->unique(ignoreRecord: true)
->helperText('Человеко-понятный URL для направления'),
]),
Toggle::make('is_active')
->label('Активное направление')
->inline(false)
->default(true)
->helperText('Отображать ли направление на сайте')
->columnSpanFull(),
]), ]),
Forms\Components\Toggle::make('is_active')->label('Активно')->columnSpanFull()->inline(false)->default(true),
]),
]); ]);
} }
@@ -51,29 +71,67 @@ class DirectionAdditionalEducationResource extends Resource
{ {
return $table return $table
->columns([ ->columns([
TextColumn::make('id')->label('ID')->sortable(), TextColumn::make('title')
TextColumn::make('title')->label('Название')->sortable()->searchable(), ->label('Название')
TextColumn::make('created_at')->label('Дата создания')->sortable(), ->searchable()
Tables\Columns\ToggleColumn::make('is_active')->label('Активно')->sortable(), ->sortable()
->limit(50),
IconColumn::make('is_active')
->label('Активно')
->boolean()
->trueIcon('heroicon-o-check-circle')
->falseIcon('heroicon-o-x-circle')
->trueColor('success')
->falseColor('danger')
->sortable(),
TextColumn::make('created_at')
->label('Дата создания')
->dateTime('d.m.Y H:i')
->sortable()
->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('updated_at')
->label('Обновлено')
->dateTime('d.m.Y H:i')
->sortable()
->toggleable(isToggledHiddenByDefault: true),
]) ])
->filters([ ->filters([
// Tables\Filters\TernaryFilter::make('is_active')
->label('Только активные')
->placeholder('Все')
->trueLabel('Активные')
->falseLabel('Неактивные'),
]) ])
->actions([ ->actions([
Tables\Actions\EditAction::make(), Tables\Actions\EditAction::make()
->iconButton()
->tooltip('Редактировать'),
Tables\Actions\ViewAction::make()
->iconButton()
->tooltip('Просмотреть'),
]) ])
->bulkActions([ ->bulkActions([
Tables\Actions\BulkActionGroup::make([ Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(), Tables\Actions\DeleteBulkAction::make()
->label('Удалить выбранные')
->modalHeading('Удаление направлений')
->modalDescription('Вы уверены, что хотите удалить выбранные направления ДПО?'),
]), ]),
]); ])
->emptyStateActions([
Tables\Actions\CreateAction::make()
->label('Добавить направление'),
])
->defaultSort('title');
} }
public static function getRelations(): array public static function getRelations(): array
{ {
return [ return [];
//
];
} }
public static function getPages(): array public static function getPages(): array
@@ -84,4 +142,4 @@ class DirectionAdditionalEducationResource extends Resource
'edit' => Pages\EditDirectionAdditionalEducation::route('/{record}/edit'), 'edit' => Pages\EditDirectionAdditionalEducation::route('/{record}/edit'),
]; ];
} }
} }
@@ -4,63 +4,132 @@ namespace App\Filament\Resources;
use App\Enums\LevelEducational; use App\Enums\LevelEducational;
use App\Filament\Resources\DirectionStudyResource\Pages; use App\Filament\Resources\DirectionStudyResource\Pages;
use App\Filament\Resources\DirectionStudyResource\RelationManagers;
use App\Models\DirectionStudy; use App\Models\DirectionStudy;
use Filament\Forms; use Filament\Forms;
use Filament\Forms\Components\Grid;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form; use Filament\Forms\Form;
use Filament\Resources\Resource; use Filament\Resources\Resource;
use Filament\Tables; use Filament\Tables;
use Filament\Tables\Columns\BadgeColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table; use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletingScope;
use Illuminate\Support\Str; use Illuminate\Support\Str;
class DirectionStudyResource extends Resource class DirectionStudyResource extends Resource
{ {
protected static ?string $model = DirectionStudy::class; protected static ?string $model = DirectionStudy::class;
protected static ?string $navigationIcon = 'heroicon-o-academic-cap';
protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack'; protected static ?string $pluralLabel = 'Направления подготовки';
protected static ?string $modelLabel = 'Направление подготовки';
protected static ?string $pluralLabel = 'Направление подготовки';
protected static ?string $navigationGroup = 'Образование'; protected static ?string $navigationGroup = 'Образование';
protected static ?string $navigationParentItem = 'Приемная-компания'; protected static ?string $navigationParentItem = 'Приемная-компания';
public static function form(Form $form): Form public static function form(Form $form): Form
{ {
return $form return $form
->schema([]); ->schema([
Section::make('Основная информация')
->description('Заполните основные данные о направлении подготовки')
->collapsible()
->schema([
Grid::make(2)
->schema([
TextInput::make('name')
->label('Название направления')
->required()
->maxLength(255)
->placeholder('Например: Информатика и вычислительная техника')
->live(onBlur: true)
->afterStateUpdated(function (string $operation, ?string $state, Forms\Set $set) {
$set('slug', Str::slug($state));
})
->helperText('Полное название направления подготовки'),
TextInput::make('slug')
->label('URL-идентификатор')
->required()
->readOnly()
->maxLength(255)
->unique(ignoreRecord: true)
->helperText('Человеко-понятный URL для направления'),
TextInput::make('code')
->label('Код направления')
->required()
->maxLength(50)
->placeholder('Например: 09.03.01')
->helperText('Код направления по ФГОС'),
Select::make('lvl_edu')
->label('Уровень образования')
->options(LevelEducational::class)
->required()
->native(false)
->placeholder('Выберите уровень')
->helperText('Выберите уровень образовательной программы'),
]),
]),
]);
} }
public static function table(Table $table): Table public static function table(Table $table): Table
{ {
return $table return $table
->columns([ ->columns([
Tables\Columns\TextColumn::make('name')->label('Название'), TextColumn::make('code')
Tables\Columns\TextColumn::make('code')->label('Код направления'), ->label('Код')
Tables\Columns\TextColumn::make('lvl_edu')->label('Уровень образования') ->searchable()
->formatStateUsing(fn ($state) => $state->getLabel()) ->sortable()
->description(fn ($record) => $record->name),
BadgeColumn::make('lvl_edu')
->label('Уровень')
->formatStateUsing(fn ($state) => LevelEducational::tryFrom($state->value)?->getLabel())
->color(fn ($state) => LevelEducational::tryFrom($state->value)?->getColor())
->sortable(),
TextColumn::make('created_at')
->label('Добавлено')
->dateTime('d.m.Y H:i')
->sortable()
->toggleable(isToggledHiddenByDefault: true),
]) ])
->filters([ ->filters([
// Tables\Filters\SelectFilter::make('lvl_edu')
->label('Уровень образования')
->options(LevelEducational::class),
]) ])
->actions([ ->actions([
Tables\Actions\EditAction::make(), Tables\Actions\EditAction::make()
->iconButton()
->tooltip('Редактировать'),
Tables\Actions\ViewAction::make()
->iconButton()
->tooltip('Просмотреть'),
]) ])
->bulkActions([ ->bulkActions([
Tables\Actions\BulkActionGroup::make([ Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(), Tables\Actions\DeleteBulkAction::make()
->label('Удалить выбранные')
->modalHeading('Удаление направлений')
->modalDescription('Вы уверены, что хотите удалить выбранные направления подготовки?'),
]), ]),
]); ])
->emptyStateActions([
Tables\Actions\CreateAction::make()
->label('Добавить направление'),
])
->defaultSort('code');
} }
public static function getRelations(): array public static function getRelations(): array
{ {
return [ return [];
//
];
} }
public static function getPages(): array public static function getPages(): array
@@ -71,4 +140,4 @@ class DirectionStudyResource extends Resource
'edit' => Pages\EditDirectionStudy::route('/{record}/edit'), 'edit' => Pages\EditDirectionStudy::route('/{record}/edit'),
]; ];
} }
} }
+119 -378
View File
@@ -2,20 +2,15 @@
namespace App\Filament\Resources; namespace App\Filament\Resources;
use App\Enums\PostStatus; use App\Filament\Components\Forms\ItemForm\Pages\ContentBuilderItem;
use App\Filament\Resources\DivisionResource\Pages; use App\Filament\Resources\DivisionResource\Pages;
use App\Filament\Resources\DivisionResource\RelationManagers; use App\Filament\Resources\DivisionResource\RelationManagers;
use App\Models\Category;
use App\Models\Division; use App\Models\Division;
use App\Models\Page;
use App\Models\Post;
use Filament\Forms; use Filament\Forms;
use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Components\Builder; use Filament\Forms\Components\Builder;
use Filament\Forms\Components\FileUpload; use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\RichEditor;
use Filament\Forms\Components\Section; use Filament\Forms\Components\Section;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Tabs; use Filament\Forms\Components\Tabs;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle; use Filament\Forms\Components\Toggle;
@@ -24,410 +19,156 @@ use Filament\Resources\Resource;
use Filament\Tables; use Filament\Tables;
use Filament\Tables\Columns\TextColumn; use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table; use Filament\Tables\Table;
use Illuminate\Database\Eloquent\SoftDeletingScope;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Mohamedsabil83\FilamentFormsTinyeditor\Components\TinyEditor;
class DivisionResource extends Resource class DivisionResource extends Resource
{ {
protected static ?string $model = Division::class; protected static ?string $model = Division::class;
protected static ?string $navigationGroup = 'Структура института'; protected static ?string $navigationGroup = 'Структура института';
protected static ?string $navigationIcon = 'heroicon-o-squares-2x2'; protected static ?string $navigationIcon = 'heroicon-o-squares-2x2';
protected static ?string $modelLabel = 'Подразделение';
public static ?string $label = 'Подразделение'; protected static ?string $pluralModelLabel = 'Подразделения института';
protected static ?int $navigationSort = 100;
protected static ?string $pluralLabel = 'Подразделения института';
public static function form(Form $form): Form public static function form(Form $form): Form
{ {
return $form return $form
->schema([ ->schema([
Section::make() Section::make('Основные настройки')
->collapsible()
->schema([ ->schema([
Tabs::make('Tabs') Tabs::make('Конструктор подразделения')
->persistTabInQueryString()
->columnSpanFull()
->tabs([ ->tabs([
Tabs\Tab::make('Основная информация') Tabs\Tab::make('Основная информация')
->icon('heroicon-o-information-circle')
->schema([ ->schema([
Forms\Components\Grid::make(2)->schema([ Forms\Components\Grid::make(2)
TextInput::make('title')->label('Заголовок')->required() ->schema([
->live(onBlur: true) TextInput::make('title')
->afterStateUpdated(function (string $operation, string|null $state, Forms\Set $set) { ->label('Название подразделения')
$set('slug', Str::slug($state)); ->required()
}), ->maxLength(255)
TextInput::make('slug')->label('Slug')->unique(ignoreRecord: true)->readOnly()->required(), ->live(onBlur: true)
]), ->afterStateUpdated(function (string $operation, string|null $state, Forms\Set $set) {
Toggle::make('is_active')->default(true)->label('Активное подразделение')->inline(false), if ($operation === 'create') {
$set('slug', Str::slug($state));
}
})
->placeholder('Введите полное название подразделения')
->helperText('Официальное название, которое будет отображаться на сайте'),
TextInput::make('slug')
->label('URL-адрес')
->unique(ignoreRecord: true)
->required()
->readOnly()
->helperText('Формируется автоматически из названия')
->prefix(fn () => route('client.division.index') . '/')
->suffixAction(
Action::make('copy')
->icon('heroicon-s-clipboard-document-check')
->action(function ($livewire, $state) {
$livewire->js(
'window.navigator.clipboard.writeText("'. route('client.division.index') . '/' . $state.'");
$tooltip("'.__('Copied to clipboard').'", { timeout: 1500 });'
);
})),
]),
Toggle::make('is_active')
->label('Активно на сайте')
->default(true)
->inline(false)
->helperText('Отключите, чтобы временно скрыть подразделение'),
]), ]),
Tabs\Tab::make('Содержание')
Tabs\Tab::make('Контент')
->icon('heroicon-o-document-text')
->schema([ ->schema([
\Filament\Forms\Components\Builder::make('description')->label('')->blocks([ ContentBuilderItem::getItem('content')
Builder\Block::make('heading')->label('Заголовок')
->schema([
TextInput::make('id')->hidden()->integer()->default(rand(2335235,324634264263426)),
TextInput::make('content')
->label('')
->live(onBlur: true)
->afterStateUpdated(function (string $operation, string $state, Forms\Set $set, $get) {
}),
]),
Builder\Block::make('paragraph')->label('Текст')
->schema([
TinyEditor::make('content')
->label('')
->profile('test')
]),
Builder\Block::make('files')->label('Файлы')
->schema([
Forms\Components\Repeater::make('file')->schema([
TextInput::make('title')
->required()
->maxLength(255)
->autofocus(),
FileUpload::make('path')
->required()
->acceptedFileTypes([
'application/pdf',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'application/zip'
])
->maxSize(512000)
->disk('public')
->directory('files')
->downloadable()
->visibility('public')
]),
]),
Builder\Block::make('person')->label('Персона')
->schema([
TextInput::make('name')
->label('Имя')
->required()
->maxLength(255),
FileUpload::make('photo')
->label('Фотография')
->image()
->disk('public')
->directory('images')
->imageEditor(),
Forms\Components\Repeater::make('info')->schema([
Forms\Components\Grid::make(2)->schema([
TextInput::make('column')
->required()
->maxLength(255),
TextInput::make('content')
->required()
->maxLength(255),
]),
])->minItems(1),
]),
Builder\Block::make('stepper')->label('Этапы')
->schema([
TextInput::make('step_name')
->label('Название шага')
->required()
->maxLength(255),
Forms\Components\Repeater::make('steps')->schema([
TextInput::make('title')
->required()
->live()
->maxLength(255)->columnSpanFull(),
TinyEditor::make('content')
->label('')
->profile('test')
->required(), ])
->itemLabel(fn (array $state): ?string => $state['title'] ?? null)
->minItems(1)
->collapsible()
->collapsed()
]),
Builder\Block::make('tabs')->label('Вкладки')
->schema([
Forms\Components\Repeater::make('tab')->schema([
TextInput::make('title')
->required()
->maxLength(255)->columnSpanFull(),
\Filament\Forms\Components\Builder::make('content')->label('')->blocks([
Builder\Block::make('heading')->label('Заголовок')
->schema([
TextInput::make('id')->hidden()->integer()->default(rand(2335235,324634264263426)),
TextInput::make('content')
->label('')
->live(onBlur: true)
->afterStateUpdated(function (string $operation, string $state, Forms\Set $set, $get) {
}),
]),
Builder\Block::make('paragraph')
->schema([
RichEditor::make('content')
->toolbarButtons([
'blockquote',
'bold',
'bulletList',
'italic',
'link',
'orderedList',
'redo',
'strike',
'underline',
'undo',
])
->label(''),
])->label('Текст'),
Builder\Block::make('files')
->schema([
Forms\Components\Repeater::make('file')->schema([
TextInput::make('title')
->required()
->maxLength(255)
->autofocus(),
FileUpload::make('path')
->required()
->acceptedFileTypes([
'application/pdf',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'application/zip'
])
->maxSize(512000)
->disk('public')
->directory('files')
->downloadable()
->visibility('public')
]),
]),
Builder\Block::make('person')
->schema([
TextInput::make('name')
->label('Имя')
->required()
->maxLength(255),
FileUpload::make('photo')
->label('Фотография')
->image()
->disk('public')
->directory('images')
->imageEditor(),
Forms\Components\Repeater::make('info')->schema([
Forms\Components\Grid::make(2)->schema([
TextInput::make('column')
->required()
->maxLength(255),
TextInput::make('content')
->required()
->maxLength(255),
]),
])->minItems(1),
]),
Builder\Block::make('stepper')
->schema([
TextInput::make('step_name')
->label('Название шага')
->required()
->maxLength(255),
Forms\Components\Repeater::make('steps')->schema([
TextInput::make('title')
->required()
->maxLength(255)->columnSpanFull(),
RichEditor::make('content')->required(),
])->minItems(1),
]),
Builder\Block::make('images')
->schema([
FileUpload::make('url')
->label('Изображение(-я)')
->image()
->multiple()
->reorderable()
->maxFiles(5)
->disk('public')
->directory('images')
->imageEditor()
->required(),
TextInput::make('alt')
->label('Описание')
->placeholder('Необязяательно')
])->label('Слайдер изображений'),
Builder\Block::make('image')
->schema([
FileUpload::make('url')
->label('Изображение(-я)')
->image()
->multiple()
->reorderable()
->maxFiles(5)
->disk('public')
->directory('images')
->imageEditor()
->required(),
TextInput::make('alt')
->label('Описание')
->placeholder('Необязяательно')
])->label('Изображение'),
Builder\Block::make('video')
->schema([
TextInput::make('mime')->readOnly(),
TextInput::make('title')
->required()
->maxLength(255)
->autofocus(),
FileUpload::make('path')
->required()
->acceptedFileTypes([
'video/mp4',
'video/quicktime',
'video/x-msvideo',
'video/x-ms-wmv',
'video/avi',
'video/webm',
'video/ogg',
'video/3gpp',
'video/3gpp2',
'video/x-m4v',
])
->disk('public')
->directory('videos')
->afterStateUpdated(fn (callable $set, $state) => $set('mime', $state?->getMimeType())),
]),
Builder\Block::make('postsList')
->schema([
Forms\Components\Grid::make(2)->schema([
TextInput::make('count')
->label('Количество запией')
->integer(),
Select::make('category')
->options(Category::all()->pluck('title', 'id'))
]),
])->label('Список новостей'),
])
->collapsed()
->blockNumbers(false)
->collapsible()
->addActionLabel('Добавить новый блок'),
])->minItems(1),
]),
Builder\Block::make('images')->label('Слайдер изображений')
->schema([
FileUpload::make('url')
->label('Изображение(-я)')
->image()
->multiple()
->reorderable()
->maxFiles(5)
->disk('public')
->directory('images')
->imageEditor()
->required(),
TextInput::make('alt')
->label('Описание')
->placeholder('Необязяательно')
]),
Builder\Block::make('image')->label('Изображение')
->schema([
FileUpload::make('url')
->label('Изображение(-я)')
->image()
->multiple()
->reorderable()
->maxFiles(5)
->disk('public')
->directory('images')
->imageEditor()
->required(),
TextInput::make('alt')
->label('Описание')
->placeholder('Необязяательно')
]),
Builder\Block::make('video')->label('Видео')
->schema([
TextInput::make('mime')->readOnly(),
TextInput::make('title')
->required()
->maxLength(255)
->autofocus(),
FileUpload::make('path')
->required()
->acceptedFileTypes([
'video/mp4',
'video/quicktime',
'video/x-msvideo',
'video/x-ms-wmv',
'video/avi',
'video/webm',
'video/ogg',
'video/3gpp',
'video/3gpp2',
'video/x-m4v',
])
->disk('public')
->directory('videos')
->afterStateUpdated(fn (callable $set, $state) => $set('mime', $state?->getMimeType())),
]),
Builder\Block::make('postsList')->label('Список новостей')
->schema([
Forms\Components\Grid::make(2)->schema([
TextInput::make('count')
->label('Количество запией')
->integer(),
Select::make('category')
->options(Category::all()->pluck('title', 'id'))
]),
]),
Builder\Block::make('postItem')->label('Новость')
->schema([
Select::make('post')
->options(Post::query()->where('status', PostStatus::PUBLISHED)->pluck('title', 'id'))
->searchable()
->required(),
]),
Builder\Block::make('pageItem')->label('Страница')
->schema([
Select::make('page')
->options(Page::query()->where('title', '!=' , null)->where('is_visible', true)->pluck('title', 'id'))
->searchable()
->required(),
]),
])
->collapsed()
->blockNumbers(false)
->collapsible()
->blockPickerColumns(3)
->blockPickerWidth('2xl')
->addActionLabel('Добавить новый блок'),
]), ]),
]), ]),
]), ]),
]); ]);
} }
public static function table(Table $table): Table public static function table(Table $table): Table
{ {
return $table return $table
->defaultSort('created_at', 'desc')
->reorderable('order_column')
->paginated([10, 25, 50, 100])
->columns([ ->columns([
TextColumn::make('id')->label('ID')->sortable(), TextColumn::make('id')
TextColumn::make('title')->label('Название')->sortable()->searchable(), ->label('ID')
TextColumn::make('created_at')->label('Дата создания')->sortable(), ->sortable()
Tables\Columns\ToggleColumn::make('is_active')->label('Активно')->sortable(), ->toggleable(isToggledHiddenByDefault: true),
])
TextColumn::make('title')
->label('Название')
->sortable()
->searchable()
->description(fn (Division $record) => Str::limit($record->slug, 30))
->wrap(),
TextColumn::make('created_at')
->label('Дата создания')
->dateTime('d.m.Y H:i')
->sortable()
->toggleable(),
TextColumn::make('updated_at')
->label('Последнее обновление')
->dateTime('d.m.Y H:i')
->sortable()
->toggleable(isToggledHiddenByDefault: true),
Tables\Columns\ToggleColumn::make('is_active')
->label('Статус')
->sortable()
->alignCenter(),
])
->filters([ ->filters([
// Tables\Filters\Filter::make('is_active')
->label('Только активные')
->query(fn (EloquentBuilder $query) => $query->where('is_active', true))
->default(),
]) ])
->actions([ ->actions([
Tables\Actions\EditAction::make(), Tables\Actions\EditAction::make()
->iconButton()
->tooltip('Редактировать'),
]) ])
->bulkActions([ ->bulkActions([
Tables\Actions\BulkActionGroup::make([ Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(), Tables\Actions\DeleteBulkAction::make()
->label('Удалить выбранные')
->modalHeading('Подтверждение удаления')
->modalSubmitActionLabel('Да, удалить')
->modalDescription('Вы уверены, что хотите удалить выбранные подразделения? Это действие нельзя отменить.'),
Tables\Actions\ForceDeleteBulkAction::make()
->label('Принудительно удалить')
->modalHeading('Подтверждение удаления')
->modalSubmitActionLabel('Да, удалить безвозвратно')
->modalDescription('Внимание! Это действие окончательно удалит записи из базы данных.'),
Tables\Actions\RestoreBulkAction::make()
->label('Восстановить выбранные'),
]), ]),
]); ])
->emptyStateActions([
Tables\Actions\CreateAction::make()
->label('Добавить подразделение'),
])
->persistFiltersInSession()
->persistSearchInSession()
->striped();
} }
public static function getRelations(): array public static function getRelations(): array
@@ -445,4 +186,4 @@ class DivisionResource extends Resource
'edit' => Pages\EditDivision::route('/{record}/edit'), 'edit' => Pages\EditDivision::route('/{record}/edit'),
]; ];
} }
} }
@@ -3,42 +3,27 @@
namespace App\Filament\Resources\DivisionResource\Pages; namespace App\Filament\Resources\DivisionResource\Pages;
use App\Filament\Resources\DivisionResource; use App\Filament\Resources\DivisionResource;
use App\Services\Filament\Traits\SeoGenerate;
use Filament\Actions; use Filament\Actions;
use Filament\Resources\Pages\CreateRecord; use Filament\Resources\Pages\CreateRecord;
use Illuminate\Support\Str; use Illuminate\Support\Str;
class CreateDivision extends CreateRecord class CreateDivision extends CreateRecord
{ {
protected static string $resource = DivisionResource::class; use SeoGenerate;
protected array $seoData; protected static string $resource = DivisionResource::class;
protected function mutateFormDataBeforeCreate(array $data): array protected function mutateFormDataBeforeCreate(array $data): array
{ {
$this->seoData = $this->generateSeo($data);
$data['search_data'] = $this->generateSearchData($data['description']); $data['search_data'] = $this->generateSearchData($data['description']);
return $data; return $data;
} }
protected function afterCreate(): void protected function afterCreate(): void
{ {
$this->record->seo()->create($this->seoData); $this->createSeo($this->record);
}
private function generateSeo(array $data) : array
{
$title = $data['title'];
$rowData = $this->getFirstBlockByName('paragraph', $data['description']);
if ($rowData !== null) {
$description = strip_tags($rowData['data']['content']);
} else {
$description = null;
}
return [
'title' => $title,
'description' => Str::limit(htmlspecialchars($description, ENT_QUOTES, 'UTF-8'), 160),
];
} }
private function generateSearchData(array $data) : string private function generateSearchData(array $data) : string
@@ -3,20 +3,21 @@
namespace App\Filament\Resources\DivisionResource\Pages; namespace App\Filament\Resources\DivisionResource\Pages;
use App\Filament\Resources\DivisionResource; use App\Filament\Resources\DivisionResource;
use App\Services\Filament\Traits\SeoGenerate;
use Filament\Actions; use Filament\Actions;
use Filament\Resources\Pages\EditRecord; use Filament\Resources\Pages\EditRecord;
use Illuminate\Support\Str; use Illuminate\Support\Str;
class EditDivision extends EditRecord class EditDivision extends EditRecord
{ {
use SeoGenerate;
protected static string $resource = DivisionResource::class; protected static string $resource = DivisionResource::class;
protected array $seoData;
protected function mutateFormDataBeforeSave(array $data): array protected function mutateFormDataBeforeSave(array $data): array
{ {
$this->seoData = $this->generateSeo($data);
$data['search_data'] = $this->generateSearchData($data['description']); $data['search_data'] = $this->generateSearchData($data['description']);
return $data; return $data;
@@ -24,25 +25,11 @@ class EditDivision extends EditRecord
protected function afterSave(): void protected function afterSave(): void
{ {
$this->record->seo()->update($this->seoData); $this->updateSeo($this->record);
} }
private function generateSeo(array $data) : array
{
$title = $data['title'];
$rowData = $this->getFirstBlockByName('paragraph', $data['description']);
if ($rowData !== null) {
$description = strip_tags($rowData['data']['content']);
} else {
$description = null;
}
return [
'title' => $title,
'description' => Str::limit(htmlspecialchars($description, ENT_QUOTES, 'UTF-8'), 160),
];
}
private function getDataFromBlocks($block) : string private function getDataFromBlocks($block) : string
{ {
@@ -14,17 +14,48 @@ use Illuminate\Database\Eloquent\SoftDeletingScope;
class WorkersRelationManager extends RelationManager class WorkersRelationManager extends RelationManager
{ {
protected static string $relationship = 'workers'; protected static string $relationship = 'workers';
protected static ?string $title = 'Сотрудники подразделения';
protected static ?string $title = 'Сотрудники'; protected static ?string $inverseRelationship = 'divisions';
public function form(Form $form): Form public function form(Form $form): Form
{ {
return $form return $form
->schema([ ->schema([
Forms\Components\TextInput::make('administrativePosition')->label('Должность')->required(), Forms\Components\Section::make('Служебная информация')
Forms\Components\TextInput::make('service_email')->label('Служебная почта'), ->description('Данные о сотруднике в рамках подразделения')
Forms\Components\TextInput::make('service_phone')->label('Служебный телефон'), ->schema([
Forms\Components\TextInput::make('cabinet')->label('Кабинет'), Forms\Components\TextInput::make('administrativePosition')
->label('Административная должность')
->required()
->maxLength(255)
->placeholder('Например: Руководитель отдела')
->helperText('Официальная должность в подразделении'),
Forms\Components\TextInput::make('service_email')
->label('Служебная почта')
->email()
->maxLength(255)
->placeholder('example@university.edu')
->helperText('Корпоративная электронная почта в подразделении'),
Forms\Components\TextInput::make('service_phone')
->label('Служебный телефон')
->tel()
->maxLength(20)
->placeholder('+7 (XXX) XXX-XX-XX')
->helperText('Формат: +7 (XXX) XXX-XX-XX')
->regex('/^\+?[0-9\s\-\(\)]{7,}$/')
->validationMessages([
'regex' => 'Пожалуйста, введите корректный номер телефона',
]),
Forms\Components\TextInput::make('cabinet')
->label('Кабинет')
->maxLength(10)
->placeholder('Например: 305а')
->helperText('Номер кабинета в подразделении'),
])
->columns(2),
]); ]);
} }
@@ -35,32 +66,114 @@ class WorkersRelationManager extends RelationManager
->reorderable('sort') ->reorderable('sort')
->defaultSort('sort') ->defaultSort('sort')
->columns([ ->columns([
Tables\Columns\TextColumn::make('name'), Tables\Columns\TextColumn::make('sort')
Tables\Columns\TextColumn::make('administrativePosition'), ->label('№')
->sortable()
->toggleable(isToggledHiddenByDefault: true),
Tables\Columns\TextColumn::make('name')
->label('ФИО')
->searchable()
->sortable()
->weight('medium')
->description(fn ($record) => $record->service_email),
Tables\Columns\TextColumn::make('administrativePosition')
->label('Должность')
->searchable()
->wrap()
->description(fn ($record) => $record->cabinet),
Tables\Columns\TextColumn::make('service_phone')
->label('Телефон')
->searchable()
->toggleable(),
]) ])
->filters([ ->filters([
//
]) ])
->headerActions([ ->headerActions([
AttachAction::make() AttachAction::make()
->label('Добавить сотрудника')
->modalHeading('Добавление сотрудника')
->modalSubmitActionLabel('Добавить')
->preloadRecordSelect() ->preloadRecordSelect()
->recordSelectOptionsQuery(fn (Builder $query) => $query->has('userDetail'))
->form(fn (AttachAction $action): array => [ ->form(fn (AttachAction $action): array => [
$action->getRecordSelect(), Forms\Components\Section::make()
Forms\Components\TextInput::make('administrativePosition')->label('Должность')->required(), ->schema([
Forms\Components\TextInput::make('service_email')->label('Служебная почта'), $action->getRecordSelect()
Forms\Components\TextInput::make('service_phone')->label('Служебный телефон'), ->label('Сотрудник')
Forms\Components\TextInput::make('cabinet')->label('Кабинет'), ->placeholder('Выберите сотрудника')
]) ->searchable()
->preload()
->required()
->columnSpanFull(),
Forms\Components\TextInput::make('administrativePosition')
->label('Административная должность')
->required()
->columnSpanFull()
->maxLength(255)
->placeholder('Например: Руководитель отдела')
->helperText('Официальная должность в подразделении'),
Forms\Components\TextInput::make('service_email')
->label('Служебная почта')
->email()
->columnSpanFull()
->maxLength(255)
->placeholder('example@university.edu')
->helperText('Корпоративная электронная почта в подразделении'),
Forms\Components\TextInput::make('service_phone')
->label('Служебный телефон')
->tel()
->columnSpanFull()
->maxLength(20)
->placeholder('+7 (XXX) XXX-XX-XX')
->helperText('Формат: +7 (XXX) XXX-XX-XX')
->regex('/^\+?[0-9\s\-\(\)]{7,}$/')
->validationMessages([
'regex' => 'Пожалуйста, введите корректный номер телефона',
]),
Forms\Components\TextInput::make('cabinet')
->label('Кабинет')
->columnSpanFull()
->maxLength(10)
->placeholder('Например: 305а')
->helperText('Номер кабинета в подразделении'),
])
->columns(2),
]),
]) ])
->actions([ ->actions([
Tables\Actions\EditAction::make(), Tables\Actions\EditAction::make()
Tables\Actions\DetachAction::make(), ->iconButton()
->tooltip('Редактировать данные сотрудника'),
Tables\Actions\DetachAction::make()
->iconButton()
->tooltip('Убрать из подразделения')
->modalHeading('Подтверждение удаления')
->modalSubmitActionLabel('Убрать')
->modalDescription('Вы уверены, что хотите убрать этого сотрудника из подразделения?'),
]) ])
->bulkActions([ ->bulkActions([
Tables\Actions\BulkActionGroup::make([ Tables\Actions\BulkActionGroup::make([
Tables\Actions\DetachBulkAction::make(), Tables\Actions\DetachBulkAction::make()
->label('Убрать выбранных')
->modalHeading('Подтверждение удаления')
->modalSubmitActionLabel('Убрать')
->modalDescription('Вы уверены, что хотите убрать выбранных сотрудников из подразделения?'),
]), ]),
]); ])
->emptyStateActions([
AttachAction::make()
->label('Добавить сотрудника'),
])
->persistFiltersInSession()
->paginated([10, 25, 50, 100])
->striped();
} }
} }
@@ -4,41 +4,64 @@ namespace App\Filament\Resources;
use App\Enums\FormEducation; use App\Enums\FormEducation;
use App\Filament\Resources\EducationalGroupResource\Pages; use App\Filament\Resources\EducationalGroupResource\Pages;
use App\Filament\Resources\EducationalGroupResource\RelationManagers;
use App\Models\EducationalGroup; use App\Models\EducationalGroup;
use App\Models\Faculty; use App\Models\Faculty;
use Filament\Forms; use Filament\Forms;
use Filament\Forms\Components\Grid;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form; use Filament\Forms\Form;
use Filament\Resources\Resource; use Filament\Resources\Resource;
use Filament\Tables; use Filament\Tables;
use Filament\Tables\Columns\BadgeColumn;
use Filament\Tables\Columns\TextColumn; use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table; use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletingScope;
class EducationalGroupResource extends Resource class EducationalGroupResource extends Resource
{ {
protected static ?string $navigationGroup = 'Расписание и группы'; protected static ?string $navigationGroup = 'Расписание и группы';
protected static ?string $model = EducationalGroup::class; protected static ?string $model = EducationalGroup::class;
protected static ?string $pluralLabel = 'Учебные группы';
protected static ?string $pluralLabel = 'Группы'; protected static ?string $modelLabel = 'учебная группа';
protected static ?string $navigationIcon = 'heroicon-o-user-group'; protected static ?string $navigationIcon = 'heroicon-o-user-group';
public static function form(Form $form): Form public static function form(Form $form): Form
{ {
return $form return $form
->schema([ ->schema([
Forms\Components\Section::make()->schema([ Section::make('Основная информация')
Forms\Components\Grid::make(2)->schema([ ->description('Заполните основные данные о группе')
Forms\Components\TextInput::make('title')->label('Название группы')->required(), ->collapsible()
Forms\Components\Select::make('faculty_id')->label('Факультет')->required() ->schema([
->options(Faculty::all()->pluck('title', 'id')), Grid::make(2)
Forms\Components\Select::make('education_form_id')->label('Форма обучения') ->schema([
->options(FormEducation::class) TextInput::make('title')
->label('Название группы')
->required()
->maxLength(50)
->placeholder('Например: ИВТ-21-1')
->helperText('Введите краткое название группы в принятом формате'),
Select::make('faculty_id')
->label('Факультет')
->required()
->options(Faculty::query()->orderBy('title')->pluck('title', 'id'))
->searchable()
->preload()
->placeholder('Выберите факультет')
->helperText('Выберите факультет, к которому относится группа'),
Select::make('education_form_id')
->label('Форма обучения')
->required()
->options(FormEducation::class)
->native(false)
->placeholder('Выберите форму обучения')
->helperText('Выберите форму обучения для группы'),
]),
]), ]),
]),
]); ]);
} }
@@ -46,29 +69,71 @@ class EducationalGroupResource extends Resource
{ {
return $table return $table
->columns([ ->columns([
TextColumn::make('id')->label('ID')->sortable(), TextColumn::make('id')
TextColumn::make('title')->label('Название')->sortable()->searchable(), ->label('ID')
TextColumn::make('created_at')->label('Дата создания')->sortable(), ->sortable()
Tables\Columns\BadgeColumn::make('faculty.title')->label('Категория')->sortable(), ->searchable()
->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('title')
->label('Название группы')
->sortable()
->searchable()
->description(fn ($record) => $record->faculty->title ?? ''),
BadgeColumn::make('education_form_id')
->label('Форма обучения')
->formatStateUsing(fn ($state) => FormEducation::tryFrom($state)?->label())
->color(fn ($state) => match($state) {
FormEducation::FULL_TIME->value => 'success',
FormEducation::PART_TIME->value => 'warning',
default => 'gray',
})
->sortable(),
TextColumn::make('created_at')
->label('Дата создания')
->dateTime('d.m.Y')
->sortable()
->toggleable(),
]) ])
->filters([ ->filters([
// Tables\Filters\SelectFilter::make('faculty_id')
->label('Факультет')
->options(Faculty::query()->orderBy('title')->pluck('title', 'id'))
->searchable(),
Tables\Filters\SelectFilter::make('education_form_id')
->label('Форма обучения')
->options(FormEducation::class),
]) ])
->actions([ ->actions([
Tables\Actions\EditAction::make(), Tables\Actions\EditAction::make()
->iconButton()
->tooltip('Редактировать'),
Tables\Actions\ViewAction::make()
->iconButton()
->tooltip('Просмотреть'),
]) ])
->bulkActions([ ->bulkActions([
Tables\Actions\BulkActionGroup::make([ Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(), Tables\Actions\DeleteBulkAction::make()
->label('Удалить выбранные')
->modalHeading('Удаление групп')
->modalDescription('Вы уверены, что хотите удалить выбранные учебные группы?'),
]), ]),
]); ])
->emptyStateActions([
Tables\Actions\CreateAction::make()
->label('Добавить группу'),
])
->defaultSort('title');
} }
public static function getRelations(): array public static function getRelations(): array
{ {
return [ return [];
//
];
} }
public static function getPages(): array public static function getPages(): array
@@ -79,4 +144,4 @@ class EducationalGroupResource extends Resource
'edit' => Pages\EditEducationalGroup::route('/{record}/edit'), 'edit' => Pages\EditEducationalGroup::route('/{record}/edit'),
]; ];
} }
} }
@@ -4,263 +4,167 @@ namespace App\Filament\Resources;
use App\Enums\EducationalProgramStatus; use App\Enums\EducationalProgramStatus;
use App\Enums\LevelEducational; use App\Enums\LevelEducational;
use App\Filament\Components\Forms\ItemForm\Pages\ContentBuilderItem;
use App\Filament\Resources\EducationalProgramResource\Pages; use App\Filament\Resources\EducationalProgramResource\Pages;
use App\Filament\Resources\EducationalProgramResource\RelationManagers;
use App\Filament\Resources\EducationalProgramResource\RelationManagers\AdmissionPlansRelationManager; use App\Filament\Resources\EducationalProgramResource\RelationManagers\AdmissionPlansRelationManager;
use App\Models\Category;
use App\Models\EducationalProgram; use App\Models\EducationalProgram;
use Filament\Forms; use Filament\Forms;
use Filament\Forms\Components\Builder; use Filament\Forms\Components\Builder;
use Filament\Forms\Components\FileUpload; use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Grid;
use Filament\Forms\Components\Hidden; use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\RichEditor; use Filament\Forms\Components\RichEditor;
use Filament\Forms\Components\Section; use Filament\Forms\Components\Section;
use Filament\Forms\Components\Select; use Filament\Forms\Components\Select;
use Filament\Forms\Components\SpatieTagsInput; use Filament\Forms\Components\Tabs;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Forms\Form; use Filament\Forms\Form;
use Filament\Resources\Resource; use Filament\Resources\Resource;
use Filament\Tables; use Filament\Tables;
use Filament\Tables\Columns\BadgeColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table; use Filament\Tables\Table;
use Illuminate\Database\Eloquent\SoftDeletingScope;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Mohamedsabil83\FilamentFormsTinyeditor\Components\TinyEditor;
class EducationalProgramResource extends Resource class EducationalProgramResource extends Resource
{ {
protected static ?string $model = EducationalProgram::class; protected static ?string $model = EducationalProgram::class;
protected static ?string $navigationIcon = 'heroicon-o-academic-cap';
protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack';
protected static ?string $navigationGroup = 'Образование'; protected static ?string $navigationGroup = 'Образование';
protected static ?string $pluralLabel = 'Образовательные программы'; protected static ?string $pluralLabel = 'Образовательные программы';
protected static ?string $modelLabel = 'Образовательная программа';
protected static ?string $navigationParentItem = 'Приемная-компания'; protected static ?string $navigationParentItem = 'Приемная-компания';
public static function form(Form $form): Form public static function form(Form $form): Form
{ {
return $form return $form
->schema([ ->schema([
Section::make() Tabs::make('')
->schema([ ->tabs([
TextInput::make('name')->label('Название')->required(), Tabs\Tab::make('Основная информация')
Section::make('О программе')->schema([ ->icon('heroicon-o-information-circle')
Builder::make('about_program')->label('')->blocks([ ->schema([
Builder\Block::make('heading')->label('Заголовок') TextInput::make('name')
->label('Название программы')
->required()
->maxLength(255)
->placeholder('Введите полное название программы')
->columnSpanFull()
->helperText('Официальное название программы как в лицензии'),
Grid::make(2)
->schema([ ->schema([
TextInput::make('id')->hidden()->integer()->default(rand(2335235,324634264263426)), Select::make('lvl_edu')
TextInput::make('content') ->label('Уровень образования')
->label('') ->options(LevelEducational::class)
->live(onBlur: true) ->required()
->afterStateUpdated(function (string $operation, string $state, Forms\Set $set, $get) { ->native(false)
}), ->helperText('Выберите уровень образовательной программы'),
Select::make('status')
->label('Статус программы')
->options(EducationalProgramStatus::class)
->required()
->native(false)
->helperText('Определяет видимость программы на сайте'),
TextInput::make('lang_stud')
->label('Язык обучения')
->required()
->placeholder('Например: русский, английский')
->helperText('Укажите основной язык преподавания')
->columnSpan(2),
]), ]),
Builder\Block::make('paragraph') ]),
->schema([
RichEditor::make('content')
->toolbarButtons([
'blockquote',
'bold',
'bulletList',
'italic',
'link',
'orderedList',
'redo',
'strike',
'underline',
'undo',
])
->label('')
->required()
]),
Builder\Block::make('image')
->schema([
FileUpload::make('url')
->label('Изображение(-я)')
->image()
->multiple()
->reorderable()
->maxFiles(5)
->disk('public')
->directory('images')
->imageEditor()
->required(),
TextInput::make('alt')
->label('Описание')
->placeholder('Необязяательно')
])->label('Изображение(-я)'),
Builder\Block::make('video')
->schema([
Hidden::make('mime'),
TextInput::make('title') Tabs\Tab::make('Описание программы')
->required() ->icon('heroicon-o-document-text')
->maxLength(255) ->schema([
->autofocus(), self::getContentBuilder('about_program', 'О программе')
->columnSpanFull(),
FileUpload::make('path') ]),
->required()
->acceptedFileTypes(['video/mp4','video/ogg','video/webm'])
->maxSize(512000)
->disk('videos')
->visibility('public')
->afterStateUpdated(fn (callable $set, $state) => $set('mime', $state?->getMimeType())),
]),
Builder\Block::make('files')
->schema([
TextInput::make('title')
->required()
->maxLength(255)
->autofocus(),
FileUpload::make('path')
->required()
->acceptedFileTypes([
'application/pdf',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'application/zip'
])
->maxSize(512000)
->disk('public')
->directory('files')
->downloadable()
->visibility('public')
])
])
->collapsed()
->blockNumbers(false)
->collapsible()
->addActionLabel('Добавить новый блок'),
]),
Section::make('Особенности программы')->schema([
Builder::make('program_features')->label('')->blocks([
Builder\Block::make('heading')->label('Заголовок')
->schema([
TextInput::make('id')->hidden()->integer()->default(rand(2335235,324634264263426)),
TextInput::make('content')
->label('')
->live(onBlur: true)
->afterStateUpdated(function (string $operation, string $state, Forms\Set $set, $get) {
}),
]),
Builder\Block::make('paragraph')
->schema([
RichEditor::make('content')
->toolbarButtons([
'blockquote',
'bold',
'bulletList',
'italic',
'link',
'orderedList',
'redo',
'strike',
'underline',
'undo',
])
->label('')
->required()
]),
Builder\Block::make('image')
->schema([
FileUpload::make('url')
->label('Изображение(-я)')
->image()
->multiple()
->reorderable()
->maxFiles(5)
->disk('public')
->directory('images')
->imageEditor()
->required(),
TextInput::make('alt')
->label('Описание')
->placeholder('Необязяательно')
])->label('Изображение(-я)'),
Builder\Block::make('video')
->schema([
Hidden::make('mime'),
TextInput::make('title')
->required()
->maxLength(255)
->autofocus(),
FileUpload::make('path')
->required()
->acceptedFileTypes(['video/mp4','video/ogg','video/webm'])
->maxSize(512000)
->disk('videos')
->visibility('public')
->afterStateUpdated(fn (callable $set, $state) => $set('mime', $state?->getMimeType())),
]),
Builder\Block::make('files')
->schema([
TextInput::make('title')
->required()
->maxLength(255)
->autofocus(),
FileUpload::make('path')
->required()
->acceptedFileTypes([
'application/pdf',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'application/zip'
])
->maxSize(512000)
->disk('public')
->directory('files')
->downloadable()
->visibility('public')
])
])
->collapsed()
->blockNumbers(false)
->collapsible()
->addActionLabel('Добавить новый блок'),
]),
Select::make('lvl_edu')->options(LevelEducational::class)->label('Уровень образования')->required(),
Select::make('status')
->options(EducationalProgramStatus::class)
->label('Статус программы')->required(),
TextInput::make('lang_stud')->label('На каком языке ведется образование')->required(),
Tabs\Tab::make('Особенности программы')
->icon('heroicon-o-sparkles')
->schema([
self::getContentBuilder('program_features', 'Особенности программы')
->columnSpanFull(),
]),
]) ])
->persistTabInQueryString()
->columnSpanFull(),
]); ]);
}
protected static function getContentBuilder(string $field, string $label): Builder
{
return ContentBuilderItem::getItem($field);
} }
public static function table(Table $table): Table public static function table(Table $table): Table
{ {
return $table return $table
->columns([ ->columns([
Tables\Columns\TextColumn::make('name')->label('Название программы')->sortable()->searchable(), TextColumn::make('name')
Tables\Columns\TextColumn::make('directionStudy.lvl_edu')->label('Уровень образования')->limit(30), ->label('Название')
->sortable()
->searchable()
->description(fn ($record) => $record->lang_stud),
BadgeColumn::make('lvl_edu')
->label('Уровень')
->formatStateUsing(fn ($state) => LevelEducational::tryFrom($state->value)?->getLabel())
->color(fn ($state) => LevelEducational::tryFrom($state->value)?->getColor())
->sortable(),
BadgeColumn::make('status')
->label('Статус')
->formatStateUsing(fn ($state) => EducationalProgramStatus::tryFrom($state)?->getLabel())
->color(fn ($state) => EducationalProgramStatus::tryFrom($state)?->getColor())
->sortable(),
TextColumn::make('updated_at')
->label('Обновлено')
->dateTime('d.m.Y H:i')
->sortable()
->toggleable(isToggledHiddenByDefault: true),
]) ])
->filters([ ->filters([
// Tables\Filters\SelectFilter::make('lvl_edu')
->label('Уровень образования')
->options(LevelEducational::class),
Tables\Filters\SelectFilter::make('status')
->label('Статус программы')
->options(EducationalProgramStatus::class),
]) ])
->actions([ ->actions([
Tables\Actions\EditAction::make(), Tables\Actions\EditAction::make()
->iconButton()
->tooltip('Редактировать'),
Tables\Actions\ViewAction::make()
->iconButton()
->tooltip('Просмотреть'),
]) ])
->bulkActions([ ->bulkActions([
Tables\Actions\BulkActionGroup::make([ Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(), Tables\Actions\DeleteBulkAction::make()
->label('Удалить выбранные')
->modalHeading('Удаление программ')
->modalDescription('Вы уверены, что хотите удалить выбранные программы?'),
]), ]),
]); ])
->emptyStateActions([
Tables\Actions\CreateAction::make()
->label('Добавить программу'),
])
->defaultSort('name');
} }
public static function getRelations(): array public static function getRelations(): array
{ {
return [ return [
AdmissionPlansRelationManager::class AdmissionPlansRelationManager::class,
]; ];
} }
@@ -272,4 +176,4 @@ class EducationalProgramResource extends Resource
'edit' => Pages\EditEducationalProgram::route('/{record}/edit'), 'edit' => Pages\EditEducationalProgram::route('/{record}/edit'),
]; ];
} }
} }
@@ -3,89 +3,191 @@
namespace App\Filament\Resources\EducationalProgramResource\RelationManagers; namespace App\Filament\Resources\EducationalProgramResource\RelationManagers;
use App\Enums\BudgetEducation; use App\Enums\BudgetEducation;
use App\Enums\EducationalProgramStatus;
use App\Enums\FormEducation; use App\Enums\FormEducation;
use App\Models\AdmissionCampaign; use App\Models\AdmissionCampaign;
use App\Models\EducationalProgram;
use Filament\Forms; use Filament\Forms;
use Filament\Forms\Components\Grid;
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\Section; use Filament\Forms\Components\Section;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Forms\Form; use Filament\Forms\Form;
use Filament\Forms\Get;
use Filament\Resources\RelationManagers\RelationManager; use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables; use Filament\Tables;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table; use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletingScope;
class AdmissionPlansRelationManager extends RelationManager class AdmissionPlansRelationManager extends RelationManager
{ {
protected static string $relationship = 'admission_plans'; protected static string $relationship = 'admission_plans';
protected static ?string $title = 'Планы приема';
protected static ?string $modelLabel = 'план приема';
protected static ?string $pluralModelLabel = 'планы приема';
public function form(Form $form): Form public function form(Form $form): Form
{ {
return $form return $form
->schema([ ->schema([
Forms\Components\Select::make('admission_campaigns_id') Select::make('admission_campaigns_id')
->label('Приемная компания') ->label('Приемная кампания')
->required() ->required()
->options(AdmissionCampaign::all()->pluck('name', 'id')), ->columnSpanFull()
Section::make('План приема')->schema([
Forms\Components\Repeater::make('exams')->label('Вступительные испытания')->schema([
TextInput::make('title')->label('Название-предмета')->required(),
Forms\Components\Select::make('type_exam')->label('Тип-ВИ')
->options(['ege' => 'ЕГЭ', 'internal_test' => 'ВИ, проводимое организацией самостоятельно'])->required(),
TextInput::make('min_score')->label('Минимальный-балл')->integer()->required()
])->live()->maxItems(10)->collapsed()->addActionLabel('Добавить вступительное испытание')->columns(3)->required() ->itemLabel(function (Get $get) {
static $count = 0;
$maxCount = count($get('exams'));
$count = ($count++ <= $maxCount) ? $count : 1;
return "Вступительное испытание #" . $count;
}),
Forms\Components\Repeater::make('contests')->label('Условия поступления')->schema([
Forms\Components\Grid::make(1)->schema([
Forms\Components\Select::make('form_education')->label('Форма образования')
->options(FormEducation::class)->required(),
]),
Forms\Components\Repeater::make('places')->schema([
Forms\Components\Select::make('form_budget')->label('Форма финансирования')
->options(BudgetEducation::class)->required(),
TextInput::make('count')->label('Количество мест')->integer()->required(),
])->columnSpanFull()->maxItems(2),
])->live()->maxItems(3)->collapsed()->addActionLabel('Добавить группу')->columns(3)->required()
->itemLabel(function (Get $get) {
static $count = 0;
$maxCount = count($get('contests'));
$count = ($count++ <= $maxCount) ? $count : 1;
return "Группа #" . $count;
}),
]), ->options(
AdmissionCampaign::query()
->orderBy('name')
->pluck('name', 'id')
)
->searchable()
->preload()
->placeholder('Выберите приемную кампанию')
->helperText('Выберите связанную приемную кампанию'),
Section::make('План приема')
->description('Настройка вступительных испытаний и условий поступления')
->collapsible()
->schema([
self::getExamsRepeater(),
self::getContestsRepeater(),
]),
]); ]);
} }
protected static function getExamsRepeater(): Repeater
{
return Repeater::make('exams')
->label('Вступительные испытания')
->schema([
TextInput::make('title')
->label('Название предмета')
->required()
->maxLength(100)
->placeholder('Например: Математика')
->helperText('Название вступительного испытания'),
Select::make('type_exam')
->label('Тип испытания')
->required()
->options([
'ege' => 'ЕГЭ',
'internal_test' => 'Внутреннее испытание',
])
->native(false)
->placeholder('Выберите тип')
->helperText('Тип вступительного испытания'),
TextInput::make('min_score')
->label('Минимальный балл')
->required()
->numeric()
->minValue(0)
->maxValue(100)
->placeholder('Укажите минимальный балл')
->helperText('Минимальный проходной балл'),
])
->columns(3)
->maxItems(10)
->collapsible()
->collapsed()
->addActionLabel('Добавить испытание')
->itemLabel(fn (array $state): ?string => $state['title'] ?? 'Новое испытание')
->helperText('Добавьте все необходимые вступительные испытания');
}
protected static function getContestsRepeater(): Repeater
{
return Repeater::make('contests')
->label('Условия поступления')
->schema([
Select::make('form_education')
->label('Форма обучения')
->options(FormEducation::class)
->required()
->native(false)
->placeholder('Выберите форму')
->columnSpanFull()
->helperText('Форма обучения для данной группы'),
Repeater::make('places')
->label('Места')
->schema([
Select::make('form_budget')
->label('Форма финансирования')
->options(BudgetEducation::class)
->required()
->native(false)
->placeholder('Выберите тип')
->helperText('Бюджетные или платные места'),
TextInput::make('count')
->label('Количество мест')
->required()
->numeric()
->minValue(0)
->placeholder('Укажите количество')
->helperText('Количество доступных мест'),
])
->columnSpanFull()
->maxItems(2)
->addActionLabel('Добавить тип мест')
])
->columns(2)
->maxItems(3)
->collapsible()
->collapsed()
->addActionLabel('Добавить группу')
->helperText('Добавьте группы с условиями поступления');
}
public function table(Table $table): Table public function table(Table $table): Table
{ {
return $table return $table
->recordTitleAttribute('name') ->recordTitleAttribute('name')
->columns([ ->columns([
Tables\Columns\TextColumn::make('admissionCampaign.name'), TextColumn::make('admissionCampaign.name')
->label('Приемная кампания')
->sortable()
->searchable(),
TextColumn::make('exams_count')
->label('Испытаний')
->getStateUsing(fn ($record) => count($record->exams ?? []))
->badge(),
TextColumn::make('contests_count')
->label('Групп')
->getStateUsing(fn ($record) => count($record->contests ?? []))
->badge(),
]) ])
->filters([ ->filters([
// //
]) ])
->headerActions([ ->headerActions([
Tables\Actions\CreateAction::make(), Tables\Actions\CreateAction::make()
->label('Добавить план')
->modalHeading('Создание плана приема'),
]) ])
->actions([ ->actions([
Tables\Actions\EditAction::make(), Tables\Actions\EditAction::make()
Tables\Actions\DeleteAction::make(), ->iconButton()
->tooltip('Редактировать'),
Tables\Actions\DeleteAction::make()
->iconButton()
->tooltip('Удалить'),
]) ])
->bulkActions([ ->bulkActions([
Tables\Actions\BulkActionGroup::make([ Tables\Actions\BulkActionGroup::make([
Tables\Actions\DetachBulkAction::make(), Tables\Actions\DeleteBulkAction::make()
->label('Удалить выбранные')
->modalHeading('Удаление планов приема')
->modalDescription('Вы уверены, что хотите удалить выбранные планы?'),
]), ]),
]); ])
->emptyStateActions([
Tables\Actions\CreateAction::make()
->label('Добавить план приема'),
])
->defaultSort('admissionCampaign.name');
} }
} }
+152 -132
View File
@@ -2,191 +2,211 @@
namespace App\Filament\Resources; namespace App\Filament\Resources;
use App\Filament\Components\Forms\ItemForm\Pages\ContentBuilderItem;
use App\Filament\Resources\EventResource\Pages; use App\Filament\Resources\EventResource\Pages;
use App\Filament\Resources\EventResource\RelationManagers;
use App\Models\Category;
use App\Models\EventCategory;
use Filament\Forms\Components\Builder;
use App\Models\Event; use App\Models\Event;
use App\Models\EventCategory;
use Filament\Forms; use Filament\Forms;
use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Components\DatePicker; use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\DateTimePicker; use Filament\Forms\Components\Grid;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\RichEditor;
use Filament\Forms\Components\Section; use Filament\Forms\Components\Section;
use Filament\Forms\Components\Select; use Filament\Forms\Components\Select;
use Filament\Forms\Components\SpatieTagsInput; use Filament\Forms\Components\SpatieTagsInput;
use Filament\Forms\Components\Tabs;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\TimePicker;
use Filament\Forms\Components\Toggle; use Filament\Forms\Components\Toggle;
use Filament\Forms\Form; use Filament\Forms\Form;
use Filament\Resources\Resource; use Filament\Resources\Resource;
use Filament\Tables; use Filament\Tables;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn; use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table; use Filament\Tables\Table;
use Illuminate\Database\Eloquent\SoftDeletingScope;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Mohamedsabil83\FilamentFormsTinyeditor\Components\TinyEditor;
class EventResource extends Resource class EventResource extends Resource
{ {
protected static ?string $model = Event::class; protected static ?string $model = Event::class;
protected static ?string $navigationGroup = 'Новости и мероприятия'; protected static ?string $navigationGroup = 'Новости и мероприятия';
protected static ?string $navigationIcon = 'heroicon-o-calendar-days'; protected static ?string $navigationIcon = 'heroicon-o-calendar-days';
protected static ?string $modelLabel = 'Мероприятие';
protected static ?string $pluralLabel = 'Мероприятия'; protected static ?string $pluralModelLabel = 'Мероприятия';
protected static ?string $navigationLabel = 'Мероприятия';
public static function form(Form $form): Form public static function form(Form $form): Form
{ {
return $form return $form
->schema([ ->schema([
Section::make() Tabs::make('Мероприятие')
->schema([ ->tabs([
Forms\Components\Grid::make(2)->schema([ Tabs\Tab::make('Основное')
TextInput::make('title')->label('Заголовок')->required() ->icon('heroicon-o-information-circle')
->live(onBlur: true) ->schema([
->afterStateUpdated(function (string $operation, $state, Forms\Set $set) { Grid::make(2)
if ($state !== null) {
$set('slug', Str::slug($state));
} else {
$set('slug', null);
}
}),
TextInput::make('slug')->label('Slug (Заполнится автоматически)')->unique(ignoreRecord: true)->readOnly()->required(),
]),
Section::make('Контент')->schema([
\Filament\Forms\Components\Builder::make('content')->label('')->blocks([
Builder\Block::make('heading')->label('Заголовок')
->schema([ ->schema([
TextInput::make('id')->hidden()->integer()->default(rand(2335235,324634264263426)), TextInput::make('title')
TextInput::make('content') ->label('Название мероприятия')
->label('') ->placeholder('Введите название мероприятия')
->helperText('Отображается на сайте')
->required()
->maxLength(255)
->live(onBlur: true) ->live(onBlur: true)
->afterStateUpdated(function (string $operation, string $state, Forms\Set $set, $get) { ->afterStateUpdated(function (string $operation, $state, Forms\Set $set) {
$set('slug', $state ? Str::slug($state) : null);
}), }),
TextInput::make('slug')
->label('URL-адрес')
->unique(ignoreRecord: true)
->required()
->readOnly()
->helperText('Формируется автоматически из названия')
->prefix(fn () => route('client.event.index') . '/')
->suffixAction(
Action::make('copy')
->icon('heroicon-s-clipboard-document-check')
->action(function ($livewire, $state) {
$livewire->js(
'window.navigator.clipboard.writeText("'. route('client.event.index') . '/' . $state.'");
$tooltip("'.__('Copied to clipboard').'", { timeout: 1500 });'
);
})),
]), ]),
Builder\Block::make('paragraph')
->schema([
TinyEditor::make('content')
->label('')
->profile('test')
->required(),
])->label('Текст'),
Builder\Block::make('image')
->schema([
FileUpload::make('url')
->label('Изображение(-я)')
->image()
->optimize('jpg')
->resize(30)
->multiple()
->reorderable()
->maxFiles(5)
->disk('public')
->directory('images')
->imageEditor()
->required(),
TextInput::make('alt')
->label('Описание')
->placeholder('Необязяательно')
])->label('Изображение(-я)'),
Builder\Block::make('video')
->schema([
Hidden::make('mime'),
TextInput::make('title') Select::make('category_id')
->required() ->label('Категория')
->maxLength(255) ->placeholder('Выберите категорию')
->autofocus(), ->options(EventCategory::all()->pluck('title', 'id'))
->preload()
->helperText('Для систематизации мероприятий'),
FileUpload::make('path') SpatieTagsInput::make('tags')
->required() ->label('Теги')
->acceptedFileTypes(['video/mp4','video/ogg','video/webm']) ->placeholder('Добавьте теги')
->maxSize(512000) ->helperText('Для фильтрации и поиска'),
->disk('videos')
->visibility('public')
->afterStateUpdated(fn (callable $set, $state) => $set('mime', $state?->getMimeType())),
]),
Builder\Block::make('files')
->schema([
TextInput::make('title')
->required()
->maxLength(255)
->autofocus(),
FileUpload::make('path')
->required()
->acceptedFileTypes([
'application/pdf',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'application/zip'
])
->maxSize(512000)
->disk('public')
->directory('files')
->downloadable()
->visibility('public')
])
])
->collapsed()
->blockNumbers(false)
->collapsible()
->addActionLabel('Добавить новый блок'),
]), Toggle::make('is_online')
TextInput::make('address')->label('Адрес')->required(), ->label('Онлайн-формат')
->helperText('Отметьте для онлайн-мероприятий')
Forms\Components\Grid::make(2)->schema([ ->default(false)
Forms\Components\Grid::make(2)->schema([ ->inline(false)
DatePicker::make('event_date_start')->label('Дата начала мероприятия')->required()->native(false) ->onColor('success')
->minDate(now()) ->offColor('gray'),
->maxDate(now()->addYear()),
Forms\Components\TimePicker::make('event_time_start')->label('Время начала мероприятия')->seconds(false)->required()->native(false),
DatePicker::make('event_date_end')->label('Дата окончания мероприятия (Опиционально)')->native(false)
->minDate(now())
->maxDate(now()->addYear()),
]), ]),
Toggle::make('is_online')->default(false)->label('Онлайн мероприятие')->inline(false) Tabs\Tab::make('Контент')
->icon('heroicon-o-document-text')
->schema([
ContentBuilderItem::getItem('content')
->columnSpanFull(),
]),
]), Tabs\Tab::make('Дата и место')
->icon('heroicon-o-map-pin')
->schema([
TextInput::make('address')
->label('Место проведения')
->placeholder('Адрес или платформа')
->helperText('Для онлайн укажите платформу (Zoom, YouTube и т.д.)')
->required()
->maxLength(255),
Forms\Components\Grid::make(2)->schema([ Grid::make(2)
Select::make('category_id') ->schema([
->options(EventCategory::all()->pluck('title', 'id')) DatePicker::make('event_date_start')
->preload() ->label('Дата начала')
->label('Категория'), ->native(false)
SpatieTagsInput::make('tags')->label('Тэги'), ->displayFormat('d/m/Y')
]), ->helperText('Когда начинается мероприятие')
->required()
->minDate(now())
->maxDate(now()->addYear()),
TimePicker::make('event_time_start')
->label('Время начала')
->seconds(false)
->native(false)
->helperText('По местному времени')
->required(),
DatePicker::make('event_date_end')
->label('Дата окончания')
->native(false)
->displayFormat('d/m/Y')
->helperText('Оставьте пустым для однодневного мероприятия')
->minDate(now())
->maxDate(now()->addYear()),
]),
]),
]) ])
->persistTabInQueryString()
->columnSpanFull(),
]); ]);
} }
public static function table(Table $table): Table public static function table(Table $table): Table
{ {
return $table return $table
->columns([ ->columns([
TextColumn::make('id')->label('ID')->sortable(), TextColumn::make('id')
TextColumn::make('title')->label('Название')->sortable()->searchable(), ->label('ID')
TextColumn::make('event_date_start')->label('Начало мероприятия')->sortable(), ->sortable()
TextColumn::make('created_at')->label('Дата создания')->sortable(), ->searchable(),
TextColumn::make('title')
->label('Название')
->sortable()
->searchable()
->limit(30),
TextColumn::make('event_date_start')
->label('Дата начала')
->date('d.m.Y H:i')
->sortable(),
IconColumn::make('is_online')
->label('Онлайн')
->boolean()
->trueIcon('heroicon-o-globe-alt')
->falseIcon('heroicon-o-map-pin'),
TextColumn::make('created_at')
->label('Создано')
->dateTime('d.m.Y H:i')
->sortable(),
]) ])
->filters([ ->filters([
// Tables\Filters\SelectFilter::make('category_id')
->label('Категория')
->relationship('category', 'title'),
Tables\Filters\Filter::make('is_online')
->label('Только онлайн')
->query(fn ($query) => $query->where('is_online', true)),
]) ])
->actions([ ->actions([
Tables\Actions\EditAction::make(), Tables\Actions\EditAction::make()
->icon('heroicon-o-pencil')
->tooltip('Редактировать'),
Tables\Actions\Action::make('view')
->icon('heroicon-o-eye')
->tooltip('Просмотреть на сайте')
->url(fn (Event $record) => route('client.event.show', $record->slug))
->openUrlInNewTab(),
]) ])
->bulkActions([ ->bulkActions([
Tables\Actions\BulkActionGroup::make([ Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(), Tables\Actions\DeleteBulkAction::make()
->label('Удалить выбранное')
->icon('heroicon-o-trash'),
]), ]),
])
->defaultSort('event_date_start', 'desc')
->emptyStateActions([
Tables\Actions\CreateAction::make()
->label('Добавить мероприятие'),
]); ]);
} }
@@ -205,4 +225,4 @@ class EventResource extends Resource
'edit' => Pages\EditEvent::route('/{record}/edit'), 'edit' => Pages\EditEvent::route('/{record}/edit'),
]; ];
} }
} }
@@ -3,10 +3,26 @@
namespace App\Filament\Resources\EventResource\Pages; namespace App\Filament\Resources\EventResource\Pages;
use App\Filament\Resources\EventResource; use App\Filament\Resources\EventResource;
use App\Services\Filament\Domain\Seo\SeoGeneratorService;
use App\Services\Filament\Traits\SeoGenerate;
use Filament\Actions; use Filament\Actions;
use Filament\Resources\Pages\CreateRecord; use Filament\Resources\Pages\CreateRecord;
class CreateEvent extends CreateRecord class CreateEvent extends CreateRecord
{ {
use SeoGenerate;
protected static string $resource = EventResource::class; protected static string $resource = EventResource::class;
// protected function mutateFormDataBeforeCreate(array $data): array
// {
// }
protected function afterCreate(): void
{
$this->createSeo($this->record);
}
} }
@@ -3,13 +3,21 @@
namespace App\Filament\Resources\EventResource\Pages; namespace App\Filament\Resources\EventResource\Pages;
use App\Filament\Resources\EventResource; use App\Filament\Resources\EventResource;
use App\Services\Filament\Traits\SeoGenerate;
use Filament\Actions; use Filament\Actions;
use Filament\Resources\Pages\EditRecord; use Filament\Resources\Pages\EditRecord;
class EditEvent extends EditRecord class EditEvent extends EditRecord
{ {
use SeoGenerate;
protected static string $resource = EventResource::class; protected static string $resource = EventResource::class;
protected function afterSave(): void
{
$this->updateSeo($this->record);
}
protected function getHeaderActions(): array protected function getHeaderActions(): array
{ {
return [ return [
@@ -1,457 +0,0 @@
<?php
namespace App\Filament\Resources\FacultyRecourceResource\RelationManagers;
use App\Enums\CustomFormStatus;
use App\Enums\PostStatus;
use App\Helpers\ByteConverter;
use App\Models\Category;
use App\Models\CustomForm;
use App\Models\Faculty;
use App\Models\Page;
use App\Models\PageReferenceList;
use App\Models\Post;
use Filament\Forms;
use Filament\Forms\Components\Builder;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\RichEditor;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Tabs;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Form;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\SoftDeletingScope;
use Illuminate\Support\Carbon;
use Illuminate\Support\Str;
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
class DepartmentsRelationManager extends RelationManager
{
protected static string $relationship = 'departments';
protected static ?string $title = 'Кафедры';
public function form(Form $form): Form
{
return $form
->schema([
Section::make()
->schema([
Tabs::make('Tabs')
->tabs([
Tabs\Tab::make('Основная информация')
->schema([
TextInput::make('title')->label('Название факультета')->required()
->live(onBlur: true)
->afterStateUpdated(function (string $operation, string|null $state, Forms\Set $set) {
$set('slug', Str::slug($state));
$set('seo.title', $state);
}),
TextInput::make('slug')->label('Slug')->unique(ignoreRecord: true)->readOnly()->required(),
Toggle::make('is_active')->default(true)->label('Активный факультет')->inline(false),
Forms\Components\Select::make('faculty_id')
->options(Faculty::all()->pluck('title', 'id'))
->label('Факультет')
->required(),
]),
Tabs\Tab::make('Описание факультета')
->schema([
Builder::make('content')->label('')->blocks([
Builder\Block::make('heading')->label('Заголовок')
->schema([
TextInput::make('id')->hidden()->integer()->default(rand(2335235,324634264263426)),
TextInput::make('content')
->label('')
->live(onBlur: true)
->afterStateUpdated(function (string $operation, string $state, Forms\Set $set, $get) {
}),
]),
Builder\Block::make('paragraph')
->schema([
RichEditor::make('content')
->toolbarButtons([
'blockquote',
'bold',
'bulletList',
'italic',
'link',
'orderedList',
'redo',
'strike',
'underline',
'undo',
])
->label(''),
])->label('Текст'),
Builder\Block::make('files')
->schema([
Forms\Components\Repeater::make('file')->schema([
Hidden::make('expansion')->required(),
Hidden::make('size')->required(),
TextInput::make('title')
->required()
->maxLength(255)
->autofocus(),
FileUpload::make('path')
->required()
->getUploadedFileNameForStorageUsing(
fn (TemporaryUploadedFile $file): string =>
str(Str::slug(pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME) . '-' . Carbon::now()->timestamp ) . '.' . $file->getClientOriginalExtension())
)
->acceptedFileTypes([
'application/pdf',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'application/zip'
])
->maxSize(512000)
->disk('public')
->directory('files')
->downloadable()
->afterStateUpdated(function ($set, $state) {
$set('expansion', $state?->getClientOriginalExtension());
$set('size', ByteConverter::bytesToHuman($state?->getSize()));
})
->visibility('public')
]),
]),
Builder\Block::make('person')
->schema([
TextInput::make('name')
->label('Имя')
->required()
->maxLength(255),
FileUpload::make('photo')
->label('Фотография')
->image()
->disk('public')
->directory('images')
->imageEditor(),
Forms\Components\Repeater::make('info')->schema([
Forms\Components\Grid::make(2)->schema([
TextInput::make('column')
->required()
->maxLength(255),
TextInput::make('content')
->required()
->maxLength(255),
]),
])->minItems(1),
]),
Builder\Block::make('stepper')
->schema([
TextInput::make('step_name')
->label('Название шага')
->required()
->maxLength(255),
Forms\Components\Repeater::make('steps')->schema([
TextInput::make('title')
->required()
->maxLength(255)->columnSpanFull(),
RichEditor::make('content')->required(),
])->minItems(1),
]),
Builder\Block::make('tabs')
->schema([
Forms\Components\Repeater::make('tab')->schema([
TextInput::make('title')
->required()
->maxLength(255)->columnSpanFull(),
\Filament\Forms\Components\Builder::make('content')->label('')->blocks([
Builder\Block::make('heading')->label('Заголовок')
->schema([
TextInput::make('id')->hidden()->integer()->default(rand(2335235,324634264263426)),
TextInput::make('content')
->label('')
->live(onBlur: true)
->afterStateUpdated(function (string $operation, string $state, Forms\Set $set, $get) {
}),
]),
Builder\Block::make('paragraph')
->schema([
RichEditor::make('content')
->toolbarButtons([
'blockquote',
'bold',
'bulletList',
'italic',
'link',
'orderedList',
'redo',
'strike',
'underline',
'undo',
])
->label(''),
])->label('Текст'),
Builder\Block::make('files')
->schema([
Forms\Components\Repeater::make('file')->schema([
TextInput::make('title')
->required()
->maxLength(255)
->autofocus(),
FileUpload::make('path')
->required()
->acceptedFileTypes([
'application/pdf',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'application/zip'
])
->maxSize(512000)
->disk('public')
->directory('files')
->downloadable()
->visibility('public')
]),
]),
Builder\Block::make('person')
->schema([
TextInput::make('name')
->label('Имя')
->required()
->maxLength(255),
FileUpload::make('photo')
->label('Фотография')
->image()
->disk('public')
->directory('images')
->imageEditor(),
Forms\Components\Repeater::make('info')->schema([
Forms\Components\Grid::make(2)->schema([
TextInput::make('column')
->required()
->maxLength(255),
TextInput::make('content')
->required()
->maxLength(255),
]),
])->minItems(1),
]),
Builder\Block::make('stepper')
->schema([
TextInput::make('step_name')
->label('Название шага')
->required()
->maxLength(255),
Forms\Components\Repeater::make('steps')->schema([
TextInput::make('title')
->required()
->maxLength(255)->columnSpanFull(),
RichEditor::make('content')->required(),
])->minItems(1),
]),
Builder\Block::make('images')
->schema([
FileUpload::make('url')
->label('Изображение(-я)')
->image()
->multiple()
->reorderable()
->maxFiles(5)
->disk('public')
->directory('images')
->imageEditor()
->required(),
TextInput::make('alt')
->label('Описание')
->placeholder('Необязяательно')
])->label('Слайдер изображений'),
Builder\Block::make('image')
->schema([
FileUpload::make('url')
->label('Изображение(-я)')
->image()
->multiple()
->reorderable()
->maxFiles(5)
->disk('public')
->directory('images')
->imageEditor()
->required(),
TextInput::make('alt')
->label('Описание')
->placeholder('Необязяательно')
])->label('Изображение'),
Builder\Block::make('video')
->schema([
TextInput::make('mime')->readOnly(),
TextInput::make('title')
->required()
->maxLength(255)
->autofocus(),
FileUpload::make('path')
->required()
->acceptedFileTypes([
'video/mp4',
'video/quicktime',
'video/x-msvideo',
'video/x-ms-wmv',
'video/avi',
'video/webm',
'video/ogg',
'video/3gpp',
'video/3gpp2',
'video/x-m4v',
])
->disk('public')
->directory('videos')
->afterStateUpdated(fn (callable $set, $state) => $set('mime', $state?->getMimeType())),
]),
Builder\Block::make('postsList')
->schema([
Forms\Components\Grid::make(2)->schema([
TextInput::make('count')
->label('Количество запией')
->integer(),
Select::make('category')
->options(Category::all()->pluck('title', 'id'))
]),
])->label('Список новостей'),
])
->collapsed()
->blockNumbers(false)
->collapsible()
->addActionLabel('Добавить новый блок'),
])->minItems(1),
]),
Builder\Block::make('images')
->schema([
FileUpload::make('url')
->label('Изображение(-я)')
->image()
->multiple()
->reorderable()
->maxFiles(5)
->disk('public')
->directory('images')
->imageEditor()
->required(),
TextInput::make('alt')
->label('Описание')
->placeholder('Необязяательно')
])->label('Слайдер изображений'),
Builder\Block::make('image')
->schema([
FileUpload::make('url')
->label('Изображение(-я)')
->image()
->multiple()
->reorderable()
->maxFiles(5)
->disk('public')
->directory('images')
->imageEditor()
->required(),
TextInput::make('alt')
->label('Описание')
->placeholder('Необязяательно')
])->label('Изображение'),
Builder\Block::make('video')
->schema([
TextInput::make('mime')->readOnly(),
TextInput::make('title')
->required()
->maxLength(255)
->autofocus(),
FileUpload::make('path')
->required()
->acceptedFileTypes([
'video/mp4',
'video/quicktime',
'video/x-msvideo',
'video/x-ms-wmv',
'video/avi',
'video/webm',
'video/ogg',
'video/3gpp',
'video/3gpp2',
'video/x-m4v',
])
->disk('public')
->directory('videos')
->afterStateUpdated(fn (callable $set, $state) => $set('mime', $state?->getMimeType())),
]),
Builder\Block::make('postsList')
->schema([
Forms\Components\Grid::make(2)->schema([
TextInput::make('count')
->label('Количество запией')
->integer(),
Select::make('category')
->options(Category::all()->pluck('title', 'id'))
]),
])->label('Список новостей'),
Builder\Block::make('postItem')
->schema([
Select::make('post')
->options(Post::query()->where('status', PostStatus::PUBLISHED)->pluck('title', 'id'))
->searchable()
->required(),
])->label('Новость'),
Builder\Block::make('pageItem')
->schema([
Select::make('page')
->options(Page::query()->where('title', '!=' , null)->where('is_visible', true)->pluck('title', 'id'))
->searchable()
->required(),
])->label('Страница'),
Builder\Block::make('customForm')
->schema([
Select::make('form')
->options(CustomForm::query()->where('status', CustomFormStatus::PUBLISHED)->pluck('title', 'form_id'))
->searchable()
->required(),
])->label('Форма'),
Builder\Block::make('pageResourceList')
->schema([
Select::make('resource')
->options(PageReferenceList::query()->where('is_active', true)->pluck('title', 'slug'))
->searchable()
->required(),
])->label('Ресурсы'),
])
->collapsed()
->blockNumbers(false)
->collapsible()
->required()
->blockPickerColumns(3)
->blockPickerWidth('2xl')
->addActionLabel('Добавить новый блок'),
]),
]),
])
]);
}
public function table(Table $table): Table
{
return $table
->recordTitleAttribute('title')
->columns([
TextColumn::make('id')->label('ID')->sortable(),
TextColumn::make('title')->label('Название')->sortable()->searchable(),
TextColumn::make('created_at')->label('Дата создания')->sortable(),
Tables\Columns\ToggleColumn::make('is_active')->label('Активно')->sortable(), ])
->filters([
//
])
->headerActions([
Tables\Actions\CreateAction::make(),
Tables\Actions\AssociateAction::make(),
])
->actions([
Tables\Actions\EditAction::make(),
]);
}
}
+107 -401
View File
@@ -4,11 +4,10 @@ namespace App\Filament\Resources;
use App\Enums\CustomFormStatus; use App\Enums\CustomFormStatus;
use App\Enums\PostStatus; use App\Enums\PostStatus;
use App\Filament\Resources\FacultyRecourceResource\RelationManagers\DepartmentsRelationManager; use App\Filament\Components\Forms\ItemForm\Pages\ContentBuilderItem;
use App\Filament\Resources\FacultyResource\Pages; use App\Filament\Resources\FacultyResource\Pages;
use App\Filament\Resources\FacultyResource\RelationManagers; use App\Filament\Resources\FacultyResource\RelationManagers\DepartmentsRelationManager;
use App\Filament\Resources\FacultyResource\RelationManagers\WorkersRelationManager; use App\Filament\Resources\FacultyResource\RelationManagers\WorkersRelationManager;
use App\Helpers\ByteConverter;
use App\Models\Category; use App\Models\Category;
use App\Models\CustomForm; use App\Models\CustomForm;
use App\Models\Faculty; use App\Models\Faculty;
@@ -16,446 +15,153 @@ use App\Models\Page;
use App\Models\PageReferenceList; use App\Models\PageReferenceList;
use App\Models\Post; use App\Models\Post;
use Filament\Forms; use Filament\Forms;
use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Components\Builder; use Filament\Forms\Components\Builder;
use Filament\Forms\Components\FileUpload; use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\RichEditor;
use Filament\Forms\Components\Section; use Filament\Forms\Components\Section;
use Filament\Forms\Components\Select; use Filament\Forms\Components\Select;
use Filament\Forms\Components\Tabs;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle; use Filament\Forms\Components\Toggle;
use Filament\Forms\Form; use Filament\Forms\Form;
use Filament\Resources\Resource; use Filament\Resources\Resource;
use Filament\Tables; use Filament\Tables;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn; use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table; use Filament\Tables\Table;
use Illuminate\Database\Eloquent\SoftDeletingScope;
use Illuminate\Support\Carbon;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
use Mohamedsabil83\FilamentFormsTinyeditor\Components\TinyEditor;
class FacultyResource extends Resource class FacultyResource extends Resource
{ {
protected static ?string $model = Faculty::class; protected static ?string $model = Faculty::class;
protected static ?string $navigationGroup = 'Структура института'; protected static ?string $navigationGroup = 'Структура института';
protected static ?string $navigationIcon = 'heroicon-o-building-office-2'; protected static ?string $navigationIcon = 'heroicon-o-building-office-2';
protected static ?string $pluralLabel = 'Факультеты'; protected static ?string $pluralLabel = 'Факультеты';
protected static ?string $modelLabel = 'факультет';
public static ?string $label = 'Факультет';
public static function form(Form $form): Form public static function form(Form $form): Form
{ {
return $form return $form
->schema([ ->schema([
Section::make() Forms\Components\Tabs::make('Настройки факультета')
->schema([ ->persistTabInQueryString()
Tabs::make('Tabs') ->columnSpanFull()
->tabs([ ->tabs([
Tabs\Tab::make('Основная информация') Forms\Components\Tabs\Tab::make('Основные данные')
->icon('heroicon-o-information-circle')
->schema([ ->schema([
TextInput::make('title')->label('Название факультета')->required() Section::make('Идентификация')
->live(onBlur: true) ->description('Основная информация о факультете')
->afterStateUpdated(function (string $operation, string|null $state, Forms\Set $set) { ->schema([
$set('slug', Str::slug($state)); TextInput::make('title')
$set('seo.title', $state); ->label('Полное название')
}), ->required()
TextInput::make('slug')->label('Slug')->unique(ignoreRecord: true)->readOnly()->required(), ->maxLength(255)
TextInput::make('abbreviation')->label('Аббревиатура')->required(), ->placeholder('Например: Факультет информационных технологий')
Toggle::make('is_active')->default(true)->label('Активный факультет')->inline(false), ->live(onBlur: true)
->afterStateUpdated(function (string $operation, ?string $state, Forms\Set $set) {
$set('slug', Str::slug($state));
if ($operation !== 'edit') {
$set('seo.title', $state);
}
})
->helperText('Официальное название факультета'),
TextInput::make('slug')
->label('URL-адрес')
->unique(ignoreRecord: true)
->required()
->readOnly()
->helperText('Формируется автоматически из названия')
->prefix(fn () => route('client.faculty.index') . '/')
->suffixAction(
Action::make('copy')
->icon('heroicon-s-clipboard-document-check')
->action(function ($livewire, $state) {
$livewire->js(
'window.navigator.clipboard.writeText("'. route('client.faculty.index') . '/' . $state.'");
$tooltip("'.__('Copied to clipboard').'", { timeout: 1500 });'
);
})),
TextInput::make('abbreviation')
->label('Аббревиатура')
->required()
->maxLength(10)
->placeholder('Например: ФИТ')
->helperText('Короткое обозначение факультета'),
Toggle::make('is_active')
->label('Активный факультет')
->inline(false)
->default(true)
->helperText('Отображать ли факультет на сайте'),
]),
]), ]),
Tabs\Tab::make('Описание факультета')
Forms\Components\Tabs\Tab::make('Контент')
->icon('heroicon-o-document-text')
->schema([ ->schema([
Builder::make('content')->label('')->blocks([ ContentBuilderItem::getItem('content')
Builder\Block::make('heading')->label('Заголовок')
->schema([
TextInput::make('id')->hidden()->integer()->default(rand(2335235,324634264263426)),
TextInput::make('content')
->label('')
->live(onBlur: true)
->afterStateUpdated(function (string $operation, string $state, Forms\Set $set, $get) {
}),
]),
Builder\Block::make('paragraph')
->schema([
TinyEditor::make('content')
->label('')
->profile('test')
->required(),
])->label('Текст'),
Builder\Block::make('files')
->schema([
Forms\Components\Repeater::make('file')->schema([
Hidden::make('expansion')->required(),
Hidden::make('size')->required(),
TextInput::make('title')
->required()
->maxLength(255)
->autofocus(),
FileUpload::make('path')
->required()
->getUploadedFileNameForStorageUsing(
fn (TemporaryUploadedFile $file): string =>
str(Str::slug(pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME) . '-' . Carbon::now()->timestamp ) . '.' . $file->getClientOriginalExtension())
)
->acceptedFileTypes([
'application/pdf',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'application/zip'
])
->maxSize(512000)
->disk('public')
->directory('files')
->downloadable()
->afterStateUpdated(function ($set, $state) {
$set('expansion', $state?->getClientOriginalExtension());
$set('size', ByteConverter::bytesToHuman($state?->getSize()));
})
->visibility('public')
]),
]),
Builder\Block::make('person')
->schema([
TextInput::make('name')
->label('Имя')
->required()
->maxLength(255),
FileUpload::make('photo')
->label('Фотография')
->image()
->disk('public')
->directory('images')
->imageEditor(),
Forms\Components\Repeater::make('info')->schema([
Forms\Components\Grid::make(2)->schema([
TextInput::make('column')
->required()
->maxLength(255),
TextInput::make('content')
->required()
->maxLength(255),
]),
])->minItems(1),
]),
Builder\Block::make('stepper')
->schema([
TextInput::make('step_name')
->label('Название шага')
->required()
->maxLength(255),
Forms\Components\Repeater::make('steps')->schema([
TextInput::make('title')
->required()
->maxLength(255)->columnSpanFull(),
TinyEditor::make('content')
->label('')
->profile('test')
->required(),
])->minItems(1),
]),
Builder\Block::make('tabs')
->schema([
Forms\Components\Repeater::make('tab')->schema([
TextInput::make('title')
->required()
->maxLength(255)->columnSpanFull(),
\Filament\Forms\Components\Builder::make('content')->label('')->blocks([
Builder\Block::make('heading')->label('Заголовок')
->schema([
TextInput::make('id')->hidden()->integer()->default(rand(2335235,324634264263426)),
TextInput::make('content')
->label('')
->live(onBlur: true)
->afterStateUpdated(function (string $operation, string $state, Forms\Set $set, $get) {
}),
]),
Builder\Block::make('paragraph')
->schema([
RichEditor::make('content')
->toolbarButtons([
'blockquote',
'bold',
'bulletList',
'italic',
'link',
'orderedList',
'redo',
'strike',
'underline',
'undo',
])
->label(''),
])->label('Текст'),
Builder\Block::make('files')
->schema([
Forms\Components\Repeater::make('file')->schema([
TextInput::make('title')
->required()
->maxLength(255)
->autofocus(),
FileUpload::make('path')
->required()
->acceptedFileTypes([
'application/pdf',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'application/zip'
])
->maxSize(512000)
->disk('public')
->directory('files')
->downloadable()
->visibility('public')
]),
]),
Builder\Block::make('person')
->schema([
TextInput::make('name')
->label('Имя')
->required()
->maxLength(255),
FileUpload::make('photo')
->label('Фотография')
->image()
->disk('public')
->directory('images')
->imageEditor(),
Forms\Components\Repeater::make('info')->schema([
Forms\Components\Grid::make(2)->schema([
TextInput::make('column')
->required()
->maxLength(255),
TextInput::make('content')
->required()
->maxLength(255),
]),
])->minItems(1),
]),
Builder\Block::make('stepper')
->schema([
TextInput::make('step_name')
->label('Название шага')
->required()
->maxLength(255),
Forms\Components\Repeater::make('steps')->schema([
TextInput::make('title')
->required()
->maxLength(255)->columnSpanFull(),
RichEditor::make('content')->required(),
])->minItems(1),
]),
Builder\Block::make('images')
->schema([
FileUpload::make('url')
->label('Изображение(-я)')
->image()
->multiple()
->reorderable()
->maxFiles(5)
->disk('public')
->directory('images')
->imageEditor()
->required(),
TextInput::make('alt')
->label('Описание')
->placeholder('Необязяательно')
])->label('Слайдер изображений'),
Builder\Block::make('image')
->schema([
FileUpload::make('url')
->label('Изображение(-я)')
->image()
->multiple()
->reorderable()
->maxFiles(5)
->disk('public')
->directory('images')
->imageEditor()
->required(),
TextInput::make('alt')
->label('Описание')
->placeholder('Необязяательно')
])->label('Изображение'),
Builder\Block::make('video')
->schema([
TextInput::make('mime')->readOnly(),
TextInput::make('title')
->required()
->maxLength(255)
->autofocus(),
FileUpload::make('path')
->required()
->acceptedFileTypes([
'video/mp4',
'video/quicktime',
'video/x-msvideo',
'video/x-ms-wmv',
'video/avi',
'video/webm',
'video/ogg',
'video/3gpp',
'video/3gpp2',
'video/x-m4v',
])
->disk('public')
->directory('videos')
->afterStateUpdated(fn (callable $set, $state) => $set('mime', $state?->getMimeType())),
]),
Builder\Block::make('postsList')
->schema([
Forms\Components\Grid::make(2)->schema([
TextInput::make('count')
->label('Количество запией')
->integer(),
Select::make('category')
->options(Category::all()->pluck('title', 'id'))
]),
])->label('Список новостей'),
])
->collapsed()
->blockNumbers(false)
->collapsible()
->addActionLabel('Добавить новый блок'),
])->minItems(1),
]),
Builder\Block::make('images')
->schema([
FileUpload::make('url')
->label('Изображение(-я)')
->image()
->multiple()
->reorderable()
->maxFiles(5)
->disk('public')
->directory('images')
->imageEditor()
->required(),
TextInput::make('alt')
->label('Описание')
->placeholder('Необязяательно')
])->label('Слайдер изображений'),
Builder\Block::make('image')
->schema([
FileUpload::make('url')
->label('Изображение(-я)')
->image()
->multiple()
->reorderable()
->maxFiles(5)
->disk('public')
->directory('images')
->imageEditor()
->required(),
TextInput::make('alt')
->label('Описание')
->placeholder('Необязяательно')
])->label('Изображение'),
Builder\Block::make('video')
->schema([
TextInput::make('mime')->readOnly(),
TextInput::make('title')
->required()
->maxLength(255)
->autofocus(),
FileUpload::make('path')
->required()
->acceptedFileTypes([
'video/mp4',
'video/quicktime',
'video/x-msvideo',
'video/x-ms-wmv',
'video/avi',
'video/webm',
'video/ogg',
'video/3gpp',
'video/3gpp2',
'video/x-m4v',
])
->disk('public')
->directory('videos')
->afterStateUpdated(fn (callable $set, $state) => $set('mime', $state?->getMimeType())),
]),
Builder\Block::make('postsList')
->schema([
Forms\Components\Grid::make(2)->schema([
TextInput::make('count')
->label('Количество запией')
->integer(),
Select::make('category')
->options(Category::all()->pluck('title', 'id'))
]),
])->label('Список новостей'),
Builder\Block::make('postItem')
->schema([
Select::make('post')
->options(Post::query()->where('status', PostStatus::PUBLISHED)->pluck('title', 'id'))
->searchable()
->required(),
])->label('Новость'),
Builder\Block::make('pageItem')
->schema([
Select::make('page')
->options(Page::query()->where('title', '!=' , null)->where('is_visible', true)->pluck('title', 'id'))
->searchable()
->required(),
])->label('Страница'),
Builder\Block::make('customForm')
->schema([
Select::make('form')
->options(CustomForm::query()->where('status', CustomFormStatus::PUBLISHED)->pluck('title', 'form_id'))
->searchable()
->required(),
])->label('Форма'),
Builder\Block::make('pageResourceList')
->schema([
Select::make('resource')
->options(PageReferenceList::query()->where('is_active', true)->pluck('title', 'slug'))
->searchable()
->required(),
])->label('Ресурсы'),
])
->collapsed()
->blockNumbers(false)
->collapsible()
->required()
->blockPickerColumns(3)
->blockPickerWidth('2xl')
->addActionLabel('Добавить новый блок'),
]), ]),
]), ]),
]) ]);
]);
} }
public static function table(Table $table): Table public static function table(Table $table): Table
{ {
return $table return $table
->columns([ ->columns([
TextColumn::make('id')->label('ID')->sortable(), TextColumn::make('title')
TextColumn::make('title')->label('Название')->sortable()->searchable(), ->label('Название')
TextColumn::make('created_at')->label('Дата создания')->sortable(), ->searchable()
Tables\Columns\ToggleColumn::make('is_active')->label('Активно')->sortable(), ->sortable()
]) ->description(fn ($record) => $record->abbreviation),
IconColumn::make('is_active')
->label('Статус')
->boolean()
->trueIcon('heroicon-o-check-circle')
->falseIcon('heroicon-o-x-circle')
->trueColor('success')
->falseColor('danger')
->sortable(),
TextColumn::make('updated_at')
->label('Обновлено')
->dateTime('d.m.Y H:i')
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->filters([ ->filters([
// Tables\Filters\TernaryFilter::make('is_active')
->label('Только активные')
->placeholder('Все')
->trueLabel('Активные')
->falseLabel('Неактивные'),
]) ])
->actions([ ->actions([
Tables\Actions\EditAction::make(), Tables\Actions\EditAction::make()
->iconButton()
->tooltip('Редактировать'),
Tables\Actions\ViewAction::make()
->iconButton()
->tooltip('Просмотреть'),
]) ])
->bulkActions([ ->bulkActions([
Tables\Actions\BulkActionGroup::make([ Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(), Tables\Actions\DeleteBulkAction::make()
->label('Удалить выбранные')
->modalHeading('Удаление факультетов')
->modalDescription('Вы уверены, что хотите удалить выбранные факультеты?'),
]), ]),
]); ])
->emptyStateActions([
Tables\Actions\CreateAction::make()
->label('Добавить факультет'),
])
->defaultSort('title');
} }
public static function getRelations(): array public static function getRelations(): array
@@ -474,4 +180,4 @@ class FacultyResource extends Resource
'edit' => Pages\EditFaculty::route('/{record}/edit'), 'edit' => Pages\EditFaculty::route('/{record}/edit'),
]; ];
} }
} }
@@ -3,42 +3,28 @@
namespace App\Filament\Resources\FacultyResource\Pages; namespace App\Filament\Resources\FacultyResource\Pages;
use App\Filament\Resources\FacultyResource; use App\Filament\Resources\FacultyResource;
use App\Services\Filament\Traits\SeoGenerate;
use Filament\Actions; use Filament\Actions;
use Filament\Resources\Pages\CreateRecord; use Filament\Resources\Pages\CreateRecord;
use Illuminate\Support\Str; use Illuminate\Support\Str;
class CreateFaculty extends CreateRecord class CreateFaculty extends CreateRecord
{ {
use SeoGenerate;
protected static string $resource = FacultyResource::class; protected static string $resource = FacultyResource::class;
protected array $seoData;
protected function mutateFormDataBeforeCreate(array $data): array protected function mutateFormDataBeforeCreate(array $data): array
{ {
$this->seoData = $this->generateSeo($data);
$data['search_data'] = $this->generateSearchData($data['content']); $data['search_data'] = $this->generateSearchData($data['content']);
return $data; return $data;
} }
protected function afterCreate(): void protected function afterCreate(): void
{ {
$this->record->seo()->create($this->seoData); $this->createSeo($this->record);
}
private function generateSeo(array $data) : array
{
$title = $data['title'];
$rowData = $this->getFirstBlockByName('paragraph', $data['content']);
if ($rowData !== null) {
$description = strip_tags($rowData['data']['content']);
} else {
$description = null;
}
return [
'title' => $title,
'description' => Str::limit(htmlspecialchars($description, ENT_QUOTES, 'UTF-8'), 160),
];
} }
private function generateSearchData(array $data) : string private function generateSearchData(array $data) : string
@@ -3,20 +3,20 @@
namespace App\Filament\Resources\FacultyResource\Pages; namespace App\Filament\Resources\FacultyResource\Pages;
use App\Filament\Resources\FacultyResource; use App\Filament\Resources\FacultyResource;
use App\Services\Filament\Traits\SeoGenerate;
use Filament\Actions; use Filament\Actions;
use Filament\Resources\Pages\EditRecord; use Filament\Resources\Pages\EditRecord;
use Illuminate\Support\Str; use Illuminate\Support\Str;
class EditFaculty extends EditRecord class EditFaculty extends EditRecord
{ {
protected static string $resource = FacultyResource::class; use SeoGenerate;
protected array $seoData; protected static string $resource = FacultyResource::class;
protected function mutateFormDataBeforeSave(array $data): array protected function mutateFormDataBeforeSave(array $data): array
{ {
$this->seoData = $this->generateSeo($data);
$data['search_data'] = $this->generateSearchData($data['content']); $data['search_data'] = $this->generateSearchData($data['content']);
return $data; return $data;
@@ -24,7 +24,7 @@ class EditFaculty extends EditRecord
protected function afterSave(): void protected function afterSave(): void
{ {
$this->record->seo()->update($this->seoData); $this->updateSeo($this->record);
} }
@@ -39,11 +39,11 @@ class DepartmentsRelationManager extends RelationManager
]) ])
->actions([ ->actions([
Tables\Actions\EditAction::make(), Tables\Actions\EditAction::make(),
Tables\Actions\DetachAction::make(), Tables\Actions\DeleteAction::make(),
]) ])
->bulkActions([ ->bulkActions([
Tables\Actions\BulkActionGroup::make([ Tables\Actions\BulkActionGroup::make([
Tables\Actions\DetachBulkAction::make(), Tables\Actions\DeleteBulkAction::make(),
]), ]),
]); ]);
} }
@@ -9,23 +9,47 @@ use Filament\Tables;
use Filament\Tables\Actions\AttachAction; use Filament\Tables\Actions\AttachAction;
use Filament\Tables\Table; use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletingScope;
class WorkersRelationManager extends RelationManager class WorkersRelationManager extends RelationManager
{ {
protected static string $relationship = 'workers'; protected static string $relationship = 'workers';
protected static ?string $title = 'Сотрудники факультета';
protected static ?string $title = 'Сотрудники'; protected static ?string $modelLabel = 'сотрудник';
protected static ?string $pluralModelLabel = 'сотрудники';
public function form(Form $form): Form public function form(Form $form): Form
{ {
return $form return $form
->schema([ ->schema([
Forms\Components\TextInput::make('position')->label('Должность')->required(), Forms\Components\Grid::make(2)
Forms\Components\TextInput::make('service_email')->label('Служебная почта'), ->schema([
Forms\Components\TextInput::make('service_phone')->label('Служебный телефон'), Forms\Components\TextInput::make('position')
Forms\Components\TextInput::make('cabinet')->label('Кабинет'), ->label('Должность')
->required()
->maxLength(255)
->placeholder('Например: Декан факультета')
->helperText('Укажите официальную должность'),
Forms\Components\TextInput::make('service_email')
->label('Рабочая почта')
->email()
->maxLength(255)
->placeholder('example@university.ru')
->helperText('Корпоративная электронная почта'),
Forms\Components\TextInput::make('service_phone')
->label('Рабочий телефон')
->tel()
->maxLength(20)
->placeholder('+7 (XXX) XXX-XX-XX')
->helperText('Номер рабочего телефона с кодом'),
Forms\Components\TextInput::make('cabinet')
->label('Кабинет')
->maxLength(10)
->placeholder('Например: 305а')
->helperText('Номер кабинета для приема'),
])
]); ]);
} }
@@ -36,9 +60,34 @@ class WorkersRelationManager extends RelationManager
->reorderable('sort') ->reorderable('sort')
->defaultSort('sort') ->defaultSort('sort')
->columns([ ->columns([
Tables\Columns\TextColumn::make('name')->label('Имя'), Tables\Columns\TextColumn::make('name')
Tables\Columns\TextColumn::make('position')->label('Должность'), ->label('ФИО')
->searchable()
->sortable(),
Tables\Columns\TextColumn::make('position')
->label('Должность')
->searchable()
->sortable()
->limit(30),
Tables\Columns\TextColumn::make('service_email')
->label('Почта')
->searchable()
->icon('heroicon-o-envelope')
->toggleable(isToggledHiddenByDefault: true),
Tables\Columns\TextColumn::make('service_phone')
->label('Телефон')
->searchable()
->icon('heroicon-o-phone')
->toggleable(isToggledHiddenByDefault: true),
Tables\Columns\TextColumn::make('cabinet')
->label('Кабинет')
->searchable()
->icon('heroicon-o-home-modern')
->toggleable(isToggledHiddenByDefault: false),
]) ])
->filters([ ->filters([
// //
@@ -46,21 +95,70 @@ class WorkersRelationManager extends RelationManager
->headerActions([ ->headerActions([
AttachAction::make() AttachAction::make()
->form(fn (AttachAction $action): array => [ ->form(fn (AttachAction $action): array => [
$action->getRecordSelect()->preload(), $action->getRecordSelect()
Forms\Components\TextInput::make('position')->label('Должность')->required(), ->label('Сотрудник')
Forms\Components\TextInput::make('service_email')->label('Служебная почта'), ->searchable()
Forms\Components\TextInput::make('service_phone')->label('Служебный телефон'), ->preload()
Forms\Components\TextInput::make('cabinet')->label('Кабинет'), ->required()
->helperText('Выберите сотрудника из списка'),
Forms\Components\TextInput::make('position')
->label('Должность на факультете')
->required()
->maxLength(255)
->placeholder('Например: Старший преподаватель')
->helperText('Укажите должность на этом факультете'),
Forms\Components\Grid::make(1)
->schema([
Forms\Components\TextInput::make('service_email')
->label('Рабочая почта')
->email()
->maxLength(255)
->placeholder('example@university.ru')
->helperText('Корпоративная электронная почта'),
Forms\Components\TextInput::make('service_phone')
->label('Рабочий телефон')
->tel()
->maxLength(20)
->placeholder('+7 (XXX) XXX-XX-XX')
->helperText('Номер рабочего телефона с кодом'),
Forms\Components\TextInput::make('cabinet')
->label('Кабинет')
->maxLength(10)
->placeholder('Например: 305а')
->helperText('Номер кабинета для приема'),
])
]) ])
->modalHeading('Добавить сотрудника')
->modalSubmitActionLabel('Добавить')
->modalButton('Добавить')
]) ])
->actions([ ->actions([
Tables\Actions\EditAction::make(), Tables\Actions\EditAction::make()
Tables\Actions\DetachAction::make(), ->iconButton()
->tooltip('Редактировать'),
Tables\Actions\DetachAction::make()
->iconButton()
->tooltip('Открепить')
->modalHeading('Открепить сотрудника')
->modalDescription('Вы уверены, что хотите открепить этого сотрудника от факультета?')
->modalSubmitActionLabel('Открепить'),
]) ])
->bulkActions([ ->bulkActions([
Tables\Actions\BulkActionGroup::make([ Tables\Actions\BulkActionGroup::make([
Tables\Actions\DetachBulkAction::make(), Tables\Actions\DetachBulkAction::make()
->label('Открепить выбранных')
->modalHeading('Открепить сотрудников')
->modalDescription('Вы уверены, что хотите открепить выбранных сотрудников от факультета?')
->modalSubmitActionLabel('Открепить'),
]), ]),
]); ])
->emptyStateHeading('Нет сотрудников')
->emptyStateDescription('Добавьте сотрудников, используя кнопку выше')
->emptyStateIcon('heroicon-o-user-group');
} }
} }
@@ -1,221 +0,0 @@
<?php
namespace App\Filament\Resources;
use App\Filament\Resources\MainSliderResource\Pages;
use App\Filament\Resources\MainSliderResource\RelationManagers;
use App\Models\Event;
use App\Models\MainSlider;
use App\Models\Page;
use App\Models\Post;
use Filament\Forms;
use Filament\Forms\Components\ColorPicker;
use Filament\Forms\Components\DateTimePicker;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Components\ToggleButtons;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletingScope;
use Illuminate\Support\Carbon;
use Illuminate\Support\Str;
use Yepsua\Filament\Forms\Components\RangeSlider;
class MainSliderResource extends Resource
{
public static ?string $label = 'Слайдер';
protected static ?string $pluralLabel = 'Главный слайдер';
protected static ?string $model = MainSlider::class;
protected static ?string $navigationIcon = 'heroicon-o-square-3-stack-3d';
public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\Section::make('Быстрая настройка слайда')->schema([
Forms\Components\Grid::make()->schema([
Forms\Components\Select::make('model_select')
->name('')
->label('Выбор типа данных')
->options([
'Post' => 'Новость',
'Page' => 'Страница',
'Event' => 'Мероприятие',
'Custom' => 'Кастомная ссылка',
])
->afterStateUpdated(function (string $operation, string|null $state, Forms\Set $set, Forms\Get $get) {
if ($get('model_select') === 'Custom') {
$set('model', null);
$set('title', null);
$set('content', null);
$set('link', null);
};
})->live(onBlur: true),
Forms\Components\Select::make('model')
->label('Поиск данных')
->name('')
->live(onBlur: true)
->searchable()
->live(onBlur: true)
->afterStateUpdated(function (string $operation, string|null $state, Forms\Set $set, Forms\Get $get) {
if ($get('model_select') === 'Post') {
$post = Post::find($state);
$set('title', $post->title);
$relativeUrl = parse_url(route('client.post.show', $post->slug), PHP_URL_PATH);
$set('link', $relativeUrl);
};
if ($get('model_select') === 'Page') {
$page = Page::find($state);
$set('title', $page->title);
$set('link', $page->path);
};
if ($get('model_select') === 'Event') {
$event = Event::find($state);
$set('title', $event->title);
$relativeUrl = parse_url(route('client.event.show', $event->slug), PHP_URL_PATH);
$set('link', $relativeUrl);
};
})
->options(function (Forms\Get $get) {
if ($get('model_select') === 'Post') {
return Post::where('status', '=', 'published')->pluck('title', 'id');
};
if ($get('model_select') === 'Page') {
return Page::where('title', '!=', null)->pluck('title', 'id');
};
if ($get('model_select') === 'Event') {
return Event::all()->pluck('title', 'id');
};
if ($get('model_select') === 'Custom') {
return [];
};
}),
]),
]),
Forms\Components\Section::make('Слайдер')->schema([
Forms\Components\Section::make('Информация слайда')->schema([
Forms\Components\TextInput::make('title')
->label('Заголовок слайда'),
Forms\Components\Textarea::make('content')
->label('Текст слайда'),
Forms\Components\Grid::make()->schema([
ColorPicker::make('color_theme')
->label('Цвет текста')
->default('#ffffff')
->required(),
Forms\Components\ToggleButtons::make('settings.text_position')
->options([
'left' => 'Текст слева',
'center' => 'Текст по середине',
'right' => 'Текст справа'
])
->inline()->default('left')->grouped()
->label('Позиция текста на слайде'),
]),
Forms\Components\Grid::make()->schema([
Toggle::make('active_button')
->label('Использовать кнопку для ссылки (Ссылка будет открываться при нажатии на слайд)')
->inline(false)
->default(true) // Проверяем, есть ли текст в link_text
->live()
->afterStateHydrated(function (Toggle $component, $state, $get) {
if ($state === null && !empty($get('settings.link_text'))) {
$component->state(true); // Устанавливаем значение по умолчанию
}
})
->dehydrated(false),
Forms\Components\TextInput::make('settings.link_text')
->default('Читать')
->label('Текст кнопки')
->disabled(fn (Forms\Get $get) => !$get('active_button'))
]),
]),
Forms\Components\Section::make('Изображение')->schema([
FileUpload::make('image.url')
->label('Изображение')
->image()
->optimize('webp')
->resize(50)
->disk('public')
->directory('images')
->imageEditor()
->required(),
ToggleButtons::make('image.shading')->inline()->grouped()->label('Уровень затемнения изображения')->options([
'1' => 'Без затемнения',
'0.7' => 'Слабое затемнение',
'0.5' => 'Среднее затемнение',
'0.3' => 'Сильное затемнение',
]),
]),
Forms\Components\Section::make('Общая часть')->schema([
Forms\Components\Grid::make()->schema([
DateTimePicker::make('start_time')
->label('Слайд начинается с')
->native()
->displayFormat('d/m/Y')
->default(Carbon::now())
->maxDate(Carbon::now()->addWeeks(2)),
DateTimePicker::make('end_time')
->label('Слайд действует до')
->native()
->displayFormat('d/m/Y')
->default(Carbon::now()->addWeeks(2))
->minDate(Carbon::now())
->maxDate(Carbon::now()->addMonth()),
]),
Forms\Components\TextInput::make('link')
->label('Ссылка кнопки')
->required(),
]),
Toggle::make('is_active')->default(true)->label('Активный слайдер')->inline(false),
]),
]);
}
public static function table(Table $table): Table
{
return $table
->reorderable('sort')
->defaultSort('sort')
->columns([
Tables\Columns\TextColumn::make('title'),
Tables\Columns\ToggleColumn::make('is_active')
])
->filters([
//
])
->actions([
Tables\Actions\EditAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListMainSliders::route('/'),
'create' => Pages\CreateMainSlider::route('/create'),
'edit' => Pages\EditMainSlider::route('/{record}/edit'),
];
}
}
@@ -1,20 +0,0 @@
<?php
namespace App\Filament\Resources\MainSliderResource\Pages;
use App\Filament\Resources\MainSliderResource;
use Filament\Actions;
use Filament\Resources\Pages\CreateRecord;
class CreateMainSlider extends CreateRecord
{
protected static string $resource = MainSliderResource::class;
protected function mutateFormDataBeforeCreate(array $data): array
{
unset($data['model_select'], $data['model']);
return $data;
}
}
@@ -1,26 +0,0 @@
<?php
namespace App\Filament\Resources\MainSliderResource\Pages;
use App\Filament\Resources\MainSliderResource;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditMainSlider extends EditRecord
{
protected static string $resource = MainSliderResource::class;
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
];
}
protected function mutateFormDataBeforeSave(array $data): array
{
unset($data['model_select'], $data['model']);
return $data;
}
}
@@ -1,19 +0,0 @@
<?php
namespace App\Filament\Resources\MainSliderResource\Pages;
use App\Filament\Resources\MainSliderResource;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
class ListMainSliders extends ListRecords
{
protected static string $resource = MainSliderResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}
}
@@ -32,6 +32,9 @@ class PageReferenceListResource extends Resource
protected static ?string $pluralLabel = 'Списки ресурсов'; protected static ?string $pluralLabel = 'Списки ресурсов';
protected static ?string $navigationGroup = 'Виджеты';
protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack'; protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack';
@@ -39,109 +42,180 @@ class PageReferenceListResource extends Resource
{ {
return $form return $form
->schema([ ->schema([
Forms\Components\Section::make('')->schema([ Forms\Components\Section::make('Ресурс')
Tabs::make('Tabs') ->description('Управление контентом ресурса')
->tabs([ ->collapsible()
Tabs\Tab::make('Основная информация') ->schema([
->schema([ Tabs::make('Настройки ресурса')
Forms\Components\Grid::make(2)->schema([ ->persistTabInQueryString()
TextInput::make('title')->label('Название ресурса')->required() ->columnSpanFull()
->live(onBlur: true) ->tabs([
->afterStateUpdated(function (string $operation, string|null $state, Forms\Set $set) { Tabs\Tab::make('Основная информация')
$set('slug', Str::slug($state)); ->icon('heroicon-o-information-circle')
$set('seo.title', $state); ->schema([
}), Forms\Components\Grid::make(2)
TextInput::make('slug')->label('Slug')->unique(ignoreRecord: true)->readOnly()->required(), ->schema([
Toggle::make('is_active')->default(true)->label('Активный ресурс')->inline(false), TextInput::make('title')
]) ->label('Название ресурса')
]), ->placeholder('Введите название ресурса')
Tabs\Tab::make('Содержание ресурса') ->helperText('Это название будет отображаться в административной панели')
->schema([ ->required()
Repeater::make('content')->label('Ресурсы')->schema([ ->maxLength(255)
Forms\Components\Section::make('Быстрая настройка ресурса')->schema([
Forms\Components\Grid::make()->schema([
Forms\Components\Select::make('model_select')
->name('')
->label('Выбор типа данных')
->options([
'Post' => 'Новость',
'Page' => 'Страница',
'Event' => 'Мероприятие',
'Custom' => 'Кастомная ссылка',
])
->afterStateUpdated(function (string $operation, string|null $state, Forms\Set $set, Forms\Get $get) {
if ($get('model_select') === 'Custom') {
$set('model', null);
$set('title', null);
$set('content', null);
$set('link', null);
};
})->live(onBlur: true),
Forms\Components\Select::make('model')
->label('Поиск данных')
->name('')
->live(onBlur: true) ->live(onBlur: true)
->searchable() ->afterStateUpdated(function (string $operation, string|null $state, Forms\Set $set) {
->live(onBlur: true) $set('slug', Str::slug($state));
->afterStateUpdated(function (string $operation, string|null $state, Forms\Set $set, Forms\Get $get) { $set('seo.title', $state);
if ($get('model_select') === 'Post') {
$post = Post::find($state);
$set('title', $post->title);
$relativeUrl = parse_url(route('client.post.show', $post->slug), PHP_URL_PATH);
$set('link', $relativeUrl);
};
if ($get('model_select') === 'Page') {
$page = Page::find($state);
$set('title', $page->title);
$set('link', $page->path);
};
if ($get('model_select') === 'Event') {
$event = Event::find($state);
$set('title', $event->title);
$relativeUrl = parse_url(route('client.event.show', $event->slug), PHP_URL_PATH);
$set('link', $relativeUrl);
};
})
->options(function (Forms\Get $get) {
if ($get('model_select') === 'Post') {
return Post::where('status', '=', 'published')->pluck('title', 'id');
};
if ($get('model_select') === 'Page') {
return Page::where('title', '!=', null)->pluck('title', 'id');
};
if ($get('model_select') === 'Event') {
return Event::all()->pluck('title', 'id');
};
if ($get('model_select') === 'Custom') {
return [];
};
}), }),
]),
]),
TextInput::make('title')->label('Заголовок ресурса')->required(),
FileUpload::make('image')
->label('Изображение предпросмотра')
->image()
->optimize('webp')
->resize(50)
->disk('public')
->directory('images')
->imageEditor(),
Forms\Components\Grid::make(2)->schema([
Forms\Components\TextInput::make('link')
->label('Ссылка ресурса')
->required(),
Forms\Components\TextInput::make('link_text')
->default('Читать')
->label('Текст кнопки')
->required(),
]),
])->collapsed()->required(),
]),
]), TextInput::make('slug')
->label('URL-адрес (Slug)')
->helperText('Автоматически генерируется из названия')
->required()
->unique(ignoreRecord: true)
->readOnly()
->maxLength(255),
Toggle::make('is_active')
->label('Активность ресурса')
->helperText('Отключите, чтобы скрыть ресурс')
->default(true)
->inline(false)
->onColor('success')
->offColor('danger')
->columnSpanFull(),
])
]),
Tabs\Tab::make('Содержание ресурса')
->icon('heroicon-o-document-text')
->schema([
Repeater::make('content')
->label('Элементы ресурса')
->helperText('Добавьте и настройте элементы ресурса')
->addActionLabel('Добавить элемент')
->collapsed()
->itemLabel(fn (array $state): ?string => $state['title'] ?? 'Новый элемент')
->required()
->schema([
Forms\Components\Section::make('Быстрая настройка')
->description('Выберите тип и источник данных')
->collapsible()
->collapsed()
->schema([
Forms\Components\Grid::make(2)
->schema([
Forms\Components\Select::make('model_select')
->label('Тип данных')
->placeholder('Выберите тип данных')
->helperText('Выберите тип контента для этого элемента')
->options([
'Post' => 'Новость',
'Page' => 'Страница',
'Event' => 'Мероприятие',
'Custom' => 'Кастомная ссылка',
])
->live(onBlur: true)
->afterStateUpdated(function (string $operation, string|null $state, Forms\Set $set, Forms\Get $get) {
if ($get('model_select') === 'Custom') {
$set('model', null);
$set('title', null);
$set('content', null);
$set('link', null);
}
}),
Forms\Components\Select::make('model')
->label('Выбор элемента')
->placeholder('Выберите элемент')
->helperText(function (Forms\Get $get) {
if ($get('model_select') === 'Post') return 'Выберите новость';
if ($get('model_select') === 'Page') return 'Выберите страницу';
if ($get('model_select') === 'Event') return 'Выберите мероприятие';
return 'Доступно после выбора типа данных';
})
->searchable()
->live(onBlur: true)
->afterStateUpdated(function (string $operation, string|null $state, Forms\Set $set, Forms\Get $get) {
if ($get('model_select') === 'Post') {
$post = Post::find($state);
if ($post) {
$set('title', $post->title);
$relativeUrl = parse_url(route('client.post.show', $post->slug), PHP_URL_PATH);
$set('link', $relativeUrl);
}
}
if ($get('model_select') === 'Page') {
$page = Page::find($state);
if ($page) {
$set('title', $page->title);
$set('link', $page->path);
}
}
if ($get('model_select') === 'Event') {
$event = Event::find($state);
if ($event) {
$set('title', $event->title);
$relativeUrl = parse_url(route('client.event.show', $event->slug), PHP_URL_PATH);
$set('link', $relativeUrl);
}
}
})
->options(function (Forms\Get $get) {
if ($get('model_select') === 'Post') {
return Post::where('status', '=', 'published')->pluck('title', 'id');
}
if ($get('model_select') === 'Page') {
return Page::whereNotNull('title')->pluck('title', 'id');
}
if ($get('model_select') === 'Event') {
return Event::all()->pluck('title', 'id');
}
return [];
})
->disabled(fn (Forms\Get $get) => empty($get('model_select')) || $get('model_select') === 'Custom'),
]),
]),
TextInput::make('title')
->label('Заголовок')
->placeholder('Введите заголовок элемента')
->helperText('Заголовок будет отображаться пользователям')
->required()
->maxLength(255),
FileUpload::make('image')
->label('Изображение предпросмотра')
->helperText('Рекомендуемый формат: PNG, JPEG, JPG')
->image()
->optimize('webp')
->resize(50)
->disk('public')
->directory('images')
->imageEditor()
->downloadable()
->openable(),
Forms\Components\Grid::make(2)
->schema([
Forms\Components\TextInput::make('link')
->label('Ссылка')
->placeholder('https://example.com или /path')
->helperText('URL-адрес или относительный путь')
->required()
->maxLength(255),
Forms\Components\TextInput::make('link_text')
->label('Текст кнопки')
->placeholder('Например: Читать далее')
->helperText('Текст для кнопки перехода')
->default('Читать')
->required()
->maxLength(50),
]),
])
->columnSpanFull(),
]),
]),
]), ]),
]); ]);
} }
@@ -4,15 +4,17 @@ namespace App\Filament\Resources\PageResource\Pages;
use App\Filament\Resources\PageResource; use App\Filament\Resources\PageResource;
use App\Models\SubSection; use App\Models\SubSection;
use App\Services\Filament\Domain\Seo\SeoGeneratorService;
use App\Services\Filament\Traits\SeoGenerate;
use Filament\Actions; use Filament\Actions;
use Filament\Resources\Pages\CreateRecord; use Filament\Resources\Pages\CreateRecord;
use Illuminate\Support\Str; use Illuminate\Support\Str;
class CreatePage extends CreateRecord class CreatePage extends CreateRecord
{ {
protected static string $resource = PageResource::class; use SeoGenerate;
protected array $seoData; protected static string $resource = PageResource::class;
protected function mutateFormDataBeforeCreate(array $data): array protected function mutateFormDataBeforeCreate(array $data): array
{ {
@@ -27,8 +29,6 @@ class CreatePage extends CreateRecord
} }
unset($data['sub_section_id']); unset($data['sub_section_id']);
$this->seoData = $this->generateSeo($data);
$data['search_data'] = $this->generateSearchData($data['content']); $data['search_data'] = $this->generateSearchData($data['content']);
return $data; return $data;
@@ -36,23 +36,9 @@ class CreatePage extends CreateRecord
protected function afterCreate(): void protected function afterCreate(): void
{ {
$this->record->seo()->create($this->seoData); $this->createSeo($this->record);
} }
private function generateSeo(array $data) : array
{
$title = $data['title'];
$rowData = $this->getFirstBlockByName('paragraph', $data['content']);
if ($rowData !== null) {
$description = strip_tags($rowData['data']['content']);
} else {
$description = null;
}
return [
'title' => $title,
'description' => Str::limit(htmlspecialchars($description, ENT_QUOTES, 'UTF-8'), 160),
];
}
private function getFirstBlockByName(string $name, array $content) : array|null private function getFirstBlockByName(string $name, array $content) : array|null
{ {
$data = null; $data = null;
@@ -3,20 +3,20 @@
namespace App\Filament\Resources\PageResource\Pages; namespace App\Filament\Resources\PageResource\Pages;
use App\Filament\Resources\PageResource; use App\Filament\Resources\PageResource;
use App\Services\Filament\Domain\Seo\SeoGeneratorService;
use App\Services\Filament\Traits\SeoGenerate;
use Filament\Actions; use Filament\Actions;
use Filament\Resources\Pages\EditRecord; use Filament\Resources\Pages\EditRecord;
use Illuminate\Support\Str; use Illuminate\Support\Str;
class EditPage extends EditRecord class EditPage extends EditRecord
{ {
protected static string $resource = PageResource::class; use SeoGenerate;
protected array $seoData; protected static string $resource = PageResource::class;
protected function mutateFormDataBeforeSave(array $data): array protected function mutateFormDataBeforeSave(array $data): array
{ {
$this->seoData = $this->generateSeo($data);
$data['search_data'] = $this->generateSearchData($data['content']); $data['search_data'] = $this->generateSearchData($data['content']);
return $data; return $data;
@@ -34,8 +34,7 @@ class EditPage extends EditRecord
} }
} }
$this->record->seo()->update($this->seoData); $this->updateSeo($this->record);
} }
@@ -47,22 +46,6 @@ class EditPage extends EditRecord
]; ];
} }
private function generateSeo(array $data) : array
{
$title = $data['title'];
$rowData = $this->getFirstBlockByName('paragraph', $data['content']);
if ($rowData !== null) {
$description = strip_tags($rowData['data']['content']);
} else {
$description = null;
}
return [
'title' => $title,
'description' => Str::limit(htmlspecialchars($description, ENT_QUOTES, 'UTF-8'), 160),
];
}
private function getFirstBlockByName(string $name, array $content) : array|null private function getFirstBlockByName(string $name, array $content) : array|null
{ {
$data = null; $data = null;
@@ -38,12 +38,11 @@ class SectionRelationManager extends RelationManager
// //
]) ])
->headerActions([ ->headerActions([
Tables\Actions\CreateAction::make()->visible(!$this->ownerRecord->section->exists()), // Tables\Actions\CreateAction::make()->visible(!$this->ownerRecord->section->exists()),
Tables\Actions\AssociateAction::make()
]) ])
->actions([ ->actions([
Tables\Actions\EditAction::make(), Tables\Actions\EditAction::make(),
Tables\Actions\DetachAction::make(), Tables\Actions\DissociateAction::make(),
]) ])
->bulkActions([ ->bulkActions([
Tables\Actions\BulkActionGroup::make([ Tables\Actions\BulkActionGroup::make([
-28
View File
@@ -2,41 +2,14 @@
namespace App\Filament\Resources; namespace App\Filament\Resources;
use App\Enums\PostStatus;
use App\Filament\Components\Forms\PostForm; use App\Filament\Components\Forms\PostForm;
use App\Filament\Resources\PostResource\Pages; use App\Filament\Resources\PostResource\Pages;
use App\Models\Category;
use App\Models\Page;
use App\Models\Post; use App\Models\Post;
use BezhanSalleh\FilamentShield\Contracts\HasShieldPermissions; use BezhanSalleh\FilamentShield\Contracts\HasShieldPermissions;
use Filament\Actions\DeleteAction;
use Filament\Facades\Filament;
use Filament\Forms;
use Filament\Forms\Components\Builder;
use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\RichEditor;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\SpatieTagsInput;
use Filament\Forms\Components\Tabs;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form; use Filament\Forms\Form;
use Filament\Forms\Get;
use Filament\Forms\Set;
use Filament\Resources\Resource; use Filament\Resources\Resource;
use Filament\Tables; use Filament\Tables;
use Filament\Tables\Table; use Filament\Tables\Table;
use Filament\Infolists\Components\Card;
use Illuminate\Database\Eloquent\SoftDeletingScope;
use Illuminate\Http\File;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Carbon;
use Illuminate\Support\Str;
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
use Mohamedsabil83\FilamentFormsTinyeditor\Components\TinyEditor;
class PostResource extends Resource implements HasShieldPermissions class PostResource extends Resource implements HasShieldPermissions
{ {
@@ -59,7 +32,6 @@ class PostResource extends Resource implements HasShieldPermissions
{ {
return $table return $table
->columns([ ->columns([
// Tables\Columns\TextColumn::make('id')->sortable(),
Tables\Columns\TextColumn::make('created_at')->label('Дата создания')->sortable(), Tables\Columns\TextColumn::make('created_at')->label('Дата создания')->sortable(),
Tables\Columns\TextColumn::make('title')->label('Заголовок')->sortable()->searchable(), Tables\Columns\TextColumn::make('title')->label('Заголовок')->sortable()->searchable(),
Tables\Columns\TextColumn::make('status')->label('Статус')->sortable()->badge(), Tables\Columns\TextColumn::make('status')->label('Статус')->sortable()->badge(),
@@ -10,13 +10,15 @@ use App\Services\Filament\Domain\Posts\PostNotificationService;
use App\Services\Filament\Domain\Posts\PostSeoGenerator; use App\Services\Filament\Domain\Posts\PostSeoGenerator;
use App\Services\Filament\Domain\Posts\PostSliderService; use App\Services\Filament\Domain\Posts\PostSliderService;
use App\Services\Filament\Domain\Posts\VkPostPublisher; use App\Services\Filament\Domain\Posts\VkPostPublisher;
use App\Services\Filament\Domain\Seo\SeoGeneratorService;
use App\Services\Filament\Traits\SeoGenerate;
use Filament\Resources\Pages\CreateRecord; use Filament\Resources\Pages\CreateRecord;
class CreatePost extends CreateRecord class CreatePost extends CreateRecord
{ {
protected static string $resource = PostResource::class; use SeoGenerate;
protected array $seoData; protected static string $resource = PostResource::class;
protected array $publicationAgreements; protected array $publicationAgreements;
protected array $slideData; protected array $slideData;
@@ -45,7 +47,7 @@ class CreatePost extends CreateRecord
protected function afterCreate(): void protected function afterCreate(): void
{ {
$this->handleSlides(); $this->handleSlides();
$this->generateSeo(); $this->createSeo($this->record);
$this->sendNotifications(); $this->sendNotifications();
$this->publishToVk(); $this->publishToVk();
} }
@@ -63,15 +65,6 @@ class CreatePost extends CreateRecord
(new PostSliderService($sliderDTO, $this->record))->create(); (new PostSliderService($sliderDTO, $this->record))->create();
} }
protected function generateSeo(): void
{
$seoData = (new PostSeoGenerator())->generate([
'title' => $this->record->title,
'content' => $this->record->content,
'preview' => $this->record->preview,
]);
$this->record->seo()->create($seoData);
}
protected function sendNotifications(): void protected function sendNotifications(): void
{ {
@@ -11,23 +11,26 @@ use App\Services\Filament\Domain\Posts\PostNotificationService;
use App\Services\Filament\Domain\Posts\PostSeoGenerator; use App\Services\Filament\Domain\Posts\PostSeoGenerator;
use App\Services\Filament\Domain\Posts\PostSliderService; use App\Services\Filament\Domain\Posts\PostSliderService;
use App\Services\Filament\Domain\Posts\VkPostPublisher; use App\Services\Filament\Domain\Posts\VkPostPublisher;
use App\Services\Filament\Domain\Seo\SeoGeneratorService;
use App\Services\Filament\Traits\SeoGenerate;
use Carbon\Carbon; use Carbon\Carbon;
use Filament\Actions; use Filament\Actions;
use Filament\Resources\Pages\EditRecord; use Filament\Resources\Pages\EditRecord;
class EditPost extends EditRecord class EditPost extends EditRecord
{ {
use SeoGenerate;
protected static string $resource = PostResource::class; protected static string $resource = PostResource::class;
protected array $seoData;
protected array $publicationAgreements; protected array $publicationAgreements;
protected array $slideData; protected array $slideData;
protected function mutateFormDataBeforeFill(array $data): array protected function mutateFormDataBeforeFill(array $data): array
{ {
$post = Post::query()->with(['seo', 'mainSlider'])->find($data['id']); $post = Post::query()->with(['seo', 'slide'])->find($data['id']);
$data['slide'] = $post->mainSlider->toArray() ?? null; $data['slide'] = $post->slide->toArray() ?? null;
return $data; return $data;
} }
@@ -52,7 +55,7 @@ class EditPost extends EditRecord
protected function afterSave(): void protected function afterSave(): void
{ {
$this->handleSlides(); $this->handleSlides();
$this->generateSeo(); $this->updateSeo($this->record);
$this->sendNotifications(); $this->sendNotifications();
$this->publishToVk(); $this->publishToVk();
} }
@@ -73,7 +76,7 @@ class EditPost extends EditRecord
protected function generateSeo(): void protected function generateSeo(): void
{ {
$seoData = (new PostSeoGenerator())->generate([ $seoData = app(SeoGeneratorService::class)->generate([
'title' => $this->record->title, 'title' => $this->record->title,
'content' => $this->record->content, 'content' => $this->record->content,
'preview' => $this->record->preview, 'preview' => $this->record->preview,
+119 -126
View File
@@ -3,151 +3,104 @@
namespace App\Filament\Resources; namespace App\Filament\Resources;
use App\Filament\Resources\ScheduleResource\Pages; use App\Filament\Resources\ScheduleResource\Pages;
use App\Filament\Resources\ScheduleResource\RelationManagers;
use App\Helpers\ByteConverter;
use App\Models\Category;
use App\Models\EducationalGroup; use App\Models\EducationalGroup;
use App\Models\Schedule; use App\Models\Schedule;
use Closure;
use Filament\Forms; use Filament\Forms;
use Filament\Forms\Components\Builder;
use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\FileUpload; use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Hidden; use Filament\Forms\Components\Grid;
use Filament\Forms\Components\RichEditor;
use Filament\Forms\Components\Section; use Filament\Forms\Components\Section;
use Filament\Forms\Components\Select; use Filament\Forms\Components\Select;
use Filament\Forms\Components\SpatieTagsInput;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Form; use Filament\Forms\Form;
use Filament\Forms\Get;
use Filament\Resources\Resource; use Filament\Resources\Resource;
use Filament\Tables; use Filament\Tables;
use Filament\Tables\Columns\IconColumn; use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table; use Filament\Tables\Table;
use Illuminate\Database\Eloquent\SoftDeletingScope;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Livewire\Component;
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile; use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
use Livewire\Livewire;
class ScheduleResource extends Resource class ScheduleResource extends Resource
{ {
protected static ?string $navigationGroup = 'Расписание и группы'; protected static ?string $navigationGroup = 'Расписание и группы';
protected static ?string $model = Schedule::class; protected static ?string $model = Schedule::class;
protected static ?string $navigationIcon = 'heroicon-o-calendar';
protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack'; protected static ?string $pluralLabel = 'Расписания';
protected static ?string $modelLabel = 'расписание';
protected static ?string $pluralLabel = 'Расписание';
protected static array $weekDays = ['Понедельник', 'Вторник', 'Среда', 'Четверг', 'Пятница', 'Суббота', 'Воскресенье'];
protected static array $typeWeek = ['Четная', 'Нечетная'];
public static function form(Form $form): Form public static function form(Form $form): Form
{ {
return $form return $form
->schema([ ->schema([
Section::make() Section::make('Основные настройки')
->description('Основная информация о расписании')
->collapsible()
->schema([ ->schema([
Forms\Components\Grid::make(2)->schema([ Grid::make(2)
Select::make('educational_group_id')->options(EducationalGroup::all()->pluck('title', 'id')) ->schema([
->live() Select::make('educational_group_id')
->label('Выбрать группу') ->label('Учебная группа')
->required(), ->options(EducationalGroup::query()->orderBy('title')->pluck('title', 'id'))
// TextInput::make('title') ->searchable()
// ->live() ->preload()
// ->label('Заголовок')->required(), ->required()
->live()
->helperText('Выберите группу для которой создается расписание'),
// Select::make('type')->options([ Toggle::make('is_zaoch')
// 'schedule' => 'Обычное расписание', ->label('Форма обучения')
// 'interval' => 'Временное расписание', ->inline(false)
// 'exam' => 'Промежуточная аттестация', ->onColor('success')
// ])->label('Тип расписания')->required()->live(), ->offColor('primary')
Forms\Components\Toggle::make('is_zaoch')->label('Очная|Заочная')->inline(false), ->helperText('Очная | Заочная')
->afterStateHydrated(function (Toggle $component, $state) {
$component->state((bool) $state);
}),
]),
]),
]), Section::make('Файлы расписания')
// Forms\Components\Repeater::make('days')->label('')->schema([ ->description('Загрузите файлы с расписанием')
// Forms\Components\Repeater::make('form')->label('')->schema([ ->collapsible()
// Forms\Components\Repeater::make('weeks')->label('')->schema([
// Forms\Components\Repeater::make('lesson_info')->label('')->schema([
// TextInput::make('title')->label('Название-пары'),
// TextInput::make('teacher')->label('Преподаватель'),
// TextInput::make('studyRoom')->label('Кабинет')
// ])->live()->maxItems(2)->collapsed()->addActionLabel('Добавить подгруппу')->columns(3)
// ->itemLabel(function (Get $get, $state) {
// static $count = 1;
// if (count($get('lesson_info')) === 1) {
// return "Общая группа";
// } else {
// $nmb = $count++ % 2 == 0 ? 2 : 1;
// return "Подгруппа " . $nmb; }
// }),
// ])->maxItems(2)->addActionLabel('Добавить четную/нечетную неделю')
// ->itemLabel(function (Get $get) {
// static $position = 0;
// if (count($get('weeks')) === 1) {
// return "Общая неделя";
// } else {
// $nmb = $position++ % 2 == 0 ? 0 : 1;
// return self::$typeWeek[$nmb] . " неделя";
// }
// })
// ->collapsed()
// ])
// ->maxItems(5)
// ->itemLabel(function (Get $get) {
// static $count = 0;
// $maxCount = count($get('form'));
// $count = ($count++ <= $maxCount) ? $count : 1;
// return "Пара #" . $count;
// })
// ->addActionLabel('Добавить пару'),
// ])->maxItems(6)->minItems(1)->itemLabel(function ($state) {
// static $position = 0;
// return self::$weekDays[$position++];
// })->addActionLabel('Добавить день недели')->collapsed()->defaultItems(6)->hidden(function (callable $get) {
// if ($get('type') === 'schedule' || $get('type') === 'interval') {
// return false;
// } else {
// return true;
// }
// }),
]),
Section::make()
->schema([ ->schema([
Forms\Components\Repeater::make('file')->schema([ Forms\Components\Repeater::make('file')
->label('')
->addActionLabel('Добавить файл расписания')
->schema([
TextInput::make('title')
->label('Название файла')
->required()
->maxLength(255)
->placeholder('Например: "Расписание на весенний семестр 2024"')
->helperText('Укажите понятное название файла для идентификации'),
TextInput::make('title') FileUpload::make('path')
->required() ->label('Файл PDF')
->maxLength(255) ->required()
->autofocus(), ->acceptedFileTypes(['application/pdf'])
FileUpload::make('path') ->maxSize(5120) // 5MB
->required() ->disk('public')
->getUploadedFileNameForStorageUsing( ->directory('schedules')
fn (TemporaryUploadedFile $file): string => ->downloadable()
str(Str::slug(pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME) . '-' . Carbon::now()->timestamp ) . '.' . $file->getClientOriginalExtension()) ->openable()
) ->previewable(false)
->acceptedFileTypes([ ->helperText('Только PDF файлы, макс. размер 5MB')
'application/pdf', ->getUploadedFileNameForStorageUsing(
]) fn (TemporaryUploadedFile $file): string =>
->maxSize(512000) str(Str::slug(pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME) . '-' . Carbon::now()->timestamp) . '.' . $file->getClientOriginalExtension()
->disk('public') )
->directory('files') ->afterStateUpdated(function ($set, $state) {
->downloadable() $set('title', pathinfo($state?->getClientOriginalName(), PATHINFO_FILENAME));
->afterStateUpdated(function ($set, $state) { })
$set('title', pathinfo($state?->getClientOriginalName(), PATHINFO_FILENAME)); ->visibility('public')),
}) ])
->visibility('public') ->itemLabel(fn (array $state): ?string => $state['title'] ?? 'Новый файл')
]), ->collapsible()
]) ->cloneable()
->defaultItems(1),
]),
]); ]);
} }
@@ -155,29 +108,69 @@ class ScheduleResource extends Resource
{ {
return $table return $table
->columns([ ->columns([
Tables\Columns\TextColumn::make('title'), TextColumn::make('educational_group.title')
Tables\Columns\TextColumn::make('type'), ->label('Учебная группа')
->sortable()
->searchable(),
TextColumn::make('file_count')
->label('Файлов')
->getStateUsing(fn ($record) => count($record->file ?? []))
->badge(),
IconColumn::make('is_zaoch') IconColumn::make('is_zaoch')
->boolean(), ->label('Форма обучения')
->boolean()
->trueIcon('heroicon-o-academic-cap')
->falseIcon('heroicon-o-building-office')
->trueColor('success')
->falseColor('primary')
->formatStateUsing(fn ($state) => $state ? 'Заочная' : 'Очная'),
TextColumn::make('updated_at')
->label('Обновлено')
->dateTime('d.m.Y H:i')
->sortable(),
]) ])
->filters([ ->filters([
// Tables\Filters\SelectFilter::make('educational_group_id')
->label('Учебная группа')
->options(EducationalGroup::query()->orderBy('title')->pluck('title', 'id'))
->searchable(),
Tables\Filters\TernaryFilter::make('is_zaoch')
->label('Форма обучения')
->placeholder('Все')
->trueLabel('Заочная')
->falseLabel('Очная'),
]) ])
->actions([ ->actions([
Tables\Actions\EditAction::make(), Tables\Actions\EditAction::make()
->iconButton()
->tooltip('Редактировать'),
Tables\Actions\ViewAction::make()
->iconButton()
->tooltip('Просмотреть'),
]) ])
->bulkActions([ ->bulkActions([
Tables\Actions\BulkActionGroup::make([ Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(), Tables\Actions\DeleteBulkAction::make()
->label('Удалить выбранные')
->modalHeading('Удаление расписаний')
->modalDescription('Вы уверены, что хотите удалить выбранные расписания? Это действие нельзя отменить.'),
]), ]),
]); ])
->emptyStateActions([
Tables\Actions\CreateAction::make()
->label('Добавить расписание'),
])
->defaultSort('educational_group.title');
} }
public static function getRelations(): array public static function getRelations(): array
{ {
return [ return [];
//
];
} }
public static function getPages(): array public static function getPages(): array
@@ -188,4 +181,4 @@ class ScheduleResource extends Resource
'edit' => Pages\EditSchedule::route('/{record}/edit'), 'edit' => Pages\EditSchedule::route('/{record}/edit'),
]; ];
} }
} }
@@ -157,9 +157,7 @@ class RoleResource extends Resource implements HasShieldPermissions
public static function getNavigationGroup(): ?string public static function getNavigationGroup(): ?string
{ {
return Utils::isResourceNavigationGroupEnabled() return 'Настройки приложения';
? __('filament-shield::filament-shield.nav.group')
: '';
} }
public static function getNavigationLabel(): string public static function getNavigationLabel(): string
+5
View File
@@ -27,6 +27,11 @@ class SlideResource extends Resource
protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack'; protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack';
protected static ?string $navigationGroup = 'Виджеты';
protected static ?string $pluralLabel = 'Слайды';
protected static ?string $navigationParentItem = 'Слайдеры';
public static function form(Form $form): Form public static function form(Form $form): Form
{ {
return $form return $form
+80 -21
View File
@@ -6,37 +6,61 @@ use App\Filament\Resources\SliderResource\Pages;
use App\Filament\Resources\SliderResource\RelationManagers; use App\Filament\Resources\SliderResource\RelationManagers;
use App\Models\Slider; use App\Models\Slider;
use Filament\Forms; use Filament\Forms;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle; use Filament\Forms\Components\Toggle;
use Filament\Forms\Form; use Filament\Forms\Form;
use Filament\Resources\Resource; use Filament\Resources\Resource;
use Filament\Tables; use Filament\Tables;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table; use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletingScope;
use Illuminate\Support\Str; use Illuminate\Support\Str;
class SliderResource extends Resource class SliderResource extends Resource
{ {
protected static ?string $model = Slider::class; protected static ?string $model = Slider::class;
protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack'; protected static ?string $navigationGroup = 'Виджеты';
protected static ?string $navigationLabel = 'Слайдеры';
protected static ?string $modelLabel = 'Слайдер';
protected static ?string $pluralModelLabel = 'Слайдеры';
protected static ?string $navigationIcon = 'heroicon-o-photo';
public static function form(Form $form): Form public static function form(Form $form): Form
{ {
return $form return $form
->schema([ ->schema([
Forms\Components\Section::make()->schema([ Section::make('Настройки слайдера')
Forms\Components\TextInput::make('title') ->description('Основные параметры отображения слайдера')
->live(onBlur: true) ->collapsible()
->afterStateUpdated(function (string $operation, string|null $state, Forms\Set $set) { ->schema([
$set('slug', Str::slug($state)); TextInput::make('title')
}) ->label('Название слайдера')
->label('Заголовок слайдера') ->placeholder('Например: Главный слайдер')
->required(), ->helperText('Это название будет использоваться в административной панели')
TextInput::make('slug')->label('Slug')->unique(ignoreRecord: true)->readOnly()->required(), ->required()
Toggle::make('is_active')->default(true)->label('Активный слайдер')->inline(false), ->maxLength(255)
]), ->live(onBlur: true)
->afterStateUpdated(function (string $operation, string|null $state, Forms\Set $set) {
$set('slug', Str::slug($state));
}),
TextInput::make('slug')
->label('URL-идентификатор')
->helperText('Автоматически генерируется из названия')
->required()
->unique(ignoreRecord: true)
->readOnly()
->maxLength(255),
Toggle::make('is_active')
->label('Активность слайдера')
->helperText('Отключите, чтобы временно скрыть слайдер')
->default(true)
->inline(false)
->onColor('success')
->offColor('danger'),
]),
]); ]);
} }
@@ -44,25 +68,60 @@ class SliderResource extends Resource
{ {
return $table return $table
->columns([ ->columns([
// TextColumn::make('title')
->label('Название')
->sortable()
->searchable()
->description(fn (Slider $record) => $record->slug),
TextColumn::make('slides_count')
->counts('slides')
->label('Кол-во слайдов')
->badge()
->color(fn (int $state): string => $state > 0 ? 'success' : 'danger'),
TextColumn::make('is_active')
->label('Статус')
->badge()
->color(fn (bool $state): string => $state ? 'success' : 'danger')
->formatStateUsing(fn (bool $state): string => $state ? 'Активен' : 'Неактивен'),
]) ])
->filters([ ->filters([
// Tables\Filters\SelectFilter::make('is_active')
->label('Статус активности')
->options([
true => 'Активные',
false => 'Неактивные',
]),
]) ])
->actions([ ->actions([
Tables\Actions\EditAction::make(), Tables\Actions\EditAction::make()
->icon('heroicon-o-pencil')
->tooltip('Редактировать'),
Tables\Actions\Action::make('manage_slides')
->icon('heroicon-o-photo')
->tooltip('Управление слайдами')
->url(fn (Slider $record) => SliderResource::getUrl('edit', ['record' => $record]) . '?activeRelationManager=0'),
]) ])
->bulkActions([ ->bulkActions([
Tables\Actions\BulkActionGroup::make([ Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(), Tables\Actions\DeleteBulkAction::make()
->icon('heroicon-o-trash')
->label('Удалить выбранное'),
]), ]),
]); ])
->emptyStateActions([
Tables\Actions\CreateAction::make()
->label('Создать слайдер'),
])
->defaultSort('title', 'asc');
} }
public static function getRelations(): array public static function getRelations(): array
{ {
return [ return [
// RelationManagers\SlidesRelationManager::class,
]; ];
} }
@@ -74,4 +133,4 @@ class SliderResource extends Resource
'edit' => Pages\EditSlider::route('/{record}/edit'), 'edit' => Pages\EditSlider::route('/{record}/edit'),
]; ];
} }
} }
@@ -0,0 +1,310 @@
<?php
namespace App\Filament\Resources\SliderResource\RelationManagers;
use App\Models\Event;
use App\Models\Page;
use App\Models\Post;
use Filament\Forms;
use Filament\Forms\Components\ColorPicker;
use Filament\Forms\Components\DateTimePicker;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Components\ToggleButtons;
use Filament\Forms\Form;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables;
use Filament\Tables\Columns\ImageColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Support\Carbon;
class SlidesRelationManager extends RelationManager
{
protected static string $relationship = 'slides';
public function form(Form $form): Form
{
return $form
->schema([
Section::make('Быстрая настройка слайда')
->description('Выберите источник данных для слайда')
->collapsible()
->collapsed()
->schema([
Forms\Components\Grid::make(2)
->schema([
Forms\Components\Select::make('model_select')
->label('Тип контента')
->placeholder('Выберите тип контента')
->helperText('Выберите откуда брать данные для слайда')
->options([
'Post' => 'Новость',
'Page' => 'Страница',
'Event' => 'Мероприятие',
'Custom' => 'Кастомная ссылка',
])
->live(onBlur: true)
->afterStateUpdated(function (string $operation, string|null $state, Forms\Set $set, Forms\Get $get) {
if ($get('model_select') === 'Custom') {
$set('model', null);
$set('title', null);
$set('content', null);
$set('link', null);
}
})
->dehydrated(false),
Forms\Components\Select::make('model')
->label('Выбор элемента')
->placeholder('Выберите элемент')
->helperText(function (Forms\Get $get) {
if ($get('model_select') === 'Post') return 'Выберите новость';
if ($get('model_select') === 'Page') return 'Выберите страницу';
if ($get('model_select') === 'Event') return 'Выберите мероприятие';
return 'Доступно после выбора типа контента';
})
->searchable()
->live(onBlur: true)
->afterStateUpdated(function (string $operation, string|null $state, Forms\Set $set, Forms\Get $get) {
if ($get('model_select') === 'Post' && $state) {
$post = Post::find($state);
if ($post) {
$set('title', $post->title);
$relativeUrl = parse_url(route('client.post.show', $post->slug), PHP_URL_PATH);
$set('link', $relativeUrl);
}
}
if ($get('model_select') === 'Page' && $state) {
$page = Page::find($state);
if ($page) {
$set('title', $page->title);
$set('link', $page->path);
}
}
if ($get('model_select') === 'Event' && $state) {
$event = Event::find($state);
if ($event) {
$set('title', $event->title);
$relativeUrl = parse_url(route('client.event.show', $event->slug), PHP_URL_PATH);
$set('link', $relativeUrl);
}
}
})
->options(function (Forms\Get $get) {
if ($get('model_select') === 'Post') {
return Post::where('status', 'published')->pluck('title', 'id');
}
if ($get('model_select') === 'Page') {
return Page::whereNotNull('title')->pluck('title', 'id');
}
if ($get('model_select') === 'Event') {
return Event::all()->pluck('title', 'id');
}
return [];
})
->disabled(fn (Forms\Get $get) => empty($get('model_select')))
->dehydrated(false),
]),
]),
Section::make('Контент слайда')
->schema([
Section::make('Текстовая часть')
->collapsible()
->schema([
TextInput::make('title')
->label('Заголовок слайда')
->placeholder('Введите заголовок слайда')
->maxLength(255),
Textarea::make('content')
->label('Описание слайда')
->placeholder('Введите текст слайда')
->maxLength(1000),
Forms\Components\Grid::make(2)
->schema([
ColorPicker::make('color_theme')
->label('Цвет текста')
->default('#ffffff')
->required(),
ToggleButtons::make('settings.text_position')
->label('Позиция текста')
->helperText('Расположение текста на слайде')
->options([
'left' => 'Слева',
'center' => 'По центру',
'right' => 'Справа'
])
->inline()
->grouped()
->default('left'),
]),
Forms\Components\Grid::make(2)
->schema([
Toggle::make('active_button')
->label('Показывать кнопку')
->helperText('Если выключено - ссылка будет работать при клике на весь слайд')
->inline(false)
->dehydrated(false)
->default(true)
->live(),
TextInput::make('settings.link_text')
->label('Текст кнопки')
->placeholder('Например: Подробнее')
->default('Читать')
->disabled(fn (Forms\Get $get) => !$get('active_button'))
->maxLength(50),
]),
]),
Section::make('Изображение')
->collapsible()
->schema([
FileUpload::make('image.url')
->label('Изображение слайда')
->helperText('Рекомендуемое соотношение сторон: 16:9')
->image()
->optimize('webp')
->resize(50)
->disk('public')
->directory('slider-images')
->imageEditor()
->required()
->downloadable()
->openable(),
ToggleButtons::make('image.shading')
->label('Затемнение фона')
->helperText('Для лучшей читаемости текста')
->options([
'1' => 'Нет',
'0.7' => 'Слабое',
'0.5' => 'Среднее',
'0.3' => 'Сильное',
])
->inline()
->grouped()
->default('0.5'),
]),
Section::make('Настройки отображения')
->collapsible()
->schema([
Forms\Components\Grid::make(2)
->schema([
DateTimePicker::make('start_time')
->label('Дата начала показа')
->helperText('Когда слайд станет активным')
->native(false)
->displayFormat('d/m/Y H:i')
->seconds(false)
->default(Carbon::now())
->minDate(fn($record, $context) => $context === 'edit' ? $record?->start_time : Carbon::now()),
DateTimePicker::make('end_time')
->label('Дата окончания показа')
->helperText('Когда слайд перестанет показываться')
->native(false)
->displayFormat('d/m/Y H:i')
->seconds(false)
->default(Carbon::now()->addWeeks(2))
->minDate(fn (Forms\Get $get) => $get('start_time') ?: Carbon::now()),
]),
TextInput::make('link')
->label('Целевая ссылка')
->placeholder('URL или относительный путь')
->required()
->maxLength(255),
Toggle::make('is_active')
->label('Активный слайд')
->helperText('Отключите чтобы временно скрыть слайд')
->default(true)
->inline(false)
->onColor('success')
->offColor('danger'),
]),
]),
]);
}
public function table(Table $table): Table
{
return $table
->recordTitleAttribute('title')
->defaultSort('sort')
->reorderable('sort')
->columns([
Tables\Columns\TextColumn::make('sort')
->label('Порядок')
->sortable(),
ImageColumn::make('image.url')
->label('Изображение')
->size(80),
Tables\Columns\TextColumn::make('title')
->label('Заголовок')
->searchable()
->limit(30),
Tables\Columns\ToggleColumn::make('is_active')
->label('Активен')
->onColor('success')
->offColor('danger')
->updateStateUsing(function ($record, $state) {
$record->is_active = $state;
$record->save();
}),
Tables\Columns\TextColumn::make('start_time')
->label('Начало')
->date('d.m.Y')
->sortable(),
Tables\Columns\TextColumn::make('end_time')
->label('Окончание')
->date('d.m.Y')
->sortable(),
])
->filters([
Tables\Filters\SelectFilter::make('is_active')
->label('Статус')
->options([
true => 'Активные',
false => 'Неактивные',
]),
])
->headerActions([
Tables\Actions\CreateAction::make()
->label('Добавить слайд'),
])
->actions([
Tables\Actions\EditAction::make()
->icon('heroicon-o-pencil')
->tooltip('Редактировать'),
Tables\Actions\DeleteAction::make()
->icon('heroicon-o-trash')
->tooltip('Удалить'),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make()
->label('Удалить выбранное'),
]),
])
->emptyStateActions([
Tables\Actions\CreateAction::make()
->label('Добавить слайд'),
]);
}}
@@ -20,7 +20,7 @@ class UserDetailResource extends Resource
{ {
protected static ?string $model = UserDetail::class; protected static ?string $model = UserDetail::class;
protected static ?string $navigationGroup = 'Settings'; protected static ?string $navigationGroup = 'Настройки приложения';
protected static ?string $pluralLabel = 'Доп. Информация'; protected static ?string $pluralLabel = 'Доп. Информация';
+1 -1
View File
@@ -21,7 +21,7 @@ class UserResource extends Resource implements HasShieldPermissions
{ {
protected static ?string $model = User::class; protected static ?string $model = User::class;
protected static ?string $navigationGroup = 'Settings'; protected static ?string $navigationGroup = 'Настройки приложения';
protected static ?string $pluralLabel = 'Пользователи'; protected static ?string $pluralLabel = 'Пользователи';
@@ -2,37 +2,76 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Enums\CacheKeys;
use App\Http\Resources\ClientAcademicJournalListResource; use App\Http\Resources\ClientAcademicJournalListResource;
use App\Http\Resources\ClientVirtualExhibitionListResource;
use App\Models\AcademicJournal; use App\Models\AcademicJournal;
use App\Models\JournalIssue; use App\Models\JournalIssue;
use App\Models\VirtualExhibition; use App\Services\App\Breadcrumb\BreadcrumbService;
use Illuminate\Http\Request; use App\Services\App\Seo\SeoPageProvider;
use Illuminate\Support\Facades\Cache;
use Inertia\Inertia; use Inertia\Inertia;
class ClientAcademicJournalController extends Controller class ClientAcademicJournalController extends Controller
{ {
public function __construct(readonly SeoPageProvider $seoPageProvider){}
public function index() public function index()
{ {
$journals = ClientAcademicJournalListResource::collection(AcademicJournal::query()->get()); $journals = Cache::remember(
CacheKeys::ACADEMIC_JOURNALS_PREFIX->value . 'list',
now()->addWeek(), // Кешируем на неделю, так как журналы меняются редко
function () {
return ClientAcademicJournalListResource::collection(
AcademicJournal::query()->get()
);
}
);
return Inertia::render('Client/AcademicJournals/Index', compact('journals')); $seo = $this->seoPageProvider->getSeoForCurrentPage();
return Inertia::render('Client/AcademicJournals/Index', compact('journals', 'seo'));
} }
public function show(string $slug) public function show(string $slug)
{ {
$journal = new ClientAcademicJournalListResource(AcademicJournal::query()->where('slug', '=', $slug)->firstOrFail()); // Кешируем основной журнал
$journalIssues = JournalIssue::where('academic_journal_id', $journal->id) [$journal, $seo] = Cache::remember(
->groupBy('year_publication')->get(); CacheKeys::ACADEMIC_JOURNAL_PREFIX->value . $slug,
now()->addWeek(),
function () use ($slug) {
$journal = AcademicJournal::query()
->where('slug', $slug)
->firstOrFail();
$seo = $this->seoPageProvider->getSeoForModel($journal);
return [
new ClientAcademicJournalListResource($journal),
$seo
];
}
);
$journals = []; // Кешируем выпуски журнала, сгруппированные по годам
$journals = Cache::remember(
CacheKeys::ACADEMIC_JOURNAL_PREFIX->value . 'issues_' . $slug,
now()->addWeek(),
function () use ($journal) {
$journalIssues = JournalIssue::where('academic_journal_id', $journal->id)
->get()
->groupBy('year_publication');
foreach ($journalIssues as $year => $journalGroup) { $groupedIssues = [];
$journals[] = [ foreach ($journalIssues as $year => $journalGroup) {
'year_publication' => $year, $groupedIssues[] = [
'journalIssues' => $journalGroup 'year_publication' => $year,
]; 'journalIssues' => $journalGroup
} ];
return Inertia::render('Client/AcademicJournals/Show', compact('journal', 'journals')); }
return $groupedIssues;
}
);
return Inertia::render('Client/AcademicJournals/Show', compact('journal', 'journals', 'seo'));
} }
} }
@@ -2,6 +2,7 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Enums\CacheKeys;
use App\Enums\FormEducation; use App\Enums\FormEducation;
use App\Http\Resources\AdditionalEducationCategoryPreviewResource; use App\Http\Resources\AdditionalEducationCategoryPreviewResource;
use App\Http\Resources\AdditionalEducationCategoryResource; use App\Http\Resources\AdditionalEducationCategoryResource;
@@ -14,128 +15,148 @@ use App\Models\AdditionalEducation;
use App\Models\AdditionalEducationCategory; use App\Models\AdditionalEducationCategory;
use App\Models\DirectionAdditionalEducation; use App\Models\DirectionAdditionalEducation;
use App\Models\Page; use App\Models\Page;
use App\Services\App\Breadcrumb\BreadcrumbService;
use App\Services\App\Seo\SeoPageProvider;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Inertia\Inertia; use Inertia\Inertia;
class ClientAdditionalEducationController extends Controller class ClientAdditionalEducationController extends Controller
{ {
public function __construct(readonly SeoPageProvider $seoPageProvider){}
public function index(Request $request) public function index(Request $request)
{ {
$cacheKey = md5(serialize([
'direction' => $request->input('direction'),
'form' => $request->input('form'),
'category' => $request->input('category'),
]));
$directionAdditionalEducations = DirectionAdditionalEducationResource::collection( // Основные данные (кешируются)
DirectionAdditionalEducation::query() $directionAdditionalEducations = Cache::remember(
->where('is_active', true) CacheKeys::ADDITIONAL_EDUCATIONAL_PROGRAMS_PREFIX->value . 'directions_' . $cacheKey,
->whereHas('additionalEducationCategories', function ($q) { now()->addDay(),
$q->whereHas('additionalEducations'); function () {
})->get()); return DirectionAdditionalEducationResource::collection(
DirectionAdditionalEducation::query()
$additionalEducations = AdditionalEducationCategoryResource::collection(AdditionalEducationCategory::query() ->where('is_active', true)
->WithActivePrograms() ->whereHas('additionalEducationCategories', fn ($q) => $q->whereHas('additionalEducations'))
->where('is_active', '=', true) ->get()
->when($request->input('direction'), function ($q, $direction) { );
$q->whereHas('direction', function ($query) use ($direction) { }
$query->where('slug', $direction);
});
})
->when(request()->input('form'), function ($query, $form) {
$query->whereHas('additionalEducations', function ($q) use ($form) {
$q->where('form_education', FormEducation::fromName($form));
});
$query->with(['additionalEducations' => function ($q) use ($form) {
$q->where('form_education', FormEducation::fromName($form));
}]);
})
->when(request()->input('category'), function ($query) {
$slugs = request()->input('category');
if (is_array($slugs)) {
$query->whereIn('slug', $slugs);
}
})
->has('additionalEducations')
->get());
$categories = AdditionalEducationCategoryPreviewResource::collection(
AdditionalEducationCategory::query()
->where('is_active', true)
->has('additionalEducations')
->get()
); );
$additionalEducations = Cache::remember(
CacheKeys::ADDITIONAL_EDUCATIONAL_PROGRAMS_PREFIX->value . $cacheKey,
now()->addDay(),
function () use ($request) {
return AdditionalEducationCategoryResource::collection(
AdditionalEducationCategory::query()
->WithActivePrograms()
->where('is_active', true)
->when($request->direction, fn ($q, $direction) =>
$q->whereHas('direction', fn ($query) => $query->where('slug', $direction))
)
->when($request->form, fn ($query, $form) =>
$query->whereHas('additionalEducations', fn ($q) =>
$q->where('form_education', FormEducation::fromName($form))
)
->when($request->category, fn ($query) =>
is_array($request->category)
? $query->whereIn('slug', $request->category)
: $query
)
->has('additionalEducations')
->get()
));
}
);
$categories = Cache::remember(
CacheKeys::ADDITIONAL_EDUCATIONAL_PROGRAMS_PREFIX->value . 'categories',
now()->addWeek(),
function () {
return AdditionalEducationCategoryPreviewResource::collection(
AdditionalEducationCategory::query()
->where('is_active', true)
->has('additionalEducations')
->get()
);
}
);
// Динамические данные (не кешируются)
$categoriesContent = []; $categoriesContent = [];
if (request()->input('category')) { if ($request->category) {
foreach (request()->input('category') as $item) { foreach ((array)$request->category as $item) {
$categoriesContent[$item] = new AdditionalEducationCategoryResource(AdditionalEducationCategory::where('slug', $item)->first()); $categoriesContent[$item] = new AdditionalEducationCategoryResource(
AdditionalEducationCategory::where('slug', $item)->first()
);
} }
} }
$forms_education = []; $forms_education = array_reduce(
foreach (FormEducation::cases() as $case) { FormEducation::cases(),
$forms_education[$case->name] = $case->getLabel(); fn ($acc, $case) => $acc + [$case->name => $case->getLabel()],
} []
);
$filters = [ $filters = [
'direction_filter' => [ 'direction_filter' => [
'type' => 'direction', 'type' => 'direction',
'value' => request()->input('direction'), 'value' => $request->input('direction'),
'param' => 'direction' 'param' => 'direction'
], ],
'form_education_filter' => [ 'form_education_filter' => [
'type' => 'form', 'type' => 'form',
'value' => request()->input('form'), 'value' => $request->input('form'),
'param' => 'form' 'param' => 'form'
], ],
'category_filter' => [ 'category_filter' => [
'type' => 'category', 'type' => 'category',
'value' => request()->input('category'), 'value' => $request->input('category'),
'param' => 'category', 'param' => 'category',
'content' => $categoriesContent, 'content' => $categoriesContent,
], ],
]; ];
$routeUrl = route('client.additionalEducation.index'); $seo = $this->seoPageProvider->getSeoForCurrentPage();
$path = ltrim(parse_url($routeUrl, PHP_URL_PATH), '/');
$page = Page::where('path', '=', $path)->with('section.pages.section', 'section.mainSection')->first();
if (isset($page->section)) { return Inertia::render('Client/Additional-educations/Index', compact(
$breadcrumbs = [ 'directionAdditionalEducations',
'mainSection' => new ClientBreadcrumbSection($page->section->mainSection), 'additionalEducations',
'subSection' => new ClientBreadcrumbSubSection($page->section), 'filters',
'page' => new ClientBreadcrumbPage($page), 'forms_education',
]; 'categories',
} else { 'seo'
$breadcrumbs = null; ));
}
return Inertia::render('Client/Additional-educations/Index',
compact(
'directionAdditionalEducations',
'additionalEducations',
'filters',
'forms_education',
'categories',
'breadcrumbs'
));
} }
public function show(string $slug) public function show(string $slug)
{ {
$additionalEducation = new AdditionalEducationResource(AdditionalEducation::query()->with('category.direction')->where('slug', $slug)->first()); // Кешируем основную программу дополнительного образования
$routeUrl = route('client.additionalEducation.index'); [$additionalEducation, $seo] = Cache::remember(
$path = ltrim(parse_url($routeUrl, PHP_URL_PATH), '/'); CacheKeys::ADDITIONAL_EDUCATIONAL_PROGRAM_PREFIX->value . $slug,
now()->addDay(),
function () use ($slug) {
$additionalEducation = AdditionalEducation::query()
->with('category.direction')
->where('slug', $slug)
->first();
$seo = $this->seoPageProvider->getSeoForModel($additionalEducation);
return [
new AdditionalEducationResource($additionalEducation),
$seo
];
}
);
$page = Page::where('path', '=', $path)->with('section.pages.section', 'section.mainSection')->first(); // SEO-данные берём из кешированного ресурса
if (isset($page->section)) { return Inertia::render('Client/Additional-educations/Show', compact(
$breadcrumbs = [ 'additionalEducation',
'mainSection' => new ClientBreadcrumbSection($page->section->mainSection), 'seo'
'subSection' => new ClientBreadcrumbSubSection($page->section), ));
'page' => new ClientBreadcrumbPage($page), }}
];
} else {
$breadcrumbs = null;
}
$seo = $additionalEducation->seo ?? null;
return Inertia::render('Client/Additional-educations/Show', compact('additionalEducation', 'breadcrumbs', 'seo'));
}
}
@@ -2,34 +2,93 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Enums\CacheKeys;
use App\Http\Resources\ClientDepartmentPreviewResource; use App\Http\Resources\ClientDepartmentPreviewResource;
use App\Http\Resources\DepartmentResource; use App\Http\Resources\DepartmentResource;
use App\Models\Department; use App\Models\Department;
use App\Models\Faculty; use App\Models\Faculty;
use App\Services\App\Breadcrumb\BreadcrumbService;
use App\Services\App\Seo\SeoPageProvider;
use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Collection;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Inertia\Inertia; use Inertia\Inertia;
class ClientDepartmentController extends Controller class ClientDepartmentController extends Controller
{ {
public function __construct(readonly SeoPageProvider $seoPageProvider){}
public function show(string $facultySlug, string $departmentSlug) public function show(string $facultySlug, string $departmentSlug)
{ {
$faculty = Faculty::query()->where('slug', $facultySlug)->first(); // Ключ для кеширования
$departments = ClientDepartmentPreviewResource::collection( $cacheKey = "{$facultySlug}_{$departmentSlug}";
Department::query()
->where('is_active', true)
->where('faculty_id', $faculty->id)
->get()
);
$department = new DepartmentResource(Department::query()
->where('slug', $departmentSlug)
->where('is_active', true)
->with(['faculty', 'workers.userDetail', 'teachers.userDetail', 'programs.directionStudy'])
->first());
$directions = $this->groupProgramsByDirection($department->programs);
$seo = $department->seo ?? null; // Кешируем факультет
return Inertia::render('Client/Departments/Show', compact('department', 'departments', 'directions', 'seo')); $faculty = Cache::remember(
CacheKeys::FACULTY_PREFIX->value . $facultySlug,
now()->addDay(),
function () use ($facultySlug) {
return Faculty::query()
->where('slug', $facultySlug)
->first();
}
);
// Кешируем список активных кафедр факультета
$departments = Cache::remember(
CacheKeys::DEPARTMENTS_PREFIX->value . 'active_' . $faculty->id,
now()->addDay(),
function () use ($faculty) {
return ClientDepartmentPreviewResource::collection(
Department::query()
->where('is_active', true)
->where('faculty_id', $faculty->id)
->get()
);
}
);
// Кешируем полные данные кафедры с отношениями
[$department, $seo] = Cache::remember(
CacheKeys::DEPARTMENT_PREFIX->value . $cacheKey,
now()->addDay(),
function () use ($departmentSlug) {
$department = Department::query()
->where('slug', $departmentSlug)
->where('is_active', true)
->with([
'faculty',
'workers.userDetail',
'teachers.userDetail',
'programs.directionStudy',
'seo'
])
->first();
$seo = $this->seoPageProvider->getSeoForModel($department);
return [
new DepartmentResource($department),
$seo
];
}
);
// Кешируем сгруппированные направления
$directions = Cache::remember(
CacheKeys::DEPARTMENT_PREFIX->value . 'directions_' . $cacheKey,
now()->addDay(),
function () use ($department) {
return $this->groupProgramsByDirection($department->programs);
}
);
return Inertia::render('Client/Departments/Show', compact(
'department',
'departments',
'directions',
'seo',
));
} }
@@ -4,22 +4,31 @@ namespace App\Http\Controllers;
use App\Http\Resources\DivisionResource; use App\Http\Resources\DivisionResource;
use App\Models\Division; use App\Models\Division;
use App\Services\App\Breadcrumb\BreadcrumbService;
use App\Services\App\Seo\SeoPageProvider;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Inertia\Inertia; use Inertia\Inertia;
class ClientDivisionController extends Controller class ClientDivisionController extends Controller
{ {
public function __construct(readonly SeoPageProvider $seoPageProvider){}
public function index() public function index()
{ {
$divisions = DivisionResource::collection(Division::query()->where('is_active', true)->get()); $divisions = DivisionResource::collection(Division::query()->where('is_active', true)->get());
return Inertia::render('Client/Divisions/Index', compact('divisions'));
$seo = $this->seoPageProvider->getSeoForCurrentPage();
return Inertia::render('Client/Divisions/Index', compact('divisions', 'seo'));
} }
public function show(string $slug) public function show(string $slug)
{ {
$divisions = DivisionResource::collection(Division::query()->where('is_active', true)->get()); $divisions = DivisionResource::collection(Division::query()->where('is_active', true)->get());
$division = new DivisionResource(Division::with('workers.userDetail')->where('is_active', true)->where('slug', $slug)->firstOrFail()); $division = new DivisionResource($divisionModel = Division::with(['workers.userDetail', 'seo'])->where('is_active', true)->where('slug', $slug)->firstOrFail());
$seo = $division->seo ?? null;
$seo = $this->seoPageProvider->getSeoForModel($divisionModel);
return Inertia::render('Client/Divisions/Show', compact('divisions', 'division', 'seo')); return Inertia::render('Client/Divisions/Show', compact('divisions', 'division', 'seo'));
} }
} }
+77 -27
View File
@@ -2,6 +2,7 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Enums\CacheKeys;
use App\Http\Resources\ClientBreadcrumbPage; use App\Http\Resources\ClientBreadcrumbPage;
use App\Http\Resources\ClientBreadcrumbSection; use App\Http\Resources\ClientBreadcrumbSection;
use App\Http\Resources\ClientBreadcrumbSubSection; use App\Http\Resources\ClientBreadcrumbSubSection;
@@ -13,53 +14,102 @@ use App\Models\Event;
use App\Models\EventCategory; use App\Models\EventCategory;
use App\Models\Page; use App\Models\Page;
use App\Services\App\Breadcrumb\BreadcrumbService; use App\Services\App\Breadcrumb\BreadcrumbService;
use App\Services\App\Seo\SeoPageProvider;
use Carbon\Carbon; use Carbon\Carbon;
use DateTime; use DateTime;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Inertia\Inertia; use Inertia\Inertia;
class ClientEventController extends Controller class ClientEventController extends Controller
{ {
public function __construct(private readonly BreadcrumbService $breadcrumbService){} public function __construct(readonly SeoPageProvider $seoPageProvider){}
public function index(Request $request): \Inertia\Response public function index(Request $request): \Inertia\Response
{ {
$currentDate = $this->getCurrentDate($request); $currentDate = $this->getCurrentDate($request);
$cacheKey = md5(serialize([$currentDate, $request->all()]));
$events = Cache::remember(
CacheKeys::EVENTS_PREFIX->value . $cacheKey,
now()->addHours(12),
fn() => $this->getEvents($currentDate)
);
$eventDates = Cache::remember(
CacheKeys::EVENTS_PREFIX->value . 'dates_' . $cacheKey,
now()->addHours(12),
fn() => $this->getEventDates($this->getFilters())
);
$categories = Cache::remember(
CacheKeys::EVENTS_PREFIX->value . 'categories',
now()->addDay(),
fn() => ClientEventCategoryResource::collection(EventCategory::has('events')->get())
);
$filters = $this->getFilters(); $filters = $this->getFilters();
$eventDates = $this->getEventDates($filters);
$events = $this->getEvents($currentDate); $seo = $this->seoPageProvider->getSeoForCurrentPage();
$categories = ClientEventCategoryResource::collection(EventCategory::has('events')->get());
$breadcrumbs = $this->breadcrumbService->generateBreadcrumbs('client.event.index'); return Inertia::render('Client/Events/Index', compact(
'eventDates',
'events',
return Inertia::render('Client/Events/Index', compact('eventDates', 'events', 'currentDate', 'filters', 'categories', 'breadcrumbs')); 'currentDate',
'filters',
'categories',
'seo'
));
} }
public function show(string $slug): \Inertia\Response public function show(string $slug): \Inertia\Response
{ {
$event = new ClientEventFullResource(Event::where('slug', '=', $slug)->with('category')->first()); [$event, $seo] = Cache::remember(
CacheKeys::EVENT_PREFIX->value . $slug,
now()->addDay(),
function ($slug) {
$event = Event::where('slug', $slug)->with(['category', 'seo'])->first();
$seo = $this->seoPageProvider->getSeoForModel($event);
return [
new ClientEventFullResource($event),
$seo
];
}
);
$breadcrumbs = $this->breadcrumbService->generateBreadcrumbs('client.event.index');
$seo = $event->seo ?? null; return Inertia::render('Client/Events/Show', compact(
'event',
return Inertia::render('Client/Events/Show', compact('event', 'breadcrumbs', 'seo')); 'seo'
));
} }
public function archive(Request $request): \Inertia\Response public function archive(Request $request): \Inertia\Response
{ {
$cacheKey = md5(serialize($request->all()));
$events = Cache::remember(
CacheKeys::EVENTS_PREFIX->value . 'archive_' . $cacheKey,
now()->addDay(),
fn() => $this->getEventsArchive()
);
$categories = Cache::remember(
CacheKeys::EVENTS_PREFIX->value . 'categories',
now()->addDay(),
fn() => ClientEventCategoryResource::collection(EventCategory::has('events')->get())
);
$filters = $this->getFilters(); $filters = $this->getFilters();
$events = $this->getEventsArchive(); $seo = $this->seoPageProvider->getSeoForCurrentPage();
$categories = ClientEventCategoryResource::collection(EventCategory::has('events')->get());
$breadcrumbs = $this->breadcrumbService->generateBreadcrumbs('client.event.archive'); return Inertia::render('Client/Events/Archive', compact(
'events',
'filters',
return Inertia::render('Client/Events/Archive', compact('events', 'filters', 'categories', 'breadcrumbs')); 'categories',
'seo'
));
} }
private function getCurrentDate(Request $request): array private function getCurrentDate(Request $request): array
@@ -164,7 +214,12 @@ class ClientEventController extends Controller
->orderBy('event_date_start') ->orderBy('event_date_start')
->get(); ->get();
$mappingDates = $events->map(function ($event) { // Получаем массив без ключей
// Извлекаем уникальные даты из событий
return $events->map(function ($event) {
$date = new DateTime($event->event_date_start); $date = new DateTime($event->event_date_start);
return [ return [
'day' => $date->format('j'), 'day' => $date->format('j'),
@@ -182,12 +237,7 @@ class ClientEventController extends Controller
]; ];
}) })
->sortKeys() // Сортируем ключи по возрастанию ->sortKeys() // Сортируем ключи по возрастанию
->values(); // Получаем массив без ключей ->values();
// Извлекаем уникальные даты из событий
return $mappingDates;
} }
private function getFilters(): array private function getFilters(): array
@@ -2,26 +2,75 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Enums\CacheKeys;
use App\Http\Resources\FacultyResource; use App\Http\Resources\FacultyResource;
use App\Http\Resources\FullFacultyResource; use App\Http\Resources\FullFacultyResource;
use App\Models\Faculty; use App\Models\Faculty;
use App\Services\App\Breadcrumb\BreadcrumbService;
use App\Services\App\Seo\SeoPageProvider;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Inertia\Inertia; use Inertia\Inertia;
class ClientFacultyController extends Controller class ClientFacultyController extends Controller
{ {
public function index() public function __construct(readonly SeoPageProvider $seoPageProvider){}
public function index(Request $request)
{ {
$faculties = FacultyResource::collection(Faculty::query()->where('is_active', true)->get()); $faculties = Cache::remember(
return Inertia::render('Client/Faculties/Index', compact('faculties')); CacheKeys::FACULTIES_PREFIX->value . 'active_list',
now()->addDay(), // Кешируем на 1 день
function () {
return FacultyResource::collection(
Faculty::query()
->where('is_active', true)
->get()
);
}
);
$seo = $this->seoPageProvider->getSeoForCurrentPage();
return Inertia::render('Client/Faculties/Index', compact('faculties', 'seo'));
} }
public function show(string $slug) public function show(string $slug)
{ {
$faculties = FacultyResource::collection(Faculty::query()->where('is_active', true)->get()); // Кешируем список факультетов
$faculty = new FullFacultyResource(Faculty::where('slug', $slug)->where('is_active', true)->with(['departments.faculty', 'workers.userDetail'])->firstOrFail()); $faculties = Cache::remember(
$seo = $faculty->seo ?? null; CacheKeys::FACULTIES_PREFIX->value . 'active_list',
now()->addDay(),
function () {
return FacultyResource::collection(
Faculty::query()
->where('is_active', true)
->get()
);
}
);
// Кешируем данные конкретного факультета
[$faculty, $seo] = Cache::remember(
CacheKeys::FACULTY_PREFIX->value . $slug,
now()->addDay(),
function () use ($slug) {
$faculty = Faculty::where('slug', $slug)
->where('is_active', true)
->with(['departments.faculty', 'workers.userDetail', 'seo'])
->firstOrFail();
$seo = $this->seoPageProvider->getSeoForModel($faculty);
return [
new FullFacultyResource($faculty),
$seo
];
}
);
return Inertia::render('Client/Faculties/Show', compact('faculty', 'faculties', 'seo')); return Inertia::render('Client/Faculties/Show', compact('faculty', 'faculties', 'seo'));
} }
} }
+8 -17
View File
@@ -3,14 +3,9 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Http\Resources\CategoryResource; use App\Http\Resources\CategoryResource;
use App\Http\Resources\ClientBreadcrumbPage;
use App\Http\Resources\ClientBreadcrumbSection;
use App\Http\Resources\ClientBreadcrumbSubSection;
use App\Http\Resources\ClientNavigationResource;
use App\Http\Resources\ClientPostListResource; use App\Http\Resources\ClientPostListResource;
use App\Http\Resources\ClientTagResource; use App\Http\Resources\ClientTagResource;
use App\Http\Resources\MainSectionResource;
use App\Http\Resources\PageResource;
use App\Http\Resources\PostResource; use App\Http\Resources\PostResource;
use App\Models\Category; use App\Models\Category;
use App\Models\MainSection; use App\Models\MainSection;
@@ -18,8 +13,8 @@ use App\Models\Page;
use App\Models\Post; use App\Models\Post;
use App\Models\Tag; use App\Models\Tag;
use App\Services\App\Breadcrumb\BreadcrumbService; use App\Services\App\Breadcrumb\BreadcrumbService;
use App\Services\App\Seo\SeoPageProvider;
use Carbon\Carbon; use Carbon\Carbon;
use Doctrine\DBAL\Schema\Column;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
@@ -27,12 +22,12 @@ use Inertia\Inertia;
class ClientPostController extends Controller class ClientPostController extends Controller
{ {
public function __construct(private readonly BreadcrumbService $breadcrumbService){} public function __construct(readonly SeoPageProvider $seoPageProvider){}
public function index(Request $request) public function index(Request $request)
{ {
// Кешируем список тегов // Кешируем список тегов
$tagIds = Cache::remember('tag_ids', now()->addHours(1), function () { $tagIds = Cache::remember('tag_ids', now()->addHours(), function () {
return DB::table('taggables') return DB::table('taggables')
->distinct() ->distinct()
->select('tag_id') ->select('tag_id')
@@ -132,11 +127,9 @@ class ClientPostController extends Controller
], ],
]; ];
$breadcrumbs = $this->breadcrumbService->generateBreadcrumbs('client.post.index'); $seo = $this->seoPageProvider->getSeoForCurrentPage();
return Inertia::render('Client/Posts/Index', compact('filters', 'posts', 'categories', 'tags', 'seo'));
return Inertia::render('Client/Posts/Index', compact('filters', 'posts', 'categories', 'tags', 'breadcrumbs'));
} }
public function show(Request $request, $slug) public function show(Request $request, $slug)
@@ -154,19 +147,17 @@ class ClientPostController extends Controller
// Преобразуем пост в ресурс // Преобразуем пост в ресурс
$postResource = new PostResource($post); $postResource = new PostResource($post);
$breadcrumbs = $this->breadcrumbService->generateBreadcrumbs('client.post.index');
// SEO-данные // SEO-данные
$seo = $post->seo ?? null; $seo = $this->seoPageProvider->getSeoForModel($post);
// Возвращаем данные для кеширования // Возвращаем данные для кеширования
return [ return [
'post' => $postResource, 'post' => $postResource,
'breadcrumbs' => $breadcrumbs,
'seo' => $seo, 'seo' => $seo,
]; ];
}); });
// Возвращаем ответ с использованием кешированных данных // Возвращаем ответ с использованием кешированных данных
return Inertia::render('Client/Posts/Show', $data); return Inertia::render('Client/Posts/Show', $data);
} }
+114 -86
View File
@@ -3,6 +3,7 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Enums\BudgetEducation; use App\Enums\BudgetEducation;
use App\Enums\CacheKeys;
use App\Enums\FormEducation; use App\Enums\FormEducation;
use App\Enums\LevelEducational; use App\Enums\LevelEducational;
use App\Http\Resources\CampaignDegreeResource; use App\Http\Resources\CampaignDegreeResource;
@@ -16,121 +17,150 @@ use App\Models\CampaignDegree;
use App\Models\DirectionStudy; use App\Models\DirectionStudy;
use App\Models\EducationalProgram; use App\Models\EducationalProgram;
use App\Models\MainSection; use App\Models\MainSection;
use App\Services\App\Seo\SeoPageProvider;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Inertia\Inertia; use Inertia\Inertia;
class ClientProgramController extends Controller class ClientProgramController extends Controller
{ {
public function __construct(readonly SeoPageProvider $seoPageProvider){}
public function index(Request $request) public function index(Request $request)
{ {
$activeCampaign = AdmissionCampaign::query()->where('status', 1)->first(); $cacheKey = CacheKeys::EDUCATION_PROGRAMS_PREFIX->value . md5(serialize($request->all()));
$uniqueValues = EducationalProgram::distinct()->pluck('lvl_edu'); $data = Cache::remember($cacheKey, now()->addHours(1), function () use ($request) {
$levelsEducational = $uniqueValues->mapWithKeys(function ($level) { $activeCampaign = AdmissionCampaign::query()->where('status', 1)->first();
return [$level->name => $level->getLabel()];
});
$direction_studies = DirectionStudy::query() $uniqueValues = EducationalProgram::distinct()->pluck('lvl_edu');
->withAdmissionCampaignByYear($activeCampaign->academic_year) $levelsEducational = $uniqueValues->mapWithKeys(function ($level) {
->withActivePrograms() return [$level->name => $level->getLabel()];
->get(); });
$level = request()->input('level'); $direction_studies = DirectionStudy::query()
$form = request()->input('form');
$budget = request()->input('budget');
$naprs = DirectionStudyResource::collection(
DirectionStudy::query()
->withAdmissionCampaignByYear($activeCampaign->academic_year) ->withAdmissionCampaignByYear($activeCampaign->academic_year)
->withActivePrograms() ->withActivePrograms()
->with('programs.admission_plans') ->get();
->when($level, function ($query) use ($level) {
$query->where('lvl_edu', LevelEducational::fromName($level)->value); $level = request()->input('level');
}) $form = request()->input('form');
->when($form, function ($query) use ($form) { $budget = request()->input('budget');
$this->applyFormFilter($query, $form);
}) $naprs = DirectionStudyResource::collection(
->when($budget, function ($query) use ($budget) { DirectionStudy::query()
$this->applyBudgetFilter($query, $budget); ->withAdmissionCampaignByYear($activeCampaign->academic_year)
}) ->withActivePrograms()
->when(request()->input('direction'), function ($query) { ->with('programs.admission_plans')
$slugs = request()->input('direction'); ->when($level, function ($query) use ($level) {
if (is_array($slugs)) { $query->where('lvl_edu', LevelEducational::fromName($level)->value);
$query->whereIn('slug', $slugs); })
} ->when($form, function ($query) use ($form) {
}) $this->applyFormFilter($query, $form);
->get() })
); ->when($budget, function ($query) use ($budget) {
$this->applyBudgetFilter($query, $budget);
})
->when(request()->input('direction'), function ($query) {
$slugs = request()->input('direction');
if (is_array($slugs)) {
$query->whereIn('slug', $slugs);
}
})
->get()
);
$campaignName = $this->getAdmissionCampaignName();
$formsEducational = FormEducation::cases();
$formsEducational = collect($formsEducational);
$formsEdu = $formsEducational->mapWithKeys(function ($formEducational) {
return [$formEducational->name => $formEducational->getLabel()];
});
$typesBudget = BudgetEducation::cases();
$typesBudget = collect($typesBudget);
$budgetEdu = $typesBudget->mapWithKeys(function ($typeBudget) {
return [$typeBudget->name => $typeBudget->getLabel()];
});
$filters = [
'level_filter' => [
'type' => 'level',
'value' => request()->input('level'),
'param' => 'level'
],
'budget_filter' => [
'type' => 'budget',
'value' => request()->input('budget'),
'param' => 'budget'
],
'formEdu_filter' => [
'type' => 'form',
'value' => request()->input('form'),
'param' => 'form'
],
'direction_filter' => [
'type' => 'direction',
'value' => request()->input('direction'),
'param' => 'direction'
],
];
$seo = $this->seoPageProvider->getSeoForCurrentPage();
$campaignName = $this->getAdmissionCampaignName(); return compact(
$formsEducational = FormEducation::cases();
$formsEducational = collect($formsEducational);
$formsEdu = $formsEducational->mapWithKeys(function ($formEducational) {
return [$formEducational->name => $formEducational->getLabel()];
});
$typesBudget = BudgetEducation::cases();
$typesBudget = collect($typesBudget);
$budgetEdu = $typesBudget->mapWithKeys(function ($typeBudget) {
return [$typeBudget->name => $typeBudget->getLabel()];
});
$filters = [
'level_filter' => [
'type' => 'level',
'value' => request()->input('level'),
'param' => 'level'
],
'budget_filter' => [
'type' => 'budget',
'value' => request()->input('budget'),
'param' => 'budget'
],
'formEdu_filter' => [
'type' => 'form',
'value' => request()->input('form'),
'param' => 'form'
],
'direction_filter' => [
'type' => 'direction',
'value' => request()->input('direction'),
'param' => 'direction'
],
];
return Inertia::render('Client/Programs/Index',
compact(
'naprs', 'naprs',
'campaignName', 'campaignName',
'levelsEducational', 'levelsEducational',
'filters', 'filters',
'formsEdu', 'formsEdu',
'budgetEdu', 'budgetEdu',
'direction_studies' 'direction_studies',
)); 'seo'
);
});
return Inertia::render('Client/Programs/Index', $data);
} }
public function show(string $slug) public function show(string $slug)
{ {
$program = new EducationalProgramFullResource(EducationalProgram::query()->where('slug', $slug)->with(['admission_plans', 'directionStudy'])->firstOrFail()); $cacheKey = CacheKeys::EDUCATION_PROGRAM_PREFIX->value . md5($slug);
$formsEducational = BudgetEducation::cases();
$formsEducational = collect($formsEducational); $data = Cache::remember($cacheKey, now()->addHours(1), function () use ($slug) {
$formsEdu = $formsEducational->mapWithKeys(function ($formEducational) { $program = new EducationalProgramFullResource(
return [$formEducational->value => $formEducational->getLabel()]; $programModel = EducationalProgram::query()
->where('slug', $slug)
->with(['admission_plans', 'directionStudy', 'seo'])
->firstOrFail()
);
$formsEducational = BudgetEducation::cases();
$formsEducational = collect($formsEducational);
$formsEdu = $formsEducational->mapWithKeys(function ($formEducational) {
return [$formEducational->value => $formEducational->getLabel()];
});
$seo = $this->seoPageProvider->getSeoForModel($programModel);
return compact('program', 'formsEdu', 'seo');
}); });
$seo = $program->seo ?? null; return Inertia::render('Client/Programs/Show', $data);
return Inertia::render('Client/Programs/Show', compact('program', 'formsEdu', 'seo'));
} }
private function getAdmissionCampaignName() : string private function getAdmissionCampaignName(): string
{ {
$campaign = AdmissionCampaign::query()->where('status', 1)->first(); $cacheKey = CacheKeys::EDUCATION_PROGRAM_PREFIX->value . 'active_campaign_name';
return $campaign->name;
}
return Cache::remember($cacheKey, now()->addHours(1), function () {
$campaign = AdmissionCampaign::query()->where('status', 1)->first();
return $campaign->name;
});
}
private function applyFormFilter($query, $form) private function applyFormFilter($query, $form)
{ {
@@ -150,7 +180,6 @@ class ClientProgramController extends Controller
{ {
$budgetValue = Str::of(BudgetEducation::fromName($budget)->value)->toString(); $budgetValue = Str::of(BudgetEducation::fromName($budget)->value)->toString();
$query->whereHas('programs.admission_plans', function ($query) use ($budgetValue) { $query->whereHas('programs.admission_plans', function ($query) use ($budgetValue) {
$query->whereJsonContains('contests', ['financing_source' => $budgetValue]); $query->whereJsonContains('contests', ['financing_source' => $budgetValue]);
}) })
@@ -160,5 +189,4 @@ class ClientProgramController extends Controller
}); });
}]); }]);
} }
}
}
@@ -8,11 +8,14 @@ use App\Http\Resources\ScheduleResource;
use App\Models\EducationalGroup; use App\Models\EducationalGroup;
use App\Models\Faculty; use App\Models\Faculty;
use App\Models\Schedule; use App\Models\Schedule;
use App\Services\App\Seo\SeoPageProvider;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Inertia\Inertia; use Inertia\Inertia;
class ClientScheduleController extends Controller class ClientScheduleController extends Controller
{ {
public function __construct(readonly SeoPageProvider $seoPageProvider){}
public function index(Request $request) public function index(Request $request)
{ {
$educationalGroups = ClientEducationalGroupResource::collection(EducationalGroup::query() $educationalGroups = ClientEducationalGroupResource::collection(EducationalGroup::query()
@@ -46,10 +49,6 @@ class ClientScheduleController extends Controller
} }
$forms_education = []; $forms_education = [];
foreach (FormEducation::cases() as $case) { foreach (FormEducation::cases() as $case) {
$forms_education[$case->name] = $case->getLabel(); $forms_education[$case->name] = $case->getLabel();
@@ -78,8 +77,10 @@ class ClientScheduleController extends Controller
] ]
]; ];
$seo = $this->seoPageProvider->getSeoForCurrentPage();
// Возвращаем данные в представление // Возвращаем данные в представление
return Inertia::render('Client/Schedules/Index', compact('educationalGroups', 'filters', 'forms_education', 'schedulesByFaculty')); return Inertia::render('Client/Schedules/Index', compact('educationalGroups', 'filters', 'forms_education', 'schedulesByFaculty', 'seo'));
} }
public function show($id) public function show($id)
@@ -2,22 +2,31 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Enums\CacheKeys;
use App\Enums\PostStatus; use App\Enums\PostStatus;
use App\Http\Resources\AdditionalEducationSearchResource; use App\Http\Resources\AdditionalEducationSearchResource;
use App\Http\Resources\PostThumbnailResource; use App\Http\Resources\PostThumbnailResource;
use App\Models\AdditionalEducation; use App\Models\AdditionalEducation;
use App\Models\Post; use App\Models\Post;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
class ClientWidgetAdditionalEducationalProgramController extends Controller class ClientWidgetAdditionalEducationalProgramController extends Controller
{ {
public function index() public function index()
{ {
return AdditionalEducationSearchResource::collection( return Cache::remember(
AdditionalEducation::query() CacheKeys::ADDITIONAL_EDUCATIONAL_PROGRAMS_PREFIX->value . 'search_list',
->where('is_active', true) now()->addDay(), // Кешируем на 1 день
->orderBy('title', 'desc') function () {
->get()); return AdditionalEducationSearchResource::collection(
AdditionalEducation::query()
->where('is_active', true)
->orderBy('title', 'desc')
->get()
);
}
);
} }
} }
@@ -2,14 +2,26 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Enums\CacheKeys;
use App\Http\Resources\ClientContactWidgetResource; use App\Http\Resources\ClientContactWidgetResource;
use App\Http\Resources\ClientPageReferenceListResource; use App\Http\Resources\ClientPageReferenceListResource;
use App\Models\ContactWidget; use App\Models\ContactWidget;
use Illuminate\Support\Facades\Cache;
class ClientWidgetContactController extends Controller class ClientWidgetContactController extends Controller
{ {
public function show(string $slug) public function show(string $slug)
{ {
return new ClientContactWidgetResource(ContactWidget::query()->where('slug', $slug)->first()); return Cache::remember(
CacheKeys::CONTACT_WIDGET_PREFIX->value . $slug,
now()->addHours(12), // Кешируем на 12 часов
function () use ($slug) {
return new ClientContactWidgetResource(
ContactWidget::query()
->where('slug', $slug)
->first()
);
}
);
} }
} }
@@ -2,19 +2,28 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Enums\CacheKeys;
use App\Enums\EducationalProgramStatus; use App\Enums\EducationalProgramStatus;
use App\Http\Resources\EducationalProgramSearchResource; use App\Http\Resources\EducationalProgramSearchResource;
use App\Models\EducationalProgram; use App\Models\EducationalProgram;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
class ClientWidgetEducationalProgramController extends Controller class ClientWidgetEducationalProgramController extends Controller
{ {
public function index() public function index()
{ {
return EducationalProgramSearchResource::collection( return Cache::remember(
EducationalProgram::query() CacheKeys::EDUCATION_PROGRAMS_PREFIX->value . 'search_list',
->where('status', EducationalProgramStatus::PUBLISHED) now()->addDay(), // Кешируем на 1 день
->orderBy('name', 'desc') function () {
->get()); return EducationalProgramSearchResource::collection(
EducationalProgram::query()
->where('status', EducationalProgramStatus::PUBLISHED)
->orderBy('name', 'desc')
->get()
);
}
);
} }
} }
@@ -2,14 +2,26 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Enums\CacheKeys;
use App\Http\Resources\ClientPageReferenceListResource; use App\Http\Resources\ClientPageReferenceListResource;
use App\Models\PageReferenceList; use App\Models\PageReferenceList;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
class ClientWidgetPageReferenceListController extends Controller class ClientWidgetPageReferenceListController extends Controller
{ {
public function show(string $slug) public function show(string $slug)
{ {
return new ClientPageReferenceListResource(PageReferenceList::query()->where('slug', $slug)->first()); return Cache::remember(
CacheKeys::PAGE_REFERENCE_LIST_PREFIX->value . $slug,
now()->addWeek(), // Кешируем на неделю, так как справочники меняются редко
function () use ($slug) {
return new ClientPageReferenceListResource(
PageReferenceList::query()
->where('slug', $slug)
->first()
);
}
);
} }
} }
@@ -15,7 +15,9 @@ class ClientWidgetSliderController extends Controller
$slider = Slider::query() $slider = Slider::query()
->where('slug', $slug) ->where('slug', $slug)
->where('is_active', true) ->where('is_active', true)
->with('slides') ->with(['slides' => function($query) {
$query->where('is_active', true);
}])
->first(); ->first();
return $slider ?: null; return $slider ?: null;

Some files were not shown because too many files have changed in this diff Show More