Rework admin panel and other

This commit is contained in:
F4ilji
2025-04-02 18:37:20 +05:00
parent 49072e50c7
commit 5cd6ff11b5
198 changed files with 9715 additions and 6664 deletions
+6
View File
@@ -9,12 +9,18 @@ server {
try_files $uri /index.php?$args; # Обработка запросов
}
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)(/.+)$; # Разделение пути
+64
View File
@@ -0,0 +1,64 @@
<?php
namespace App\Containers\Post\Models;
use App\Enums\PostStatus;
use App\Models\Category;
use App\Models\MainSlider;
use App\Models\Seo;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\MorphOne;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
use Illuminate\Support\Facades\Cache;
use Spatie\Tags\HasTags;
class Post extends Model
{
use HasFactory, HasTags;
protected $guarded = false;
protected static function booted()
{
static::saved(function ($post) {
Cache::forget('post_' . $post->id);
Cache::forget('posts_' . $post->category_id . '_*'); // Очистка кеша для всех постов в категории
});
static::deleted(function ($post) {
Cache::forget('post_' . $post->id);
Cache::forget('posts_' . $post->category_id . '_*'); // Очистка кеша для всех постов в категории
});
}
public function category() : BelongsTo
{
return $this->belongsTo(Category::class);
}
public function author() : BelongsTo
{
return $this->belongsTo(User::class, 'user_id');
}
public function seo()
{
return $this->morphOne(Seo::class, 'seoable');
}
public function mainSlider()
{
return $this->morphOne(MainSlider::class, 'slidable');
}
protected $casts = [
'content' => 'array',
'authors' => 'array',
'status' => PostStatus::class,
'images' => 'array'
];
}
+3
View File
@@ -17,6 +17,7 @@ class MainSliderDTO
public ?Carbon $start_time,
public ?Carbon $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,
];
}
}
+31
View File
@@ -0,0 +1,31 @@
<?php
namespace App\Enums;
use Filament\Support\Contracts\HasLabel;
use Filament\Support\Contracts\HasColor;
enum AdmissionCampaignStatus: int implements HasLabel, HasColor
{
case ACTIVE = 1;
case ARCHIVED = 2;
case HIDDEN = 3;
public function getLabel(): ?string
{
return match ($this) {
self::ACTIVE => 'Активная',
self::ARCHIVED => 'Архивная',
self::HIDDEN => 'Скрытая',
};
}
public function getColor(): string|array|null
{
return match ($this) {
self::ACTIVE => 'success',
self::ARCHIVED => 'gray',
self::HIDDEN => 'danger',
};
}
}
+57
View File
@@ -0,0 +1,57 @@
<?php
namespace App\Enums;
enum CacheKeys: string {
case USER_PREFIX = 'person_';
case POST_PREFIX = 'post_';
case RECENT_POSTS_PREFIX = 'recent_posts_';
case POSTS_PREFIX = 'posts_';
case EDUCATION_PROGRAM_PREFIX = 'education_program_';
case EDUCATION_PROGRAMS_PREFIX = 'education_programs_';
case ADDITIONAL_EDUCATIONAL_PROGRAM_PREFIX = 'additional_education_program_';
case ADDITIONAL_EDUCATIONAL_PROGRAMS_PREFIX = 'additional_education_programs_';
case ACADEMIC_JOURNAL_PREFIX = 'academic_journal_';
case ACADEMIC_JOURNALS_PREFIX = 'academic_journals_';
case CATEGORIES_PREFIX = 'categories_';
case CONTACT_WIDGET_PREFIX = 'contact_widget_';
case CONTACT_WIDGETS_PREFIX = 'contact_widgets_';
// Department
case DEPARTMENT_PREFIX = 'department_';
case DEPARTMENTS_PREFIX = 'departments_';
// Division
case DIVISION_PREFIX = 'division_';
case DIVISIONS_PREFIX = 'divisions_';
// Event
case EVENT_PREFIX = 'event_';
case EVENTS_PREFIX = 'events_';
// Faculty
case FACULTY_PREFIX = 'faculty_';
case FACULTIES_PREFIX = 'faculties_';
// Navigation
case NAVIGATION_PREFIX = 'navigation';
// Page
case PAGE_PREFIX = 'page_';
case PAGE_DATA_PREFIX = 'page_data_';
case PAGE_REFERENCE_LIST_PREFIX = 'page_reference_list_';
case PAGE_REFERENCE_LISTS_PREFIX = 'page_reference_lists_';
// Schedule
case SCHEDULE_PREFIX = 'schedule_';
case SCHEDULES_PREFIX = 'schedules_';
// Slider
case SLIDER_PREFIX = 'slider_';
// Tag
case TAG_IDS_PREFIX = 'tag_ids_';
case TAGS_PREFIX = 'tags_';
case TAG_CONTENT_PREFIX = 'tag_content_';
}
+151 -22
View File
@@ -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()
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 формы')->unique(ignoreRecord: true)->required(),
TextInput::make('form_id')
->label('Уникальный ID формы')
->helperText('Автоматически генерируется из названия')
->required()
->unique(ignoreRecord: true)
->maxLength(255),
]),
Forms\Components\Textarea::make('description')->label('Описание формы')->required(),
Select::make('status')->label('Статус формы')->required()
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')->schema([]),
])->required(),
])
->collapsed(),
]),
Builder\Block::make('answers')
->label('Ответы формы')
->schema([]),
]),
])
->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('Настройка')
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')
->label('Длинный текст текст')
->icon('heroicon-o-document-text')
->label('Длинный текст')
->schema([
TextInput::make('title_field')
->label('Заголовок поля')
->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 );
$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('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,40 +32,67 @@ 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('Текст'),
->label('Содержимое')
->placeholder('Введите текст...')
->hint('Поддерживается форматирование')
->helperText('Для заголовков используйте стили H3-H4')
->required()
->columnSpanFull(),
]),
// Файлы
Builder\Block::make('files')
->label('Файл(-ы)')
->icon('heroicon-o-paper-clip')
->label('Файлы для скачивания')
->schema([
Repeater::make('file')
->label('')
->hint('Максимум 10 файлов')
->schema([
Forms\Components\Repeater::make('file')->schema([
Hidden::make('expansion')->required(),
Hidden::make('size')->required(),
TextInput::make('title')
->label('Название файла')
->placeholder('Годовой отчет 2023.pdf')
->required()
->maxLength(255)
->autofocus(),
->maxLength(255),
FileUpload::make('path')
->label('Выберите файл')
->helperText('Допустимы: PDF, DOCX, XLSX, PPTX, ZIP')
->hint('Макс. размер 500KB')
->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',
@@ -74,157 +104,275 @@ class ContentBuilderItem
->disk('public')
->directory('files')
->downloadable()
->preserveFilenames()
->afterStateUpdated(function ($set, $state) {
$set('expansion', $state?->getClientOriginalExtension());
$set('size', ByteConverter::bytesToHuman($state?->getSize()));
})
->visibility('public')
]),
}),
])
->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([
Grid::make(2)
->schema([
Forms\Components\Grid::make(2)->schema([
TextInput::make('count')
->label('Количество запией')
->integer(),
->label('Количество')
->numeric()
->minValue(1)
->maxValue(20)
->default(5)
->required(),
Select::make('category')
->label('Категория')
->options(Category::all()->pluck('title', 'id'))
->searchable()
->placeholder('Все категории'),
]),
])->label('Список новостей'),
]),
// Отдельная новость
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('Добавить новый блок');
->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,46 +31,72 @@ class TabBuilderItem
{
public static function getItem()
{
return
Builder\Block::make('tabs')
->label('Вкладки')
return Builder::make('content')
->label('Содержимое вкладки')
->blocks([
Builder\Block::make('heading')
->label('Заголовок')
->icon('heroicon-o-hashtag')
->schema([
Forms\Components\Repeater::make('tab')->schema([
TextInput::make('title')
->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('id')
->hidden()
->integer()
->default(rand(2335235, 324634264263426)),
TextInput::make('content')
->label('')
->label('Текст заголовка')
->placeholder('Введите текст заголовка')
->helperText('Основной заголовок раздела')
->live(onBlur: true)
->afterStateUpdated(function (string $operation, string $state, Forms\Set $set, $get) {
}),
->required()
->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('')
->label('Текст')
->placeholder('Начните вводить текст...')
->profile('test')
->required(),
])->label('Текст'),
->required()
->helperText('Основное текстовое содержимое блока'),
]),
Builder\Block::make('files')
->label('Файл(-ы)')
->label('Файлы')
->icon('heroicon-o-paper-clip')
->schema([
Forms\Components\Repeater::make('file')
->label('Файлы')
->helperText('Загрузите один или несколько файлов')
->schema([
Forms\Components\Repeater::make('file')->schema([
Hidden::make('expansion')->required(),
Hidden::make('size')->required(),
TextInput::make('title')
->label('Название файла')
->placeholder('Введите название файла')
->required()
->maxLength(255)
->autofocus(),
->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())
str(Str::slug(pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME) . '-' . Carbon::now()->timestamp) . '.' . $file->getClientOriginalExtension())
)
->acceptedFileTypes([
'application/pdf',
@@ -86,89 +114,155 @@ class TabBuilderItem
$set('size', ByteConverter::bytesToHuman($state?->getSize()));
})
->visibility('public')
->preserveFilenames()
])
->itemLabel(fn (array $state): ?string => $state['title'] ?? null)
->collapsible()
->cloneable()
->grid(2),
]),
]),
Builder\Block::make('person')
->label('Персона')
->icon('heroicon-o-user')
->schema([
TextInput::make('name')
->label('Имя')
->label('Имя персоны')
->placeholder('Введите имя')
->required()
->maxLength(255),
->maxLength(255)
->helperText('Полное имя персоны'),
FileUpload::make('photo')
->label('Фотография')
->image()
->helperText('Рекомендуемый формат: WebP')
->optimize('webp')
->resize(50)
->disk('public')
->directory('images')
->imageEditor(),
Forms\Components\Repeater::make('info')->schema([
->imageEditor()
->required()
->downloadable()
->openable(),
Forms\Components\Repeater::make('info')
->label('Дополнительная информация')
->helperText('Добавьте характеристики персоны')
->schema([
TextInput::make('column')
->label('Название колонки')
->label('Название характеристики')
->placeholder('Например: Должность')
->required()
->maxLength(255),
Forms\Components\Textarea::make('content')
->label('Содержание')
->label('Значение')
->placeholder('Например: Главный инженер')
->required()
->maxLength(1000),
])->minItems(1)->label('Информация о персоне'),
->maxLength(1000)
->columnSpanFull(),
])
->minItems(1)
->grid(2)
->collapsible()
->cloneable()
->itemLabel(fn (array $state): ?string => $state['column'] ?? null),
]),
Builder\Block::make('stepper')
->label('Строитель этапов')
->label(тапы')
->icon('heroicon-o-list-bullet')
->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')
->label('Название процесса')
->placeholder('Например: Процесс оформления')
->required()
->maxLength(255)
->autofocus(),
->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',
@@ -184,55 +278,109 @@ class TabBuilderItem
])
->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([
Forms\Components\Grid::make(2)->schema([
TextInput::make('count')
->label('Количество запией')
->integer(),
->label('Количество записей')
->integer()
->minValue(1)
->maxValue(20)
->default(5)
->helperText('От 1 до 20 записей'),
Select::make('category')
->label('Категория')
->options(Category::all()->pluck('title', 'id'))
->searchable()
->helperText('Выберите категорию или оставьте пустым для всех'),
]),
])->label('Список новостей'),
]),
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(),
])->label('Новость'),
->required()
->helperText('Выберите опубликованную новость'),
]),
Builder\Block::make('pageItem')
->label('Конкретная страница')
->icon('heroicon-o-document')
->schema([
Select::make('page')
->options(Page::query()->where('title', '!=' , null)->where('is_visible', true)->pluck('title', 'id'))
->label('Страница')
->options(Page::query()->where('title', '!=', null)->where('is_visible', true)->pluck('title', 'id'))
->searchable()
->required(),
])->label('Страница'),
->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(),
])->label('Форма'),
->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(),
])->label('Ресурсы'),
->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()
->blockNumbers(false)
->collapsible()
->blockPickerColumns(3)
->blockPickerWidth('2xl')
->addActionLabel('Добавить новый блок'),
])->minItems(1),
]);
->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),
]),
->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')
->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('Список новостей'),
])
->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')
->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('Список новостей'),
->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('Добавить новый блок');
->addActionLabel('Добавить новый блок')
->cloneable()
->reorderableWithButtons();
}
+94 -33
View File
@@ -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()
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('Текстовый идентификатор страницы')->unique(ignoreRecord: true)->required()
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('Подраздел')
Select::make('sub_section_id')
->label('Родительский подраздел')
->relationship('section', 'title')
->preload()
->searchable()
->placeholder('Выберите подраздел')
->helperText('Выберите раздел, к которому принадлежит эта страница')
->createOptionForm([
Forms\Components\TextInput::make('title')->label('Название подраздела')->required()
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('Slug')->unique(ignoreRecord: true)->readOnly()->required(),
TextInput::make('slug')
->label('URL подраздела')
->unique(ignoreRecord: true)
->readOnly()
->required()
->maxLength(255)
->helperText('Автоматически генерируется из названия'),
]),
Select::make('code')->options([
'200' => 'Открытая страница',
'404' => 'Не найдено',
'500' => 'Ведутся технические работы',
])->label('Статус')->required()->default('200'),
Toggle::make('searchable')->default(true)->label('Индексируется поиском')->inline(false),
]),
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')
->label('Icon'),
TextInput::make('search_data')->hidden(),
->helperText('Выберите иконку для отображения в навигации')
->columns(6),
TextInput::make('search_data')
->hidden(),
]),
Forms\Components\Tabs\Tab::make('Контент')->schema([
Forms\Components\Tabs\Tab::make('Содержание')
->icon('heroicon-o-document-text')
->schema([
ContentBuilderItem::getItem('content')
->helperText('Создайте содержимое страницы используя конструктор')
]),
Forms\Components\Tabs\Tab::make('Настройки')->schema([
Section::make()->schema([
Forms\Components\Tabs\Tab::make('Дополнительные настройки')
->icon('heroicon-o-cog')
->schema([
Section::make('Отображение элементов')
->description('Управление видимостью элементов на странице')
->collapsible()
->schema([
Toggle::make('settings.hide_page_sub_section_links')
->label('Скрыть сайдбар смежных страниц')
->label('Скрыть боковую панель с ссылками на страницы раздела')
->helperText('Скрывает список страниц текущего раздела')
->columnSpan(1),
Toggle::make('settings.hide_page_navigate_links')
->label('Скрыть навигацию по страницу')
->label('Скрыть навигацию по странице')
->helperText('Скрывает навигацию по заголовкам')
->columnSpan(1),
Toggle::make('settings.hide_breadcrumbs')
->label('Скрыть хлебные крошки')
->helperText('Скрывает навигационную цепочку вверху страницы')
->columnSpan(1),
//
// Toggle::make('settings.full_width_page')
// ->label('Страница на всю ширину')
// ->columnSpan(1)->default(true),
])
->columns(2),
]),
]),
]),
])
]);
}
+193 -85
View File
@@ -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()
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('Slug')->unique(ignoreRecord: true)->readOnly()->required(),
TextInput::make('slug')
->label('URL-адрес')
->unique(ignoreRecord: true)
->readOnly()
->required()
->helperText('Этот URL будет использоваться для страницы новости')
->maxLength(255),
]),
Select::make('status')->options(PostStatus::class)
->label('Статус')->required()
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([
->label('Авторы')
->placeholder('Добавить автора')
->helperText('Укажите авторов новости')
->suggestions([
'Редакция',
'Администратор',
]),
Section::make('Отложенная публикация')
->description('Настройте автоматическую публикацию новости в указанное время')
->collapsible()
->schema([
Grid::make(2)
->schema([
Toggle::make('publish_setting.publish_after')
->label('Включить')
->label('Включить отложенную публикацию')
->inline(false)
->default(false)
->live(),
->live()
->helperText('Активируйте для публикации в указанное время'),
DateTimePicker::make('publish_setting.publish_at')
->label('Дата публикации')
->native()
->displayFormat('d/m/Y')
->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(Carbon::now()->subWeek())
->maxDate(Carbon::now()->addMonth()),
->minDate(now())
->maxDate(now()->addMonth()),
]),
]),
Section::make('Публикация в сервисах')->schema([
Forms\Components\Grid::make()->schema([
Toggle::make('publication.vk')->label('Публикация в VK')->default(true),
Toggle::make('publication.telegram')->label('Публикация в Telegram')->default(true),
]),
]),
]),
Tabs\Tab::make('Содержание новости')
Section::make('Публикация в соцсетях')
->description('Управление автоматической публикацией в социальных сетях')
->collapsible()
->schema([
ContentBuilderItem::getItem('content')->required(),
]),
Tabs\Tab::make('Изображения')
Grid::make()
->schema([
FileUpload::make('preview')->label('Превью новости')
Toggle::make('publication.vk')
->label('Опубликовать в VK')
->default(true)
->helperText('Новость будет автоматически опубликована в VK'),
Toggle::make('publication.telegram')
->label('Опубликовать в Telegram')
->default(true)
->helperText('Новость будет автоматически опубликована в Telegram'),
]),
]),
]),
Tabs\Tab::make('Содержание')
->icon('heroicon-o-document-text')
->schema([
ContentBuilderItem::getItem('content')
->required()
->helperText('Создайте содержимое новости используя конструктор'),
]),
Tabs\Tab::make('Медиа')
->icon('heroicon-o-photo')
->schema([
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('Заголовок слайда'),
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('Текст слайда'),
Forms\Components\Grid::make()->schema([
->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([
Section::make('Изображение слайда')
->schema([
FileUpload::make('slide.image.url')
->label('Изображение')
->label('Фоновое изображение')
->image()
->directory('sliders')
->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' => 'Сильное затемнение',
->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(),
]),
]);
}
}
+1 -1
View File
@@ -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
+121 -734
View File
@@ -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()
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('Slug')->unique(ignoreRecord: true)->readOnly()->required(),
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) {
}),
ContentBuilderItem::getItem('main_info')
->label('Описание журнала')
->helperText('Добавьте полное описание журнала, его историю и основные направления'),
]),
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('Форма'),
])
->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('Добавить редактора'),
]),
]),
Tabs\Tab::make('Информация для авторов')
Tabs\Tab::make('Редакционная коллегия')
->icon('heroicon-o-user-group')
->schema([
Builder::make('for_authors')->label('')->blocks([
Builder\Block::make('heading')->label('Заголовок')
Section::make('Главный редактор')
->description('Информация о главном редакторе журнала')
->collapsible()
->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')
Forms\Components\Repeater::make('chief_editor')->label('')
->schema([
TextInput::make('name')
->label('Имя')
->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')
->maxLength(100)
->placeholder('Иванов Иван Иванович'),
TextInput::make('academicTitle')
->label('Учёная степень')
->required()
->maxLength(255),
TextInput::make('content')
->maxLength(50)
->placeholder('д.т.н., профессор'),
TextInput::make('position')
->label('Должность')
->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')
->maxLength(100)
->placeholder('Главный научный сотрудник'),
TextInput::make('institution')
->label('Учреждение')
->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'
->placeholder('МГУ имени М.В. Ломоносова'),
])
->maxSize(512000)
->disk('public')
->directory('files')
->downloadable()
->visibility('public')
->maxItems(1)
->reorderable(false)
->helperText('Укажите данные главного редактора журнала'),
]),
]),
Builder\Block::make('person')
Section::make('Редакционная коллегия')
->description('Состав редакционной коллегии журнала')
->collapsible()
->schema([
Forms\Components\Repeater::make('editors')->label('')
->schema([
TextInput::make('name')
->label('Имя')
->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')
->maxLength(100)
->placeholder('Петров Петр Петрович'),
TextInput::make('academicTitle')
->label('Учёная степень')
->required()
->maxLength(255),
TextInput::make('content')
->maxLength(50)
->placeholder('к.ф.-м.н., доцент'),
TextInput::make('position')
->label('Должность')
->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')
->maxLength(100)
->placeholder('Доцент кафедры'),
TextInput::make('institution')
->label('Учреждение')
->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('Список новостей'),
->placeholder('СПбГУ'),
])
->collapsed()
->blockNumbers(false)
->collapsible()
->addActionLabel('Добавить новый блок'),
])->minItems(1),
->addActionLabel('Добавить редактора')
->reorderable(true)
->itemLabel(fn (array $state): ?string => $state['name'] ?? null)
->helperText('Добавьте членов редакционной коллегии журнала'),
]),
Builder\Block::make('images')
->schema([
FileUpload::make('url')
->label('Изображение(-я)')
->image()
->multiple()
->reorderable()
->maxFiles(5)
->disk('public')
->directory('images')
->imageEditor()
->required(),
TextInput::make('alt')
->label('Описание')
->placeholder('Необязяательно')
])->label('Слайдер изображений'),
Builder\Block::make('image')
->schema([
FileUpload::make('url')
->label('Изображение(-я)')
->image()
->multiple()
->reorderable()
->maxFiles(5)
->disk('public')
->directory('images')
->imageEditor()
->required(),
TextInput::make('alt')
->label('Описание')
->placeholder('Необязяательно')
])->label('Изображение'),
Builder\Block::make('video')
->schema([
TextInput::make('mime')->readOnly(),
TextInput::make('title')
->required()
->maxLength(255)
->autofocus(),
FileUpload::make('path')
->required()
->acceptedFileTypes([
'video/mp4',
'video/quicktime',
'video/x-msvideo',
'video/x-ms-wmv',
'video/avi',
'video/webm',
'video/ogg',
'video/3gpp',
'video/3gpp2',
'video/x-m4v',
])
->disk('public')
->directory('videos')
->afterStateUpdated(fn (callable $set, $state) => $set('mime', $state?->getMimeType())),
]),
Builder\Block::make('postsList')
->schema([
Forms\Components\Grid::make(2)->schema([
TextInput::make('count')
->label('Количество запией')
->integer(),
Select::make('category')
->options(Category::all()->pluck('title', 'id'))
]),
])->label('Список новостей'),
Builder\Block::make('postItem')
->schema([
Select::make('post')
->options(Post::query()->where('status', PostStatus::PUBLISHED)->pluck('title', 'id'))
->searchable()
->required(),
])->label('Новость'),
Builder\Block::make('pageItem')
->schema([
Select::make('page')
->options(Page::query()->where('title', '!=' , null)->where('is_visible', true)->pluck('title', 'id'))
->searchable()
->required(),
])->label('Страница'),
Builder\Block::make('customForm')
->schema([
Select::make('form')
->options(CustomForm::query()->where('status', CustomFormStatus::PUBLISHED)->pluck('title', 'form_id'))
->searchable()
->required(),
])->label('Форма'),
])
->collapsed()
->blockNumbers(false)
->collapsible()
->blockPickerColumns(3)
->blockPickerWidth('2xl')
->addActionLabel('Добавить новый блок'),
]),
])->columnSpanFull()
Tabs\Tab::make('Для авторов')
->icon('heroicon-o-pencil')
->schema([
ContentBuilderItem::getItem('for_authors')
->label('Информация для авторов')
->helperText('Разместите требования к статьям, правила оформления и сроки подачи'),
]),
])
]);
}
@@ -814,18 +179,39 @@ class AcademicJournalResource extends Resource
{
return $table
->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
{
@@ -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,26 +30,56 @@ 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()
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|null $state, Forms\Set $set) {
->afterStateUpdated(function (string $operation, ?string $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('Направление доп. образования')
})
->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()
->options(DirectionAdditionalEducation::where('is_active', true)->pluck('title', 'id'))
->searchable()
->placeholder('Выберите направление')
->helperText('К какому направлению относится категория'),
]),
Forms\Components\Toggle::make('is_active')->label('Активно')->columnSpanFull()->inline(false)->default(true),
Toggle::make('is_active')
->label('Активная категория')
->inline(false)
->default(true)
->helperText('Отображать ли категорию на сайте')
->columnSpanFull(),
]),
]);
}
@@ -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
@@ -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,399 +29,115 @@ 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')
Forms\Components\Tabs::make('Программа ДПО')
->persistTabInQueryString()
->columnSpanFull()
->tabs([
Tabs\Tab::make('Основная информация')
Forms\Components\Tabs\Tab::make('Основные данные')
->icon('heroicon-o-information-circle')
->schema([
Forms\Components\Grid::make('2')->schema([
TextInput::make('title')->label('Заголовок')->required()
Section::make('Общая информация')
->description('Основные сведения о программе')
->schema([
Grid::make(2)
->schema([
TextInput::make('title')
->label('Название программы')
->required()
->maxLength(255)
->placeholder('Например: "Цифровые технологии в управлении"')
->live(onBlur: true)
->afterStateUpdated(function (string $operation, string|null $state, Forms\Set $set) {
->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\Select::make('category_id')->required()->label('Категория')
->options(AdditionalEducationCategory::where('is_active', true)->pluck('title', 'id'))->preload()->searchable()
]),
Forms\Components\TextInput::make('target_group')->required()->columnSpanFull()->label('Целевая аудитория'),
Forms\Components\TextInput::make('qualification')->required()->columnSpanFull()->label('Присваиваемая квалификация'),
Forms\Components\Grid::make('2')->schema([
Forms\Components\TextInput::make('price')->required()->integer()->label('Стоимость'),
Forms\Components\TextInput::make('learning_time')->required()->integer()->label('Объем обучения'),
]),
Forms\Components\Select::make('form_education')->label('Форма обучения')->required()->options(FormEducation::class),
Forms\Components\Toggle::make('is_active')->label('Активно')->columnSpanFull()->inline(false)->default(true),
]),
Tabs\Tab::make('Контент')
->schema([
Builder::make('content')->label('')->blocks([
Builder\Block::make('heading')->label('Заголовок')
->schema([
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('Имя')
->helperText('Полное официальное название программы'),
TextInput::make('slug')
->label('URL-адрес')
->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),
->readonly()
->maxLength(255)
->unique(ignoreRecord: true)
->helperText('Человеко-понятный URL для страницы программы'),
]),
])->minItems(1),
]),
Builder\Block::make('stepper')
->schema([
TextInput::make('step_name')
->label('Название шага')
Select::make('category_id')
->label('Категория')
->options(AdditionalEducationCategory::where('is_active', true)->pluck('title', 'id'))
->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')
->preload()
->searchable()
->placeholder('Выберите категорию')
->helperText('К какой категории относится программа'),
TextInput::make('target_group')
->label('Целевая аудитория')
->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')
->placeholder('Например: "Руководители среднего звена"')
->columnSpanFull()
->helperText('Для кого предназначена эта программа'),
TextInput::make('qualification')
->label('Выдаваемый документ')
->required()
->maxLength(255)
->autofocus(),
FileUpload::make('path')
->placeholder('Например: "Удостоверение о повышении квалификации"')
->columnSpanFull()
->helperText('Какой документ получат слушатели'),
]),
Section::make('Параметры обучения')
->schema([
Grid::make(2)
->schema([
TextInput::make('price')
->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')
->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')
->numeric()
->minValue(0)
->placeholder('Укажите стоимость')
->helperText('Полная стоимость программы'),
TextInput::make('learning_time')
->label('Объем (часов)')
->required()
->maxLength(255)
->autofocus(),
FileUpload::make('path')
->numeric()
->minValue(1)
->placeholder('Укажите количество часов')
->helperText('Общий объем программы в академических часах'),
Select::make('form_education')
->label('Форма обучения')
->options(FormEducation::class)
->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())),
->native(false)
->placeholder('Выберите форму')
->helperText('Основная форма проведения занятий'),
Toggle::make('is_active')
->label('Активна для записи')
->inline(false)
->default(true)
->helperText('Отображать ли программу на сайте'),
]),
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('Добавить новый блок'),
]),
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
@@ -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
{
@@ -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,59 +2,137 @@
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('Определяет видимость и доступность кампании'),
]),
]),
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(),
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('Общее количество программ по набору'),
]),
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(),
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('Количество платных мест'),
]),
]),
])->columnSpanFull(),
])
->itemLabel(fn (array $state): ?string =>
LevelEducational::tryFrom($state['edu_name'] ?? '')?->getLabel() ?? 'Новый уровень')
->collapsible()
->cloneable()
->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,13 +214,13 @@ 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;
}
+116 -32
View File
@@ -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
+104 -22
View File
@@ -24,44 +24,126 @@ 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')
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('Название ресурса')->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('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),
})
->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('Содержание ресурса')
->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(),
]),
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;
+96 -417
View File
@@ -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')
Forms\Components\Tabs::make('Настройки кафедры')
->persistTabInQueryString()
->columnSpanFull()
->tabs([
Tabs\Tab::make('Основная информация')
Forms\Components\Tabs\Tab::make('Основные данные')
->icon('heroicon-o-information-circle')
->schema([
Forms\Components\Grid::make()->schema([
TextInput::make('title')->label('Название факультета')->required()
Section::make('Идентификация')
->description('Основная информация о кафедре')
->schema([
TextInput::make('title')
->label('Полное название')
->required()
->maxLength(255)
->placeholder('Например: Кафедра программной инженерии')
->live(onBlur: true)
->afterStateUpdated(function (string $operation, string|null $state, Forms\Set $set) {
->afterStateUpdated(function (string $operation, ?string $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')
->helperText('Официальное название кафедры'),
TextInput::make('slug')
->label('URL-идентификатор')
->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'))
->unique(ignoreRecord: true)
->helperText('Человеко-понятный URL для страницы кафедры'),
Select::make('faculty_id')
->label('Факультет')
->options(Faculty::query()->orderBy('title')->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()
->preload()
->required()
->blockPickerColumns(3)
->blockPickerWidth('2xl')
->addActionLabel('Добавить новый блок'),
->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
@@ -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
@@ -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\Section::make('Основная информация')
->description('Связь образовательной программы с кафедрой')
->schema([
Forms\Components\TextInput::make('name')
->label('Название программы')
->required()
->maxLength(255),
->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();
}
}
@@ -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();
}
}
@@ -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,24 +26,43 @@ 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()
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|null $state, Forms\Set $set) {
->afterStateUpdated(function (string $operation, ?string $state, Forms\Set $set) {
$set('slug', Str::slug($state));
$set('seo.title', $state);
}),
TextInput::make('slug')->label('Slug')->unique(ignoreRecord: true)->readOnly()->required(),
})
->helperText('Укажите понятное название направления'),
TextInput::make('slug')
->label('URL-идентификатор')
->required()
->maxLength(255)
->unique(ignoreRecord: true)
->helperText('Человеко-понятный URL для направления'),
]),
Forms\Components\Toggle::make('is_active')->label('Активно')->columnSpanFull()->inline(false)->default(true),
Toggle::make('is_active')
->label('Активное направление')
->inline(false)
->default(true)
->helperText('Отображать ли направление на сайте')
->columnSpanFull(),
]),
]);
}
@@ -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
@@ -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
+112 -371
View File
@@ -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()
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));
}),
TextInput::make('slug')->label('Slug')->unique(ignoreRecord: true)->readOnly()->required(),
]),
Toggle::make('is_active')->default(true)->label('Активное подразделение')->inline(false),
]),
Tabs\Tab::make('Содержание')
->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')
}
})
->placeholder('Введите полное название подразделения')
->helperText('Официальное название, которое будет отображаться на сайте'),
TextInput::make('slug')
->label('URL-адрес')
->unique(ignoreRecord: true)
->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()
->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 });'
);
})),
]),
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('Добавить новый блок'),
]),
]),
Toggle::make('is_active')
->label('Активно на сайте')
->default(true)
->inline(false)
->helperText('Отключите, чтобы временно скрыть подразделение'),
]),
Tabs\Tab::make('Контент')
->icon('heroicon-o-document-text')
->schema([
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
@@ -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
{
@@ -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,39 +4,62 @@ 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('Форма обучения')
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
@@ -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()
Tabs::make('')
->tabs([
Tabs\Tab::make('Основная информация')
->icon('heroicon-o-information-circle')
->schema([
TextInput::make('name')->label('Название')->required(),
Section::make('О программе')->schema([
Builder::make('about_program')->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')
TextInput::make('name')
->label('Название программы')
->required()
->maxLength(255)
->autofocus(),
->placeholder('Введите полное название программы')
->columnSpanFull()
->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')
Grid::make(2)
->schema([
TextInput::make('title')
Select::make('lvl_edu')
->label('Уровень образования')
->options(LevelEducational::class)
->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('Добавить новый блок'),
->native(false)
->helperText('Выберите уровень образовательной программы'),
]),
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')
->label('Статус программы')
->options(EducationalProgramStatus::class)
->label('Статус программы')->required(),
TextInput::make('lang_stud')->label('На каком языке ведется образование')->required(),
->required()
->native(false)
->helperText('Определяет видимость программы на сайте'),
TextInput::make('lang_stud')
->label('Язык обучения')
->required()
->placeholder('Например: русский, английский')
->helperText('Укажите основной язык преподавания')
->columnSpan(2),
]),
]),
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,
];
}
@@ -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');
}
}
+150 -130
View File
@@ -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()
Tabs::make('Мероприятие')
->tabs([
Tabs\Tab::make('Основное')
->icon('heroicon-o-information-circle')
->schema([
Forms\Components\Grid::make(2)->schema([
TextInput::make('title')->label('Заголовок')->required()
Grid::make(2)
->schema([
TextInput::make('title')
->label('Название мероприятия')
->placeholder('Введите название мероприятия')
->helperText('Отображается на сайте')
->required()
->maxLength(255)
->live(onBlur: true)
->afterStateUpdated(function (string $operation, $state, Forms\Set $set) {
if ($state !== null) {
$set('slug', Str::slug($state));
} else {
$set('slug', null);
}
$set('slug', $state ? Str::slug($state) : null);
}),
TextInput::make('slug')->label('Slug (Заполнится автоматически)')->unique(ignoreRecord: true)->readOnly()->required(),
]),
Section::make('Контент')->schema([
\Filament\Forms\Components\Builder::make('content')->label('')->blocks([
Builder\Block::make('heading')->label('Заголовок')
->schema([
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('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')
TextInput::make('slug')
->label('URL-адрес')
->unique(ignoreRecord: true)
->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('Добавить новый блок'),
]),
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()),
->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 });'
);
})),
]),
Toggle::make('is_online')->default(false)->label('Онлайн мероприятие')->inline(false)
]),
Forms\Components\Grid::make(2)->schema([
Select::make('category_id')
->label('Категория')
->placeholder('Выберите категорию')
->options(EventCategory::all()->pluck('title', 'id'))
->preload()
->label('Категория'),
SpatieTagsInput::make('tags')->label('Тэги'),
->helperText('Для систематизации мероприятий'),
SpatieTagsInput::make('tags')
->label('Теги')
->placeholder('Добавьте теги')
->helperText('Для фильтрации и поиска'),
Toggle::make('is_online')
->label('Онлайн-формат')
->helperText('Отметьте для онлайн-мероприятий')
->default(false)
->inline(false)
->onColor('success')
->offColor('gray'),
]),
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),
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('Добавить мероприятие'),
]);
}
@@ -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 [
@@ -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(),
]);
}
}
+99 -393
View File
@@ -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()
Section::make('Идентификация')
->description('Основная информация о факультете')
->schema([
TextInput::make('title')
->label('Полное название')
->required()
->maxLength(255)
->placeholder('Например: Факультет информационных технологий')
->live(onBlur: true)
->afterStateUpdated(function (string $operation, string|null $state, Forms\Set $set) {
->afterStateUpdated(function (string $operation, ?string $state, Forms\Set $set) {
$set('slug', Str::slug($state));
if ($operation !== 'edit') {
$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),
]),
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([
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')
->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('Отображать ли факультет на сайте'),
]),
]),
Builder\Block::make('person')
Forms\Components\Tabs\Tab::make('Контент')
->icon('heroicon-o-document-text')
->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')
ContentBuilderItem::getItem('content')
]),
]),
Builder\Block::make('person')
->schema([
TextInput::make('name')
->label('Имя')
->required()
->maxLength(255),
FileUpload::make('photo')
->label('Фотография')
->image()
->disk('public')
->directory('images')
->imageEditor(),
Forms\Components\Repeater::make('info')->schema([
Forms\Components\Grid::make(2)->schema([
TextInput::make('column')
->required()
->maxLength(255),
TextInput::make('content')
->required()
->maxLength(255),
]),
])->minItems(1),
]),
Builder\Block::make('stepper')
->schema([
TextInput::make('step_name')
->label('Название шага')
->required()
->maxLength(255),
Forms\Components\Repeater::make('steps')->schema([
TextInput::make('title')
->required()
->maxLength(255)->columnSpanFull(),
RichEditor::make('content')->required(),
])->minItems(1),
]),
Builder\Block::make('images')
->schema([
FileUpload::make('url')
->label('Изображение(-я)')
->image()
->multiple()
->reorderable()
->maxFiles(5)
->disk('public')
->directory('images')
->imageEditor()
->required(),
TextInput::make('alt')
->label('Описание')
->placeholder('Необязяательно')
])->label('Слайдер изображений'),
Builder\Block::make('image')
->schema([
FileUpload::make('url')
->label('Изображение(-я)')
->image()
->multiple()
->reorderable()
->maxFiles(5)
->disk('public')
->directory('images')
->imageEditor()
->required(),
TextInput::make('alt')
->label('Описание')
->placeholder('Необязяательно')
])->label('Изображение'),
Builder\Block::make('video')
->schema([
TextInput::make('mime')->readOnly(),
TextInput::make('title')
->required()
->maxLength(255)
->autofocus(),
FileUpload::make('path')
->required()
->acceptedFileTypes([
'video/mp4',
'video/quicktime',
'video/x-msvideo',
'video/x-ms-wmv',
'video/avi',
'video/webm',
'video/ogg',
'video/3gpp',
'video/3gpp2',
'video/x-m4v',
])
->disk('public')
->directory('videos')
->afterStateUpdated(fn (callable $set, $state) => $set('mime', $state?->getMimeType())),
]),
Builder\Block::make('postsList')
->schema([
Forms\Components\Grid::make(2)->schema([
TextInput::make('count')
->label('Количество запией')
->integer(),
Select::make('category')
->options(Category::all()->pluck('title', 'id'))
]),
])->label('Список новостей'),
])
->collapsed()
->blockNumbers(false)
->collapsible()
->addActionLabel('Добавить новый блок'),
])->minItems(1),
]),
Builder\Block::make('images')
->schema([
FileUpload::make('url')
->label('Изображение(-я)')
->image()
->multiple()
->reorderable()
->maxFiles(5)
->disk('public')
->directory('images')
->imageEditor()
->required(),
TextInput::make('alt')
->label('Описание')
->placeholder('Необязяательно')
])->label('Слайдер изображений'),
Builder\Block::make('image')
->schema([
FileUpload::make('url')
->label('Изображение(-я)')
->image()
->multiple()
->reorderable()
->maxFiles(5)
->disk('public')
->directory('images')
->imageEditor()
->required(),
TextInput::make('alt')
->label('Описание')
->placeholder('Необязяательно')
])->label('Изображение'),
Builder\Block::make('video')
->schema([
TextInput::make('mime')->readOnly(),
TextInput::make('title')
->required()
->maxLength(255)
->autofocus(),
FileUpload::make('path')
->required()
->acceptedFileTypes([
'video/mp4',
'video/quicktime',
'video/x-msvideo',
'video/x-ms-wmv',
'video/avi',
'video/webm',
'video/ogg',
'video/3gpp',
'video/3gpp2',
'video/x-m4v',
])
->disk('public')
->directory('videos')
->afterStateUpdated(fn (callable $set, $state) => $set('mime', $state?->getMimeType())),
]),
Builder\Block::make('postsList')
->schema([
Forms\Components\Grid::make(2)->schema([
TextInput::make('count')
->label('Количество запией')
->integer(),
Select::make('category')
->options(Category::all()->pluck('title', 'id'))
]),
])->label('Список новостей'),
Builder\Block::make('postItem')
->schema([
Select::make('post')
->options(Post::query()->where('status', PostStatus::PUBLISHED)->pluck('title', 'id'))
->searchable()
->required(),
])->label('Новость'),
Builder\Block::make('pageItem')
->schema([
Select::make('page')
->options(Page::query()->where('title', '!=' , null)->where('is_visible', true)->pluck('title', 'id'))
->searchable()
->required(),
])->label('Страница'),
Builder\Block::make('customForm')
->schema([
Select::make('form')
->options(CustomForm::query()->where('status', CustomFormStatus::PUBLISHED)->pluck('title', 'form_id'))
->searchable()
->required(),
])->label('Форма'),
Builder\Block::make('pageResourceList')
->schema([
Select::make('resource')
->options(PageReferenceList::query()->where('is_active', true)->pluck('title', 'slug'))
->searchable()
->required(),
])->label('Ресурсы'),
])
->collapsed()
->blockNumbers(false)
->collapsible()
->required()
->blockPickerColumns(3)
->blockPickerWidth('2xl')
->addActionLabel('Добавить новый блок'),
]),
]),
])
]);
}
public static function table(Table $table): Table
{
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
@@ -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);
}
@@ -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(),
]),
]);
}
@@ -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,108 +42,179 @@ class PageReferenceListResource extends Resource
{
return $form
->schema([
Forms\Components\Section::make('')->schema([
Tabs::make('Tabs')
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('Название ресурса')->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('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),
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([
Repeater::make('content')->label('Ресурсы')->schema([
Forms\Components\Section::make('Быстрая настройка ресурса')->schema([
Forms\Components\Grid::make()->schema([
Forms\Components\Select::make('model_select')
->name('')
->label('Выбор типа данных')
->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);
};
})->live(onBlur: true),
}
}),
Forms\Components\Select::make('model')
->label('Поиск данных')
->name('')
->live(onBlur: true)
->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::where('title', '!=', null)->pluck('title', 'id');
};
return Page::whereNotNull('title')->pluck('title', 'id');
}
if ($get('model_select') === 'Event') {
return Event::all()->pluck('title', 'id');
};
if ($get('model_select') === 'Custom') {
}
return [];
};
}),
})
->disabled(fn (Forms\Get $get) => empty($get('model_select')) || $get('model_select') === 'Custom'),
]),
]),
TextInput::make('title')->label('Заголовок ресурса')->required(),
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(),
Forms\Components\Grid::make(2)->schema([
Forms\Components\TextInput::make('link')
->label('Ссылка ресурса')
->required(),
Forms\Components\TextInput::make('link_text')
->default('Читать')
->label('Текст кнопки')
->required(),
]),
])->collapsed()->required(),
]),
->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([
-28
View File
@@ -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,
+106 -113
View File
@@ -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'))
Grid::make(2)
->schema([
Select::make('educational_group_id')
->label('Учебная группа')
->options(EducationalGroup::query()->orderBy('title')->pluck('title', 'id'))
->searchable()
->preload()
->required()
->live()
->label('Выбрать группу')
->required(),
// TextInput::make('title')
// ->live()
// ->label('Заголовок')->required(),
->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')
->label('')
->addActionLabel('Добавить файл расписания')
->schema([
Forms\Components\Repeater::make('file')->schema([
TextInput::make('title')
->label('Название файла')
->required()
->maxLength(255)
->autofocus(),
->placeholder('Например: "Расписание на весенний семестр 2024"')
->helperText('Укажите понятное название файла для идентификации'),
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())
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')
]),
->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
@@ -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
+5
View File
@@ -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
+75 -16
View File
@@ -6,36 +6,60 @@ 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')
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));
})
->label('Заголовок слайдера')
->required(),
TextInput::make('slug')->label('Slug')->unique(ignoreRecord: true)->readOnly()->required(),
Toggle::make('is_active')->default(true)->label('Активный слайдер')->inline(false),
}),
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,
];
}
@@ -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 = 'Доп. Информация';
+1 -1
View File
@@ -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());
// Кешируем основной журнал
[$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 = Cache::remember(
CacheKeys::ACADEMIC_JOURNAL_PREFIX->value . 'issues_' . $slug,
now()->addWeek(),
function () use ($journal) {
$journalIssues = JournalIssue::where('academic_journal_id', $journal->id)
->groupBy('year_publication')->get();
$journals = [];
->get()
->groupBy('year_publication');
$groupedIssues = [];
foreach ($journalIssues as $year => $journalGroup) {
$journals[] = [
$groupedIssues[] = [
'year_publication' => $year,
'journalIssues' => $journalGroup
];
}
return Inertia::render('Client/AcademicJournals/Show', compact('journal', 'journals'));
return $groupedIssues;
}
);
return Inertia::render('Client/AcademicJournals/Show', compact('journal', 'journals', 'seo'));
}
}
@@ -2,6 +2,7 @@
namespace App\Http\Controllers;
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(
// Основные данные (кешируются)
$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', 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);
->whereHas('additionalEducationCategories', fn ($q) => $q->whereHas('additionalEducations'))
->get()
);
}
})
->has('additionalEducations')
->get());
);
$categories = AdditionalEducationCategoryPreviewResource::collection(
$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(
return Inertia::render('Client/Additional-educations/Index', compact(
'directionAdditionalEducations',
'additionalEducations',
'filters',
'forms_education',
'categories',
'breadcrumbs'
'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), '/');
$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),
// Кешируем основную программу дополнительного образования
[$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
];
} else {
$breadcrumbs = null;
}
);
$seo = $additionalEducation->seo ?? null;
// SEO-данные берём из кешированного ресурса
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(
// Ключ для кеширования
$cacheKey = "{$facultySlug}_{$departmentSlug}";
// Кешируем факультет
$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 = new DepartmentResource(Department::query()
}
);
// Кешируем полные данные кафедры с отношениями
[$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'])
->first());
$directions = $this->groupProgramsByDirection($department->programs);
->with([
'faculty',
'workers.userDetail',
'teachers.userDetail',
'programs.directionStudy',
'seo'
])
->first();
$seo = $department->seo ?? null;
return Inertia::render('Client/Departments/Show', compact('department', 'departments', 'directions', 'seo'));
$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'));
}
}
+77 -27
View File
@@ -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'));
}
}
+8 -17
View File
@@ -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,15 +17,22 @@ 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)
{
$cacheKey = CacheKeys::EDUCATION_PROGRAMS_PREFIX->value . md5(serialize($request->all()));
$data = Cache::remember($cacheKey, now()->addHours(1), function () use ($request) {
$activeCampaign = AdmissionCampaign::query()->where('status', 1)->first();
$uniqueValues = EducationalProgram::distinct()->pluck('lvl_edu');
@@ -64,7 +72,6 @@ class ClientProgramController extends Controller
->get()
);
$campaignName = $this->getAdmissionCampaignName();
$formsEducational = FormEducation::cases();
$formsEducational = collect($formsEducational);
@@ -100,38 +107,61 @@ class ClientProgramController extends Controller
],
];
return Inertia::render('Client/Programs/Index',
compact(
$seo = $this->seoPageProvider->getSeoForCurrentPage();
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());
$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 = $program->seo ?? null;
return Inertia::render('Client/Programs/Show', compact('program', 'formsEdu', 'seo'));
$seo = $this->seoPageProvider->getSeoForModel($programModel);
return compact('program', 'formsEdu', 'seo');
});
return Inertia::render('Client/Programs/Show', $data);
}
private function getAdmissionCampaignName() : string
private function getAdmissionCampaignName(): string
{
$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)
{
$formValue = Str::of(FormEducation::fromName($form)->value)->toString();
@@ -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 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());
->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 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());
->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