Rework frontend
This commit is contained in:
@@ -9,12 +9,18 @@ server {
|
||||
try_files $uri /index.php?$args; # Обработка запросов
|
||||
}
|
||||
|
||||
|
||||
|
||||
location /sveden/ {
|
||||
alias /var/www/public/sveden/;
|
||||
index index.html;
|
||||
try_files $uri $uri/ /sveden/index.html; # Обработка статических файлов
|
||||
}
|
||||
|
||||
location = /sveden {
|
||||
return 301 /sveden/;
|
||||
}
|
||||
|
||||
location ~ \.php$ {
|
||||
try_files $uri =404; # Если файл не найден, возвращаем 404
|
||||
fastcgi_split_path_info ^(.+\.php)(/.+)$; # Разделение пути
|
||||
|
||||
@@ -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'
|
||||
];
|
||||
}
|
||||
@@ -17,6 +17,7 @@ class MainSliderDTO
|
||||
public ?Carbon $start_time,
|
||||
public ?Carbon $end_time,
|
||||
public ?int $sort,
|
||||
public int $slider_id,
|
||||
) {}
|
||||
|
||||
// Опционально: метод для создания DTO из массива
|
||||
@@ -33,6 +34,7 @@ class MainSliderDTO
|
||||
start_time: isset($data['start_time']) ? Carbon::parse($data['start_time']) : null,
|
||||
end_time: isset($data['end_time']) ? Carbon::parse($data['end_time']) : null,
|
||||
sort: $data['sort'] ?? null,
|
||||
slider_id: $data['slider_id'],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -50,6 +52,7 @@ class MainSliderDTO
|
||||
'start_time' => $this->start_time?->toDateTimeString(),
|
||||
'end_time' => $this->end_time?->toDateTimeString(),
|
||||
'sort' => $this->sort,
|
||||
'slider_id' => $this->slider_id,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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_';
|
||||
|
||||
|
||||
}
|
||||
@@ -13,6 +13,7 @@ use App\Models\Page;
|
||||
use App\Models\Post;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Components\Builder;
|
||||
use Filament\Forms\Components\DateTimePicker;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\Hidden;
|
||||
use Filament\Forms\Components\RichEditor;
|
||||
@@ -36,52 +37,180 @@ class CustomFormForm
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Section::make()
|
||||
Section::make('Настройки формы')
|
||||
->description('Конфигурация пользовательской формы')
|
||||
->collapsible()
|
||||
->schema([
|
||||
Tabs::make('Tabs')
|
||||
Tabs::make('Конфигурация формы')
|
||||
->persistTabInQueryString()
|
||||
->columnSpanFull()
|
||||
->tabs([
|
||||
Tabs\Tab::make('Основная информация')
|
||||
->icon('heroicon-o-information-circle')
|
||||
->schema([
|
||||
Forms\Components\Grid::make(2)->schema([
|
||||
TextInput::make('title')->label('Заголовок')->required()
|
||||
->live(onBlur: true)
|
||||
->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 формы')->unique(ignoreRecord: true)->required(),
|
||||
]),
|
||||
Forms\Components\Textarea::make('description')->label('Описание формы')->required(),
|
||||
Select::make('status')->label('Статус формы')->required()
|
||||
Forms\Components\Grid::make(2)
|
||||
->schema([
|
||||
TextInput::make('title')
|
||||
->label('Название формы')
|
||||
->placeholder('Введите название формы')
|
||||
->helperText('Это название будет видно пользователям')
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->live(onBlur: true)
|
||||
->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)
|
||||
->required()
|
||||
->native(false)
|
||||
->helperText('Определяет видимость формы на сайте')
|
||||
->columnSpanFull(),
|
||||
]),
|
||||
Tabs\Tab::make('Колонки')
|
||||
|
||||
Tabs\Tab::make('Поля формы')
|
||||
->icon('heroicon-o-view-columns')
|
||||
->schema([
|
||||
FormBuilderItem::getItem(),
|
||||
FormBuilderItem::getItem()
|
||||
->columnSpanFull(),
|
||||
]),
|
||||
|
||||
Tabs\Tab::make('Кнопка отправки')
|
||||
->icon('heroicon-o-paper-airplane')
|
||||
->schema([
|
||||
TextInput::make('button')->label('Текст кнопки отправки')->required(),
|
||||
Forms\Components\Textarea::make('send_message')->label('Текст после отправления письма')->required(),
|
||||
TextInput::make('button')
|
||||
->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([
|
||||
Forms\Components\Repeater::make('mail_settings')
|
||||
->label('')
|
||||
->label('Настройки уведомлений')
|
||||
->addActionLabel('Добавить получателя')
|
||||
->helperText('Укажите, кому и какие уведомления отправлять')
|
||||
->collapsed()
|
||||
->itemLabel(fn (array $state): ?string => $state['target'] ?? 'Новый получатель')
|
||||
->schema([
|
||||
TextInput::make('target')->label('Кому')->email()->required(),
|
||||
TextInput::make('topic')->label('Тема')->required(),
|
||||
Builder::make('data')->schema([
|
||||
Builder\Block::make('text')->schema([
|
||||
RichEditor::make('content')->required(),
|
||||
TextInput::make('target')
|
||||
->label('Email получателя')
|
||||
->placeholder('email@example.com')
|
||||
->email()
|
||||
->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\FileUpload;
|
||||
use Filament\Forms\Components\Hidden;
|
||||
use Filament\Forms\Components\Repeater;
|
||||
use Filament\Forms\Components\RichEditor;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Forms\Form;
|
||||
@@ -29,23 +31,44 @@ use Mohamedsabil83\FilamentFormsTinyeditor\Components\TinyEditor;
|
||||
|
||||
class FormBuilderItem
|
||||
{
|
||||
public static function getItem()
|
||||
public static function getItem(): Builder
|
||||
{
|
||||
return Builder::make('columns')
|
||||
->label('Конструктор полей формы')
|
||||
->addActionLabel('Добавить новое поле')
|
||||
->blockPickerColumns(3)
|
||||
->blockPickerWidth('2xl')
|
||||
->collapsed()
|
||||
->collapsible()
|
||||
->cloneable()
|
||||
->schema([
|
||||
// Email поле
|
||||
Builder\Block::make('email')
|
||||
->label('Почта')
|
||||
->icon('heroicon-o-envelope')
|
||||
->label('Поле Email')
|
||||
->schema([
|
||||
TextInput::make('title_field')
|
||||
->label('Заголовок поля')
|
||||
->label('Название поля')
|
||||
->placeholder('Например: Ваш Email')
|
||||
->helperText('Это название будет отображаться пользователям')
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->live(onBlur: true)
|
||||
->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()
|
||||
->statePath('rules')
|
||||
->schema([
|
||||
@@ -54,18 +77,34 @@ class FormBuilderItem
|
||||
RuleLengthLimitComponent::getComponent(),
|
||||
]),
|
||||
]),
|
||||
|
||||
// Phone поле
|
||||
Builder\Block::make('phone')
|
||||
->label('Телефон')
|
||||
->icon('heroicon-o-phone')
|
||||
->label('Поле Телефона')
|
||||
->schema([
|
||||
TextInput::make('title_field')
|
||||
->label('Заголовок поля')
|
||||
->label('Название поля')
|
||||
->placeholder('Например: Ваш телефон')
|
||||
->helperText('Укажите контактный номер для связи')
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->live(onBlur: true)
|
||||
->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('Например: +7 (XXX) XXX-XX-XX')
|
||||
->maxLength(500)
|
||||
->columnSpanFull(),
|
||||
|
||||
Section::make('Дополнительные настройки')
|
||||
->collapsible()
|
||||
->collapsed()
|
||||
->statePath('rules')
|
||||
->schema([
|
||||
RuleRequiredComponent::getComponent(),
|
||||
@@ -73,73 +112,135 @@ class FormBuilderItem
|
||||
RuleLengthLimitComponent::getComponent(),
|
||||
]),
|
||||
]),
|
||||
|
||||
// Короткий текст
|
||||
Builder\Block::make('text')
|
||||
->icon('heroicon-o-pencil')
|
||||
->label('Короткий текст')
|
||||
->schema([
|
||||
TextInput::make('title_field')
|
||||
->label('Заголовок поля')
|
||||
->label('Название поля')
|
||||
->placeholder('Например: Ваше имя')
|
||||
->helperText('Краткий текст (до 255 символов)')
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->live(onBlur: true)
|
||||
->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('Настройка')
|
||||
->statePath('rules')
|
||||
->schema([
|
||||
RuleRequiredComponent::getComponent(),
|
||||
RuleLengthLimitComponent::getComponent(),
|
||||
]),
|
||||
]),
|
||||
Builder\Block::make('textarea')
|
||||
->label('Длинный текст текст')
|
||||
->schema([
|
||||
TextInput::make('title_field')
|
||||
->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('Настройка')
|
||||
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('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')
|
||||
->icon('heroicon-o-calendar')
|
||||
->label('Дата')
|
||||
->schema([
|
||||
TextInput::make('title_field')
|
||||
->label('Заголовок поля')
|
||||
->label('Название поля')
|
||||
->placeholder('Например: Дата рождения')
|
||||
->helperText('Выбор даты из календаря')
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->live(onBlur: true)
|
||||
->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')
|
||||
->schema([
|
||||
RuleRequiredComponent::getComponent(),
|
||||
]),
|
||||
|
||||
]),
|
||||
|
||||
// Ссылка
|
||||
Builder\Block::make('url')
|
||||
->icon('heroicon-o-link')
|
||||
->label('Ссылка')
|
||||
->schema([
|
||||
TextInput::make('title_field')
|
||||
->label('Заголовок поля')
|
||||
->label('Название поля')
|
||||
->placeholder('Например: Ваш сайт')
|
||||
->helperText('Введите корректный URL адрес')
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->live(onBlur: true)
|
||||
->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('Например: https://example.com')
|
||||
->maxLength(500)
|
||||
->columnSpanFull(),
|
||||
|
||||
Section::make('Дополнительные настройки')
|
||||
->collapsible()
|
||||
->collapsed()
|
||||
->statePath('rules')
|
||||
->schema([
|
||||
RuleRequiredComponent::getComponent(),
|
||||
@@ -147,140 +248,178 @@ class FormBuilderItem
|
||||
RuleLengthLimitComponent::getComponent(),
|
||||
]),
|
||||
]),
|
||||
|
||||
// Множественный выбор
|
||||
Builder\Block::make('multiple_choice')
|
||||
->label('Несколько вариантов')
|
||||
->icon('heroicon-o-check-circle')
|
||||
->label('Множественный выбор')
|
||||
->schema([
|
||||
TextInput::make('title_field')
|
||||
->label('Заголовок поля')
|
||||
->label('Название группы')
|
||||
->placeholder('Например: Ваши интересы')
|
||||
->helperText('Несколько вариантов с возможностью выбора')
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->live(onBlur: true)
|
||||
->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')
|
||||
->schema([
|
||||
RuleRequiredComponent::getComponent(),
|
||||
]),
|
||||
]),
|
||||
|
||||
|
||||
// Одиночный выбор
|
||||
Builder\Block::make('single_choice')
|
||||
->label('Один вариант')
|
||||
->icon('heroicon-o-radio')
|
||||
->label('Одиночный выбор')
|
||||
->schema([
|
||||
TextInput::make('title_field')
|
||||
->label('Заголовок поля')
|
||||
->label('Название группы')
|
||||
->placeholder('Например: Ваш пол')
|
||||
->helperText('Один вариант из предложенных')
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->live(onBlur: true)
|
||||
->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')
|
||||
->schema([
|
||||
RuleRequiredComponent::getComponent(),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
|
||||
// Дополнительное образование
|
||||
Builder\Block::make('additional_education_choice')
|
||||
->label('Выбрать дополнительное образование')
|
||||
->icon('heroicon-o-academic-cap')
|
||||
->label('Доп. образование')
|
||||
->schema([
|
||||
TextInput::make('title_field')
|
||||
->label('Заголовок поля')
|
||||
->label('Название поля')
|
||||
->placeholder('Например: Дополнительное образование')
|
||||
->helperText('Выбор из списка доп. образования')
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->live(onBlur: true)
|
||||
->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')
|
||||
->schema([
|
||||
// RuleRequiredComponent::getComponent(),
|
||||
// RuleLengthLimitComponent::getComponent(),
|
||||
]),
|
||||
->schema([]),
|
||||
])
|
||||
->maxItems(1),
|
||||
|
||||
// Образовательная программа
|
||||
Builder\Block::make('educational_program_choice')
|
||||
->label('Выбрать Образовательную программу')
|
||||
->icon('heroicon-o-book-open')
|
||||
->label('Образовательная программа')
|
||||
->schema([
|
||||
TextInput::make('title_field')
|
||||
->label('Заголовок поля')
|
||||
->label('Название поля')
|
||||
->placeholder('Например: Основная программа')
|
||||
->helperText('Выбор из списка образовательных программ')
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->live(onBlur: true)
|
||||
->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')
|
||||
->schema([
|
||||
RuleRequiredComponent::getComponent(),
|
||||
RuleLengthLimitComponent::getComponent(),
|
||||
]),
|
||||
])
|
||||
->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();
|
||||
|
||||
->maxItems(1)
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -13,10 +13,13 @@ use App\Models\Post;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Components\Builder;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\Grid;
|
||||
use Filament\Forms\Components\Hidden;
|
||||
use Filament\Forms\Components\Repeater;
|
||||
use Filament\Forms\Components\RichEditor;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Forms\Form;
|
||||
@@ -29,202 +32,347 @@ class ContentBuilderItem
|
||||
{
|
||||
public static function getItem(string $name)
|
||||
{
|
||||
return
|
||||
Builder::make($name)->label('')->blocks([
|
||||
Builder\Block::make('heading')->label('Заголовок')
|
||||
return Builder::make($name)
|
||||
->label('Конструктор содержимого')
|
||||
->blocks([
|
||||
// Заголовок
|
||||
Builder\Block::make('heading')
|
||||
->icon('heroicon-o-title')
|
||||
->label('Заголовок')
|
||||
->schema([
|
||||
TextInput::make('id')->hidden()->integer()->default(rand(2335235,324634264263426)),
|
||||
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) {
|
||||
}),
|
||||
->label('Текст заголовка')
|
||||
->placeholder('Введите заголовок H2-H4')
|
||||
->hint('Рекомендуется 50-80 символов')
|
||||
->helperText('Используйте для семантической структуры')
|
||||
->minLength(10)
|
||||
->maxLength(120)
|
||||
->required()
|
||||
->live(onBlur: true),
|
||||
]),
|
||||
|
||||
// Текстовый блок
|
||||
Builder\Block::make('paragraph')
|
||||
->icon('heroicon-o-document-text')
|
||||
->label('Текстовый блок')
|
||||
->schema([
|
||||
TinyEditor::make('content')
|
||||
->label('')
|
||||
->profile('test')
|
||||
->required(),
|
||||
])->label('Текст'),
|
||||
Builder\Block::make('files')
|
||||
->label('Файл(-ы)')
|
||||
->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')
|
||||
]),
|
||||
->label('Содержимое')
|
||||
->placeholder('Введите текст...')
|
||||
->hint('Поддерживается форматирование')
|
||||
->helperText('Для заголовков используйте стили H3-H4')
|
||||
->required()
|
||||
->columnSpanFull(),
|
||||
]),
|
||||
|
||||
// Файлы
|
||||
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')
|
||||
->label('Персона')
|
||||
->icon('heroicon-o-user')
|
||||
->label('Карточка сотрудника')
|
||||
->schema([
|
||||
TextInput::make('name')
|
||||
->label('Имя')
|
||||
->label('ФИО')
|
||||
->placeholder('Иванов Иван Иванович')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
|
||||
FileUpload::make('photo')
|
||||
->label('Фотография')
|
||||
->hint('Оптимальный размер 500x500px')
|
||||
->helperText('Автоматическая конвертация в WebP')
|
||||
->image()
|
||||
->optimize('webp')
|
||||
->resize(50)
|
||||
->disk('public')
|
||||
->directory('images')
|
||||
->imageEditor(),
|
||||
Forms\Components\Repeater::make('info')->schema([
|
||||
->directory('personnel')
|
||||
->imageEditor()
|
||||
->required(),
|
||||
|
||||
Repeater::make('info')
|
||||
->label('Характеристики')
|
||||
->hint('Добавьте 3-5 ключевых пунктов')
|
||||
->schema([
|
||||
TextInput::make('column')
|
||||
->label('Название колонки')
|
||||
->label('Параметр')
|
||||
->placeholder('Стаж работы')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
Forms\Components\Textarea::make('content')
|
||||
->label('Содержание')
|
||||
->maxLength(100),
|
||||
|
||||
Textarea::make('content')
|
||||
->label('Значение')
|
||||
->placeholder('10 лет')
|
||||
->required()
|
||||
->maxLength(1000),
|
||||
])->minItems(1)->label('Информация о персоне'),
|
||||
->maxLength(500),
|
||||
])
|
||||
->minItems(1)
|
||||
->maxItems(10)
|
||||
->collapsible()
|
||||
->itemLabel(fn (array $state): string => $state['column'] ?? 'Новый параметр'),
|
||||
]),
|
||||
|
||||
// Этапы
|
||||
Builder\Block::make('stepper')
|
||||
->label('Строитель этапов')
|
||||
->icon('heroicon-o-list-bullet')
|
||||
->label('Пошаговый процесс')
|
||||
->schema([
|
||||
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()
|
||||
->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')
|
||||
->label('Видео (Не стабильно)')
|
||||
->icon('heroicon-o-film')
|
||||
->label('Видео')
|
||||
->schema([
|
||||
TextInput::make('mime')->readOnly(),
|
||||
TextInput::make('mime')
|
||||
->label('Формат')
|
||||
->readOnly(),
|
||||
|
||||
TextInput::make('title')
|
||||
->label('Название видео')
|
||||
->placeholder('Обзор продукта')
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->autofocus(),
|
||||
->maxLength(255),
|
||||
|
||||
FileUpload::make('path')
|
||||
->label('Видеофайл')
|
||||
->hint('MP4, WebM, до 50MB')
|
||||
->helperText('Рекомендуемое разрешение 1080p')
|
||||
->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',
|
||||
])
|
||||
->maxSize(51200)
|
||||
->disk('public')
|
||||
->directory('videos')
|
||||
->afterStateUpdated(fn (callable $set, $state) => $set('mime', $state?->getMimeType())),
|
||||
->directory('videos'),
|
||||
]),
|
||||
|
||||
// Список новостей
|
||||
Builder\Block::make('postsList')
|
||||
->icon('heroicon-o-newspaper')
|
||||
->label('Лента новостей')
|
||||
->schema([
|
||||
Forms\Components\Grid::make(2)->schema([
|
||||
TextInput::make('count')
|
||||
->label('Количество запией')
|
||||
->integer(),
|
||||
Select::make('category')
|
||||
->options(Category::all()->pluck('title', 'id'))
|
||||
]),
|
||||
])->label('Список новостей'),
|
||||
Grid::make(2)
|
||||
->schema([
|
||||
TextInput::make('count')
|
||||
->label('Количество')
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->maxValue(20)
|
||||
->default(5)
|
||||
->required(),
|
||||
|
||||
Select::make('category')
|
||||
->label('Категория')
|
||||
->options(Category::all()->pluck('title', 'id'))
|
||||
->searchable()
|
||||
->placeholder('Все категории'),
|
||||
]),
|
||||
]),
|
||||
|
||||
// Отдельная новость
|
||||
Builder\Block::make('postItem')
|
||||
->icon('heroicon-o-document-text')
|
||||
->label('Конкретная новость')
|
||||
->schema([
|
||||
Select::make('post')
|
||||
->options(Post::query()->where('status', PostStatus::PUBLISHED)->pluck('title', 'id'))
|
||||
->label('Выберите новость')
|
||||
->options(Post::published()->pluck('title', 'id'))
|
||||
->searchable()
|
||||
->required(),
|
||||
])->label('Новость'),
|
||||
->required()
|
||||
->placeholder('Начните вводить название'),
|
||||
]),
|
||||
|
||||
// Страница
|
||||
Builder\Block::make('pageItem')
|
||||
->icon('heroicon-o-document')
|
||||
->label('Ссылка на страницу')
|
||||
->schema([
|
||||
Select::make('page')
|
||||
->options(Page::query()->where('title', '!=' , null)->where('is_visible', true)->pluck('title', 'id'))
|
||||
->label('Страница')
|
||||
->options(Page::visible()->pluck('title', 'id'))
|
||||
->searchable()
|
||||
->required(),
|
||||
])->label('Страница'),
|
||||
]),
|
||||
|
||||
// Форма
|
||||
Builder\Block::make('customForm')
|
||||
->icon('heroicon-o-clipboard-document')
|
||||
->label('Форма')
|
||||
->schema([
|
||||
Select::make('form')
|
||||
->options(CustomForm::query()->where('status', CustomFormStatus::PUBLISHED)->pluck('title', 'form_id'))
|
||||
->label('Выберите форму')
|
||||
->options(CustomForm::published()->pluck('title', 'form_id'))
|
||||
->searchable()
|
||||
->required(),
|
||||
])->label('Форма'),
|
||||
]),
|
||||
|
||||
// Ресурсы
|
||||
Builder\Block::make('pageResourceList')
|
||||
->icon('heroicon-o-archive-box')
|
||||
->label('Список ресурсов')
|
||||
->schema([
|
||||
Select::make('resource')
|
||||
->options(PageReferenceList::query()->where('is_active', true)->pluck('title', 'slug'))
|
||||
->label('Ресурс')
|
||||
->options(PageReferenceList::active()->pluck('title', 'slug'))
|
||||
->searchable()
|
||||
->required(),
|
||||
])->label('Ресурсы'),
|
||||
]),
|
||||
])
|
||||
->collapsed()
|
||||
->blockNumbers(false)
|
||||
->collapsible()
|
||||
->blockPickerColumns(3)
|
||||
->blockPickerWidth('2xl')
|
||||
->addActionLabel('Добавить новый блок');
|
||||
->collapsed()
|
||||
->blockNumbers(false)
|
||||
->collapsible()
|
||||
->blockPickerColumns(3)
|
||||
->blockPickerWidth('2xl')
|
||||
->addActionLabel('Добавить блок')
|
||||
->addBetweenActionLabel('Вставить блок между')
|
||||
->cloneActionLabel('Клонировать блок');
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -6,10 +6,12 @@ use App\Enums\CustomFormStatus;
|
||||
use App\Enums\PostStatus;
|
||||
use App\Helpers\ByteConverter;
|
||||
use App\Models\Category;
|
||||
use App\Models\ContactWidget;
|
||||
use App\Models\CustomForm;
|
||||
use App\Models\Page;
|
||||
use App\Models\PageReferenceList;
|
||||
use App\Models\Post;
|
||||
use App\Models\Slider;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Components\Builder;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
@@ -29,210 +31,356 @@ class TabBuilderItem
|
||||
{
|
||||
public static function getItem()
|
||||
{
|
||||
return
|
||||
Builder\Block::make('tabs')
|
||||
->label('Вкладки')
|
||||
->schema([
|
||||
Forms\Components\Repeater::make('tab')->schema([
|
||||
TextInput::make('title')
|
||||
return Builder::make('content')
|
||||
->label('Содержимое вкладки')
|
||||
->blocks([
|
||||
Builder\Block::make('heading')
|
||||
->label('Заголовок')
|
||||
->icon('heroicon-o-hashtag')
|
||||
->schema([
|
||||
TextInput::make('id')
|
||||
->hidden()
|
||||
->integer()
|
||||
->default(rand(2335235, 324634264263426)),
|
||||
TextInput::make('content')
|
||||
->label('Текст заголовка')
|
||||
->placeholder('Введите текст заголовка')
|
||||
->helperText('Основной заголовок раздела')
|
||||
->live(onBlur: true)
|
||||
->required()
|
||||
->maxLength(255)->columnSpanFull(),
|
||||
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([
|
||||
TinyEditor::make('content')
|
||||
->label('')
|
||||
->profile('test')
|
||||
->required(),
|
||||
])->label('Текст'),
|
||||
Builder\Block::make('files')
|
||||
->label('Файл(-ы)')
|
||||
->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')
|
||||
->label('Персона')
|
||||
->schema([
|
||||
TextInput::make('name')
|
||||
->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')
|
||||
->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)
|
||||
->maxLength(255),
|
||||
]),
|
||||
|
||||
Builder\Block::make('paragraph')
|
||||
->label('Текст')
|
||||
->icon('heroicon-o-document-text')
|
||||
->schema([
|
||||
Toggle::make('seo_active')
|
||||
->label('Использовать блок как SEO-текст')
|
||||
->helperText('Этот текст будет использоваться для SEO-оптимизации')
|
||||
->live(onBlur: true)
|
||||
->required()
|
||||
->disabled(function ($state, Forms\Get $get) {
|
||||
$data = $get('../../');
|
||||
return self::findSeoActive($data) && !$state;
|
||||
})
|
||||
->dehydrated(),
|
||||
TinyEditor::make('content')
|
||||
->label('Текст')
|
||||
->placeholder('Начните вводить текст...')
|
||||
->profile('test')
|
||||
->required()
|
||||
->helperText('Основное текстовое содержимое блока'),
|
||||
]),
|
||||
|
||||
Builder\Block::make('files')
|
||||
->label('Файлы')
|
||||
->icon('heroicon-o-paper-clip')
|
||||
->schema([
|
||||
Forms\Components\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()
|
||||
->blockPickerColumns(3)
|
||||
->blockPickerWidth('2xl')
|
||||
->addActionLabel('Добавить новый блок'),
|
||||
])->minItems(1),
|
||||
]);
|
||||
->cloneable()
|
||||
->grid(2),
|
||||
]),
|
||||
|
||||
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\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\Models\Category;
|
||||
use App\Models\ContactWidget;
|
||||
@@ -14,6 +30,7 @@ use App\Models\Post;
|
||||
use App\Models\Slider;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Components\Builder;
|
||||
use Filament\Forms\Components\Fieldset;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\Hidden;
|
||||
use Filament\Forms\Components\RichEditor;
|
||||
@@ -29,407 +46,99 @@ use Mohamedsabil83\FilamentFormsTinyeditor\Components\TinyEditor;
|
||||
|
||||
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')
|
||||
->schema([
|
||||
Toggle::make('seo_active')->label('Использовать блок как seo')
|
||||
->live(onBlur: true)
|
||||
->required()
|
||||
->disabled(function ($state, Forms\Get $get) {
|
||||
$data = $get('../../');
|
||||
return self::findSeoActive($data) && !$state;
|
||||
})
|
||||
->dehydrated(),
|
||||
TinyEditor::make('content')
|
||||
->label('')
|
||||
->profile('test')
|
||||
->required(),
|
||||
])->label('Текст'),
|
||||
->label('Текст')
|
||||
->icon('heroicon-o-document-text')
|
||||
->schema(ParagraphBlock::schema()),
|
||||
|
||||
Builder\Block::make('files')
|
||||
->label('Файл(-ы)')
|
||||
->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')
|
||||
]),
|
||||
]),
|
||||
->label('Файлы')
|
||||
->icon('heroicon-o-paper-clip')
|
||||
->schema(FilesBlock::schema()),
|
||||
|
||||
Builder\Block::make('person')
|
||||
->label('Персона')
|
||||
->schema([
|
||||
TextInput::make('name')
|
||||
->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('Информация о персоне'),
|
||||
]),
|
||||
->icon('heroicon-o-user')
|
||||
->schema(PersonBlock::schema()),
|
||||
|
||||
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),
|
||||
]),
|
||||
->label('Этапы')
|
||||
->icon('heroicon-o-list-bullet')
|
||||
->schema(StepperBlock::schema()),
|
||||
|
||||
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),
|
||||
]),
|
||||
->icon('heroicon-o-rectangle-stack')
|
||||
->schema(TabBlock::schema()),
|
||||
|
||||
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('Слайдер изображений'),
|
||||
->label('Слайдер изображений')
|
||||
->icon('heroicon-o-photo')
|
||||
->schema(ImagesBlock::schema()),
|
||||
|
||||
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('Изображение'),
|
||||
->label('Изображение')
|
||||
->icon('heroicon-o-photo')
|
||||
->schema(ImagesBlock::schema()),
|
||||
|
||||
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())),
|
||||
]),
|
||||
->label('Видео')
|
||||
->icon('heroicon-o-film')
|
||||
->schema(VideoBlock::schema()),
|
||||
|
||||
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('Список новостей'),
|
||||
->label('Список новостей')
|
||||
->icon('heroicon-o-newspaper')
|
||||
->schema(PostListBlock::schema()),
|
||||
|
||||
Builder\Block::make('postItem')
|
||||
->schema([
|
||||
Select::make('post')
|
||||
->options(Post::query()->where('status', PostStatus::PUBLISHED)->pluck('title', 'id'))
|
||||
->searchable()
|
||||
->required(),
|
||||
])->label('Новость'),
|
||||
->label('Конкретная новость')
|
||||
->icon('heroicon-o-document-text')
|
||||
->schema(PostItemBlock::schema()),
|
||||
|
||||
Builder\Block::make('pageItem')
|
||||
->schema([
|
||||
Select::make('page')
|
||||
->options(Page::query()->where('title', '!=' , null)->where('is_visible', true)->pluck('title', 'id'))
|
||||
->searchable()
|
||||
->required(),
|
||||
])->label('Страница'),
|
||||
->label('Конкретная страница')
|
||||
->icon('heroicon-o-document')
|
||||
->schema(PageItemBlock::schema()),
|
||||
|
||||
Builder\Block::make('customForm')
|
||||
->schema([
|
||||
Select::make('form')
|
||||
->options(CustomForm::query()->where('status', CustomFormStatus::PUBLISHED)->pluck('title', 'form_id'))
|
||||
->searchable()
|
||||
->required(),
|
||||
])->label('Форма'),
|
||||
->label('Пользовательская форма')
|
||||
->icon('heroicon-o-clipboard-document-list')
|
||||
->schema(CustomFormBlock::schema()),
|
||||
|
||||
Builder\Block::make('pageResourceList')
|
||||
->schema([
|
||||
Select::make('resource')
|
||||
->options(PageReferenceList::query()->where('is_active', true)->pluck('title', 'slug'))
|
||||
->searchable()
|
||||
->required(),
|
||||
])->label('Ресурсы'),
|
||||
->label('Ресурсы')
|
||||
->icon('heroicon-o-archive-box')
|
||||
->schema(PageResourceListBlock::schema()),
|
||||
|
||||
Builder\Block::make('contact')
|
||||
->schema([
|
||||
Select::make('contact')
|
||||
->options(ContactWidget::query()->where('is_active', true)->pluck('title', 'slug'))
|
||||
->searchable()
|
||||
->required(),
|
||||
])->label('Контакты'),
|
||||
->label('Контакты')
|
||||
->icon('heroicon-o-phone')
|
||||
->schema(ContactBlock::schema()),
|
||||
|
||||
Builder\Block::make('slider')
|
||||
->schema([
|
||||
Select::make('slider')
|
||||
->options(Slider::query()->whereHas('slides')->where('is_active', true)->pluck('title', 'slug'))
|
||||
->searchable()
|
||||
->required(),
|
||||
])->label('Слайдеры'),
|
||||
->label('Слайдер')
|
||||
->icon('heroicon-o-presentation-chart-line')
|
||||
->schema(SliderBlock::schema()),
|
||||
])
|
||||
->collapsed()
|
||||
->blockNumbers(false)
|
||||
->collapsible()
|
||||
->blockPickerColumns(3)
|
||||
->blockPickerWidth('2xl')
|
||||
->addActionLabel('Добавить новый блок');
|
||||
->collapsed()
|
||||
->blockNumbers(false)
|
||||
->collapsible()
|
||||
->blockPickerColumns(3)
|
||||
->blockPickerWidth('2xl')
|
||||
->addActionLabel('Добавить новый блок')
|
||||
->cloneable()
|
||||
->reorderableWithButtons();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ use App\Enums\CustomFormStatus;
|
||||
use App\Enums\PostStatus;
|
||||
use App\Filament\Components\Forms\ItemForm\Pages\ContentBuilderItem;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
@@ -22,69 +23,129 @@ class PageForm
|
||||
->schema([
|
||||
Section::make()
|
||||
->schema([
|
||||
Forms\Components\Tabs::make('')->schema([
|
||||
Forms\Components\Tabs\Tab::make('Основная информация')->schema([
|
||||
Forms\Components\Grid::make(2)->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('path', Str::slug($state));
|
||||
}),
|
||||
TextInput::make('slug')->label('Текстовый идентификатор страницы')->unique(ignoreRecord: true)->required()
|
||||
->live(onBlur: true)
|
||||
->afterStateUpdated(function (string $operation, string $state, Forms\Set $set, $get) {
|
||||
$set('path', Str::slug($state));
|
||||
}),
|
||||
]),
|
||||
Select::make('sub_section_id')->label('Подраздел')
|
||||
->relationship('section', 'title')
|
||||
->createOptionForm([
|
||||
Forms\Components\TextInput::make('title')->label('Название подраздела')->required()
|
||||
->live(onBlur: true)
|
||||
->afterStateUpdated(function (string $operation, string $state, Forms\Set $set) {
|
||||
$set('slug', Str::slug($state));
|
||||
}),
|
||||
TextInput::make('slug')->label('Slug')->unique(ignoreRecord: true)->readOnly()->required(),
|
||||
Forms\Components\Tabs::make('Настройки страницы')
|
||||
->persistTabInQueryString()
|
||||
->columnSpanFull()
|
||||
->tabs([
|
||||
Forms\Components\Tabs\Tab::make('Основная информация')
|
||||
->icon('heroicon-o-information-circle')
|
||||
->schema([
|
||||
Forms\Components\Grid::make(2)
|
||||
->schema([
|
||||
TextInput::make('title')
|
||||
->label('Заголовок страницы')
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->placeholder('Введите название страницы')
|
||||
->helperText('Этот заголовок будет отображаться в заголовке страницы и в навигации')
|
||||
->live(onBlur: true)
|
||||
->afterStateUpdated(function (string $operation, string|null $state, Forms\Set $set) {
|
||||
$set('slug', Str::slug($state));
|
||||
$set('path', Str::slug($state));
|
||||
}),
|
||||
TextInput::make('slug')
|
||||
->label('URL-адрес страницы')
|
||||
->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),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
|
||||
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ use App\Models\CustomForm;
|
||||
use App\Models\Page;
|
||||
use App\Models\PageReferenceList;
|
||||
use App\Models\Post;
|
||||
use App\Models\Slider;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Components\Builder;
|
||||
use Filament\Forms\Components\ColorPicker;
|
||||
@@ -38,8 +39,6 @@ use Symfony\Component\Finder\Finder;
|
||||
|
||||
class PostForm
|
||||
{
|
||||
|
||||
|
||||
public static function getForm(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
@@ -49,155 +48,264 @@ class PostForm
|
||||
Tabs::make('Tabs')
|
||||
->tabs([
|
||||
Tabs\Tab::make('Основная информация')
|
||||
->icon('heroicon-o-information-circle')
|
||||
->schema([
|
||||
Forms\Components\Grid::make(2)->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(),
|
||||
]),
|
||||
Select::make('status')->options(PostStatus::class)
|
||||
->label('Статус')->required()
|
||||
Grid::make(2)
|
||||
->schema([
|
||||
TextInput::make('title')
|
||||
->label('Заголовок')
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->placeholder('Введите заголовок новости')
|
||||
->helperText('Этот заголовок будет отображаться на сайте')
|
||||
->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('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 =>
|
||||
$value == PostStatus::PUBLISHED->value && !auth()->user()->can('publish_post')
|
||||
)
|
||||
->default(PostStatus::VERIFICATION),
|
||||
),
|
||||
Select::make('category_id')
|
||||
->label('Категория')
|
||||
->options(Category::all()->pluck('title', 'id'))
|
||||
->searchable()
|
||||
->preload()
|
||||
->label('Категория'),
|
||||
SpatieTagsInput::make('tags')->label('Тэги'),
|
||||
->placeholder('Выберите категорию')
|
||||
->helperText('Выберите категорию для новости'),
|
||||
SpatieTagsInput::make('tags')
|
||||
->label('Теги')
|
||||
->placeholder('Добавьте теги')
|
||||
->helperText('Добавьте теги для лучшей классификации'),
|
||||
Forms\Components\TagsInput::make('authors')
|
||||
->label('Авторы')->placeholder('Добавить автора'),
|
||||
Section::make('Отложенная публикация')->schema([
|
||||
Grid::make(2)->schema([
|
||||
Toggle::make('publish_setting.publish_after')
|
||||
->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()),
|
||||
->label('Авторы')
|
||||
->placeholder('Добавить автора')
|
||||
->helperText('Укажите авторов новости')
|
||||
->suggestions([
|
||||
'Редакция',
|
||||
'Администратор',
|
||||
]),
|
||||
]),
|
||||
Section::make('Публикация в сервисах')->schema([
|
||||
Forms\Components\Grid::make()->schema([
|
||||
Toggle::make('publication.vk')->label('Публикация в VK')->default(true),
|
||||
Toggle::make('publication.telegram')->label('Публикация в Telegram')->default(true),
|
||||
Section::make('Отложенная публикация')
|
||||
->description('Настройте автоматическую публикацию новости в указанное время')
|
||||
->collapsible()
|
||||
->schema([
|
||||
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([
|
||||
ContentBuilderItem::getItem('content')->required(),
|
||||
ContentBuilderItem::getItem('content')
|
||||
->required()
|
||||
->helperText('Создайте содержимое новости используя конструктор'),
|
||||
]),
|
||||
Tabs\Tab::make('Изображения')
|
||||
Tabs\Tab::make('Медиа')
|
||||
->icon('heroicon-o-photo')
|
||||
->schema([
|
||||
FileUpload::make('preview')->label('Превью новости')
|
||||
FileUpload::make('preview')
|
||||
->label('Главное изображение')
|
||||
->image()
|
||||
->directory('posts/previews')
|
||||
->optimize('webp')
|
||||
->resize(50)
|
||||
->imageEditor()
|
||||
->directory('images'),
|
||||
FileUpload::make('images')->label('Альбом')
|
||||
->helperText('Загрузите главное изображение для новости')
|
||||
->maxSize(2048)
|
||||
->acceptedFileTypes(['image/jpeg', 'image/png', 'image/webp'])
|
||||
->imagePreviewHeight('150')
|
||||
->panelLayout('integrated'),
|
||||
FileUpload::make('images')
|
||||
->label('Галерея изображений')
|
||||
->image()
|
||||
->directory('posts/gallery')
|
||||
->optimize('jpg')
|
||||
->resize(30)
|
||||
->imageEditor()
|
||||
->panelLayout('grid')
|
||||
->reorderable()
|
||||
->imageEditor()
|
||||
->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([
|
||||
Toggle::make('is_slider_enabled')
|
||||
->label('Добавить новый слайд')
|
||||
->label('Добавить в слайдер')
|
||||
->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)
|
||||
->default(false),
|
||||
Section::make()
|
||||
Section::make('Настройки слайда')
|
||||
->description('Настройте отображение новости в слайдере')
|
||||
->collapsible()
|
||||
->collapsed()
|
||||
->schema([
|
||||
Forms\Components\Section::make('Информация слайда')->schema([
|
||||
Forms\Components\TextInput::make('slide.title')
|
||||
->label('Заголовок слайда'),
|
||||
Forms\Components\Textarea::make('slide.content')
|
||||
->label('Текст слайда'),
|
||||
Forms\Components\Grid::make()->schema([
|
||||
Select::make('slide.slider_id')
|
||||
->label('Выберите слайдер')
|
||||
->options(Slider::where('is_active', true)->pluck('title', 'id'))
|
||||
->required()
|
||||
->helperText('Выберите слайдер для размещения'),
|
||||
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')
|
||||
->label('Цвет текста')
|
||||
->default('#ffffff')
|
||||
->required(),
|
||||
Forms\Components\ToggleButtons::make('slide.settings.text_position')
|
||||
->required()
|
||||
->helperText('Выберите цвет текста на слайде'),
|
||||
ToggleButtons::make('slide.settings.text_position')
|
||||
->label('Позиция текста')
|
||||
->options([
|
||||
'left' => 'Текст слева',
|
||||
'center' => 'Текст по середине',
|
||||
'right' => 'Текст справа'
|
||||
'left' => 'Слева',
|
||||
'center' => 'По центру',
|
||||
'right' => 'Справа',
|
||||
])
|
||||
->inline()->default('left')->grouped()
|
||||
->label('Позиция текста на слайде'),
|
||||
->inline()
|
||||
->grouped()
|
||||
->default('left')
|
||||
->helperText('Выберите расположение текста на слайде'),
|
||||
]),
|
||||
Forms\Components\Grid::make()->schema([
|
||||
Grid::make()
|
||||
->schema([
|
||||
Toggle::make('active_button')
|
||||
->label('Использовать кнопку для ссылки (Ссылка будет открываться при нажатии на слайд)')
|
||||
->label('Добавить кнопку')
|
||||
->inline(false)
|
||||
->live()
|
||||
->helperText('Добавить кнопку со ссылкой на новость')
|
||||
->afterStateHydrated(function (Toggle $component, $state, $get) {
|
||||
$component->state(true);
|
||||
})
|
||||
->dehydrated(false),
|
||||
Forms\Components\TextInput::make('slide.settings.link_text')
|
||||
->default('Читать')
|
||||
TextInput::make('slide.settings.link_text')
|
||||
->label('Текст кнопки')
|
||||
->default('Читать')
|
||||
->maxLength(20)
|
||||
->disabled(fn (Forms\Get $get) => !$get('active_button'))
|
||||
->helperText('Текст для кнопки перехода'),
|
||||
]),
|
||||
]),
|
||||
Forms\Components\Section::make('Изображение')->schema([
|
||||
FileUpload::make('slide.image.url')
|
||||
->label('Изображение')
|
||||
->image()
|
||||
->optimize('webp')
|
||||
->resize(50)
|
||||
->disk('public')
|
||||
->directory('images')
|
||||
->imageEditor()
|
||||
->required(),
|
||||
ToggleButtons::make('slide.image.shading')->inline()->grouped()->label('Уровень затемнения изображения')->options([
|
||||
'1' => 'Без затемнения',
|
||||
'0.7' => 'Слабое затемнение',
|
||||
'0.5' => 'Среднее затемнение',
|
||||
'0.3' => 'Сильное затемнение',
|
||||
Section::make('Изображение слайда')
|
||||
->schema([
|
||||
FileUpload::make('slide.image.url')
|
||||
->label('Фоновое изображение')
|
||||
->image()
|
||||
->directory('sliders')
|
||||
->optimize('webp')
|
||||
->resize(50)
|
||||
->imageEditor()
|
||||
->required()
|
||||
->maxSize(2048)
|
||||
->helperText('Загрузите фоновое изображение для слайда')
|
||||
->acceptedFileTypes(['image/jpeg', 'image/png', 'image/webp']),
|
||||
ToggleButtons::make('slide.image.shading')
|
||||
->label('Затемнение фона')
|
||||
->inline()
|
||||
->grouped()
|
||||
->options([
|
||||
'1' => 'Нет',
|
||||
'0.7' => 'Слабое',
|
||||
'0.5' => 'Среднее',
|
||||
'0.3' => 'Сильное',
|
||||
])
|
||||
->helperText('Выберите уровень затемнения фона'),
|
||||
]),
|
||||
]),
|
||||
Forms\Components\Section::make('Общая часть')->schema([
|
||||
Forms\Components\Grid::make()->schema([
|
||||
Section::make('Время показа')
|
||||
->schema([
|
||||
DateTimePicker::make('slide.end_time')
|
||||
->label('Слайд действует до')
|
||||
->native()
|
||||
->displayFormat('d/m/Y')
|
||||
->minDate(Carbon::now())
|
||||
->maxDate(Carbon::now()->addMonth()),
|
||||
->label('Дата окончания показа')
|
||||
->native(false)
|
||||
->displayFormat('d/m/Y H:i')
|
||||
->minDate(now())
|
||||
->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(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ class Backups extends BaseBackups
|
||||
|
||||
public static function getNavigationGroup(): ?string
|
||||
{
|
||||
return 'Settings';
|
||||
return 'Настройки приложения';
|
||||
}
|
||||
|
||||
public static function getNavigationLabel(): string
|
||||
|
||||
@@ -15,6 +15,7 @@ class CheckpointSettingsPage extends SettingsPage
|
||||
{
|
||||
protected static ?string $slug = 'checkpoint/settings';
|
||||
|
||||
|
||||
protected static ?string $navigationIcon = 'heroicon-o-adjustments-horizontal';
|
||||
|
||||
protected static string $settings = CheckpointSettings::class;
|
||||
@@ -36,7 +37,7 @@ class CheckpointSettingsPage extends SettingsPage
|
||||
|
||||
public static function getNavigationGroup(): ?string
|
||||
{
|
||||
return 'Settings'; // Группа навигации
|
||||
return 'Настройки приложения'; // Группа навигации
|
||||
}
|
||||
|
||||
public function form(Form $form): Form
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Filament\Resources;
|
||||
|
||||
use App\Enums\CustomFormStatus;
|
||||
use App\Enums\PostStatus;
|
||||
use App\Filament\Components\Forms\ItemForm\Pages\ContentBuilderItem;
|
||||
use App\Filament\Resources\AcademicJournalResource\Pages;
|
||||
use App\Filament\Resources\AcademicJournalResource\RelationManagers;
|
||||
use App\Filament\Resources\AcademicJournalResource\RelationManagers\JournalsRelationManager;
|
||||
@@ -14,10 +15,13 @@ use App\Models\CustomForm;
|
||||
use App\Models\Page;
|
||||
use App\Models\Post;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Forms\Components\Builder;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\Grid;
|
||||
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;
|
||||
@@ -33,780 +37,141 @@ use Mohamedsabil83\FilamentFormsTinyeditor\Components\TinyEditor;
|
||||
|
||||
class AcademicJournalResource extends Resource
|
||||
{
|
||||
|
||||
protected static ?string $navigationGroup = 'Наука';
|
||||
|
||||
public static ?string $label = 'Журнал';
|
||||
|
||||
protected static ?string $pluralLabel = 'Научные журналы';
|
||||
|
||||
protected static ?string $model = AcademicJournal::class;
|
||||
|
||||
protected static ?string $navigationIcon = 'heroicon-o-beaker';
|
||||
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Forms\Components\Section::make()->schema([
|
||||
Forms\Components\Grid::make(2)->schema([
|
||||
TextInput::make('title')->label('Заголовок')->required()
|
||||
->live(onBlur: true)
|
||||
->afterStateUpdated(function (string $operation, string|null $state, Forms\Set $set) {
|
||||
$set('slug', Str::slug($state));
|
||||
}),
|
||||
TextInput::make('slug')->label('Slug')->unique(ignoreRecord: true)->readOnly()->required(),
|
||||
Section::make('Основные данные')
|
||||
->description('Основная информация о научном журнале')
|
||||
->schema([
|
||||
Grid::make(2)
|
||||
->schema([
|
||||
TextInput::make('title')
|
||||
->label('Название журнала')
|
||||
->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\Tab::make('Основная информация журнала')
|
||||
Tabs\Tab::make('Основная информация')
|
||||
->icon('heroicon-o-information-circle')
|
||||
->schema([
|
||||
Builder::make('main_info')->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')
|
||||
ContentBuilderItem::getItem('main_info')
|
||||
->label('Описание журнала')
|
||||
->helperText('Добавьте полное описание журнала, его историю и основные направления'),
|
||||
]),
|
||||
|
||||
Tabs\Tab::make('Редакционная коллегия')
|
||||
->icon('heroicon-o-user-group')
|
||||
->schema([
|
||||
Section::make('Главный редактор')
|
||||
->description('Информация о главном редакторе журнала')
|
||||
->collapsible()
|
||||
->schema([
|
||||
Forms\Components\Repeater::make('chief_editor')->label('')
|
||||
->schema([
|
||||
TextInput::make('name')
|
||||
->label('ФИО')
|
||||
->required()
|
||||
->maxLength(100)
|
||||
->placeholder('Иванов Иван Иванович'),
|
||||
TextInput::make('academicTitle')
|
||||
->label('Учёная степень')
|
||||
->required()
|
||||
->maxLength(50)
|
||||
->placeholder('д.т.н., профессор'),
|
||||
TextInput::make('position')
|
||||
->label('Должность')
|
||||
->required()
|
||||
->maxLength(100)
|
||||
->placeholder('Главный научный сотрудник'),
|
||||
TextInput::make('institution')
|
||||
->label('Учреждение')
|
||||
->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('Форма'),
|
||||
])
|
||||
->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('Добавить редактора'),
|
||||
->placeholder('МГУ имени М.В. Ломоносова'),
|
||||
])
|
||||
->maxItems(1)
|
||||
->reorderable(false)
|
||||
->helperText('Укажите данные главного редактора журнала'),
|
||||
]),
|
||||
|
||||
]),
|
||||
]),
|
||||
Tabs\Tab::make('Информация для авторов')
|
||||
->schema([
|
||||
Builder::make('for_authors')->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')
|
||||
Section::make('Редакционная коллегия')
|
||||
->description('Состав редакционной коллегии журнала')
|
||||
->collapsible()
|
||||
->schema([
|
||||
Forms\Components\Repeater::make('editors')->label('')
|
||||
->schema([
|
||||
TextInput::make('name')
|
||||
->label('ФИО')
|
||||
->required()
|
||||
->maxLength(100)
|
||||
->placeholder('Петров Петр Петрович'),
|
||||
TextInput::make('academicTitle')
|
||||
->label('Учёная степень')
|
||||
->required()
|
||||
->maxLength(50)
|
||||
->placeholder('к.ф.-м.н., доцент'),
|
||||
TextInput::make('position')
|
||||
->label('Должность')
|
||||
->required()
|
||||
->maxLength(100)
|
||||
->placeholder('Доцент кафедры'),
|
||||
TextInput::make('institution')
|
||||
->label('Учреждение')
|
||||
->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('Форма'),
|
||||
])
|
||||
->collapsed()
|
||||
->blockNumbers(false)
|
||||
->collapsible()
|
||||
->blockPickerColumns(3)
|
||||
->blockPickerWidth('2xl')
|
||||
->addActionLabel('Добавить новый блок'),
|
||||
->placeholder('СПбГУ'),
|
||||
])
|
||||
->collapsed()
|
||||
->collapsible()
|
||||
->addActionLabel('Добавить редактора')
|
||||
->reorderable(true)
|
||||
->itemLabel(fn (array $state): ?string => $state['name'] ?? null)
|
||||
->helperText('Добавьте членов редакционной коллегии журнала'),
|
||||
]),
|
||||
]),
|
||||
])->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
|
||||
->columns([
|
||||
//
|
||||
Tables\Columns\TextColumn::make('title')
|
||||
->label('Название журнала')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('created_at')
|
||||
->label('Дата создания')
|
||||
->dateTime('d.m.Y')
|
||||
->sortable(),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\EditAction::make(),
|
||||
Tables\Actions\EditAction::make()
|
||||
->iconButton()
|
||||
->tooltip('Редактировать'),
|
||||
Tables\Actions\DeleteAction::make()
|
||||
->iconButton()
|
||||
->tooltip('Удалить'),
|
||||
Tables\Actions\RestoreAction::make()
|
||||
->iconButton()
|
||||
->tooltip('Восстановить'),
|
||||
])
|
||||
->bulkActions([
|
||||
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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,42 +3,27 @@
|
||||
namespace App\Filament\Resources\AcademicJournalResource\Pages;
|
||||
|
||||
use App\Filament\Resources\AcademicJournalResource;
|
||||
use App\Services\Filament\Traits\SeoGenerate;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
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
|
||||
{
|
||||
$this->seoData = $this->generateSeo($data);
|
||||
$data['search_data'] = $this->generateSearchData($data['main_info']);
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function afterCreate(): void
|
||||
{
|
||||
$this->record->seo()->create($this->seoData);
|
||||
}
|
||||
|
||||
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),
|
||||
];
|
||||
$this->createSeo($this->record);
|
||||
}
|
||||
|
||||
private function generateSearchData(array $data) : string
|
||||
|
||||
@@ -3,20 +3,21 @@
|
||||
namespace App\Filament\Resources\AcademicJournalResource\Pages;
|
||||
|
||||
use App\Filament\Resources\AcademicJournalResource;
|
||||
use App\Services\Filament\Traits\SeoGenerate;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class EditAcademicJournal extends EditRecord
|
||||
{
|
||||
use SeoGenerate;
|
||||
|
||||
protected static string $resource = AcademicJournalResource::class;
|
||||
|
||||
protected array $seoData;
|
||||
|
||||
|
||||
protected function mutateFormDataBeforeSave(array $data): array
|
||||
{
|
||||
$this->seoData = $this->generateSeo($data);
|
||||
$data['search_data'] = $this->generateSearchData($data['main_info']);
|
||||
|
||||
return $data;
|
||||
@@ -24,24 +25,7 @@ class EditAcademicJournal extends EditRecord
|
||||
|
||||
protected function afterSave(): void
|
||||
{
|
||||
$this->record->seo()->update($this->seoData);
|
||||
}
|
||||
|
||||
|
||||
|
||||
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),
|
||||
];
|
||||
$this->updateSeo($this->record);
|
||||
}
|
||||
|
||||
private function getDataFromBlocks($block) : string
|
||||
@@ -94,15 +78,6 @@ class EditAcademicJournal extends EditRecord
|
||||
|
||||
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
|
||||
{
|
||||
|
||||
+103
-17
@@ -4,7 +4,9 @@ namespace App\Filament\Resources\AcademicJournalResource\RelationManagers;
|
||||
|
||||
use App\Models\AcademicJournal;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Components\DatePicker;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\RelationManagers\RelationManager;
|
||||
@@ -17,27 +19,54 @@ class JournalsRelationManager extends RelationManager
|
||||
{
|
||||
protected static string $relationship = 'journals';
|
||||
|
||||
protected static ?string $modelLabel = 'выпуск';
|
||||
protected static ?string $pluralModelLabel = 'выпуски';
|
||||
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('title')->required(),
|
||||
TextInput::make('title')
|
||||
->label('Название выпуска')
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->placeholder('Введите название выпуска журнала')
|
||||
->helperText('Например: "Том 15, №3 (2023)" или специальное название выпуска'),
|
||||
|
||||
FileUpload::make('path_file')
|
||||
->label('Файл выпуска')
|
||||
->required()
|
||||
->acceptedFileTypes([
|
||||
'application/pdf',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||
'application/zip'
|
||||
'application/pdf' => 'PDF',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'DOCX',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'XLSX',
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.presentation' => 'PPTX',
|
||||
'application/zip' => 'ZIP',
|
||||
])
|
||||
->maxSize(512000)
|
||||
->disk('public')
|
||||
->directory('files')
|
||||
->directory('journals/files')
|
||||
->downloadable()
|
||||
->visibility('public'),
|
||||
Forms\Components\TextInput::make('year_publication')->integer(),
|
||||
Toggle::make('is_active')->default(true)->label('Активный выпуск')->inline(false),
|
||||
->visibility('public')
|
||||
->helperText('Максимальный размер файла: 512MB. Допустимые форматы: PDF, DOCX, XLSX, PPTX, ZIP')
|
||||
->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')
|
||||
->recordTitleAttribute('title')
|
||||
->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([
|
||||
//
|
||||
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([
|
||||
Tables\Actions\CreateAction::make(),
|
||||
Tables\Actions\CreateAction::make()
|
||||
->label('Добавить выпуск')
|
||||
->modalHeading('Добавление нового выпуска'),
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\EditAction::make(),
|
||||
Tables\Actions\DetachAction::make(),
|
||||
Tables\Actions\EditAction::make()
|
||||
->iconButton()
|
||||
->tooltip('Редактировать'),
|
||||
|
||||
Tables\Actions\DeleteAction::make()
|
||||
->iconButton()
|
||||
->tooltip('Удалить')
|
||||
->modalHeading('Удаление выпуска')
|
||||
->modalDescription('Вы уверены, что хотите удалить этот выпуск?'),
|
||||
])
|
||||
->bulkActions([
|
||||
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;
|
||||
|
||||
use App\Filament\Resources\AdditionalEducationCategoryResource\Pages;
|
||||
use App\Filament\Resources\AdditionalEducationCategoryResource\RelationManagers;
|
||||
use App\Models\AdditionalEducationCategory;
|
||||
use App\Models\DirectionAdditionalEducation;
|
||||
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\Toggle;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Columns\BadgeColumn;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\SoftDeletingScope;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class AdditionalEducationCategoryResource extends Resource
|
||||
@@ -27,27 +30,57 @@ class AdditionalEducationCategoryResource extends Resource
|
||||
protected static ?string $pluralLabel = 'Категории дополнительного образования';
|
||||
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
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Forms\Components\Section::make()->schema([
|
||||
Forms\Components\Grid::make('2')->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(),
|
||||
Forms\Components\Select::make('dir_addit_educat_id')->required()->label('Направление доп. образования')
|
||||
->preload()
|
||||
->options(DirectionAdditionalEducation::where('is_active', true)->pluck('title', 'id'))
|
||||
Section::make('Основная информация')
|
||||
->description('Заполните данные о категории программ ДПО')
|
||||
->collapsible()
|
||||
->schema([
|
||||
Grid::make(2)
|
||||
->schema([
|
||||
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));
|
||||
})
|
||||
->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
|
||||
->columns([
|
||||
TextColumn::make('id')->label('ID')->sortable(),
|
||||
TextColumn::make('title')->label('Название')->sortable()->searchable(),
|
||||
TextColumn::make('created_at')->label('Дата создания')->sortable(),
|
||||
Tables\Columns\BadgeColumn::make('direction.title')->label('Направление')->sortable(),
|
||||
Tables\Columns\ToggleColumn::make('is_active')->label('Активно')->sortable(),
|
||||
TextColumn::make('title')
|
||||
->label('Название')
|
||||
->searchable()
|
||||
->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([
|
||||
//
|
||||
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([
|
||||
Tables\Actions\EditAction::make(),
|
||||
Tables\Actions\EditAction::make()
|
||||
->iconButton()
|
||||
->tooltip('Редактировать'),
|
||||
|
||||
Tables\Actions\ViewAction::make()
|
||||
->iconButton()
|
||||
->tooltip('Просмотреть'),
|
||||
])
|
||||
->bulkActions([
|
||||
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
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
return [];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
@@ -89,4 +169,4 @@ class AdditionalEducationCategoryResource extends Resource
|
||||
'edit' => Pages\EditAdditionalEducationCategory::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,41 +2,25 @@
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Enums\CustomFormStatus;
|
||||
use App\Enums\FormEducation;
|
||||
use App\Enums\LevelEducational;
|
||||
use App\Enums\PostStatus;
|
||||
use App\Filament\Components\Forms\ItemForm\Pages\ContentBuilderItem;
|
||||
use App\Filament\Resources\AdditionalEducationResource\Pages;
|
||||
use App\Filament\Resources\AdditionalEducationResource\RelationManagers;
|
||||
use App\Helpers\ByteConverter;
|
||||
use App\Models\AdditionalEducation;
|
||||
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\Components\Builder;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\Hidden;
|
||||
use Filament\Forms\Components\RichEditor;
|
||||
use Filament\Forms\Components\Grid;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\SpatieTagsInput;
|
||||
use Filament\Forms\Components\Tabs;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Columns\BadgeColumn;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
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;
|
||||
use Mohamedsabil83\FilamentFormsTinyeditor\Components\TinyEditor;
|
||||
|
||||
class AdditionalEducationResource extends Resource
|
||||
{
|
||||
@@ -45,401 +29,117 @@ class AdditionalEducationResource extends Resource
|
||||
protected static ?string $navigationGroup = 'Образование';
|
||||
|
||||
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
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Section::make()->schema([
|
||||
Tabs::make('Tabs')
|
||||
->tabs([
|
||||
Tabs\Tab::make('Основная информация')
|
||||
->schema([
|
||||
Forms\Components\Grid::make('2')->schema([
|
||||
TextInput::make('title')->label('Заголовок')->required()
|
||||
->live(onBlur: true)
|
||||
->afterStateUpdated(function (string $operation, string|null $state, Forms\Set $set) {
|
||||
$set('slug', Str::slug($state));
|
||||
}),
|
||||
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('Заголовок')
|
||||
Forms\Components\Tabs::make('Программа ДПО')
|
||||
->persistTabInQueryString()
|
||||
->columnSpanFull()
|
||||
->tabs([
|
||||
Forms\Components\Tabs\Tab::make('Основные данные')
|
||||
->icon('heroicon-o-information-circle')
|
||||
->schema([
|
||||
Section::make('Общая информация')
|
||||
->description('Основные сведения о программе')
|
||||
->schema([
|
||||
Grid::make(2)
|
||||
->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')
|
||||
->label('Название программы')
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->autofocus(),
|
||||
FileUpload::make('path')
|
||||
->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()
|
||||
->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())),
|
||||
->readonly()
|
||||
->maxLength(255)
|
||||
->unique(ignoreRecord: true)
|
||||
->helperText('Человеко-понятный URL для страницы программы'),
|
||||
]),
|
||||
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([
|
||||
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('Добавить новый блок'),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
TextInput::make('price')
|
||||
->label('Стоимость (руб)')
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(0)
|
||||
->placeholder('Укажите стоимость')
|
||||
->helperText('Полная стоимость программы'),
|
||||
|
||||
TextInput::make('learning_time')
|
||||
->label('Объем (часов)')
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->placeholder('Укажите количество часов')
|
||||
->helperText('Общий объем программы в академических часах'),
|
||||
|
||||
Select::make('form_education')
|
||||
->label('Форма обучения')
|
||||
->options(FormEducation::class)
|
||||
->required()
|
||||
->native(false)
|
||||
->placeholder('Выберите форму')
|
||||
->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')
|
||||
->label('Описание программы')
|
||||
->helperText('Создайте подробное описание программы с помощью конструктора')
|
||||
]),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -447,30 +147,94 @@ class AdditionalEducationResource extends Resource
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('id')->label('ID')->sortable(),
|
||||
TextColumn::make('title')->label('Название')->sortable()->searchable(),
|
||||
TextColumn::make('created_at')->label('Дата создания')->sortable(),
|
||||
Tables\Columns\BadgeColumn::make('category.title')->label('Категория')->sortable(),
|
||||
Tables\Columns\ToggleColumn::make('is_active')->label('Активно')->sortable(),
|
||||
TextColumn::make('title')
|
||||
->label('Название')
|
||||
->searchable()
|
||||
->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([
|
||||
//
|
||||
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([
|
||||
Tables\Actions\EditAction::make(),
|
||||
Tables\Actions\EditAction::make()
|
||||
->iconButton()
|
||||
->tooltip('Редактировать'),
|
||||
|
||||
Tables\Actions\ViewAction::make()
|
||||
->iconButton()
|
||||
->tooltip('Просмотреть'),
|
||||
])
|
||||
->bulkActions([
|
||||
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
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
return [];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
@@ -481,4 +245,4 @@ class AdditionalEducationResource extends Resource
|
||||
'edit' => Pages\EditAdditionalEducation::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-27
@@ -3,20 +3,20 @@
|
||||
namespace App\Filament\Resources\AdditionalEducationResource\Pages;
|
||||
|
||||
use App\Filament\Resources\AdditionalEducationResource;
|
||||
use App\Services\Filament\Traits\SeoGenerate;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class CreateAdditionalEducation extends CreateRecord
|
||||
{
|
||||
use SeoGenerate;
|
||||
|
||||
protected static string $resource = AdditionalEducationResource::class;
|
||||
|
||||
protected array $seoData;
|
||||
|
||||
protected function mutateFormDataBeforeCreate(array $data): array
|
||||
{
|
||||
$this->seoData = $this->generateSeo($data);
|
||||
|
||||
$data['search_data'] = $this->generateSearchData($data['content']);
|
||||
|
||||
return $data;
|
||||
@@ -24,32 +24,9 @@ class CreateAdditionalEducation extends CreateRecord
|
||||
|
||||
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
|
||||
{
|
||||
|
||||
+4
-20
@@ -3,22 +3,22 @@
|
||||
namespace App\Filament\Resources\AdditionalEducationResource\Pages;
|
||||
|
||||
use App\Filament\Resources\AdditionalEducationResource;
|
||||
use App\Services\Filament\Traits\SeoGenerate;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class EditAdditionalEducation extends EditRecord
|
||||
{
|
||||
use SeoGenerate;
|
||||
|
||||
protected static string $resource = AdditionalEducationResource::class;
|
||||
|
||||
protected array $seoData;
|
||||
|
||||
|
||||
|
||||
protected function mutateFormDataBeforeSave(array $data): array
|
||||
{
|
||||
$this->seoData = $this->generateSeo($data);
|
||||
|
||||
$data['search_data'] = $this->generateSearchData($data['content']);
|
||||
|
||||
return $data;
|
||||
@@ -26,25 +26,9 @@ class EditAdditionalEducation extends EditRecord
|
||||
|
||||
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
|
||||
{
|
||||
$data = null;
|
||||
|
||||
@@ -2,60 +2,138 @@
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Enums\FormEducation;
|
||||
use App\Enums\AdmissionCampaignStatus;
|
||||
use App\Enums\LevelEducational;
|
||||
use App\Filament\Resources\AdmissionCampaignResource\Pages;
|
||||
use App\Filament\Resources\AdmissionCampaignResource\RelationManagers;
|
||||
use App\Models\AdmissionCampaign;
|
||||
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\Form;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Columns\BadgeColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\SoftDeletingScope;
|
||||
|
||||
class AdmissionCampaignResource extends Resource
|
||||
{
|
||||
protected static ?string $model = AdmissionCampaign::class;
|
||||
|
||||
protected static ?string $navigationGroup = 'Образование';
|
||||
|
||||
protected static ?string $navigationIcon = 'heroicon-o-clipboard-document-check';
|
||||
|
||||
protected static ?string $pluralLabel = 'Приемная-компания';
|
||||
|
||||
protected static ?string $modelLabel = 'Приемная кампания';
|
||||
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Forms\Components\Section::make()->schema([
|
||||
TextInput::make('name')->label('Название')->required()->columnSpanFull(),
|
||||
Forms\Components\Grid::make()->schema([
|
||||
Forms\Components\Select::make('academic_year')->label('Академический год')->required()
|
||||
->options(self::generateAcademicYears()),
|
||||
Forms\Components\Select::make('status')->label('Статус')->required()
|
||||
->options(['1' => 'Активный', '2' => 'Архивный', '3' => 'Скрыт']),
|
||||
Section::make('Основные настройки')
|
||||
->description('Общая информация о приемной кампании')
|
||||
->collapsible()
|
||||
->schema([
|
||||
TextInput::make('name')
|
||||
->label('Название кампании')
|
||||
->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
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('name'),
|
||||
Tables\Columns\TextColumn::make('academic_year'),
|
||||
Tables\Columns\TextColumn::make('status'),
|
||||
TextColumn::make('name')
|
||||
->label('Название')
|
||||
->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([
|
||||
//
|
||||
Tables\Filters\SelectFilter::make('status')
|
||||
->label('Статус')
|
||||
->options(AdmissionCampaignStatus::class),
|
||||
|
||||
Tables\Filters\SelectFilter::make('academic_year')
|
||||
->label('Учебный год')
|
||||
->options(self::generateAcademicYears()),
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\EditAction::make(),
|
||||
Tables\Actions\EditAction::make()
|
||||
->iconButton()
|
||||
->tooltip('Редактировать'),
|
||||
|
||||
Tables\Actions\ViewAction::make()
|
||||
->iconButton()
|
||||
->tooltip('Просмотреть'),
|
||||
])
|
||||
->bulkActions([
|
||||
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
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
return [];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
@@ -99,15 +214,15 @@ class AdmissionCampaignResource extends Resource
|
||||
protected static function generateAcademicYears(): array
|
||||
{
|
||||
$currentYear = (int) date('Y') - 5;
|
||||
$yearsAhead = 10; // Количество лет вперед
|
||||
$yearsAhead = 10;
|
||||
$academicYears = [];
|
||||
|
||||
for ($i = 0; $i < $yearsAhead; $i++) {
|
||||
$startYear = $currentYear + $i;
|
||||
$endYear = $startYear + 1;
|
||||
$academicYears[$startYear] = "{$startYear}/{$endYear}";
|
||||
$academicYears["{$startYear}/{$endYear}"] = "{$startYear}/{$endYear}";
|
||||
}
|
||||
|
||||
return $academicYears;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,5 +8,6 @@ use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateAdmissionCampaign extends CreateRecord
|
||||
{
|
||||
|
||||
protected static string $resource = AdmissionCampaignResource::class;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,9 @@ use Filament\Forms;
|
||||
use Filament\Forms\Components\Builder;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\Hidden;
|
||||
use Filament\Forms\Components\Repeater;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Forms\Get;
|
||||
@@ -42,45 +44,127 @@ class AdmissionPlanResource extends Resource
|
||||
return $form
|
||||
->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()
|
||||
->label('Образовательная программа')
|
||||
->options(EducationalProgram::whereIn('status', [EducationalProgramStatus::PUBLISHED, EducationalProgramStatus::IN_PROGRESS])->pluck('name', 'id')),
|
||||
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;
|
||||
}),
|
||||
->preload()
|
||||
->placeholder('Выберите образовательную программу'),
|
||||
|
||||
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
|
||||
{
|
||||
return $table
|
||||
|
||||
@@ -24,46 +24,128 @@ class ContactWidgetResource extends Resource
|
||||
{
|
||||
protected static ?string $model = ContactWidget::class;
|
||||
|
||||
protected static ?string $pluralLabel = 'Контактная информация';
|
||||
|
||||
|
||||
protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack';
|
||||
|
||||
protected static ?string $navigationGroup = 'Виджеты';
|
||||
|
||||
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Forms\Components\Section::make('')->schema([
|
||||
Tabs::make('Tabs')
|
||||
->tabs([
|
||||
Tabs\Tab::make('Основная информация')
|
||||
->schema([
|
||||
Forms\Components\Grid::make(2)->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),
|
||||
])
|
||||
]),
|
||||
Tabs\Tab::make('Содержание ресурса')
|
||||
->schema([
|
||||
Repeater::make('content')->label('Ресурсы')->schema([
|
||||
TextInput::make('title')->label('Главный заголовок столбца')->required(),
|
||||
Repeater::make('items')->label('Контакты')->schema([
|
||||
TextInput::make('header')->label('Заголовок')->required(),
|
||||
Repeater::make('details')->label('Компонент контакта')->schema([
|
||||
Forms\Components\Grid::make(2)->schema([
|
||||
TextInput::make('content')->label('содержание')->required(),
|
||||
TextInput::make('url')->label('Ссылка(Необязательно)'),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
])->collapsed()->required(),
|
||||
]),
|
||||
Forms\Components\Section::make('Ресурс')
|
||||
->description('Настройка контактных ресурсов')
|
||||
->collapsible()
|
||||
->schema([
|
||||
Tabs::make('Настройки ресурса')
|
||||
->persistTabInQueryString()
|
||||
->columnSpanFull()
|
||||
->tabs([
|
||||
Tabs\Tab::make('Основная информация')
|
||||
->icon('heroicon-o-information-circle')
|
||||
->schema([
|
||||
Forms\Components\Grid::make(2)
|
||||
->schema([
|
||||
TextInput::make('title')
|
||||
->label('Название ресурса')
|
||||
->placeholder('Введите название ресурса')
|
||||
->helperText('Это название будет отображаться в интерфейсе')
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->live(onBlur: true)
|
||||
->afterStateUpdated(function (string $operation, string|null $state, Forms\Set $set) {
|
||||
$set('slug', Str::slug($state));
|
||||
$set('seo.title', $state);
|
||||
})
|
||||
->columnSpan(1),
|
||||
|
||||
]),
|
||||
]),
|
||||
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 = 'Форма';
|
||||
protected static ?string $pluralLabel = 'Пользовательские формы';
|
||||
|
||||
protected static ?string $navigationGroup = 'Виджеты';
|
||||
|
||||
|
||||
|
||||
protected static ?string $model = CustomForm::class;
|
||||
|
||||
|
||||
@@ -2,476 +2,155 @@
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Enums\CustomFormStatus;
|
||||
use App\Enums\PostStatus;
|
||||
use App\Filament\Components\Forms\ItemForm\Pages\ContentBuilderItem;
|
||||
use App\Filament\Resources\DepartmentResource\Pages;
|
||||
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\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\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
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 DepartmentResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Department::class;
|
||||
|
||||
protected static ?string $navigationGroup = 'Структура института';
|
||||
|
||||
protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack';
|
||||
|
||||
protected static ?string $navigationIcon = 'heroicon-o-building-office';
|
||||
protected static ?string $pluralLabel = 'Кафедры';
|
||||
|
||||
public static ?string $label = 'Кафедра';
|
||||
|
||||
|
||||
protected static ?string $modelLabel = 'кафедра';
|
||||
protected static ?string $navigationParentItem = 'Факультеты';
|
||||
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Section::make()
|
||||
->schema([
|
||||
Tabs::make('Tabs')
|
||||
->tabs([
|
||||
Tabs\Tab::make('Основная информация')
|
||||
Forms\Components\Tabs::make('Настройки кафедры')
|
||||
->persistTabInQueryString()
|
||||
->columnSpanFull()
|
||||
->tabs([
|
||||
Forms\Components\Tabs\Tab::make('Основные данные')
|
||||
->icon('heroicon-o-information-circle')
|
||||
->schema([
|
||||
Section::make('Идентификация')
|
||||
->description('Основная информация о кафедре')
|
||||
->schema([
|
||||
Forms\Components\Grid::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()
|
||||
TextInput::make('title')
|
||||
->label('Полное название')
|
||||
->required()
|
||||
->blockPickerColumns(3)
|
||||
->blockPickerWidth('2xl')
|
||||
->addActionLabel('Добавить новый блок'),
|
||||
->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 для страницы кафедры'),
|
||||
|
||||
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
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('id')->label('ID')->sortable(),
|
||||
TextColumn::make('title')->label('Название')->sortable()->searchable(),
|
||||
Tables\Columns\TextColumn::make('faculty.title')->label('Факультет')->words(2),
|
||||
TextColumn::make('created_at')->label('Дата создания')->sortable(),
|
||||
Tables\Columns\ToggleColumn::make('is_active')->label('Активно')->sortable(),
|
||||
TextColumn::make('title')
|
||||
->label('Название')
|
||||
->searchable()
|
||||
->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([
|
||||
//
|
||||
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([
|
||||
Tables\Actions\EditAction::make(),
|
||||
Tables\Actions\EditAction::make()
|
||||
->iconButton()
|
||||
->tooltip('Редактировать'),
|
||||
|
||||
Tables\Actions\ViewAction::make()
|
||||
->iconButton()
|
||||
->tooltip('Просмотреть'),
|
||||
])
|
||||
->bulkActions([
|
||||
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
|
||||
@@ -491,4 +170,4 @@ class DepartmentResource extends Resource
|
||||
'edit' => Pages\EditDepartment::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,41 +3,27 @@
|
||||
namespace App\Filament\Resources\DepartmentResource\Pages;
|
||||
|
||||
use App\Filament\Resources\DepartmentResource;
|
||||
use App\Services\Filament\Traits\SeoGenerate;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class CreateDepartment extends CreateRecord
|
||||
{
|
||||
use SeoGenerate;
|
||||
|
||||
protected static string $resource = DepartmentResource::class;
|
||||
|
||||
protected array $seoData;
|
||||
|
||||
protected function mutateFormDataBeforeCreate(array $data): array
|
||||
{
|
||||
$this->seoData = $this->generateSeo($data);
|
||||
$data['search_data'] = $this->generateSearchData($data['content']);
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function afterCreate(): void
|
||||
{
|
||||
$this->record->seo()->create($this->seoData);
|
||||
}
|
||||
|
||||
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),
|
||||
];
|
||||
$this->createSeo($this->record);
|
||||
}
|
||||
|
||||
private function generateSearchData(array $data) : string
|
||||
@@ -52,15 +38,6 @@ class CreateDepartment extends CreateRecord
|
||||
|
||||
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
|
||||
|
||||
@@ -3,20 +3,20 @@
|
||||
namespace App\Filament\Resources\DepartmentResource\Pages;
|
||||
|
||||
use App\Filament\Resources\DepartmentResource;
|
||||
use App\Services\Filament\Traits\SeoGenerate;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
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
|
||||
{
|
||||
$this->seoData = $this->generateSeo($data);
|
||||
$data['search_data'] = $this->generateSearchData($data['content']);
|
||||
|
||||
return $data;
|
||||
@@ -24,24 +24,7 @@ class EditDepartment extends EditRecord
|
||||
|
||||
protected function afterSave(): void
|
||||
{
|
||||
$this->record->seo()->update($this->seoData);
|
||||
}
|
||||
|
||||
|
||||
|
||||
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),
|
||||
];
|
||||
$this->updateSeo($this->record);
|
||||
}
|
||||
|
||||
private function getDataFromBlocks($block) : string
|
||||
|
||||
+61
-12
@@ -15,14 +15,22 @@ use Illuminate\Database\Eloquent\SoftDeletingScope;
|
||||
class ProgramsRelationManager extends RelationManager
|
||||
{
|
||||
protected static string $relationship = 'programs';
|
||||
protected static ?string $title = 'Образовательные программы кафедры';
|
||||
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('name')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
Forms\Components\Section::make('Основная информация')
|
||||
->description('Связь образовательной программы с кафедрой')
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('name')
|
||||
->label('Название программы')
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->placeholder('Например: Информатика и вычислительная техника')
|
||||
->helperText('Полное название образовательной программы'),
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -31,24 +39,65 @@ class ProgramsRelationManager extends RelationManager
|
||||
return $table
|
||||
->recordTitleAttribute('name')
|
||||
->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([
|
||||
//
|
||||
Tables\Filters\SelectFilter::make('status')
|
||||
->label('Статус программы')
|
||||
->options(EducationalProgramStatus::class)
|
||||
->default(EducationalProgramStatus::PUBLISHED->value),
|
||||
])
|
||||
->headerActions([
|
||||
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([
|
||||
Tables\Actions\EditAction::make(),
|
||||
Tables\Actions\DetachAction::make(),
|
||||
|
||||
Tables\Actions\DetachAction::make()
|
||||
->iconButton()
|
||||
->tooltip('Открепить программу')
|
||||
->modalHeading('Открепление программы')
|
||||
->modalSubmitActionLabel('Открепить')
|
||||
->modalDescription('Вы уверены, что хотите открепить эту программу от кафедры?'),
|
||||
])
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
Tables\Actions\DetachBulkAction::make(),
|
||||
Tables\Actions\DetachBulkAction::make()
|
||||
->label('Открепить выбранные')
|
||||
->modalHeading('Открепление программ')
|
||||
->modalSubmitActionLabel('Открепить')
|
||||
->modalDescription('Вы уверены, что хотите открепить выбранные программы от кафедры?'),
|
||||
]),
|
||||
]);
|
||||
])
|
||||
->emptyStateActions([
|
||||
AttachAction::make()
|
||||
->label('Добавить программу'),
|
||||
])
|
||||
->defaultSort('name')
|
||||
->deferLoading()
|
||||
->persistFiltersInSession();
|
||||
}
|
||||
}
|
||||
}
|
||||
+123
-20
@@ -14,21 +14,48 @@ use Illuminate\Database\Eloquent\SoftDeletingScope;
|
||||
class TeachersRelationManager extends RelationManager
|
||||
{
|
||||
protected static string $relationship = 'teachers';
|
||||
|
||||
protected static ?string $inverseRelationship = 'departments_teach';
|
||||
|
||||
|
||||
protected static ?string $title = 'Преподаватели кафедры';
|
||||
|
||||
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('teaching_position')->label('Преподавательская должность')->required(),
|
||||
Forms\Components\TextInput::make('service_email')->label('Служебная почта'),
|
||||
Forms\Components\TextInput::make('service_phone')->label('Служебный телефон'),
|
||||
Forms\Components\TextInput::make('cabinet')->label('Кабинет'),
|
||||
Forms\Components\Section::make('Информация о преподавателе')
|
||||
->description('Основные данные о работе преподавателя на кафедре')
|
||||
->schema([
|
||||
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
|
||||
->recordTitleAttribute('name')
|
||||
->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([
|
||||
//
|
||||
])
|
||||
->headerActions([
|
||||
AttachAction::make()
|
||||
->preloadRecordSelect()
|
||||
->recordSelectOptionsQuery(fn (Builder $query) => $query->has('userDetail'))
|
||||
->form(fn (AttachAction $action): array => [
|
||||
$action->getRecordSelect(),
|
||||
Forms\Components\TextInput::make('teaching_position')->label('Преподавательская должность')->required(),
|
||||
Forms\Components\TextInput::make('service_email')->label('Служебная почта'),
|
||||
Forms\Components\TextInput::make('service_phone')->label('Служебный телефон'),
|
||||
Forms\Components\TextInput::make('cabinet')->label('Кабинет'),
|
||||
Forms\Components\Section::make('')
|
||||
->schema([
|
||||
$action->getRecordSelect()
|
||||
->placeholder('Выбрать преподавателя')
|
||||
->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([
|
||||
Tables\Actions\EditAction::make(),
|
||||
Tables\Actions\DetachAction::make(),
|
||||
Tables\Actions\EditAction::make()
|
||||
->iconButton()
|
||||
->tooltip('Редактировать'),
|
||||
|
||||
Tables\Actions\DetachAction::make()
|
||||
->iconButton()
|
||||
->tooltip('Убрать с кафедры')
|
||||
->modalHeading('Удаление связи')
|
||||
->modalSubmitActionLabel('Убрать')
|
||||
->modalDescription('Вы уверены, что хотите убрать этого преподавателя с кафедры?'),
|
||||
])
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
Tables\Actions\DetachBulkAction::make(),
|
||||
Tables\Actions\DetachBulkAction::make()
|
||||
->label('Убрать выбранных')
|
||||
->modalHeading('Удаление связей')
|
||||
->modalSubmitActionLabel('Убрать')
|
||||
->modalDescription('Вы уверены, что хотите убрать выбранных преподавателей с кафедры?'),
|
||||
]),
|
||||
]);
|
||||
])
|
||||
->emptyStateActions([
|
||||
AttachAction::make()
|
||||
->label('Добавить преподавателя'),
|
||||
])
|
||||
->defaultSort('name')
|
||||
->deferLoading();
|
||||
}
|
||||
}
|
||||
}
|
||||
+123
-20
@@ -14,20 +14,48 @@ use Illuminate\Database\Eloquent\SoftDeletingScope;
|
||||
class WorkersRelationManager extends RelationManager
|
||||
{
|
||||
protected static string $relationship = 'workers';
|
||||
|
||||
protected static ?string $inverseRelationship = 'departments_work';
|
||||
|
||||
protected static ?string $title = 'Сотрудники кафедры';
|
||||
|
||||
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('position')->label('Должность')->required(),
|
||||
Forms\Components\TextInput::make('service_email')->label('Служебная почта'),
|
||||
Forms\Components\TextInput::make('service_phone')->label('Служебный телефон'),
|
||||
Forms\Components\TextInput::make('cabinet')->label('Кабинет'),
|
||||
Forms\Components\Section::make('Информация о должности')
|
||||
->description('Основные данные о работе сотрудника на кафедре')
|
||||
->schema([
|
||||
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
|
||||
->recordTitleAttribute('name')
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('name'),
|
||||
Tables\Columns\TextColumn::make('position'),
|
||||
Tables\Columns\TextColumn::make('name')
|
||||
->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([
|
||||
//
|
||||
])
|
||||
->headerActions([
|
||||
AttachAction::make()
|
||||
->preloadRecordSelect()
|
||||
->recordSelectOptionsQuery(fn (Builder $query) => $query->has('userDetail'))
|
||||
->form(fn (AttachAction $action): array => [
|
||||
$action->getRecordSelect(),
|
||||
Forms\Components\TextInput::make('position')->label('Должность')->required(),
|
||||
Forms\Components\TextInput::make('service_email')->label('Служебная почта'),
|
||||
Forms\Components\TextInput::make('service_phone')->label('Служебный телефон'),
|
||||
Forms\Components\TextInput::make('cabinet')->label('Кабинет'),
|
||||
Forms\Components\Section::make('')
|
||||
->schema([
|
||||
$action->getRecordSelect()
|
||||
->placeholder('Выбрать сотрудника')
|
||||
->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([
|
||||
Tables\Actions\EditAction::make(),
|
||||
Tables\Actions\DetachAction::make(),
|
||||
Tables\Actions\EditAction::make()
|
||||
->iconButton()
|
||||
->tooltip('Редактировать'),
|
||||
|
||||
Tables\Actions\DetachAction::make()
|
||||
->iconButton()
|
||||
->tooltip('Убрать с кафедры')
|
||||
->modalHeading('Удаление связи')
|
||||
->modalSubmitActionLabel('Убрать')
|
||||
->modalDescription('Вы уверены, что хотите убрать этого сотрудника с кафедры?'),
|
||||
])
|
||||
->bulkActions([
|
||||
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;
|
||||
|
||||
use App\Filament\Resources\DirectionAdditionalEducationResource\Pages;
|
||||
use App\Filament\Resources\DirectionAdditionalEducationResource\RelationManagers;
|
||||
use App\Models\DirectionAdditionalEducation;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Components\Grid;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\SoftDeletingScope;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class DirectionAdditionalEducationResource extends Resource
|
||||
@@ -25,25 +26,44 @@ class DirectionAdditionalEducationResource extends Resource
|
||||
public static ?string $label = 'Направление';
|
||||
protected static ?string $pluralLabel = 'Направления дополнительного образования';
|
||||
protected static ?string $navigationParentItem = 'Дополнительное Образование';
|
||||
|
||||
protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack';
|
||||
protected static ?string $navigationIcon = 'heroicon-o-arrow-trending-up';
|
||||
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Forms\Components\Section::make()->schema([
|
||||
Forms\Components\Grid::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(),
|
||||
Section::make('Основная информация')
|
||||
->description('Заполните данные о направлении дополнительного образования')
|
||||
->collapsible()
|
||||
->schema([
|
||||
Grid::make(2)
|
||||
->schema([
|
||||
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));
|
||||
})
|
||||
->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
|
||||
->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(),
|
||||
TextColumn::make('title')
|
||||
->label('Название')
|
||||
->searchable()
|
||||
->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([
|
||||
//
|
||||
Tables\Filters\TernaryFilter::make('is_active')
|
||||
->label('Только активные')
|
||||
->placeholder('Все')
|
||||
->trueLabel('Активные')
|
||||
->falseLabel('Неактивные'),
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\EditAction::make(),
|
||||
Tables\Actions\EditAction::make()
|
||||
->iconButton()
|
||||
->tooltip('Редактировать'),
|
||||
|
||||
Tables\Actions\ViewAction::make()
|
||||
->iconButton()
|
||||
->tooltip('Просмотреть'),
|
||||
])
|
||||
->bulkActions([
|
||||
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
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
return [];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
@@ -84,4 +142,4 @@ class DirectionAdditionalEducationResource extends Resource
|
||||
'edit' => Pages\EditDirectionAdditionalEducation::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,63 +4,132 @@ namespace App\Filament\Resources;
|
||||
|
||||
use App\Enums\LevelEducational;
|
||||
use App\Filament\Resources\DirectionStudyResource\Pages;
|
||||
use App\Filament\Resources\DirectionStudyResource\RelationManagers;
|
||||
use App\Models\DirectionStudy;
|
||||
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\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Columns\BadgeColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\SoftDeletingScope;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class DirectionStudyResource extends Resource
|
||||
{
|
||||
protected static ?string $model = DirectionStudy::class;
|
||||
|
||||
protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack';
|
||||
|
||||
protected static ?string $pluralLabel = 'Направление подготовки';
|
||||
protected static ?string $navigationIcon = 'heroicon-o-academic-cap';
|
||||
protected static ?string $pluralLabel = 'Направления подготовки';
|
||||
protected static ?string $modelLabel = 'Направление подготовки';
|
||||
|
||||
protected static ?string $navigationGroup = 'Образование';
|
||||
|
||||
|
||||
protected static ?string $navigationParentItem = 'Приемная-компания';
|
||||
|
||||
public static function form(Form $form): 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
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('name')->label('Название'),
|
||||
Tables\Columns\TextColumn::make('code')->label('Код направления'),
|
||||
Tables\Columns\TextColumn::make('lvl_edu')->label('Уровень образования')
|
||||
->formatStateUsing(fn ($state) => $state->getLabel())
|
||||
TextColumn::make('code')
|
||||
->label('Код')
|
||||
->searchable()
|
||||
->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([
|
||||
//
|
||||
Tables\Filters\SelectFilter::make('lvl_edu')
|
||||
->label('Уровень образования')
|
||||
->options(LevelEducational::class),
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\EditAction::make(),
|
||||
Tables\Actions\EditAction::make()
|
||||
->iconButton()
|
||||
->tooltip('Редактировать'),
|
||||
|
||||
Tables\Actions\ViewAction::make()
|
||||
->iconButton()
|
||||
->tooltip('Просмотреть'),
|
||||
])
|
||||
->bulkActions([
|
||||
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
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
return [];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
@@ -71,4 +140,4 @@ class DirectionStudyResource extends Resource
|
||||
'edit' => Pages\EditDirectionStudy::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,20 +2,15 @@
|
||||
|
||||
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\RelationManagers;
|
||||
use App\Models\Category;
|
||||
use App\Models\Division;
|
||||
use App\Models\Page;
|
||||
use App\Models\Post;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Forms\Components\Builder;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\Hidden;
|
||||
use Filament\Forms\Components\RichEditor;
|
||||
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
|
||||
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;
|
||||
@@ -24,410 +19,156 @@ use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\SoftDeletingScope;
|
||||
use Illuminate\Support\Str;
|
||||
use Mohamedsabil83\FilamentFormsTinyeditor\Components\TinyEditor;
|
||||
|
||||
class DivisionResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Division::class;
|
||||
|
||||
protected static ?string $navigationGroup = 'Структура института';
|
||||
|
||||
protected static ?string $navigationIcon = 'heroicon-o-squares-2x2';
|
||||
|
||||
public static ?string $label = 'Подразделение';
|
||||
|
||||
protected static ?string $pluralLabel = 'Подразделения института';
|
||||
protected static ?string $modelLabel = 'Подразделение';
|
||||
protected static ?string $pluralModelLabel = 'Подразделения института';
|
||||
protected static ?int $navigationSort = 100;
|
||||
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Section::make()
|
||||
Section::make('Основные настройки')
|
||||
->collapsible()
|
||||
->schema([
|
||||
Tabs::make('Tabs')
|
||||
Tabs::make('Конструктор подразделения')
|
||||
->persistTabInQueryString()
|
||||
->columnSpanFull()
|
||||
->tabs([
|
||||
Tabs\Tab::make('Основная информация')
|
||||
->icon('heroicon-o-information-circle')
|
||||
->schema([
|
||||
Forms\Components\Grid::make(2)->schema([
|
||||
TextInput::make('title')->label('Заголовок')->required()
|
||||
->live(onBlur: true)
|
||||
->afterStateUpdated(function (string $operation, string|null $state, Forms\Set $set) {
|
||||
$set('slug', Str::slug($state));
|
||||
}),
|
||||
TextInput::make('slug')->label('Slug')->unique(ignoreRecord: true)->readOnly()->required(),
|
||||
]),
|
||||
Toggle::make('is_active')->default(true)->label('Активное подразделение')->inline(false),
|
||||
Forms\Components\Grid::make(2)
|
||||
->schema([
|
||||
TextInput::make('title')
|
||||
->label('Название подразделения')
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->live(onBlur: true)
|
||||
->afterStateUpdated(function (string $operation, string|null $state, Forms\Set $set) {
|
||||
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([
|
||||
\Filament\Forms\Components\Builder::make('description')->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')->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('Добавить новый блок'),
|
||||
ContentBuilderItem::getItem('content')
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
|
||||
]);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->defaultSort('created_at', 'desc')
|
||||
->reorderable('order_column')
|
||||
->paginated([10, 25, 50, 100])
|
||||
->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(),
|
||||
])
|
||||
TextColumn::make('id')
|
||||
->label('ID')
|
||||
->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([
|
||||
//
|
||||
Tables\Filters\Filter::make('is_active')
|
||||
->label('Только активные')
|
||||
->query(fn (EloquentBuilder $query) => $query->where('is_active', true))
|
||||
->default(),
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\EditAction::make(),
|
||||
Tables\Actions\EditAction::make()
|
||||
->iconButton()
|
||||
->tooltip('Редактировать'),
|
||||
])
|
||||
->bulkActions([
|
||||
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
|
||||
@@ -445,4 +186,4 @@ class DivisionResource extends Resource
|
||||
'edit' => Pages\EditDivision::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,42 +3,27 @@
|
||||
namespace App\Filament\Resources\DivisionResource\Pages;
|
||||
|
||||
use App\Filament\Resources\DivisionResource;
|
||||
use App\Services\Filament\Traits\SeoGenerate;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
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
|
||||
{
|
||||
$this->seoData = $this->generateSeo($data);
|
||||
$data['search_data'] = $this->generateSearchData($data['description']);
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function afterCreate(): void
|
||||
{
|
||||
$this->record->seo()->create($this->seoData);
|
||||
}
|
||||
|
||||
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),
|
||||
];
|
||||
$this->createSeo($this->record);
|
||||
}
|
||||
|
||||
private function generateSearchData(array $data) : string
|
||||
|
||||
@@ -3,20 +3,21 @@
|
||||
namespace App\Filament\Resources\DivisionResource\Pages;
|
||||
|
||||
use App\Filament\Resources\DivisionResource;
|
||||
use App\Services\Filament\Traits\SeoGenerate;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class EditDivision extends EditRecord
|
||||
{
|
||||
use SeoGenerate;
|
||||
|
||||
protected static string $resource = DivisionResource::class;
|
||||
|
||||
protected array $seoData;
|
||||
|
||||
|
||||
protected function mutateFormDataBeforeSave(array $data): array
|
||||
{
|
||||
$this->seoData = $this->generateSeo($data);
|
||||
$data['search_data'] = $this->generateSearchData($data['description']);
|
||||
|
||||
return $data;
|
||||
@@ -24,25 +25,11 @@ class EditDivision extends EditRecord
|
||||
|
||||
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
|
||||
{
|
||||
|
||||
+133
-20
@@ -14,17 +14,48 @@ use Illuminate\Database\Eloquent\SoftDeletingScope;
|
||||
class WorkersRelationManager extends RelationManager
|
||||
{
|
||||
protected static string $relationship = 'workers';
|
||||
|
||||
protected static ?string $title = 'Сотрудники';
|
||||
protected static ?string $title = 'Сотрудники подразделения';
|
||||
protected static ?string $inverseRelationship = 'divisions';
|
||||
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('administrativePosition')->label('Должность')->required(),
|
||||
Forms\Components\TextInput::make('service_email')->label('Служебная почта'),
|
||||
Forms\Components\TextInput::make('service_phone')->label('Служебный телефон'),
|
||||
Forms\Components\TextInput::make('cabinet')->label('Кабинет'),
|
||||
Forms\Components\Section::make('Служебная информация')
|
||||
->description('Данные о сотруднике в рамках подразделения')
|
||||
->schema([
|
||||
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')
|
||||
->defaultSort('sort')
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('name'),
|
||||
Tables\Columns\TextColumn::make('administrativePosition'),
|
||||
Tables\Columns\TextColumn::make('sort')
|
||||
->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([
|
||||
//
|
||||
])
|
||||
->headerActions([
|
||||
AttachAction::make()
|
||||
->label('Добавить сотрудника')
|
||||
->modalHeading('Добавление сотрудника')
|
||||
->modalSubmitActionLabel('Добавить')
|
||||
->preloadRecordSelect()
|
||||
->recordSelectOptionsQuery(fn (Builder $query) => $query->has('userDetail'))
|
||||
->form(fn (AttachAction $action): array => [
|
||||
$action->getRecordSelect(),
|
||||
Forms\Components\TextInput::make('administrativePosition')->label('Должность')->required(),
|
||||
Forms\Components\TextInput::make('service_email')->label('Служебная почта'),
|
||||
Forms\Components\TextInput::make('service_phone')->label('Служебный телефон'),
|
||||
Forms\Components\TextInput::make('cabinet')->label('Кабинет'),
|
||||
])
|
||||
Forms\Components\Section::make()
|
||||
->schema([
|
||||
$action->getRecordSelect()
|
||||
->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([
|
||||
Tables\Actions\EditAction::make(),
|
||||
Tables\Actions\DetachAction::make(),
|
||||
Tables\Actions\EditAction::make()
|
||||
->iconButton()
|
||||
->tooltip('Редактировать данные сотрудника'),
|
||||
|
||||
Tables\Actions\DetachAction::make()
|
||||
->iconButton()
|
||||
->tooltip('Убрать из подразделения')
|
||||
->modalHeading('Подтверждение удаления')
|
||||
->modalSubmitActionLabel('Убрать')
|
||||
->modalDescription('Вы уверены, что хотите убрать этого сотрудника из подразделения?'),
|
||||
])
|
||||
->bulkActions([
|
||||
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\Filament\Resources\EducationalGroupResource\Pages;
|
||||
use App\Filament\Resources\EducationalGroupResource\RelationManagers;
|
||||
use App\Models\EducationalGroup;
|
||||
use App\Models\Faculty;
|
||||
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\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Columns\BadgeColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\SoftDeletingScope;
|
||||
|
||||
class EducationalGroupResource extends Resource
|
||||
{
|
||||
protected static ?string $navigationGroup = 'Расписание и группы';
|
||||
|
||||
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';
|
||||
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Forms\Components\Section::make()->schema([
|
||||
Forms\Components\Grid::make(2)->schema([
|
||||
Forms\Components\TextInput::make('title')->label('Название группы')->required(),
|
||||
Forms\Components\Select::make('faculty_id')->label('Факультет')->required()
|
||||
->options(Faculty::all()->pluck('title', 'id')),
|
||||
Forms\Components\Select::make('education_form_id')->label('Форма обучения')
|
||||
->options(FormEducation::class)
|
||||
Section::make('Основная информация')
|
||||
->description('Заполните основные данные о группе')
|
||||
->collapsible()
|
||||
->schema([
|
||||
Grid::make(2)
|
||||
->schema([
|
||||
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
|
||||
->columns([
|
||||
TextColumn::make('id')->label('ID')->sortable(),
|
||||
TextColumn::make('title')->label('Название')->sortable()->searchable(),
|
||||
TextColumn::make('created_at')->label('Дата создания')->sortable(),
|
||||
Tables\Columns\BadgeColumn::make('faculty.title')->label('Категория')->sortable(),
|
||||
TextColumn::make('id')
|
||||
->label('ID')
|
||||
->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([
|
||||
//
|
||||
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([
|
||||
Tables\Actions\EditAction::make(),
|
||||
Tables\Actions\EditAction::make()
|
||||
->iconButton()
|
||||
->tooltip('Редактировать'),
|
||||
|
||||
Tables\Actions\ViewAction::make()
|
||||
->iconButton()
|
||||
->tooltip('Просмотреть'),
|
||||
])
|
||||
->bulkActions([
|
||||
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
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
return [];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
@@ -79,4 +144,4 @@ class EducationalGroupResource extends Resource
|
||||
'edit' => Pages\EditEducationalGroup::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,263 +4,167 @@ namespace App\Filament\Resources;
|
||||
|
||||
use App\Enums\EducationalProgramStatus;
|
||||
use App\Enums\LevelEducational;
|
||||
use App\Filament\Components\Forms\ItemForm\Pages\ContentBuilderItem;
|
||||
use App\Filament\Resources\EducationalProgramResource\Pages;
|
||||
use App\Filament\Resources\EducationalProgramResource\RelationManagers;
|
||||
use App\Filament\Resources\EducationalProgramResource\RelationManagers\AdmissionPlansRelationManager;
|
||||
use App\Models\Category;
|
||||
use App\Models\EducationalProgram;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Components\Builder;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\Grid;
|
||||
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\TextInput;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Columns\BadgeColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\SoftDeletingScope;
|
||||
use Illuminate\Support\Str;
|
||||
use Mohamedsabil83\FilamentFormsTinyeditor\Components\TinyEditor;
|
||||
|
||||
class EducationalProgramResource extends Resource
|
||||
{
|
||||
protected static ?string $model = EducationalProgram::class;
|
||||
|
||||
protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack';
|
||||
|
||||
protected static ?string $navigationIcon = 'heroicon-o-academic-cap';
|
||||
protected static ?string $navigationGroup = 'Образование';
|
||||
|
||||
protected static ?string $pluralLabel = 'Образовательные программы';
|
||||
|
||||
protected static ?string $modelLabel = 'Образовательная программа';
|
||||
protected static ?string $navigationParentItem = 'Приемная-компания';
|
||||
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Section::make()
|
||||
->schema([
|
||||
TextInput::make('name')->label('Название')->required(),
|
||||
Section::make('О программе')->schema([
|
||||
Builder::make('about_program')->label('')->blocks([
|
||||
Builder\Block::make('heading')->label('Заголовок')
|
||||
Tabs::make('')
|
||||
->tabs([
|
||||
Tabs\Tab::make('Основная информация')
|
||||
->icon('heroicon-o-information-circle')
|
||||
->schema([
|
||||
TextInput::make('name')
|
||||
->label('Название программы')
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->placeholder('Введите полное название программы')
|
||||
->columnSpanFull()
|
||||
->helperText('Официальное название программы как в лицензии'),
|
||||
|
||||
Grid::make(2)
|
||||
->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) {
|
||||
}),
|
||||
Select::make('lvl_edu')
|
||||
->label('Уровень образования')
|
||||
->options(LevelEducational::class)
|
||||
->required()
|
||||
->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')
|
||||
->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('Добавить новый блок'),
|
||||
|
||||
]),
|
||||
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-document-text')
|
||||
->schema([
|
||||
self::getContentBuilder('about_program', 'О программе')
|
||||
->columnSpanFull(),
|
||||
]),
|
||||
|
||||
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
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('name')->label('Название программы')->sortable()->searchable(),
|
||||
Tables\Columns\TextColumn::make('directionStudy.lvl_edu')->label('Уровень образования')->limit(30),
|
||||
TextColumn::make('name')
|
||||
->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([
|
||||
//
|
||||
Tables\Filters\SelectFilter::make('lvl_edu')
|
||||
->label('Уровень образования')
|
||||
->options(LevelEducational::class),
|
||||
|
||||
Tables\Filters\SelectFilter::make('status')
|
||||
->label('Статус программы')
|
||||
->options(EducationalProgramStatus::class),
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\EditAction::make(),
|
||||
Tables\Actions\EditAction::make()
|
||||
->iconButton()
|
||||
->tooltip('Редактировать'),
|
||||
|
||||
Tables\Actions\ViewAction::make()
|
||||
->iconButton()
|
||||
->tooltip('Просмотреть'),
|
||||
])
|
||||
->bulkActions([
|
||||
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
|
||||
{
|
||||
return [
|
||||
AdmissionPlansRelationManager::class
|
||||
AdmissionPlansRelationManager::class,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -272,4 +176,4 @@ class EducationalProgramResource extends Resource
|
||||
'edit' => Pages\EditEducationalProgram::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
+147
-45
@@ -3,89 +3,191 @@
|
||||
namespace App\Filament\Resources\EducationalProgramResource\RelationManagers;
|
||||
|
||||
use App\Enums\BudgetEducation;
|
||||
use App\Enums\EducationalProgramStatus;
|
||||
use App\Enums\FormEducation;
|
||||
use App\Models\AdmissionCampaign;
|
||||
use App\Models\EducationalProgram;
|
||||
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\Form;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Resources\RelationManagers\RelationManager;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\SoftDeletingScope;
|
||||
|
||||
class AdmissionPlansRelationManager extends RelationManager
|
||||
{
|
||||
protected static string $relationship = 'admission_plans';
|
||||
protected static ?string $title = 'Планы приема';
|
||||
protected static ?string $modelLabel = 'план приема';
|
||||
protected static ?string $pluralModelLabel = 'планы приема';
|
||||
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Forms\Components\Select::make('admission_campaigns_id')
|
||||
->label('Приемная компания')
|
||||
Select::make('admission_campaigns_id')
|
||||
->label('Приемная кампания')
|
||||
->required()
|
||||
->options(AdmissionCampaign::all()->pluck('name', 'id')),
|
||||
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;
|
||||
}),
|
||||
->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 function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->recordTitleAttribute('name')
|
||||
->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([
|
||||
//
|
||||
])
|
||||
->headerActions([
|
||||
Tables\Actions\CreateAction::make(),
|
||||
Tables\Actions\CreateAction::make()
|
||||
->label('Добавить план')
|
||||
->modalHeading('Создание плана приема'),
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\EditAction::make(),
|
||||
Tables\Actions\DeleteAction::make(),
|
||||
Tables\Actions\EditAction::make()
|
||||
->iconButton()
|
||||
->tooltip('Редактировать'),
|
||||
|
||||
Tables\Actions\DeleteAction::make()
|
||||
->iconButton()
|
||||
->tooltip('Удалить'),
|
||||
])
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
Tables\Actions\DetachBulkAction::make(),
|
||||
Tables\Actions\DeleteBulkAction::make()
|
||||
->label('Удалить выбранные')
|
||||
->modalHeading('Удаление планов приема')
|
||||
->modalDescription('Вы уверены, что хотите удалить выбранные планы?'),
|
||||
]),
|
||||
]);
|
||||
])
|
||||
->emptyStateActions([
|
||||
Tables\Actions\CreateAction::make()
|
||||
->label('Добавить план приема'),
|
||||
])
|
||||
->defaultSort('admissionCampaign.name');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,191 +2,211 @@
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Components\Forms\ItemForm\Pages\ContentBuilderItem;
|
||||
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\EventCategory;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Forms\Components\DatePicker;
|
||||
use Filament\Forms\Components\DateTimePicker;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\Hidden;
|
||||
use Filament\Forms\Components\RichEditor;
|
||||
use Filament\Forms\Components\Grid;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\SpatieTagsInput;
|
||||
use Filament\Forms\Components\Tabs;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\TimePicker;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\SoftDeletingScope;
|
||||
use Illuminate\Support\Str;
|
||||
use Mohamedsabil83\FilamentFormsTinyeditor\Components\TinyEditor;
|
||||
|
||||
class EventResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Event::class;
|
||||
|
||||
protected static ?string $navigationGroup = 'Новости и мероприятия';
|
||||
|
||||
protected static ?string $navigationIcon = 'heroicon-o-calendar-days';
|
||||
|
||||
protected static ?string $pluralLabel = 'Мероприятия';
|
||||
protected static ?string $modelLabel = 'Мероприятие';
|
||||
protected static ?string $pluralModelLabel = 'Мероприятия';
|
||||
protected static ?string $navigationLabel = 'Мероприятия';
|
||||
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Section::make()
|
||||
->schema([
|
||||
Forms\Components\Grid::make(2)->schema([
|
||||
TextInput::make('title')->label('Заголовок')->required()
|
||||
->live(onBlur: true)
|
||||
->afterStateUpdated(function (string $operation, $state, Forms\Set $set) {
|
||||
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('Заголовок')
|
||||
Tabs::make('Мероприятие')
|
||||
->tabs([
|
||||
Tabs\Tab::make('Основное')
|
||||
->icon('heroicon-o-information-circle')
|
||||
->schema([
|
||||
Grid::make(2)
|
||||
->schema([
|
||||
TextInput::make('id')->hidden()->integer()->default(rand(2335235,324634264263426)),
|
||||
TextInput::make('content')
|
||||
->label('')
|
||||
TextInput::make('title')
|
||||
->label('Название мероприятия')
|
||||
->placeholder('Введите название мероприятия')
|
||||
->helperText('Отображается на сайте')
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->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')
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->autofocus(),
|
||||
Select::make('category_id')
|
||||
->label('Категория')
|
||||
->placeholder('Выберите категорию')
|
||||
->options(EventCategory::all()->pluck('title', 'id'))
|
||||
->preload()
|
||||
->helperText('Для систематизации мероприятий'),
|
||||
|
||||
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('Добавить новый блок'),
|
||||
SpatieTagsInput::make('tags')
|
||||
->label('Теги')
|
||||
->placeholder('Добавьте теги')
|
||||
->helperText('Для фильтрации и поиска'),
|
||||
|
||||
]),
|
||||
TextInput::make('address')->label('Адрес')->required(),
|
||||
|
||||
Forms\Components\Grid::make(2)->schema([
|
||||
Forms\Components\Grid::make(2)->schema([
|
||||
DatePicker::make('event_date_start')->label('Дата начала мероприятия')->required()->native(false)
|
||||
->minDate(now())
|
||||
->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')
|
||||
->label('Онлайн-формат')
|
||||
->helperText('Отметьте для онлайн-мероприятий')
|
||||
->default(false)
|
||||
->inline(false)
|
||||
->onColor('success')
|
||||
->offColor('gray'),
|
||||
]),
|
||||
|
||||
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([
|
||||
Select::make('category_id')
|
||||
->options(EventCategory::all()->pluck('title', 'id'))
|
||||
->preload()
|
||||
->label('Категория'),
|
||||
SpatieTagsInput::make('tags')->label('Тэги'),
|
||||
]),
|
||||
Grid::make(2)
|
||||
->schema([
|
||||
DatePicker::make('event_date_start')
|
||||
->label('Дата начала')
|
||||
->native(false)
|
||||
->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
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('id')->label('ID')->sortable(),
|
||||
TextColumn::make('title')->label('Название')->sortable()->searchable(),
|
||||
TextColumn::make('event_date_start')->label('Начало мероприятия')->sortable(),
|
||||
TextColumn::make('created_at')->label('Дата создания')->sortable(),
|
||||
TextColumn::make('id')
|
||||
->label('ID')
|
||||
->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([
|
||||
//
|
||||
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([
|
||||
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([
|
||||
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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,26 @@
|
||||
namespace App\Filament\Resources\EventResource\Pages;
|
||||
|
||||
use App\Filament\Resources\EventResource;
|
||||
use App\Services\Filament\Domain\Seo\SeoGeneratorService;
|
||||
use App\Services\Filament\Traits\SeoGenerate;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateEvent extends CreateRecord
|
||||
{
|
||||
use SeoGenerate;
|
||||
|
||||
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;
|
||||
|
||||
use App\Filament\Resources\EventResource;
|
||||
use App\Services\Filament\Traits\SeoGenerate;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditEvent extends EditRecord
|
||||
{
|
||||
use SeoGenerate;
|
||||
|
||||
protected static string $resource = EventResource::class;
|
||||
|
||||
protected function afterSave(): void
|
||||
{
|
||||
$this->updateSeo($this->record);
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
|
||||
-457
@@ -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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -4,11 +4,10 @@ namespace App\Filament\Resources;
|
||||
|
||||
use App\Enums\CustomFormStatus;
|
||||
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\RelationManagers;
|
||||
use App\Filament\Resources\FacultyResource\RelationManagers\DepartmentsRelationManager;
|
||||
use App\Filament\Resources\FacultyResource\RelationManagers\WorkersRelationManager;
|
||||
use App\Helpers\ByteConverter;
|
||||
use App\Models\Category;
|
||||
use App\Models\CustomForm;
|
||||
use App\Models\Faculty;
|
||||
@@ -16,446 +15,153 @@ use App\Models\Page;
|
||||
use App\Models\PageReferenceList;
|
||||
use App\Models\Post;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
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\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
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;
|
||||
use Mohamedsabil83\FilamentFormsTinyeditor\Components\TinyEditor;
|
||||
|
||||
class FacultyResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Faculty::class;
|
||||
|
||||
protected static ?string $navigationGroup = 'Структура института';
|
||||
|
||||
protected static ?string $navigationIcon = 'heroicon-o-building-office-2';
|
||||
|
||||
protected static ?string $pluralLabel = 'Факультеты';
|
||||
|
||||
public static ?string $label = 'Факультет';
|
||||
|
||||
|
||||
protected static ?string $modelLabel = 'факультет';
|
||||
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Section::make()
|
||||
->schema([
|
||||
Tabs::make('Tabs')
|
||||
Forms\Components\Tabs::make('Настройки факультета')
|
||||
->persistTabInQueryString()
|
||||
->columnSpanFull()
|
||||
->tabs([
|
||||
Tabs\Tab::make('Основная информация')
|
||||
Forms\Components\Tabs\Tab::make('Основные данные')
|
||||
->icon('heroicon-o-information-circle')
|
||||
->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(),
|
||||
TextInput::make('abbreviation')->label('Аббревиатура')->required(),
|
||||
Toggle::make('is_active')->default(true)->label('Активный факультет')->inline(false),
|
||||
Section::make('Идентификация')
|
||||
->description('Основная информация о факультете')
|
||||
->schema([
|
||||
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));
|
||||
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([
|
||||
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([
|
||||
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('Добавить новый блок'),
|
||||
ContentBuilderItem::getItem('content')
|
||||
]),
|
||||
]),
|
||||
])
|
||||
]);
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->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(),
|
||||
])
|
||||
TextColumn::make('title')
|
||||
->label('Название')
|
||||
->searchable()
|
||||
->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([
|
||||
//
|
||||
Tables\Filters\TernaryFilter::make('is_active')
|
||||
->label('Только активные')
|
||||
->placeholder('Все')
|
||||
->trueLabel('Активные')
|
||||
->falseLabel('Неактивные'),
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\EditAction::make(),
|
||||
Tables\Actions\EditAction::make()
|
||||
->iconButton()
|
||||
->tooltip('Редактировать'),
|
||||
|
||||
Tables\Actions\ViewAction::make()
|
||||
->iconButton()
|
||||
->tooltip('Просмотреть'),
|
||||
])
|
||||
->bulkActions([
|
||||
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
|
||||
@@ -474,4 +180,4 @@ class FacultyResource extends Resource
|
||||
'edit' => Pages\EditFaculty::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,42 +3,28 @@
|
||||
namespace App\Filament\Resources\FacultyResource\Pages;
|
||||
|
||||
use App\Filament\Resources\FacultyResource;
|
||||
use App\Services\Filament\Traits\SeoGenerate;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class CreateFaculty extends CreateRecord
|
||||
{
|
||||
use SeoGenerate;
|
||||
|
||||
protected static string $resource = FacultyResource::class;
|
||||
|
||||
protected array $seoData;
|
||||
|
||||
|
||||
protected function mutateFormDataBeforeCreate(array $data): array
|
||||
{
|
||||
$this->seoData = $this->generateSeo($data);
|
||||
$data['search_data'] = $this->generateSearchData($data['content']);
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function afterCreate(): void
|
||||
{
|
||||
$this->record->seo()->create($this->seoData);
|
||||
}
|
||||
|
||||
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),
|
||||
];
|
||||
$this->createSeo($this->record);
|
||||
}
|
||||
|
||||
private function generateSearchData(array $data) : string
|
||||
|
||||
@@ -3,20 +3,20 @@
|
||||
namespace App\Filament\Resources\FacultyResource\Pages;
|
||||
|
||||
use App\Filament\Resources\FacultyResource;
|
||||
use App\Services\Filament\Traits\SeoGenerate;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
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
|
||||
{
|
||||
$this->seoData = $this->generateSeo($data);
|
||||
$data['search_data'] = $this->generateSearchData($data['content']);
|
||||
|
||||
return $data;
|
||||
@@ -24,7 +24,7 @@ class EditFaculty extends EditRecord
|
||||
|
||||
protected function afterSave(): void
|
||||
{
|
||||
$this->record->seo()->update($this->seoData);
|
||||
$this->updateSeo($this->record);
|
||||
}
|
||||
|
||||
|
||||
|
||||
+2
-2
@@ -39,11 +39,11 @@ class DepartmentsRelationManager extends RelationManager
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\EditAction::make(),
|
||||
Tables\Actions\DetachAction::make(),
|
||||
Tables\Actions\DeleteAction::make(),
|
||||
])
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
Tables\Actions\DetachBulkAction::make(),
|
||||
Tables\Actions\DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
+118
-20
@@ -9,23 +9,47 @@ use Filament\Tables;
|
||||
use Filament\Tables\Actions\AttachAction;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\SoftDeletingScope;
|
||||
|
||||
class WorkersRelationManager extends RelationManager
|
||||
{
|
||||
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
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('position')->label('Должность')->required(),
|
||||
Forms\Components\TextInput::make('service_email')->label('Служебная почта'),
|
||||
Forms\Components\TextInput::make('service_phone')->label('Служебный телефон'),
|
||||
Forms\Components\TextInput::make('cabinet')->label('Кабинет'),
|
||||
Forms\Components\Grid::make(2)
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('position')
|
||||
->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')
|
||||
->defaultSort('sort')
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('name')->label('Имя'),
|
||||
Tables\Columns\TextColumn::make('position')->label('Должность'),
|
||||
Tables\Columns\TextColumn::make('name')
|
||||
->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([
|
||||
//
|
||||
@@ -46,21 +95,70 @@ class WorkersRelationManager extends RelationManager
|
||||
->headerActions([
|
||||
AttachAction::make()
|
||||
->form(fn (AttachAction $action): array => [
|
||||
$action->getRecordSelect()->preload(),
|
||||
Forms\Components\TextInput::make('position')->label('Должность')->required(),
|
||||
Forms\Components\TextInput::make('service_email')->label('Служебная почта'),
|
||||
Forms\Components\TextInput::make('service_phone')->label('Служебный телефон'),
|
||||
Forms\Components\TextInput::make('cabinet')->label('Кабинет'),
|
||||
$action->getRecordSelect()
|
||||
->label('Сотрудник')
|
||||
->searchable()
|
||||
->preload()
|
||||
->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([
|
||||
Tables\Actions\EditAction::make(),
|
||||
Tables\Actions\DetachAction::make(),
|
||||
Tables\Actions\EditAction::make()
|
||||
->iconButton()
|
||||
->tooltip('Редактировать'),
|
||||
|
||||
Tables\Actions\DetachAction::make()
|
||||
->iconButton()
|
||||
->tooltip('Открепить')
|
||||
->modalHeading('Открепить сотрудника')
|
||||
->modalDescription('Вы уверены, что хотите открепить этого сотрудника от факультета?')
|
||||
->modalSubmitActionLabel('Открепить'),
|
||||
])
|
||||
->bulkActions([
|
||||
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 $navigationGroup = 'Виджеты';
|
||||
|
||||
|
||||
|
||||
protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack';
|
||||
|
||||
@@ -39,109 +42,180 @@ class PageReferenceListResource extends Resource
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Forms\Components\Section::make('')->schema([
|
||||
Tabs::make('Tabs')
|
||||
->tabs([
|
||||
Tabs\Tab::make('Основная информация')
|
||||
->schema([
|
||||
Forms\Components\Grid::make(2)->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),
|
||||
])
|
||||
]),
|
||||
Tabs\Tab::make('Содержание ресурса')
|
||||
->schema([
|
||||
Repeater::make('content')->label('Ресурсы')->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('')
|
||||
Forms\Components\Section::make('Ресурс')
|
||||
->description('Управление контентом ресурса')
|
||||
->collapsible()
|
||||
->schema([
|
||||
Tabs::make('Настройки ресурса')
|
||||
->persistTabInQueryString()
|
||||
->columnSpanFull()
|
||||
->tabs([
|
||||
Tabs\Tab::make('Основная информация')
|
||||
->icon('heroicon-o-information-circle')
|
||||
->schema([
|
||||
Forms\Components\Grid::make(2)
|
||||
->schema([
|
||||
TextInput::make('title')
|
||||
->label('Название ресурса')
|
||||
->placeholder('Введите название ресурса')
|
||||
->helperText('Это название будет отображаться в административной панели')
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->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 [];
|
||||
};
|
||||
->afterStateUpdated(function (string $operation, string|null $state, Forms\Set $set) {
|
||||
$set('slug', Str::slug($state));
|
||||
$set('seo.title', $state);
|
||||
}),
|
||||
]),
|
||||
]),
|
||||
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\Models\SubSection;
|
||||
use App\Services\Filament\Domain\Seo\SeoGeneratorService;
|
||||
use App\Services\Filament\Traits\SeoGenerate;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
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
|
||||
{
|
||||
@@ -27,8 +29,6 @@ class CreatePage extends CreateRecord
|
||||
}
|
||||
unset($data['sub_section_id']);
|
||||
|
||||
$this->seoData = $this->generateSeo($data);
|
||||
|
||||
$data['search_data'] = $this->generateSearchData($data['content']);
|
||||
|
||||
return $data;
|
||||
@@ -36,23 +36,9 @@ class CreatePage extends CreateRecord
|
||||
|
||||
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;
|
||||
|
||||
@@ -3,20 +3,20 @@
|
||||
namespace App\Filament\Resources\PageResource\Pages;
|
||||
|
||||
use App\Filament\Resources\PageResource;
|
||||
use App\Services\Filament\Domain\Seo\SeoGeneratorService;
|
||||
use App\Services\Filament\Traits\SeoGenerate;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
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
|
||||
{
|
||||
$this->seoData = $this->generateSeo($data);
|
||||
|
||||
$data['search_data'] = $this->generateSearchData($data['content']);
|
||||
|
||||
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
|
||||
{
|
||||
$data = null;
|
||||
|
||||
@@ -38,12 +38,11 @@ class SectionRelationManager extends RelationManager
|
||||
//
|
||||
])
|
||||
->headerActions([
|
||||
Tables\Actions\CreateAction::make()->visible(!$this->ownerRecord->section->exists()),
|
||||
Tables\Actions\AssociateAction::make()
|
||||
// Tables\Actions\CreateAction::make()->visible(!$this->ownerRecord->section->exists()),
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\EditAction::make(),
|
||||
Tables\Actions\DetachAction::make(),
|
||||
Tables\Actions\DissociateAction::make(),
|
||||
])
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
|
||||
@@ -2,41 +2,14 @@
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Enums\PostStatus;
|
||||
use App\Filament\Components\Forms\PostForm;
|
||||
use App\Filament\Resources\PostResource\Pages;
|
||||
use App\Models\Category;
|
||||
use App\Models\Page;
|
||||
use App\Models\Post;
|
||||
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\Get;
|
||||
use Filament\Forms\Set;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
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
|
||||
{
|
||||
@@ -59,7 +32,6 @@ class PostResource extends Resource implements HasShieldPermissions
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
// Tables\Columns\TextColumn::make('id')->sortable(),
|
||||
Tables\Columns\TextColumn::make('created_at')->label('Дата создания')->sortable(),
|
||||
Tables\Columns\TextColumn::make('title')->label('Заголовок')->sortable()->searchable(),
|
||||
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\PostSliderService;
|
||||
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;
|
||||
|
||||
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 $slideData;
|
||||
@@ -45,7 +47,7 @@ class CreatePost extends CreateRecord
|
||||
protected function afterCreate(): void
|
||||
{
|
||||
$this->handleSlides();
|
||||
$this->generateSeo();
|
||||
$this->createSeo($this->record);
|
||||
$this->sendNotifications();
|
||||
$this->publishToVk();
|
||||
}
|
||||
@@ -63,15 +65,6 @@ class CreatePost extends CreateRecord
|
||||
(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
|
||||
{
|
||||
|
||||
@@ -11,23 +11,26 @@ use App\Services\Filament\Domain\Posts\PostNotificationService;
|
||||
use App\Services\Filament\Domain\Posts\PostSeoGenerator;
|
||||
use App\Services\Filament\Domain\Posts\PostSliderService;
|
||||
use App\Services\Filament\Domain\Posts\VkPostPublisher;
|
||||
use App\Services\Filament\Domain\Seo\SeoGeneratorService;
|
||||
use App\Services\Filament\Traits\SeoGenerate;
|
||||
use Carbon\Carbon;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditPost extends EditRecord
|
||||
{
|
||||
use SeoGenerate;
|
||||
|
||||
protected static string $resource = PostResource::class;
|
||||
|
||||
protected array $seoData;
|
||||
protected array $publicationAgreements;
|
||||
|
||||
protected array $slideData;
|
||||
|
||||
protected function mutateFormDataBeforeFill(array $data): array
|
||||
{
|
||||
$post = Post::query()->with(['seo', 'mainSlider'])->find($data['id']);
|
||||
$data['slide'] = $post->mainSlider->toArray() ?? null;
|
||||
$post = Post::query()->with(['seo', 'slide'])->find($data['id']);
|
||||
$data['slide'] = $post->slide->toArray() ?? null;
|
||||
return $data;
|
||||
}
|
||||
|
||||
@@ -52,7 +55,7 @@ class EditPost extends EditRecord
|
||||
protected function afterSave(): void
|
||||
{
|
||||
$this->handleSlides();
|
||||
$this->generateSeo();
|
||||
$this->updateSeo($this->record);
|
||||
$this->sendNotifications();
|
||||
$this->publishToVk();
|
||||
}
|
||||
@@ -73,7 +76,7 @@ class EditPost extends EditRecord
|
||||
|
||||
protected function generateSeo(): void
|
||||
{
|
||||
$seoData = (new PostSeoGenerator())->generate([
|
||||
$seoData = app(SeoGeneratorService::class)->generate([
|
||||
'title' => $this->record->title,
|
||||
'content' => $this->record->content,
|
||||
'preview' => $this->record->preview,
|
||||
|
||||
@@ -3,151 +3,104 @@
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
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\Schedule;
|
||||
use Closure;
|
||||
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\Grid;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\SpatieTagsInput;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\SoftDeletingScope;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Str;
|
||||
use Livewire\Component;
|
||||
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
|
||||
use Livewire\Livewire;
|
||||
|
||||
class ScheduleResource extends Resource
|
||||
{
|
||||
protected static ?string $navigationGroup = 'Расписание и группы';
|
||||
|
||||
protected static ?string $model = Schedule::class;
|
||||
|
||||
protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack';
|
||||
|
||||
protected static ?string $pluralLabel = 'Расписание';
|
||||
|
||||
|
||||
protected static array $weekDays = ['Понедельник', 'Вторник', 'Среда', 'Четверг', 'Пятница', 'Суббота', 'Воскресенье'];
|
||||
protected static array $typeWeek = ['Четная', 'Нечетная'];
|
||||
|
||||
|
||||
|
||||
|
||||
protected static ?string $navigationIcon = 'heroicon-o-calendar';
|
||||
protected static ?string $pluralLabel = 'Расписания';
|
||||
protected static ?string $modelLabel = 'расписание';
|
||||
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Section::make()
|
||||
Section::make('Основные настройки')
|
||||
->description('Основная информация о расписании')
|
||||
->collapsible()
|
||||
->schema([
|
||||
Forms\Components\Grid::make(2)->schema([
|
||||
Select::make('educational_group_id')->options(EducationalGroup::all()->pluck('title', 'id'))
|
||||
->live()
|
||||
->label('Выбрать группу')
|
||||
->required(),
|
||||
// TextInput::make('title')
|
||||
// ->live()
|
||||
// ->label('Заголовок')->required(),
|
||||
Grid::make(2)
|
||||
->schema([
|
||||
Select::make('educational_group_id')
|
||||
->label('Учебная группа')
|
||||
->options(EducationalGroup::query()->orderBy('title')->pluck('title', 'id'))
|
||||
->searchable()
|
||||
->preload()
|
||||
->required()
|
||||
->live()
|
||||
->helperText('Выберите группу для которой создается расписание'),
|
||||
|
||||
// Select::make('type')->options([
|
||||
// 'schedule' => 'Обычное расписание',
|
||||
// 'interval' => 'Временное расписание',
|
||||
// 'exam' => 'Промежуточная аттестация',
|
||||
// ])->label('Тип расписания')->required()->live(),
|
||||
Forms\Components\Toggle::make('is_zaoch')->label('Очная|Заочная')->inline(false),
|
||||
Toggle::make('is_zaoch')
|
||||
->label('Форма обучения')
|
||||
->inline(false)
|
||||
->onColor('success')
|
||||
->offColor('primary')
|
||||
->helperText('Очная | Заочная')
|
||||
->afterStateHydrated(function (Toggle $component, $state) {
|
||||
$component->state((bool) $state);
|
||||
}),
|
||||
]),
|
||||
]),
|
||||
|
||||
]),
|
||||
// Forms\Components\Repeater::make('days')->label('')->schema([
|
||||
// Forms\Components\Repeater::make('form')->label('')->schema([
|
||||
// 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()
|
||||
Section::make('Файлы расписания')
|
||||
->description('Загрузите файлы с расписанием')
|
||||
->collapsible()
|
||||
->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')
|
||||
->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',
|
||||
])
|
||||
->maxSize(512000)
|
||||
->disk('public')
|
||||
->directory('files')
|
||||
->downloadable()
|
||||
->afterStateUpdated(function ($set, $state) {
|
||||
$set('title', pathinfo($state?->getClientOriginalName(), PATHINFO_FILENAME));
|
||||
})
|
||||
->visibility('public')
|
||||
]),
|
||||
])
|
||||
FileUpload::make('path')
|
||||
->label('Файл PDF')
|
||||
->required()
|
||||
->acceptedFileTypes(['application/pdf'])
|
||||
->maxSize(5120) // 5MB
|
||||
->disk('public')
|
||||
->directory('schedules')
|
||||
->downloadable()
|
||||
->openable()
|
||||
->previewable(false)
|
||||
->helperText('Только PDF файлы, макс. размер 5MB')
|
||||
->getUploadedFileNameForStorageUsing(
|
||||
fn (TemporaryUploadedFile $file): string =>
|
||||
str(Str::slug(pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME) . '-' . Carbon::now()->timestamp) . '.' . $file->getClientOriginalExtension()
|
||||
)
|
||||
->afterStateUpdated(function ($set, $state) {
|
||||
$set('title', pathinfo($state?->getClientOriginalName(), PATHINFO_FILENAME));
|
||||
})
|
||||
->visibility('public')),
|
||||
])
|
||||
->itemLabel(fn (array $state): ?string => $state['title'] ?? 'Новый файл')
|
||||
->collapsible()
|
||||
->cloneable()
|
||||
->defaultItems(1),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -155,29 +108,69 @@ class ScheduleResource extends Resource
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('title'),
|
||||
Tables\Columns\TextColumn::make('type'),
|
||||
TextColumn::make('educational_group.title')
|
||||
->label('Учебная группа')
|
||||
->sortable()
|
||||
->searchable(),
|
||||
|
||||
TextColumn::make('file_count')
|
||||
->label('Файлов')
|
||||
->getStateUsing(fn ($record) => count($record->file ?? []))
|
||||
->badge(),
|
||||
|
||||
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([
|
||||
//
|
||||
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([
|
||||
Tables\Actions\EditAction::make(),
|
||||
Tables\Actions\EditAction::make()
|
||||
->iconButton()
|
||||
->tooltip('Редактировать'),
|
||||
|
||||
Tables\Actions\ViewAction::make()
|
||||
->iconButton()
|
||||
->tooltip('Просмотреть'),
|
||||
])
|
||||
->bulkActions([
|
||||
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
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
return [];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
@@ -188,4 +181,4 @@ class ScheduleResource extends Resource
|
||||
'edit' => Pages\EditSchedule::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -157,9 +157,7 @@ class RoleResource extends Resource implements HasShieldPermissions
|
||||
|
||||
public static function getNavigationGroup(): ?string
|
||||
{
|
||||
return Utils::isResourceNavigationGroupEnabled()
|
||||
? __('filament-shield::filament-shield.nav.group')
|
||||
: '';
|
||||
return 'Настройки приложения';
|
||||
}
|
||||
|
||||
public static function getNavigationLabel(): string
|
||||
|
||||
@@ -27,6 +27,11 @@ class SlideResource extends Resource
|
||||
|
||||
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
|
||||
{
|
||||
return $form
|
||||
|
||||
@@ -6,37 +6,61 @@ use App\Filament\Resources\SliderResource\Pages;
|
||||
use App\Filament\Resources\SliderResource\RelationManagers;
|
||||
use App\Models\Slider;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\SoftDeletingScope;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class SliderResource extends Resource
|
||||
{
|
||||
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
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Forms\Components\Section::make()->schema([
|
||||
Forms\Components\TextInput::make('title')
|
||||
->live(onBlur: true)
|
||||
->afterStateUpdated(function (string $operation, string|null $state, Forms\Set $set) {
|
||||
$set('slug', Str::slug($state));
|
||||
})
|
||||
->label('Заголовок слайдера')
|
||||
->required(),
|
||||
TextInput::make('slug')->label('Slug')->unique(ignoreRecord: true)->readOnly()->required(),
|
||||
Toggle::make('is_active')->default(true)->label('Активный слайдер')->inline(false),
|
||||
]),
|
||||
Section::make('Настройки слайдера')
|
||||
->description('Основные параметры отображения слайдера')
|
||||
->collapsible()
|
||||
->schema([
|
||||
TextInput::make('title')
|
||||
->label('Название слайдера')
|
||||
->placeholder('Например: Главный слайдер')
|
||||
->helperText('Это название будет использоваться в административной панели')
|
||||
->required()
|
||||
->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
|
||||
->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([
|
||||
//
|
||||
Tables\Filters\SelectFilter::make('is_active')
|
||||
->label('Статус активности')
|
||||
->options([
|
||||
true => 'Активные',
|
||||
false => 'Неактивные',
|
||||
]),
|
||||
])
|
||||
->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([
|
||||
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
|
||||
{
|
||||
return [
|
||||
//
|
||||
RelationManagers\SlidesRelationManager::class,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -74,4 +133,4 @@ class SliderResource extends Resource
|
||||
'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 $navigationGroup = 'Settings';
|
||||
protected static ?string $navigationGroup = 'Настройки приложения';
|
||||
|
||||
|
||||
protected static ?string $pluralLabel = 'Доп. Информация';
|
||||
|
||||
@@ -21,7 +21,7 @@ class UserResource extends Resource implements HasShieldPermissions
|
||||
{
|
||||
protected static ?string $model = User::class;
|
||||
|
||||
protected static ?string $navigationGroup = 'Settings';
|
||||
protected static ?string $navigationGroup = 'Настройки приложения';
|
||||
|
||||
protected static ?string $pluralLabel = 'Пользователи';
|
||||
|
||||
|
||||
@@ -2,37 +2,76 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Enums\CacheKeys;
|
||||
use App\Http\Resources\ClientAcademicJournalListResource;
|
||||
use App\Http\Resources\ClientVirtualExhibitionListResource;
|
||||
use App\Models\AcademicJournal;
|
||||
use App\Models\JournalIssue;
|
||||
use App\Models\VirtualExhibition;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Services\App\Breadcrumb\BreadcrumbService;
|
||||
use App\Services\App\Seo\SeoPageProvider;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class ClientAcademicJournalController extends Controller
|
||||
{
|
||||
public function __construct(readonly SeoPageProvider $seoPageProvider){}
|
||||
|
||||
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)
|
||||
{
|
||||
$journal = new ClientAcademicJournalListResource(AcademicJournal::query()->where('slug', '=', $slug)->firstOrFail());
|
||||
$journalIssues = JournalIssue::where('academic_journal_id', $journal->id)
|
||||
->groupBy('year_publication')->get();
|
||||
// Кешируем основной журнал
|
||||
[$journal, $seo] = Cache::remember(
|
||||
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) {
|
||||
$journals[] = [
|
||||
'year_publication' => $year,
|
||||
'journalIssues' => $journalGroup
|
||||
];
|
||||
}
|
||||
return Inertia::render('Client/AcademicJournals/Show', compact('journal', 'journals'));
|
||||
$groupedIssues = [];
|
||||
foreach ($journalIssues as $year => $journalGroup) {
|
||||
$groupedIssues[] = [
|
||||
'year_publication' => $year,
|
||||
'journalIssues' => $journalGroup
|
||||
];
|
||||
}
|
||||
|
||||
return $groupedIssues;
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
return Inertia::render('Client/AcademicJournals/Show', compact('journal', 'journals', 'seo'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Enums\CacheKeys;
|
||||
use App\Enums\FormEducation;
|
||||
use App\Http\Resources\AdditionalEducationCategoryPreviewResource;
|
||||
use App\Http\Resources\AdditionalEducationCategoryResource;
|
||||
@@ -14,128 +15,148 @@ use App\Models\AdditionalEducation;
|
||||
use App\Models\AdditionalEducationCategory;
|
||||
use App\Models\DirectionAdditionalEducation;
|
||||
use App\Models\Page;
|
||||
use App\Services\App\Breadcrumb\BreadcrumbService;
|
||||
use App\Services\App\Seo\SeoPageProvider;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class ClientAdditionalEducationController extends Controller
|
||||
{
|
||||
public function __construct(readonly SeoPageProvider $seoPageProvider){}
|
||||
|
||||
|
||||
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()
|
||||
->where('is_active', true)
|
||||
->whereHas('additionalEducationCategories', function ($q) {
|
||||
$q->whereHas('additionalEducations');
|
||||
})->get());
|
||||
|
||||
$additionalEducations = AdditionalEducationCategoryResource::collection(AdditionalEducationCategory::query()
|
||||
->WithActivePrograms()
|
||||
->where('is_active', '=', true)
|
||||
->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()
|
||||
// Основные данные (кешируются)
|
||||
$directionAdditionalEducations = Cache::remember(
|
||||
CacheKeys::ADDITIONAL_EDUCATIONAL_PROGRAMS_PREFIX->value . 'directions_' . $cacheKey,
|
||||
now()->addDay(),
|
||||
function () {
|
||||
return DirectionAdditionalEducationResource::collection(
|
||||
DirectionAdditionalEducation::query()
|
||||
->where('is_active', true)
|
||||
->whereHas('additionalEducationCategories', fn ($q) => $q->whereHas('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 = [];
|
||||
if (request()->input('category')) {
|
||||
foreach (request()->input('category') as $item) {
|
||||
$categoriesContent[$item] = new AdditionalEducationCategoryResource(AdditionalEducationCategory::where('slug', $item)->first());
|
||||
if ($request->category) {
|
||||
foreach ((array)$request->category as $item) {
|
||||
$categoriesContent[$item] = new AdditionalEducationCategoryResource(
|
||||
AdditionalEducationCategory::where('slug', $item)->first()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$forms_education = [];
|
||||
foreach (FormEducation::cases() as $case) {
|
||||
$forms_education[$case->name] = $case->getLabel();
|
||||
}
|
||||
$forms_education = array_reduce(
|
||||
FormEducation::cases(),
|
||||
fn ($acc, $case) => $acc + [$case->name => $case->getLabel()],
|
||||
[]
|
||||
);
|
||||
|
||||
$filters = [
|
||||
'direction_filter' => [
|
||||
'type' => 'direction',
|
||||
'value' => request()->input('direction'),
|
||||
'value' => $request->input('direction'),
|
||||
'param' => 'direction'
|
||||
],
|
||||
'form_education_filter' => [
|
||||
'type' => 'form',
|
||||
'value' => request()->input('form'),
|
||||
'value' => $request->input('form'),
|
||||
'param' => 'form'
|
||||
],
|
||||
'category_filter' => [
|
||||
'type' => 'category',
|
||||
'value' => request()->input('category'),
|
||||
'value' => $request->input('category'),
|
||||
'param' => 'category',
|
||||
'content' => $categoriesContent,
|
||||
],
|
||||
];
|
||||
|
||||
$routeUrl = route('client.additionalEducation.index');
|
||||
$path = ltrim(parse_url($routeUrl, PHP_URL_PATH), '/');
|
||||
$seo = $this->seoPageProvider->getSeoForCurrentPage();
|
||||
|
||||
$page = Page::where('path', '=', $path)->with('section.pages.section', 'section.mainSection')->first();
|
||||
|
||||
if (isset($page->section)) {
|
||||
$breadcrumbs = [
|
||||
'mainSection' => new ClientBreadcrumbSection($page->section->mainSection),
|
||||
'subSection' => new ClientBreadcrumbSubSection($page->section),
|
||||
'page' => new ClientBreadcrumbPage($page),
|
||||
];
|
||||
} else {
|
||||
$breadcrumbs = null;
|
||||
}
|
||||
|
||||
return Inertia::render('Client/Additional-educations/Index',
|
||||
compact(
|
||||
'directionAdditionalEducations',
|
||||
'additionalEducations',
|
||||
'filters',
|
||||
'forms_education',
|
||||
'categories',
|
||||
'breadcrumbs'
|
||||
));
|
||||
return Inertia::render('Client/Additional-educations/Index', compact(
|
||||
'directionAdditionalEducations',
|
||||
'additionalEducations',
|
||||
'filters',
|
||||
'forms_education',
|
||||
'categories',
|
||||
'seo'
|
||||
));
|
||||
}
|
||||
|
||||
public function show(string $slug)
|
||||
{
|
||||
$additionalEducation = new AdditionalEducationResource(AdditionalEducation::query()->with('category.direction')->where('slug', $slug)->first());
|
||||
$routeUrl = route('client.additionalEducation.index');
|
||||
$path = ltrim(parse_url($routeUrl, PHP_URL_PATH), '/');
|
||||
// Кешируем основную программу дополнительного образования
|
||||
[$additionalEducation, $seo] = Cache::remember(
|
||||
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)) {
|
||||
$breadcrumbs = [
|
||||
'mainSection' => new ClientBreadcrumbSection($page->section->mainSection),
|
||||
'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'));
|
||||
}
|
||||
}
|
||||
return Inertia::render('Client/Additional-educations/Show', compact(
|
||||
'additionalEducation',
|
||||
'seo'
|
||||
));
|
||||
}}
|
||||
|
||||
@@ -2,34 +2,93 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Enums\CacheKeys;
|
||||
use App\Http\Resources\ClientDepartmentPreviewResource;
|
||||
use App\Http\Resources\DepartmentResource;
|
||||
use App\Models\Department;
|
||||
use App\Models\Faculty;
|
||||
use App\Services\App\Breadcrumb\BreadcrumbService;
|
||||
use App\Services\App\Seo\SeoPageProvider;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class ClientDepartmentController extends Controller
|
||||
{
|
||||
public function __construct(readonly SeoPageProvider $seoPageProvider){}
|
||||
|
||||
public function show(string $facultySlug, string $departmentSlug)
|
||||
{
|
||||
$faculty = Faculty::query()->where('slug', $facultySlug)->first();
|
||||
$departments = ClientDepartmentPreviewResource::collection(
|
||||
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);
|
||||
// Ключ для кеширования
|
||||
$cacheKey = "{$facultySlug}_{$departmentSlug}";
|
||||
|
||||
$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\Models\Division;
|
||||
use App\Services\App\Breadcrumb\BreadcrumbService;
|
||||
use App\Services\App\Seo\SeoPageProvider;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class ClientDivisionController extends Controller
|
||||
{
|
||||
public function __construct(readonly SeoPageProvider $seoPageProvider){}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$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)
|
||||
{
|
||||
$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());
|
||||
$seo = $division->seo ?? null;
|
||||
$division = new DivisionResource($divisionModel = Division::with(['workers.userDetail', 'seo'])->where('is_active', true)->where('slug', $slug)->firstOrFail());
|
||||
|
||||
$seo = $this->seoPageProvider->getSeoForModel($divisionModel);
|
||||
|
||||
return Inertia::render('Client/Divisions/Show', compact('divisions', 'division', 'seo'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Enums\CacheKeys;
|
||||
use App\Http\Resources\ClientBreadcrumbPage;
|
||||
use App\Http\Resources\ClientBreadcrumbSection;
|
||||
use App\Http\Resources\ClientBreadcrumbSubSection;
|
||||
@@ -13,53 +14,102 @@ use App\Models\Event;
|
||||
use App\Models\EventCategory;
|
||||
use App\Models\Page;
|
||||
use App\Services\App\Breadcrumb\BreadcrumbService;
|
||||
use App\Services\App\Seo\SeoPageProvider;
|
||||
use Carbon\Carbon;
|
||||
use DateTime;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class ClientEventController extends Controller
|
||||
{
|
||||
public function __construct(private readonly BreadcrumbService $breadcrumbService){}
|
||||
|
||||
public function __construct(readonly SeoPageProvider $seoPageProvider){}
|
||||
|
||||
public function index(Request $request): \Inertia\Response
|
||||
{
|
||||
$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();
|
||||
$eventDates = $this->getEventDates($filters);
|
||||
|
||||
$events = $this->getEvents($currentDate);
|
||||
$categories = ClientEventCategoryResource::collection(EventCategory::has('events')->get());
|
||||
$seo = $this->seoPageProvider->getSeoForCurrentPage();
|
||||
|
||||
$breadcrumbs = $this->breadcrumbService->generateBreadcrumbs('client.event.index');
|
||||
|
||||
|
||||
return Inertia::render('Client/Events/Index', compact('eventDates', 'events', 'currentDate', 'filters', 'categories', 'breadcrumbs'));
|
||||
return Inertia::render('Client/Events/Index', compact(
|
||||
'eventDates',
|
||||
'events',
|
||||
'currentDate',
|
||||
'filters',
|
||||
'categories',
|
||||
'seo'
|
||||
));
|
||||
}
|
||||
|
||||
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', 'breadcrumbs', 'seo'));
|
||||
return Inertia::render('Client/Events/Show', compact(
|
||||
'event',
|
||||
'seo'
|
||||
));
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
$events = $this->getEventsArchive();
|
||||
$categories = ClientEventCategoryResource::collection(EventCategory::has('events')->get());
|
||||
$seo = $this->seoPageProvider->getSeoForCurrentPage();
|
||||
|
||||
$breadcrumbs = $this->breadcrumbService->generateBreadcrumbs('client.event.archive');
|
||||
|
||||
|
||||
return Inertia::render('Client/Events/Archive', compact('events', 'filters', 'categories', 'breadcrumbs'));
|
||||
return Inertia::render('Client/Events/Archive', compact(
|
||||
'events',
|
||||
'filters',
|
||||
'categories',
|
||||
'seo'
|
||||
));
|
||||
}
|
||||
|
||||
private function getCurrentDate(Request $request): array
|
||||
@@ -164,7 +214,12 @@ class ClientEventController extends Controller
|
||||
->orderBy('event_date_start')
|
||||
->get();
|
||||
|
||||
$mappingDates = $events->map(function ($event) {
|
||||
// Получаем массив без ключей
|
||||
|
||||
|
||||
|
||||
// Извлекаем уникальные даты из событий
|
||||
return $events->map(function ($event) {
|
||||
$date = new DateTime($event->event_date_start);
|
||||
return [
|
||||
'day' => $date->format('j'),
|
||||
@@ -182,12 +237,7 @@ class ClientEventController extends Controller
|
||||
];
|
||||
})
|
||||
->sortKeys() // Сортируем ключи по возрастанию
|
||||
->values(); // Получаем массив без ключей
|
||||
|
||||
|
||||
|
||||
// Извлекаем уникальные даты из событий
|
||||
return $mappingDates;
|
||||
->values();
|
||||
}
|
||||
|
||||
private function getFilters(): array
|
||||
|
||||
@@ -2,26 +2,75 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Enums\CacheKeys;
|
||||
use App\Http\Resources\FacultyResource;
|
||||
use App\Http\Resources\FullFacultyResource;
|
||||
use App\Models\Faculty;
|
||||
use App\Services\App\Breadcrumb\BreadcrumbService;
|
||||
use App\Services\App\Seo\SeoPageProvider;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Inertia\Inertia;
|
||||
|
||||
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());
|
||||
return Inertia::render('Client/Faculties/Index', compact('faculties'));
|
||||
$faculties = Cache::remember(
|
||||
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)
|
||||
{
|
||||
$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());
|
||||
$seo = $faculty->seo ?? null;
|
||||
// Кешируем список факультетов
|
||||
$faculties = Cache::remember(
|
||||
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'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,14 +3,9 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
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\ClientTagResource;
|
||||
use App\Http\Resources\MainSectionResource;
|
||||
use App\Http\Resources\PageResource;
|
||||
|
||||
use App\Http\Resources\PostResource;
|
||||
use App\Models\Category;
|
||||
use App\Models\MainSection;
|
||||
@@ -18,8 +13,8 @@ use App\Models\Page;
|
||||
use App\Models\Post;
|
||||
use App\Models\Tag;
|
||||
use App\Services\App\Breadcrumb\BreadcrumbService;
|
||||
use App\Services\App\Seo\SeoPageProvider;
|
||||
use Carbon\Carbon;
|
||||
use Doctrine\DBAL\Schema\Column;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
@@ -27,12 +22,12 @@ use Inertia\Inertia;
|
||||
|
||||
class ClientPostController extends Controller
|
||||
{
|
||||
public function __construct(private readonly BreadcrumbService $breadcrumbService){}
|
||||
public function __construct(readonly SeoPageProvider $seoPageProvider){}
|
||||
|
||||
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')
|
||||
->distinct()
|
||||
->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', 'breadcrumbs'));
|
||||
return Inertia::render('Client/Posts/Index', compact('filters', 'posts', 'categories', 'tags', 'seo'));
|
||||
}
|
||||
|
||||
public function show(Request $request, $slug)
|
||||
@@ -154,19 +147,17 @@ class ClientPostController extends Controller
|
||||
// Преобразуем пост в ресурс
|
||||
$postResource = new PostResource($post);
|
||||
|
||||
$breadcrumbs = $this->breadcrumbService->generateBreadcrumbs('client.post.index');
|
||||
|
||||
// SEO-данные
|
||||
$seo = $post->seo ?? null;
|
||||
$seo = $this->seoPageProvider->getSeoForModel($post);
|
||||
|
||||
// Возвращаем данные для кеширования
|
||||
return [
|
||||
'post' => $postResource,
|
||||
'breadcrumbs' => $breadcrumbs,
|
||||
'seo' => $seo,
|
||||
];
|
||||
});
|
||||
|
||||
|
||||
// Возвращаем ответ с использованием кешированных данных
|
||||
return Inertia::render('Client/Posts/Show', $data);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Enums\BudgetEducation;
|
||||
use App\Enums\CacheKeys;
|
||||
use App\Enums\FormEducation;
|
||||
use App\Enums\LevelEducational;
|
||||
use App\Http\Resources\CampaignDegreeResource;
|
||||
@@ -16,121 +17,150 @@ use App\Models\CampaignDegree;
|
||||
use App\Models\DirectionStudy;
|
||||
use App\Models\EducationalProgram;
|
||||
use App\Models\MainSection;
|
||||
use App\Services\App\Seo\SeoPageProvider;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class ClientProgramController extends Controller
|
||||
{
|
||||
public function __construct(readonly SeoPageProvider $seoPageProvider){}
|
||||
|
||||
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');
|
||||
$levelsEducational = $uniqueValues->mapWithKeys(function ($level) {
|
||||
return [$level->name => $level->getLabel()];
|
||||
});
|
||||
$data = Cache::remember($cacheKey, now()->addHours(1), function () use ($request) {
|
||||
$activeCampaign = AdmissionCampaign::query()->where('status', 1)->first();
|
||||
|
||||
$direction_studies = DirectionStudy::query()
|
||||
->withAdmissionCampaignByYear($activeCampaign->academic_year)
|
||||
->withActivePrograms()
|
||||
->get();
|
||||
$uniqueValues = EducationalProgram::distinct()->pluck('lvl_edu');
|
||||
$levelsEducational = $uniqueValues->mapWithKeys(function ($level) {
|
||||
return [$level->name => $level->getLabel()];
|
||||
});
|
||||
|
||||
$level = request()->input('level');
|
||||
$form = request()->input('form');
|
||||
$budget = request()->input('budget');
|
||||
|
||||
$naprs = DirectionStudyResource::collection(
|
||||
DirectionStudy::query()
|
||||
$direction_studies = DirectionStudy::query()
|
||||
->withAdmissionCampaignByYear($activeCampaign->academic_year)
|
||||
->withActivePrograms()
|
||||
->with('programs.admission_plans')
|
||||
->when($level, function ($query) use ($level) {
|
||||
$query->where('lvl_edu', LevelEducational::fromName($level)->value);
|
||||
})
|
||||
->when($form, function ($query) use ($form) {
|
||||
$this->applyFormFilter($query, $form);
|
||||
})
|
||||
->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()
|
||||
);
|
||||
->get();
|
||||
|
||||
$level = request()->input('level');
|
||||
$form = request()->input('form');
|
||||
$budget = request()->input('budget');
|
||||
|
||||
$naprs = DirectionStudyResource::collection(
|
||||
DirectionStudy::query()
|
||||
->withAdmissionCampaignByYear($activeCampaign->academic_year)
|
||||
->withActivePrograms()
|
||||
->with('programs.admission_plans')
|
||||
->when($level, function ($query) use ($level) {
|
||||
$query->where('lvl_edu', LevelEducational::fromName($level)->value);
|
||||
})
|
||||
->when($form, function ($query) use ($form) {
|
||||
$this->applyFormFilter($query, $form);
|
||||
})
|
||||
->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();
|
||||
$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(
|
||||
return compact(
|
||||
'naprs',
|
||||
'campaignName',
|
||||
'levelsEducational',
|
||||
'filters',
|
||||
'formsEdu',
|
||||
'budgetEdu',
|
||||
'direction_studies'
|
||||
));
|
||||
'direction_studies',
|
||||
'seo'
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
return Inertia::render('Client/Programs/Index', $data);
|
||||
}
|
||||
|
||||
public function show(string $slug)
|
||||
{
|
||||
$program = new EducationalProgramFullResource(EducationalProgram::query()->where('slug', $slug)->with(['admission_plans', 'directionStudy'])->firstOrFail());
|
||||
$formsEducational = BudgetEducation::cases();
|
||||
$formsEducational = collect($formsEducational);
|
||||
$formsEdu = $formsEducational->mapWithKeys(function ($formEducational) {
|
||||
return [$formEducational->value => $formEducational->getLabel()];
|
||||
$cacheKey = CacheKeys::EDUCATION_PROGRAM_PREFIX->value . md5($slug);
|
||||
|
||||
$data = Cache::remember($cacheKey, now()->addHours(1), function () use ($slug) {
|
||||
$program = new EducationalProgramFullResource(
|
||||
$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', compact('program', 'formsEdu', 'seo'));
|
||||
return Inertia::render('Client/Programs/Show', $data);
|
||||
}
|
||||
|
||||
private function getAdmissionCampaignName() : string
|
||||
private function getAdmissionCampaignName(): string
|
||||
{
|
||||
$campaign = AdmissionCampaign::query()->where('status', 1)->first();
|
||||
return $campaign->name;
|
||||
}
|
||||
$cacheKey = CacheKeys::EDUCATION_PROGRAM_PREFIX->value . 'active_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)
|
||||
{
|
||||
@@ -150,7 +180,6 @@ class ClientProgramController extends Controller
|
||||
{
|
||||
$budgetValue = Str::of(BudgetEducation::fromName($budget)->value)->toString();
|
||||
|
||||
|
||||
$query->whereHas('programs.admission_plans', function ($query) use ($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\Faculty;
|
||||
use App\Models\Schedule;
|
||||
use App\Services\App\Seo\SeoPageProvider;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class ClientScheduleController extends Controller
|
||||
{
|
||||
public function __construct(readonly SeoPageProvider $seoPageProvider){}
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
$educationalGroups = ClientEducationalGroupResource::collection(EducationalGroup::query()
|
||||
@@ -46,10 +49,6 @@ class ClientScheduleController extends Controller
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
$forms_education = [];
|
||||
foreach (FormEducation::cases() as $case) {
|
||||
$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)
|
||||
|
||||
@@ -2,22 +2,31 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Enums\CacheKeys;
|
||||
use App\Enums\PostStatus;
|
||||
use App\Http\Resources\AdditionalEducationSearchResource;
|
||||
use App\Http\Resources\PostThumbnailResource;
|
||||
use App\Models\AdditionalEducation;
|
||||
use App\Models\Post;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class ClientWidgetAdditionalEducationalProgramController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
return AdditionalEducationSearchResource::collection(
|
||||
AdditionalEducation::query()
|
||||
->where('is_active', true)
|
||||
->orderBy('title', 'desc')
|
||||
->get());
|
||||
return Cache::remember(
|
||||
CacheKeys::ADDITIONAL_EDUCATIONAL_PROGRAMS_PREFIX->value . 'search_list',
|
||||
now()->addDay(), // Кешируем на 1 день
|
||||
function () {
|
||||
return AdditionalEducationSearchResource::collection(
|
||||
AdditionalEducation::query()
|
||||
->where('is_active', true)
|
||||
->orderBy('title', 'desc')
|
||||
->get()
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,14 +2,26 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Enums\CacheKeys;
|
||||
use App\Http\Resources\ClientContactWidgetResource;
|
||||
use App\Http\Resources\ClientPageReferenceListResource;
|
||||
use App\Models\ContactWidget;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class ClientWidgetContactController extends Controller
|
||||
{
|
||||
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;
|
||||
|
||||
use App\Enums\CacheKeys;
|
||||
use App\Enums\EducationalProgramStatus;
|
||||
use App\Http\Resources\EducationalProgramSearchResource;
|
||||
use App\Models\EducationalProgram;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class ClientWidgetEducationalProgramController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
return EducationalProgramSearchResource::collection(
|
||||
EducationalProgram::query()
|
||||
->where('status', EducationalProgramStatus::PUBLISHED)
|
||||
->orderBy('name', 'desc')
|
||||
->get());
|
||||
return Cache::remember(
|
||||
CacheKeys::EDUCATION_PROGRAMS_PREFIX->value . 'search_list',
|
||||
now()->addDay(), // Кешируем на 1 день
|
||||
function () {
|
||||
return EducationalProgramSearchResource::collection(
|
||||
EducationalProgram::query()
|
||||
->where('status', EducationalProgramStatus::PUBLISHED)
|
||||
->orderBy('name', 'desc')
|
||||
->get()
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,26 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Enums\CacheKeys;
|
||||
use App\Http\Resources\ClientPageReferenceListResource;
|
||||
use App\Models\PageReferenceList;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class ClientWidgetPageReferenceListController extends Controller
|
||||
{
|
||||
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()
|
||||
->where('slug', $slug)
|
||||
->where('is_active', true)
|
||||
->with('slides')
|
||||
->with(['slides' => function($query) {
|
||||
$query->where('is_active', true);
|
||||
}])
|
||||
->first();
|
||||
|
||||
return $slider ?: null;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user