From 5cd6ff11b56294ea53afaa7d7441a6cb8a720dd8 Mon Sep 17 00:00:00 2001 From: F4ilji Date: Wed, 2 Apr 2025 18:33:41 +0500 Subject: [PATCH] Rework admin panel and other --- _docker/nginx/conf.d/nginx.conf | 6 + app/Containers/Post/Models/Post.php | 64 ++ app/Dto/MainSliderDTO.php | 3 + app/Enums/AdmissionCampaignStatus.php | 31 + app/Enums/CacheKeys.php | 57 ++ .../Components/Forms/CustomFormForm.php | 183 +++- .../Forms/ItemForm/Blocks/BlockSchema.php | 8 + .../Forms/ItemForm/Blocks/ContactBlock.php | 31 + .../Forms/ItemForm/Blocks/CustomFormBlock.php | 28 + .../Forms/ItemForm/Blocks/FilesBlock.php | 66 ++ .../Forms/ItemForm/Blocks/HeadingBlock.php | 27 + .../Forms/ItemForm/Blocks/ImageBlock.php | 38 + .../Forms/ItemForm/Blocks/ImagesBlock.php | 38 + .../Forms/ItemForm/Blocks/PageItemBlock.php | 31 + .../ItemForm/Blocks/PageResourceListBlock.php | 31 + .../Forms/ItemForm/Blocks/ParagraphBlock.php | 51 + .../Forms/ItemForm/Blocks/PersonBlock.php | 63 ++ .../Forms/ItemForm/Blocks/PostItemBlock.php | 23 + .../Forms/ItemForm/Blocks/PostListBlock.php | 41 + .../Forms/ItemForm/Blocks/SliderBlock.php | 31 + .../Forms/ItemForm/Blocks/StepperBlock.php | 55 ++ .../Forms/ItemForm/Blocks/TabBlock.php | 128 +++ .../Forms/ItemForm/Blocks/VideoBlock.php | 53 + .../ItemForm/CustomForm/FormBuilderItem.php | 411 +++++--- .../ItemForm/Defaults/ContentBuilderItem.php | 414 +++++--- .../ItemForm/Defaults/TabBuilderItem.php | 552 +++++++---- .../ItemForm/Pages/ContentBuilderItem.php | 471 ++------- app/Filament/Components/Forms/PageForm.php | 183 ++-- app/Filament/Components/Forms/PostForm.php | 312 ++++-- app/Filament/Pages/Backups.php | 2 +- app/Filament/Pages/CheckpointSettingsPage.php | 3 +- .../Resources/AcademicJournalResource.php | 905 +++--------------- .../Pages/CreateAcademicJournal.php | 23 +- .../Pages/EditAcademicJournal.php | 33 +- .../JournalsRelationManager.php | 120 ++- .../AdditionalEducationCategoryResource.php | 142 ++- .../Resources/AdditionalEducationResource.php | 594 ++++-------- .../Pages/CreateAdditionalEducation.php | 31 +- .../Pages/EditAdditionalEducation.php | 24 +- .../Resources/AdmissionCampaignResource.php | 205 +++- .../Pages/CreateAdmissionCampaign.php | 1 + .../Resources/AdmissionPlanResource.php | 148 ++- .../Resources/ContactWidgetResource.php | 148 ++- app/Filament/Resources/CustomFormResource.php | 3 + app/Filament/Resources/DepartmentResource.php | 529 ++-------- .../Pages/CreateDepartment.php | 31 +- .../Pages/EditDepartment.php | 25 +- .../ProgramsRelationManager.php | 73 +- .../TeachersRelationManager.php | 143 ++- .../WorkersRelationManager.php | 143 ++- .../DirectionAdditionalEducationResource.php | 114 ++- .../Resources/DirectionStudyResource.php | 113 ++- app/Filament/Resources/DivisionResource.php | 497 +++------- .../DivisionResource/Pages/CreateDivision.php | 23 +- .../DivisionResource/Pages/EditDivision.php | 21 +- .../WorkersRelationManager.php | 153 ++- .../Resources/EducationalGroupResource.php | 117 ++- .../Resources/EducationalProgramResource.php | 314 +++--- .../AdmissionPlansRelationManager.php | 192 +++- app/Filament/Resources/EventResource.php | 284 +++--- .../EventResource/Pages/CreateEvent.php | 16 + .../EventResource/Pages/EditEvent.php | 8 + .../DepartmentsRelationManager.php | 457 --------- app/Filament/Resources/FacultyResource.php | 508 +++------- .../FacultyResource/Pages/CreateFaculty.php | 22 +- .../FacultyResource/Pages/EditFaculty.php | 8 +- .../DepartmentsRelationManager.php | 4 +- .../WorkersRelationManager.php | 138 ++- app/Filament/Resources/MainSliderResource.php | 221 ----- .../Pages/CreateMainSlider.php | 20 - .../Pages/EditMainSlider.php | 26 - .../Pages/ListMainSliders.php | 19 - .../Resources/PageReferenceListResource.php | 274 ++++-- .../PageResource/Pages/CreatePage.php | 24 +- .../Resources/PageResource/Pages/EditPage.php | 27 +- .../SectionRelationManager.php | 5 +- app/Filament/Resources/PostResource.php | 28 - .../PostResource/Pages/CreatePost.php | 17 +- .../Resources/PostResource/Pages/EditPost.php | 13 +- app/Filament/Resources/ScheduleResource.php | 245 +++-- .../Resources/Shield/RoleResource.php | 4 +- app/Filament/Resources/SlideResource.php | 5 + app/Filament/Resources/SliderResource.php | 101 +- .../SlidesRelationManager.php | 310 ++++++ app/Filament/Resources/UserDetailResource.php | 2 +- app/Filament/Resources/UserResource.php | 2 +- .../ClientAcademicJournalController.php | 71 +- .../ClientAdditionalEducationController.php | 199 ++-- .../ClientDepartmentController.php | 89 +- .../Controllers/ClientDivisionController.php | 15 +- .../Controllers/ClientEventController.php | 104 +- .../Controllers/ClientFacultyController.php | 61 +- app/Http/Controllers/ClientPostController.php | 25 +- .../Controllers/ClientProgramController.php | 200 ++-- .../Controllers/ClientScheduleController.php | 11 +- ...AdditionalEducationalProgramController.php | 19 +- .../ClientWidgetContactController.php | 14 +- ...ientWidgetEducationalProgramController.php | 19 +- ...lientWidgetPageReferenceListController.php | 14 +- .../ClientWidgetSliderController.php | 4 +- app/Http/Controllers/PageController.php | 41 +- app/Http/Controllers/PersonController.php | 28 +- app/Http/Controllers/SearchController.php | 1 + .../Controllers/StaticSearchController.php | 28 + app/Http/Kernel.php | 2 + .../Middleware/FormTimePeriodMiddleware.php | 47 + app/Http/Middleware/HandleInertiaRequests.php | 30 +- .../Resources/ClientBreadcrumbSection.php | 4 +- .../Resources/ClientEventFullResource.php | 2 +- app/Http/Resources/ClientEventResource.php | 2 +- app/Models/AcademicJournal.php | 8 +- app/Models/CustomForm.php | 1 + app/Models/Page.php | 9 - app/Models/Post.php | 6 +- app/Models/Slide.php | 5 + app/Observers/AcademicJournalObserver.php | 56 ++ app/Observers/AdditionalEducationObserver.php | 56 ++ app/Observers/ContactWidgetObserver.php | 59 ++ app/Observers/DepartmentObserver.php | 59 ++ app/Observers/DivisionObserver.php | 59 ++ app/Observers/EducationalProgramObserver.php | 59 ++ app/Observers/EventObserver.php | 59 ++ app/Observers/FacultyObserver.php | 59 ++ app/Observers/PageObserver.php | 4 +- app/Observers/PageReferenceListObserver.php | 59 ++ app/Observers/ScheduleObserver.php | 59 ++ app/Observers/SlideObserver.php | 77 ++ app/Observers/UserObserver.php | 59 ++ app/Providers/AppServiceProvider.php | 48 +- .../App/Breadcrumb/BreadcrumbService.php | 69 +- .../App/Cache/AcademicJournalCacheService.php | 37 + .../Cache/AdditionalEducationCacheService.php | 35 + .../App/Cache/CategoryCacheService.php | 32 +- .../App/Cache/ContactWidgetCacheService.php | 35 + .../App/Cache/DepartmentCacheService.php | 36 + .../App/Cache/DivisionCacheService.php | 56 ++ .../Cache/EducationalProgramCacheService.php | 52 + app/Services/App/Cache/EventCacheService.php | 42 + .../App/Cache/FacultyCacheService.php | 42 + .../App/Cache/MainSectionCacheService.php | 39 +- .../App/Cache/MainSliderCacheService.php | 48 - app/Services/App/Cache/PageCacheService.php | 56 +- .../Cache/PageReferenceListCacheService.php | 42 + app/Services/App/Cache/PostCacheService.php | 65 +- .../App/Cache/ScheduleCacheService.php | 46 + app/Services/App/Cache/SliderCacheService.php | 36 + .../App/Cache/SubSectionCacheService.php | 42 +- app/Services/App/Cache/TagCacheService.php | 50 +- app/Services/App/Cache/UserCacheService.php | 37 + .../App/Seo/SeoDescriptionInterface.php | 8 + app/Services/App/Seo/SeoPageProvider.php | 39 + .../Domain/Posts/PostSliderService.php | 47 +- .../Domain/Seo/SeoGeneratorService.php | 95 ++ .../Services/BreadcrumbFinderService.php | 37 + .../Services/CategoryFinderService.php | 36 + .../Filament/Services/StaticFileSearch.php | 198 ++++ app/Services/Filament/Traits/SeoGenerate.php | 34 + composer.json | 1 + composer.lock | 54 +- ...3_23_173349_add_morphs_to_slides_table.php | 29 + ..._settings_column_to_custom_forms_table.php | 28 + public/logos/logo_urgpu.png | Bin 0 -> 267101 bytes resources/js/Navbars/DesktopNavBar.vue | 27 +- resources/js/Navbars/MainPageNavbar.vue | 265 +++-- resources/js/Navbars/MobileNavbar.vue | 6 +- .../Pages/Client/AcademicJournals/Index.vue | 10 +- .../js/Pages/Client/AcademicJournals/Show.vue | 13 +- resources/js/Pages/Client/Events/Archive.vue | 17 +- resources/js/Pages/Client/Events/Index.vue | 20 +- resources/js/Pages/Client/Events/Show.vue | 38 +- resources/js/Pages/Client/Posts/Index.vue | 12 +- .../js/Pages/Dashboard/CreateSchedule.vue | 125 --- resources/js/Pages/Main.vue | 2 +- .../AcademicJournalsItemBreadcrumbs.vue | 209 ++++ .../AcademicJournalsListBreadcrumbs.vue.vue | 180 ++++ .../pages/components/PageBreadcrumbs.vue | 4 +- .../posts/components/PostListItem.vue | 6 +- .../search/components/GlobalSearch.vue | 165 ++-- .../features/search/components/ResultList.vue | 10 +- .../search/components/StaticSearch.vue | 231 +++++ .../sliders/components/SliderBlockBody.vue | 8 +- .../sliders/components/SliderBody.vue | 10 +- .../builder/formBuilder/FormBuilder.vue | 347 ++++--- .../formBuilder/blocks/CaptchaBlock.vue | 4 +- .../formBuilder/blocks/PersonalDataBlock.vue | 20 +- .../builder/pageBuilder/blocks/FormBlock.vue | 38 +- .../componentss/shared/modals/SearchModal.vue | 18 +- .../notifications/SuccessNotification.vue | 2 +- .../shared/pageResource/PageResourceList.vue | 4 +- .../ui/wrappers/HeadItemsWrapper.vue | 2 +- resources/js/footers/BasicFooter.vue | 6 +- resources/js/mixins/LinksReform.js | 47 +- resources/js/store/index.js | 53 +- resources/js/utils/cookies.js | 36 + .../pages/profile-page.blade.php | 3 + .../filament-panels/components/logo.blade.php | 2 +- routes/api.php | 7 +- routes/web.php | 10 +- 198 files changed, 9715 insertions(+), 6664 deletions(-) create mode 100644 app/Containers/Post/Models/Post.php create mode 100644 app/Enums/AdmissionCampaignStatus.php create mode 100644 app/Enums/CacheKeys.php create mode 100644 app/Filament/Components/Forms/ItemForm/Blocks/BlockSchema.php create mode 100644 app/Filament/Components/Forms/ItemForm/Blocks/ContactBlock.php create mode 100644 app/Filament/Components/Forms/ItemForm/Blocks/CustomFormBlock.php create mode 100644 app/Filament/Components/Forms/ItemForm/Blocks/FilesBlock.php create mode 100644 app/Filament/Components/Forms/ItemForm/Blocks/HeadingBlock.php create mode 100644 app/Filament/Components/Forms/ItemForm/Blocks/ImageBlock.php create mode 100644 app/Filament/Components/Forms/ItemForm/Blocks/ImagesBlock.php create mode 100644 app/Filament/Components/Forms/ItemForm/Blocks/PageItemBlock.php create mode 100644 app/Filament/Components/Forms/ItemForm/Blocks/PageResourceListBlock.php create mode 100644 app/Filament/Components/Forms/ItemForm/Blocks/ParagraphBlock.php create mode 100644 app/Filament/Components/Forms/ItemForm/Blocks/PersonBlock.php create mode 100644 app/Filament/Components/Forms/ItemForm/Blocks/PostItemBlock.php create mode 100644 app/Filament/Components/Forms/ItemForm/Blocks/PostListBlock.php create mode 100644 app/Filament/Components/Forms/ItemForm/Blocks/SliderBlock.php create mode 100644 app/Filament/Components/Forms/ItemForm/Blocks/StepperBlock.php create mode 100644 app/Filament/Components/Forms/ItemForm/Blocks/TabBlock.php create mode 100644 app/Filament/Components/Forms/ItemForm/Blocks/VideoBlock.php delete mode 100644 app/Filament/Resources/FacultyRecourceResource/RelationManagers/DepartmentsRelationManager.php delete mode 100644 app/Filament/Resources/MainSliderResource.php delete mode 100644 app/Filament/Resources/MainSliderResource/Pages/CreateMainSlider.php delete mode 100644 app/Filament/Resources/MainSliderResource/Pages/EditMainSlider.php delete mode 100644 app/Filament/Resources/MainSliderResource/Pages/ListMainSliders.php create mode 100644 app/Filament/Resources/SliderResource/RelationManagers/SlidesRelationManager.php create mode 100644 app/Http/Controllers/StaticSearchController.php create mode 100644 app/Http/Middleware/FormTimePeriodMiddleware.php create mode 100644 app/Observers/AcademicJournalObserver.php create mode 100644 app/Observers/AdditionalEducationObserver.php create mode 100644 app/Observers/ContactWidgetObserver.php create mode 100644 app/Observers/DepartmentObserver.php create mode 100644 app/Observers/DivisionObserver.php create mode 100644 app/Observers/EducationalProgramObserver.php create mode 100644 app/Observers/EventObserver.php create mode 100644 app/Observers/FacultyObserver.php create mode 100644 app/Observers/PageReferenceListObserver.php create mode 100644 app/Observers/ScheduleObserver.php create mode 100644 app/Observers/SlideObserver.php create mode 100644 app/Observers/UserObserver.php create mode 100644 app/Services/App/Cache/AcademicJournalCacheService.php create mode 100644 app/Services/App/Cache/AdditionalEducationCacheService.php create mode 100644 app/Services/App/Cache/ContactWidgetCacheService.php create mode 100644 app/Services/App/Cache/DepartmentCacheService.php create mode 100644 app/Services/App/Cache/DivisionCacheService.php create mode 100644 app/Services/App/Cache/EducationalProgramCacheService.php create mode 100644 app/Services/App/Cache/EventCacheService.php create mode 100644 app/Services/App/Cache/FacultyCacheService.php delete mode 100644 app/Services/App/Cache/MainSliderCacheService.php create mode 100644 app/Services/App/Cache/PageReferenceListCacheService.php create mode 100644 app/Services/App/Cache/ScheduleCacheService.php create mode 100644 app/Services/App/Cache/SliderCacheService.php create mode 100644 app/Services/App/Cache/UserCacheService.php create mode 100644 app/Services/App/Seo/SeoDescriptionInterface.php create mode 100644 app/Services/App/Seo/SeoPageProvider.php create mode 100644 app/Services/Filament/Domain/Seo/SeoGeneratorService.php create mode 100644 app/Services/Filament/Services/BreadcrumbFinderService.php create mode 100644 app/Services/Filament/Services/CategoryFinderService.php create mode 100644 app/Services/Filament/Services/StaticFileSearch.php create mode 100644 app/Services/Filament/Traits/SeoGenerate.php create mode 100644 database/migrations/2025_03_23_173349_add_morphs_to_slides_table.php create mode 100644 database/migrations/2025_03_26_214117_add_settings_column_to_custom_forms_table.php create mode 100644 public/logos/logo_urgpu.png delete mode 100644 resources/js/Pages/Dashboard/CreateSchedule.vue create mode 100644 resources/js/componentss/features/academicJournals/components/AcademicJournalsItemBreadcrumbs.vue create mode 100644 resources/js/componentss/features/academicJournals/components/AcademicJournalsListBreadcrumbs.vue.vue create mode 100644 resources/js/componentss/features/search/components/StaticSearch.vue create mode 100644 resources/js/utils/cookies.js create mode 100644 resources/views/filament/resources/user-resource/pages/profile-page.blade.php diff --git a/_docker/nginx/conf.d/nginx.conf b/_docker/nginx/conf.d/nginx.conf index 22767be..cfe5efd 100644 --- a/_docker/nginx/conf.d/nginx.conf +++ b/_docker/nginx/conf.d/nginx.conf @@ -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)(/.+)$; # Разделение пути diff --git a/app/Containers/Post/Models/Post.php b/app/Containers/Post/Models/Post.php new file mode 100644 index 0000000..9c047bf --- /dev/null +++ b/app/Containers/Post/Models/Post.php @@ -0,0 +1,64 @@ +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' + ]; +} diff --git a/app/Dto/MainSliderDTO.php b/app/Dto/MainSliderDTO.php index f36f798..37fcc24 100644 --- a/app/Dto/MainSliderDTO.php +++ b/app/Dto/MainSliderDTO.php @@ -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, ]; } } \ No newline at end of file diff --git a/app/Enums/AdmissionCampaignStatus.php b/app/Enums/AdmissionCampaignStatus.php new file mode 100644 index 0000000..09c1207 --- /dev/null +++ b/app/Enums/AdmissionCampaignStatus.php @@ -0,0 +1,31 @@ + 'Активная', + self::ARCHIVED => 'Архивная', + self::HIDDEN => 'Скрытая', + }; + } + + public function getColor(): string|array|null + { + return match ($this) { + self::ACTIVE => 'success', + self::ARCHIVED => 'gray', + self::HIDDEN => 'danger', + }; + } +} \ No newline at end of file diff --git a/app/Enums/CacheKeys.php b/app/Enums/CacheKeys.php new file mode 100644 index 0000000..46c590d --- /dev/null +++ b/app/Enums/CacheKeys.php @@ -0,0 +1,57 @@ +schema([ - Section::make() + Section::make('Настройки формы') + ->description('Конфигурация пользовательской формы') + ->collapsible() ->schema([ - Tabs::make('Tabs') + Tabs::make('Конфигурация формы') + ->persistTabInQueryString() + ->columnSpanFull() ->tabs([ Tabs\Tab::make('Основная информация') + ->icon('heroicon-o-information-circle') ->schema([ - Forms\Components\Grid::make(2)->schema([ - TextInput::make('title')->label('Заголовок')->required() - ->live(onBlur: true) - ->afterStateUpdated(function (string $operation, string|null $state, Forms\Set $set) { - $set('form_id', Str::slug($state) . Carbon::now()->timestamp); - }), - TextInput::make('form_id')->label('ID формы')->unique(ignoreRecord: true)->required(), - ]), - Forms\Components\Textarea::make('description')->label('Описание формы')->required(), - Select::make('status')->label('Статус формы')->required() + Forms\Components\Grid::make(2) + ->schema([ + TextInput::make('title') + ->label('Название формы') + ->placeholder('Введите название формы') + ->helperText('Это название будет видно пользователям') + ->required() + ->maxLength(255) + ->live(onBlur: true) + ->afterStateUpdated(function (string $operation, string|null $state, Forms\Set $set) { + $set('form_id', Str::slug($state) . Carbon::now()->timestamp); + }), + + TextInput::make('form_id') + ->label('Уникальный ID формы') + ->helperText('Автоматически генерируется из названия') + ->required() + ->unique(ignoreRecord: true) + ->maxLength(255), + ]), + + Forms\Components\Textarea::make('description') + ->label('Описание формы') + ->placeholder('Опишите назначение этой формы') + ->helperText('Это описание будет видно пользователям') + ->required() + ->maxLength(2000) + ->columnSpanFull(), + + Select::make('status') + ->label('Статус формы') ->options(CustomFormStatus::class) + ->required() + ->native(false) + ->helperText('Определяет видимость формы на сайте') + ->columnSpanFull(), ]), - Tabs\Tab::make('Колонки') + + Tabs\Tab::make('Поля формы') + ->icon('heroicon-o-view-columns') ->schema([ - FormBuilderItem::getItem(), + FormBuilderItem::getItem() + ->columnSpanFull(), ]), + Tabs\Tab::make('Кнопка отправки') + ->icon('heroicon-o-paper-airplane') ->schema([ - TextInput::make('button')->label('Текст кнопки отправки')->required(), - Forms\Components\Textarea::make('send_message')->label('Текст после отправления письма')->required(), + TextInput::make('button') + ->label('Текст кнопки отправки') + ->placeholder('Например: Отправить заявку') + ->helperText('Текст, который будет отображаться на кнопке отправки формы') + ->required() + ->maxLength(255), + + Forms\Components\Textarea::make('send_message') + ->label('Сообщение после отправки') + ->placeholder('Спасибо! Ваша заявка принята.') + ->helperText('Это сообщение увидят пользователи после успешной отправки формы') + ->required() + ->maxLength(1000) + ->columnSpanFull(), ]), - Tabs\Tab::make('Настройка интеграции с почтой') + + Tabs\Tab::make('Настройки') + ->icon('heroicon-o-cog') + ->schema([ + Toggle::make('settings.personal_data') + ->label('Согласие на обработку данных') + ->helperText('Показывать checkbox для согласия на обработку персональных данных') + ->inline(false) + ->onColor('success') + ->offColor('gray'), + + Toggle::make('settings.captcha') + ->label('Защита CAPTCHA') + ->helperText('Включить защиту от спама с помощью CAPTCHA') + ->inline(false) + ->onColor('success') + ->offColor('gray'), + + Section::make('Ограничение по времени') + ->collapsible() + ->schema([ + Toggle::make('is_time_period') + ->label('Ограничить период работы формы') + ->helperText('Форма будет активна только в указанный период') + ->dehydrated(false) + ->live(true) + ->inline(false), + + Forms\Components\Grid::make(2) + ->schema([ + DateTimePicker::make('settings.period.start_time') + ->label('Дата начала') + ->native(false) + ->displayFormat('d/m/Y H:i') + ->seconds(false) + ->helperText('Когда форма станет доступна') + ->default(Carbon::now()) + ->minDate(Carbon::now()), + + DateTimePicker::make('settings.period.end_time') + ->label('Дата окончания') + ->native(false) + ->displayFormat('d/m/Y H:i') + ->seconds(false) + ->helperText('Когда форма перестанет быть доступна') + ->default(Carbon::now()->addWeeks(2)) + ->minDate(Carbon::now()), + ]) + ->hidden(fn(Forms\Get $get): bool => $get('is_time_period') !== true), + ]), + ]), + + Tabs\Tab::make('Настройки почты') + ->icon('heroicon-o-envelope') ->schema([ Forms\Components\Repeater::make('mail_settings') - ->label('') + ->label('Настройки уведомлений') ->addActionLabel('Добавить получателя') + ->helperText('Укажите, кому и какие уведомления отправлять') + ->collapsed() + ->itemLabel(fn (array $state): ?string => $state['target'] ?? 'Новый получатель') ->schema([ - TextInput::make('target')->label('Кому')->email()->required(), - TextInput::make('topic')->label('Тема')->required(), - Builder::make('data')->schema([ - Builder\Block::make('text')->schema([ - RichEditor::make('content')->required(), + TextInput::make('target') + ->label('Email получателя') + ->placeholder('email@example.com') + ->email() + ->required() + ->maxLength(255), + + TextInput::make('topic') + ->label('Тема письма') + ->placeholder('Новая заявка с формы') + ->required() + ->maxLength(255), + + Builder::make('data') + ->label('Содержимое письма') + ->blockNumbers(false) + ->collapsible() + ->schema([ + Builder\Block::make('text') + ->label('Текст письма') + ->schema([ + RichEditor::make('content') + ->label('') + ->required() + ->toolbarButtons([ + 'bold', 'italic', 'link', + 'orderedList', 'bulletList' + ]), + ]), + Builder\Block::make('answers') + ->label('Ответы формы') + ->schema([]), ]), - Builder\Block::make('answers')->schema([]), - ])->required(), ]) - ->collapsed(), + ->grid(2), ]), ]), - ]) + ]), ]); } } \ No newline at end of file diff --git a/app/Filament/Components/Forms/ItemForm/Blocks/BlockSchema.php b/app/Filament/Components/Forms/ItemForm/Blocks/BlockSchema.php new file mode 100644 index 0000000..0dda2df --- /dev/null +++ b/app/Filament/Components/Forms/ItemForm/Blocks/BlockSchema.php @@ -0,0 +1,8 @@ +label('Виджет контактов') + ->options(ContactWidget::query()->where('is_active', true)->pluck('title', 'slug')) + ->searchable() + ->required() + ->helperText('Выберите активный виджет контактов'), + ]; + } +} \ No newline at end of file diff --git a/app/Filament/Components/Forms/ItemForm/Blocks/CustomFormBlock.php b/app/Filament/Components/Forms/ItemForm/Blocks/CustomFormBlock.php new file mode 100644 index 0000000..be12347 --- /dev/null +++ b/app/Filament/Components/Forms/ItemForm/Blocks/CustomFormBlock.php @@ -0,0 +1,28 @@ +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), + ]), + ]; + } +} \ No newline at end of file diff --git a/app/Filament/Components/Forms/ItemForm/Blocks/FilesBlock.php b/app/Filament/Components/Forms/ItemForm/Blocks/FilesBlock.php new file mode 100644 index 0000000..c586c61 --- /dev/null +++ b/app/Filament/Components/Forms/ItemForm/Blocks/FilesBlock.php @@ -0,0 +1,66 @@ +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), + ]; + } +} \ No newline at end of file diff --git a/app/Filament/Components/Forms/ItemForm/Blocks/HeadingBlock.php b/app/Filament/Components/Forms/ItemForm/Blocks/HeadingBlock.php new file mode 100644 index 0000000..e3ace7a --- /dev/null +++ b/app/Filament/Components/Forms/ItemForm/Blocks/HeadingBlock.php @@ -0,0 +1,27 @@ +hidden() + ->integer() + ->default(rand(2335235, 324634264263426)), + TextInput::make('content') + ->label('Текст заголовка') + ->placeholder('Введите текст заголовка') + ->helperText('Основной заголовок раздела') + ->live(onBlur: true) + ->required() + ->maxLength(255), + ]; + } +} \ No newline at end of file diff --git a/app/Filament/Components/Forms/ItemForm/Blocks/ImageBlock.php b/app/Filament/Components/Forms/ItemForm/Blocks/ImageBlock.php new file mode 100644 index 0000000..ddaecf1 --- /dev/null +++ b/app/Filament/Components/Forms/ItemForm/Blocks/ImageBlock.php @@ -0,0 +1,38 @@ +label('Изображение') + ->image() + ->multiple() + ->reorderable() + ->maxFiles(5) + ->disk('public') + ->directory('images') + ->imageEditor() + ->required() + ->helperText('Можно загрузить до 5 изображений'), + TextInput::make('alt') + ->label('Альтернативный текст') + ->placeholder('Необязательно') + ->helperText('Описание изображения для SEO'), + ]; + } +} \ No newline at end of file diff --git a/app/Filament/Components/Forms/ItemForm/Blocks/ImagesBlock.php b/app/Filament/Components/Forms/ItemForm/Blocks/ImagesBlock.php new file mode 100644 index 0000000..be59793 --- /dev/null +++ b/app/Filament/Components/Forms/ItemForm/Blocks/ImagesBlock.php @@ -0,0 +1,38 @@ +label('Изображения') + ->image() + ->multiple() + ->reorderable() + ->maxFiles(5) + ->disk('public') + ->directory('images') + ->imageEditor() + ->required() + ->helperText('Максимум 5 изображений. Можно перетаскивать для изменения порядка'), + TextInput::make('alt') + ->label('Описание изображений') + ->placeholder('Необязательно') + ->helperText('Используется для SEO и доступности'), + ]; + } +} \ No newline at end of file diff --git a/app/Filament/Components/Forms/ItemForm/Blocks/PageItemBlock.php b/app/Filament/Components/Forms/ItemForm/Blocks/PageItemBlock.php new file mode 100644 index 0000000..148847e --- /dev/null +++ b/app/Filament/Components/Forms/ItemForm/Blocks/PageItemBlock.php @@ -0,0 +1,31 @@ +label('Страница') + ->options(Page::query()->where('title', '!=', null)->where('is_visible', true)->pluck('title', 'id')) + ->searchable() + ->required() + ->helperText('Выберите видимую страницу'), + ]; + } +} \ No newline at end of file diff --git a/app/Filament/Components/Forms/ItemForm/Blocks/PageResourceListBlock.php b/app/Filament/Components/Forms/ItemForm/Blocks/PageResourceListBlock.php new file mode 100644 index 0000000..d3b82ac --- /dev/null +++ b/app/Filament/Components/Forms/ItemForm/Blocks/PageResourceListBlock.php @@ -0,0 +1,31 @@ +label('Ресурс') + ->options(PageReferenceList::query()->where('is_active', true)->pluck('title', 'slug')) + ->searchable() + ->required() + ->helperText('Выберите активный ресурс'), + ]; + } +} \ No newline at end of file diff --git a/app/Filament/Components/Forms/ItemForm/Blocks/ParagraphBlock.php b/app/Filament/Components/Forms/ItemForm/Blocks/ParagraphBlock.php new file mode 100644 index 0000000..a65108a --- /dev/null +++ b/app/Filament/Components/Forms/ItemForm/Blocks/ParagraphBlock.php @@ -0,0 +1,51 @@ +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; + } +} \ No newline at end of file diff --git a/app/Filament/Components/Forms/ItemForm/Blocks/PersonBlock.php b/app/Filament/Components/Forms/ItemForm/Blocks/PersonBlock.php new file mode 100644 index 0000000..6b0213c --- /dev/null +++ b/app/Filament/Components/Forms/ItemForm/Blocks/PersonBlock.php @@ -0,0 +1,63 @@ +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), + ]; + } +} \ No newline at end of file diff --git a/app/Filament/Components/Forms/ItemForm/Blocks/PostItemBlock.php b/app/Filament/Components/Forms/ItemForm/Blocks/PostItemBlock.php new file mode 100644 index 0000000..f6855b9 --- /dev/null +++ b/app/Filament/Components/Forms/ItemForm/Blocks/PostItemBlock.php @@ -0,0 +1,23 @@ +label('Новость') + ->options(Post::query()->where('status', PostStatus::PUBLISHED)->pluck('title', 'id')) + ->searchable() + ->required() + ->helperText('Выберите опубликованную новость'), + ]; + } +} \ No newline at end of file diff --git a/app/Filament/Components/Forms/ItemForm/Blocks/PostListBlock.php b/app/Filament/Components/Forms/ItemForm/Blocks/PostListBlock.php new file mode 100644 index 0000000..c098713 --- /dev/null +++ b/app/Filament/Components/Forms/ItemForm/Blocks/PostListBlock.php @@ -0,0 +1,41 @@ +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('Выберите категорию или оставьте пустым для всех'), + ]), + ]; + } +} \ No newline at end of file diff --git a/app/Filament/Components/Forms/ItemForm/Blocks/SliderBlock.php b/app/Filament/Components/Forms/ItemForm/Blocks/SliderBlock.php new file mode 100644 index 0000000..4410cc4 --- /dev/null +++ b/app/Filament/Components/Forms/ItemForm/Blocks/SliderBlock.php @@ -0,0 +1,31 @@ +label('Слайдер') + ->options(Slider::query()->whereHas('slides')->where('is_active', true)->pluck('title', 'slug')) + ->searchable() + ->required() + ->helperText('Выберите активный слайдер с изображениями'), + ]; + } +} \ No newline at end of file diff --git a/app/Filament/Components/Forms/ItemForm/Blocks/StepperBlock.php b/app/Filament/Components/Forms/ItemForm/Blocks/StepperBlock.php new file mode 100644 index 0000000..e2e9333 --- /dev/null +++ b/app/Filament/Components/Forms/ItemForm/Blocks/StepperBlock.php @@ -0,0 +1,55 @@ +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), + ]; + } +} \ No newline at end of file diff --git a/app/Filament/Components/Forms/ItemForm/Blocks/TabBlock.php b/app/Filament/Components/Forms/ItemForm/Blocks/TabBlock.php new file mode 100644 index 0000000..4e32382 --- /dev/null +++ b/app/Filament/Components/Forms/ItemForm/Blocks/TabBlock.php @@ -0,0 +1,128 @@ +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), + ]; + } +} \ No newline at end of file diff --git a/app/Filament/Components/Forms/ItemForm/Blocks/VideoBlock.php b/app/Filament/Components/Forms/ItemForm/Blocks/VideoBlock.php new file mode 100644 index 0000000..4addf8a --- /dev/null +++ b/app/Filament/Components/Forms/ItemForm/Blocks/VideoBlock.php @@ -0,0 +1,53 @@ +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())), + ]; + } +} \ No newline at end of file diff --git a/app/Filament/Components/Forms/ItemForm/CustomForm/FormBuilderItem.php b/app/Filament/Components/Forms/ItemForm/CustomForm/FormBuilderItem.php index 43326e4..8501b43 100644 --- a/app/Filament/Components/Forms/ItemForm/CustomForm/FormBuilderItem.php +++ b/app/Filament/Components/Forms/ItemForm/CustomForm/FormBuilderItem.php @@ -16,9 +16,11 @@ use Filament\Forms; use Filament\Forms\Components\Builder; use Filament\Forms\Components\FileUpload; use Filament\Forms\Components\Hidden; +use Filament\Forms\Components\Repeater; use Filament\Forms\Components\RichEditor; use Filament\Forms\Components\Section; use Filament\Forms\Components\Select; +use Filament\Forms\Components\Textarea; use Filament\Forms\Components\TextInput; use Filament\Forms\Components\Toggle; use Filament\Forms\Form; @@ -29,23 +31,44 @@ use Mohamedsabil83\FilamentFormsTinyeditor\Components\TinyEditor; class FormBuilderItem { - public static function getItem() + public static function getItem(): Builder { return Builder::make('columns') + ->label('Конструктор полей формы') + ->addActionLabel('Добавить новое поле') + ->blockPickerColumns(3) + ->blockPickerWidth('2xl') + ->collapsed() + ->collapsible() + ->cloneable() ->schema([ + // Email поле Builder\Block::make('email') - ->label('Почта') + ->icon('heroicon-o-envelope') + ->label('Поле Email') ->schema([ TextInput::make('title_field') - ->label('Заголовок поля') + ->label('Название поля') + ->placeholder('Например: Ваш Email') + ->helperText('Это название будет отображаться пользователям') + ->required() + ->maxLength(255) ->live(onBlur: true) ->afterStateUpdated(function (string $operation, string $state, Forms\Set $set) { - $set('name_field', Str::slug($state) . Carbon::now()->timestamp ); + $set('name_field', Str::slug($state) . Carbon::now()->timestamp); }), - Forms\Components\Hidden::make('name_field')->required(), - Forms\Components\Textarea::make('description')->label('Описание поля (опционально)'), - Section::make('Настройка') + Hidden::make('name_field')->required(), + + Textarea::make('description') + ->label('Подсказка для поля') + ->placeholder('Например: Введите действующий email') + ->helperText('Необязательное пояснение для пользователей') + ->maxLength(500) + ->columnSpanFull(), + + Section::make('Дополнительные настройки') + ->collapsible() ->collapsed() ->statePath('rules') ->schema([ @@ -54,18 +77,34 @@ class FormBuilderItem RuleLengthLimitComponent::getComponent(), ]), ]), + + // Phone поле Builder\Block::make('phone') - ->label('Телефон') + ->icon('heroicon-o-phone') + ->label('Поле Телефона') ->schema([ TextInput::make('title_field') - ->label('Заголовок поля') + ->label('Название поля') + ->placeholder('Например: Ваш телефон') + ->helperText('Укажите контактный номер для связи') + ->required() + ->maxLength(255) ->live(onBlur: true) ->afterStateUpdated(function (string $operation, string $state, Forms\Set $set) { - $set('name_field', Str::slug($state) . Carbon::now()->timestamp ); + $set('name_field', Str::slug($state) . Carbon::now()->timestamp); }), - Forms\Components\Hidden::make('name_field')->required(), - Forms\Components\Textarea::make('description')->label('Описание поля (опционально)'), - Section::make('Настройка') + + Hidden::make('name_field')->required(), + + Textarea::make('description') + ->label('Подсказка для поля') + ->placeholder('Например: +7 (XXX) XXX-XX-XX') + ->maxLength(500) + ->columnSpanFull(), + + Section::make('Дополнительные настройки') + ->collapsible() + ->collapsed() ->statePath('rules') ->schema([ RuleRequiredComponent::getComponent(), @@ -73,73 +112,135 @@ class FormBuilderItem RuleLengthLimitComponent::getComponent(), ]), ]), + + // Короткий текст Builder\Block::make('text') + ->icon('heroicon-o-pencil') ->label('Короткий текст') ->schema([ TextInput::make('title_field') - ->label('Заголовок поля') + ->label('Название поля') + ->placeholder('Например: Ваше имя') + ->helperText('Краткий текст (до 255 символов)') + ->required() + ->maxLength(255) ->live(onBlur: true) ->afterStateUpdated(function (string $operation, string $state, Forms\Set $set) { - $set('name_field', Str::slug($state) . Carbon::now()->timestamp ); + $set('name_field', Str::slug($state) . Carbon::now()->timestamp); }), - Forms\Components\Hidden::make('name_field')->required(), - Forms\Components\Textarea::make('description')->label('Описание поля (опционально)'), - Section::make('Настройка') - ->statePath('rules') - ->schema([ - RuleRequiredComponent::getComponent(), - RuleLengthLimitComponent::getComponent(), - ]), - ]), - Builder\Block::make('textarea') - ->label('Длинный текст текст') - ->schema([ - TextInput::make('title_field') - ->label('Заголовок поля') - ->live(onBlur: true) - ->afterStateUpdated(function (string $operation, string $state, Forms\Set $set) { - $set('name_field', Str::slug($state) . Carbon::now()->timestamp ); - }), - Forms\Components\Hidden::make('name_field')->required(), - Forms\Components\Textarea::make('description')->label('Описание поля (опционально)'), - Section::make('Настройка') + Hidden::make('name_field')->required(), + + Textarea::make('description') + ->label('Подсказка для поля') + ->placeholder('Например: Введите ваше полное имя') + ->maxLength(500) + ->columnSpanFull(), + + Section::make('Дополнительные настройки') + ->collapsible() + ->collapsed() ->statePath('rules') ->schema([ RuleRequiredComponent::getComponent(), RuleLengthLimitComponent::getComponent(), ]), ]), + + // Длинный текст + Builder\Block::make('textarea') + ->icon('heroicon-o-document-text') + ->label('Длинный текст') + ->schema([ + TextInput::make('title_field') + ->label('Название поля') + ->placeholder('Например: Ваш комментарий') + ->helperText('Расширенный текст (до 5000 символов)') + ->required() + ->maxLength(255) + ->live(onBlur: true) + ->afterStateUpdated(function (string $operation, string $state, Forms\Set $set) { + $set('name_field', Str::slug($state) . Carbon::now()->timestamp); + }), + + Hidden::make('name_field')->required(), + + Textarea::make('description') + ->label('Подсказка для поля') + ->placeholder('Например: Опишите вашу проблему подробно') + ->maxLength(500) + ->columnSpanFull(), + + Section::make('Дополнительные настройки') + ->collapsible() + ->collapsed() + ->statePath('rules') + ->schema([ + RuleRequiredComponent::getComponent(), + RuleLengthLimitComponent::getComponent(), + ]), + ]), + + // Дата Builder\Block::make('date') + ->icon('heroicon-o-calendar') ->label('Дата') ->schema([ TextInput::make('title_field') - ->label('Заголовок поля') + ->label('Название поля') + ->placeholder('Например: Дата рождения') + ->helperText('Выбор даты из календаря') + ->required() + ->maxLength(255) ->live(onBlur: true) ->afterStateUpdated(function (string $operation, string $state, Forms\Set $set) { - $set('name_field', Str::slug($state) . Carbon::now()->timestamp ); + $set('name_field', Str::slug($state) . Carbon::now()->timestamp); }), - Forms\Components\Hidden::make('name_field')->required(), - Forms\Components\Textarea::make('description')->label('Описание поля (опционально)'), - Section::make('Настройка') + + Hidden::make('name_field')->required(), + + Textarea::make('description') + ->label('Подсказка для поля') + ->placeholder('Например: Укажите вашу дату рождения') + ->maxLength(500) + ->columnSpanFull(), + + Section::make('Дополнительные настройки') + ->collapsible() + ->collapsed() ->statePath('rules') ->schema([ RuleRequiredComponent::getComponent(), ]), - ]), + + // Ссылка Builder\Block::make('url') + ->icon('heroicon-o-link') ->label('Ссылка') ->schema([ TextInput::make('title_field') - ->label('Заголовок поля') + ->label('Название поля') + ->placeholder('Например: Ваш сайт') + ->helperText('Введите корректный URL адрес') + ->required() + ->maxLength(255) ->live(onBlur: true) ->afterStateUpdated(function (string $operation, string $state, Forms\Set $set) { - $set('name_field', Str::slug($state) . Carbon::now()->timestamp ); + $set('name_field', Str::slug($state) . Carbon::now()->timestamp); }), - Forms\Components\Hidden::make('name_field')->required(), - Forms\Components\Textarea::make('description')->label('Описание поля (опционально)'), - Section::make('Настройка') + + Hidden::make('name_field')->required(), + + Textarea::make('description') + ->label('Подсказка для поля') + ->placeholder('Например: https://example.com') + ->maxLength(500) + ->columnSpanFull(), + + Section::make('Дополнительные настройки') + ->collapsible() + ->collapsed() ->statePath('rules') ->schema([ RuleRequiredComponent::getComponent(), @@ -147,140 +248,178 @@ class FormBuilderItem RuleLengthLimitComponent::getComponent(), ]), ]), + + // Множественный выбор Builder\Block::make('multiple_choice') - ->label('Несколько вариантов') + ->icon('heroicon-o-check-circle') + ->label('Множественный выбор') ->schema([ TextInput::make('title_field') - ->label('Заголовок поля') + ->label('Название группы') + ->placeholder('Например: Ваши интересы') + ->helperText('Несколько вариантов с возможностью выбора') + ->required() + ->maxLength(255) ->live(onBlur: true) ->afterStateUpdated(function (string $operation, string $state, Forms\Set $set) { - $set('name_field', Str::slug($state) . Carbon::now()->timestamp ); + $set('name_field', Str::slug($state) . Carbon::now()->timestamp); }), - Forms\Components\Hidden::make('name_field')->required(), Forms\Components\Repeater::make('columns')->schema([ - TextInput::make('title_field') - ->live(onBlur: true) - ->afterStateUpdated(function (string $operation, string $state, Forms\Set $set) { - $set('name_field', Str::slug($state) . Carbon::now()->timestamp ); - }), - Forms\Components\Hidden::make('name_field')->required(), - Forms\Components\Textarea::make('description')->label('Описание поля (опционально)'), - ])->collapsed(), - Section::make('Настройка') + Hidden::make('name_field')->required(), + + Repeater::make('columns') + ->label('Варианты выбора') + ->addActionLabel('Добавить вариант') + ->collapsible() + ->cloneable() + ->itemLabel(fn (array $state): ?string => $state['title_field'] ?? 'Новый вариант') + ->schema([ + TextInput::make('title_field') + ->label('Текст варианта') + ->placeholder('Например: Спорт') + ->required() + ->maxLength(255) + ->live(onBlur: true) + ->afterStateUpdated(function (string $operation, string $state, Forms\Set $set) { + $set('name_field', Str::slug($state) . Carbon::now()->timestamp); + }), + + Hidden::make('name_field')->required(), + + Textarea::make('description') + ->label('Описание варианта') + ->placeholder('Необязательное описание') + ->maxLength(500), + ]), + + Section::make('Дополнительные настройки') + ->collapsible() + ->collapsed() ->statePath('rules') ->schema([ RuleRequiredComponent::getComponent(), ]), ]), + + + // Одиночный выбор Builder\Block::make('single_choice') - ->label('Один вариант') + ->icon('heroicon-o-radio') + ->label('Одиночный выбор') ->schema([ TextInput::make('title_field') - ->label('Заголовок поля') + ->label('Название группы') + ->placeholder('Например: Ваш пол') + ->helperText('Один вариант из предложенных') + ->required() + ->maxLength(255) ->live(onBlur: true) ->afterStateUpdated(function (string $operation, string $state, Forms\Set $set) { - $set('name_field', Str::slug($state) . Carbon::now()->timestamp ); + $set('name_field', Str::slug($state) . Carbon::now()->timestamp); }), - Forms\Components\Hidden::make('name_field')->required(), Forms\Components\Repeater::make('columns')->schema([ - TextInput::make('title_field') - ->live(onBlur: true) - ->afterStateUpdated(function (string $operation, string $state, Forms\Set $set) { - $set('name_field', Str::slug($state) . Carbon::now()->timestamp ); - }), - Forms\Components\Hidden::make('name_field')->required(), - Forms\Components\Textarea::make('description')->label('Описание поля (опционально)'), - ])->collapsed(), - Section::make('Настройка') + Hidden::make('name_field')->required(), + + Repeater::make('columns') + ->label('Варианты выбора') + ->addActionLabel('Добавить вариант') + ->collapsible() + ->cloneable() + ->itemLabel(fn (array $state): ?string => $state['title_field'] ?? 'Новый вариант') + ->schema([ + TextInput::make('title_field') + ->label('Текст варианта') + ->placeholder('Например: Мужской') + ->required() + ->maxLength(255) + ->live(onBlur: true) + ->afterStateUpdated(function (string $operation, string $state, Forms\Set $set) { + $set('name_field', Str::slug($state) . Carbon::now()->timestamp); + }), + + Hidden::make('name_field')->required(), + + Textarea::make('description') + ->label('Описание варианта') + ->placeholder('Необязательное описание') + ->maxLength(500), + ]), + + Section::make('Дополнительные настройки') + ->collapsible() + ->collapsed() ->statePath('rules') ->schema([ RuleRequiredComponent::getComponent(), - ]), + ]), ]), + + // Дополнительное образование Builder\Block::make('additional_education_choice') - ->label('Выбрать дополнительное образование') + ->icon('heroicon-o-academic-cap') + ->label('Доп. образование') ->schema([ TextInput::make('title_field') - ->label('Заголовок поля') + ->label('Название поля') + ->placeholder('Например: Дополнительное образование') + ->helperText('Выбор из списка доп. образования') + ->required() + ->maxLength(255) ->live(onBlur: true) ->afterStateUpdated(function (string $operation, string $state, Forms\Set $set) { - $set('name_field', Str::slug($state) . Carbon::now()->timestamp ); + $set('name_field', Str::slug($state) . Carbon::now()->timestamp); }), - Forms\Components\Hidden::make('name_field')->required(), - Forms\Components\Textarea::make('description')->label('Описание поля (опционально)'), - Section::make('Настройка') + Hidden::make('name_field')->required(), + + Textarea::make('description') + ->label('Подсказка для поля') + ->placeholder('Например: Выберите интересующую программу') + ->maxLength(500) + ->columnSpanFull(), + + Section::make('Дополнительные настройки') + ->collapsible() + ->collapsed() ->statePath('rules') - ->schema([ -// RuleRequiredComponent::getComponent(), -// RuleLengthLimitComponent::getComponent(), - ]), + ->schema([]), ]) ->maxItems(1), + + // Образовательная программа Builder\Block::make('educational_program_choice') - ->label('Выбрать Образовательную программу') + ->icon('heroicon-o-book-open') + ->label('Образовательная программа') ->schema([ TextInput::make('title_field') - ->label('Заголовок поля') + ->label('Название поля') + ->placeholder('Например: Основная программа') + ->helperText('Выбор из списка образовательных программ') + ->required() + ->maxLength(255) ->live(onBlur: true) ->afterStateUpdated(function (string $operation, string $state, Forms\Set $set) { - $set('name_field', Str::slug($state) . Carbon::now()->timestamp ); + $set('name_field', Str::slug($state) . Carbon::now()->timestamp); }), - Forms\Components\Hidden::make('name_field')->required(), - Forms\Components\Textarea::make('description')->label('Описание поля (опционально)'), - Section::make('Настройка') + Hidden::make('name_field')->required(), + + Textarea::make('description') + ->label('Подсказка для поля') + ->placeholder('Например: Выберите основную программу обучения') + ->maxLength(500) + ->columnSpanFull(), + + Section::make('Дополнительные настройки') + ->collapsible() + ->collapsed() ->statePath('rules') ->schema([ RuleRequiredComponent::getComponent(), RuleLengthLimitComponent::getComponent(), ]), ]) - ->maxItems(1), - Builder\Block::make('captcha') - ->label('reCaptcha') - ->schema([ - TextInput::make('title_field') - ->label('Заголовок поля') - ->live(onBlur: true) - ->default('Капча') - ->disabled(true) - ->dehydrated(true), - Forms\Components\Hidden::make('name_field')->required()->default(Str::slug('reCaptcha') . Carbon::now()->timestamp), - Section::make('Настройка') - ->collapsed() - ->statePath('rules') - ->schema([ - RuleRequiredComponent::getComponent()->default(true), - ]), - ]), - Builder\Block::make('personal_data') - ->label('Соглашение на обработку персональных данных') - ->schema([ - TextInput::make('title_field') - ->label('Заголовок поля') - ->live(onBlur: true) - ->default('Соглашение на обработку персональных данных') - ->afterStateUpdated(function (string $operation, string $state, Forms\Set $set) { - $set('name_field', Str::slug($state) . Carbon::now()->timestamp ); - }), - Forms\Components\Hidden::make('name_field')->required()->default(Str::slug('Соглашение на обработку персональных данных') . Carbon::now()->timestamp), - - Section::make('Настройка') - ->collapsed() - ->statePath('rules') - ->schema([ - RuleRequiredComponent::getComponent()->default(true), - ]), - ]), - ]) - ->label('') - ->addActionLabel('Добавить поле') - ->blockPickerColumns(3) - ->blockPickerWidth('2xl') - ->collapsed(); - + ->maxItems(1) + ]); } - - } \ No newline at end of file diff --git a/app/Filament/Components/Forms/ItemForm/Defaults/ContentBuilderItem.php b/app/Filament/Components/Forms/ItemForm/Defaults/ContentBuilderItem.php index 4780d77..c88c244 100644 --- a/app/Filament/Components/Forms/ItemForm/Defaults/ContentBuilderItem.php +++ b/app/Filament/Components/Forms/ItemForm/Defaults/ContentBuilderItem.php @@ -13,10 +13,13 @@ use App\Models\Post; use Filament\Forms; use Filament\Forms\Components\Builder; use Filament\Forms\Components\FileUpload; +use Filament\Forms\Components\Grid; use Filament\Forms\Components\Hidden; +use Filament\Forms\Components\Repeater; use Filament\Forms\Components\RichEditor; use Filament\Forms\Components\Section; use Filament\Forms\Components\Select; +use Filament\Forms\Components\Textarea; use Filament\Forms\Components\TextInput; use Filament\Forms\Components\Toggle; use Filament\Forms\Form; @@ -29,202 +32,347 @@ class ContentBuilderItem { public static function getItem(string $name) { - return - Builder::make($name)->label('')->blocks([ - Builder\Block::make('heading')->label('Заголовок') + return Builder::make($name) + ->label('Конструктор содержимого') + ->blocks([ + // Заголовок + Builder\Block::make('heading') + ->icon('heroicon-o-title') + ->label('Заголовок') ->schema([ - TextInput::make('id')->hidden()->integer()->default(rand(2335235,324634264263426)), + TextInput::make('id') + ->hidden() + ->integer() + ->default(rand(2335235, 324634264263426)), + TextInput::make('content') - ->label('') - ->live(onBlur: true) - ->afterStateUpdated(function (string $operation, string $state, Forms\Set $set, $get) { - }), + ->label('Текст заголовка') + ->placeholder('Введите заголовок H2-H4') + ->hint('Рекомендуется 50-80 символов') + ->helperText('Используйте для семантической структуры') + ->minLength(10) + ->maxLength(120) + ->required() + ->live(onBlur: true), ]), + + // Текстовый блок Builder\Block::make('paragraph') + ->icon('heroicon-o-document-text') + ->label('Текстовый блок') ->schema([ TinyEditor::make('content') - ->label('') - ->profile('test') - ->required(), - ])->label('Текст'), - Builder\Block::make('files') - ->label('Файл(-ы)') - ->schema([ - Forms\Components\Repeater::make('file')->schema([ - Hidden::make('expansion')->required(), - Hidden::make('size')->required(), - TextInput::make('title') - ->required() - ->maxLength(255) - ->autofocus(), - FileUpload::make('path') - ->required() - ->getUploadedFileNameForStorageUsing( - fn (TemporaryUploadedFile $file): string => - str(Str::slug(pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME) . '-' . Carbon::now()->timestamp ) . '.' . $file->getClientOriginalExtension()) - ) - ->acceptedFileTypes([ - 'application/pdf', - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - 'application/vnd.openxmlformats-officedocument.presentationml.presentation', - 'application/zip' - ]) - ->maxSize(512000) - ->disk('public') - ->directory('files') - ->downloadable() - ->afterStateUpdated(function ($set, $state) { - $set('expansion', $state?->getClientOriginalExtension()); - $set('size', ByteConverter::bytesToHuman($state?->getSize())); - }) - ->visibility('public') - ]), + ->label('Содержимое') + ->placeholder('Введите текст...') + ->hint('Поддерживается форматирование') + ->helperText('Для заголовков используйте стили H3-H4') + ->required() + ->columnSpanFull(), ]), + + // Файлы + Builder\Block::make('files') + ->icon('heroicon-o-paper-clip') + ->label('Файлы для скачивания') + ->schema([ + Repeater::make('file') + ->label('') + ->hint('Максимум 10 файлов') + ->schema([ + Hidden::make('expansion')->required(), + Hidden::make('size')->required(), + + TextInput::make('title') + ->label('Название файла') + ->placeholder('Годовой отчет 2023.pdf') + ->required() + ->maxLength(255), + + FileUpload::make('path') + ->label('Выберите файл') + ->helperText('Допустимы: PDF, DOCX, XLSX, PPTX, ZIP') + ->hint('Макс. размер 500KB') + ->required() + ->acceptedFileTypes([ + 'application/pdf', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'application/zip' + ]) + ->maxSize(512000) + ->disk('public') + ->directory('files') + ->downloadable() + ->preserveFilenames() + ->afterStateUpdated(function ($set, $state) { + $set('expansion', $state?->getClientOriginalExtension()); + $set('size', ByteConverter::bytesToHuman($state?->getSize())); + }), + ]) + ->maxItems(10) + ->collapsible() + ->itemLabel(fn (array $state): string => $state['title'] ?? 'Новый файл'), + ]), + + // Карточка персоны Builder\Block::make('person') - ->label('Персона') + ->icon('heroicon-o-user') + ->label('Карточка сотрудника') ->schema([ TextInput::make('name') - ->label('Имя') + ->label('ФИО') + ->placeholder('Иванов Иван Иванович') ->required() ->maxLength(255), + FileUpload::make('photo') ->label('Фотография') + ->hint('Оптимальный размер 500x500px') + ->helperText('Автоматическая конвертация в WebP') ->image() ->optimize('webp') ->resize(50) ->disk('public') - ->directory('images') - ->imageEditor(), - Forms\Components\Repeater::make('info')->schema([ + ->directory('personnel') + ->imageEditor() + ->required(), + + Repeater::make('info') + ->label('Характеристики') + ->hint('Добавьте 3-5 ключевых пунктов') + ->schema([ TextInput::make('column') - ->label('Название колонки') + ->label('Параметр') + ->placeholder('Стаж работы') ->required() - ->maxLength(255), - Forms\Components\Textarea::make('content') - ->label('Содержание') + ->maxLength(100), + + Textarea::make('content') + ->label('Значение') + ->placeholder('10 лет') ->required() - ->maxLength(1000), - ])->minItems(1)->label('Информация о персоне'), + ->maxLength(500), + ]) + ->minItems(1) + ->maxItems(10) + ->collapsible() + ->itemLabel(fn (array $state): string => $state['column'] ?? 'Новый параметр'), ]), + + // Этапы Builder\Block::make('stepper') - ->label('Строитель этапов') + ->icon('heroicon-o-list-bullet') + ->label('Пошаговый процесс') ->schema([ TextInput::make('step_name') - ->label('Название шага') + ->label('Название процесса') + ->placeholder('Процесс согласования') + ->required() + ->maxLength(100), + + Repeater::make('steps') + ->label('Этапы') + ->hint('Добавьте последовательные шаги') + ->schema([ + TextInput::make('title') + ->label('Шаг') + ->placeholder('1. Подготовка документов') + ->required() + ->maxLength(100), + + RichEditor::make('content') + ->label('Описание') + ->required() + ->maxLength(2000), + ]) + ->minItems(2) + ->collapsible() + ->itemLabel(fn (array $state): string => $state['title'] ?? 'Новый этап'), + ]), + + // Табы + Builder\Block::make('tabs') + ->icon('heroicon-o-rectangle-stack') + ->label('Табы') + ->schema([ + Repeater::make('tabs') + ->label('') + ->hint('Оптимально 3-5 вкладок') + ->schema([ + TextInput::make('title') + ->label('Название вкладки') + ->placeholder('Характеристики') + ->required() + ->maxLength(50), + + RichEditor::make('content') + ->label('Содержимое') + ->required(), + ]) + ->minItems(2) + ->maxItems(8) + ->collapsible() + ->itemLabel(fn (array $state): string => $state['title'] ?? 'Новая вкладка'), + ]), + + // Слайдер изображений + Builder\Block::make('images') + ->icon('heroicon-o-photo') + ->label('Галерея изображений') + ->schema([ + FileUpload::make('url') + ->label('Изображения') + ->hint('Оптимально 3-5 изображений') + ->helperText('Поддерживаются JPG, PNG, WEBP') + ->image() + ->multiple() + ->reorderable() + ->minFiles(1) + ->maxFiles(10) + ->disk('public') + ->directory('gallery') + ->imageEditor() + ->required(), + + TextInput::make('alt') + ->label('Описание для SEO') + ->placeholder('Наш офис в Москве') + ->hint('Краткое описание изображения') + ->maxLength(255), + ]), + + // Одиночное изображение + Builder\Block::make('image') + ->icon('heroicon-o-photo') + ->label('Изображение') + ->schema([ + FileUpload::make('url') + ->label('Выберите изображение') + ->helperText('Рекомендуемое соотношение 16:9') + ->image() + ->disk('public') + ->directory('images') + ->imageEditor() + ->required(), + + TextInput::make('alt') + ->label('ALT-текст') + ->placeholder('Описание изображения') ->required() ->maxLength(255), - Forms\Components\Repeater::make('steps')->schema([ - TextInput::make('title') - ->required() - ->maxLength(255)->columnSpanFull(), - RichEditor::make('content')->required(), - ])->minItems(1), ]), - TabBuilderItem::getItem(), - Builder\Block::make('images') - ->schema([ - FileUpload::make('url') - ->label('Изображение(-я)') - ->image() - ->multiple() - ->reorderable() - ->maxFiles(5) - ->disk('public') - ->directory('images') - ->imageEditor() - ->required(), - TextInput::make('alt') - ->label('Описание') - ->placeholder('Необязяательно') - ])->label('Слайдер изображений'), - Builder\Block::make('image') - ->schema([ - FileUpload::make('url') - ->label('Изображение(-я)') - ->image() - ->multiple() - ->reorderable() - ->maxFiles(5) - ->disk('public') - ->directory('images') - ->imageEditor() - ->required(), - TextInput::make('alt') - ->label('Описание') - ->placeholder('Необязяательно') - ])->label('Изображение'), + + // Видео Builder\Block::make('video') - ->label('Видео (Не стабильно)') + ->icon('heroicon-o-film') + ->label('Видео') ->schema([ - TextInput::make('mime')->readOnly(), + TextInput::make('mime') + ->label('Формат') + ->readOnly(), + TextInput::make('title') + ->label('Название видео') + ->placeholder('Обзор продукта') ->required() - ->maxLength(255) - ->autofocus(), + ->maxLength(255), + FileUpload::make('path') + ->label('Видеофайл') + ->hint('MP4, WebM, до 50MB') + ->helperText('Рекомендуемое разрешение 1080p') ->required() ->acceptedFileTypes([ 'video/mp4', - 'video/quicktime', - 'video/x-msvideo', - 'video/x-ms-wmv', - 'video/avi', 'video/webm', 'video/ogg', - 'video/3gpp', - 'video/3gpp2', - 'video/x-m4v', ]) + ->maxSize(51200) ->disk('public') - ->directory('videos') - ->afterStateUpdated(fn (callable $set, $state) => $set('mime', $state?->getMimeType())), + ->directory('videos'), ]), + + // Список новостей Builder\Block::make('postsList') + ->icon('heroicon-o-newspaper') + ->label('Лента новостей') ->schema([ - Forms\Components\Grid::make(2)->schema([ - TextInput::make('count') - ->label('Количество запией') - ->integer(), - Select::make('category') - ->options(Category::all()->pluck('title', 'id')) - ]), - ])->label('Список новостей'), + Grid::make(2) + ->schema([ + TextInput::make('count') + ->label('Количество') + ->numeric() + ->minValue(1) + ->maxValue(20) + ->default(5) + ->required(), + + Select::make('category') + ->label('Категория') + ->options(Category::all()->pluck('title', 'id')) + ->searchable() + ->placeholder('Все категории'), + ]), + ]), + + // Отдельная новость Builder\Block::make('postItem') + ->icon('heroicon-o-document-text') + ->label('Конкретная новость') ->schema([ Select::make('post') - ->options(Post::query()->where('status', PostStatus::PUBLISHED)->pluck('title', 'id')) + ->label('Выберите новость') + ->options(Post::published()->pluck('title', 'id')) ->searchable() - ->required(), - ])->label('Новость'), + ->required() + ->placeholder('Начните вводить название'), + ]), + + // Страница Builder\Block::make('pageItem') + ->icon('heroicon-o-document') + ->label('Ссылка на страницу') ->schema([ Select::make('page') - ->options(Page::query()->where('title', '!=' , null)->where('is_visible', true)->pluck('title', 'id')) + ->label('Страница') + ->options(Page::visible()->pluck('title', 'id')) ->searchable() ->required(), - ])->label('Страница'), + ]), + + // Форма Builder\Block::make('customForm') + ->icon('heroicon-o-clipboard-document') + ->label('Форма') ->schema([ Select::make('form') - ->options(CustomForm::query()->where('status', CustomFormStatus::PUBLISHED)->pluck('title', 'form_id')) + ->label('Выберите форму') + ->options(CustomForm::published()->pluck('title', 'form_id')) ->searchable() ->required(), - ])->label('Форма'), + ]), + + // Ресурсы Builder\Block::make('pageResourceList') + ->icon('heroicon-o-archive-box') + ->label('Список ресурсов') ->schema([ Select::make('resource') - ->options(PageReferenceList::query()->where('is_active', true)->pluck('title', 'slug')) + ->label('Ресурс') + ->options(PageReferenceList::active()->pluck('title', 'slug')) ->searchable() ->required(), - ])->label('Ресурсы'), + ]), ]) - ->collapsed() - ->blockNumbers(false) - ->collapsible() - ->blockPickerColumns(3) - ->blockPickerWidth('2xl') - ->addActionLabel('Добавить новый блок'); + ->collapsed() + ->blockNumbers(false) + ->collapsible() + ->blockPickerColumns(3) + ->blockPickerWidth('2xl') + ->addActionLabel('Добавить блок') + ->addBetweenActionLabel('Вставить блок между') + ->cloneActionLabel('Клонировать блок'); } diff --git a/app/Filament/Components/Forms/ItemForm/Defaults/TabBuilderItem.php b/app/Filament/Components/Forms/ItemForm/Defaults/TabBuilderItem.php index 2b9d07d..8a5901e 100644 --- a/app/Filament/Components/Forms/ItemForm/Defaults/TabBuilderItem.php +++ b/app/Filament/Components/Forms/ItemForm/Defaults/TabBuilderItem.php @@ -6,10 +6,12 @@ use App\Enums\CustomFormStatus; use App\Enums\PostStatus; use App\Helpers\ByteConverter; use App\Models\Category; +use App\Models\ContactWidget; use App\Models\CustomForm; use App\Models\Page; use App\Models\PageReferenceList; use App\Models\Post; +use App\Models\Slider; use Filament\Forms; use Filament\Forms\Components\Builder; use Filament\Forms\Components\FileUpload; @@ -29,210 +31,356 @@ class TabBuilderItem { public static function getItem() { - return - Builder\Block::make('tabs') - ->label('Вкладки') - ->schema([ - Forms\Components\Repeater::make('tab')->schema([ - TextInput::make('title') + return Builder::make('content') + ->label('Содержимое вкладки') + ->blocks([ + Builder\Block::make('heading') + ->label('Заголовок') + ->icon('heroicon-o-hashtag') + ->schema([ + TextInput::make('id') + ->hidden() + ->integer() + ->default(rand(2335235, 324634264263426)), + TextInput::make('content') + ->label('Текст заголовка') + ->placeholder('Введите текст заголовка') + ->helperText('Основной заголовок раздела') + ->live(onBlur: true) ->required() - ->maxLength(255)->columnSpanFull(), - Builder::make('content')->label('')->blocks([ - Builder\Block::make('heading')->label('Заголовок') - ->schema([ - TextInput::make('id')->hidden()->integer()->default(rand(2335235,324634264263426)), - TextInput::make('content') - ->label('') - ->live(onBlur: true) - ->afterStateUpdated(function (string $operation, string $state, Forms\Set $set, $get) { - }), - ]), - Builder\Block::make('paragraph') - ->schema([ - TinyEditor::make('content') - ->label('') - ->profile('test') - ->required(), - ])->label('Текст'), - Builder\Block::make('files') - ->label('Файл(-ы)') - ->schema([ - Forms\Components\Repeater::make('file')->schema([ - Hidden::make('expansion')->required(), - Hidden::make('size')->required(), - TextInput::make('title') - ->required() - ->maxLength(255) - ->autofocus(), - FileUpload::make('path') - ->required() - ->getUploadedFileNameForStorageUsing( - fn (TemporaryUploadedFile $file): string => - str(Str::slug(pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME) . '-' . Carbon::now()->timestamp ) . '.' . $file->getClientOriginalExtension()) - ) - ->acceptedFileTypes([ - 'application/pdf', - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - 'application/vnd.openxmlformats-officedocument.presentationml.presentation', - 'application/zip' - ]) - ->maxSize(512000) - ->disk('public') - ->directory('files') - ->downloadable() - ->afterStateUpdated(function ($set, $state) { - $set('expansion', $state?->getClientOriginalExtension()); - $set('size', ByteConverter::bytesToHuman($state?->getSize())); - }) - ->visibility('public') - ]), - ]), - Builder\Block::make('person') - ->label('Персона') - ->schema([ - TextInput::make('name') - ->label('Имя') - ->required() - ->maxLength(255), - FileUpload::make('photo') - ->label('Фотография') - ->image() - ->optimize('webp') - ->resize(50) - ->disk('public') - ->directory('images') - ->imageEditor(), - Forms\Components\Repeater::make('info')->schema([ - TextInput::make('column') - ->label('Название колонки') - ->required() - ->maxLength(255), - Forms\Components\Textarea::make('content') - ->label('Содержание') - ->required() - ->maxLength(1000), - ])->minItems(1)->label('Информация о персоне'), - ]), - Builder\Block::make('stepper') - ->label('Строитель этапов') - ->schema([ - TextInput::make('step_name') - ->label('Название шага') - ->required() - ->maxLength(255), - Forms\Components\Repeater::make('steps')->schema([ - TextInput::make('title') - ->required() - ->maxLength(255)->columnSpanFull(), - RichEditor::make('content')->required(), - ])->minItems(1), - ]), - Builder\Block::make('images') - ->schema([ - FileUpload::make('url') - ->label('Изображение(-я)') - ->image() - ->multiple() - ->reorderable() - ->maxFiles(5) - ->disk('public') - ->directory('images') - ->imageEditor() - ->required(), - TextInput::make('alt') - ->label('Описание') - ->placeholder('Необязяательно') - ])->label('Слайдер изображений'), - Builder\Block::make('image') - ->schema([ - FileUpload::make('url') - ->label('Изображение(-я)') - ->image() - ->multiple() - ->reorderable() - ->maxFiles(5) - ->disk('public') - ->directory('images') - ->imageEditor() - ->required(), - TextInput::make('alt') - ->label('Описание') - ->placeholder('Необязяательно') - ])->label('Изображение'), - Builder\Block::make('video') - ->label('Видео (Не стабильно)') - ->schema([ - TextInput::make('mime')->readOnly(), - TextInput::make('title') - ->required() - ->maxLength(255) - ->autofocus(), - FileUpload::make('path') - ->required() - ->acceptedFileTypes([ - 'video/mp4', - 'video/quicktime', - 'video/x-msvideo', - 'video/x-ms-wmv', - 'video/avi', - 'video/webm', - 'video/ogg', - 'video/3gpp', - 'video/3gpp2', - 'video/x-m4v', - ]) - ->disk('public') - ->directory('videos') - ->afterStateUpdated(fn (callable $set, $state) => $set('mime', $state?->getMimeType())), - ]), - Builder\Block::make('postsList') - ->schema([ - Forms\Components\Grid::make(2)->schema([ - TextInput::make('count') - ->label('Количество запией') - ->integer(), - Select::make('category') - ->options(Category::all()->pluck('title', 'id')) - ]), - ])->label('Список новостей'), - Builder\Block::make('postItem') - ->schema([ - Select::make('post') - ->options(Post::query()->where('status', PostStatus::PUBLISHED)->pluck('title', 'id')) - ->searchable() - ->required(), - ])->label('Новость'), - Builder\Block::make('pageItem') - ->schema([ - Select::make('page') - ->options(Page::query()->where('title', '!=' , null)->where('is_visible', true)->pluck('title', 'id')) - ->searchable() - ->required(), - ])->label('Страница'), - Builder\Block::make('customForm') - ->schema([ - Select::make('form') - ->options(CustomForm::query()->where('status', CustomFormStatus::PUBLISHED)->pluck('title', 'form_id')) - ->searchable() - ->required(), - ])->label('Форма'), - Builder\Block::make('pageResourceList') - ->schema([ - Select::make('resource') - ->options(PageReferenceList::query()->where('is_active', true)->pluck('title', 'slug')) - ->searchable() - ->required(), - ])->label('Ресурсы'), - ]) - ->collapsed() - ->blockNumbers(false) + ->maxLength(255), + ]), + + Builder\Block::make('paragraph') + ->label('Текст') + ->icon('heroicon-o-document-text') + ->schema([ + Toggle::make('seo_active') + ->label('Использовать блок как SEO-текст') + ->helperText('Этот текст будет использоваться для SEO-оптимизации') + ->live(onBlur: true) + ->required() + ->disabled(function ($state, Forms\Get $get) { + $data = $get('../../'); + return self::findSeoActive($data) && !$state; + }) + ->dehydrated(), + TinyEditor::make('content') + ->label('Текст') + ->placeholder('Начните вводить текст...') + ->profile('test') + ->required() + ->helperText('Основное текстовое содержимое блока'), + ]), + + Builder\Block::make('files') + ->label('Файлы') + ->icon('heroicon-o-paper-clip') + ->schema([ + Forms\Components\Repeater::make('file') + ->label('Файлы') + ->helperText('Загрузите один или несколько файлов') + ->schema([ + Hidden::make('expansion')->required(), + Hidden::make('size')->required(), + TextInput::make('title') + ->label('Название файла') + ->placeholder('Введите название файла') + ->required() + ->maxLength(255) + ->autofocus() + ->helperText('Это название будет отображаться пользователям'), + FileUpload::make('path') + ->label('Файл') + ->required() + ->helperText('Поддерживаются PDF, Word, Excel, PowerPoint и ZIP файлы (макс. 500KB)') + ->getUploadedFileNameForStorageUsing( + fn (TemporaryUploadedFile $file): string => + str(Str::slug(pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME) . '-' . Carbon::now()->timestamp) . '.' . $file->getClientOriginalExtension()) + ) + ->acceptedFileTypes([ + 'application/pdf', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'application/zip' + ]) + ->maxSize(512000) + ->disk('public') + ->directory('files') + ->downloadable() + ->afterStateUpdated(function ($set, $state) { + $set('expansion', $state?->getClientOriginalExtension()); + $set('size', ByteConverter::bytesToHuman($state?->getSize())); + }) + ->visibility('public') + ->preserveFilenames() + ]) + ->itemLabel(fn (array $state): ?string => $state['title'] ?? null) ->collapsible() - ->blockPickerColumns(3) - ->blockPickerWidth('2xl') - ->addActionLabel('Добавить новый блок'), - ])->minItems(1), - ]); + ->cloneable() + ->grid(2), + ]), + + Builder\Block::make('person') + ->label('Персона') + ->icon('heroicon-o-user') + ->schema([ + TextInput::make('name') + ->label('Имя персоны') + ->placeholder('Введите имя') + ->required() + ->maxLength(255) + ->helperText('Полное имя персоны'), + FileUpload::make('photo') + ->label('Фотография') + ->image() + ->helperText('Рекомендуемый формат: WebP') + ->optimize('webp') + ->resize(50) + ->disk('public') + ->directory('images') + ->imageEditor() + ->required() + ->downloadable() + ->openable(), + Forms\Components\Repeater::make('info') + ->label('Дополнительная информация') + ->helperText('Добавьте характеристики персоны') + ->schema([ + TextInput::make('column') + ->label('Название характеристики') + ->placeholder('Например: Должность') + ->required() + ->maxLength(255), + Forms\Components\Textarea::make('content') + ->label('Значение') + ->placeholder('Например: Главный инженер') + ->required() + ->maxLength(1000) + ->columnSpanFull(), + ]) + ->minItems(1) + ->grid(2) + ->collapsible() + ->cloneable() + ->itemLabel(fn (array $state): ?string => $state['column'] ?? null), + ]), + + Builder\Block::make('stepper') + ->label('Этапы') + ->icon('heroicon-o-list-bullet') + ->schema([ + TextInput::make('step_name') + ->label('Название процесса') + ->placeholder('Например: Процесс оформления') + ->required() + ->maxLength(255) + ->helperText('Общее название для всех шагов'), + Forms\Components\Repeater::make('steps') + ->label('Шаги') + ->helperText('Добавьте шаги процесса') + ->schema([ + TextInput::make('title') + ->label('Название шага') + ->placeholder('Например: Шаг 1') + ->required() + ->maxLength(255) + ->columnSpanFull(), + RichEditor::make('content') + ->label('Описание шага') + ->required() + ->toolbarButtons([ + 'bold', + 'italic', + 'link', + 'orderedList', + 'bulletList', + ]), + ]) + ->minItems(1) + ->collapsible() + ->cloneable() + ->itemLabel(fn (array $state): ?string => $state['title'] ?? null), + ]), + + Builder\Block::make('images') + ->label('Слайдер изображений') + ->icon('heroicon-o-photo') + ->schema([ + FileUpload::make('url') + ->label('Изображения') + ->image() + ->multiple() + ->reorderable() + ->maxFiles(5) + ->disk('public') + ->directory('images') + ->imageEditor() + ->required() + ->helperText('Максимум 5 изображений. Можно перетаскивать для изменения порядка'), + TextInput::make('alt') + ->label('Описание изображений') + ->placeholder('Необязательно') + ->helperText('Используется для SEO и доступности'), + ]), + + Builder\Block::make('image') + ->label('Изображение') + ->icon('heroicon-o-photo') + ->schema([ + FileUpload::make('url') + ->label('Изображение') + ->image() + ->multiple() + ->reorderable() + ->maxFiles(5) + ->disk('public') + ->directory('images') + ->imageEditor() + ->required() + ->helperText('Можно загрузить до 5 изображений'), + TextInput::make('alt') + ->label('Альтернативный текст') + ->placeholder('Необязательно') + ->helperText('Описание изображения для SEO'), + ]), + + Builder\Block::make('video') + ->label('Видео') + ->icon('heroicon-o-film') + ->schema([ + TextInput::make('mime') + ->label('Тип видео') + ->readOnly() + ->helperText('Определяется автоматически'), + TextInput::make('title') + ->label('Название видео') + ->placeholder('Введите название видео') + ->required() + ->maxLength(255) + ->autofocus() + ->helperText('Это название будет отображаться перед видео'), + FileUpload::make('path') + ->label('Видеофайл') + ->required() + ->acceptedFileTypes([ + 'video/mp4', + 'video/quicktime', + 'video/x-msvideo', + 'video/x-ms-wmv', + 'video/avi', + 'video/webm', + 'video/ogg', + 'video/3gpp', + 'video/3gpp2', + 'video/x-m4v', + ]) + ->disk('public') + ->directory('videos') + ->helperText('Поддерживаются популярные видеоформаты (MP4, MOV, AVI и др.)') + ->afterStateUpdated(fn (callable $set, $state) => $set('mime', $state?->getMimeType())), + ]), + + Builder\Block::make('postsList') + ->label('Список новостей') + ->icon('heroicon-o-newspaper') + ->schema([ + Forms\Components\Grid::make(2) + ->schema([ + TextInput::make('count') + ->label('Количество записей') + ->integer() + ->minValue(1) + ->maxValue(20) + ->default(5) + ->helperText('От 1 до 20 записей'), + Select::make('category') + ->label('Категория') + ->options(Category::all()->pluck('title', 'id')) + ->searchable() + ->helperText('Выберите категорию или оставьте пустым для всех'), + ]), + ]), + + Builder\Block::make('postItem') + ->label('Конкретная новость') + ->icon('heroicon-o-document-text') + ->schema([ + Select::make('post') + ->label('Новость') + ->options(Post::query()->where('status', PostStatus::PUBLISHED)->pluck('title', 'id')) + ->searchable() + ->required() + ->helperText('Выберите опубликованную новость'), + ]), + + Builder\Block::make('pageItem') + ->label('Конкретная страница') + ->icon('heroicon-o-document') + ->schema([ + Select::make('page') + ->label('Страница') + ->options(Page::query()->where('title', '!=', null)->where('is_visible', true)->pluck('title', 'id')) + ->searchable() + ->required() + ->helperText('Выберите видимую страницу'), + ]), + + Builder\Block::make('customForm') + ->label('Пользовательская форма') + ->icon('heroicon-o-clipboard-document-list') + ->schema([ + Select::make('form') + ->label('Форма') + ->options(CustomForm::query()->where('status', CustomFormStatus::PUBLISHED)->pluck('title', 'form_id')) + ->searchable() + ->required() + ->helperText('Выберите опубликованную форму'), + ]), + + Builder\Block::make('pageResourceList') + ->label('Ресурсы') + ->icon('heroicon-o-archive-box') + ->schema([ + Select::make('resource') + ->label('Ресурс') + ->options(PageReferenceList::query()->where('is_active', true)->pluck('title', 'slug')) + ->searchable() + ->required() + ->helperText('Выберите активный ресурс'), + ]), + + Builder\Block::make('contact') + ->label('Контакты') + ->icon('heroicon-o-phone') + ->schema([ + Select::make('contact') + ->label('Виджет контактов') + ->options(ContactWidget::query()->where('is_active', true)->pluck('title', 'slug')) + ->searchable() + ->required() + ->helperText('Выберите активный виджет контактов'), + ]), + + Builder\Block::make('slider') + ->label('Слайдер') + ->icon('heroicon-o-presentation-chart-line') + ->schema([ + Select::make('slider') + ->label('Слайдер') + ->options(Slider::query()->whereHas('slides')->where('is_active', true)->pluck('title', 'slug')) + ->searchable() + ->required() + ->helperText('Выберите активный слайдер с изображениями'), + ]), + ]) + ->collapsed() + ->blockPickerColumns(3) + ->blockPickerWidth('2xl') + ->blockNumbers(false) + ->collapsible() + ->addActionLabel('Добавить блок в вкладку'); } diff --git a/app/Filament/Components/Forms/ItemForm/Pages/ContentBuilderItem.php b/app/Filament/Components/Forms/ItemForm/Pages/ContentBuilderItem.php index 17646e5..8db9cf9 100644 --- a/app/Filament/Components/Forms/ItemForm/Pages/ContentBuilderItem.php +++ b/app/Filament/Components/Forms/ItemForm/Pages/ContentBuilderItem.php @@ -4,6 +4,22 @@ namespace App\Filament\Components\Forms\ItemForm\Pages; use App\Enums\CustomFormStatus; use App\Enums\PostStatus; +use App\Filament\Components\Forms\ItemForm\Blocks\ContactBlock; +use App\Filament\Components\Forms\ItemForm\Blocks\CustomFormBlock; +use App\Filament\Components\Forms\ItemForm\Blocks\FilesBlock; +use App\Filament\Components\Forms\ItemForm\Blocks\HeadingBlock; +use App\Filament\Components\Forms\ItemForm\Blocks\ImagesBlock; +use App\Filament\Components\Forms\ItemForm\Blocks\PageItemBlock; +use App\Filament\Components\Forms\ItemForm\Blocks\PageResourceListBlock; +use App\Filament\Components\Forms\ItemForm\Blocks\ParagraphBlock; +use App\Filament\Components\Forms\ItemForm\Blocks\PersonBlock; +use App\Filament\Components\Forms\ItemForm\Blocks\PostItemBlock; +use App\Filament\Components\Forms\ItemForm\Blocks\PostListBlock; +use App\Filament\Components\Forms\ItemForm\Blocks\SliderBlock; +use App\Filament\Components\Forms\ItemForm\Blocks\StepperBlock; +use App\Filament\Components\Forms\ItemForm\Blocks\TabBlock; +use App\Filament\Components\Forms\ItemForm\Blocks\VideoBlock; +use App\Filament\Components\Forms\ItemForm\Defaults\TabBuilderItem; use App\Helpers\ByteConverter; use App\Models\Category; use App\Models\ContactWidget; @@ -14,6 +30,7 @@ use App\Models\Post; use App\Models\Slider; use Filament\Forms; use Filament\Forms\Components\Builder; +use Filament\Forms\Components\Fieldset; use Filament\Forms\Components\FileUpload; use Filament\Forms\Components\Hidden; use Filament\Forms\Components\RichEditor; @@ -29,407 +46,99 @@ use Mohamedsabil83\FilamentFormsTinyeditor\Components\TinyEditor; class ContentBuilderItem { - private static function findSeoActive(array $data) : bool + public static function getItem(string $name): Builder { - $bool = false; + return Builder::make($name) + ->label('') + ->blocks([ + Builder\Block::make('heading') + ->label('Заголовок') + ->icon('heroicon-o-hashtag') + ->schema(HeadingBlock::schema()), - foreach ($data as $item) { - if ($item['type'] !== 'paragraph') { - continue; - } - if ($item['data']['seo_active'] === true) { - $bool = true; - break; - } - } - return $bool; - } - public static function getItem(string $name) - { - return - Builder::make($name)->label('')->blocks([ - Builder\Block::make('heading')->label('Заголовок') - ->schema([ - TextInput::make('id')->hidden()->integer()->default(rand(2335235,324634264263426)), - TextInput::make('content') - ->label('') - ->live(onBlur: true) - ->afterStateUpdated(function (string $operation, string $state, Forms\Set $set, $get) { - }), - ]), Builder\Block::make('paragraph') - ->schema([ - Toggle::make('seo_active')->label('Использовать блок как seo') - ->live(onBlur: true) - ->required() - ->disabled(function ($state, Forms\Get $get) { - $data = $get('../../'); - return self::findSeoActive($data) && !$state; - }) - ->dehydrated(), - TinyEditor::make('content') - ->label('') - ->profile('test') - ->required(), - ])->label('Текст'), + ->label('Текст') + ->icon('heroicon-o-document-text') + ->schema(ParagraphBlock::schema()), + Builder\Block::make('files') - ->label('Файл(-ы)') - ->schema([ - Forms\Components\Repeater::make('file')->schema([ - Hidden::make('expansion')->required(), - Hidden::make('size')->required(), - TextInput::make('title') - ->required() - ->maxLength(255) - ->autofocus(), - FileUpload::make('path') - ->required() - ->getUploadedFileNameForStorageUsing( - fn (TemporaryUploadedFile $file): string => - str(Str::slug(pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME) . '-' . Carbon::now()->timestamp ) . '.' . $file->getClientOriginalExtension()) - ) - ->acceptedFileTypes([ - 'application/pdf', - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - 'application/vnd.openxmlformats-officedocument.presentationml.presentation', - 'application/zip' - ]) - ->maxSize(512000) - ->disk('public') - ->directory('files') - ->downloadable() - ->afterStateUpdated(function ($set, $state) { - $set('expansion', $state?->getClientOriginalExtension()); - $set('size', ByteConverter::bytesToHuman($state?->getSize())); - }) - ->visibility('public') - ]), - ]), + ->label('Файлы') + ->icon('heroicon-o-paper-clip') + ->schema(FilesBlock::schema()), + Builder\Block::make('person') ->label('Персона') - ->schema([ - TextInput::make('name') - ->label('Имя') - ->required() - ->maxLength(255), - FileUpload::make('photo') - ->label('Фотография') - ->image() - ->optimize('webp') - ->resize(50) - ->disk('public') - ->directory('images') - ->imageEditor(), - Forms\Components\Repeater::make('info')->schema([ - TextInput::make('column') - ->label('Название колонки') - ->required() - ->maxLength(255), - Forms\Components\Textarea::make('content') - ->label('Содержание') - ->required() - ->maxLength(1000), - ])->minItems(1)->label('Информация о персоне'), - ]), + ->icon('heroicon-o-user') + ->schema(PersonBlock::schema()), + Builder\Block::make('stepper') - ->label('Строитель этапов') - ->schema([ - TextInput::make('step_name') - ->label('Название шага') - ->required() - ->maxLength(255), - Forms\Components\Repeater::make('steps')->schema([ - TextInput::make('title') - ->required() - ->maxLength(255)->columnSpanFull(), - RichEditor::make('content')->required(), - ])->minItems(1), - ]), + ->label('Этапы') + ->icon('heroicon-o-list-bullet') + ->schema(StepperBlock::schema()), + Builder\Block::make('tabs') ->label('Вкладки') - ->schema([ - Forms\Components\Repeater::make('tab')->schema([ - TextInput::make('title') - ->required() - ->maxLength(255)->columnSpanFull(), - \Filament\Forms\Components\Builder::make('content')->label('')->blocks([ - Builder\Block::make('heading')->label('Заголовок') - ->schema([ - TextInput::make('id')->hidden()->integer()->default(rand(2335235,324634264263426)), - TextInput::make('content') - ->label('') - ->live(onBlur: true) - ->afterStateUpdated(function (string $operation, string $state, Forms\Set $set, $get) { - }), - ]), - Builder\Block::make('paragraph') - ->schema([ - RichEditor::make('content') - ->toolbarButtons([ - 'blockquote', - 'bold', - 'bulletList', - 'italic', - 'link', - 'orderedList', - 'redo', - 'strike', - 'underline', - 'undo', - ]) - ->label(''), - ])->label('Текст'), - Builder\Block::make('files') - ->schema([ - Forms\Components\Repeater::make('file')->schema([ - TextInput::make('title') - ->required() - ->maxLength(255) - ->autofocus(), - FileUpload::make('path') - ->required() - ->acceptedFileTypes([ - 'application/pdf', - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - 'application/vnd.openxmlformats-officedocument.presentationml.presentation', - 'application/zip' - ]) - ->maxSize(512000) - ->disk('public') - ->directory('files') - ->downloadable() - ->visibility('public') - ]), - ]), - Builder\Block::make('person') - ->schema([ - TextInput::make('name') - ->label('Имя') - ->required() - ->maxLength(255), - FileUpload::make('photo') - ->label('Фотография') - ->image() - ->disk('public') - ->directory('images') - ->imageEditor(), - Forms\Components\Repeater::make('info')->schema([ - Forms\Components\Grid::make(2)->schema([ - TextInput::make('column') - ->required() - ->maxLength(255), - TextInput::make('content') - ->required() - ->maxLength(255), - ]), - ])->minItems(1), - ]), - Builder\Block::make('stepper') - ->schema([ - TextInput::make('step_name') - ->label('Название шага') - ->required() - ->maxLength(255), - Forms\Components\Repeater::make('steps')->schema([ - TextInput::make('title') - ->required() - ->maxLength(255)->columnSpanFull(), - RichEditor::make('content')->required(), - ])->minItems(1), - ]), - Builder\Block::make('images') - ->schema([ - FileUpload::make('url') - ->label('Изображение(-я)') - ->image() - ->multiple() - ->reorderable() - ->maxFiles(5) - ->disk('public') - ->directory('images') - ->imageEditor() - ->required(), - TextInput::make('alt') - ->label('Описание') - ->placeholder('Необязяательно') - ])->label('Слайдер изображений'), - Builder\Block::make('image') - ->schema([ - FileUpload::make('url') - ->label('Изображение(-я)') - ->image() - ->multiple() - ->reorderable() - ->maxFiles(5) - ->disk('public') - ->directory('images') - ->imageEditor() - ->required(), - TextInput::make('alt') - ->label('Описание') - ->placeholder('Необязяательно') - ])->label('Изображение'), - Builder\Block::make('video') - ->schema([ - TextInput::make('mime')->readOnly(), - TextInput::make('title') - ->required() - ->maxLength(255) - ->autofocus(), - FileUpload::make('path') - ->required() - ->acceptedFileTypes([ - 'video/mp4', - 'video/quicktime', - 'video/x-msvideo', - 'video/x-ms-wmv', - 'video/avi', - 'video/webm', - 'video/ogg', - 'video/3gpp', - 'video/3gpp2', - 'video/x-m4v', - ]) - ->disk('public') - ->directory('videos') - ->afterStateUpdated(fn (callable $set, $state) => $set('mime', $state?->getMimeType())), - ]), - Builder\Block::make('postsList') - ->schema([ - Forms\Components\Grid::make(2)->schema([ - TextInput::make('count') - ->label('Количество запией') - ->integer(), - Select::make('category') - ->options(Category::all()->pluck('title', 'id')) - ]), - ])->label('Список новостей'), - ]) - ->collapsed() - ->blockNumbers(false) - ->collapsible() - ->addActionLabel('Добавить новый блок'), - ])->minItems(1), - ]), + ->icon('heroicon-o-rectangle-stack') + ->schema(TabBlock::schema()), + Builder\Block::make('images') - ->schema([ - FileUpload::make('url') - ->label('Изображение(-я)') - ->image() - ->multiple() - ->reorderable() - ->maxFiles(5) - ->disk('public') - ->directory('images') - ->imageEditor() - ->required(), - TextInput::make('alt') - ->label('Описание') - ->placeholder('Необязяательно') - ])->label('Слайдер изображений'), + ->label('Слайдер изображений') + ->icon('heroicon-o-photo') + ->schema(ImagesBlock::schema()), + Builder\Block::make('image') - ->schema([ - FileUpload::make('url') - ->label('Изображение(-я)') - ->image() - ->multiple() - ->reorderable() - ->maxFiles(5) - ->disk('public') - ->directory('images') - ->imageEditor() - ->required(), - TextInput::make('alt') - ->label('Описание') - ->placeholder('Необязяательно') - ])->label('Изображение'), + ->label('Изображение') + ->icon('heroicon-o-photo') + ->schema(ImagesBlock::schema()), + Builder\Block::make('video') - ->label('Видео (Не стабильно)') - ->schema([ - TextInput::make('mime')->readOnly(), - TextInput::make('title') - ->required() - ->maxLength(255) - ->autofocus(), - FileUpload::make('path') - ->required() - ->acceptedFileTypes([ - 'video/mp4', - 'video/quicktime', - 'video/x-msvideo', - 'video/x-ms-wmv', - 'video/avi', - 'video/webm', - 'video/ogg', - 'video/3gpp', - 'video/3gpp2', - 'video/x-m4v', - ]) - ->disk('public') - ->directory('videos') - ->afterStateUpdated(fn (callable $set, $state) => $set('mime', $state?->getMimeType())), - ]), + ->label('Видео') + ->icon('heroicon-o-film') + ->schema(VideoBlock::schema()), + Builder\Block::make('postsList') - ->schema([ - Forms\Components\Grid::make(2)->schema([ - TextInput::make('count') - ->label('Количество запией') - ->integer(), - Select::make('category') - ->options(Category::all()->pluck('title', 'id')) - ]), - ])->label('Список новостей'), + ->label('Список новостей') + ->icon('heroicon-o-newspaper') + ->schema(PostListBlock::schema()), + Builder\Block::make('postItem') - ->schema([ - Select::make('post') - ->options(Post::query()->where('status', PostStatus::PUBLISHED)->pluck('title', 'id')) - ->searchable() - ->required(), - ])->label('Новость'), + ->label('Конкретная новость') + ->icon('heroicon-o-document-text') + ->schema(PostItemBlock::schema()), + Builder\Block::make('pageItem') - ->schema([ - Select::make('page') - ->options(Page::query()->where('title', '!=' , null)->where('is_visible', true)->pluck('title', 'id')) - ->searchable() - ->required(), - ])->label('Страница'), + ->label('Конкретная страница') + ->icon('heroicon-o-document') + ->schema(PageItemBlock::schema()), + Builder\Block::make('customForm') - ->schema([ - Select::make('form') - ->options(CustomForm::query()->where('status', CustomFormStatus::PUBLISHED)->pluck('title', 'form_id')) - ->searchable() - ->required(), - ])->label('Форма'), + ->label('Пользовательская форма') + ->icon('heroicon-o-clipboard-document-list') + ->schema(CustomFormBlock::schema()), + Builder\Block::make('pageResourceList') - ->schema([ - Select::make('resource') - ->options(PageReferenceList::query()->where('is_active', true)->pluck('title', 'slug')) - ->searchable() - ->required(), - ])->label('Ресурсы'), + ->label('Ресурсы') + ->icon('heroicon-o-archive-box') + ->schema(PageResourceListBlock::schema()), + Builder\Block::make('contact') - ->schema([ - Select::make('contact') - ->options(ContactWidget::query()->where('is_active', true)->pluck('title', 'slug')) - ->searchable() - ->required(), - ])->label('Контакты'), + ->label('Контакты') + ->icon('heroicon-o-phone') + ->schema(ContactBlock::schema()), + Builder\Block::make('slider') - ->schema([ - Select::make('slider') - ->options(Slider::query()->whereHas('slides')->where('is_active', true)->pluck('title', 'slug')) - ->searchable() - ->required(), - ])->label('Слайдеры'), + ->label('Слайдер') + ->icon('heroicon-o-presentation-chart-line') + ->schema(SliderBlock::schema()), ]) - ->collapsed() - ->blockNumbers(false) - ->collapsible() - ->blockPickerColumns(3) - ->blockPickerWidth('2xl') - ->addActionLabel('Добавить новый блок'); + ->collapsed() + ->blockNumbers(false) + ->collapsible() + ->blockPickerColumns(3) + ->blockPickerWidth('2xl') + ->addActionLabel('Добавить новый блок') + ->cloneable() + ->reorderableWithButtons(); } diff --git a/app/Filament/Components/Forms/PageForm.php b/app/Filament/Components/Forms/PageForm.php index 721a0b9..42e09b5 100644 --- a/app/Filament/Components/Forms/PageForm.php +++ b/app/Filament/Components/Forms/PageForm.php @@ -6,6 +6,7 @@ use App\Enums\CustomFormStatus; use App\Enums\PostStatus; use App\Filament\Components\Forms\ItemForm\Pages\ContentBuilderItem; use Filament\Forms; +use Filament\Forms\Components\Actions\Action; use Filament\Forms\Components\Section; use Filament\Forms\Components\Select; use Filament\Forms\Components\TextInput; @@ -22,69 +23,129 @@ class PageForm ->schema([ Section::make() ->schema([ - Forms\Components\Tabs::make('')->schema([ - Forms\Components\Tabs\Tab::make('Основная информация')->schema([ - Forms\Components\Grid::make(2)->schema([ - TextInput::make('title')->label('Заголовок')->required() - ->live(onBlur: true) - ->afterStateUpdated(function (string $operation, string|null $state, Forms\Set $set) { - $set('slug', Str::slug($state)); - $set('path', Str::slug($state)); - }), - TextInput::make('slug')->label('Текстовый идентификатор страницы')->unique(ignoreRecord: true)->required() - ->live(onBlur: true) - ->afterStateUpdated(function (string $operation, string $state, Forms\Set $set, $get) { - $set('path', Str::slug($state)); - }), - ]), - Select::make('sub_section_id')->label('Подраздел') - ->relationship('section', 'title') - ->createOptionForm([ - Forms\Components\TextInput::make('title')->label('Название подраздела')->required() - ->live(onBlur: true) - ->afterStateUpdated(function (string $operation, string $state, Forms\Set $set) { - $set('slug', Str::slug($state)); - }), - TextInput::make('slug')->label('Slug')->unique(ignoreRecord: true)->readOnly()->required(), + Forms\Components\Tabs::make('Настройки страницы') + ->persistTabInQueryString() + ->columnSpanFull() + ->tabs([ + Forms\Components\Tabs\Tab::make('Основная информация') + ->icon('heroicon-o-information-circle') + ->schema([ + Forms\Components\Grid::make(2) + ->schema([ + TextInput::make('title') + ->label('Заголовок страницы') + ->required() + ->maxLength(255) + ->placeholder('Введите название страницы') + ->helperText('Этот заголовок будет отображаться в заголовке страницы и в навигации') + ->live(onBlur: true) + ->afterStateUpdated(function (string $operation, string|null $state, Forms\Set $set) { + $set('slug', Str::slug($state)); + $set('path', Str::slug($state)); + }), + TextInput::make('slug') + ->label('URL-адрес страницы') + ->required() + ->unique(ignoreRecord: true) + ->maxLength(255) + ->helperText('Человеко-понятный URL для страницы') + ->placeholder('example-page') +// ->prefix(fn ($record) => url('/') . '/' . substr($record->path, 0, strrpos($record->path, '/'))) + ->live(onBlur: true) + ->afterStateUpdated(function (string $operation, string $state, Forms\Set $set, $get) { + $set('path', Str::slug($state)); + }) + ->suffixAction( + Action::make('copy') + ->icon('heroicon-s-clipboard-document-check') + ->action(function ($livewire, $state, $record) { + $livewire->js( + 'window.navigator.clipboard.writeText("'. url('/') . '/' . substr($record->path, 0, strrpos($record->path, '/')) . '/' . $state.'"); + $tooltip("'.__('Copied to clipboard').'", { timeout: 1500 });' + ); + })), + + ]), + Select::make('sub_section_id') + ->label('Родительский подраздел') + ->relationship('section', 'title') + ->preload() + ->searchable() + ->placeholder('Выберите подраздел') + ->helperText('Выберите раздел, к которому принадлежит эта страница') + ->createOptionForm([ + Forms\Components\Grid::make(2) + ->schema([ + Forms\Components\TextInput::make('title') + ->label('Название подраздела') + ->required() + ->maxLength(255) + ->placeholder('Введите название подраздела') + ->live(onBlur: true) + ->afterStateUpdated(function (string $operation, string $state, Forms\Set $set) { + $set('slug', Str::slug($state)); + }), + TextInput::make('slug') + ->label('URL подраздела') + ->unique(ignoreRecord: true) + ->readOnly() + ->required() + ->maxLength(255) + ->helperText('Автоматически генерируется из названия'), + ]), + ]), + Select::make('code') + ->label('HTTP статус страницы') + ->options([ + '200' => 'Обычная страница (200 OK)', + '404' => 'Страница не найдена (404 Not Found)', + '500' => 'Технические работы (500 Server Error)', + ]) + ->required() + ->default('200') + ->helperText('Выберите HTTP статус, с которым будет отдаваться страница'), + Toggle::make('searchable') + ->label('Индексировать в поиске') + ->default(true) + ->inline(false) + ->helperText('Разрешить локальному поиску индексировать страницу'), + IconPicker::make('icon') + ->label('Иконка страницы') + ->default('heroicon-o-academic-cap') + ->helperText('Выберите иконку для отображения в навигации') + ->columns(6), + TextInput::make('search_data') + ->hidden(), + ]), + Forms\Components\Tabs\Tab::make('Содержание') + ->icon('heroicon-o-document-text') + ->schema([ + ContentBuilderItem::getItem('content') + ->helperText('Создайте содержимое страницы используя конструктор') + ]), + Forms\Components\Tabs\Tab::make('Дополнительные настройки') + ->icon('heroicon-o-cog') + ->schema([ + Section::make('Отображение элементов') + ->description('Управление видимостью элементов на странице') + ->collapsible() + ->schema([ + Toggle::make('settings.hide_page_sub_section_links') + ->label('Скрыть боковую панель с ссылками на страницы раздела') + ->helperText('Скрывает список страниц текущего раздела') + ->columnSpan(1), + Toggle::make('settings.hide_page_navigate_links') + ->label('Скрыть навигацию по странице') + ->helperText('Скрывает навигацию по заголовкам') + ->columnSpan(1), + Toggle::make('settings.hide_breadcrumbs') + ->label('Скрыть хлебные крошки') + ->helperText('Скрывает навигационную цепочку вверху страницы') + ->columnSpan(1), + ]) + ->columns(2), ]), - Select::make('code')->options([ - '200' => 'Открытая страница', - '404' => 'Не найдено', - '500' => 'Ведутся технические работы', - ])->label('Статус')->required()->default('200'), - Toggle::make('searchable')->default(true)->label('Индексируется поиском')->inline(false), - IconPicker::make('icon') - ->default('heroicon-o-academic-cap') - ->label('Icon'), - - - TextInput::make('search_data')->hidden(), ]), - Forms\Components\Tabs\Tab::make('Контент')->schema([ - ContentBuilderItem::getItem('content') - ]), - Forms\Components\Tabs\Tab::make('Настройки')->schema([ - Section::make()->schema([ - Toggle::make('settings.hide_page_sub_section_links') - ->label('Скрыть сайдбар смежных страниц') - ->columnSpan(1), - - Toggle::make('settings.hide_page_navigate_links') - ->label('Скрыть навигацию по страницу') - ->columnSpan(1), - - Toggle::make('settings.hide_breadcrumbs') - ->label('Скрыть хлебные крошки') - ->columnSpan(1), -// -// Toggle::make('settings.full_width_page') -// ->label('Страница на всю ширину') -// ->columnSpan(1)->default(true), - ]), - ]), - ]), - - ]) ]); } diff --git a/app/Filament/Components/Forms/PostForm.php b/app/Filament/Components/Forms/PostForm.php index 12fd48e..6220fab 100644 --- a/app/Filament/Components/Forms/PostForm.php +++ b/app/Filament/Components/Forms/PostForm.php @@ -11,6 +11,7 @@ use App\Models\CustomForm; use App\Models\Page; use App\Models\PageReferenceList; use App\Models\Post; +use App\Models\Slider; use Filament\Forms; use Filament\Forms\Components\Builder; use Filament\Forms\Components\ColorPicker; @@ -38,8 +39,6 @@ use Symfony\Component\Finder\Finder; class PostForm { - - public static function getForm(Form $form): Form { return $form @@ -49,155 +48,264 @@ class PostForm Tabs::make('Tabs') ->tabs([ Tabs\Tab::make('Основная информация') + ->icon('heroicon-o-information-circle') ->schema([ - Forms\Components\Grid::make(2)->schema([ - TextInput::make('title')->label('Заголовок')->required() - ->live(onBlur: true) - ->afterStateUpdated(function (string $operation, string|null $state, Forms\Set $set) { - $set('slug', Str::slug($state)); - $set('seo.title', $state); - }), - TextInput::make('slug')->label('Slug')->unique(ignoreRecord: true)->readOnly()->required(), - ]), - Select::make('status')->options(PostStatus::class) - ->label('Статус')->required() + Grid::make(2) + ->schema([ + TextInput::make('title') + ->label('Заголовок') + ->required() + ->maxLength(255) + ->placeholder('Введите заголовок новости') + ->helperText('Этот заголовок будет отображаться на сайте') + ->live(onBlur: true) + ->afterStateUpdated(function (string $operation, string|null $state, Forms\Set $set) { + $set('slug', Str::slug($state)); + $set('seo.title', $state); + }), + TextInput::make('slug') + ->label('URL-адрес') + ->unique(ignoreRecord: true) + ->readOnly() + ->required() + ->helperText('Этот URL будет использоваться для страницы новости') + ->maxLength(255), + ]), + Select::make('status') + ->label('Статус публикации') + ->options(PostStatus::class) + ->required() + ->default(PostStatus::VERIFICATION) + ->helperText('Выберите статус публикации новости') ->disableOptionWhen(fn (string $value): bool => $value == PostStatus::PUBLISHED->value && !auth()->user()->can('publish_post') - ) - ->default(PostStatus::VERIFICATION), + ), Select::make('category_id') + ->label('Категория') ->options(Category::all()->pluck('title', 'id')) + ->searchable() ->preload() - ->label('Категория'), - SpatieTagsInput::make('tags')->label('Тэги'), + ->placeholder('Выберите категорию') + ->helperText('Выберите категорию для новости'), + SpatieTagsInput::make('tags') + ->label('Теги') + ->placeholder('Добавьте теги') + ->helperText('Добавьте теги для лучшей классификации'), Forms\Components\TagsInput::make('authors') - ->label('Авторы')->placeholder('Добавить автора'), - Section::make('Отложенная публикация')->schema([ - Grid::make(2)->schema([ - Toggle::make('publish_setting.publish_after') - ->label('Включить') - ->inline(false) - ->default(false) - ->live(), - DateTimePicker::make('publish_setting.publish_at') - ->label('Дата публикации') - ->native() - ->displayFormat('d/m/Y') - - ->required(fn (Forms\Get $get) => $get('publish_setting.publish_after')) - ->disabled(fn (Forms\Get $get) => !$get('publish_setting.publish_after')) - ->minDate(Carbon::now()->subWeek()) - ->maxDate(Carbon::now()->addMonth()), + ->label('Авторы') + ->placeholder('Добавить автора') + ->helperText('Укажите авторов новости') + ->suggestions([ + 'Редакция', + 'Администратор', ]), - ]), - Section::make('Публикация в сервисах')->schema([ - Forms\Components\Grid::make()->schema([ - Toggle::make('publication.vk')->label('Публикация в VK')->default(true), - Toggle::make('publication.telegram')->label('Публикация в Telegram')->default(true), + Section::make('Отложенная публикация') + ->description('Настройте автоматическую публикацию новости в указанное время') + ->collapsible() + ->schema([ + Grid::make(2) + ->schema([ + Toggle::make('publish_setting.publish_after') + ->label('Включить отложенную публикацию') + ->inline(false) + ->default(false) + ->live() + ->helperText('Активируйте для публикации в указанное время'), + DateTimePicker::make('publish_setting.publish_at') + ->label('Дата и время публикации') + ->native(false) + ->displayFormat('d/m/Y H:i') + ->seconds(false) + ->minutesStep(15) + ->helperText('Выберите дату и время публикации') + ->required(fn (Forms\Get $get) => $get('publish_setting.publish_after')) + ->disabled(fn (Forms\Get $get) => !$get('publish_setting.publish_after')) + ->minDate(now()) + ->maxDate(now()->addMonth()), + ]), + ]), + Section::make('Публикация в соцсетях') + ->description('Управление автоматической публикацией в социальных сетях') + ->collapsible() + ->schema([ + Grid::make() + ->schema([ + Toggle::make('publication.vk') + ->label('Опубликовать в VK') + ->default(true) + ->helperText('Новость будет автоматически опубликована в VK'), + Toggle::make('publication.telegram') + ->label('Опубликовать в Telegram') + ->default(true) + ->helperText('Новость будет автоматически опубликована в Telegram'), + ]), ]), - ]), ]), - Tabs\Tab::make('Содержание новости') + Tabs\Tab::make('Содержание') + ->icon('heroicon-o-document-text') ->schema([ - ContentBuilderItem::getItem('content')->required(), + ContentBuilderItem::getItem('content') + ->required() + ->helperText('Создайте содержимое новости используя конструктор'), ]), - Tabs\Tab::make('Изображения') + Tabs\Tab::make('Медиа') + ->icon('heroicon-o-photo') ->schema([ - FileUpload::make('preview')->label('Превью новости') + FileUpload::make('preview') + ->label('Главное изображение') ->image() + ->directory('posts/previews') ->optimize('webp') ->resize(50) ->imageEditor() - ->directory('images'), - FileUpload::make('images')->label('Альбом') + ->helperText('Загрузите главное изображение для новости') + ->maxSize(2048) + ->acceptedFileTypes(['image/jpeg', 'image/png', 'image/webp']) + ->imagePreviewHeight('150') + ->panelLayout('integrated'), + FileUpload::make('images') + ->label('Галерея изображений') ->image() + ->directory('posts/gallery') ->optimize('jpg') ->resize(30) ->imageEditor() - ->panelLayout('grid') - ->reorderable() - ->imageEditor() ->multiple() - ->directory('images'), + ->reorderable() + ->panelLayout('grid') + ->helperText('Загрузите дополнительные изображения для галереи') + ->maxFiles(10) + ->maxSize(2048) + ->acceptedFileTypes(['image/jpeg', 'image/png']) + ->imagePreviewHeight('150'), ]), - Tabs\Tab::make('Добавление новости в слайдер') + Tabs\Tab::make('Слайдер') + ->icon('heroicon-o-view-columns') ->schema([ Toggle::make('is_slider_enabled') - ->label('Добавить новый слайд') + ->label('Добавить в слайдер') ->live() - ->hidden(fn (string $context): bool => $context === 'edit') + ->helperText('Активируйте для добавления новости в слайдер') + ->hidden(function (Forms\Get $get, string $context) { + if ($context === 'edit') { + return true; + } + return false; + }) ->dehydrated(false) ->default(false), - Section::make() + Section::make('Настройки слайда') + ->description('Настройте отображение новости в слайдере') + ->collapsible() + ->collapsed() ->schema([ - Forms\Components\Section::make('Информация слайда')->schema([ - Forms\Components\TextInput::make('slide.title') - ->label('Заголовок слайда'), - Forms\Components\Textarea::make('slide.content') - ->label('Текст слайда'), - Forms\Components\Grid::make()->schema([ + Select::make('slide.slider_id') + ->label('Выберите слайдер') + ->options(Slider::where('is_active', true)->pluck('title', 'id')) + ->required() + ->helperText('Выберите слайдер для размещения'), + TextInput::make('slide.title') + ->label('Заголовок слайда') + ->maxLength(100) + ->helperText('Короткий заголовок для слайда') + ->placeholder('Введите заголовок'), + Forms\Components\Textarea::make('slide.content') + ->label('Текст слайда') + ->maxLength(255) + ->helperText('Краткое описание для слайда') + ->placeholder('Введите текст слайда'), + Grid::make() + ->schema([ ColorPicker::make('slide.color_theme') ->label('Цвет текста') ->default('#ffffff') - ->required(), - Forms\Components\ToggleButtons::make('slide.settings.text_position') + ->required() + ->helperText('Выберите цвет текста на слайде'), + ToggleButtons::make('slide.settings.text_position') + ->label('Позиция текста') ->options([ - 'left' => 'Текст слева', - 'center' => 'Текст по середине', - 'right' => 'Текст справа' + 'left' => 'Слева', + 'center' => 'По центру', + 'right' => 'Справа', ]) - ->inline()->default('left')->grouped() - ->label('Позиция текста на слайде'), + ->inline() + ->grouped() + ->default('left') + ->helperText('Выберите расположение текста на слайде'), ]), - Forms\Components\Grid::make()->schema([ + Grid::make() + ->schema([ Toggle::make('active_button') - ->label('Использовать кнопку для ссылки (Ссылка будет открываться при нажатии на слайд)') + ->label('Добавить кнопку') ->inline(false) ->live() + ->helperText('Добавить кнопку со ссылкой на новость') ->afterStateHydrated(function (Toggle $component, $state, $get) { $component->state(true); }) ->dehydrated(false), - Forms\Components\TextInput::make('slide.settings.link_text') - ->default('Читать') + TextInput::make('slide.settings.link_text') ->label('Текст кнопки') + ->default('Читать') + ->maxLength(20) ->disabled(fn (Forms\Get $get) => !$get('active_button')) + ->helperText('Текст для кнопки перехода'), ]), - ]), - Forms\Components\Section::make('Изображение')->schema([ - FileUpload::make('slide.image.url') - ->label('Изображение') - ->image() - ->optimize('webp') - ->resize(50) - ->disk('public') - ->directory('images') - ->imageEditor() - ->required(), - ToggleButtons::make('slide.image.shading')->inline()->grouped()->label('Уровень затемнения изображения')->options([ - '1' => 'Без затемнения', - '0.7' => 'Слабое затемнение', - '0.5' => 'Среднее затемнение', - '0.3' => 'Сильное затемнение', + Section::make('Изображение слайда') + ->schema([ + FileUpload::make('slide.image.url') + ->label('Фоновое изображение') + ->image() + ->directory('sliders') + ->optimize('webp') + ->resize(50) + ->imageEditor() + ->required() + ->maxSize(2048) + ->helperText('Загрузите фоновое изображение для слайда') + ->acceptedFileTypes(['image/jpeg', 'image/png', 'image/webp']), + ToggleButtons::make('slide.image.shading') + ->label('Затемнение фона') + ->inline() + ->grouped() + ->options([ + '1' => 'Нет', + '0.7' => 'Слабое', + '0.5' => 'Среднее', + '0.3' => 'Сильное', + ]) + ->helperText('Выберите уровень затемнения фона'), ]), - ]), - Forms\Components\Section::make('Общая часть')->schema([ - Forms\Components\Grid::make()->schema([ + Section::make('Время показа') + ->schema([ DateTimePicker::make('slide.end_time') - ->label('Слайд действует до') - ->native() - ->displayFormat('d/m/Y') - ->minDate(Carbon::now()) - ->maxDate(Carbon::now()->addMonth()), + ->label('Дата окончания показа') + ->native(false) + ->displayFormat('d/m/Y H:i') + ->minDate(now()) + ->maxDate(now()->addMonth()) + ->helperText('Укажите до какого времени слайд будет активен'), ]), - ]), ]) - ->hidden(fn(Forms\Get $get) => !$get('is_slider_enabled')) - - - ]), - ]), - ]) + ->hidden(function (Forms\Get $get) { + if ($get('is_slider_enabled') === true) { + return false; + } + if ($get('slide')['slider_id'] !== null) { + return false; + } + return true; + }), + ]) + ->hidden(function (Forms\Get $get, string $context) { + if ($context === 'edit' && $get('slide')['slider_id'] === null) { + return true; + } + return false; + }), + ]) + ->persistTabInQueryString(), + ]), ]); } } \ No newline at end of file diff --git a/app/Filament/Pages/Backups.php b/app/Filament/Pages/Backups.php index 9b25468..503af9e 100644 --- a/app/Filament/Pages/Backups.php +++ b/app/Filament/Pages/Backups.php @@ -24,7 +24,7 @@ class Backups extends BaseBackups public static function getNavigationGroup(): ?string { - return 'Settings'; + return 'Настройки приложения'; } public static function getNavigationLabel(): string diff --git a/app/Filament/Pages/CheckpointSettingsPage.php b/app/Filament/Pages/CheckpointSettingsPage.php index 1ed9a1b..d3dc60d 100644 --- a/app/Filament/Pages/CheckpointSettingsPage.php +++ b/app/Filament/Pages/CheckpointSettingsPage.php @@ -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 diff --git a/app/Filament/Resources/AcademicJournalResource.php b/app/Filament/Resources/AcademicJournalResource.php index 48e5370..e00f8ac 100644 --- a/app/Filament/Resources/AcademicJournalResource.php +++ b/app/Filament/Resources/AcademicJournalResource.php @@ -4,6 +4,7 @@ namespace App\Filament\Resources; use App\Enums\CustomFormStatus; use App\Enums\PostStatus; +use App\Filament\Components\Forms\ItemForm\Pages\ContentBuilderItem; use App\Filament\Resources\AcademicJournalResource\Pages; use App\Filament\Resources\AcademicJournalResource\RelationManagers; use App\Filament\Resources\AcademicJournalResource\RelationManagers\JournalsRelationManager; @@ -14,10 +15,13 @@ use App\Models\CustomForm; use App\Models\Page; use App\Models\Post; use Filament\Forms; +use Filament\Forms\Components\Actions\Action; use Filament\Forms\Components\Builder; use Filament\Forms\Components\FileUpload; +use Filament\Forms\Components\Grid; use Filament\Forms\Components\Hidden; use Filament\Forms\Components\RichEditor; +use Filament\Forms\Components\Section; use Filament\Forms\Components\Select; use Filament\Forms\Components\Tabs; use Filament\Forms\Components\TextInput; @@ -33,780 +37,141 @@ use Mohamedsabil83\FilamentFormsTinyeditor\Components\TinyEditor; class AcademicJournalResource extends Resource { - protected static ?string $navigationGroup = 'Наука'; - public static ?string $label = 'Журнал'; - protected static ?string $pluralLabel = 'Научные журналы'; - protected static ?string $model = AcademicJournal::class; - protected static ?string $navigationIcon = 'heroicon-o-beaker'; public static function form(Form $form): Form { return $form ->schema([ - Forms\Components\Section::make()->schema([ - Forms\Components\Grid::make(2)->schema([ - TextInput::make('title')->label('Заголовок')->required() - ->live(onBlur: true) - ->afterStateUpdated(function (string $operation, string|null $state, Forms\Set $set) { - $set('slug', Str::slug($state)); - }), - TextInput::make('slug')->label('Slug')->unique(ignoreRecord: true)->readOnly()->required(), + Section::make('Основные данные') + ->description('Основная информация о научном журнале') + ->schema([ + Grid::make(2) + ->schema([ + TextInput::make('title') + ->label('Название журнала') + ->required() + ->maxLength(255) + ->placeholder('Введите полное название журнала') + ->helperText('Официальное название журнала как в регистрационных документах') + ->live(onBlur: true) + ->afterStateUpdated(function (string $operation, string|null $state, Forms\Set $set) { + $set('slug', Str::slug($state)); + }), + TextInput::make('slug') + ->label('URL-адрес') + ->unique(ignoreRecord: true) + ->required() + ->readOnly() + ->helperText('Формируется автоматически из названия') + ->prefix(fn () => route('client.academicJournals.index') . '/') + ->suffixAction( + Action::make('copy') + ->icon('heroicon-s-clipboard-document-check') + ->action(function ($livewire, $state) { + $livewire->js( + 'window.navigator.clipboard.writeText("'. route('client.academicJournals.index') . '/' . $state.'"); + $tooltip("'.__('Copied to clipboard').'", { timeout: 1500 });' + ); + })), + ]), ]), - ]), - Tabs::make('Tabs') + + Tabs::make('Настройки журнала') + ->persistTabInQueryString() + ->columnSpanFull() ->tabs([ - Tabs\Tab::make('Основная информация журнала') + Tabs\Tab::make('Основная информация') + ->icon('heroicon-o-information-circle') ->schema([ - Builder::make('main_info')->label('')->blocks([ - Builder\Block::make('heading')->label('Заголовок') - ->schema([ - TextInput::make('id')->hidden()->integer()->default(rand(2335235,324634264263426)), - TextInput::make('content') - ->label('') - ->live(onBlur: true) - ->afterStateUpdated(function (string $operation, string $state, Forms\Set $set, $get) { - }), - ]), - Builder\Block::make('paragraph') - ->schema([ - RichEditor::make('content') - ->toolbarButtons([ - 'blockquote', - 'bold', - 'bulletList', - 'italic', - 'link', - 'orderedList', - 'redo', - 'strike', - 'underline', - 'undo', - ]) - ->label(''), - ])->label('Текст'), - Builder\Block::make('files') - ->schema([ - Forms\Components\Repeater::make('file')->schema([ - Hidden::make('expansion')->required(), - Hidden::make('size')->required(), - TextInput::make('title') + ContentBuilderItem::getItem('main_info') + ->label('Описание журнала') + ->helperText('Добавьте полное описание журнала, его историю и основные направления'), + ]), + + Tabs\Tab::make('Редакционная коллегия') + ->icon('heroicon-o-user-group') + ->schema([ + Section::make('Главный редактор') + ->description('Информация о главном редакторе журнала') + ->collapsible() + ->schema([ + Forms\Components\Repeater::make('chief_editor')->label('') + ->schema([ + TextInput::make('name') + ->label('ФИО') + ->required() + ->maxLength(100) + ->placeholder('Иванов Иван Иванович'), + TextInput::make('academicTitle') + ->label('Учёная степень') + ->required() + ->maxLength(50) + ->placeholder('д.т.н., профессор'), + TextInput::make('position') + ->label('Должность') + ->required() + ->maxLength(100) + ->placeholder('Главный научный сотрудник'), + TextInput::make('institution') + ->label('Учреждение') ->required() ->maxLength(255) - ->autofocus(), - FileUpload::make('path') - ->required() - ->getUploadedFileNameForStorageUsing( - fn (TemporaryUploadedFile $file): string => - str(Str::slug(pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME) . '-' . Carbon::now()->timestamp ) . '.' . $file->getClientOriginalExtension()) - ) - ->acceptedFileTypes([ - 'application/pdf', - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - 'application/vnd.openxmlformats-officedocument.presentationml.presentation', - 'application/zip' - ]) - ->maxSize(512000) - ->disk('public') - ->directory('files') - ->downloadable() - ->afterStateUpdated(function ($set, $state) { - $set('expansion', $state?->getClientOriginalExtension()); - $set('size', ByteConverter::bytesToHuman($state?->getSize())); - }) - ->visibility('public') - ]), - ]), - Builder\Block::make('person') - ->schema([ - TextInput::make('name') - ->label('Имя') - ->required() - ->maxLength(255), - FileUpload::make('photo') - ->label('Фотография') - ->image() - ->disk('public') - ->directory('images') - ->imageEditor(), - Forms\Components\Repeater::make('info')->schema([ - Forms\Components\Grid::make(2)->schema([ - TextInput::make('column') - ->required() - ->maxLength(255), - TextInput::make('content') - ->required() - ->maxLength(255), - ]), - ])->minItems(1), - ]), - Builder\Block::make('stepper') - ->schema([ - TextInput::make('step_name') - ->label('Название шага') - ->required() - ->maxLength(255), - Forms\Components\Repeater::make('steps')->schema([ - TextInput::make('title') - ->required() - ->maxLength(255)->columnSpanFull(), - RichEditor::make('content')->required(), - ])->minItems(1), - ]), - Builder\Block::make('tabs') - ->schema([ - Forms\Components\Repeater::make('tab')->schema([ - TextInput::make('title') - ->required() - ->maxLength(255)->columnSpanFull(), - \Filament\Forms\Components\Builder::make('content')->label('')->blocks([ - Builder\Block::make('heading')->label('Заголовок') - ->schema([ - TextInput::make('id')->hidden()->integer()->default(rand(2335235,324634264263426)), - TextInput::make('content') - ->label('') - ->live(onBlur: true) - ->afterStateUpdated(function (string $operation, string $state, Forms\Set $set, $get) { - }), - ]), - Builder\Block::make('paragraph') - ->schema([ - RichEditor::make('content') - ->toolbarButtons([ - 'blockquote', - 'bold', - 'bulletList', - 'italic', - 'link', - 'orderedList', - 'redo', - 'strike', - 'underline', - 'undo', - ]) - ->label(''), - ])->label('Текст'), - Builder\Block::make('files') - ->schema([ - Forms\Components\Repeater::make('file')->schema([ - TextInput::make('title') - ->required() - ->maxLength(255) - ->autofocus(), - FileUpload::make('path') - ->required() - ->acceptedFileTypes([ - 'application/pdf', - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - 'application/vnd.openxmlformats-officedocument.presentationml.presentation', - 'application/zip' - ]) - ->maxSize(512000) - ->disk('public') - ->directory('files') - ->downloadable() - ->visibility('public') - ]), - ]), - Builder\Block::make('person') - ->schema([ - TextInput::make('name') - ->label('Имя') - ->required() - ->maxLength(255), - FileUpload::make('photo') - ->label('Фотография') - ->image() - ->disk('public') - ->directory('images') - ->imageEditor(), - Forms\Components\Repeater::make('info')->schema([ - Forms\Components\Grid::make(2)->schema([ - TextInput::make('column') - ->required() - ->maxLength(255), - TextInput::make('content') - ->required() - ->maxLength(255), - ]), - ])->minItems(1), - ]), - Builder\Block::make('stepper') - ->schema([ - TextInput::make('step_name') - ->label('Название шага') - ->required() - ->maxLength(255), - Forms\Components\Repeater::make('steps')->schema([ - TextInput::make('title') - ->required() - ->maxLength(255)->columnSpanFull(), - RichEditor::make('content')->required(), - ])->minItems(1), - ]), - Builder\Block::make('images') - ->schema([ - FileUpload::make('url') - ->label('Изображение(-я)') - ->image() - ->multiple() - ->reorderable() - ->maxFiles(5) - ->disk('public') - ->directory('images') - ->imageEditor() - ->required(), - TextInput::make('alt') - ->label('Описание') - ->placeholder('Необязяательно') - ])->label('Слайдер изображений'), - Builder\Block::make('image') - ->schema([ - FileUpload::make('url') - ->label('Изображение(-я)') - ->image() - ->multiple() - ->reorderable() - ->maxFiles(5) - ->disk('public') - ->directory('images') - ->imageEditor() - ->required(), - TextInput::make('alt') - ->label('Описание') - ->placeholder('Необязяательно') - ])->label('Изображение'), - Builder\Block::make('video') - ->schema([ - TextInput::make('mime')->readOnly(), - TextInput::make('title') - ->required() - ->maxLength(255) - ->autofocus(), - FileUpload::make('path') - ->required() - ->acceptedFileTypes([ - 'video/mp4', - 'video/quicktime', - 'video/x-msvideo', - 'video/x-ms-wmv', - 'video/avi', - 'video/webm', - 'video/ogg', - 'video/3gpp', - 'video/3gpp2', - 'video/x-m4v', - ]) - ->disk('public') - ->directory('videos') - ->afterStateUpdated(fn (callable $set, $state) => $set('mime', $state?->getMimeType())), - ]), - Builder\Block::make('postsList') - ->schema([ - Forms\Components\Grid::make(2)->schema([ - TextInput::make('count') - ->label('Количество запией') - ->integer(), - Select::make('category') - ->options(Category::all()->pluck('title', 'id')) - ]), - ])->label('Список новостей'), - ]) - ->collapsed() - ->blockNumbers(false) - ->collapsible() - ->addActionLabel('Добавить новый блок'), - ])->minItems(1), - ]), - Builder\Block::make('images') - ->schema([ - FileUpload::make('url') - ->label('Изображение(-я)') - ->image() - ->multiple() - ->reorderable() - ->maxFiles(5) - ->disk('public') - ->directory('images') - ->imageEditor() - ->required(), - TextInput::make('alt') - ->label('Описание') - ->placeholder('Необязяательно') - ])->label('Слайдер изображений'), - Builder\Block::make('image') - ->schema([ - FileUpload::make('url') - ->label('Изображение(-я)') - ->image() - ->multiple() - ->reorderable() - ->maxFiles(5) - ->disk('public') - ->directory('images') - ->imageEditor() - ->required(), - TextInput::make('alt') - ->label('Описание') - ->placeholder('Необязяательно') - ])->label('Изображение'), - Builder\Block::make('video') - ->schema([ - TextInput::make('mime')->readOnly(), - TextInput::make('title') - ->required() - ->maxLength(255) - ->autofocus(), - FileUpload::make('path') - ->required() - ->acceptedFileTypes([ - 'video/mp4', - 'video/quicktime', - 'video/x-msvideo', - 'video/x-ms-wmv', - 'video/avi', - 'video/webm', - 'video/ogg', - 'video/3gpp', - 'video/3gpp2', - 'video/x-m4v', - ]) - ->disk('public') - ->directory('videos') - ->afterStateUpdated(fn (callable $set, $state) => $set('mime', $state?->getMimeType())), - ]), - Builder\Block::make('postsList') - ->schema([ - Forms\Components\Grid::make(2)->schema([ - TextInput::make('count') - ->label('Количество запией') - ->integer(), - Select::make('category') - ->options(Category::all()->pluck('title', 'id')) - ]), - ])->label('Список новостей'), - Builder\Block::make('postItem') - ->schema([ - Select::make('post') - ->options(Post::query()->where('status', PostStatus::PUBLISHED)->pluck('title', 'id')) - ->searchable() - ->required(), - ])->label('Новость'), - Builder\Block::make('pageItem') - ->schema([ - Select::make('page') - ->options(Page::query()->where('title', '!=' , null)->where('is_visible', true)->pluck('title', 'id')) - ->searchable() - ->required(), - ])->label('Страница'), - Builder\Block::make('customForm') - ->schema([ - Select::make('form') - ->options(CustomForm::query()->where('status', CustomFormStatus::PUBLISHED)->pluck('title', 'form_id')) - ->searchable() - ->required(), - ])->label('Форма'), - ]) - ->collapsed() - ->blockNumbers(false) - ->collapsible() - ->blockPickerColumns(3) - ->blockPickerWidth('2xl') - ->addActionLabel('Добавить новый блок'), - ]), - Tabs\Tab::make('Редакция') - ->schema([ - Forms\Components\Section::make('Главный редактор')->schema([ - Forms\Components\Repeater::make('chief_editor')->label('')->schema([ - TextInput::make('name')->label('Имя'), - TextInput::make('academicTitle')->label('Ученная степень'), - TextInput::make('position')->label('Должность'), - TextInput::make('institution')->label('Учереждение'), - ])->maxItems(1)->reorderable(false), - ]), - Forms\Components\Section::make('Редакторы')->schema([ - Forms\Components\Repeater::make('editors')->label('')->schema([ - TextInput::make('name')->label('Имя'), - TextInput::make('academicTitle')->label('Ученная степень'), - TextInput::make('position')->label('Должность'), - TextInput::make('institution')->label('Учереждение'), - ]) - ->collapsed() - ->collapsible() - ->label('') - ->addActionLabel('Добавить редактора'), + ->placeholder('МГУ имени М.В. Ломоносова'), + ]) + ->maxItems(1) + ->reorderable(false) + ->helperText('Укажите данные главного редактора журнала'), + ]), - ]), - ]), - Tabs\Tab::make('Информация для авторов') - ->schema([ - Builder::make('for_authors')->label('')->blocks([ - Builder\Block::make('heading')->label('Заголовок') - ->schema([ - TextInput::make('id')->hidden()->integer()->default(rand(2335235,324634264263426)), - TextInput::make('content') - ->label('') - ->live(onBlur: true) - ->afterStateUpdated(function (string $operation, string $state, Forms\Set $set, $get) { - }), - ]), - Builder\Block::make('paragraph') - ->schema([ - RichEditor::make('content') - ->toolbarButtons([ - 'blockquote', - 'bold', - 'bulletList', - 'italic', - 'link', - 'orderedList', - 'redo', - 'strike', - 'underline', - 'undo', - ]) - ->label(''), - ])->label('Текст'), - Builder\Block::make('files') - ->schema([ - Forms\Components\Repeater::make('file')->schema([ - Hidden::make('expansion')->required(), - Hidden::make('size')->required(), - TextInput::make('title') + Section::make('Редакционная коллегия') + ->description('Состав редакционной коллегии журнала') + ->collapsible() + ->schema([ + Forms\Components\Repeater::make('editors')->label('') + ->schema([ + TextInput::make('name') + ->label('ФИО') + ->required() + ->maxLength(100) + ->placeholder('Петров Петр Петрович'), + TextInput::make('academicTitle') + ->label('Учёная степень') + ->required() + ->maxLength(50) + ->placeholder('к.ф.-м.н., доцент'), + TextInput::make('position') + ->label('Должность') + ->required() + ->maxLength(100) + ->placeholder('Доцент кафедры'), + TextInput::make('institution') + ->label('Учреждение') ->required() ->maxLength(255) - ->autofocus(), - FileUpload::make('path') - ->required() - ->getUploadedFileNameForStorageUsing( - fn (TemporaryUploadedFile $file): string => - str(Str::slug(pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME) . '-' . Carbon::now()->timestamp ) . '.' . $file->getClientOriginalExtension()) - ) - ->acceptedFileTypes([ - 'application/pdf', - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - 'application/vnd.openxmlformats-officedocument.presentationml.presentation', - 'application/zip' - ]) - ->maxSize(512000) - ->disk('public') - ->directory('files') - ->downloadable() - ->afterStateUpdated(function ($set, $state) { - $set('expansion', $state?->getClientOriginalExtension()); - $set('size', ByteConverter::bytesToHuman($state?->getSize())); - }) - ->visibility('public') - ]), - ]), - Builder\Block::make('person') - ->schema([ - TextInput::make('name') - ->label('Имя') - ->required() - ->maxLength(255), - FileUpload::make('photo') - ->label('Фотография') - ->image() - ->disk('public') - ->directory('images') - ->imageEditor(), - Forms\Components\Repeater::make('info')->schema([ - Forms\Components\Grid::make(2)->schema([ - TextInput::make('column') - ->required() - ->maxLength(255), - TextInput::make('content') - ->required() - ->maxLength(255), - ]), - ])->minItems(1), - ]), - Builder\Block::make('stepper') - ->schema([ - TextInput::make('step_name') - ->label('Название шага') - ->required() - ->maxLength(255), - Forms\Components\Repeater::make('steps')->schema([ - TextInput::make('title') - ->required() - ->maxLength(255)->columnSpanFull(), - RichEditor::make('content')->required(), - ])->minItems(1), - ]), - Builder\Block::make('tabs') - ->schema([ - Forms\Components\Repeater::make('tab')->schema([ - TextInput::make('title') - ->required() - ->maxLength(255)->columnSpanFull(), - \Filament\Forms\Components\Builder::make('content')->label('')->blocks([ - Builder\Block::make('heading')->label('Заголовок') - ->schema([ - TextInput::make('id')->hidden()->integer()->default(rand(2335235,324634264263426)), - TextInput::make('content') - ->label('') - ->live(onBlur: true) - ->afterStateUpdated(function (string $operation, string $state, Forms\Set $set, $get) { - }), - ]), - Builder\Block::make('paragraph') - ->schema([ - RichEditor::make('content') - ->toolbarButtons([ - 'blockquote', - 'bold', - 'bulletList', - 'italic', - 'link', - 'orderedList', - 'redo', - 'strike', - 'underline', - 'undo', - ]) - ->label(''), - ])->label('Текст'), - Builder\Block::make('files') - ->schema([ - Forms\Components\Repeater::make('file')->schema([ - TextInput::make('title') - ->required() - ->maxLength(255) - ->autofocus(), - FileUpload::make('path') - ->required() - ->acceptedFileTypes([ - 'application/pdf', - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - 'application/vnd.openxmlformats-officedocument.presentationml.presentation', - 'application/zip' - ]) - ->maxSize(512000) - ->disk('public') - ->directory('files') - ->downloadable() - ->visibility('public') - ]), - ]), - Builder\Block::make('person') - ->schema([ - TextInput::make('name') - ->label('Имя') - ->required() - ->maxLength(255), - FileUpload::make('photo') - ->label('Фотография') - ->image() - ->disk('public') - ->directory('images') - ->imageEditor(), - Forms\Components\Repeater::make('info')->schema([ - Forms\Components\Grid::make(2)->schema([ - TextInput::make('column') - ->required() - ->maxLength(255), - TextInput::make('content') - ->required() - ->maxLength(255), - ]), - ])->minItems(1), - ]), - Builder\Block::make('stepper') - ->schema([ - TextInput::make('step_name') - ->label('Название шага') - ->required() - ->maxLength(255), - Forms\Components\Repeater::make('steps')->schema([ - TextInput::make('title') - ->required() - ->maxLength(255)->columnSpanFull(), - RichEditor::make('content')->required(), - ])->minItems(1), - ]), - Builder\Block::make('images') - ->schema([ - FileUpload::make('url') - ->label('Изображение(-я)') - ->image() - ->multiple() - ->reorderable() - ->maxFiles(5) - ->disk('public') - ->directory('images') - ->imageEditor() - ->required(), - TextInput::make('alt') - ->label('Описание') - ->placeholder('Необязяательно') - ])->label('Слайдер изображений'), - Builder\Block::make('image') - ->schema([ - FileUpload::make('url') - ->label('Изображение(-я)') - ->image() - ->multiple() - ->reorderable() - ->maxFiles(5) - ->disk('public') - ->directory('images') - ->imageEditor() - ->required(), - TextInput::make('alt') - ->label('Описание') - ->placeholder('Необязяательно') - ])->label('Изображение'), - Builder\Block::make('video') - ->schema([ - TextInput::make('mime')->readOnly(), - TextInput::make('title') - ->required() - ->maxLength(255) - ->autofocus(), - FileUpload::make('path') - ->required() - ->acceptedFileTypes([ - 'video/mp4', - 'video/quicktime', - 'video/x-msvideo', - 'video/x-ms-wmv', - 'video/avi', - 'video/webm', - 'video/ogg', - 'video/3gpp', - 'video/3gpp2', - 'video/x-m4v', - ]) - ->disk('public') - ->directory('videos') - ->afterStateUpdated(fn (callable $set, $state) => $set('mime', $state?->getMimeType())), - ]), - Builder\Block::make('postsList') - ->schema([ - Forms\Components\Grid::make(2)->schema([ - TextInput::make('count') - ->label('Количество запией') - ->integer(), - Select::make('category') - ->options(Category::all()->pluck('title', 'id')) - ]), - ])->label('Список новостей'), - ]) - ->collapsed() - ->blockNumbers(false) - ->collapsible() - ->addActionLabel('Добавить новый блок'), - ])->minItems(1), - ]), - Builder\Block::make('images') - ->schema([ - FileUpload::make('url') - ->label('Изображение(-я)') - ->image() - ->multiple() - ->reorderable() - ->maxFiles(5) - ->disk('public') - ->directory('images') - ->imageEditor() - ->required(), - TextInput::make('alt') - ->label('Описание') - ->placeholder('Необязяательно') - ])->label('Слайдер изображений'), - Builder\Block::make('image') - ->schema([ - FileUpload::make('url') - ->label('Изображение(-я)') - ->image() - ->multiple() - ->reorderable() - ->maxFiles(5) - ->disk('public') - ->directory('images') - ->imageEditor() - ->required(), - TextInput::make('alt') - ->label('Описание') - ->placeholder('Необязяательно') - ])->label('Изображение'), - Builder\Block::make('video') - ->schema([ - TextInput::make('mime')->readOnly(), - TextInput::make('title') - ->required() - ->maxLength(255) - ->autofocus(), - FileUpload::make('path') - ->required() - ->acceptedFileTypes([ - 'video/mp4', - 'video/quicktime', - 'video/x-msvideo', - 'video/x-ms-wmv', - 'video/avi', - 'video/webm', - 'video/ogg', - 'video/3gpp', - 'video/3gpp2', - 'video/x-m4v', - ]) - ->disk('public') - ->directory('videos') - ->afterStateUpdated(fn (callable $set, $state) => $set('mime', $state?->getMimeType())), - ]), - Builder\Block::make('postsList') - ->schema([ - Forms\Components\Grid::make(2)->schema([ - TextInput::make('count') - ->label('Количество запией') - ->integer(), - Select::make('category') - ->options(Category::all()->pluck('title', 'id')) - ]), - ])->label('Список новостей'), - Builder\Block::make('postItem') - ->schema([ - Select::make('post') - ->options(Post::query()->where('status', PostStatus::PUBLISHED)->pluck('title', 'id')) - ->searchable() - ->required(), - ])->label('Новость'), - Builder\Block::make('pageItem') - ->schema([ - Select::make('page') - ->options(Page::query()->where('title', '!=' , null)->where('is_visible', true)->pluck('title', 'id')) - ->searchable() - ->required(), - ])->label('Страница'), - Builder\Block::make('customForm') - ->schema([ - Select::make('form') - ->options(CustomForm::query()->where('status', CustomFormStatus::PUBLISHED)->pluck('title', 'form_id')) - ->searchable() - ->required(), - ])->label('Форма'), - ]) - ->collapsed() - ->blockNumbers(false) - ->collapsible() - ->blockPickerColumns(3) - ->blockPickerWidth('2xl') - ->addActionLabel('Добавить новый блок'), + ->placeholder('СПбГУ'), + ]) + ->collapsed() + ->collapsible() + ->addActionLabel('Добавить редактора') + ->reorderable(true) + ->itemLabel(fn (array $state): ?string => $state['name'] ?? null) + ->helperText('Добавьте членов редакционной коллегии журнала'), + ]), ]), - ])->columnSpanFull() - + Tabs\Tab::make('Для авторов') + ->icon('heroicon-o-pencil') + ->schema([ + ContentBuilderItem::getItem('for_authors') + ->label('Информация для авторов') + ->helperText('Разместите требования к статьям, правила оформления и сроки подачи'), + ]), + ]) ]); } @@ -814,18 +179,39 @@ class AcademicJournalResource extends Resource { return $table ->columns([ - // + Tables\Columns\TextColumn::make('title') + ->label('Название журнала') + ->searchable() + ->sortable(), + Tables\Columns\TextColumn::make('created_at') + ->label('Дата создания') + ->dateTime('d.m.Y') + ->sortable(), ]) ->filters([ - // ]) ->actions([ - Tables\Actions\EditAction::make(), + Tables\Actions\EditAction::make() + ->iconButton() + ->tooltip('Редактировать'), + Tables\Actions\DeleteAction::make() + ->iconButton() + ->tooltip('Удалить'), + Tables\Actions\RestoreAction::make() + ->iconButton() + ->tooltip('Восстановить'), ]) ->bulkActions([ Tables\Actions\BulkActionGroup::make([ - Tables\Actions\DeleteBulkAction::make(), + Tables\Actions\DeleteBulkAction::make() + ->label('Удалить выбранное'), + Tables\Actions\RestoreBulkAction::make() + ->label('Восстановить выбранное'), ]), + ]) + ->emptyStateActions([ + Tables\Actions\CreateAction::make() + ->label('Добавить журнал'), ]); } @@ -844,4 +230,5 @@ class AcademicJournalResource extends Resource 'edit' => Pages\EditAcademicJournal::route('/{record}/edit'), ]; } -} + +} \ No newline at end of file diff --git a/app/Filament/Resources/AcademicJournalResource/Pages/CreateAcademicJournal.php b/app/Filament/Resources/AcademicJournalResource/Pages/CreateAcademicJournal.php index 6b4a999..f7e1a3d 100644 --- a/app/Filament/Resources/AcademicJournalResource/Pages/CreateAcademicJournal.php +++ b/app/Filament/Resources/AcademicJournalResource/Pages/CreateAcademicJournal.php @@ -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 diff --git a/app/Filament/Resources/AcademicJournalResource/Pages/EditAcademicJournal.php b/app/Filament/Resources/AcademicJournalResource/Pages/EditAcademicJournal.php index 3787d15..a849a71 100644 --- a/app/Filament/Resources/AcademicJournalResource/Pages/EditAcademicJournal.php +++ b/app/Filament/Resources/AcademicJournalResource/Pages/EditAcademicJournal.php @@ -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 { diff --git a/app/Filament/Resources/AcademicJournalResource/RelationManagers/JournalsRelationManager.php b/app/Filament/Resources/AcademicJournalResource/RelationManagers/JournalsRelationManager.php index 8e2c81c..3efdb3c 100644 --- a/app/Filament/Resources/AcademicJournalResource/RelationManagers/JournalsRelationManager.php +++ b/app/Filament/Resources/AcademicJournalResource/RelationManagers/JournalsRelationManager.php @@ -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(), ]); } -} +} \ No newline at end of file diff --git a/app/Filament/Resources/AdditionalEducationCategoryResource.php b/app/Filament/Resources/AdditionalEducationCategoryResource.php index a26be30..15311fa 100644 --- a/app/Filament/Resources/AdditionalEducationCategoryResource.php +++ b/app/Filament/Resources/AdditionalEducationCategoryResource.php @@ -3,18 +3,21 @@ namespace App\Filament\Resources; use App\Filament\Resources\AdditionalEducationCategoryResource\Pages; -use App\Filament\Resources\AdditionalEducationCategoryResource\RelationManagers; use App\Models\AdditionalEducationCategory; use App\Models\DirectionAdditionalEducation; use Filament\Forms; +use Filament\Forms\Components\Grid; +use Filament\Forms\Components\Section; +use Filament\Forms\Components\Select; use Filament\Forms\Components\TextInput; +use Filament\Forms\Components\Toggle; use Filament\Forms\Form; use Filament\Resources\Resource; use Filament\Tables; +use Filament\Tables\Columns\BadgeColumn; +use Filament\Tables\Columns\IconColumn; use Filament\Tables\Columns\TextColumn; use Filament\Tables\Table; -use Illuminate\Database\Eloquent\Builder; -use Illuminate\Database\Eloquent\SoftDeletingScope; use Illuminate\Support\Str; class AdditionalEducationCategoryResource extends Resource @@ -27,27 +30,57 @@ class AdditionalEducationCategoryResource extends Resource protected static ?string $pluralLabel = 'Категории дополнительного образования'; protected static ?string $navigationParentItem = 'Дополнительное Образование'; - protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack'; + protected static ?string $navigationIcon = 'heroicon-o-tag'; public static function form(Form $form): Form { return $form ->schema([ - Forms\Components\Section::make()->schema([ - Forms\Components\Grid::make('2')->schema([ - TextInput::make('title')->label('Заголовок')->required() - ->live(onBlur: true) - ->afterStateUpdated(function (string $operation, string|null $state, Forms\Set $set) { - $set('slug', Str::slug($state)); - $set('seo.title', $state); - }), - TextInput::make('slug')->label('Slug')->unique(ignoreRecord: true)->readOnly()->required(), - Forms\Components\Select::make('dir_addit_educat_id')->required()->label('Направление доп. образования') - ->preload() - ->options(DirectionAdditionalEducation::where('is_active', true)->pluck('title', 'id')) + Section::make('Основная информация') + ->description('Заполните данные о категории программ ДПО') + ->collapsible() + ->schema([ + Grid::make(2) + ->schema([ + TextInput::make('title') + ->label('Название категории') + ->required() + ->maxLength(255) + ->placeholder('Например: "Профессиональная переподготовка"') + ->live(onBlur: true) + ->afterStateUpdated(function (string $operation, ?string $state, Forms\Set $set) { + $set('slug', Str::slug($state)); + }) + ->helperText('Укажите понятное название категории'), + + TextInput::make('slug') + ->label('URL-идентификатор') + ->required() + ->maxLength(255) + ->unique(ignoreRecord: true) + ->helperText('Человеко-понятный URL для категории'), + + Select::make('dir_addit_educat_id') + ->label('Направление ДПО') + ->options( + DirectionAdditionalEducation::where('is_active', true) + ->orderBy('title') + ->pluck('title', 'id') + ) + ->required() + ->preload() + ->searchable() + ->placeholder('Выберите направление') + ->helperText('К какому направлению относится категория'), + ]), + + Toggle::make('is_active') + ->label('Активная категория') + ->inline(false) + ->default(true) + ->helperText('Отображать ли категорию на сайте') + ->columnSpanFull(), ]), - Forms\Components\Toggle::make('is_active')->label('Активно')->columnSpanFull()->inline(false)->default(true), - ]), ]); } @@ -55,30 +88,77 @@ class AdditionalEducationCategoryResource extends Resource { return $table ->columns([ - TextColumn::make('id')->label('ID')->sortable(), - TextColumn::make('title')->label('Название')->sortable()->searchable(), - TextColumn::make('created_at')->label('Дата создания')->sortable(), - Tables\Columns\BadgeColumn::make('direction.title')->label('Направление')->sortable(), - Tables\Columns\ToggleColumn::make('is_active')->label('Активно')->sortable(), + TextColumn::make('title') + ->label('Название') + ->searchable() + ->sortable() + ->description(fn ($record) => $record->direction->title ?? '') + ->limit(50), + + BadgeColumn::make('direction.title') + ->label('Направление') + ->sortable() + ->searchable() + ->color('primary'), + + IconColumn::make('is_active') + ->label('Активна') + ->boolean() + ->trueIcon('heroicon-o-check-circle') + ->falseIcon('heroicon-o-x-circle') + ->trueColor('success') + ->falseColor('danger') + ->sortable(), + + TextColumn::make('updated_at') + ->label('Обновлено') + ->dateTime('d.m.Y H:i') + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), ]) ->filters([ - // + Tables\Filters\SelectFilter::make('dir_addit_educat_id') + ->label('Направление ДПО') + ->options( + DirectionAdditionalEducation::where('is_active', true) + ->orderBy('title') + ->pluck('title', 'id') + ) + ->searchable(), + + Tables\Filters\TernaryFilter::make('is_active') + ->label('Только активные') + ->placeholder('Все') + ->trueLabel('Активные') + ->falseLabel('Неактивные'), ]) ->actions([ - Tables\Actions\EditAction::make(), + Tables\Actions\EditAction::make() + ->iconButton() + ->tooltip('Редактировать'), + + Tables\Actions\ViewAction::make() + ->iconButton() + ->tooltip('Просмотреть'), ]) ->bulkActions([ Tables\Actions\BulkActionGroup::make([ - Tables\Actions\DeleteBulkAction::make(), + Tables\Actions\DeleteBulkAction::make() + ->label('Удалить выбранные') + ->modalHeading('Удаление категорий') + ->modalDescription('Вы уверены, что хотите удалить выбранные категории ДПО?'), ]), - ]); + ]) + ->emptyStateActions([ + Tables\Actions\CreateAction::make() + ->label('Добавить категорию'), + ]) + ->defaultSort('title'); } public static function getRelations(): array { - return [ - // - ]; + return []; } public static function getPages(): array @@ -89,4 +169,4 @@ class AdditionalEducationCategoryResource extends Resource 'edit' => Pages\EditAdditionalEducationCategory::route('/{record}/edit'), ]; } -} +} \ No newline at end of file diff --git a/app/Filament/Resources/AdditionalEducationResource.php b/app/Filament/Resources/AdditionalEducationResource.php index d13e242..3c09b91 100644 --- a/app/Filament/Resources/AdditionalEducationResource.php +++ b/app/Filament/Resources/AdditionalEducationResource.php @@ -2,41 +2,25 @@ namespace App\Filament\Resources; -use App\Enums\CustomFormStatus; use App\Enums\FormEducation; -use App\Enums\LevelEducational; -use App\Enums\PostStatus; +use App\Filament\Components\Forms\ItemForm\Pages\ContentBuilderItem; use App\Filament\Resources\AdditionalEducationResource\Pages; -use App\Filament\Resources\AdditionalEducationResource\RelationManagers; -use App\Helpers\ByteConverter; use App\Models\AdditionalEducation; use App\Models\AdditionalEducationCategory; -use App\Models\Category; -use App\Models\CustomForm; -use App\Models\DirectionAdditionalEducation; -use App\Models\Page; -use App\Models\Post; use Filament\Forms; -use Filament\Forms\Components\Builder; -use Filament\Forms\Components\FileUpload; -use Filament\Forms\Components\Hidden; -use Filament\Forms\Components\RichEditor; +use Filament\Forms\Components\Grid; use Filament\Forms\Components\Section; use Filament\Forms\Components\Select; -use Filament\Forms\Components\SpatieTagsInput; -use Filament\Forms\Components\Tabs; use Filament\Forms\Components\TextInput; use Filament\Forms\Components\Toggle; use Filament\Forms\Form; use Filament\Resources\Resource; use Filament\Tables; +use Filament\Tables\Columns\BadgeColumn; +use Filament\Tables\Columns\IconColumn; use Filament\Tables\Columns\TextColumn; use Filament\Tables\Table; -use Illuminate\Database\Eloquent\SoftDeletingScope; -use Illuminate\Support\Carbon; use Illuminate\Support\Str; -use Livewire\Features\SupportFileUploads\TemporaryUploadedFile; -use Mohamedsabil83\FilamentFormsTinyeditor\Components\TinyEditor; class AdditionalEducationResource extends Resource { @@ -45,401 +29,117 @@ class AdditionalEducationResource extends Resource protected static ?string $navigationGroup = 'Образование'; public static ?string $label = 'Дополнительное образование'; - protected static ?string $pluralLabel = 'Дополнительное образование'; - protected static ?string $navigationIcon = 'heroicon-o-academic-cap'; + protected static ?string $pluralLabel = 'Дополнительное образование'; + protected static ?string $navigationIcon = 'heroicon-o-book-open'; public static function form(Form $form): Form { return $form ->schema([ - Section::make()->schema([ - Tabs::make('Tabs') - ->tabs([ - Tabs\Tab::make('Основная информация') - ->schema([ - Forms\Components\Grid::make('2')->schema([ - TextInput::make('title')->label('Заголовок')->required() - ->live(onBlur: true) - ->afterStateUpdated(function (string $operation, string|null $state, Forms\Set $set) { - $set('slug', Str::slug($state)); - }), - TextInput::make('slug')->label('Slug')->unique(ignoreRecord: true)->readOnly()->required(), - - Forms\Components\Select::make('category_id')->required()->label('Категория') - ->options(AdditionalEducationCategory::where('is_active', true)->pluck('title', 'id'))->preload()->searchable() - ]), - Forms\Components\TextInput::make('target_group')->required()->columnSpanFull()->label('Целевая аудитория'), - Forms\Components\TextInput::make('qualification')->required()->columnSpanFull()->label('Присваиваемая квалификация'), - Forms\Components\Grid::make('2')->schema([ - Forms\Components\TextInput::make('price')->required()->integer()->label('Стоимость'), - Forms\Components\TextInput::make('learning_time')->required()->integer()->label('Объем обучения'), - ]), - Forms\Components\Select::make('form_education')->label('Форма обучения')->required()->options(FormEducation::class), - Forms\Components\Toggle::make('is_active')->label('Активно')->columnSpanFull()->inline(false)->default(true), - ]), - Tabs\Tab::make('Контент') - ->schema([ - Builder::make('content')->label('')->blocks([ - Builder\Block::make('heading')->label('Заголовок') + Forms\Components\Tabs::make('Программа ДПО') + ->persistTabInQueryString() + ->columnSpanFull() + ->tabs([ + Forms\Components\Tabs\Tab::make('Основные данные') + ->icon('heroicon-o-information-circle') + ->schema([ + Section::make('Общая информация') + ->description('Основные сведения о программе') + ->schema([ + Grid::make(2) ->schema([ - TextInput::make('id')->hidden()->integer()->default(rand(2335235,324634264263426)), - TextInput::make('content') - ->label('') - ->live(onBlur: true) - ->afterStateUpdated(function (string $operation, string $state, Forms\Set $set, $get) { - }), - ]), - Builder\Block::make('paragraph') - ->schema([ - RichEditor::make('content') - ->toolbarButtons([ - 'blockquote', - 'bold', - 'bulletList', - 'italic', - 'link', - 'orderedList', - 'redo', - 'strike', - 'underline', - 'undo', - ]) - ->label(''), - ])->label('Текст'), - Builder\Block::make('files') - ->schema([ - Forms\Components\Repeater::make('file')->schema([ - Hidden::make('expansion')->required(), - Hidden::make('size')->required(), - TextInput::make('title') - ->required() - ->maxLength(255) - ->autofocus(), - FileUpload::make('path') - ->required() - ->getUploadedFileNameForStorageUsing( - fn (TemporaryUploadedFile $file): string => - str(Str::slug(pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME) . '-' . Carbon::now()->timestamp ) . '.' . $file->getClientOriginalExtension()) - ) - ->acceptedFileTypes([ - 'application/pdf', - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - 'application/vnd.openxmlformats-officedocument.presentationml.presentation', - 'application/zip' - ]) - ->maxSize(512000) - ->disk('public') - ->directory('files') - ->downloadable() - ->afterStateUpdated(function ($set, $state) { - $set('expansion', $state?->getClientOriginalExtension()); - $set('size', ByteConverter::bytesToHuman($state?->getSize())); - }) - ->visibility('public') - ]), - ]), - Builder\Block::make('person') - ->schema([ - TextInput::make('name') - ->label('Имя') - ->required() - ->maxLength(255), - FileUpload::make('photo') - ->label('Фотография') - ->image() - ->disk('public') - ->directory('images') - ->imageEditor(), - Forms\Components\Repeater::make('info')->schema([ - Forms\Components\Grid::make(2)->schema([ - TextInput::make('column') - ->required() - ->maxLength(255), - TextInput::make('content') - ->required() - ->maxLength(255), - ]), - ])->minItems(1), - ]), - Builder\Block::make('stepper') - ->schema([ - TextInput::make('step_name') - ->label('Название шага') - ->required() - ->maxLength(255), - Forms\Components\Repeater::make('steps')->schema([ - TextInput::make('title') - ->required() - ->maxLength(255)->columnSpanFull(), - RichEditor::make('content')->required(), - ])->minItems(1), - ]), - Builder\Block::make('tabs') - ->schema([ - Forms\Components\Repeater::make('tab')->schema([ - TextInput::make('title') - ->required() - ->maxLength(255)->columnSpanFull(), - \Filament\Forms\Components\Builder::make('content')->label('')->blocks([ - Builder\Block::make('heading')->label('Заголовок') - ->schema([ - TextInput::make('id')->hidden()->integer()->default(rand(2335235,324634264263426)), - TextInput::make('content') - ->label('') - ->live(onBlur: true) - ->afterStateUpdated(function (string $operation, string $state, Forms\Set $set, $get) { - }), - ]), - Builder\Block::make('paragraph') - ->schema([ - RichEditor::make('content') - ->toolbarButtons([ - 'blockquote', - 'bold', - 'bulletList', - 'italic', - 'link', - 'orderedList', - 'redo', - 'strike', - 'underline', - 'undo', - ]) - ->label(''), - ])->label('Текст'), - Builder\Block::make('files') - ->schema([ - Forms\Components\Repeater::make('file')->schema([ - TextInput::make('title') - ->required() - ->maxLength(255) - ->autofocus(), - FileUpload::make('path') - ->required() - ->acceptedFileTypes([ - 'application/pdf', - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - 'application/vnd.openxmlformats-officedocument.presentationml.presentation', - 'application/zip' - ]) - ->maxSize(512000) - ->disk('public') - ->directory('files') - ->downloadable() - ->visibility('public') - ]), - ]), - Builder\Block::make('person') - ->schema([ - TextInput::make('name') - ->label('Имя') - ->required() - ->maxLength(255), - FileUpload::make('photo') - ->label('Фотография') - ->image() - ->disk('public') - ->directory('images') - ->imageEditor(), - Forms\Components\Repeater::make('info')->schema([ - Forms\Components\Grid::make(2)->schema([ - TextInput::make('column') - ->required() - ->maxLength(255), - TextInput::make('content') - ->required() - ->maxLength(255), - ]), - ])->minItems(1), - ]), - Builder\Block::make('stepper') - ->schema([ - TextInput::make('step_name') - ->label('Название шага') - ->required() - ->maxLength(255), - Forms\Components\Repeater::make('steps')->schema([ - TextInput::make('title') - ->required() - ->maxLength(255)->columnSpanFull(), - RichEditor::make('content')->required(), - ])->minItems(1), - ]), - Builder\Block::make('images') - ->schema([ - FileUpload::make('url') - ->label('Изображение(-я)') - ->image() - ->multiple() - ->reorderable() - ->maxFiles(5) - ->disk('public') - ->directory('images') - ->imageEditor() - ->required(), - TextInput::make('alt') - ->label('Описание') - ->placeholder('Необязяательно') - ])->label('Слайдер изображений'), - Builder\Block::make('image') - ->schema([ - FileUpload::make('url') - ->label('Изображение(-я)') - ->image() - ->multiple() - ->reorderable() - ->maxFiles(5) - ->disk('public') - ->directory('images') - ->imageEditor() - ->required(), - TextInput::make('alt') - ->label('Описание') - ->placeholder('Необязяательно') - ])->label('Изображение'), - Builder\Block::make('video') - ->schema([ - TextInput::make('mime')->readOnly(), - TextInput::make('title') - ->required() - ->maxLength(255) - ->autofocus(), - FileUpload::make('path') - ->required() - ->acceptedFileTypes([ - 'video/mp4', - 'video/quicktime', - 'video/x-msvideo', - 'video/x-ms-wmv', - 'video/avi', - 'video/webm', - 'video/ogg', - 'video/3gpp', - 'video/3gpp2', - 'video/x-m4v', - ]) - ->disk('public') - ->directory('videos') - ->afterStateUpdated(fn (callable $set, $state) => $set('mime', $state?->getMimeType())), - ]), - Builder\Block::make('postsList') - ->schema([ - Forms\Components\Grid::make(2)->schema([ - TextInput::make('count') - ->label('Количество запией') - ->integer(), - Select::make('category') - ->options(Category::all()->pluck('title', 'id')) - ]), - ])->label('Список новостей'), - ]) - ->collapsed() - ->blockNumbers(false) - ->collapsible() - ->addActionLabel('Добавить новый блок'), - ])->minItems(1), - ]), - Builder\Block::make('images') - ->schema([ - FileUpload::make('url') - ->label('Изображение(-я)') - ->image() - ->multiple() - ->reorderable() - ->maxFiles(5) - ->disk('public') - ->directory('images') - ->imageEditor() - ->required(), - TextInput::make('alt') - ->label('Описание') - ->placeholder('Необязяательно') - ])->label('Слайдер изображений'), - Builder\Block::make('image') - ->schema([ - FileUpload::make('url') - ->label('Изображение(-я)') - ->image() - ->multiple() - ->reorderable() - ->maxFiles(5) - ->disk('public') - ->directory('images') - ->imageEditor() - ->required(), - TextInput::make('alt') - ->label('Описание') - ->placeholder('Необязяательно') - ])->label('Изображение'), - Builder\Block::make('video') - ->schema([ - TextInput::make('mime')->readOnly(), TextInput::make('title') + ->label('Название программы') ->required() ->maxLength(255) - ->autofocus(), - FileUpload::make('path') + ->placeholder('Например: "Цифровые технологии в управлении"') + ->live(onBlur: true) + ->afterStateUpdated(function (string $operation, ?string $state, Forms\Set $set) { + $set('slug', Str::slug($state)); + }) + ->helperText('Полное официальное название программы'), + + TextInput::make('slug') + ->label('URL-адрес') ->required() - ->acceptedFileTypes([ - 'video/mp4', - 'video/quicktime', - 'video/x-msvideo', - 'video/x-ms-wmv', - 'video/avi', - 'video/webm', - 'video/ogg', - 'video/3gpp', - 'video/3gpp2', - 'video/x-m4v', - ]) - ->disk('public') - ->directory('videos') - ->afterStateUpdated(fn (callable $set, $state) => $set('mime', $state?->getMimeType())), + ->readonly() + ->maxLength(255) + ->unique(ignoreRecord: true) + ->helperText('Человеко-понятный URL для страницы программы'), ]), - Builder\Block::make('postsList') + + Select::make('category_id') + ->label('Категория') + ->options(AdditionalEducationCategory::where('is_active', true)->pluck('title', 'id')) + ->required() + ->preload() + ->searchable() + ->placeholder('Выберите категорию') + ->helperText('К какой категории относится программа'), + + TextInput::make('target_group') + ->label('Целевая аудитория') + ->required() + ->maxLength(255) + ->placeholder('Например: "Руководители среднего звена"') + ->columnSpanFull() + ->helperText('Для кого предназначена эта программа'), + + TextInput::make('qualification') + ->label('Выдаваемый документ') + ->required() + ->maxLength(255) + ->placeholder('Например: "Удостоверение о повышении квалификации"') + ->columnSpanFull() + ->helperText('Какой документ получат слушатели'), + ]), + + Section::make('Параметры обучения') + ->schema([ + Grid::make(2) ->schema([ - Forms\Components\Grid::make(2)->schema([ - TextInput::make('count') - ->label('Количество запией') - ->integer(), - Select::make('category') - ->options(Category::all()->pluck('title', 'id')) - ]), - ])->label('Список новостей'), - Builder\Block::make('postItem') - ->schema([ - Select::make('post') - ->options(Post::query()->where('status', PostStatus::PUBLISHED)->pluck('title', 'id')) - ->searchable() - ->required(), - ])->label('Новость'), - Builder\Block::make('pageItem') - ->schema([ - Select::make('page') - ->options(Page::query()->where('title', '!=' , null)->where('is_visible', true)->pluck('title', 'id')) - ->searchable() - ->required(), - ])->label('Страница'), - Builder\Block::make('customForm') - ->schema([ - Select::make('form') - ->options(CustomForm::query()->where('status', CustomFormStatus::PUBLISHED)->pluck('title', 'form_id')) - ->searchable() - ->required(), - ])->label('Форма'), - ]) - ->collapsed() - ->blockNumbers(false) - ->collapsible() - ->blockPickerColumns(3) - ->blockPickerWidth('2xl') - ->addActionLabel('Добавить новый блок'), - ]), - ]), - ]), + TextInput::make('price') + ->label('Стоимость (руб)') + ->required() + ->numeric() + ->minValue(0) + ->placeholder('Укажите стоимость') + ->helperText('Полная стоимость программы'), + + TextInput::make('learning_time') + ->label('Объем (часов)') + ->required() + ->numeric() + ->minValue(1) + ->placeholder('Укажите количество часов') + ->helperText('Общий объем программы в академических часах'), + + Select::make('form_education') + ->label('Форма обучения') + ->options(FormEducation::class) + ->required() + ->native(false) + ->placeholder('Выберите форму') + ->helperText('Основная форма проведения занятий'), + + Toggle::make('is_active') + ->label('Активна для записи') + ->inline(false) + ->default(true) + ->helperText('Отображать ли программу на сайте'), + ]), + ]), + ]), + + Forms\Components\Tabs\Tab::make('Содержание программы') + ->icon('heroicon-o-document-text') + ->schema([ + ContentBuilderItem::getItem('content') + ->label('Описание программы') + ->helperText('Создайте подробное описание программы с помощью конструктора') + ]), + ]), ]); } @@ -447,30 +147,94 @@ class AdditionalEducationResource extends Resource { return $table ->columns([ - TextColumn::make('id')->label('ID')->sortable(), - TextColumn::make('title')->label('Название')->sortable()->searchable(), - TextColumn::make('created_at')->label('Дата создания')->sortable(), - Tables\Columns\BadgeColumn::make('category.title')->label('Категория')->sortable(), - Tables\Columns\ToggleColumn::make('is_active')->label('Активно')->sortable(), + TextColumn::make('title') + ->label('Название') + ->searchable() + ->sortable() + ->description(fn ($record) => $record->target_group) + ->limit(50), + + BadgeColumn::make('category.title') + ->label('Категория') + ->sortable() + ->searchable() + ->color('primary'), + + TextColumn::make('price') + ->label('Стоимость') + ->sortable() + ->money('RUB') + ->alignEnd(), + + TextColumn::make('learning_time') + ->label('Часов') + ->sortable() + ->alignCenter(), + + BadgeColumn::make('form_education') + ->label('Форма') + ->formatStateUsing(fn ($state) => FormEducation::tryFrom($state->value)?->getLabel()) + ->color(fn ($state) => FormEducation::tryFrom($state->value)?->getColor()) + ->sortable(), + + IconColumn::make('is_active') + ->label('Активна') + ->boolean() + ->trueIcon('heroicon-o-check-circle') + ->falseIcon('heroicon-o-x-circle') + ->trueColor('success') + ->falseColor('danger') + ->sortable(), + + TextColumn::make('updated_at') + ->label('Обновлено') + ->dateTime('d.m.Y H:i') + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), ]) ->filters([ - // + Tables\Filters\SelectFilter::make('category_id') + ->label('Категория') + ->options(AdditionalEducationCategory::where('is_active', true)->pluck('title', 'id')) + ->searchable(), + + Tables\Filters\SelectFilter::make('form_education') + ->label('Форма обучения') + ->options(FormEducation::class), + + Tables\Filters\TernaryFilter::make('is_active') + ->label('Только активные') + ->placeholder('Все') + ->trueLabel('Активные') + ->falseLabel('Неактивные'), ]) ->actions([ - Tables\Actions\EditAction::make(), + Tables\Actions\EditAction::make() + ->iconButton() + ->tooltip('Редактировать'), + + Tables\Actions\ViewAction::make() + ->iconButton() + ->tooltip('Просмотреть'), ]) ->bulkActions([ Tables\Actions\BulkActionGroup::make([ - Tables\Actions\DeleteBulkAction::make(), + Tables\Actions\DeleteBulkAction::make() + ->label('Удалить выбранные') + ->modalHeading('Удаление программ') + ->modalDescription('Вы уверены, что хотите удалить выбранные программы ДПО?'), ]), - ]); + ]) + ->emptyStateActions([ + Tables\Actions\CreateAction::make() + ->label('Добавить программу'), + ]) + ->defaultSort('title'); } public static function getRelations(): array { - return [ - // - ]; + return []; } public static function getPages(): array @@ -481,4 +245,4 @@ class AdditionalEducationResource extends Resource 'edit' => Pages\EditAdditionalEducation::route('/{record}/edit'), ]; } -} +} \ No newline at end of file diff --git a/app/Filament/Resources/AdditionalEducationResource/Pages/CreateAdditionalEducation.php b/app/Filament/Resources/AdditionalEducationResource/Pages/CreateAdditionalEducation.php index e8615ed..aed4530 100644 --- a/app/Filament/Resources/AdditionalEducationResource/Pages/CreateAdditionalEducation.php +++ b/app/Filament/Resources/AdditionalEducationResource/Pages/CreateAdditionalEducation.php @@ -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 { diff --git a/app/Filament/Resources/AdditionalEducationResource/Pages/EditAdditionalEducation.php b/app/Filament/Resources/AdditionalEducationResource/Pages/EditAdditionalEducation.php index fae9c11..ef8b8ef 100644 --- a/app/Filament/Resources/AdditionalEducationResource/Pages/EditAdditionalEducation.php +++ b/app/Filament/Resources/AdditionalEducationResource/Pages/EditAdditionalEducation.php @@ -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; diff --git a/app/Filament/Resources/AdmissionCampaignResource.php b/app/Filament/Resources/AdmissionCampaignResource.php index 79fc2e5..70e4d21 100644 --- a/app/Filament/Resources/AdmissionCampaignResource.php +++ b/app/Filament/Resources/AdmissionCampaignResource.php @@ -2,60 +2,138 @@ namespace App\Filament\Resources; -use App\Enums\FormEducation; +use App\Enums\AdmissionCampaignStatus; use App\Enums\LevelEducational; use App\Filament\Resources\AdmissionCampaignResource\Pages; -use App\Filament\Resources\AdmissionCampaignResource\RelationManagers; use App\Models\AdmissionCampaign; use Filament\Forms; +use Filament\Forms\Components\Grid; +use Filament\Forms\Components\Repeater; +use Filament\Forms\Components\Section; +use Filament\Forms\Components\Select; use Filament\Forms\Components\TextInput; use Filament\Forms\Form; use Filament\Resources\Resource; use Filament\Tables; +use Filament\Tables\Columns\BadgeColumn; +use Filament\Tables\Columns\TextColumn; use Filament\Tables\Table; -use Illuminate\Database\Eloquent\Builder; -use Illuminate\Database\Eloquent\SoftDeletingScope; class AdmissionCampaignResource extends Resource { protected static ?string $model = AdmissionCampaign::class; - protected static ?string $navigationGroup = 'Образование'; - protected static ?string $navigationIcon = 'heroicon-o-clipboard-document-check'; - protected static ?string $pluralLabel = 'Приемная-компания'; - + protected static ?string $modelLabel = 'Приемная кампания'; public static function form(Form $form): Form { return $form ->schema([ - Forms\Components\Section::make()->schema([ - TextInput::make('name')->label('Название')->required()->columnSpanFull(), - Forms\Components\Grid::make()->schema([ - Forms\Components\Select::make('academic_year')->label('Академический год')->required() - ->options(self::generateAcademicYears()), - Forms\Components\Select::make('status')->label('Статус')->required() - ->options(['1' => 'Активный', '2' => 'Архивный', '3' => 'Скрыт']), + Section::make('Основные настройки') + ->description('Общая информация о приемной кампании') + ->collapsible() + ->schema([ + TextInput::make('name') + ->label('Название кампании') + ->required() + ->maxLength(255) + ->placeholder('Например: "Приемная кампания 2024"') + ->columnSpanFull() + ->helperText('Укажите понятное название для идентификации кампании'), + + Grid::make(2) + ->schema([ + Select::make('academic_year') + ->label('Академический год') + ->required() + ->options(self::generateAcademicYears()) + ->searchable() + ->placeholder('Выберите учебный год') + ->helperText('Выберите учебный год, к которому относится кампания'), + + Select::make('status') + ->label('Статус кампании') + ->required() + ->options(AdmissionCampaignStatus::class) + ->native(false) + ->placeholder('Выберите статус') + ->helperText('Определяет видимость и доступность кампании'), + ]), + ]), + + Section::make('Информация о наборе') + ->description('Данные о программах и местах для разных уровней образования') + ->collapsible() + ->schema([ + Repeater::make('info') + ->label('') + ->addActionLabel('Добавить уровень образования') + ->schema([ + Select::make('edu_name') + ->label('Уровень образования') + ->options(LevelEducational::class) + ->required() + ->native(false) + ->placeholder('Выберите уровень образования') + ->helperText('Выберите уровень образовательной программы'), + + Grid::make(2) + ->schema([ + Section::make('Программы') + ->schema([ + TextInput::make('total_programs') + ->label('Количество программ') + ->required() + ->numeric() + ->minValue(0) + ->placeholder('Укажите количество') + ->helperText('Общее количество программ по набору'), + ]), + + Section::make('Распределение мест') + ->schema([ + TextInput::make('och_count') + ->label('Очная форма') + ->required() + ->numeric() + ->minValue(0) + ->placeholder('Укажите количество') + ->helperText('Количество мест на очной форме'), + + TextInput::make('zaoch_count') + ->label('Заочная форма') + ->required() + ->numeric() + ->minValue(0) + ->placeholder('Укажите количество') + ->helperText('Количество мест на заочной форме'), + + TextInput::make('budget_places') + ->label('Бюджетные места') + ->required() + ->numeric() + ->minValue(0) + ->placeholder('Укажите количество') + ->helperText('Количество бюджетных мест'), + + TextInput::make('non_budget_places') + ->label('Платные места') + ->required() + ->numeric() + ->minValue(0) + ->placeholder('Укажите количество') + ->helperText('Количество платных мест'), + ]), + ]), + ]) + ->itemLabel(fn (array $state): ?string => + LevelEducational::tryFrom($state['edu_name'] ?? '')?->getLabel() ?? 'Новый уровень') + ->collapsible() + ->cloneable() + ->columnSpanFull(), ]), - ]), - Forms\Components\Section::make()->schema([ - Forms\Components\Repeater::make('info')->schema([ - Forms\Components\Select::make('edu_name')->options(LevelEducational::class), - Forms\Components\Grid::make(2)->schema([ - Forms\Components\Section::make()->schema([ - TextInput::make('total_programs')->label('Количество программ по набору')->integer()->required(), - ]), - Forms\Components\Section::make('Места')->schema([ - TextInput::make('och_count')->label('Количество мест (Очная форма)')->integer()->required(), - TextInput::make('zaoch_count')->label('Количество мест (Заочная форма)')->integer()->required(), - TextInput::make('budget_places')->label('Количество бюджетных мест')->integer()->required(), - TextInput::make('non_budget_places')->label('Количество платных мест')->integer()->required(), - ]), - ]), - ])->columnSpanFull(), - ]), ]); } @@ -63,28 +141,65 @@ class AdmissionCampaignResource extends Resource { return $table ->columns([ - Tables\Columns\TextColumn::make('name'), - Tables\Columns\TextColumn::make('academic_year'), - Tables\Columns\TextColumn::make('status'), + TextColumn::make('name') + ->label('Название') + ->searchable() + ->sortable() + ->description(fn ($record) => $record->academic_year), + + BadgeColumn::make('status') + ->label('Статус') + ->formatStateUsing(fn ($state) => AdmissionCampaignStatus::tryFrom($state)?->getLabel()) + ->color(fn ($state) => AdmissionCampaignStatus::tryFrom($state)?->getColor()) + ->sortable(), + + TextColumn::make('info_count') + ->label('Программ') + ->getStateUsing(fn ($record) => count($record->info ?? [])) + ->badge(), + + TextColumn::make('updated_at') + ->label('Обновлено') + ->dateTime('d.m.Y H:i') + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), ]) ->filters([ - // + Tables\Filters\SelectFilter::make('status') + ->label('Статус') + ->options(AdmissionCampaignStatus::class), + + Tables\Filters\SelectFilter::make('academic_year') + ->label('Учебный год') + ->options(self::generateAcademicYears()), ]) ->actions([ - Tables\Actions\EditAction::make(), + Tables\Actions\EditAction::make() + ->iconButton() + ->tooltip('Редактировать'), + + Tables\Actions\ViewAction::make() + ->iconButton() + ->tooltip('Просмотреть'), ]) ->bulkActions([ Tables\Actions\BulkActionGroup::make([ - Tables\Actions\DeleteBulkAction::make(), + Tables\Actions\DeleteBulkAction::make() + ->label('Удалить выбранные') + ->modalHeading('Удаление приемных кампаний') + ->modalDescription('Вы уверены, что хотите удалить выбранные кампании? Это действие нельзя отменить.'), ]), - ]); + ]) + ->emptyStateActions([ + Tables\Actions\CreateAction::make() + ->label('Создать кампанию'), + ]) + ->defaultSort('academic_year', 'desc'); } public static function getRelations(): array { - return [ - // - ]; + return []; } public static function getPages(): array @@ -99,15 +214,15 @@ class AdmissionCampaignResource extends Resource protected static function generateAcademicYears(): array { $currentYear = (int) date('Y') - 5; - $yearsAhead = 10; // Количество лет вперед + $yearsAhead = 10; $academicYears = []; for ($i = 0; $i < $yearsAhead; $i++) { $startYear = $currentYear + $i; $endYear = $startYear + 1; - $academicYears[$startYear] = "{$startYear}/{$endYear}"; + $academicYears["{$startYear}/{$endYear}"] = "{$startYear}/{$endYear}"; } return $academicYears; } -} +} \ No newline at end of file diff --git a/app/Filament/Resources/AdmissionCampaignResource/Pages/CreateAdmissionCampaign.php b/app/Filament/Resources/AdmissionCampaignResource/Pages/CreateAdmissionCampaign.php index ba38266..602bc11 100644 --- a/app/Filament/Resources/AdmissionCampaignResource/Pages/CreateAdmissionCampaign.php +++ b/app/Filament/Resources/AdmissionCampaignResource/Pages/CreateAdmissionCampaign.php @@ -8,5 +8,6 @@ use Filament\Resources\Pages\CreateRecord; class CreateAdmissionCampaign extends CreateRecord { + protected static string $resource = AdmissionCampaignResource::class; } diff --git a/app/Filament/Resources/AdmissionPlanResource.php b/app/Filament/Resources/AdmissionPlanResource.php index 91f1e2a..9007767 100644 --- a/app/Filament/Resources/AdmissionPlanResource.php +++ b/app/Filament/Resources/AdmissionPlanResource.php @@ -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 diff --git a/app/Filament/Resources/ContactWidgetResource.php b/app/Filament/Resources/ContactWidgetResource.php index 4b06a7c..8ff6c40 100644 --- a/app/Filament/Resources/ContactWidgetResource.php +++ b/app/Filament/Resources/ContactWidgetResource.php @@ -24,46 +24,128 @@ class ContactWidgetResource extends Resource { protected static ?string $model = ContactWidget::class; + protected static ?string $pluralLabel = 'Контактная информация'; + + protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack'; + protected static ?string $navigationGroup = 'Виджеты'; + + public static function form(Form $form): Form { return $form ->schema([ - Forms\Components\Section::make('')->schema([ - Tabs::make('Tabs') - ->tabs([ - Tabs\Tab::make('Основная информация') - ->schema([ - Forms\Components\Grid::make(2)->schema([ - TextInput::make('title')->label('Название ресурса')->required() - ->live(onBlur: true) - ->afterStateUpdated(function (string $operation, string|null $state, Forms\Set $set) { - $set('slug', Str::slug($state)); - $set('seo.title', $state); - }), - TextInput::make('slug')->label('Slug')->unique(ignoreRecord: true)->readOnly()->required(), - Toggle::make('is_active')->default(true)->label('Активный ресурс')->inline(false), - ]) - ]), - Tabs\Tab::make('Содержание ресурса') - ->schema([ - Repeater::make('content')->label('Ресурсы')->schema([ - TextInput::make('title')->label('Главный заголовок столбца')->required(), - Repeater::make('items')->label('Контакты')->schema([ - TextInput::make('header')->label('Заголовок')->required(), - Repeater::make('details')->label('Компонент контакта')->schema([ - Forms\Components\Grid::make(2)->schema([ - TextInput::make('content')->label('содержание')->required(), - TextInput::make('url')->label('Ссылка(Необязательно)'), - ]), - ]), - ]), - ])->collapsed()->required(), - ]), + Forms\Components\Section::make('Ресурс') + ->description('Настройка контактных ресурсов') + ->collapsible() + ->schema([ + Tabs::make('Настройки ресурса') + ->persistTabInQueryString() + ->columnSpanFull() + ->tabs([ + Tabs\Tab::make('Основная информация') + ->icon('heroicon-o-information-circle') + ->schema([ + Forms\Components\Grid::make(2) + ->schema([ + TextInput::make('title') + ->label('Название ресурса') + ->placeholder('Введите название ресурса') + ->helperText('Это название будет отображаться в интерфейсе') + ->required() + ->maxLength(255) + ->live(onBlur: true) + ->afterStateUpdated(function (string $operation, string|null $state, Forms\Set $set) { + $set('slug', Str::slug($state)); + $set('seo.title', $state); + }) + ->columnSpan(1), - ]), - ]), + TextInput::make('slug') + ->label('URL-адрес (Slug)') + ->helperText('Автоматически генерируется из названия') + ->hintIcon('heroicon-o-information-circle', tooltip: 'Изменить можно только вручную') + ->unique(ignoreRecord: true) + ->readOnly() + ->required() + ->columnSpan(1), + + Toggle::make('is_active') + ->label('Активность ресурса') + ->helperText('Отключите, чтобы скрыть ресурс') + ->default(true) + ->inline(false) + ->onColor('success') + ->offColor('danger') + ->columnSpanFull(), + ]) + ]), + + Tabs\Tab::make('Содержание ресурса') + ->icon('heroicon-o-document-text') + ->schema([ + Repeater::make('content') + ->label('Структура ресурса') + ->helperText('Добавьте столбцы с контактной информацией') + ->addActionLabel('Добавить столбец') + ->itemLabel(fn (array $state): ?string => $state['title'] ?? 'Новый столбец') + ->collapsible() + ->cloneable() + ->grid(2) + ->schema([ + TextInput::make('title') + ->label('Заголовок столбца') + ->placeholder('Например: Контакты') + ->helperText('Основной заголовок для группы контактов') + ->required() + ->maxLength(255), + + Repeater::make('items') + ->label('Контактные блоки') + ->helperText('Добавьте контактные блоки в этот столбец') + ->addActionLabel('Добавить контактный блок') + ->itemLabel(fn (array $state): ?string => $state['header'] ?? 'Новый контакт') + ->collapsible() + ->cloneable() + ->schema([ + TextInput::make('header') + ->label('Заголовок контакта') + ->placeholder('Например: Телефон') + ->helperText('Название контактной информации') + ->required() + ->maxLength(255), + + Repeater::make('details') + ->label('Детали контакта') + ->helperText('Добавьте контактные данные') + ->addActionLabel('Добавить деталь') + ->collapsible() + ->cloneable() + ->schema([ + Forms\Components\Grid::make(2) + ->schema([ + TextInput::make('content') + ->label('Значение') + ->placeholder('Например: +7 (123) 456-78-90') + ->helperText('Основная контактная информация') + ->columnSpanFull() + ->required(), + + TextInput::make('url') + ->label('Ссылка') + ->placeholder('https://example.com') + ->helperText('Необязательная ссылка, связанная с контактом') + ->url() + ->columnSpanFull(), + ]) + ]) + ]) + ]) + ->required(), + ]), + ]), + ]), ]); } diff --git a/app/Filament/Resources/CustomFormResource.php b/app/Filament/Resources/CustomFormResource.php index 0e2d687..ad93ab1 100644 --- a/app/Filament/Resources/CustomFormResource.php +++ b/app/Filament/Resources/CustomFormResource.php @@ -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; diff --git a/app/Filament/Resources/DepartmentResource.php b/app/Filament/Resources/DepartmentResource.php index 1605319..0c0421f 100644 --- a/app/Filament/Resources/DepartmentResource.php +++ b/app/Filament/Resources/DepartmentResource.php @@ -2,476 +2,155 @@ namespace App\Filament\Resources; -use App\Enums\CustomFormStatus; -use App\Enums\PostStatus; +use App\Filament\Components\Forms\ItemForm\Pages\ContentBuilderItem; use App\Filament\Resources\DepartmentResource\Pages; use App\Filament\Resources\DepartmentResource\RelationManagers; -use App\Helpers\ByteConverter; -use App\Models\Category; -use App\Models\CustomForm; use App\Models\Department; use App\Models\Faculty; -use App\Models\Page; -use App\Models\PageReferenceList; -use App\Models\Post; use Filament\Forms; -use Filament\Forms\Components\Builder; -use Filament\Forms\Components\FileUpload; -use Filament\Forms\Components\Hidden; -use Filament\Forms\Components\RichEditor; use Filament\Forms\Components\Section; use Filament\Forms\Components\Select; -use Filament\Forms\Components\Tabs; use Filament\Forms\Components\TextInput; use Filament\Forms\Components\Toggle; use Filament\Forms\Form; use Filament\Resources\Resource; use Filament\Tables; +use Filament\Tables\Columns\IconColumn; use Filament\Tables\Columns\TextColumn; use Filament\Tables\Table; -use Illuminate\Database\Eloquent\SoftDeletingScope; -use Illuminate\Support\Carbon; use Illuminate\Support\Str; -use Livewire\Features\SupportFileUploads\TemporaryUploadedFile; class DepartmentResource extends Resource { protected static ?string $model = Department::class; - protected static ?string $navigationGroup = 'Структура института'; - - protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack'; - + protected static ?string $navigationIcon = 'heroicon-o-building-office'; protected static ?string $pluralLabel = 'Кафедры'; - - public static ?string $label = 'Кафедра'; - - + protected static ?string $modelLabel = 'кафедра'; protected static ?string $navigationParentItem = 'Факультеты'; public static function form(Form $form): Form { return $form ->schema([ - Section::make() - ->schema([ - Tabs::make('Tabs') - ->tabs([ - Tabs\Tab::make('Основная информация') + Forms\Components\Tabs::make('Настройки кафедры') + ->persistTabInQueryString() + ->columnSpanFull() + ->tabs([ + Forms\Components\Tabs\Tab::make('Основные данные') + ->icon('heroicon-o-information-circle') + ->schema([ + Section::make('Идентификация') + ->description('Основная информация о кафедре') ->schema([ - Forms\Components\Grid::make()->schema([ - TextInput::make('title')->label('Название факультета')->required() - ->live(onBlur: true) - ->afterStateUpdated(function (string $operation, string|null $state, Forms\Set $set) { - $set('slug', Str::slug($state)); - $set('seo.title', $state); - }), - TextInput::make('slug')->label('Slug')->unique(ignoreRecord: true)->readOnly()->required(), - ]), - Toggle::make('is_active')->default(true)->label('Активный факультет')->inline(false), - Forms\Components\Select::make('faculty_id') - ->options(Faculty::all()->pluck('title', 'id')) - ->label('Факультет') - ->required(), - ]), - Tabs\Tab::make('Описание факультета') - ->schema([ - Builder::make('content')->label('')->blocks([ - Builder\Block::make('heading')->label('Заголовок') - ->schema([ - TextInput::make('id')->hidden()->integer()->default(rand(2335235,324634264263426)), - TextInput::make('content') - ->label('') - ->live(onBlur: true) - ->afterStateUpdated(function (string $operation, string $state, Forms\Set $set, $get) { - }), - ]), - Builder\Block::make('paragraph') - ->schema([ - RichEditor::make('content') - ->toolbarButtons([ - 'blockquote', - 'bold', - 'bulletList', - 'italic', - 'link', - 'orderedList', - 'redo', - 'strike', - 'underline', - 'undo', - ]) - ->label(''), - ])->label('Текст'), - Builder\Block::make('files') - ->schema([ - Forms\Components\Repeater::make('file')->schema([ - Hidden::make('expansion')->required(), - Hidden::make('size')->required(), - TextInput::make('title') - ->required() - ->maxLength(255) - ->autofocus(), - FileUpload::make('path') - ->required() - ->getUploadedFileNameForStorageUsing( - fn (TemporaryUploadedFile $file): string => - str(Str::slug(pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME) . '-' . Carbon::now()->timestamp ) . '.' . $file->getClientOriginalExtension()) - ) - ->acceptedFileTypes([ - 'application/pdf', - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - 'application/vnd.openxmlformats-officedocument.presentationml.presentation', - 'application/zip' - ]) - ->maxSize(512000) - ->disk('public') - ->directory('files') - ->downloadable() - ->afterStateUpdated(function ($set, $state) { - $set('expansion', $state?->getClientOriginalExtension()); - $set('size', ByteConverter::bytesToHuman($state?->getSize())); - }) - ->visibility('public') - ]), - ]), - Builder\Block::make('person') - ->schema([ - TextInput::make('name') - ->label('Имя') - ->required() - ->maxLength(255), - FileUpload::make('photo') - ->label('Фотография') - ->image() - ->disk('public') - ->directory('images') - ->imageEditor(), - Forms\Components\Repeater::make('info')->schema([ - Forms\Components\Grid::make(2)->schema([ - TextInput::make('column') - ->required() - ->maxLength(255), - TextInput::make('content') - ->required() - ->maxLength(255), - ]), - ])->minItems(1), - ]), - Builder\Block::make('stepper') - ->schema([ - TextInput::make('step_name') - ->label('Название шага') - ->required() - ->maxLength(255), - Forms\Components\Repeater::make('steps')->schema([ - TextInput::make('title') - ->required() - ->maxLength(255)->columnSpanFull(), - RichEditor::make('content')->required(), - ])->minItems(1), - ]), - Builder\Block::make('tabs') - ->schema([ - Forms\Components\Repeater::make('tab')->schema([ - TextInput::make('title') - ->required() - ->maxLength(255)->columnSpanFull(), - \Filament\Forms\Components\Builder::make('content')->label('')->blocks([ - Builder\Block::make('heading')->label('Заголовок') - ->schema([ - TextInput::make('id')->hidden()->integer()->default(rand(2335235,324634264263426)), - TextInput::make('content') - ->label('') - ->live(onBlur: true) - ->afterStateUpdated(function (string $operation, string $state, Forms\Set $set, $get) { - }), - ]), - Builder\Block::make('paragraph') - ->schema([ - RichEditor::make('content') - ->toolbarButtons([ - 'blockquote', - 'bold', - 'bulletList', - 'italic', - 'link', - 'orderedList', - 'redo', - 'strike', - 'underline', - 'undo', - ]) - ->label(''), - ])->label('Текст'), - Builder\Block::make('files') - ->schema([ - Forms\Components\Repeater::make('file')->schema([ - TextInput::make('title') - ->required() - ->maxLength(255) - ->autofocus(), - FileUpload::make('path') - ->required() - ->acceptedFileTypes([ - 'application/pdf', - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - 'application/vnd.openxmlformats-officedocument.presentationml.presentation', - 'application/zip' - ]) - ->maxSize(512000) - ->disk('public') - ->directory('files') - ->downloadable() - ->visibility('public') - ]), - ]), - Builder\Block::make('person') - ->schema([ - TextInput::make('name') - ->label('Имя') - ->required() - ->maxLength(255), - FileUpload::make('photo') - ->label('Фотография') - ->image() - ->disk('public') - ->directory('images') - ->imageEditor(), - Forms\Components\Repeater::make('info')->schema([ - Forms\Components\Grid::make(2)->schema([ - TextInput::make('column') - ->required() - ->maxLength(255), - TextInput::make('content') - ->required() - ->maxLength(255), - ]), - ])->minItems(1), - ]), - Builder\Block::make('stepper') - ->schema([ - TextInput::make('step_name') - ->label('Название шага') - ->required() - ->maxLength(255), - Forms\Components\Repeater::make('steps')->schema([ - TextInput::make('title') - ->required() - ->maxLength(255)->columnSpanFull(), - RichEditor::make('content')->required(), - ])->minItems(1), - ]), - Builder\Block::make('images') - ->schema([ - FileUpload::make('url') - ->label('Изображение(-я)') - ->image() - ->multiple() - ->reorderable() - ->maxFiles(5) - ->disk('public') - ->directory('images') - ->imageEditor() - ->required(), - TextInput::make('alt') - ->label('Описание') - ->placeholder('Необязяательно') - ])->label('Слайдер изображений'), - Builder\Block::make('image') - ->schema([ - FileUpload::make('url') - ->label('Изображение(-я)') - ->image() - ->multiple() - ->reorderable() - ->maxFiles(5) - ->disk('public') - ->directory('images') - ->imageEditor() - ->required(), - TextInput::make('alt') - ->label('Описание') - ->placeholder('Необязяательно') - ])->label('Изображение'), - Builder\Block::make('video') - ->schema([ - TextInput::make('mime')->readOnly(), - TextInput::make('title') - ->required() - ->maxLength(255) - ->autofocus(), - FileUpload::make('path') - ->required() - ->acceptedFileTypes([ - 'video/mp4', - 'video/quicktime', - 'video/x-msvideo', - 'video/x-ms-wmv', - 'video/avi', - 'video/webm', - 'video/ogg', - 'video/3gpp', - 'video/3gpp2', - 'video/x-m4v', - ]) - ->disk('public') - ->directory('videos') - ->afterStateUpdated(fn (callable $set, $state) => $set('mime', $state?->getMimeType())), - ]), - Builder\Block::make('postsList') - ->schema([ - Forms\Components\Grid::make(2)->schema([ - TextInput::make('count') - ->label('Количество запией') - ->integer(), - Select::make('category') - ->options(Category::all()->pluck('title', 'id')) - ]), - ])->label('Список новостей'), - ]) - ->collapsed() - ->blockNumbers(false) - ->collapsible() - ->addActionLabel('Добавить новый блок'), - ])->minItems(1), - ]), - Builder\Block::make('images') - ->schema([ - FileUpload::make('url') - ->label('Изображение(-я)') - ->image() - ->multiple() - ->reorderable() - ->maxFiles(5) - ->disk('public') - ->directory('images') - ->imageEditor() - ->required(), - TextInput::make('alt') - ->label('Описание') - ->placeholder('Необязяательно') - ])->label('Слайдер изображений'), - Builder\Block::make('image') - ->schema([ - FileUpload::make('url') - ->label('Изображение(-я)') - ->image() - ->multiple() - ->reorderable() - ->maxFiles(5) - ->disk('public') - ->directory('images') - ->imageEditor() - ->required(), - TextInput::make('alt') - ->label('Описание') - ->placeholder('Необязяательно') - ])->label('Изображение'), - Builder\Block::make('video') - ->schema([ - TextInput::make('mime')->readOnly(), - TextInput::make('title') - ->required() - ->maxLength(255) - ->autofocus(), - FileUpload::make('path') - ->required() - ->acceptedFileTypes([ - 'video/mp4', - 'video/quicktime', - 'video/x-msvideo', - 'video/x-ms-wmv', - 'video/avi', - 'video/webm', - 'video/ogg', - 'video/3gpp', - 'video/3gpp2', - 'video/x-m4v', - ]) - ->disk('public') - ->directory('videos') - ->afterStateUpdated(fn (callable $set, $state) => $set('mime', $state?->getMimeType())), - ]), - Builder\Block::make('postsList') - ->schema([ - Forms\Components\Grid::make(2)->schema([ - TextInput::make('count') - ->label('Количество запией') - ->integer(), - Select::make('category') - ->options(Category::all()->pluck('title', 'id')) - ]), - ])->label('Список новостей'), - Builder\Block::make('postItem') - ->schema([ - Select::make('post') - ->options(Post::query()->where('status', PostStatus::PUBLISHED)->pluck('title', 'id')) - ->searchable() - ->required(), - ])->label('Новость'), - Builder\Block::make('pageItem') - ->schema([ - Select::make('page') - ->options(Page::query()->where('title', '!=' , null)->where('is_visible', true)->pluck('title', 'id')) - ->searchable() - ->required(), - ])->label('Страница'), - Builder\Block::make('customForm') - ->schema([ - Select::make('form') - ->options(CustomForm::query()->where('status', CustomFormStatus::PUBLISHED)->pluck('title', 'form_id')) - ->searchable() - ->required(), - ])->label('Форма'), - Builder\Block::make('pageResourceList') - ->schema([ - Select::make('resource') - ->options(PageReferenceList::query()->where('is_active', true)->pluck('title', 'slug')) - ->searchable() - ->required(), - ])->label('Ресурсы'), - ]) - ->collapsed() - ->blockNumbers(false) - ->collapsible() + TextInput::make('title') + ->label('Полное название') ->required() - ->blockPickerColumns(3) - ->blockPickerWidth('2xl') - ->addActionLabel('Добавить новый блок'), + ->maxLength(255) + ->placeholder('Например: Кафедра программной инженерии') + ->live(onBlur: true) + ->afterStateUpdated(function (string $operation, ?string $state, Forms\Set $set) { + $set('slug', Str::slug($state)); + }) + ->helperText('Официальное название кафедры'), + + TextInput::make('slug') + ->label('URL-идентификатор') + ->required() + ->maxLength(255) + ->unique(ignoreRecord: true) + ->helperText('Человеко-понятный URL для страницы кафедры'), + + Select::make('faculty_id') + ->label('Факультет') + ->options(Faculty::query()->orderBy('title')->pluck('title', 'id')) + ->searchable() + ->preload() + ->required() + ->native(false) + ->helperText('К какому факультету относится кафедра'), + + Toggle::make('is_active') + ->label('Активная кафедра') + ->inline(false) + ->default(true) + ->helperText('Отображать ли кафедру на сайте'), ]), ]), - ]) + + Forms\Components\Tabs\Tab::make('Контент') + ->icon('heroicon-o-document-text') + ->schema([ + ContentBuilderItem::getItem('content') + ]), + ]), ]); - - } - - public static function table(Table $table): Table { return $table ->columns([ - TextColumn::make('id')->label('ID')->sortable(), - TextColumn::make('title')->label('Название')->sortable()->searchable(), - Tables\Columns\TextColumn::make('faculty.title')->label('Факультет')->words(2), - TextColumn::make('created_at')->label('Дата создания')->sortable(), - Tables\Columns\ToggleColumn::make('is_active')->label('Активно')->sortable(), + TextColumn::make('title') + ->label('Название') + ->searchable() + ->sortable() + ->description(fn ($record) => $record->faculty->abbreviation ?? ''), + + TextColumn::make('faculty.title') + ->label('Факультет') + ->sortable() + ->toggleable(isToggledHiddenByDefault: false), + + IconColumn::make('is_active') + ->label('Статус') + ->boolean() + ->trueIcon('heroicon-o-check-circle') + ->falseIcon('heroicon-o-x-circle') + ->trueColor('success') + ->falseColor('danger') + ->sortable(), + + TextColumn::make('updated_at') + ->label('Обновлено') + ->dateTime('d.m.Y H:i') + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), ]) ->filters([ - // + Tables\Filters\SelectFilter::make('faculty_id') + ->label('Факультет') + ->options(Faculty::query()->orderBy('title')->pluck('title', 'id')) + ->searchable(), + + Tables\Filters\TernaryFilter::make('is_active') + ->label('Только активные') + ->placeholder('Все') + ->trueLabel('Активные') + ->falseLabel('Неактивные'), ]) ->actions([ - Tables\Actions\EditAction::make(), + Tables\Actions\EditAction::make() + ->iconButton() + ->tooltip('Редактировать'), + + Tables\Actions\ViewAction::make() + ->iconButton() + ->tooltip('Просмотреть'), ]) ->bulkActions([ Tables\Actions\BulkActionGroup::make([ - Tables\Actions\DeleteBulkAction::make(), + Tables\Actions\DeleteBulkAction::make() + ->label('Удалить выбранные') + ->modalHeading('Удаление кафедр') + ->modalDescription('Вы уверены, что хотите удалить выбранные кафедры?'), ]), - ]); + ]) + ->emptyStateActions([ + Tables\Actions\CreateAction::make() + ->label('Добавить кафедру'), + ]) + ->defaultSort('title'); } public static function getRelations(): array @@ -491,4 +170,4 @@ class DepartmentResource extends Resource 'edit' => Pages\EditDepartment::route('/{record}/edit'), ]; } -} +} \ No newline at end of file diff --git a/app/Filament/Resources/DepartmentResource/Pages/CreateDepartment.php b/app/Filament/Resources/DepartmentResource/Pages/CreateDepartment.php index 299b5e3..43ad9f7 100644 --- a/app/Filament/Resources/DepartmentResource/Pages/CreateDepartment.php +++ b/app/Filament/Resources/DepartmentResource/Pages/CreateDepartment.php @@ -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 diff --git a/app/Filament/Resources/DepartmentResource/Pages/EditDepartment.php b/app/Filament/Resources/DepartmentResource/Pages/EditDepartment.php index 5fb2f96..4e3e001 100644 --- a/app/Filament/Resources/DepartmentResource/Pages/EditDepartment.php +++ b/app/Filament/Resources/DepartmentResource/Pages/EditDepartment.php @@ -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 diff --git a/app/Filament/Resources/DepartmentResource/RelationManagers/ProgramsRelationManager.php b/app/Filament/Resources/DepartmentResource/RelationManagers/ProgramsRelationManager.php index ca343a2..b09e89e 100644 --- a/app/Filament/Resources/DepartmentResource/RelationManagers/ProgramsRelationManager.php +++ b/app/Filament/Resources/DepartmentResource/RelationManagers/ProgramsRelationManager.php @@ -15,14 +15,22 @@ use Illuminate\Database\Eloquent\SoftDeletingScope; class ProgramsRelationManager extends RelationManager { protected static string $relationship = 'programs'; + protected static ?string $title = 'Образовательные программы кафедры'; public function form(Form $form): Form { return $form ->schema([ - Forms\Components\TextInput::make('name') - ->required() - ->maxLength(255), + Forms\Components\Section::make('Основная информация') + ->description('Связь образовательной программы с кафедрой') + ->schema([ + Forms\Components\TextInput::make('name') + ->label('Название программы') + ->required() + ->maxLength(255) + ->placeholder('Например: Информатика и вычислительная техника') + ->helperText('Полное название образовательной программы'), + ]) ]); } @@ -31,24 +39,65 @@ class ProgramsRelationManager extends RelationManager return $table ->recordTitleAttribute('name') ->columns([ - Tables\Columns\TextColumn::make('name'), + Tables\Columns\TextColumn::make('name') + ->label('Название программы') + ->searchable() + ->sortable() + ->wrap(), + + Tables\Columns\TextColumn::make('status') + ->label('Статус') + ->badge() + ->formatStateUsing(fn($state): string => EducationalProgramStatus::tryFrom($state)->getLabel()) + ->color(fn($state): string => EducationalProgramStatus::tryFrom($state)->getColor()) + ->sortable(), ]) ->filters([ - // + Tables\Filters\SelectFilter::make('status') + ->label('Статус программы') + ->options(EducationalProgramStatus::class) + ->default(EducationalProgramStatus::PUBLISHED->value), ]) ->headerActions([ AttachAction::make() - ->recordSelectOptionsQuery(fn (Builder $query) => $query->where('status', EducationalProgramStatus::PUBLISHED)), - + ->label('Добавить программу') + ->modalHeading('Добавление программы к кафедре') + ->modalSubmitActionLabel('Добавить') + ->preloadRecordSelect() + ->recordSelectOptionsQuery(fn(Builder $query) => $query->where('status', EducationalProgramStatus::PUBLISHED)) + ->recordSelect( + fn(Forms\Components\Select $select) => $select + ->placeholder('Выберите программу') + ->label('Образовательная программа') + ->helperText('Только опубликованные программы') + ->searchable() + ->columnSpanFull() + ) ]) ->actions([ - Tables\Actions\EditAction::make(), - Tables\Actions\DetachAction::make(), + + Tables\Actions\DetachAction::make() + ->iconButton() + ->tooltip('Открепить программу') + ->modalHeading('Открепление программы') + ->modalSubmitActionLabel('Открепить') + ->modalDescription('Вы уверены, что хотите открепить эту программу от кафедры?'), ]) ->bulkActions([ Tables\Actions\BulkActionGroup::make([ - Tables\Actions\DetachBulkAction::make(), + Tables\Actions\DetachBulkAction::make() + ->label('Открепить выбранные') + ->modalHeading('Открепление программ') + ->modalSubmitActionLabel('Открепить') + ->modalDescription('Вы уверены, что хотите открепить выбранные программы от кафедры?'), ]), - ]); + ]) + ->emptyStateActions([ + AttachAction::make() + ->label('Добавить программу'), + ]) + ->defaultSort('name') + ->deferLoading() + ->persistFiltersInSession(); } -} +} \ No newline at end of file diff --git a/app/Filament/Resources/DepartmentResource/RelationManagers/TeachersRelationManager.php b/app/Filament/Resources/DepartmentResource/RelationManagers/TeachersRelationManager.php index 16f8815..a11d8cd 100644 --- a/app/Filament/Resources/DepartmentResource/RelationManagers/TeachersRelationManager.php +++ b/app/Filament/Resources/DepartmentResource/RelationManagers/TeachersRelationManager.php @@ -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(); } -} +} \ No newline at end of file diff --git a/app/Filament/Resources/DepartmentResource/RelationManagers/WorkersRelationManager.php b/app/Filament/Resources/DepartmentResource/RelationManagers/WorkersRelationManager.php index 6c08b63..b13ca2e 100644 --- a/app/Filament/Resources/DepartmentResource/RelationManagers/WorkersRelationManager.php +++ b/app/Filament/Resources/DepartmentResource/RelationManagers/WorkersRelationManager.php @@ -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(); } -} +} \ No newline at end of file diff --git a/app/Filament/Resources/DirectionAdditionalEducationResource.php b/app/Filament/Resources/DirectionAdditionalEducationResource.php index 54f3015..152d6e8 100644 --- a/app/Filament/Resources/DirectionAdditionalEducationResource.php +++ b/app/Filament/Resources/DirectionAdditionalEducationResource.php @@ -3,17 +3,18 @@ namespace App\Filament\Resources; use App\Filament\Resources\DirectionAdditionalEducationResource\Pages; -use App\Filament\Resources\DirectionAdditionalEducationResource\RelationManagers; use App\Models\DirectionAdditionalEducation; use Filament\Forms; +use Filament\Forms\Components\Grid; +use Filament\Forms\Components\Section; use Filament\Forms\Components\TextInput; +use Filament\Forms\Components\Toggle; use Filament\Forms\Form; use Filament\Resources\Resource; use Filament\Tables; +use Filament\Tables\Columns\IconColumn; use Filament\Tables\Columns\TextColumn; use Filament\Tables\Table; -use Illuminate\Database\Eloquent\Builder; -use Illuminate\Database\Eloquent\SoftDeletingScope; use Illuminate\Support\Str; class DirectionAdditionalEducationResource extends Resource @@ -25,25 +26,44 @@ class DirectionAdditionalEducationResource extends Resource public static ?string $label = 'Направление'; protected static ?string $pluralLabel = 'Направления дополнительного образования'; protected static ?string $navigationParentItem = 'Дополнительное Образование'; - - protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack'; + protected static ?string $navigationIcon = 'heroicon-o-arrow-trending-up'; public static function form(Form $form): Form { return $form ->schema([ - Forms\Components\Section::make()->schema([ - Forms\Components\Grid::make()->schema([ - TextInput::make('title')->label('Заголовок')->required() - ->live(onBlur: true) - ->afterStateUpdated(function (string $operation, string|null $state, Forms\Set $set) { - $set('slug', Str::slug($state)); - $set('seo.title', $state); - }), - TextInput::make('slug')->label('Slug')->unique(ignoreRecord: true)->readOnly()->required(), + Section::make('Основная информация') + ->description('Заполните данные о направлении дополнительного образования') + ->collapsible() + ->schema([ + Grid::make(2) + ->schema([ + TextInput::make('title') + ->label('Название направления') + ->required() + ->maxLength(255) + ->placeholder('Например: "Информационные технологии"') + ->live(onBlur: true) + ->afterStateUpdated(function (string $operation, ?string $state, Forms\Set $set) { + $set('slug', Str::slug($state)); + }) + ->helperText('Укажите понятное название направления'), + + TextInput::make('slug') + ->label('URL-идентификатор') + ->required() + ->maxLength(255) + ->unique(ignoreRecord: true) + ->helperText('Человеко-понятный URL для направления'), + ]), + + Toggle::make('is_active') + ->label('Активное направление') + ->inline(false) + ->default(true) + ->helperText('Отображать ли направление на сайте') + ->columnSpanFull(), ]), - Forms\Components\Toggle::make('is_active')->label('Активно')->columnSpanFull()->inline(false)->default(true), - ]), ]); } @@ -51,29 +71,67 @@ class DirectionAdditionalEducationResource extends Resource { return $table ->columns([ - TextColumn::make('id')->label('ID')->sortable(), - TextColumn::make('title')->label('Название')->sortable()->searchable(), - TextColumn::make('created_at')->label('Дата создания')->sortable(), - Tables\Columns\ToggleColumn::make('is_active')->label('Активно')->sortable(), + TextColumn::make('title') + ->label('Название') + ->searchable() + ->sortable() + ->limit(50), + + IconColumn::make('is_active') + ->label('Активно') + ->boolean() + ->trueIcon('heroicon-o-check-circle') + ->falseIcon('heroicon-o-x-circle') + ->trueColor('success') + ->falseColor('danger') + ->sortable(), + + TextColumn::make('created_at') + ->label('Дата создания') + ->dateTime('d.m.Y H:i') + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + + TextColumn::make('updated_at') + ->label('Обновлено') + ->dateTime('d.m.Y H:i') + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), ]) ->filters([ - // + Tables\Filters\TernaryFilter::make('is_active') + ->label('Только активные') + ->placeholder('Все') + ->trueLabel('Активные') + ->falseLabel('Неактивные'), ]) ->actions([ - Tables\Actions\EditAction::make(), + Tables\Actions\EditAction::make() + ->iconButton() + ->tooltip('Редактировать'), + + Tables\Actions\ViewAction::make() + ->iconButton() + ->tooltip('Просмотреть'), ]) ->bulkActions([ Tables\Actions\BulkActionGroup::make([ - Tables\Actions\DeleteBulkAction::make(), + Tables\Actions\DeleteBulkAction::make() + ->label('Удалить выбранные') + ->modalHeading('Удаление направлений') + ->modalDescription('Вы уверены, что хотите удалить выбранные направления ДПО?'), ]), - ]); + ]) + ->emptyStateActions([ + Tables\Actions\CreateAction::make() + ->label('Добавить направление'), + ]) + ->defaultSort('title'); } public static function getRelations(): array { - return [ - // - ]; + return []; } public static function getPages(): array @@ -84,4 +142,4 @@ class DirectionAdditionalEducationResource extends Resource 'edit' => Pages\EditDirectionAdditionalEducation::route('/{record}/edit'), ]; } -} +} \ No newline at end of file diff --git a/app/Filament/Resources/DirectionStudyResource.php b/app/Filament/Resources/DirectionStudyResource.php index c171796..8041efc 100644 --- a/app/Filament/Resources/DirectionStudyResource.php +++ b/app/Filament/Resources/DirectionStudyResource.php @@ -4,63 +4,132 @@ namespace App\Filament\Resources; use App\Enums\LevelEducational; use App\Filament\Resources\DirectionStudyResource\Pages; -use App\Filament\Resources\DirectionStudyResource\RelationManagers; use App\Models\DirectionStudy; use Filament\Forms; +use Filament\Forms\Components\Grid; +use Filament\Forms\Components\Section; +use Filament\Forms\Components\Select; +use Filament\Forms\Components\TextInput; use Filament\Forms\Form; use Filament\Resources\Resource; use Filament\Tables; +use Filament\Tables\Columns\BadgeColumn; +use Filament\Tables\Columns\TextColumn; use Filament\Tables\Table; -use Illuminate\Database\Eloquent\Builder; -use Illuminate\Database\Eloquent\SoftDeletingScope; use Illuminate\Support\Str; class DirectionStudyResource extends Resource { protected static ?string $model = DirectionStudy::class; - - protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack'; - - protected static ?string $pluralLabel = 'Направление подготовки'; + protected static ?string $navigationIcon = 'heroicon-o-academic-cap'; + protected static ?string $pluralLabel = 'Направления подготовки'; + protected static ?string $modelLabel = 'Направление подготовки'; protected static ?string $navigationGroup = 'Образование'; - - protected static ?string $navigationParentItem = 'Приемная-компания'; public static function form(Form $form): Form { return $form - ->schema([]); + ->schema([ + Section::make('Основная информация') + ->description('Заполните основные данные о направлении подготовки') + ->collapsible() + ->schema([ + Grid::make(2) + ->schema([ + TextInput::make('name') + ->label('Название направления') + ->required() + ->maxLength(255) + ->placeholder('Например: Информатика и вычислительная техника') + ->live(onBlur: true) + ->afterStateUpdated(function (string $operation, ?string $state, Forms\Set $set) { + $set('slug', Str::slug($state)); + }) + ->helperText('Полное название направления подготовки'), + + TextInput::make('slug') + ->label('URL-идентификатор') + ->required() + ->readOnly() + ->maxLength(255) + ->unique(ignoreRecord: true) + ->helperText('Человеко-понятный URL для направления'), + + TextInput::make('code') + ->label('Код направления') + ->required() + ->maxLength(50) + ->placeholder('Например: 09.03.01') + ->helperText('Код направления по ФГОС'), + + Select::make('lvl_edu') + ->label('Уровень образования') + ->options(LevelEducational::class) + ->required() + ->native(false) + ->placeholder('Выберите уровень') + ->helperText('Выберите уровень образовательной программы'), + ]), + ]), + ]); } public static function table(Table $table): Table { return $table ->columns([ - Tables\Columns\TextColumn::make('name')->label('Название'), - Tables\Columns\TextColumn::make('code')->label('Код направления'), - Tables\Columns\TextColumn::make('lvl_edu')->label('Уровень образования') - ->formatStateUsing(fn ($state) => $state->getLabel()) + TextColumn::make('code') + ->label('Код') + ->searchable() + ->sortable() + ->description(fn ($record) => $record->name), + + BadgeColumn::make('lvl_edu') + ->label('Уровень') + ->formatStateUsing(fn ($state) => LevelEducational::tryFrom($state->value)?->getLabel()) + ->color(fn ($state) => LevelEducational::tryFrom($state->value)?->getColor()) + ->sortable(), + + TextColumn::make('created_at') + ->label('Добавлено') + ->dateTime('d.m.Y H:i') + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), ]) ->filters([ - // + Tables\Filters\SelectFilter::make('lvl_edu') + ->label('Уровень образования') + ->options(LevelEducational::class), ]) ->actions([ - Tables\Actions\EditAction::make(), + Tables\Actions\EditAction::make() + ->iconButton() + ->tooltip('Редактировать'), + + Tables\Actions\ViewAction::make() + ->iconButton() + ->tooltip('Просмотреть'), ]) ->bulkActions([ Tables\Actions\BulkActionGroup::make([ - Tables\Actions\DeleteBulkAction::make(), + Tables\Actions\DeleteBulkAction::make() + ->label('Удалить выбранные') + ->modalHeading('Удаление направлений') + ->modalDescription('Вы уверены, что хотите удалить выбранные направления подготовки?'), ]), - ]); + ]) + ->emptyStateActions([ + Tables\Actions\CreateAction::make() + ->label('Добавить направление'), + ]) + ->defaultSort('code'); } public static function getRelations(): array { - return [ - // - ]; + return []; } public static function getPages(): array @@ -71,4 +140,4 @@ class DirectionStudyResource extends Resource 'edit' => Pages\EditDirectionStudy::route('/{record}/edit'), ]; } -} +} \ No newline at end of file diff --git a/app/Filament/Resources/DivisionResource.php b/app/Filament/Resources/DivisionResource.php index 56e6573..8faf67d 100644 --- a/app/Filament/Resources/DivisionResource.php +++ b/app/Filament/Resources/DivisionResource.php @@ -2,20 +2,15 @@ namespace App\Filament\Resources; -use App\Enums\PostStatus; +use App\Filament\Components\Forms\ItemForm\Pages\ContentBuilderItem; use App\Filament\Resources\DivisionResource\Pages; use App\Filament\Resources\DivisionResource\RelationManagers; -use App\Models\Category; use App\Models\Division; -use App\Models\Page; -use App\Models\Post; use Filament\Forms; +use Filament\Forms\Components\Actions\Action; use Filament\Forms\Components\Builder; -use Filament\Forms\Components\FileUpload; -use Filament\Forms\Components\Hidden; -use Filament\Forms\Components\RichEditor; +use Illuminate\Database\Eloquent\Builder as EloquentBuilder; use Filament\Forms\Components\Section; -use Filament\Forms\Components\Select; use Filament\Forms\Components\Tabs; use Filament\Forms\Components\TextInput; use Filament\Forms\Components\Toggle; @@ -24,410 +19,156 @@ use Filament\Resources\Resource; use Filament\Tables; use Filament\Tables\Columns\TextColumn; use Filament\Tables\Table; -use Illuminate\Database\Eloquent\SoftDeletingScope; use Illuminate\Support\Str; -use Mohamedsabil83\FilamentFormsTinyeditor\Components\TinyEditor; class DivisionResource extends Resource { protected static ?string $model = Division::class; - protected static ?string $navigationGroup = 'Структура института'; - protected static ?string $navigationIcon = 'heroicon-o-squares-2x2'; - - public static ?string $label = 'Подразделение'; - - protected static ?string $pluralLabel = 'Подразделения института'; + protected static ?string $modelLabel = 'Подразделение'; + protected static ?string $pluralModelLabel = 'Подразделения института'; + protected static ?int $navigationSort = 100; public static function form(Form $form): Form { return $form ->schema([ - Section::make() + Section::make('Основные настройки') + ->collapsible() ->schema([ - Tabs::make('Tabs') + Tabs::make('Конструктор подразделения') + ->persistTabInQueryString() + ->columnSpanFull() ->tabs([ Tabs\Tab::make('Основная информация') + ->icon('heroicon-o-information-circle') ->schema([ - Forms\Components\Grid::make(2)->schema([ - TextInput::make('title')->label('Заголовок')->required() - ->live(onBlur: true) - ->afterStateUpdated(function (string $operation, string|null $state, Forms\Set $set) { - $set('slug', Str::slug($state)); - }), - TextInput::make('slug')->label('Slug')->unique(ignoreRecord: true)->readOnly()->required(), - ]), - Toggle::make('is_active')->default(true)->label('Активное подразделение')->inline(false), + Forms\Components\Grid::make(2) + ->schema([ + TextInput::make('title') + ->label('Название подразделения') + ->required() + ->maxLength(255) + ->live(onBlur: true) + ->afterStateUpdated(function (string $operation, string|null $state, Forms\Set $set) { + if ($operation === 'create') { + $set('slug', Str::slug($state)); + } + }) + ->placeholder('Введите полное название подразделения') + ->helperText('Официальное название, которое будет отображаться на сайте'), + + TextInput::make('slug') + ->label('URL-адрес') + ->unique(ignoreRecord: true) + ->required() + ->readOnly() + ->helperText('Формируется автоматически из названия') + ->prefix(fn () => route('client.division.index') . '/') + ->suffixAction( + Action::make('copy') + ->icon('heroicon-s-clipboard-document-check') + ->action(function ($livewire, $state) { + $livewire->js( + 'window.navigator.clipboard.writeText("'. route('client.division.index') . '/' . $state.'"); + $tooltip("'.__('Copied to clipboard').'", { timeout: 1500 });' + ); + })), + + + + ]), + + Toggle::make('is_active') + ->label('Активно на сайте') + ->default(true) + ->inline(false) + ->helperText('Отключите, чтобы временно скрыть подразделение'), ]), - Tabs\Tab::make('Содержание') + + Tabs\Tab::make('Контент') + ->icon('heroicon-o-document-text') ->schema([ - \Filament\Forms\Components\Builder::make('description')->label('')->blocks([ - Builder\Block::make('heading')->label('Заголовок') - ->schema([ - TextInput::make('id')->hidden()->integer()->default(rand(2335235,324634264263426)), - TextInput::make('content') - ->label('') - ->live(onBlur: true) - ->afterStateUpdated(function (string $operation, string $state, Forms\Set $set, $get) { - }), - ]), - Builder\Block::make('paragraph')->label('Текст') - ->schema([ - TinyEditor::make('content') - ->label('') - ->profile('test') - ]), - Builder\Block::make('files')->label('Файлы') - ->schema([ - Forms\Components\Repeater::make('file')->schema([ - TextInput::make('title') - ->required() - ->maxLength(255) - ->autofocus(), - FileUpload::make('path') - ->required() - ->acceptedFileTypes([ - 'application/pdf', - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - 'application/vnd.openxmlformats-officedocument.presentationml.presentation', - 'application/zip' - ]) - ->maxSize(512000) - ->disk('public') - ->directory('files') - ->downloadable() - ->visibility('public') - ]), - ]), - Builder\Block::make('person')->label('Персона') - ->schema([ - TextInput::make('name') - ->label('Имя') - ->required() - ->maxLength(255), - FileUpload::make('photo') - ->label('Фотография') - ->image() - ->disk('public') - ->directory('images') - ->imageEditor(), - Forms\Components\Repeater::make('info')->schema([ - Forms\Components\Grid::make(2)->schema([ - TextInput::make('column') - ->required() - ->maxLength(255), - TextInput::make('content') - ->required() - ->maxLength(255), - ]), - ])->minItems(1), - ]), - Builder\Block::make('stepper')->label('Этапы') - ->schema([ - TextInput::make('step_name') - ->label('Название шага') - ->required() - ->maxLength(255), - Forms\Components\Repeater::make('steps')->schema([ - TextInput::make('title') - ->required() - ->live() - ->maxLength(255)->columnSpanFull(), - TinyEditor::make('content') - ->label('') - ->profile('test') - ->required(), ]) - ->itemLabel(fn (array $state): ?string => $state['title'] ?? null) - ->minItems(1) - ->collapsible() - ->collapsed() - - - ]), - Builder\Block::make('tabs')->label('Вкладки') - ->schema([ - Forms\Components\Repeater::make('tab')->schema([ - TextInput::make('title') - ->required() - ->maxLength(255)->columnSpanFull(), - \Filament\Forms\Components\Builder::make('content')->label('')->blocks([ - Builder\Block::make('heading')->label('Заголовок') - ->schema([ - TextInput::make('id')->hidden()->integer()->default(rand(2335235,324634264263426)), - TextInput::make('content') - ->label('') - ->live(onBlur: true) - ->afterStateUpdated(function (string $operation, string $state, Forms\Set $set, $get) { - }), - ]), - Builder\Block::make('paragraph') - ->schema([ - RichEditor::make('content') - ->toolbarButtons([ - 'blockquote', - 'bold', - 'bulletList', - 'italic', - 'link', - 'orderedList', - 'redo', - 'strike', - 'underline', - 'undo', - ]) - ->label(''), - ])->label('Текст'), - Builder\Block::make('files') - ->schema([ - Forms\Components\Repeater::make('file')->schema([ - TextInput::make('title') - ->required() - ->maxLength(255) - ->autofocus(), - FileUpload::make('path') - ->required() - ->acceptedFileTypes([ - 'application/pdf', - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - 'application/vnd.openxmlformats-officedocument.presentationml.presentation', - 'application/zip' - ]) - ->maxSize(512000) - ->disk('public') - ->directory('files') - ->downloadable() - ->visibility('public') - ]), - ]), - Builder\Block::make('person') - ->schema([ - TextInput::make('name') - ->label('Имя') - ->required() - ->maxLength(255), - FileUpload::make('photo') - ->label('Фотография') - ->image() - ->disk('public') - ->directory('images') - ->imageEditor(), - Forms\Components\Repeater::make('info')->schema([ - Forms\Components\Grid::make(2)->schema([ - TextInput::make('column') - ->required() - ->maxLength(255), - TextInput::make('content') - ->required() - ->maxLength(255), - ]), - ])->minItems(1), - ]), - Builder\Block::make('stepper') - ->schema([ - TextInput::make('step_name') - ->label('Название шага') - ->required() - ->maxLength(255), - Forms\Components\Repeater::make('steps')->schema([ - TextInput::make('title') - ->required() - ->maxLength(255)->columnSpanFull(), - RichEditor::make('content')->required(), - ])->minItems(1), - ]), - Builder\Block::make('images') - ->schema([ - FileUpload::make('url') - ->label('Изображение(-я)') - ->image() - ->multiple() - ->reorderable() - ->maxFiles(5) - ->disk('public') - ->directory('images') - ->imageEditor() - ->required(), - TextInput::make('alt') - ->label('Описание') - ->placeholder('Необязяательно') - ])->label('Слайдер изображений'), - Builder\Block::make('image') - ->schema([ - FileUpload::make('url') - ->label('Изображение(-я)') - ->image() - ->multiple() - ->reorderable() - ->maxFiles(5) - ->disk('public') - ->directory('images') - ->imageEditor() - ->required(), - TextInput::make('alt') - ->label('Описание') - ->placeholder('Необязяательно') - ])->label('Изображение'), - Builder\Block::make('video') - ->schema([ - TextInput::make('mime')->readOnly(), - TextInput::make('title') - ->required() - ->maxLength(255) - ->autofocus(), - FileUpload::make('path') - ->required() - ->acceptedFileTypes([ - 'video/mp4', - 'video/quicktime', - 'video/x-msvideo', - 'video/x-ms-wmv', - 'video/avi', - 'video/webm', - 'video/ogg', - 'video/3gpp', - 'video/3gpp2', - 'video/x-m4v', - ]) - ->disk('public') - ->directory('videos') - ->afterStateUpdated(fn (callable $set, $state) => $set('mime', $state?->getMimeType())), - ]), - Builder\Block::make('postsList') - ->schema([ - Forms\Components\Grid::make(2)->schema([ - TextInput::make('count') - ->label('Количество запией') - ->integer(), - Select::make('category') - ->options(Category::all()->pluck('title', 'id')) - ]), - ])->label('Список новостей'), - ]) - ->collapsed() - ->blockNumbers(false) - ->collapsible() - ->addActionLabel('Добавить новый блок'), - ])->minItems(1), - ]), - Builder\Block::make('images')->label('Слайдер изображений') - ->schema([ - FileUpload::make('url') - ->label('Изображение(-я)') - ->image() - ->multiple() - ->reorderable() - ->maxFiles(5) - ->disk('public') - - ->directory('images') - ->imageEditor() - ->required(), - TextInput::make('alt') - ->label('Описание') - ->placeholder('Необязяательно') - ]), - Builder\Block::make('image')->label('Изображение') - ->schema([ - FileUpload::make('url') - ->label('Изображение(-я)') - ->image() - ->multiple() - ->reorderable() - ->maxFiles(5) - ->disk('public') - ->directory('images') - ->imageEditor() - ->required(), - TextInput::make('alt') - ->label('Описание') - ->placeholder('Необязяательно') - ]), - Builder\Block::make('video')->label('Видео') - ->schema([ - TextInput::make('mime')->readOnly(), - TextInput::make('title') - ->required() - ->maxLength(255) - ->autofocus(), - FileUpload::make('path') - ->required() - ->acceptedFileTypes([ - 'video/mp4', - 'video/quicktime', - 'video/x-msvideo', - 'video/x-ms-wmv', - 'video/avi', - 'video/webm', - 'video/ogg', - 'video/3gpp', - 'video/3gpp2', - 'video/x-m4v', - ]) - ->disk('public') - ->directory('videos') - ->afterStateUpdated(fn (callable $set, $state) => $set('mime', $state?->getMimeType())), - ]), - Builder\Block::make('postsList')->label('Список новостей') - ->schema([ - Forms\Components\Grid::make(2)->schema([ - TextInput::make('count') - ->label('Количество запией') - ->integer(), - Select::make('category') - ->options(Category::all()->pluck('title', 'id')) - ]), - ]), - Builder\Block::make('postItem')->label('Новость') - ->schema([ - Select::make('post') - ->options(Post::query()->where('status', PostStatus::PUBLISHED)->pluck('title', 'id')) - ->searchable() - ->required(), - ]), - Builder\Block::make('pageItem')->label('Страница') - ->schema([ - Select::make('page') - ->options(Page::query()->where('title', '!=' , null)->where('is_visible', true)->pluck('title', 'id')) - ->searchable() - ->required(), - ]), - ]) - ->collapsed() - ->blockNumbers(false) - ->collapsible() - ->blockPickerColumns(3) - ->blockPickerWidth('2xl') - ->addActionLabel('Добавить новый блок'), + ContentBuilderItem::getItem('content') ]), ]), ]), - ]); } public static function table(Table $table): Table { return $table + ->defaultSort('created_at', 'desc') + ->reorderable('order_column') + ->paginated([10, 25, 50, 100]) ->columns([ - TextColumn::make('id')->label('ID')->sortable(), - TextColumn::make('title')->label('Название')->sortable()->searchable(), - TextColumn::make('created_at')->label('Дата создания')->sortable(), - Tables\Columns\ToggleColumn::make('is_active')->label('Активно')->sortable(), - ]) + TextColumn::make('id') + ->label('ID') + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + + TextColumn::make('title') + ->label('Название') + ->sortable() + ->searchable() + ->description(fn (Division $record) => Str::limit($record->slug, 30)) + ->wrap(), + + TextColumn::make('created_at') + ->label('Дата создания') + ->dateTime('d.m.Y H:i') + ->sortable() + ->toggleable(), + + TextColumn::make('updated_at') + ->label('Последнее обновление') + ->dateTime('d.m.Y H:i') + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + + Tables\Columns\ToggleColumn::make('is_active') + ->label('Статус') + ->sortable() + ->alignCenter(), + ]) ->filters([ - // + Tables\Filters\Filter::make('is_active') + ->label('Только активные') + ->query(fn (EloquentBuilder $query) => $query->where('is_active', true)) + ->default(), ]) ->actions([ - Tables\Actions\EditAction::make(), + Tables\Actions\EditAction::make() + ->iconButton() + ->tooltip('Редактировать'), ]) ->bulkActions([ Tables\Actions\BulkActionGroup::make([ - Tables\Actions\DeleteBulkAction::make(), + Tables\Actions\DeleteBulkAction::make() + ->label('Удалить выбранные') + ->modalHeading('Подтверждение удаления') + ->modalSubmitActionLabel('Да, удалить') + ->modalDescription('Вы уверены, что хотите удалить выбранные подразделения? Это действие нельзя отменить.'), + + Tables\Actions\ForceDeleteBulkAction::make() + ->label('Принудительно удалить') + ->modalHeading('Подтверждение удаления') + ->modalSubmitActionLabel('Да, удалить безвозвратно') + ->modalDescription('Внимание! Это действие окончательно удалит записи из базы данных.'), + + Tables\Actions\RestoreBulkAction::make() + ->label('Восстановить выбранные'), ]), - ]); + ]) + ->emptyStateActions([ + Tables\Actions\CreateAction::make() + ->label('Добавить подразделение'), + ]) + ->persistFiltersInSession() + ->persistSearchInSession() + ->striped(); } public static function getRelations(): array @@ -445,4 +186,4 @@ class DivisionResource extends Resource 'edit' => Pages\EditDivision::route('/{record}/edit'), ]; } -} +} \ No newline at end of file diff --git a/app/Filament/Resources/DivisionResource/Pages/CreateDivision.php b/app/Filament/Resources/DivisionResource/Pages/CreateDivision.php index ba801e3..6f9ad6b 100644 --- a/app/Filament/Resources/DivisionResource/Pages/CreateDivision.php +++ b/app/Filament/Resources/DivisionResource/Pages/CreateDivision.php @@ -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 diff --git a/app/Filament/Resources/DivisionResource/Pages/EditDivision.php b/app/Filament/Resources/DivisionResource/Pages/EditDivision.php index 76ca95d..130b512 100644 --- a/app/Filament/Resources/DivisionResource/Pages/EditDivision.php +++ b/app/Filament/Resources/DivisionResource/Pages/EditDivision.php @@ -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 { diff --git a/app/Filament/Resources/DivisionResource/RelationManagers/WorkersRelationManager.php b/app/Filament/Resources/DivisionResource/RelationManagers/WorkersRelationManager.php index 585e8a1..f1297fa 100644 --- a/app/Filament/Resources/DivisionResource/RelationManagers/WorkersRelationManager.php +++ b/app/Filament/Resources/DivisionResource/RelationManagers/WorkersRelationManager.php @@ -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(); } -} +} \ No newline at end of file diff --git a/app/Filament/Resources/EducationalGroupResource.php b/app/Filament/Resources/EducationalGroupResource.php index 069f70c..dbf0871 100644 --- a/app/Filament/Resources/EducationalGroupResource.php +++ b/app/Filament/Resources/EducationalGroupResource.php @@ -4,41 +4,64 @@ namespace App\Filament\Resources; use App\Enums\FormEducation; use App\Filament\Resources\EducationalGroupResource\Pages; -use App\Filament\Resources\EducationalGroupResource\RelationManagers; use App\Models\EducationalGroup; use App\Models\Faculty; use Filament\Forms; +use Filament\Forms\Components\Grid; +use Filament\Forms\Components\Section; +use Filament\Forms\Components\Select; +use Filament\Forms\Components\TextInput; use Filament\Forms\Form; use Filament\Resources\Resource; use Filament\Tables; +use Filament\Tables\Columns\BadgeColumn; use Filament\Tables\Columns\TextColumn; use Filament\Tables\Table; use Illuminate\Database\Eloquent\Builder; -use Illuminate\Database\Eloquent\SoftDeletingScope; class EducationalGroupResource extends Resource { protected static ?string $navigationGroup = 'Расписание и группы'; - protected static ?string $model = EducationalGroup::class; - - protected static ?string $pluralLabel = 'Группы'; - + protected static ?string $pluralLabel = 'Учебные группы'; + protected static ?string $modelLabel = 'учебная группа'; protected static ?string $navigationIcon = 'heroicon-o-user-group'; public static function form(Form $form): Form { return $form ->schema([ - Forms\Components\Section::make()->schema([ - Forms\Components\Grid::make(2)->schema([ - Forms\Components\TextInput::make('title')->label('Название группы')->required(), - Forms\Components\Select::make('faculty_id')->label('Факультет')->required() - ->options(Faculty::all()->pluck('title', 'id')), - Forms\Components\Select::make('education_form_id')->label('Форма обучения') - ->options(FormEducation::class) + Section::make('Основная информация') + ->description('Заполните основные данные о группе') + ->collapsible() + ->schema([ + Grid::make(2) + ->schema([ + TextInput::make('title') + ->label('Название группы') + ->required() + ->maxLength(50) + ->placeholder('Например: ИВТ-21-1') + ->helperText('Введите краткое название группы в принятом формате'), + + Select::make('faculty_id') + ->label('Факультет') + ->required() + ->options(Faculty::query()->orderBy('title')->pluck('title', 'id')) + ->searchable() + ->preload() + ->placeholder('Выберите факультет') + ->helperText('Выберите факультет, к которому относится группа'), + + Select::make('education_form_id') + ->label('Форма обучения') + ->required() + ->options(FormEducation::class) + ->native(false) + ->placeholder('Выберите форму обучения') + ->helperText('Выберите форму обучения для группы'), + ]), ]), - ]), ]); } @@ -46,29 +69,71 @@ class EducationalGroupResource extends Resource { return $table ->columns([ - TextColumn::make('id')->label('ID')->sortable(), - TextColumn::make('title')->label('Название')->sortable()->searchable(), - TextColumn::make('created_at')->label('Дата создания')->sortable(), - Tables\Columns\BadgeColumn::make('faculty.title')->label('Категория')->sortable(), + TextColumn::make('id') + ->label('ID') + ->sortable() + ->searchable() + ->toggleable(isToggledHiddenByDefault: true), + + TextColumn::make('title') + ->label('Название группы') + ->sortable() + ->searchable() + ->description(fn ($record) => $record->faculty->title ?? ''), + + BadgeColumn::make('education_form_id') + ->label('Форма обучения') + ->formatStateUsing(fn ($state) => FormEducation::tryFrom($state)?->label()) + ->color(fn ($state) => match($state) { + FormEducation::FULL_TIME->value => 'success', + FormEducation::PART_TIME->value => 'warning', + default => 'gray', + }) + ->sortable(), + + TextColumn::make('created_at') + ->label('Дата создания') + ->dateTime('d.m.Y') + ->sortable() + ->toggleable(), ]) ->filters([ - // + Tables\Filters\SelectFilter::make('faculty_id') + ->label('Факультет') + ->options(Faculty::query()->orderBy('title')->pluck('title', 'id')) + ->searchable(), + + Tables\Filters\SelectFilter::make('education_form_id') + ->label('Форма обучения') + ->options(FormEducation::class), ]) ->actions([ - Tables\Actions\EditAction::make(), + Tables\Actions\EditAction::make() + ->iconButton() + ->tooltip('Редактировать'), + + Tables\Actions\ViewAction::make() + ->iconButton() + ->tooltip('Просмотреть'), ]) ->bulkActions([ Tables\Actions\BulkActionGroup::make([ - Tables\Actions\DeleteBulkAction::make(), + Tables\Actions\DeleteBulkAction::make() + ->label('Удалить выбранные') + ->modalHeading('Удаление групп') + ->modalDescription('Вы уверены, что хотите удалить выбранные учебные группы?'), ]), - ]); + ]) + ->emptyStateActions([ + Tables\Actions\CreateAction::make() + ->label('Добавить группу'), + ]) + ->defaultSort('title'); } public static function getRelations(): array { - return [ - // - ]; + return []; } public static function getPages(): array @@ -79,4 +144,4 @@ class EducationalGroupResource extends Resource 'edit' => Pages\EditEducationalGroup::route('/{record}/edit'), ]; } -} +} \ No newline at end of file diff --git a/app/Filament/Resources/EducationalProgramResource.php b/app/Filament/Resources/EducationalProgramResource.php index f83a373..f5f17b6 100644 --- a/app/Filament/Resources/EducationalProgramResource.php +++ b/app/Filament/Resources/EducationalProgramResource.php @@ -4,263 +4,167 @@ namespace App\Filament\Resources; use App\Enums\EducationalProgramStatus; use App\Enums\LevelEducational; +use App\Filament\Components\Forms\ItemForm\Pages\ContentBuilderItem; use App\Filament\Resources\EducationalProgramResource\Pages; -use App\Filament\Resources\EducationalProgramResource\RelationManagers; use App\Filament\Resources\EducationalProgramResource\RelationManagers\AdmissionPlansRelationManager; -use App\Models\Category; use App\Models\EducationalProgram; use Filament\Forms; use Filament\Forms\Components\Builder; use Filament\Forms\Components\FileUpload; +use Filament\Forms\Components\Grid; use Filament\Forms\Components\Hidden; use Filament\Forms\Components\RichEditor; use Filament\Forms\Components\Section; use Filament\Forms\Components\Select; -use Filament\Forms\Components\SpatieTagsInput; +use Filament\Forms\Components\Tabs; use Filament\Forms\Components\TextInput; use Filament\Forms\Form; use Filament\Resources\Resource; use Filament\Tables; +use Filament\Tables\Columns\BadgeColumn; +use Filament\Tables\Columns\TextColumn; use Filament\Tables\Table; -use Illuminate\Database\Eloquent\SoftDeletingScope; use Illuminate\Support\Str; -use Mohamedsabil83\FilamentFormsTinyeditor\Components\TinyEditor; class EducationalProgramResource extends Resource { protected static ?string $model = EducationalProgram::class; - - protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack'; - + protected static ?string $navigationIcon = 'heroicon-o-academic-cap'; protected static ?string $navigationGroup = 'Образование'; - protected static ?string $pluralLabel = 'Образовательные программы'; - + protected static ?string $modelLabel = 'Образовательная программа'; protected static ?string $navigationParentItem = 'Приемная-компания'; public static function form(Form $form): Form { return $form ->schema([ - Section::make() - ->schema([ - TextInput::make('name')->label('Название')->required(), - Section::make('О программе')->schema([ - Builder::make('about_program')->label('')->blocks([ - Builder\Block::make('heading')->label('Заголовок') + Tabs::make('') + ->tabs([ + Tabs\Tab::make('Основная информация') + ->icon('heroicon-o-information-circle') + ->schema([ + TextInput::make('name') + ->label('Название программы') + ->required() + ->maxLength(255) + ->placeholder('Введите полное название программы') + ->columnSpanFull() + ->helperText('Официальное название программы как в лицензии'), + + Grid::make(2) ->schema([ - TextInput::make('id')->hidden()->integer()->default(rand(2335235,324634264263426)), - TextInput::make('content') - ->label('') - ->live(onBlur: true) - ->afterStateUpdated(function (string $operation, string $state, Forms\Set $set, $get) { - }), + Select::make('lvl_edu') + ->label('Уровень образования') + ->options(LevelEducational::class) + ->required() + ->native(false) + ->helperText('Выберите уровень образовательной программы'), + + Select::make('status') + ->label('Статус программы') + ->options(EducationalProgramStatus::class) + ->required() + ->native(false) + ->helperText('Определяет видимость программы на сайте'), + + TextInput::make('lang_stud') + ->label('Язык обучения') + ->required() + ->placeholder('Например: русский, английский') + ->helperText('Укажите основной язык преподавания') + ->columnSpan(2), ]), - Builder\Block::make('paragraph') - ->schema([ - RichEditor::make('content') - ->toolbarButtons([ - 'blockquote', - 'bold', - 'bulletList', - 'italic', - 'link', - 'orderedList', - 'redo', - 'strike', - 'underline', - 'undo', - ]) - ->label('') - ->required() - ]), - Builder\Block::make('image') - ->schema([ - FileUpload::make('url') - ->label('Изображение(-я)') - ->image() - ->multiple() - ->reorderable() - ->maxFiles(5) - ->disk('public') - ->directory('images') - ->imageEditor() - ->required(), - TextInput::make('alt') - ->label('Описание') - ->placeholder('Необязяательно') - ])->label('Изображение(-я)'), - Builder\Block::make('video') - ->schema([ - Hidden::make('mime'), + ]), - TextInput::make('title') - ->required() - ->maxLength(255) - ->autofocus(), - - FileUpload::make('path') - ->required() - ->acceptedFileTypes(['video/mp4','video/ogg','video/webm']) - ->maxSize(512000) - ->disk('videos') - ->visibility('public') - ->afterStateUpdated(fn (callable $set, $state) => $set('mime', $state?->getMimeType())), - ]), - Builder\Block::make('files') - ->schema([ - TextInput::make('title') - ->required() - ->maxLength(255) - ->autofocus(), - FileUpload::make('path') - ->required() - ->acceptedFileTypes([ - 'application/pdf', - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - 'application/vnd.openxmlformats-officedocument.presentationml.presentation', - 'application/zip' - ]) - ->maxSize(512000) - ->disk('public') - ->directory('files') - ->downloadable() - ->visibility('public') - ]) - ]) - ->collapsed() - ->blockNumbers(false) - ->collapsible() - ->addActionLabel('Добавить новый блок'), - - ]), - Section::make('Особенности программы')->schema([ - Builder::make('program_features')->label('')->blocks([ - Builder\Block::make('heading')->label('Заголовок') - ->schema([ - TextInput::make('id')->hidden()->integer()->default(rand(2335235,324634264263426)), - TextInput::make('content') - ->label('') - ->live(onBlur: true) - ->afterStateUpdated(function (string $operation, string $state, Forms\Set $set, $get) { - }), - ]), - Builder\Block::make('paragraph') - ->schema([ - RichEditor::make('content') - ->toolbarButtons([ - 'blockquote', - 'bold', - 'bulletList', - 'italic', - 'link', - 'orderedList', - 'redo', - 'strike', - 'underline', - 'undo', - ]) - ->label('') - ->required() - ]), - Builder\Block::make('image') - ->schema([ - FileUpload::make('url') - ->label('Изображение(-я)') - ->image() - ->multiple() - ->reorderable() - ->maxFiles(5) - ->disk('public') - ->directory('images') - ->imageEditor() - ->required(), - TextInput::make('alt') - ->label('Описание') - ->placeholder('Необязяательно') - ])->label('Изображение(-я)'), - Builder\Block::make('video') - ->schema([ - Hidden::make('mime'), - - TextInput::make('title') - ->required() - ->maxLength(255) - ->autofocus(), - - FileUpload::make('path') - ->required() - ->acceptedFileTypes(['video/mp4','video/ogg','video/webm']) - ->maxSize(512000) - ->disk('videos') - ->visibility('public') - ->afterStateUpdated(fn (callable $set, $state) => $set('mime', $state?->getMimeType())), - ]), - Builder\Block::make('files') - ->schema([ - TextInput::make('title') - ->required() - ->maxLength(255) - ->autofocus(), - FileUpload::make('path') - ->required() - ->acceptedFileTypes([ - 'application/pdf', - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - 'application/vnd.openxmlformats-officedocument.presentationml.presentation', - 'application/zip' - ]) - ->maxSize(512000) - ->disk('public') - ->directory('files') - ->downloadable() - ->visibility('public') - ]) - ]) - ->collapsed() - ->blockNumbers(false) - ->collapsible() - ->addActionLabel('Добавить новый блок'), - - ]), - - Select::make('lvl_edu')->options(LevelEducational::class)->label('Уровень образования')->required(), - Select::make('status') - ->options(EducationalProgramStatus::class) - ->label('Статус программы')->required(), - TextInput::make('lang_stud')->label('На каком языке ведется образование')->required(), + Tabs\Tab::make('Описание программы') + ->icon('heroicon-o-document-text') + ->schema([ + self::getContentBuilder('about_program', 'О программе') + ->columnSpanFull(), + ]), + Tabs\Tab::make('Особенности программы') + ->icon('heroicon-o-sparkles') + ->schema([ + self::getContentBuilder('program_features', 'Особенности программы') + ->columnSpanFull(), + ]), ]) + ->persistTabInQueryString() + ->columnSpanFull(), ]); - + } + protected static function getContentBuilder(string $field, string $label): Builder + { + return ContentBuilderItem::getItem($field); } public static function table(Table $table): Table { return $table ->columns([ - Tables\Columns\TextColumn::make('name')->label('Название программы')->sortable()->searchable(), - Tables\Columns\TextColumn::make('directionStudy.lvl_edu')->label('Уровень образования')->limit(30), + TextColumn::make('name') + ->label('Название') + ->sortable() + ->searchable() + ->description(fn ($record) => $record->lang_stud), + + BadgeColumn::make('lvl_edu') + ->label('Уровень') + ->formatStateUsing(fn ($state) => LevelEducational::tryFrom($state->value)?->getLabel()) + ->color(fn ($state) => LevelEducational::tryFrom($state->value)?->getColor()) + ->sortable(), + + BadgeColumn::make('status') + ->label('Статус') + ->formatStateUsing(fn ($state) => EducationalProgramStatus::tryFrom($state)?->getLabel()) + ->color(fn ($state) => EducationalProgramStatus::tryFrom($state)?->getColor()) + ->sortable(), + + TextColumn::make('updated_at') + ->label('Обновлено') + ->dateTime('d.m.Y H:i') + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), ]) ->filters([ - // + Tables\Filters\SelectFilter::make('lvl_edu') + ->label('Уровень образования') + ->options(LevelEducational::class), + + Tables\Filters\SelectFilter::make('status') + ->label('Статус программы') + ->options(EducationalProgramStatus::class), ]) ->actions([ - Tables\Actions\EditAction::make(), + Tables\Actions\EditAction::make() + ->iconButton() + ->tooltip('Редактировать'), + + Tables\Actions\ViewAction::make() + ->iconButton() + ->tooltip('Просмотреть'), ]) ->bulkActions([ Tables\Actions\BulkActionGroup::make([ - Tables\Actions\DeleteBulkAction::make(), + Tables\Actions\DeleteBulkAction::make() + ->label('Удалить выбранные') + ->modalHeading('Удаление программ') + ->modalDescription('Вы уверены, что хотите удалить выбранные программы?'), ]), - ]); + ]) + ->emptyStateActions([ + Tables\Actions\CreateAction::make() + ->label('Добавить программу'), + ]) + ->defaultSort('name'); } public static function getRelations(): array { return [ - AdmissionPlansRelationManager::class + AdmissionPlansRelationManager::class, ]; } @@ -272,4 +176,4 @@ class EducationalProgramResource extends Resource 'edit' => Pages\EditEducationalProgram::route('/{record}/edit'), ]; } -} +} \ No newline at end of file diff --git a/app/Filament/Resources/EducationalProgramResource/RelationManagers/AdmissionPlansRelationManager.php b/app/Filament/Resources/EducationalProgramResource/RelationManagers/AdmissionPlansRelationManager.php index e04e5f1..0f7170b 100644 --- a/app/Filament/Resources/EducationalProgramResource/RelationManagers/AdmissionPlansRelationManager.php +++ b/app/Filament/Resources/EducationalProgramResource/RelationManagers/AdmissionPlansRelationManager.php @@ -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'); } -} +} \ No newline at end of file diff --git a/app/Filament/Resources/EventResource.php b/app/Filament/Resources/EventResource.php index 75bd74a..961b1ad 100644 --- a/app/Filament/Resources/EventResource.php +++ b/app/Filament/Resources/EventResource.php @@ -2,191 +2,211 @@ namespace App\Filament\Resources; +use App\Filament\Components\Forms\ItemForm\Pages\ContentBuilderItem; use App\Filament\Resources\EventResource\Pages; -use App\Filament\Resources\EventResource\RelationManagers; -use App\Models\Category; -use App\Models\EventCategory; -use Filament\Forms\Components\Builder; - use App\Models\Event; +use App\Models\EventCategory; use Filament\Forms; +use Filament\Forms\Components\Actions\Action; use Filament\Forms\Components\DatePicker; -use Filament\Forms\Components\DateTimePicker; -use Filament\Forms\Components\FileUpload; -use Filament\Forms\Components\Hidden; -use Filament\Forms\Components\RichEditor; +use Filament\Forms\Components\Grid; use Filament\Forms\Components\Section; use Filament\Forms\Components\Select; use Filament\Forms\Components\SpatieTagsInput; +use Filament\Forms\Components\Tabs; use Filament\Forms\Components\TextInput; +use Filament\Forms\Components\TimePicker; use Filament\Forms\Components\Toggle; use Filament\Forms\Form; use Filament\Resources\Resource; use Filament\Tables; +use Filament\Tables\Columns\IconColumn; use Filament\Tables\Columns\TextColumn; use Filament\Tables\Table; -use Illuminate\Database\Eloquent\SoftDeletingScope; use Illuminate\Support\Str; -use Mohamedsabil83\FilamentFormsTinyeditor\Components\TinyEditor; class EventResource extends Resource { protected static ?string $model = Event::class; protected static ?string $navigationGroup = 'Новости и мероприятия'; - protected static ?string $navigationIcon = 'heroicon-o-calendar-days'; - - protected static ?string $pluralLabel = 'Мероприятия'; + protected static ?string $modelLabel = 'Мероприятие'; + protected static ?string $pluralModelLabel = 'Мероприятия'; + protected static ?string $navigationLabel = 'Мероприятия'; public static function form(Form $form): Form { return $form ->schema([ - Section::make() - ->schema([ - Forms\Components\Grid::make(2)->schema([ - TextInput::make('title')->label('Заголовок')->required() - ->live(onBlur: true) - ->afterStateUpdated(function (string $operation, $state, Forms\Set $set) { - if ($state !== null) { - $set('slug', Str::slug($state)); - } else { - $set('slug', null); - } - }), - TextInput::make('slug')->label('Slug (Заполнится автоматически)')->unique(ignoreRecord: true)->readOnly()->required(), - ]), - Section::make('Контент')->schema([ - \Filament\Forms\Components\Builder::make('content')->label('')->blocks([ - Builder\Block::make('heading')->label('Заголовок') + Tabs::make('Мероприятие') + ->tabs([ + Tabs\Tab::make('Основное') + ->icon('heroicon-o-information-circle') + ->schema([ + Grid::make(2) ->schema([ - TextInput::make('id')->hidden()->integer()->default(rand(2335235,324634264263426)), - TextInput::make('content') - ->label('') + TextInput::make('title') + ->label('Название мероприятия') + ->placeholder('Введите название мероприятия') + ->helperText('Отображается на сайте') + ->required() + ->maxLength(255) ->live(onBlur: true) - ->afterStateUpdated(function (string $operation, string $state, Forms\Set $set, $get) { + ->afterStateUpdated(function (string $operation, $state, Forms\Set $set) { + $set('slug', $state ? Str::slug($state) : null); }), + + TextInput::make('slug') + ->label('URL-адрес') + ->unique(ignoreRecord: true) + ->required() + ->readOnly() + ->helperText('Формируется автоматически из названия') + ->prefix(fn () => route('client.event.index') . '/') + ->suffixAction( + Action::make('copy') + ->icon('heroicon-s-clipboard-document-check') + ->action(function ($livewire, $state) { + $livewire->js( + 'window.navigator.clipboard.writeText("'. route('client.event.index') . '/' . $state.'"); + $tooltip("'.__('Copied to clipboard').'", { timeout: 1500 });' + ); + })), ]), - Builder\Block::make('paragraph') - ->schema([ - TinyEditor::make('content') - ->label('') - ->profile('test') - ->required(), - ])->label('Текст'), - Builder\Block::make('image') - ->schema([ - FileUpload::make('url') - ->label('Изображение(-я)') - ->image() - ->optimize('jpg') - ->resize(30) - ->multiple() - ->reorderable() - ->maxFiles(5) - ->disk('public') - ->directory('images') - ->imageEditor() - ->required(), - TextInput::make('alt') - ->label('Описание') - ->placeholder('Необязяательно') - ])->label('Изображение(-я)'), - Builder\Block::make('video') - ->schema([ - Hidden::make('mime'), - TextInput::make('title') - ->required() - ->maxLength(255) - ->autofocus(), + Select::make('category_id') + ->label('Категория') + ->placeholder('Выберите категорию') + ->options(EventCategory::all()->pluck('title', 'id')) + ->preload() + ->helperText('Для систематизации мероприятий'), - FileUpload::make('path') - ->required() - ->acceptedFileTypes(['video/mp4','video/ogg','video/webm']) - ->maxSize(512000) - ->disk('videos') - ->visibility('public') - ->afterStateUpdated(fn (callable $set, $state) => $set('mime', $state?->getMimeType())), - ]), - Builder\Block::make('files') - ->schema([ - TextInput::make('title') - ->required() - ->maxLength(255) - ->autofocus(), - FileUpload::make('path') - ->required() - ->acceptedFileTypes([ - 'application/pdf', - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - 'application/vnd.openxmlformats-officedocument.presentationml.presentation', - 'application/zip' - ]) - ->maxSize(512000) - ->disk('public') - ->directory('files') - ->downloadable() - ->visibility('public') - ]) - ]) - ->collapsed() - ->blockNumbers(false) - ->collapsible() - ->addActionLabel('Добавить новый блок'), + SpatieTagsInput::make('tags') + ->label('Теги') + ->placeholder('Добавьте теги') + ->helperText('Для фильтрации и поиска'), - ]), - TextInput::make('address')->label('Адрес')->required(), - - Forms\Components\Grid::make(2)->schema([ - Forms\Components\Grid::make(2)->schema([ - DatePicker::make('event_date_start')->label('Дата начала мероприятия')->required()->native(false) - ->minDate(now()) - ->maxDate(now()->addYear()), - Forms\Components\TimePicker::make('event_time_start')->label('Время начала мероприятия')->seconds(false)->required()->native(false), - DatePicker::make('event_date_end')->label('Дата окончания мероприятия (Опиционально)')->native(false) - ->minDate(now()) - ->maxDate(now()->addYear()), + Toggle::make('is_online') + ->label('Онлайн-формат') + ->helperText('Отметьте для онлайн-мероприятий') + ->default(false) + ->inline(false) + ->onColor('success') + ->offColor('gray'), ]), - Toggle::make('is_online')->default(false)->label('Онлайн мероприятие')->inline(false) + Tabs\Tab::make('Контент') + ->icon('heroicon-o-document-text') + ->schema([ + ContentBuilderItem::getItem('content') + ->columnSpanFull(), + ]), - ]), + Tabs\Tab::make('Дата и место') + ->icon('heroicon-o-map-pin') + ->schema([ + TextInput::make('address') + ->label('Место проведения') + ->placeholder('Адрес или платформа') + ->helperText('Для онлайн укажите платформу (Zoom, YouTube и т.д.)') + ->required() + ->maxLength(255), - Forms\Components\Grid::make(2)->schema([ - Select::make('category_id') - ->options(EventCategory::all()->pluck('title', 'id')) - ->preload() - ->label('Категория'), - SpatieTagsInput::make('tags')->label('Тэги'), - ]), + Grid::make(2) + ->schema([ + DatePicker::make('event_date_start') + ->label('Дата начала') + ->native(false) + ->displayFormat('d/m/Y') + ->helperText('Когда начинается мероприятие') + ->required() + ->minDate(now()) + ->maxDate(now()->addYear()), + TimePicker::make('event_time_start') + ->label('Время начала') + ->seconds(false) + ->native(false) + ->helperText('По местному времени') + ->required(), + + DatePicker::make('event_date_end') + ->label('Дата окончания') + ->native(false) + ->displayFormat('d/m/Y') + ->helperText('Оставьте пустым для однодневного мероприятия') + ->minDate(now()) + ->maxDate(now()->addYear()), + ]), + ]), ]) + ->persistTabInQueryString() + ->columnSpanFull(), ]); - } - public static function table(Table $table): Table { return $table ->columns([ - TextColumn::make('id')->label('ID')->sortable(), - TextColumn::make('title')->label('Название')->sortable()->searchable(), - TextColumn::make('event_date_start')->label('Начало мероприятия')->sortable(), - TextColumn::make('created_at')->label('Дата создания')->sortable(), + TextColumn::make('id') + ->label('ID') + ->sortable() + ->searchable(), + + TextColumn::make('title') + ->label('Название') + ->sortable() + ->searchable() + ->limit(30), + + TextColumn::make('event_date_start') + ->label('Дата начала') + ->date('d.m.Y H:i') + ->sortable(), + + IconColumn::make('is_online') + ->label('Онлайн') + ->boolean() + ->trueIcon('heroicon-o-globe-alt') + ->falseIcon('heroicon-o-map-pin'), + + TextColumn::make('created_at') + ->label('Создано') + ->dateTime('d.m.Y H:i') + ->sortable(), ]) ->filters([ - // + Tables\Filters\SelectFilter::make('category_id') + ->label('Категория') + ->relationship('category', 'title'), + + Tables\Filters\Filter::make('is_online') + ->label('Только онлайн') + ->query(fn ($query) => $query->where('is_online', true)), ]) ->actions([ - Tables\Actions\EditAction::make(), + Tables\Actions\EditAction::make() + ->icon('heroicon-o-pencil') + ->tooltip('Редактировать'), + + Tables\Actions\Action::make('view') + ->icon('heroicon-o-eye') + ->tooltip('Просмотреть на сайте') + ->url(fn (Event $record) => route('client.event.show', $record->slug)) + ->openUrlInNewTab(), ]) ->bulkActions([ Tables\Actions\BulkActionGroup::make([ - Tables\Actions\DeleteBulkAction::make(), + Tables\Actions\DeleteBulkAction::make() + ->label('Удалить выбранное') + ->icon('heroicon-o-trash'), ]), + ]) + ->defaultSort('event_date_start', 'desc') + ->emptyStateActions([ + Tables\Actions\CreateAction::make() + ->label('Добавить мероприятие'), ]); } @@ -205,4 +225,4 @@ class EventResource extends Resource 'edit' => Pages\EditEvent::route('/{record}/edit'), ]; } -} +} \ No newline at end of file diff --git a/app/Filament/Resources/EventResource/Pages/CreateEvent.php b/app/Filament/Resources/EventResource/Pages/CreateEvent.php index dcf0ade..7bd40de 100644 --- a/app/Filament/Resources/EventResource/Pages/CreateEvent.php +++ b/app/Filament/Resources/EventResource/Pages/CreateEvent.php @@ -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); + } + + } diff --git a/app/Filament/Resources/EventResource/Pages/EditEvent.php b/app/Filament/Resources/EventResource/Pages/EditEvent.php index 6f2497e..0f975c0 100644 --- a/app/Filament/Resources/EventResource/Pages/EditEvent.php +++ b/app/Filament/Resources/EventResource/Pages/EditEvent.php @@ -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 [ diff --git a/app/Filament/Resources/FacultyRecourceResource/RelationManagers/DepartmentsRelationManager.php b/app/Filament/Resources/FacultyRecourceResource/RelationManagers/DepartmentsRelationManager.php deleted file mode 100644 index be608a6..0000000 --- a/app/Filament/Resources/FacultyRecourceResource/RelationManagers/DepartmentsRelationManager.php +++ /dev/null @@ -1,457 +0,0 @@ -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(), - ]); - } -} diff --git a/app/Filament/Resources/FacultyResource.php b/app/Filament/Resources/FacultyResource.php index f38b5e8..1d7eadd 100644 --- a/app/Filament/Resources/FacultyResource.php +++ b/app/Filament/Resources/FacultyResource.php @@ -4,11 +4,10 @@ namespace App\Filament\Resources; use App\Enums\CustomFormStatus; use App\Enums\PostStatus; -use App\Filament\Resources\FacultyRecourceResource\RelationManagers\DepartmentsRelationManager; +use App\Filament\Components\Forms\ItemForm\Pages\ContentBuilderItem; use App\Filament\Resources\FacultyResource\Pages; -use App\Filament\Resources\FacultyResource\RelationManagers; +use App\Filament\Resources\FacultyResource\RelationManagers\DepartmentsRelationManager; use App\Filament\Resources\FacultyResource\RelationManagers\WorkersRelationManager; -use App\Helpers\ByteConverter; use App\Models\Category; use App\Models\CustomForm; use App\Models\Faculty; @@ -16,446 +15,153 @@ use App\Models\Page; use App\Models\PageReferenceList; use App\Models\Post; use Filament\Forms; +use Filament\Forms\Components\Actions\Action; use Filament\Forms\Components\Builder; use Filament\Forms\Components\FileUpload; -use Filament\Forms\Components\Hidden; -use Filament\Forms\Components\RichEditor; use Filament\Forms\Components\Section; use Filament\Forms\Components\Select; -use Filament\Forms\Components\Tabs; use Filament\Forms\Components\TextInput; use Filament\Forms\Components\Toggle; use Filament\Forms\Form; use Filament\Resources\Resource; use Filament\Tables; +use Filament\Tables\Columns\IconColumn; use Filament\Tables\Columns\TextColumn; use Filament\Tables\Table; -use Illuminate\Database\Eloquent\SoftDeletingScope; -use Illuminate\Support\Carbon; use Illuminate\Support\Str; -use Livewire\Features\SupportFileUploads\TemporaryUploadedFile; -use Mohamedsabil83\FilamentFormsTinyeditor\Components\TinyEditor; class FacultyResource extends Resource { protected static ?string $model = Faculty::class; - protected static ?string $navigationGroup = 'Структура института'; - protected static ?string $navigationIcon = 'heroicon-o-building-office-2'; - protected static ?string $pluralLabel = 'Факультеты'; - - public static ?string $label = 'Факультет'; - - + protected static ?string $modelLabel = 'факультет'; public static function form(Form $form): Form { return $form ->schema([ - Section::make() - ->schema([ - Tabs::make('Tabs') + Forms\Components\Tabs::make('Настройки факультета') + ->persistTabInQueryString() + ->columnSpanFull() ->tabs([ - Tabs\Tab::make('Основная информация') + Forms\Components\Tabs\Tab::make('Основные данные') + ->icon('heroicon-o-information-circle') ->schema([ - TextInput::make('title')->label('Название факультета')->required() - ->live(onBlur: true) - ->afterStateUpdated(function (string $operation, string|null $state, Forms\Set $set) { - $set('slug', Str::slug($state)); - $set('seo.title', $state); - }), - TextInput::make('slug')->label('Slug')->unique(ignoreRecord: true)->readOnly()->required(), - TextInput::make('abbreviation')->label('Аббревиатура')->required(), - Toggle::make('is_active')->default(true)->label('Активный факультет')->inline(false), + Section::make('Идентификация') + ->description('Основная информация о факультете') + ->schema([ + TextInput::make('title') + ->label('Полное название') + ->required() + ->maxLength(255) + ->placeholder('Например: Факультет информационных технологий') + ->live(onBlur: true) + ->afterStateUpdated(function (string $operation, ?string $state, Forms\Set $set) { + $set('slug', Str::slug($state)); + if ($operation !== 'edit') { + $set('seo.title', $state); + } + }) + ->helperText('Официальное название факультета'), + + TextInput::make('slug') + ->label('URL-адрес') + ->unique(ignoreRecord: true) + ->required() + ->readOnly() + ->helperText('Формируется автоматически из названия') + ->prefix(fn () => route('client.faculty.index') . '/') + ->suffixAction( + Action::make('copy') + ->icon('heroicon-s-clipboard-document-check') + ->action(function ($livewire, $state) { + $livewire->js( + 'window.navigator.clipboard.writeText("'. route('client.faculty.index') . '/' . $state.'"); + $tooltip("'.__('Copied to clipboard').'", { timeout: 1500 });' + ); + })), + + TextInput::make('abbreviation') + ->label('Аббревиатура') + ->required() + ->maxLength(10) + ->placeholder('Например: ФИТ') + ->helperText('Короткое обозначение факультета'), + + Toggle::make('is_active') + ->label('Активный факультет') + ->inline(false) + ->default(true) + ->helperText('Отображать ли факультет на сайте'), + ]), ]), - Tabs\Tab::make('Описание факультета') + + Forms\Components\Tabs\Tab::make('Контент') + ->icon('heroicon-o-document-text') ->schema([ - Builder::make('content')->label('')->blocks([ - Builder\Block::make('heading')->label('Заголовок') - ->schema([ - TextInput::make('id')->hidden()->integer()->default(rand(2335235,324634264263426)), - TextInput::make('content') - ->label('') - ->live(onBlur: true) - ->afterStateUpdated(function (string $operation, string $state, Forms\Set $set, $get) { - }), - ]), - Builder\Block::make('paragraph') - ->schema([ - TinyEditor::make('content') - ->label('') - ->profile('test') - ->required(), - ])->label('Текст'), - Builder\Block::make('files') - ->schema([ - Forms\Components\Repeater::make('file')->schema([ - Hidden::make('expansion')->required(), - Hidden::make('size')->required(), - TextInput::make('title') - ->required() - ->maxLength(255) - ->autofocus(), - FileUpload::make('path') - ->required() - ->getUploadedFileNameForStorageUsing( - fn (TemporaryUploadedFile $file): string => - str(Str::slug(pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME) . '-' . Carbon::now()->timestamp ) . '.' . $file->getClientOriginalExtension()) - ) - ->acceptedFileTypes([ - 'application/pdf', - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - 'application/vnd.openxmlformats-officedocument.presentationml.presentation', - 'application/zip' - ]) - ->maxSize(512000) - ->disk('public') - ->directory('files') - ->downloadable() - ->afterStateUpdated(function ($set, $state) { - $set('expansion', $state?->getClientOriginalExtension()); - $set('size', ByteConverter::bytesToHuman($state?->getSize())); - }) - ->visibility('public') - ]), - ]), - Builder\Block::make('person') - ->schema([ - TextInput::make('name') - ->label('Имя') - ->required() - ->maxLength(255), - FileUpload::make('photo') - ->label('Фотография') - ->image() - ->disk('public') - ->directory('images') - ->imageEditor(), - Forms\Components\Repeater::make('info')->schema([ - Forms\Components\Grid::make(2)->schema([ - TextInput::make('column') - ->required() - ->maxLength(255), - TextInput::make('content') - ->required() - ->maxLength(255), - ]), - ])->minItems(1), - ]), - Builder\Block::make('stepper') - ->schema([ - TextInput::make('step_name') - ->label('Название шага') - ->required() - ->maxLength(255), - Forms\Components\Repeater::make('steps')->schema([ - TextInput::make('title') - ->required() - ->maxLength(255)->columnSpanFull(), - TinyEditor::make('content') - ->label('') - ->profile('test') - ->required(), - ])->minItems(1), - ]), - Builder\Block::make('tabs') - ->schema([ - Forms\Components\Repeater::make('tab')->schema([ - TextInput::make('title') - ->required() - ->maxLength(255)->columnSpanFull(), - \Filament\Forms\Components\Builder::make('content')->label('')->blocks([ - Builder\Block::make('heading')->label('Заголовок') - ->schema([ - TextInput::make('id')->hidden()->integer()->default(rand(2335235,324634264263426)), - TextInput::make('content') - ->label('') - ->live(onBlur: true) - ->afterStateUpdated(function (string $operation, string $state, Forms\Set $set, $get) { - }), - ]), - Builder\Block::make('paragraph') - ->schema([ - RichEditor::make('content') - ->toolbarButtons([ - 'blockquote', - 'bold', - 'bulletList', - 'italic', - 'link', - 'orderedList', - 'redo', - 'strike', - 'underline', - 'undo', - ]) - ->label(''), - ])->label('Текст'), - Builder\Block::make('files') - ->schema([ - Forms\Components\Repeater::make('file')->schema([ - TextInput::make('title') - ->required() - ->maxLength(255) - ->autofocus(), - FileUpload::make('path') - ->required() - ->acceptedFileTypes([ - 'application/pdf', - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - 'application/vnd.openxmlformats-officedocument.presentationml.presentation', - 'application/zip' - ]) - ->maxSize(512000) - ->disk('public') - ->directory('files') - ->downloadable() - ->visibility('public') - ]), - ]), - Builder\Block::make('person') - ->schema([ - TextInput::make('name') - ->label('Имя') - ->required() - ->maxLength(255), - FileUpload::make('photo') - ->label('Фотография') - ->image() - ->disk('public') - ->directory('images') - ->imageEditor(), - Forms\Components\Repeater::make('info')->schema([ - Forms\Components\Grid::make(2)->schema([ - TextInput::make('column') - ->required() - ->maxLength(255), - TextInput::make('content') - ->required() - ->maxLength(255), - ]), - ])->minItems(1), - ]), - Builder\Block::make('stepper') - ->schema([ - TextInput::make('step_name') - ->label('Название шага') - ->required() - ->maxLength(255), - Forms\Components\Repeater::make('steps')->schema([ - TextInput::make('title') - ->required() - ->maxLength(255)->columnSpanFull(), - RichEditor::make('content')->required(), - ])->minItems(1), - ]), - Builder\Block::make('images') - ->schema([ - FileUpload::make('url') - ->label('Изображение(-я)') - ->image() - ->multiple() - ->reorderable() - ->maxFiles(5) - ->disk('public') - ->directory('images') - ->imageEditor() - ->required(), - TextInput::make('alt') - ->label('Описание') - ->placeholder('Необязяательно') - ])->label('Слайдер изображений'), - Builder\Block::make('image') - ->schema([ - FileUpload::make('url') - ->label('Изображение(-я)') - ->image() - ->multiple() - ->reorderable() - ->maxFiles(5) - ->disk('public') - ->directory('images') - ->imageEditor() - ->required(), - TextInput::make('alt') - ->label('Описание') - ->placeholder('Необязяательно') - ])->label('Изображение'), - Builder\Block::make('video') - ->schema([ - TextInput::make('mime')->readOnly(), - TextInput::make('title') - ->required() - ->maxLength(255) - ->autofocus(), - FileUpload::make('path') - ->required() - ->acceptedFileTypes([ - 'video/mp4', - 'video/quicktime', - 'video/x-msvideo', - 'video/x-ms-wmv', - 'video/avi', - 'video/webm', - 'video/ogg', - 'video/3gpp', - 'video/3gpp2', - 'video/x-m4v', - ]) - ->disk('public') - ->directory('videos') - ->afterStateUpdated(fn (callable $set, $state) => $set('mime', $state?->getMimeType())), - ]), - Builder\Block::make('postsList') - ->schema([ - Forms\Components\Grid::make(2)->schema([ - TextInput::make('count') - ->label('Количество запией') - ->integer(), - Select::make('category') - ->options(Category::all()->pluck('title', 'id')) - ]), - ])->label('Список новостей'), - ]) - ->collapsed() - ->blockNumbers(false) - ->collapsible() - ->addActionLabel('Добавить новый блок'), - ])->minItems(1), - ]), - Builder\Block::make('images') - ->schema([ - FileUpload::make('url') - ->label('Изображение(-я)') - ->image() - ->multiple() - ->reorderable() - ->maxFiles(5) - ->disk('public') - ->directory('images') - ->imageEditor() - ->required(), - TextInput::make('alt') - ->label('Описание') - ->placeholder('Необязяательно') - ])->label('Слайдер изображений'), - Builder\Block::make('image') - ->schema([ - FileUpload::make('url') - ->label('Изображение(-я)') - ->image() - ->multiple() - ->reorderable() - ->maxFiles(5) - ->disk('public') - ->directory('images') - ->imageEditor() - ->required(), - TextInput::make('alt') - ->label('Описание') - ->placeholder('Необязяательно') - ])->label('Изображение'), - Builder\Block::make('video') - ->schema([ - TextInput::make('mime')->readOnly(), - TextInput::make('title') - ->required() - ->maxLength(255) - ->autofocus(), - FileUpload::make('path') - ->required() - ->acceptedFileTypes([ - 'video/mp4', - 'video/quicktime', - 'video/x-msvideo', - 'video/x-ms-wmv', - 'video/avi', - 'video/webm', - 'video/ogg', - 'video/3gpp', - 'video/3gpp2', - 'video/x-m4v', - ]) - ->disk('public') - ->directory('videos') - ->afterStateUpdated(fn (callable $set, $state) => $set('mime', $state?->getMimeType())), - ]), - Builder\Block::make('postsList') - ->schema([ - Forms\Components\Grid::make(2)->schema([ - TextInput::make('count') - ->label('Количество запией') - ->integer(), - Select::make('category') - ->options(Category::all()->pluck('title', 'id')) - ]), - ])->label('Список новостей'), - Builder\Block::make('postItem') - ->schema([ - Select::make('post') - ->options(Post::query()->where('status', PostStatus::PUBLISHED)->pluck('title', 'id')) - ->searchable() - ->required(), - ])->label('Новость'), - Builder\Block::make('pageItem') - ->schema([ - Select::make('page') - ->options(Page::query()->where('title', '!=' , null)->where('is_visible', true)->pluck('title', 'id')) - ->searchable() - ->required(), - ])->label('Страница'), - Builder\Block::make('customForm') - ->schema([ - Select::make('form') - ->options(CustomForm::query()->where('status', CustomFormStatus::PUBLISHED)->pluck('title', 'form_id')) - ->searchable() - ->required(), - ])->label('Форма'), - Builder\Block::make('pageResourceList') - ->schema([ - Select::make('resource') - ->options(PageReferenceList::query()->where('is_active', true)->pluck('title', 'slug')) - ->searchable() - ->required(), - ])->label('Ресурсы'), - ]) - ->collapsed() - ->blockNumbers(false) - ->collapsible() - ->required() - ->blockPickerColumns(3) - ->blockPickerWidth('2xl') - ->addActionLabel('Добавить новый блок'), + ContentBuilderItem::getItem('content') ]), ]), - ]) - ]); + ]); } + public static function table(Table $table): Table { return $table ->columns([ - TextColumn::make('id')->label('ID')->sortable(), - TextColumn::make('title')->label('Название')->sortable()->searchable(), - TextColumn::make('created_at')->label('Дата создания')->sortable(), - Tables\Columns\ToggleColumn::make('is_active')->label('Активно')->sortable(), - ]) + TextColumn::make('title') + ->label('Название') + ->searchable() + ->sortable() + ->description(fn ($record) => $record->abbreviation), + + IconColumn::make('is_active') + ->label('Статус') + ->boolean() + ->trueIcon('heroicon-o-check-circle') + ->falseIcon('heroicon-o-x-circle') + ->trueColor('success') + ->falseColor('danger') + ->sortable(), + + TextColumn::make('updated_at') + ->label('Обновлено') + ->dateTime('d.m.Y H:i') + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + ]) ->filters([ - // + Tables\Filters\TernaryFilter::make('is_active') + ->label('Только активные') + ->placeholder('Все') + ->trueLabel('Активные') + ->falseLabel('Неактивные'), ]) ->actions([ - Tables\Actions\EditAction::make(), + Tables\Actions\EditAction::make() + ->iconButton() + ->tooltip('Редактировать'), + + Tables\Actions\ViewAction::make() + ->iconButton() + ->tooltip('Просмотреть'), ]) ->bulkActions([ Tables\Actions\BulkActionGroup::make([ - Tables\Actions\DeleteBulkAction::make(), + Tables\Actions\DeleteBulkAction::make() + ->label('Удалить выбранные') + ->modalHeading('Удаление факультетов') + ->modalDescription('Вы уверены, что хотите удалить выбранные факультеты?'), ]), - ]); + ]) + ->emptyStateActions([ + Tables\Actions\CreateAction::make() + ->label('Добавить факультет'), + ]) + ->defaultSort('title'); } public static function getRelations(): array @@ -474,4 +180,4 @@ class FacultyResource extends Resource 'edit' => Pages\EditFaculty::route('/{record}/edit'), ]; } -} +} \ No newline at end of file diff --git a/app/Filament/Resources/FacultyResource/Pages/CreateFaculty.php b/app/Filament/Resources/FacultyResource/Pages/CreateFaculty.php index 466ba62..61400c2 100644 --- a/app/Filament/Resources/FacultyResource/Pages/CreateFaculty.php +++ b/app/Filament/Resources/FacultyResource/Pages/CreateFaculty.php @@ -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 diff --git a/app/Filament/Resources/FacultyResource/Pages/EditFaculty.php b/app/Filament/Resources/FacultyResource/Pages/EditFaculty.php index 85d7b3f..84070ec 100644 --- a/app/Filament/Resources/FacultyResource/Pages/EditFaculty.php +++ b/app/Filament/Resources/FacultyResource/Pages/EditFaculty.php @@ -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); } diff --git a/app/Filament/Resources/FacultyResource/RelationManagers/DepartmentsRelationManager.php b/app/Filament/Resources/FacultyResource/RelationManagers/DepartmentsRelationManager.php index 7484878..4edd5f9 100644 --- a/app/Filament/Resources/FacultyResource/RelationManagers/DepartmentsRelationManager.php +++ b/app/Filament/Resources/FacultyResource/RelationManagers/DepartmentsRelationManager.php @@ -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(), ]), ]); } diff --git a/app/Filament/Resources/FacultyResource/RelationManagers/WorkersRelationManager.php b/app/Filament/Resources/FacultyResource/RelationManagers/WorkersRelationManager.php index c0651bd..5f442ce 100644 --- a/app/Filament/Resources/FacultyResource/RelationManagers/WorkersRelationManager.php +++ b/app/Filament/Resources/FacultyResource/RelationManagers/WorkersRelationManager.php @@ -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'); } -} +} \ No newline at end of file diff --git a/app/Filament/Resources/MainSliderResource.php b/app/Filament/Resources/MainSliderResource.php deleted file mode 100644 index b2c62ad..0000000 --- a/app/Filament/Resources/MainSliderResource.php +++ /dev/null @@ -1,221 +0,0 @@ -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'), - ]; - } -} diff --git a/app/Filament/Resources/MainSliderResource/Pages/CreateMainSlider.php b/app/Filament/Resources/MainSliderResource/Pages/CreateMainSlider.php deleted file mode 100644 index 150ddbc..0000000 --- a/app/Filament/Resources/MainSliderResource/Pages/CreateMainSlider.php +++ /dev/null @@ -1,20 +0,0 @@ -schema([ - Forms\Components\Section::make('')->schema([ - Tabs::make('Tabs') - ->tabs([ - Tabs\Tab::make('Основная информация') - ->schema([ - Forms\Components\Grid::make(2)->schema([ - TextInput::make('title')->label('Название ресурса')->required() - ->live(onBlur: true) - ->afterStateUpdated(function (string $operation, string|null $state, Forms\Set $set) { - $set('slug', Str::slug($state)); - $set('seo.title', $state); - }), - TextInput::make('slug')->label('Slug')->unique(ignoreRecord: true)->readOnly()->required(), - Toggle::make('is_active')->default(true)->label('Активный ресурс')->inline(false), - ]) - ]), - Tabs\Tab::make('Содержание ресурса') - ->schema([ - Repeater::make('content')->label('Ресурсы')->schema([ - Forms\Components\Section::make('Быстрая настройка ресурса')->schema([ - Forms\Components\Grid::make()->schema([ - Forms\Components\Select::make('model_select') - ->name('') - ->label('Выбор типа данных') - ->options([ - 'Post' => 'Новость', - 'Page' => 'Страница', - 'Event' => 'Мероприятие', - 'Custom' => 'Кастомная ссылка', - ]) - ->afterStateUpdated(function (string $operation, string|null $state, Forms\Set $set, Forms\Get $get) { - if ($get('model_select') === 'Custom') { - $set('model', null); - $set('title', null); - $set('content', null); - $set('link', null); - }; - })->live(onBlur: true), - - Forms\Components\Select::make('model') - ->label('Поиск данных') - ->name('') + Forms\Components\Section::make('Ресурс') + ->description('Управление контентом ресурса') + ->collapsible() + ->schema([ + Tabs::make('Настройки ресурса') + ->persistTabInQueryString() + ->columnSpanFull() + ->tabs([ + Tabs\Tab::make('Основная информация') + ->icon('heroicon-o-information-circle') + ->schema([ + Forms\Components\Grid::make(2) + ->schema([ + TextInput::make('title') + ->label('Название ресурса') + ->placeholder('Введите название ресурса') + ->helperText('Это название будет отображаться в административной панели') + ->required() + ->maxLength(255) ->live(onBlur: true) - ->searchable() - ->live(onBlur: true) - ->afterStateUpdated(function (string $operation, string|null $state, Forms\Set $set, Forms\Get $get) { - if ($get('model_select') === 'Post') { - $post = Post::find($state); - $set('title', $post->title); - $relativeUrl = parse_url(route('client.post.show', $post->slug), PHP_URL_PATH); - $set('link', $relativeUrl); - }; - if ($get('model_select') === 'Page') { - $page = Page::find($state); - $set('title', $page->title); - $set('link', $page->path); - }; - if ($get('model_select') === 'Event') { - $event = Event::find($state); - $set('title', $event->title); - $relativeUrl = parse_url(route('client.event.show', $event->slug), PHP_URL_PATH); - $set('link', $relativeUrl); - }; - - }) - ->options(function (Forms\Get $get) { - if ($get('model_select') === 'Post') { - return Post::where('status', '=', 'published')->pluck('title', 'id'); - }; - if ($get('model_select') === 'Page') { - return Page::where('title', '!=', null)->pluck('title', 'id'); - }; - if ($get('model_select') === 'Event') { - return Event::all()->pluck('title', 'id'); - }; - if ($get('model_select') === 'Custom') { - return []; - }; + ->afterStateUpdated(function (string $operation, string|null $state, Forms\Set $set) { + $set('slug', Str::slug($state)); + $set('seo.title', $state); }), - ]), - ]), - TextInput::make('title')->label('Заголовок ресурса')->required(), - FileUpload::make('image') - ->label('Изображение предпросмотра') - ->image() - ->optimize('webp') - ->resize(50) - ->disk('public') - ->directory('images') - ->imageEditor(), - Forms\Components\Grid::make(2)->schema([ - Forms\Components\TextInput::make('link') - ->label('Ссылка ресурса') - ->required(), - Forms\Components\TextInput::make('link_text') - ->default('Читать') - ->label('Текст кнопки') - ->required(), - ]), - ])->collapsed()->required(), - ]), - ]), + TextInput::make('slug') + ->label('URL-адрес (Slug)') + ->helperText('Автоматически генерируется из названия') + ->required() + ->unique(ignoreRecord: true) + ->readOnly() + ->maxLength(255), + + Toggle::make('is_active') + ->label('Активность ресурса') + ->helperText('Отключите, чтобы скрыть ресурс') + ->default(true) + ->inline(false) + ->onColor('success') + ->offColor('danger') + ->columnSpanFull(), + ]) + ]), + + Tabs\Tab::make('Содержание ресурса') + ->icon('heroicon-o-document-text') + ->schema([ + Repeater::make('content') + ->label('Элементы ресурса') + ->helperText('Добавьте и настройте элементы ресурса') + ->addActionLabel('Добавить элемент') + ->collapsed() + ->itemLabel(fn (array $state): ?string => $state['title'] ?? 'Новый элемент') + ->required() + ->schema([ + Forms\Components\Section::make('Быстрая настройка') + ->description('Выберите тип и источник данных') + ->collapsible() + ->collapsed() + ->schema([ + Forms\Components\Grid::make(2) + ->schema([ + Forms\Components\Select::make('model_select') + ->label('Тип данных') + ->placeholder('Выберите тип данных') + ->helperText('Выберите тип контента для этого элемента') + ->options([ + 'Post' => 'Новость', + 'Page' => 'Страница', + 'Event' => 'Мероприятие', + 'Custom' => 'Кастомная ссылка', + ]) + ->live(onBlur: true) + ->afterStateUpdated(function (string $operation, string|null $state, Forms\Set $set, Forms\Get $get) { + if ($get('model_select') === 'Custom') { + $set('model', null); + $set('title', null); + $set('content', null); + $set('link', null); + } + }), + + Forms\Components\Select::make('model') + ->label('Выбор элемента') + ->placeholder('Выберите элемент') + ->helperText(function (Forms\Get $get) { + if ($get('model_select') === 'Post') return 'Выберите новость'; + if ($get('model_select') === 'Page') return 'Выберите страницу'; + if ($get('model_select') === 'Event') return 'Выберите мероприятие'; + return 'Доступно после выбора типа данных'; + }) + ->searchable() + ->live(onBlur: true) + ->afterStateUpdated(function (string $operation, string|null $state, Forms\Set $set, Forms\Get $get) { + if ($get('model_select') === 'Post') { + $post = Post::find($state); + if ($post) { + $set('title', $post->title); + $relativeUrl = parse_url(route('client.post.show', $post->slug), PHP_URL_PATH); + $set('link', $relativeUrl); + } + } + if ($get('model_select') === 'Page') { + $page = Page::find($state); + if ($page) { + $set('title', $page->title); + $set('link', $page->path); + } + } + if ($get('model_select') === 'Event') { + $event = Event::find($state); + if ($event) { + $set('title', $event->title); + $relativeUrl = parse_url(route('client.event.show', $event->slug), PHP_URL_PATH); + $set('link', $relativeUrl); + } + } + }) + ->options(function (Forms\Get $get) { + if ($get('model_select') === 'Post') { + return Post::where('status', '=', 'published')->pluck('title', 'id'); + } + if ($get('model_select') === 'Page') { + return Page::whereNotNull('title')->pluck('title', 'id'); + } + if ($get('model_select') === 'Event') { + return Event::all()->pluck('title', 'id'); + } + return []; + }) + ->disabled(fn (Forms\Get $get) => empty($get('model_select')) || $get('model_select') === 'Custom'), + ]), + ]), + + TextInput::make('title') + ->label('Заголовок') + ->placeholder('Введите заголовок элемента') + ->helperText('Заголовок будет отображаться пользователям') + ->required() + ->maxLength(255), + + FileUpload::make('image') + ->label('Изображение предпросмотра') + ->helperText('Рекомендуемый формат: PNG, JPEG, JPG') + ->image() + ->optimize('webp') + ->resize(50) + ->disk('public') + ->directory('images') + ->imageEditor() + ->downloadable() + ->openable(), + + Forms\Components\Grid::make(2) + ->schema([ + Forms\Components\TextInput::make('link') + ->label('Ссылка') + ->placeholder('https://example.com или /path') + ->helperText('URL-адрес или относительный путь') + ->required() + ->maxLength(255), + + Forms\Components\TextInput::make('link_text') + ->label('Текст кнопки') + ->placeholder('Например: Читать далее') + ->helperText('Текст для кнопки перехода') + ->default('Читать') + ->required() + ->maxLength(50), + ]), + ]) + ->columnSpanFull(), + ]), + ]), ]), ]); } diff --git a/app/Filament/Resources/PageResource/Pages/CreatePage.php b/app/Filament/Resources/PageResource/Pages/CreatePage.php index 52d422e..68b7e1a 100644 --- a/app/Filament/Resources/PageResource/Pages/CreatePage.php +++ b/app/Filament/Resources/PageResource/Pages/CreatePage.php @@ -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; diff --git a/app/Filament/Resources/PageResource/Pages/EditPage.php b/app/Filament/Resources/PageResource/Pages/EditPage.php index 65a749e..b93d16c 100644 --- a/app/Filament/Resources/PageResource/Pages/EditPage.php +++ b/app/Filament/Resources/PageResource/Pages/EditPage.php @@ -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; diff --git a/app/Filament/Resources/PageResource/RelationManagers/SectionRelationManager.php b/app/Filament/Resources/PageResource/RelationManagers/SectionRelationManager.php index c45f80b..9181176 100644 --- a/app/Filament/Resources/PageResource/RelationManagers/SectionRelationManager.php +++ b/app/Filament/Resources/PageResource/RelationManagers/SectionRelationManager.php @@ -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([ diff --git a/app/Filament/Resources/PostResource.php b/app/Filament/Resources/PostResource.php index 8935126..c04b28a 100644 --- a/app/Filament/Resources/PostResource.php +++ b/app/Filament/Resources/PostResource.php @@ -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(), diff --git a/app/Filament/Resources/PostResource/Pages/CreatePost.php b/app/Filament/Resources/PostResource/Pages/CreatePost.php index 19dff94..5b2d412 100644 --- a/app/Filament/Resources/PostResource/Pages/CreatePost.php +++ b/app/Filament/Resources/PostResource/Pages/CreatePost.php @@ -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 { diff --git a/app/Filament/Resources/PostResource/Pages/EditPost.php b/app/Filament/Resources/PostResource/Pages/EditPost.php index 5a35db6..2d16bc3 100644 --- a/app/Filament/Resources/PostResource/Pages/EditPost.php +++ b/app/Filament/Resources/PostResource/Pages/EditPost.php @@ -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, diff --git a/app/Filament/Resources/ScheduleResource.php b/app/Filament/Resources/ScheduleResource.php index ce00bbd..57485b5 100644 --- a/app/Filament/Resources/ScheduleResource.php +++ b/app/Filament/Resources/ScheduleResource.php @@ -3,151 +3,104 @@ namespace App\Filament\Resources; use App\Filament\Resources\ScheduleResource\Pages; -use App\Filament\Resources\ScheduleResource\RelationManagers; -use App\Helpers\ByteConverter; -use App\Models\Category; use App\Models\EducationalGroup; use App\Models\Schedule; -use Closure; use Filament\Forms; -use Filament\Forms\Components\Builder; - -use Filament\Forms\Components\Checkbox; use Filament\Forms\Components\FileUpload; -use Filament\Forms\Components\Hidden; -use Filament\Forms\Components\RichEditor; +use Filament\Forms\Components\Grid; use Filament\Forms\Components\Section; use Filament\Forms\Components\Select; -use Filament\Forms\Components\SpatieTagsInput; use Filament\Forms\Components\TextInput; +use Filament\Forms\Components\Toggle; use Filament\Forms\Form; -use Filament\Forms\Get; use Filament\Resources\Resource; use Filament\Tables; use Filament\Tables\Columns\IconColumn; +use Filament\Tables\Columns\TextColumn; use Filament\Tables\Table; -use Illuminate\Database\Eloquent\SoftDeletingScope; use Illuminate\Support\Carbon; use Illuminate\Support\Str; -use Livewire\Component; use Livewire\Features\SupportFileUploads\TemporaryUploadedFile; -use Livewire\Livewire; class ScheduleResource extends Resource { protected static ?string $navigationGroup = 'Расписание и группы'; - protected static ?string $model = Schedule::class; - - protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack'; - - protected static ?string $pluralLabel = 'Расписание'; - - - protected static array $weekDays = ['Понедельник', 'Вторник', 'Среда', 'Четверг', 'Пятница', 'Суббота', 'Воскресенье']; - protected static array $typeWeek = ['Четная', 'Нечетная']; - - - - + protected static ?string $navigationIcon = 'heroicon-o-calendar'; + protected static ?string $pluralLabel = 'Расписания'; + protected static ?string $modelLabel = 'расписание'; public static function form(Form $form): Form { return $form ->schema([ - Section::make() + Section::make('Основные настройки') + ->description('Основная информация о расписании') + ->collapsible() ->schema([ - Forms\Components\Grid::make(2)->schema([ - Select::make('educational_group_id')->options(EducationalGroup::all()->pluck('title', 'id')) - ->live() - ->label('Выбрать группу') - ->required(), -// TextInput::make('title') -// ->live() -// ->label('Заголовок')->required(), + Grid::make(2) + ->schema([ + Select::make('educational_group_id') + ->label('Учебная группа') + ->options(EducationalGroup::query()->orderBy('title')->pluck('title', 'id')) + ->searchable() + ->preload() + ->required() + ->live() + ->helperText('Выберите группу для которой создается расписание'), -// Select::make('type')->options([ -// 'schedule' => 'Обычное расписание', -// 'interval' => 'Временное расписание', -// 'exam' => 'Промежуточная аттестация', -// ])->label('Тип расписания')->required()->live(), - Forms\Components\Toggle::make('is_zaoch')->label('Очная|Заочная')->inline(false), + Toggle::make('is_zaoch') + ->label('Форма обучения') + ->inline(false) + ->onColor('success') + ->offColor('primary') + ->helperText('Очная | Заочная') + ->afterStateHydrated(function (Toggle $component, $state) { + $component->state((bool) $state); + }), + ]), + ]), - ]), -// Forms\Components\Repeater::make('days')->label('')->schema([ -// Forms\Components\Repeater::make('form')->label('')->schema([ -// Forms\Components\Repeater::make('weeks')->label('')->schema([ -// Forms\Components\Repeater::make('lesson_info')->label('')->schema([ -// TextInput::make('title')->label('Название-пары'), -// TextInput::make('teacher')->label('Преподаватель'), -// TextInput::make('studyRoom')->label('Кабинет') -// ])->live()->maxItems(2)->collapsed()->addActionLabel('Добавить подгруппу')->columns(3) -// ->itemLabel(function (Get $get, $state) { -// static $count = 1; -// if (count($get('lesson_info')) === 1) { -// return "Общая группа"; -// } else { -// $nmb = $count++ % 2 == 0 ? 2 : 1; -// return "Подгруппа " . $nmb; } -// }), -// ])->maxItems(2)->addActionLabel('Добавить четную/нечетную неделю') -// ->itemLabel(function (Get $get) { -// static $position = 0; -// if (count($get('weeks')) === 1) { -// return "Общая неделя"; -// } else { -// $nmb = $position++ % 2 == 0 ? 0 : 1; -// return self::$typeWeek[$nmb] . " неделя"; -// } -// }) -// ->collapsed() -// ]) -// ->maxItems(5) -// ->itemLabel(function (Get $get) { -// static $count = 0; -// $maxCount = count($get('form')); -// $count = ($count++ <= $maxCount) ? $count : 1; -// return "Пара #" . $count; -// }) -// ->addActionLabel('Добавить пару'), -// ])->maxItems(6)->minItems(1)->itemLabel(function ($state) { -// static $position = 0; -// return self::$weekDays[$position++]; -// })->addActionLabel('Добавить день недели')->collapsed()->defaultItems(6)->hidden(function (callable $get) { -// if ($get('type') === 'schedule' || $get('type') === 'interval') { -// return false; -// } else { -// return true; -// } -// }), - ]), - Section::make() + Section::make('Файлы расписания') + ->description('Загрузите файлы с расписанием') + ->collapsible() ->schema([ - Forms\Components\Repeater::make('file')->schema([ + Forms\Components\Repeater::make('file') + ->label('') + ->addActionLabel('Добавить файл расписания') + ->schema([ + TextInput::make('title') + ->label('Название файла') + ->required() + ->maxLength(255) + ->placeholder('Например: "Расписание на весенний семестр 2024"') + ->helperText('Укажите понятное название файла для идентификации'), - TextInput::make('title') - ->required() - ->maxLength(255) - ->autofocus(), - FileUpload::make('path') - ->required() - ->getUploadedFileNameForStorageUsing( - fn (TemporaryUploadedFile $file): string => - str(Str::slug(pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME) . '-' . Carbon::now()->timestamp ) . '.' . $file->getClientOriginalExtension()) - ) - ->acceptedFileTypes([ - 'application/pdf', - ]) - ->maxSize(512000) - ->disk('public') - ->directory('files') - ->downloadable() - ->afterStateUpdated(function ($set, $state) { - $set('title', pathinfo($state?->getClientOriginalName(), PATHINFO_FILENAME)); - }) - ->visibility('public') - ]), - ]) + FileUpload::make('path') + ->label('Файл PDF') + ->required() + ->acceptedFileTypes(['application/pdf']) + ->maxSize(5120) // 5MB + ->disk('public') + ->directory('schedules') + ->downloadable() + ->openable() + ->previewable(false) + ->helperText('Только PDF файлы, макс. размер 5MB') + ->getUploadedFileNameForStorageUsing( + fn (TemporaryUploadedFile $file): string => + str(Str::slug(pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME) . '-' . Carbon::now()->timestamp) . '.' . $file->getClientOriginalExtension() + ) + ->afterStateUpdated(function ($set, $state) { + $set('title', pathinfo($state?->getClientOriginalName(), PATHINFO_FILENAME)); + }) + ->visibility('public')), + ]) + ->itemLabel(fn (array $state): ?string => $state['title'] ?? 'Новый файл') + ->collapsible() + ->cloneable() + ->defaultItems(1), + ]), ]); } @@ -155,29 +108,69 @@ class ScheduleResource extends Resource { return $table ->columns([ - Tables\Columns\TextColumn::make('title'), - Tables\Columns\TextColumn::make('type'), + TextColumn::make('educational_group.title') + ->label('Учебная группа') + ->sortable() + ->searchable(), + + TextColumn::make('file_count') + ->label('Файлов') + ->getStateUsing(fn ($record) => count($record->file ?? [])) + ->badge(), + IconColumn::make('is_zaoch') - ->boolean(), + ->label('Форма обучения') + ->boolean() + ->trueIcon('heroicon-o-academic-cap') + ->falseIcon('heroicon-o-building-office') + ->trueColor('success') + ->falseColor('primary') + ->formatStateUsing(fn ($state) => $state ? 'Заочная' : 'Очная'), + + TextColumn::make('updated_at') + ->label('Обновлено') + ->dateTime('d.m.Y H:i') + ->sortable(), ]) ->filters([ - // + Tables\Filters\SelectFilter::make('educational_group_id') + ->label('Учебная группа') + ->options(EducationalGroup::query()->orderBy('title')->pluck('title', 'id')) + ->searchable(), + + Tables\Filters\TernaryFilter::make('is_zaoch') + ->label('Форма обучения') + ->placeholder('Все') + ->trueLabel('Заочная') + ->falseLabel('Очная'), ]) ->actions([ - Tables\Actions\EditAction::make(), + Tables\Actions\EditAction::make() + ->iconButton() + ->tooltip('Редактировать'), + + Tables\Actions\ViewAction::make() + ->iconButton() + ->tooltip('Просмотреть'), ]) ->bulkActions([ Tables\Actions\BulkActionGroup::make([ - Tables\Actions\DeleteBulkAction::make(), + Tables\Actions\DeleteBulkAction::make() + ->label('Удалить выбранные') + ->modalHeading('Удаление расписаний') + ->modalDescription('Вы уверены, что хотите удалить выбранные расписания? Это действие нельзя отменить.'), ]), - ]); + ]) + ->emptyStateActions([ + Tables\Actions\CreateAction::make() + ->label('Добавить расписание'), + ]) + ->defaultSort('educational_group.title'); } public static function getRelations(): array { - return [ - // - ]; + return []; } public static function getPages(): array @@ -188,4 +181,4 @@ class ScheduleResource extends Resource 'edit' => Pages\EditSchedule::route('/{record}/edit'), ]; } -} +} \ No newline at end of file diff --git a/app/Filament/Resources/Shield/RoleResource.php b/app/Filament/Resources/Shield/RoleResource.php index e0e5a1c..27bc2aa 100644 --- a/app/Filament/Resources/Shield/RoleResource.php +++ b/app/Filament/Resources/Shield/RoleResource.php @@ -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 diff --git a/app/Filament/Resources/SlideResource.php b/app/Filament/Resources/SlideResource.php index 0f1bdae..fcae4fc 100644 --- a/app/Filament/Resources/SlideResource.php +++ b/app/Filament/Resources/SlideResource.php @@ -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 diff --git a/app/Filament/Resources/SliderResource.php b/app/Filament/Resources/SliderResource.php index f458429..28c13c5 100644 --- a/app/Filament/Resources/SliderResource.php +++ b/app/Filament/Resources/SliderResource.php @@ -6,37 +6,61 @@ use App\Filament\Resources\SliderResource\Pages; use App\Filament\Resources\SliderResource\RelationManagers; use App\Models\Slider; use Filament\Forms; +use Filament\Forms\Components\Section; use Filament\Forms\Components\TextInput; use Filament\Forms\Components\Toggle; use Filament\Forms\Form; use Filament\Resources\Resource; use Filament\Tables; +use Filament\Tables\Columns\TextColumn; use Filament\Tables\Table; -use Illuminate\Database\Eloquent\Builder; -use Illuminate\Database\Eloquent\SoftDeletingScope; use Illuminate\Support\Str; class SliderResource extends Resource { protected static ?string $model = Slider::class; - protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack'; + protected static ?string $navigationGroup = 'Виджеты'; + protected static ?string $navigationLabel = 'Слайдеры'; + protected static ?string $modelLabel = 'Слайдер'; + protected static ?string $pluralModelLabel = 'Слайдеры'; + protected static ?string $navigationIcon = 'heroicon-o-photo'; public static function form(Form $form): Form { return $form ->schema([ - Forms\Components\Section::make()->schema([ - Forms\Components\TextInput::make('title') - ->live(onBlur: true) - ->afterStateUpdated(function (string $operation, string|null $state, Forms\Set $set) { - $set('slug', Str::slug($state)); - }) - ->label('Заголовок слайдера') - ->required(), - TextInput::make('slug')->label('Slug')->unique(ignoreRecord: true)->readOnly()->required(), - Toggle::make('is_active')->default(true)->label('Активный слайдер')->inline(false), - ]), + Section::make('Настройки слайдера') + ->description('Основные параметры отображения слайдера') + ->collapsible() + ->schema([ + TextInput::make('title') + ->label('Название слайдера') + ->placeholder('Например: Главный слайдер') + ->helperText('Это название будет использоваться в административной панели') + ->required() + ->maxLength(255) + ->live(onBlur: true) + ->afterStateUpdated(function (string $operation, string|null $state, Forms\Set $set) { + $set('slug', Str::slug($state)); + }), + + TextInput::make('slug') + ->label('URL-идентификатор') + ->helperText('Автоматически генерируется из названия') + ->required() + ->unique(ignoreRecord: true) + ->readOnly() + ->maxLength(255), + + Toggle::make('is_active') + ->label('Активность слайдера') + ->helperText('Отключите, чтобы временно скрыть слайдер') + ->default(true) + ->inline(false) + ->onColor('success') + ->offColor('danger'), + ]), ]); } @@ -44,25 +68,60 @@ class SliderResource extends Resource { return $table ->columns([ - // + TextColumn::make('title') + ->label('Название') + ->sortable() + ->searchable() + ->description(fn (Slider $record) => $record->slug), + + TextColumn::make('slides_count') + ->counts('slides') + ->label('Кол-во слайдов') + ->badge() + ->color(fn (int $state): string => $state > 0 ? 'success' : 'danger'), + + TextColumn::make('is_active') + ->label('Статус') + ->badge() + ->color(fn (bool $state): string => $state ? 'success' : 'danger') + ->formatStateUsing(fn (bool $state): string => $state ? 'Активен' : 'Неактивен'), ]) ->filters([ - // + Tables\Filters\SelectFilter::make('is_active') + ->label('Статус активности') + ->options([ + true => 'Активные', + false => 'Неактивные', + ]), ]) ->actions([ - Tables\Actions\EditAction::make(), + Tables\Actions\EditAction::make() + ->icon('heroicon-o-pencil') + ->tooltip('Редактировать'), + + Tables\Actions\Action::make('manage_slides') + ->icon('heroicon-o-photo') + ->tooltip('Управление слайдами') + ->url(fn (Slider $record) => SliderResource::getUrl('edit', ['record' => $record]) . '?activeRelationManager=0'), ]) ->bulkActions([ Tables\Actions\BulkActionGroup::make([ - Tables\Actions\DeleteBulkAction::make(), + Tables\Actions\DeleteBulkAction::make() + ->icon('heroicon-o-trash') + ->label('Удалить выбранное'), ]), - ]); + ]) + ->emptyStateActions([ + Tables\Actions\CreateAction::make() + ->label('Создать слайдер'), + ]) + ->defaultSort('title', 'asc'); } public static function getRelations(): array { return [ - // + RelationManagers\SlidesRelationManager::class, ]; } @@ -74,4 +133,4 @@ class SliderResource extends Resource 'edit' => Pages\EditSlider::route('/{record}/edit'), ]; } -} +} \ No newline at end of file diff --git a/app/Filament/Resources/SliderResource/RelationManagers/SlidesRelationManager.php b/app/Filament/Resources/SliderResource/RelationManagers/SlidesRelationManager.php new file mode 100644 index 0000000..edac4f3 --- /dev/null +++ b/app/Filament/Resources/SliderResource/RelationManagers/SlidesRelationManager.php @@ -0,0 +1,310 @@ +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('Добавить слайд'), + ]); + }} \ No newline at end of file diff --git a/app/Filament/Resources/UserDetailResource.php b/app/Filament/Resources/UserDetailResource.php index 63d3a44..9234b7d 100644 --- a/app/Filament/Resources/UserDetailResource.php +++ b/app/Filament/Resources/UserDetailResource.php @@ -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 = 'Доп. Информация'; diff --git a/app/Filament/Resources/UserResource.php b/app/Filament/Resources/UserResource.php index 3a169e0..8bb490d 100644 --- a/app/Filament/Resources/UserResource.php +++ b/app/Filament/Resources/UserResource.php @@ -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 = 'Пользователи'; diff --git a/app/Http/Controllers/ClientAcademicJournalController.php b/app/Http/Controllers/ClientAcademicJournalController.php index 12658eb..eec9efb 100644 --- a/app/Http/Controllers/ClientAcademicJournalController.php +++ b/app/Http/Controllers/ClientAcademicJournalController.php @@ -2,37 +2,76 @@ namespace App\Http\Controllers; +use App\Enums\CacheKeys; use App\Http\Resources\ClientAcademicJournalListResource; -use App\Http\Resources\ClientVirtualExhibitionListResource; use App\Models\AcademicJournal; use App\Models\JournalIssue; -use App\Models\VirtualExhibition; -use Illuminate\Http\Request; +use App\Services\App\Breadcrumb\BreadcrumbService; +use App\Services\App\Seo\SeoPageProvider; +use Illuminate\Support\Facades\Cache; use Inertia\Inertia; class ClientAcademicJournalController extends Controller { + public function __construct(readonly SeoPageProvider $seoPageProvider){} + public function index() { - $journals = ClientAcademicJournalListResource::collection(AcademicJournal::query()->get()); + $journals = Cache::remember( + CacheKeys::ACADEMIC_JOURNALS_PREFIX->value . 'list', + now()->addWeek(), // Кешируем на неделю, так как журналы меняются редко + function () { + return ClientAcademicJournalListResource::collection( + AcademicJournal::query()->get() + ); + } + ); - return Inertia::render('Client/AcademicJournals/Index', compact('journals')); + $seo = $this->seoPageProvider->getSeoForCurrentPage(); + + return Inertia::render('Client/AcademicJournals/Index', compact('journals', 'seo')); } public function show(string $slug) { - $journal = new ClientAcademicJournalListResource(AcademicJournal::query()->where('slug', '=', $slug)->firstOrFail()); - $journalIssues = JournalIssue::where('academic_journal_id', $journal->id) - ->groupBy('year_publication')->get(); + // Кешируем основной журнал + [$journal, $seo] = Cache::remember( + CacheKeys::ACADEMIC_JOURNAL_PREFIX->value . $slug, + now()->addWeek(), + function () use ($slug) { + $journal = AcademicJournal::query() + ->where('slug', $slug) + ->firstOrFail(); + $seo = $this->seoPageProvider->getSeoForModel($journal); + return [ + new ClientAcademicJournalListResource($journal), + $seo + ]; + } + ); - $journals = []; + // Кешируем выпуски журнала, сгруппированные по годам + $journals = Cache::remember( + CacheKeys::ACADEMIC_JOURNAL_PREFIX->value . 'issues_' . $slug, + now()->addWeek(), + function () use ($journal) { + $journalIssues = JournalIssue::where('academic_journal_id', $journal->id) + ->get() + ->groupBy('year_publication'); - foreach ($journalIssues as $year => $journalGroup) { - $journals[] = [ - 'year_publication' => $year, - 'journalIssues' => $journalGroup - ]; - } - return Inertia::render('Client/AcademicJournals/Show', compact('journal', 'journals')); + $groupedIssues = []; + foreach ($journalIssues as $year => $journalGroup) { + $groupedIssues[] = [ + 'year_publication' => $year, + 'journalIssues' => $journalGroup + ]; + } + + return $groupedIssues; + } + ); + + + return Inertia::render('Client/AcademicJournals/Show', compact('journal', 'journals', 'seo')); } } diff --git a/app/Http/Controllers/ClientAdditionalEducationController.php b/app/Http/Controllers/ClientAdditionalEducationController.php index 6ca9288..557561a 100644 --- a/app/Http/Controllers/ClientAdditionalEducationController.php +++ b/app/Http/Controllers/ClientAdditionalEducationController.php @@ -2,6 +2,7 @@ namespace App\Http\Controllers; +use App\Enums\CacheKeys; use App\Enums\FormEducation; use App\Http\Resources\AdditionalEducationCategoryPreviewResource; use App\Http\Resources\AdditionalEducationCategoryResource; @@ -14,128 +15,148 @@ use App\Models\AdditionalEducation; use App\Models\AdditionalEducationCategory; use App\Models\DirectionAdditionalEducation; use App\Models\Page; +use App\Services\App\Breadcrumb\BreadcrumbService; +use App\Services\App\Seo\SeoPageProvider; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Cache; use Inertia\Inertia; class ClientAdditionalEducationController extends Controller { + public function __construct(readonly SeoPageProvider $seoPageProvider){} + + public function index(Request $request) { + $cacheKey = md5(serialize([ + 'direction' => $request->input('direction'), + 'form' => $request->input('form'), + 'category' => $request->input('category'), + ])); - $directionAdditionalEducations = DirectionAdditionalEducationResource::collection( - DirectionAdditionalEducation::query() - ->where('is_active', true) - ->whereHas('additionalEducationCategories', function ($q) { - $q->whereHas('additionalEducations'); - })->get()); - - $additionalEducations = AdditionalEducationCategoryResource::collection(AdditionalEducationCategory::query() - ->WithActivePrograms() - ->where('is_active', '=', true) - ->when($request->input('direction'), function ($q, $direction) { - $q->whereHas('direction', function ($query) use ($direction) { - $query->where('slug', $direction); - }); - }) - ->when(request()->input('form'), function ($query, $form) { - $query->whereHas('additionalEducations', function ($q) use ($form) { - $q->where('form_education', FormEducation::fromName($form)); - }); - $query->with(['additionalEducations' => function ($q) use ($form) { - $q->where('form_education', FormEducation::fromName($form)); - }]); - }) - ->when(request()->input('category'), function ($query) { - $slugs = request()->input('category'); - if (is_array($slugs)) { - $query->whereIn('slug', $slugs); - } - }) - ->has('additionalEducations') - ->get()); - - $categories = AdditionalEducationCategoryPreviewResource::collection( - AdditionalEducationCategory::query() - ->where('is_active', true) - ->has('additionalEducations') - ->get() + // Основные данные (кешируются) + $directionAdditionalEducations = Cache::remember( + CacheKeys::ADDITIONAL_EDUCATIONAL_PROGRAMS_PREFIX->value . 'directions_' . $cacheKey, + now()->addDay(), + function () { + return DirectionAdditionalEducationResource::collection( + DirectionAdditionalEducation::query() + ->where('is_active', true) + ->whereHas('additionalEducationCategories', fn ($q) => $q->whereHas('additionalEducations')) + ->get() + ); + } ); + + $additionalEducations = Cache::remember( + CacheKeys::ADDITIONAL_EDUCATIONAL_PROGRAMS_PREFIX->value . $cacheKey, + now()->addDay(), + function () use ($request) { + return AdditionalEducationCategoryResource::collection( + AdditionalEducationCategory::query() + ->WithActivePrograms() + ->where('is_active', true) + ->when($request->direction, fn ($q, $direction) => + $q->whereHas('direction', fn ($query) => $query->where('slug', $direction)) + ) + ->when($request->form, fn ($query, $form) => + $query->whereHas('additionalEducations', fn ($q) => + $q->where('form_education', FormEducation::fromName($form)) + ) + ->when($request->category, fn ($query) => + is_array($request->category) + ? $query->whereIn('slug', $request->category) + : $query + ) + ->has('additionalEducations') + ->get() + )); + } + ); + + $categories = Cache::remember( + CacheKeys::ADDITIONAL_EDUCATIONAL_PROGRAMS_PREFIX->value . 'categories', + now()->addWeek(), + function () { + return AdditionalEducationCategoryPreviewResource::collection( + AdditionalEducationCategory::query() + ->where('is_active', true) + ->has('additionalEducations') + ->get() + ); + } + ); + + // Динамические данные (не кешируются) $categoriesContent = []; - if (request()->input('category')) { - foreach (request()->input('category') as $item) { - $categoriesContent[$item] = new AdditionalEducationCategoryResource(AdditionalEducationCategory::where('slug', $item)->first()); + if ($request->category) { + foreach ((array)$request->category as $item) { + $categoriesContent[$item] = new AdditionalEducationCategoryResource( + AdditionalEducationCategory::where('slug', $item)->first() + ); } } - $forms_education = []; - foreach (FormEducation::cases() as $case) { - $forms_education[$case->name] = $case->getLabel(); - } + $forms_education = array_reduce( + FormEducation::cases(), + fn ($acc, $case) => $acc + [$case->name => $case->getLabel()], + [] + ); + $filters = [ 'direction_filter' => [ 'type' => 'direction', - 'value' => request()->input('direction'), + 'value' => $request->input('direction'), 'param' => 'direction' ], 'form_education_filter' => [ 'type' => 'form', - 'value' => request()->input('form'), + 'value' => $request->input('form'), 'param' => 'form' ], 'category_filter' => [ 'type' => 'category', - 'value' => request()->input('category'), + 'value' => $request->input('category'), 'param' => 'category', 'content' => $categoriesContent, ], ]; - $routeUrl = route('client.additionalEducation.index'); - $path = ltrim(parse_url($routeUrl, PHP_URL_PATH), '/'); + $seo = $this->seoPageProvider->getSeoForCurrentPage(); - $page = Page::where('path', '=', $path)->with('section.pages.section', 'section.mainSection')->first(); - if (isset($page->section)) { - $breadcrumbs = [ - 'mainSection' => new ClientBreadcrumbSection($page->section->mainSection), - 'subSection' => new ClientBreadcrumbSubSection($page->section), - 'page' => new ClientBreadcrumbPage($page), - ]; - } else { - $breadcrumbs = null; - } - - return Inertia::render('Client/Additional-educations/Index', - compact( - 'directionAdditionalEducations', - 'additionalEducations', - 'filters', - 'forms_education', - 'categories', - 'breadcrumbs' - )); + return Inertia::render('Client/Additional-educations/Index', compact( + 'directionAdditionalEducations', + 'additionalEducations', + 'filters', + 'forms_education', + 'categories', + 'seo' + )); } - public function show(string $slug) { - $additionalEducation = new AdditionalEducationResource(AdditionalEducation::query()->with('category.direction')->where('slug', $slug)->first()); - $routeUrl = route('client.additionalEducation.index'); - $path = ltrim(parse_url($routeUrl, PHP_URL_PATH), '/'); + // Кешируем основную программу дополнительного образования + [$additionalEducation, $seo] = Cache::remember( + CacheKeys::ADDITIONAL_EDUCATIONAL_PROGRAM_PREFIX->value . $slug, + now()->addDay(), + function () use ($slug) { + $additionalEducation = AdditionalEducation::query() + ->with('category.direction') + ->where('slug', $slug) + ->first(); + $seo = $this->seoPageProvider->getSeoForModel($additionalEducation); + return [ + new AdditionalEducationResource($additionalEducation), + $seo + ]; + } + ); - $page = Page::where('path', '=', $path)->with('section.pages.section', 'section.mainSection')->first(); + // SEO-данные берём из кешированного ресурса - if (isset($page->section)) { - $breadcrumbs = [ - 'mainSection' => new ClientBreadcrumbSection($page->section->mainSection), - 'subSection' => new ClientBreadcrumbSubSection($page->section), - 'page' => new ClientBreadcrumbPage($page), - ]; - } else { - $breadcrumbs = null; - } - - $seo = $additionalEducation->seo ?? null; - - return Inertia::render('Client/Additional-educations/Show', compact('additionalEducation', 'breadcrumbs', 'seo')); - } -} + return Inertia::render('Client/Additional-educations/Show', compact( + 'additionalEducation', + 'seo' + )); + }} diff --git a/app/Http/Controllers/ClientDepartmentController.php b/app/Http/Controllers/ClientDepartmentController.php index d515ebd..7f3b49e 100644 --- a/app/Http/Controllers/ClientDepartmentController.php +++ b/app/Http/Controllers/ClientDepartmentController.php @@ -2,34 +2,93 @@ namespace App\Http\Controllers; +use App\Enums\CacheKeys; use App\Http\Resources\ClientDepartmentPreviewResource; use App\Http\Resources\DepartmentResource; use App\Models\Department; use App\Models\Faculty; +use App\Services\App\Breadcrumb\BreadcrumbService; +use App\Services\App\Seo\SeoPageProvider; use Illuminate\Database\Eloquent\Collection; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Cache; use Inertia\Inertia; class ClientDepartmentController extends Controller { + public function __construct(readonly SeoPageProvider $seoPageProvider){} + public function show(string $facultySlug, string $departmentSlug) { - $faculty = Faculty::query()->where('slug', $facultySlug)->first(); - $departments = ClientDepartmentPreviewResource::collection( - Department::query() - ->where('is_active', true) - ->where('faculty_id', $faculty->id) - ->get() - ); - $department = new DepartmentResource(Department::query() - ->where('slug', $departmentSlug) - ->where('is_active', true) - ->with(['faculty', 'workers.userDetail', 'teachers.userDetail', 'programs.directionStudy']) - ->first()); - $directions = $this->groupProgramsByDirection($department->programs); + // Ключ для кеширования + $cacheKey = "{$facultySlug}_{$departmentSlug}"; - $seo = $department->seo ?? null; - return Inertia::render('Client/Departments/Show', compact('department', 'departments', 'directions', 'seo')); + // Кешируем факультет + $faculty = Cache::remember( + CacheKeys::FACULTY_PREFIX->value . $facultySlug, + now()->addDay(), + function () use ($facultySlug) { + return Faculty::query() + ->where('slug', $facultySlug) + ->first(); + } + ); + + // Кешируем список активных кафедр факультета + $departments = Cache::remember( + CacheKeys::DEPARTMENTS_PREFIX->value . 'active_' . $faculty->id, + now()->addDay(), + function () use ($faculty) { + return ClientDepartmentPreviewResource::collection( + Department::query() + ->where('is_active', true) + ->where('faculty_id', $faculty->id) + ->get() + ); + } + ); + + // Кешируем полные данные кафедры с отношениями + [$department, $seo] = Cache::remember( + CacheKeys::DEPARTMENT_PREFIX->value . $cacheKey, + now()->addDay(), + function () use ($departmentSlug) { + $department = Department::query() + ->where('slug', $departmentSlug) + ->where('is_active', true) + ->with([ + 'faculty', + 'workers.userDetail', + 'teachers.userDetail', + 'programs.directionStudy', + 'seo' + ]) + ->first(); + + $seo = $this->seoPageProvider->getSeoForModel($department); + return [ + new DepartmentResource($department), + $seo + ]; + } + ); + + // Кешируем сгруппированные направления + $directions = Cache::remember( + CacheKeys::DEPARTMENT_PREFIX->value . 'directions_' . $cacheKey, + now()->addDay(), + function () use ($department) { + return $this->groupProgramsByDirection($department->programs); + } + ); + + + return Inertia::render('Client/Departments/Show', compact( + 'department', + 'departments', + 'directions', + 'seo', + )); } diff --git a/app/Http/Controllers/ClientDivisionController.php b/app/Http/Controllers/ClientDivisionController.php index 5ef8d33..b2d6e58 100644 --- a/app/Http/Controllers/ClientDivisionController.php +++ b/app/Http/Controllers/ClientDivisionController.php @@ -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')); } } diff --git a/app/Http/Controllers/ClientEventController.php b/app/Http/Controllers/ClientEventController.php index 31fe635..9570431 100644 --- a/app/Http/Controllers/ClientEventController.php +++ b/app/Http/Controllers/ClientEventController.php @@ -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 diff --git a/app/Http/Controllers/ClientFacultyController.php b/app/Http/Controllers/ClientFacultyController.php index 0265c5f..c81f0df 100644 --- a/app/Http/Controllers/ClientFacultyController.php +++ b/app/Http/Controllers/ClientFacultyController.php @@ -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')); } } diff --git a/app/Http/Controllers/ClientPostController.php b/app/Http/Controllers/ClientPostController.php index c4988bf..03e0e24 100644 --- a/app/Http/Controllers/ClientPostController.php +++ b/app/Http/Controllers/ClientPostController.php @@ -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); } diff --git a/app/Http/Controllers/ClientProgramController.php b/app/Http/Controllers/ClientProgramController.php index 3866c1e..9f18d32 100644 --- a/app/Http/Controllers/ClientProgramController.php +++ b/app/Http/Controllers/ClientProgramController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers; use App\Enums\BudgetEducation; +use App\Enums\CacheKeys; use App\Enums\FormEducation; use App\Enums\LevelEducational; use App\Http\Resources\CampaignDegreeResource; @@ -16,121 +17,150 @@ use App\Models\CampaignDegree; use App\Models\DirectionStudy; use App\Models\EducationalProgram; use App\Models\MainSection; +use App\Services\App\Seo\SeoPageProvider; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\DB; use Illuminate\Support\Str; use Inertia\Inertia; class ClientProgramController extends Controller { + public function __construct(readonly SeoPageProvider $seoPageProvider){} + public function index(Request $request) { - $activeCampaign = AdmissionCampaign::query()->where('status', 1)->first(); + $cacheKey = CacheKeys::EDUCATION_PROGRAMS_PREFIX->value . md5(serialize($request->all())); - $uniqueValues = EducationalProgram::distinct()->pluck('lvl_edu'); - $levelsEducational = $uniqueValues->mapWithKeys(function ($level) { - return [$level->name => $level->getLabel()]; - }); + $data = Cache::remember($cacheKey, now()->addHours(1), function () use ($request) { + $activeCampaign = AdmissionCampaign::query()->where('status', 1)->first(); - $direction_studies = DirectionStudy::query() - ->withAdmissionCampaignByYear($activeCampaign->academic_year) - ->withActivePrograms() - ->get(); + $uniqueValues = EducationalProgram::distinct()->pluck('lvl_edu'); + $levelsEducational = $uniqueValues->mapWithKeys(function ($level) { + return [$level->name => $level->getLabel()]; + }); - $level = request()->input('level'); - $form = request()->input('form'); - $budget = request()->input('budget'); - - $naprs = DirectionStudyResource::collection( - DirectionStudy::query() + $direction_studies = DirectionStudy::query() ->withAdmissionCampaignByYear($activeCampaign->academic_year) ->withActivePrograms() - ->with('programs.admission_plans') - ->when($level, function ($query) use ($level) { - $query->where('lvl_edu', LevelEducational::fromName($level)->value); - }) - ->when($form, function ($query) use ($form) { - $this->applyFormFilter($query, $form); - }) - ->when($budget, function ($query) use ($budget) { - $this->applyBudgetFilter($query, $budget); - }) - ->when(request()->input('direction'), function ($query) { - $slugs = request()->input('direction'); - if (is_array($slugs)) { - $query->whereIn('slug', $slugs); - } - }) - ->get() - ); + ->get(); + + $level = request()->input('level'); + $form = request()->input('form'); + $budget = request()->input('budget'); + + $naprs = DirectionStudyResource::collection( + DirectionStudy::query() + ->withAdmissionCampaignByYear($activeCampaign->academic_year) + ->withActivePrograms() + ->with('programs.admission_plans') + ->when($level, function ($query) use ($level) { + $query->where('lvl_edu', LevelEducational::fromName($level)->value); + }) + ->when($form, function ($query) use ($form) { + $this->applyFormFilter($query, $form); + }) + ->when($budget, function ($query) use ($budget) { + $this->applyBudgetFilter($query, $budget); + }) + ->when(request()->input('direction'), function ($query) { + $slugs = request()->input('direction'); + if (is_array($slugs)) { + $query->whereIn('slug', $slugs); + } + }) + ->get() + ); + + $campaignName = $this->getAdmissionCampaignName(); + $formsEducational = FormEducation::cases(); + $formsEducational = collect($formsEducational); + $formsEdu = $formsEducational->mapWithKeys(function ($formEducational) { + return [$formEducational->name => $formEducational->getLabel()]; + }); + $typesBudget = BudgetEducation::cases(); + $typesBudget = collect($typesBudget); + $budgetEdu = $typesBudget->mapWithKeys(function ($typeBudget) { + return [$typeBudget->name => $typeBudget->getLabel()]; + }); + + $filters = [ + 'level_filter' => [ + 'type' => 'level', + 'value' => request()->input('level'), + 'param' => 'level' + ], + 'budget_filter' => [ + 'type' => 'budget', + 'value' => request()->input('budget'), + 'param' => 'budget' + ], + 'formEdu_filter' => [ + 'type' => 'form', + 'value' => request()->input('form'), + 'param' => 'form' + ], + 'direction_filter' => [ + 'type' => 'direction', + 'value' => request()->input('direction'), + 'param' => 'direction' + ], + ]; + + $seo = $this->seoPageProvider->getSeoForCurrentPage(); - $campaignName = $this->getAdmissionCampaignName(); - $formsEducational = FormEducation::cases(); - $formsEducational = collect($formsEducational); - $formsEdu = $formsEducational->mapWithKeys(function ($formEducational) { - return [$formEducational->name => $formEducational->getLabel()]; - }); - $typesBudget = BudgetEducation::cases(); - $typesBudget = collect($typesBudget); - $budgetEdu = $typesBudget->mapWithKeys(function ($typeBudget) { - return [$typeBudget->name => $typeBudget->getLabel()]; - }); - - $filters = [ - 'level_filter' => [ - 'type' => 'level', - 'value' => request()->input('level'), - 'param' => 'level' - ], - 'budget_filter' => [ - 'type' => 'budget', - 'value' => request()->input('budget'), - 'param' => 'budget' - ], - 'formEdu_filter' => [ - 'type' => 'form', - 'value' => request()->input('form'), - 'param' => 'form' - ], - 'direction_filter' => [ - 'type' => 'direction', - 'value' => request()->input('direction'), - 'param' => 'direction' - ], - ]; - - return Inertia::render('Client/Programs/Index', - compact( + return compact( 'naprs', 'campaignName', 'levelsEducational', 'filters', 'formsEdu', 'budgetEdu', - 'direction_studies' - )); + 'direction_studies', + 'seo' + ); + }); + + + return Inertia::render('Client/Programs/Index', $data); } public function show(string $slug) { - $program = new EducationalProgramFullResource(EducationalProgram::query()->where('slug', $slug)->with(['admission_plans', 'directionStudy'])->firstOrFail()); - $formsEducational = BudgetEducation::cases(); - $formsEducational = collect($formsEducational); - $formsEdu = $formsEducational->mapWithKeys(function ($formEducational) { - return [$formEducational->value => $formEducational->getLabel()]; + $cacheKey = CacheKeys::EDUCATION_PROGRAM_PREFIX->value . md5($slug); + + $data = Cache::remember($cacheKey, now()->addHours(1), function () use ($slug) { + $program = new EducationalProgramFullResource( + $programModel = EducationalProgram::query() + ->where('slug', $slug) + ->with(['admission_plans', 'directionStudy', 'seo']) + ->firstOrFail() + ); + + $formsEducational = BudgetEducation::cases(); + $formsEducational = collect($formsEducational); + $formsEdu = $formsEducational->mapWithKeys(function ($formEducational) { + return [$formEducational->value => $formEducational->getLabel()]; + }); + + $seo = $this->seoPageProvider->getSeoForModel($programModel); + + return compact('program', 'formsEdu', 'seo'); }); - $seo = $program->seo ?? null; - return Inertia::render('Client/Programs/Show', compact('program', 'formsEdu', 'seo')); + return Inertia::render('Client/Programs/Show', $data); } - private function getAdmissionCampaignName() : string + private function getAdmissionCampaignName(): string { - $campaign = AdmissionCampaign::query()->where('status', 1)->first(); - return $campaign->name; - } + $cacheKey = CacheKeys::EDUCATION_PROGRAM_PREFIX->value . 'active_campaign_name'; + return Cache::remember($cacheKey, now()->addHours(1), function () { + $campaign = AdmissionCampaign::query()->where('status', 1)->first(); + return $campaign->name; + }); + } private function applyFormFilter($query, $form) { @@ -150,7 +180,6 @@ class ClientProgramController extends Controller { $budgetValue = Str::of(BudgetEducation::fromName($budget)->value)->toString(); - $query->whereHas('programs.admission_plans', function ($query) use ($budgetValue) { $query->whereJsonContains('contests', ['financing_source' => $budgetValue]); }) @@ -160,5 +189,4 @@ class ClientProgramController extends Controller }); }]); } - -} +} \ No newline at end of file diff --git a/app/Http/Controllers/ClientScheduleController.php b/app/Http/Controllers/ClientScheduleController.php index 52ce791..0be3318 100644 --- a/app/Http/Controllers/ClientScheduleController.php +++ b/app/Http/Controllers/ClientScheduleController.php @@ -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) diff --git a/app/Http/Controllers/ClientWidgetAdditionalEducationalProgramController.php b/app/Http/Controllers/ClientWidgetAdditionalEducationalProgramController.php index 7726afd..8fdd9c2 100644 --- a/app/Http/Controllers/ClientWidgetAdditionalEducationalProgramController.php +++ b/app/Http/Controllers/ClientWidgetAdditionalEducationalProgramController.php @@ -2,22 +2,31 @@ namespace App\Http\Controllers; +use App\Enums\CacheKeys; use App\Enums\PostStatus; use App\Http\Resources\AdditionalEducationSearchResource; use App\Http\Resources\PostThumbnailResource; use App\Models\AdditionalEducation; use App\Models\Post; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Cache; class ClientWidgetAdditionalEducationalProgramController extends Controller { public function index() { - return AdditionalEducationSearchResource::collection( - AdditionalEducation::query() - ->where('is_active', true) - ->orderBy('title', 'desc') - ->get()); + return Cache::remember( + CacheKeys::ADDITIONAL_EDUCATIONAL_PROGRAMS_PREFIX->value . 'search_list', + now()->addDay(), // Кешируем на 1 день + function () { + return AdditionalEducationSearchResource::collection( + AdditionalEducation::query() + ->where('is_active', true) + ->orderBy('title', 'desc') + ->get() + ); + } + ); } } diff --git a/app/Http/Controllers/ClientWidgetContactController.php b/app/Http/Controllers/ClientWidgetContactController.php index f0716be..7655a2c 100644 --- a/app/Http/Controllers/ClientWidgetContactController.php +++ b/app/Http/Controllers/ClientWidgetContactController.php @@ -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() + ); + } + ); } } diff --git a/app/Http/Controllers/ClientWidgetEducationalProgramController.php b/app/Http/Controllers/ClientWidgetEducationalProgramController.php index fe96d62..c549b9c 100644 --- a/app/Http/Controllers/ClientWidgetEducationalProgramController.php +++ b/app/Http/Controllers/ClientWidgetEducationalProgramController.php @@ -2,19 +2,28 @@ namespace App\Http\Controllers; +use App\Enums\CacheKeys; use App\Enums\EducationalProgramStatus; use App\Http\Resources\EducationalProgramSearchResource; use App\Models\EducationalProgram; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Cache; class ClientWidgetEducationalProgramController extends Controller { public function index() { - return EducationalProgramSearchResource::collection( - EducationalProgram::query() - ->where('status', EducationalProgramStatus::PUBLISHED) - ->orderBy('name', 'desc') - ->get()); + return Cache::remember( + CacheKeys::EDUCATION_PROGRAMS_PREFIX->value . 'search_list', + now()->addDay(), // Кешируем на 1 день + function () { + return EducationalProgramSearchResource::collection( + EducationalProgram::query() + ->where('status', EducationalProgramStatus::PUBLISHED) + ->orderBy('name', 'desc') + ->get() + ); + } + ); } } diff --git a/app/Http/Controllers/ClientWidgetPageReferenceListController.php b/app/Http/Controllers/ClientWidgetPageReferenceListController.php index cf277c6..422f30c 100644 --- a/app/Http/Controllers/ClientWidgetPageReferenceListController.php +++ b/app/Http/Controllers/ClientWidgetPageReferenceListController.php @@ -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() + ); + } + ); } } diff --git a/app/Http/Controllers/ClientWidgetSliderController.php b/app/Http/Controllers/ClientWidgetSliderController.php index 2dd1d27..d1d6951 100644 --- a/app/Http/Controllers/ClientWidgetSliderController.php +++ b/app/Http/Controllers/ClientWidgetSliderController.php @@ -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; diff --git a/app/Http/Controllers/PageController.php b/app/Http/Controllers/PageController.php index 530efbf..8164e80 100644 --- a/app/Http/Controllers/PageController.php +++ b/app/Http/Controllers/PageController.php @@ -14,6 +14,7 @@ use App\Http\Resources\RegisteredPageResource; use App\Models\MainSection; use App\Models\Page; use App\Models\Post; +use App\Services\App\Seo\SeoPageProvider; use Illuminate\Http\Request; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Http; @@ -22,7 +23,9 @@ use Inertia\Inertia; class PageController extends Controller { - public function render(Request $request, $path) + public function __construct(readonly SeoPageProvider $seoPageProvider){} + + public function render(string $path): \Inertia\Response { // Генерируем уникальный ключ для кеширования $cacheKey = 'page_' . md5($path); @@ -37,45 +40,17 @@ class PageController extends Controller if ($page === null) { abort(404); } + $subSectionPages = $page->section ? PageResource::collection($page->section->pages) : null; - if (isset($page->section)) { - $subSectionPages = PageResource::collection($page->section->pages); - $breadcrumbs = [ - 'mainSection' => new ClientBreadcrumbSection($page->section->mainSection), - 'subSection' => new ClientBreadcrumbSubSection($page->section), - 'page' => new ClientBreadcrumbPage($page), - ]; - } else { - $subSectionPages = null; - $breadcrumbs = null; - } - - $seo = $page->seo ?? null; + $seo = $this->seoPageProvider->getSeoForModel($page); $page = new PageResource($page); - $error = $page->code; if ($page->code != 200) { - abort($error); + abort($page->code); } - return Inertia::render('Page', compact('page', 'subSectionPages', 'breadcrumbs', 'seo')); - } - public function getRegisteredPages() - { - $pages = PageResource::collection(Page::query() - ->when(request()->input('search'), function ($query, $search) { - $query->where('title', 'like', "%{$search}%"); - }) - ->where('is_registered', true) - ->where('is_visible', true) - ->orderBy('id', 'desc') - ->paginate(request()->input('perPage', 9)) - ->withQueryString()); - $filters = [ - 'search' => request()->input('search'), - ]; - return Inertia::render('AdminPanel/Pages/Registered', compact('pages', 'filters')); + return Inertia::render('Page', compact('page', 'subSectionPages', 'seo')); } } diff --git a/app/Http/Controllers/PersonController.php b/app/Http/Controllers/PersonController.php index 1b0146c..93888c8 100644 --- a/app/Http/Controllers/PersonController.php +++ b/app/Http/Controllers/PersonController.php @@ -2,6 +2,7 @@ namespace App\Http\Controllers; +use App\Enums\CacheKeys; use App\Http\Resources\ClientFullInfoPersonResource; use App\Http\Resources\ClientNavigationResource; use App\Http\Resources\MainSectionResource; @@ -10,23 +11,30 @@ use App\Http\Resources\UserResource; use App\Models\MainSection; use App\Models\User; use App\Models\UserDetail; +use App\Services\App\Seo\SeoPageProvider; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Cache; use Inertia\Inertia; class PersonController extends Controller { - public function index() - { - $persons = UserDetailResource::collection(UserDetail::all()); - $filters = [ - 'search' => request()->input('search'), - ]; - return Inertia::render('Client/Persons/Index', compact('persons', 'filters')); - } + public function __construct(readonly SeoPageProvider $seoPageProvider){} public function show(string $slug) { - $person = new ClientFullInfoPersonResource(User::query()->with(['userDetail', 'departments_work.faculty', 'departments_teach.faculty', 'divisions', 'faculties'])->where('slug', $slug)->firstOrFail()); - return Inertia::render('Client/Persons/Show', compact('person')); + $cacheKey = CacheKeys::USER_PREFIX->value . md5($slug); + + [$person, $seo] = Cache::remember($cacheKey, now()->addHours(24), function () use ($slug) { + $person = User::query()->with(['userDetail', 'departments_work.faculty', 'departments_teach.faculty', 'divisions', 'faculties', 'seo']) + ->where('slug', $slug) + ->firstOrFail(); + $seo = $this->seoPageProvider->getSeoForModel($person); + return [ + new ClientFullInfoPersonResource($person), + $seo + ]; + }); + + return Inertia::render('Client/Persons/Show', compact('person', 'seo')); } } diff --git a/app/Http/Controllers/SearchController.php b/app/Http/Controllers/SearchController.php index e376144..861d672 100644 --- a/app/Http/Controllers/SearchController.php +++ b/app/Http/Controllers/SearchController.php @@ -9,6 +9,7 @@ use App\Http\Resources\EventSearchResource; use App\Http\Resources\FacultySearchResource; use App\Http\Resources\PageSearchResource; use App\Http\Resources\PostSearchResource; +use App\Http\Resources\StaticPageSearchResource; use App\Http\Resources\UserSearchResource; use App\Models\AdditionalEducation; use App\Models\EducationalGroup; diff --git a/app/Http/Controllers/StaticSearchController.php b/app/Http/Controllers/StaticSearchController.php new file mode 100644 index 0000000..8848b9c --- /dev/null +++ b/app/Http/Controllers/StaticSearchController.php @@ -0,0 +1,28 @@ +search( + $request->input('search'), + $request->input('page', 1) + ); + } + + public function getCategories() + { + return Cache::remember('page_static_categories', now()->addWeek(), function () { + return app(CategoryFinderService::class)->getCategories(); + }); + } +} + diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 30caad4..6b5c487 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -3,6 +3,7 @@ namespace App\Http; use App\Http\Middleware\AccessCheck; +use App\Http\Middleware\FormTimePeriodMiddleware; use App\Http\Middleware\InternalRequestOnly; use App\Http\Middleware\LimitPost; use App\Http\Middleware\RateLimitCheckMiddleware; @@ -86,6 +87,7 @@ class Kernel extends HttpKernel 'ensure.browser' => InternalRequestOnly::class, 'superadmin' => \App\Http\Middleware\EnsureUserIsSuperadmin::class, 'limit.post' => LimitPost::class, + 'form.time.period' => FormTimePeriodMiddleware::class, ); } diff --git a/app/Http/Middleware/FormTimePeriodMiddleware.php b/app/Http/Middleware/FormTimePeriodMiddleware.php new file mode 100644 index 0000000..ffbaa4d --- /dev/null +++ b/app/Http/Middleware/FormTimePeriodMiddleware.php @@ -0,0 +1,47 @@ +find($request->route('id')); + + if (!$form) { + abort(Response::HTTP_NOT_FOUND, 'Form not found'); + } + + if (!isset($form->settings['period'])) { + abort(Response::HTTP_BAD_REQUEST, 'Invalid form settings'); + } + + $period = $form->settings['period']; + + try { + $start_time = Carbon::parse($period['start_time']); + $end_time = Carbon::parse($period['end_time']); + } catch (\Exception $e) { + abort(Response::HTTP_BAD_REQUEST, 'Invalid time format'); + } + + $now = Carbon::now(); + + if ($now >= $start_time && $now <= $end_time) { + return $next($request); + } + + abort(Response::HTTP_FORBIDDEN, 'Form is not available at this time'); + } +} diff --git a/app/Http/Middleware/HandleInertiaRequests.php b/app/Http/Middleware/HandleInertiaRequests.php index cea890e..b44d531 100644 --- a/app/Http/Middleware/HandleInertiaRequests.php +++ b/app/Http/Middleware/HandleInertiaRequests.php @@ -4,6 +4,7 @@ namespace App\Http\Middleware; use App\Http\Resources\ClientNavigationResource; use App\Models\MainSection; +use App\Services\App\Breadcrumb\BreadcrumbService; use Illuminate\Http\Request; use Illuminate\Support\Facades\Cache; use Inertia\Middleware; @@ -11,28 +12,16 @@ use Tightenco\Ziggy\Ziggy; class HandleInertiaRequests extends Middleware { - /** - * The root template that is loaded on the first page visit. - * - * @var string - */ protected $rootView = 'app'; - /** - * Determine the current asset version. - */ - public function version(Request $request): string|null + public function version(Request $request): ?string { return parent::version($request); } - /** - * Define the props that are shared by default. - * - * @return array - */ public function share(Request $request): array { + // Навигация (кешированная) $navigation = Cache::remember('navigation', now()->addHours(1), function () { return ClientNavigationResource::collection( MainSection::with('subSections.pages.section') @@ -41,6 +30,9 @@ class HandleInertiaRequests extends Middleware ); }); + // Хлебные крошки (автоматически по текущему URL) + $breadcrumbs = app(BreadcrumbService::class)->generateBreadcrumbs(); + return [ ...parent::share($request), 'auth' => [ @@ -51,13 +43,13 @@ class HandleInertiaRequests extends Middleware 'location' => $request->url(), ], 'navigation' => $navigation, - 'urlPrev' => function() { - if (url()->previous() !== '' && url()->previous() !== url()->current()) { + 'breadcrumbs' => $breadcrumbs, // Добавляем хлебные крошки + 'urlPrev' => function () { + if (url()->previous() !== url()->current()) { return url()->previous(); - } else { - return 'empty'; } + return 'empty'; }, ]; } -} +} \ No newline at end of file diff --git a/app/Http/Resources/ClientBreadcrumbSection.php b/app/Http/Resources/ClientBreadcrumbSection.php index 58cb6a6..d9ec1b4 100644 --- a/app/Http/Resources/ClientBreadcrumbSection.php +++ b/app/Http/Resources/ClientBreadcrumbSection.php @@ -15,8 +15,8 @@ class ClientBreadcrumbSection extends JsonResource public function toArray(Request $request): array { return [ - 'title' => $this->title, - 'slug' => $this->slug, + 'title' => $this->title ?? null, + 'slug' => $this->slug ?? null, ]; } } diff --git a/app/Http/Resources/ClientEventFullResource.php b/app/Http/Resources/ClientEventFullResource.php index 5344bd8..680687e 100644 --- a/app/Http/Resources/ClientEventFullResource.php +++ b/app/Http/Resources/ClientEventFullResource.php @@ -26,7 +26,7 @@ class ClientEventFullResource extends JsonResource 'event_time_start' => Carbon::parse($this->event_time_start)->format('H:i'), 'address' => $this->address, 'is_online' => $this->is_online, - 'category' => $this->category->title ?? null, + 'category' => $this->category ?? null, ]; } } diff --git a/app/Http/Resources/ClientEventResource.php b/app/Http/Resources/ClientEventResource.php index fddfd87..ec3178c 100644 --- a/app/Http/Resources/ClientEventResource.php +++ b/app/Http/Resources/ClientEventResource.php @@ -23,7 +23,7 @@ class ClientEventResource extends JsonResource 'event_time_start' => Carbon::parse($this->event_time_start)->format('H:i'), 'address' => $this->address, 'is_online' => $this->is_online, - 'category' => $this->category->title ?? null, + 'category' => $this->category ?? null, ]; } } diff --git a/app/Models/AcademicJournal.php b/app/Models/AcademicJournal.php index 9edc9e4..ec32a9b 100644 --- a/app/Models/AcademicJournal.php +++ b/app/Models/AcademicJournal.php @@ -2,10 +2,11 @@ namespace App\Models; +use App\Services\App\Seo\SeoDescriptionInterface; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; -class AcademicJournal extends Model +class AcademicJournal extends Model implements SeoDescriptionInterface { use HasFactory; @@ -27,4 +28,9 @@ class AcademicJournal extends Model { return $this->morphOne(Seo::class, 'seoable'); } + + public function getSeoDescription(): array + { + return $this->main_info; + } } diff --git a/app/Models/CustomForm.php b/app/Models/CustomForm.php index 5f7c9bc..f37edcb 100644 --- a/app/Models/CustomForm.php +++ b/app/Models/CustomForm.php @@ -16,6 +16,7 @@ class CustomForm extends Model 'columns' => 'array', 'status' => CustomFormStatus::class, 'mail_settings' => 'array', + 'settings' => 'array', ]; public function responses() diff --git a/app/Models/Page.php b/app/Models/Page.php index 8479009..274fbb4 100644 --- a/app/Models/Page.php +++ b/app/Models/Page.php @@ -23,15 +23,6 @@ class Page extends Model return $this->morphOne(Seo::class, 'seoable'); } - public function getBreadcrumbs(string $url) : array - { - $path = ltrim(parse_url(route($url), PHP_URL_PATH), '/'); - - $page = self::where('path', '=', $path)->with('section.pages.section', 'section.mainSection')->first(); - - dd($page); - } - protected $casts = [ 'content' => 'array', 'settings' => 'array', diff --git a/app/Models/Post.php b/app/Models/Post.php index b7e8877..bea16a3 100644 --- a/app/Models/Post.php +++ b/app/Models/Post.php @@ -41,14 +41,14 @@ class Post extends Model return $this->belongsTo(User::class, 'user_id'); } - public function seo() + public function seo(): MorphOne { return $this->morphOne(Seo::class, 'seoable'); } - public function mainSlider() + public function slide(): MorphOne { - return $this->morphOne(MainSlider::class, 'slidable'); + return $this->morphOne(Slide::class, 'slidable'); } protected $casts = [ diff --git a/app/Models/Slide.php b/app/Models/Slide.php index 3d9e6ba..1c23da3 100644 --- a/app/Models/Slide.php +++ b/app/Models/Slide.php @@ -21,4 +21,9 @@ class Slide extends Model { return $this->belongsTo(Slider::class); } + + public function slidable() + { + return $this->morphTo(); + } } diff --git a/app/Observers/AcademicJournalObserver.php b/app/Observers/AcademicJournalObserver.php new file mode 100644 index 0000000..0de345a --- /dev/null +++ b/app/Observers/AcademicJournalObserver.php @@ -0,0 +1,56 @@ +academicJournalCacheService = app(AcademicJournalCacheService::class); + } + + /** + * Handle the AcademicJournal "created" event. + */ + public function created(AcademicJournal $academicJournal): void + { + $this->academicJournalCacheService->clearAllCacheByModel(); + } + + /** + * Handle the AcademicJournal "updated" event. + */ + public function updated(AcademicJournal $academicJournal): void + { + $this->academicJournalCacheService->clearCache($academicJournal); + } + + /** + * Handle the AcademicJournal "deleted" event. + */ + public function deleted(AcademicJournal $academicJournal): void + { + $this->academicJournalCacheService->clearAllCacheByModel(); + } + + /** + * Handle the AcademicJournal "restored" event. + */ + public function restored(AcademicJournal $academicJournal): void + { + // + } + + /** + * Handle the AcademicJournal "force deleted" event. + */ + public function forceDeleted(AcademicJournal $academicJournal): void + { + // + } +} \ No newline at end of file diff --git a/app/Observers/AdditionalEducationObserver.php b/app/Observers/AdditionalEducationObserver.php new file mode 100644 index 0000000..d2d8bf7 --- /dev/null +++ b/app/Observers/AdditionalEducationObserver.php @@ -0,0 +1,56 @@ +additionalEducationCacheService = app(AdditionalEducationCacheService::class); + } + + /** + * Handle the AdditionalEducation "created" event. + */ + public function created(AdditionalEducation $additionalEducation): void + { + $this->additionalEducationCacheService->clearAllCacheByModel(); + } + + /** + * Handle the AdditionalEducation "updated" event. + */ + public function updated(AdditionalEducation $additionalEducation): void + { + $this->additionalEducationCacheService->clearCache($additionalEducation); + } + + /** + * Handle the AdditionalEducation "deleted" event. + */ + public function deleted(AdditionalEducation $additionalEducation): void + { + $this->additionalEducationCacheService->clearAllCacheByModel(); + } + + /** + * Handle the AdditionalEducation "restored" event. + */ + public function restored(AdditionalEducation $additionalEducation): void + { + $this->additionalEducationCacheService->clearAllCacheByModel(); + } + + /** + * Handle the AdditionalEducation "force deleted" event. + */ + public function forceDeleted(AdditionalEducation $additionalEducation): void + { + $this->additionalEducationCacheService->clearAllCacheByModel(); + } +} \ No newline at end of file diff --git a/app/Observers/ContactWidgetObserver.php b/app/Observers/ContactWidgetObserver.php new file mode 100644 index 0000000..8a11dfd --- /dev/null +++ b/app/Observers/ContactWidgetObserver.php @@ -0,0 +1,59 @@ +contactWidgetCacheService = app(ContactWidgetCacheService::class); + } + + /** + * Handle the ContactWidget "created" event. + */ + public function created(ContactWidget $contactWidget): void + { + $this->contactWidgetCacheService->clearAllCacheByModel(); + } + + /** + * Handle the ContactWidget "updated" event. + */ + public function updated(ContactWidget $contactWidget): void + { + $this->contactWidgetCacheService->clearCache($contactWidget); + $this->contactWidgetCacheService->clearAllCacheByModel(); + } + + /** + * Handle the ContactWidget "deleted" event. + */ + public function deleted(ContactWidget $contactWidget): void + { + $this->contactWidgetCacheService->clearCache($contactWidget); + $this->contactWidgetCacheService->clearAllCacheByModel(); + } + + /** + * Handle the ContactWidget "restored" event. + */ + public function restored(ContactWidget $contactWidget): void + { + $this->contactWidgetCacheService->clearAllCacheByModel(); + } + + /** + * Handle the ContactWidget "force deleted" event. + */ + public function forceDeleted(ContactWidget $contactWidget): void + { + $this->contactWidgetCacheService->clearCache($contactWidget); + $this->contactWidgetCacheService->clearAllCacheByModel(); + } +} \ No newline at end of file diff --git a/app/Observers/DepartmentObserver.php b/app/Observers/DepartmentObserver.php new file mode 100644 index 0000000..eafff36 --- /dev/null +++ b/app/Observers/DepartmentObserver.php @@ -0,0 +1,59 @@ +departmentCacheService = app(DepartmentCacheService::class); + } + + /** + * Handle the Department "created" event. + */ + public function created(Department $department): void + { + $this->departmentCacheService->clearAllCacheByModel(); + } + + /** + * Handle the Department "updated" event. + */ + public function updated(Department $department): void + { + $this->departmentCacheService->clearCache($department); + $this->departmentCacheService->clearAllCacheByModel(); // Очищаем и общий кэш, если есть списки + } + + /** + * Handle the Department "deleted" event. + */ + public function deleted(Department $department): void + { + $this->departmentCacheService->clearCache($department); + $this->departmentCacheService->clearAllCacheByModel(); + } + + /** + * Handle the Department "restored" event. + */ + public function restored(Department $department): void + { + $this->departmentCacheService->clearAllCacheByModel(); + } + + /** + * Handle the Department "force deleted" event. + */ + public function forceDeleted(Department $department): void + { + $this->departmentCacheService->clearCache($department); + $this->departmentCacheService->clearAllCacheByModel(); + } +} \ No newline at end of file diff --git a/app/Observers/DivisionObserver.php b/app/Observers/DivisionObserver.php new file mode 100644 index 0000000..7ae030f --- /dev/null +++ b/app/Observers/DivisionObserver.php @@ -0,0 +1,59 @@ +divisionCacheService = app(DivisionCacheService::class); + } + + /** + * Handle the Division "created" event. + */ + public function created(Division $division): void + { + $this->divisionCacheService->clearAllCacheByModel(); + } + + /** + * Handle the Division "updated" event. + */ + public function updated(Division $division): void + { + $this->divisionCacheService->clearCache($division); + $this->divisionCacheService->clearAllCacheByModel(); + } + + /** + * Handle the Division "deleted" event. + */ + public function deleted(Division $division): void + { + $this->divisionCacheService->clearCache($division); + $this->divisionCacheService->clearAllCacheByModel(); + } + + /** + * Handle the Division "restored" event. + */ + public function restored(Division $division): void + { + $this->divisionCacheService->clearAllCacheByModel(); + } + + /** + * Handle the Division "force deleted" event. + */ + public function forceDeleted(Division $division): void + { + $this->divisionCacheService->clearCache($division); + $this->divisionCacheService->clearAllCacheByModel(); + } +} \ No newline at end of file diff --git a/app/Observers/EducationalProgramObserver.php b/app/Observers/EducationalProgramObserver.php new file mode 100644 index 0000000..38ce337 --- /dev/null +++ b/app/Observers/EducationalProgramObserver.php @@ -0,0 +1,59 @@ +educationalProgramCacheService = app(EducationalProgramCacheService::class); + } + + /** + * Handle the EducationalProgram "created" event. + */ + public function created(EducationalProgram $educationalProgram): void + { + $this->educationalProgramCacheService->clearAllCacheByModel(); + } + + /** + * Handle the EducationalProgram "updated" event. + */ + public function updated(EducationalProgram $educationalProgram): void + { + $this->educationalProgramCacheService->clearCache($educationalProgram); + $this->educationalProgramCacheService->clearAllCacheByModel(); + } + + /** + * Handle the EducationalProgram "deleted" event. + */ + public function deleted(EducationalProgram $educationalProgram): void + { + $this->educationalProgramCacheService->clearCache($educationalProgram); + $this->educationalProgramCacheService->clearAllCacheByModel(); + } + + /** + * Handle the EducationalProgram "restored" event. + */ + public function restored(EducationalProgram $educationalProgram): void + { + $this->educationalProgramCacheService->clearAllCacheByModel(); + } + + /** + * Handle the EducationalProgram "force deleted" event. + */ + public function forceDeleted(EducationalProgram $educationalProgram): void + { + $this->educationalProgramCacheService->clearCache($educationalProgram); + $this->educationalProgramCacheService->clearAllCacheByModel(); + } +} \ No newline at end of file diff --git a/app/Observers/EventObserver.php b/app/Observers/EventObserver.php new file mode 100644 index 0000000..cefc23c --- /dev/null +++ b/app/Observers/EventObserver.php @@ -0,0 +1,59 @@ +eventCacheService = app(EventCacheService::class); + } + + /** + * Handle the Event "created" event. + */ + public function created(Event $event): void + { + $this->eventCacheService->clearAllCacheByModel(); + } + + /** + * Handle the Event "updated" event. + */ + public function updated(Event $event): void + { + $this->eventCacheService->clearCache($event); + $this->eventCacheService->clearAllCacheByModel(); + } + + /** + * Handle the Event "deleted" event. + */ + public function deleted(Event $event): void + { + $this->eventCacheService->clearCache($event); + $this->eventCacheService->clearAllCacheByModel(); + } + + /** + * Handle the Event "restored" event. + */ + public function restored(Event $event): void + { + $this->eventCacheService->clearAllCacheByModel(); + } + + /** + * Handle the Event "force deleted" event. + */ + public function forceDeleted(Event $event): void + { + $this->eventCacheService->clearCache($event); + $this->eventCacheService->clearAllCacheByModel(); + } +} \ No newline at end of file diff --git a/app/Observers/FacultyObserver.php b/app/Observers/FacultyObserver.php new file mode 100644 index 0000000..2b459ef --- /dev/null +++ b/app/Observers/FacultyObserver.php @@ -0,0 +1,59 @@ +facultyCacheService = app(FacultyCacheService::class); + } + + /** + * Handle the Faculty "created" event. + */ + public function created(Faculty $faculty): void + { + $this->facultyCacheService->clearAllCacheByModel(); + } + + /** + * Handle the Faculty "updated" event. + */ + public function updated(Faculty $faculty): void + { + $this->facultyCacheService->clearCache($faculty); + $this->facultyCacheService->clearAllCacheByModel(); + } + + /** + * Handle the Faculty "deleted" event. + */ + public function deleted(Faculty $faculty): void + { + $this->facultyCacheService->clearCache($faculty); + $this->facultyCacheService->clearAllCacheByModel(); + } + + /** + * Handle the Faculty "restored" event. + */ + public function restored(Faculty $faculty): void + { + $this->facultyCacheService->clearAllCacheByModel(); + } + + /** + * Handle the Faculty "force deleted" event. + */ + public function forceDeleted(Faculty $faculty): void + { + $this->facultyCacheService->clearCache($faculty); + $this->facultyCacheService->clearAllCacheByModel(); + } +} \ No newline at end of file diff --git a/app/Observers/PageObserver.php b/app/Observers/PageObserver.php index c074945..aff5546 100644 --- a/app/Observers/PageObserver.php +++ b/app/Observers/PageObserver.php @@ -26,7 +26,7 @@ class PageObserver /** * Handle the Page "updated" event. */ - public function updated(Page $page) + public function updated(Page $page): void { $this->pageCacheService->clearCache($page); } @@ -34,7 +34,7 @@ class PageObserver /** * Handle the Page "deleted" event. */ - public function deleted(Page $page) + public function deleted(Page $page): void { $this->pageCacheService->clearAllCacheByModel(); } diff --git a/app/Observers/PageReferenceListObserver.php b/app/Observers/PageReferenceListObserver.php new file mode 100644 index 0000000..4527f06 --- /dev/null +++ b/app/Observers/PageReferenceListObserver.php @@ -0,0 +1,59 @@ +pageReferenceListCacheService = app(PageReferenceListCacheService::class); + } + + /** + * Handle the PageReferenceList "created" event. + */ + public function created(PageReferenceList $pageReferenceList): void + { + $this->pageReferenceListCacheService->clearAllCacheByModel(); + } + + /** + * Handle the PageReferenceList "updated" event. + */ + public function updated(PageReferenceList $pageReferenceList): void + { + $this->pageReferenceListCacheService->clearCache($pageReferenceList); + $this->pageReferenceListCacheService->clearAllCacheByModel(); + } + + /** + * Handle the PageReferenceList "deleted" event. + */ + public function deleted(PageReferenceList $pageReferenceList): void + { + $this->pageReferenceListCacheService->clearCache($pageReferenceList); + $this->pageReferenceListCacheService->clearAllCacheByModel(); + } + + /** + * Handle the PageReferenceList "restored" event. + */ + public function restored(PageReferenceList $pageReferenceList): void + { + $this->pageReferenceListCacheService->clearAllCacheByModel(); + } + + /** + * Handle the PageReferenceList "force deleted" event. + */ + public function forceDeleted(PageReferenceList $pageReferenceList): void + { + $this->pageReferenceListCacheService->clearCache($pageReferenceList); + $this->pageReferenceListCacheService->clearAllCacheByModel(); + } +} \ No newline at end of file diff --git a/app/Observers/ScheduleObserver.php b/app/Observers/ScheduleObserver.php new file mode 100644 index 0000000..e279498 --- /dev/null +++ b/app/Observers/ScheduleObserver.php @@ -0,0 +1,59 @@ +scheduleCacheService = app(ScheduleCacheService::class); + } + + /** + * Handle the Schedule "created" event. + */ + public function created(Schedule $schedule): void + { + $this->scheduleCacheService->clearAllCacheByModel(); + } + + /** + * Handle the Schedule "updated" event. + */ + public function updated(Schedule $schedule): void + { + $this->scheduleCacheService->clearCache($schedule); + $this->scheduleCacheService->clearAllCacheByModel(); + } + + /** + * Handle the Schedule "deleted" event. + */ + public function deleted(Schedule $schedule): void + { + $this->scheduleCacheService->clearCache($schedule); + $this->scheduleCacheService->clearAllCacheByModel(); + } + + /** + * Handle the Schedule "restored" event. + */ + public function restored(Schedule $schedule): void + { + $this->scheduleCacheService->clearAllCacheByModel(); + } + + /** + * Handle the Schedule "force deleted" event. + */ + public function forceDeleted(Schedule $schedule): void + { + $this->scheduleCacheService->clearCache($schedule); + $this->scheduleCacheService->clearAllCacheByModel(); + } +} \ No newline at end of file diff --git a/app/Observers/SlideObserver.php b/app/Observers/SlideObserver.php new file mode 100644 index 0000000..256dea8 --- /dev/null +++ b/app/Observers/SlideObserver.php @@ -0,0 +1,77 @@ +cacheService = new SliderCacheService(); + } + /** + * Handle the MainSlider "created" event. + */ + public function created(Slide $slide): void + { + // Устанавливаем сортировку для новой записи + $slide->sort = 1; + $slide->save(); + + // Обновляем сортировку для всех остальных записей + $this->updateSortOrder($slide->id, $slide->slider_id); + + $this->cacheService->clearAllCacheByModel(); + } + + /** + * Handle the MainSlider "updated" event. + */ + public function updated(Slide $slide): void + { + $this->cacheService->clearAllCacheByModel(); + } + + /** + * Handle the MainSlider "deleted" event. + */ + public function deleted(Slide $slide): void + { + $this->cacheService->clearAllCacheByModel(); + } + + /** + * Handle the MainSlider "restored" event. + */ + public function restored(Slide $slide): void + { + // + } + + /** + * Handle the MainSlider "force deleted" event. + */ + public function forceDeleted(Slide $slide): void + { + // + } + + protected function updateSortOrder($id, $slider_id): void + { + // Получаем все записи, отсортированные по текущему значению sort + $slides = Slide::orderBy('sort', 'asc')->where([['slider_id', $slider_id], ['id', '!=', $id]])->get(); + + if ($slides->count() > 0) { + foreach ($slides as $index => $slide) { + $slide->sort = $index + 2; // Начинаем с 1 + $slide->save(); + } + } + } +} diff --git a/app/Observers/UserObserver.php b/app/Observers/UserObserver.php new file mode 100644 index 0000000..0f564a4 --- /dev/null +++ b/app/Observers/UserObserver.php @@ -0,0 +1,59 @@ +userCacheService = app(UserCacheService::class); + } + + /** + * Handle the User "created" event. + */ + public function created(User $user): void + { + $this->userCacheService->clearAllCacheByModel(); + } + + /** + * Handle the User "updated" event. + */ + public function updated(User $user): void + { + $this->userCacheService->clearCache($user); + $this->userCacheService->clearAllCacheByModel(); + } + + /** + * Handle the User "deleted" event. + */ + public function deleted(User $user): void + { + $this->userCacheService->clearCache($user); + $this->userCacheService->clearAllCacheByModel(); + } + + /** + * Handle the User "restored" event. + */ + public function restored(User $user): void + { + $this->userCacheService->clearAllCacheByModel(); + } + + /** + * Handle the User "force deleted" event. + */ + public function forceDeleted(User $user): void + { + $this->userCacheService->clearCache($user); + $this->userCacheService->clearAllCacheByModel(); + } +} \ No newline at end of file diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 986a946..5e56104 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -2,17 +2,42 @@ namespace App\Providers; +use App\Models\AcademicJournal; +use App\Models\AdditionalEducation; +use App\Models\ContactWidget; +use App\Models\Department; +use App\Models\Division; +use App\Models\EducationalProgram; +use App\Models\Event; +use App\Models\Faculty; use App\Models\MainSection; use App\Models\MainSlider; use App\Models\Page; +use App\Models\PageReferenceList; use App\Models\Post; +use App\Models\Schedule; +use App\Models\Slide; use App\Models\SubSection; +use App\Models\User; +use App\Observers\AcademicJournalObserver; +use App\Observers\AdditionalEducationObserver; +use App\Observers\ContactWidgetObserver; +use App\Observers\DepartmentObserver; +use App\Observers\DivisionObserver; +use App\Observers\EducationalProgramObserver; +use App\Observers\EventObserver; +use App\Observers\FacultyObserver; use App\Observers\MainSectionObserver; use App\Observers\MainSliderObserver; use App\Observers\PageObserver; +use App\Observers\PageReferenceListObserver; use App\Observers\PostObserver; +use App\Observers\ScheduleObserver; +use App\Observers\SlideObserver; use App\Observers\SubSectionObserver; +use App\Observers\UserObserver; use App\Services\App\Cache\MainSliderCacheService; +use App\Services\App\Cache\SliderCacheService; use Carbon\Carbon; use Filament\Facades\Filament; use Filament\Support\Facades\FilamentView; @@ -47,7 +72,7 @@ class AppServiceProvider extends ServiceProvider $this->loadViewsFrom(__DIR__.'/path/to/views', 'checkpoint'); FilamentView::registerRenderHook(TablesRenderHook::TOOLBAR_REORDER_TRIGGER_AFTER, function () { - (new MainSliderCacheService())->clearAllCacheByModel(); + (new SliderCacheService())->clearAllCacheByModel(); }); } @@ -56,7 +81,18 @@ class AppServiceProvider extends ServiceProvider Post::observe(PostObserver::class); MainSection::observe(MainSectionObserver::class); SubSection::observe(SubSectionObserver::class); - MainSlider::observe(MainSliderObserver::class); + Slide::observe(SlideObserver::class); + AcademicJournal::observe(AcademicJournalObserver::class); + AdditionalEducation::observe(AdditionalEducationObserver::class); + Department::observe(DepartmentObserver::class); + Division::observe(DivisionObserver::class); + Event::observe(EventObserver::class); + Faculty::observe(FacultyObserver::class); + User::observe(UserObserver::class); + EducationalProgram::observe(EducationalProgramObserver::class); + Schedule::observe(ScheduleObserver::class); + ContactWidget::observe(ContactWidgetObserver::class); + PageReferenceList::observe(PageReferenceListObserver::class); } private static function setLocaleTime() : void { @@ -65,15 +101,17 @@ class AppServiceProvider extends ServiceProvider } - private static function registerFilamentNavigationGroups() + private static function registerFilamentNavigationGroups(): void { Filament::registerNavigationGroups([ + 'Виджеты', + 'Новости и мероприятия', 'Структура приложения', 'Структура института', 'Образование', 'Расписание и группы', - 'Новости и мероприятия', - 'Библиотека', + 'Наука', + 'Settings', ]); } } diff --git a/app/Services/App/Breadcrumb/BreadcrumbService.php b/app/Services/App/Breadcrumb/BreadcrumbService.php index c7fa523..e499abe 100644 --- a/app/Services/App/Breadcrumb/BreadcrumbService.php +++ b/app/Services/App/Breadcrumb/BreadcrumbService.php @@ -7,34 +7,71 @@ use App\Http\Resources\ClientBreadcrumbSection; use App\Http\Resources\ClientBreadcrumbSubSection; use App\Models\Page; use Illuminate\Support\Facades\Cache; +use Illuminate\Support\Facades\Route; class BreadcrumbService { - public function generateBreadcrumbs($routeName) : array|null + public function generateBreadcrumbs(): ?array { - $path = $this->generatePath($routeName); + $routeName = Route::currentRouteName(); - // Кешируем страницу - $page = Cache::remember('page_' . $path, now()->addHours(1), function () use ($path) { - return 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; + // Пытаемся найти index-версию маршрута + $indexRouteName = $this->getIndexRouteName($routeName); + + if ($indexRouteName === null) { + return null; } - return $breadcrumbs; + + // Используем index-версию, если она существует + $finalRouteName = Route::has($indexRouteName) ? $indexRouteName : $routeName; + + + + $path = $this->generatePath($finalRouteName); + + $page = Cache::remember('page_' . $path, now()->addHours(1), function () use ($path) { + return Page::where('path', $path) + ->with('section.pages.section', 'section.mainSection') + ->first(); + }); + + if (!$page?->section) { + return null; + } + + return [ + 'mainSection' => new ClientBreadcrumbSection($page->section->mainSection), + 'subSection' => new ClientBreadcrumbSubSection($page->section), + 'page' => new ClientBreadcrumbPage($page), + ]; } - private function generatePath($routeName) : string + private function generatePath(string $routeName): string { + if ($routeName === 'page.view') { + return request()->path(); + } $routeUrl = route($routeName); + return ltrim(parse_url($routeUrl, PHP_URL_PATH), '/'); } + + private function getIndexRouteName(string $routeName = null): string|null + { + if ($routeName === null) { + return null; + } + $parts = explode('.', $routeName); + + // Если в маршруте нет точек или он уже заканчивается на index + if (count($parts) <= 1 || end($parts) === 'index') { + return $routeName; + } + + // Заменяем последнюю часть на index + $parts[count($parts) - 1] = 'index'; + return implode('.', $parts); + } } \ No newline at end of file diff --git a/app/Services/App/Cache/AcademicJournalCacheService.php b/app/Services/App/Cache/AcademicJournalCacheService.php new file mode 100644 index 0000000..c6037bc --- /dev/null +++ b/app/Services/App/Cache/AcademicJournalCacheService.php @@ -0,0 +1,37 @@ +clearAllCacheByModel(); + } + } + + public function clearAllCacheByModel(): void + { + $this->clearCacheByPrefix(CacheKeys::ACADEMIC_JOURNAL_PREFIX->value.'*'); + $this->clearCacheByPrefix(CacheKeys::ACADEMIC_JOURNALS_PREFIX->value.'*'); + } + + public function getCachedData(string $key) + { + return Cache::get($key); + } + + public function cacheData(string $key, $data, int $ttl = null): void + { + Cache::put($key, $data, $ttl ?? self::DEFAULT_TTL); + } + + +} \ No newline at end of file diff --git a/app/Services/App/Cache/AdditionalEducationCacheService.php b/app/Services/App/Cache/AdditionalEducationCacheService.php new file mode 100644 index 0000000..00efabe --- /dev/null +++ b/app/Services/App/Cache/AdditionalEducationCacheService.php @@ -0,0 +1,35 @@ +clearAllCacheByModel(); + } + } + + public function clearAllCacheByModel(): void + { + $this->clearCacheByPrefix(CacheKeys::ADDITIONAL_EDUCATIONAL_PROGRAM_PREFIX->value.'*'); + $this->clearCacheByPrefix(CacheKeys::ADDITIONAL_EDUCATIONAL_PROGRAMS_PREFIX->value.'*'); + } + + public function getCachedData(string $key) + { + return Cache::get($key); + } + + public function cacheData(string $key, $data, int $ttl = null): void + { + Cache::put($key, $data, $ttl ?? self::DEFAULT_TTL); + } +} \ No newline at end of file diff --git a/app/Services/App/Cache/CategoryCacheService.php b/app/Services/App/Cache/CategoryCacheService.php index b7967a2..c3ae770 100644 --- a/app/Services/App/Cache/CategoryCacheService.php +++ b/app/Services/App/Cache/CategoryCacheService.php @@ -2,48 +2,30 @@ namespace App\Services\App\Cache; +use App\Enums\CacheKeys; use Illuminate\Support\Facades\Cache; class CategoryCacheService extends AbstractCacheService implements CacheInterface { - /** - * Очищает кеш, связанный с постом. - * - * @param mixed $entity Пост или связанная сущность - * @return void - */ + private const DEFAULT_TTL = 3600; + public function clearCache($entity): void { + $this->clearAllCacheByModel(); } public function clearAllCacheByModel(): void { - $this->clearCacheByPrefix('categories*'); - $this->clearCacheByPrefix('category_content_*'); + $this->clearCacheByPrefix(CacheKeys::CATEGORIES_PREFIX->value.'*'); } - - /** - * Получает кешированные данные по ключу. - * - * @param string $key Ключ кеша - * @return mixed - */ public function getCachedData(string $key) { return Cache::get($key); } - /** - * Кеширует данные по ключу. - * - * @param string $key Ключ кеша - * @param mixed $data Данные для кеширования - * @param int $ttl Время жизни кеша в секундах - * @return void - */ - public function cacheData(string $key, $data, int $ttl = 3600): void + public function cacheData(string $key, $data, int $ttl = null): void { - Cache::put($key, $data, $ttl); + Cache::put($key, $data, $ttl ?? self::DEFAULT_TTL); } } \ No newline at end of file diff --git a/app/Services/App/Cache/ContactWidgetCacheService.php b/app/Services/App/Cache/ContactWidgetCacheService.php new file mode 100644 index 0000000..422d17a --- /dev/null +++ b/app/Services/App/Cache/ContactWidgetCacheService.php @@ -0,0 +1,35 @@ +clearAllCacheByModel(); + } + } + + public function clearAllCacheByModel(): void + { + $this->clearCacheByPrefix(CacheKeys::CONTACT_WIDGET_PREFIX->value.'*'); + $this->clearCacheByPrefix(CacheKeys::CONTACT_WIDGETS_PREFIX->value.'*'); + } + + public function getCachedData(string $key) + { + return Cache::get($key); + } + + public function cacheData(string $key, $data, int $ttl = null): void + { + Cache::put($key, $data, $ttl ?? self::DEFAULT_TTL); + } +} \ No newline at end of file diff --git a/app/Services/App/Cache/DepartmentCacheService.php b/app/Services/App/Cache/DepartmentCacheService.php new file mode 100644 index 0000000..3bd641d --- /dev/null +++ b/app/Services/App/Cache/DepartmentCacheService.php @@ -0,0 +1,36 @@ +clearAllCacheByModel(); + } + } + + public function clearAllCacheByModel(): void + { + $this->clearCacheByPrefix(CacheKeys::DEPARTMENT_PREFIX->value.'*'); + $this->clearCacheByPrefix(CacheKeys::DEPARTMENTS_PREFIX->value.'*'); + } + + public function getCachedData(string $key) + { + return Cache::get($key); + } + + public function cacheData(string $key, $data, int $ttl = null): void + { + Cache::put($key, $data, $ttl ?? self::DEFAULT_TTL); + } + +} \ No newline at end of file diff --git a/app/Services/App/Cache/DivisionCacheService.php b/app/Services/App/Cache/DivisionCacheService.php new file mode 100644 index 0000000..3f57661 --- /dev/null +++ b/app/Services/App/Cache/DivisionCacheService.php @@ -0,0 +1,56 @@ +clearAllCacheByModel(); + } + } + + public function clearAllCacheByModel(): void + { + $this->clearCacheByPrefix(self::CACHE_PREFIX.'*'); + $this->clearAllDivisionsCache(); + } + + public function getCachedData(string $key) + { + return Cache::get($key); + } + + public function cacheData(string $key, $data, int $ttl = null): void + { + Cache::put($key, $data, $ttl ?? self::DEFAULT_TTL); + } + + public function getCacheKey(int $id): string + { + return self::CACHE_PREFIX . $id; + } + + public function getAllCacheKey(): string + { + return self::ALL_CACHE_KEY; + } + + private function forgetDivisionCache(int $id): void + { + Cache::forget($this->getCacheKey($id)); + } + + private function clearAllDivisionsCache(): void + { + Cache::forget(self::ALL_CACHE_KEY); + } +} \ No newline at end of file diff --git a/app/Services/App/Cache/EducationalProgramCacheService.php b/app/Services/App/Cache/EducationalProgramCacheService.php new file mode 100644 index 0000000..79590d8 --- /dev/null +++ b/app/Services/App/Cache/EducationalProgramCacheService.php @@ -0,0 +1,52 @@ +clearAllCacheByModel(); + + } + } + + public function clearAllCacheByModel(): void + { + $this->clearCacheByPrefix(CacheKeys::EDUCATION_PROGRAMS_PREFIX->value.'*'); + $this->clearCacheByPrefix(CacheKeys::EDUCATION_PROGRAM_PREFIX->value.'*'); + + } + + public function getCachedData(string $key) + { + return Cache::get($key); + } + + public function cacheData(string $key, $data, int $ttl = null): void + { + Cache::put($key, $data, $ttl ?? self::DEFAULT_TTL); + } + + public function getCacheKey(int $id): string + { + return CacheKeys::EDUCATION_PROGRAM_PREFIX->value . $id; + } + + private function forgetProgramCache(int $id): void + { + Cache::forget($this->getCacheKey($id)); + } + + private function clearAllProgramsCache(): void + { + Cache::forget(CacheKeys::EDUCATION_PROGRAMS_PREFIX->value . '*'); + } +} \ No newline at end of file diff --git a/app/Services/App/Cache/EventCacheService.php b/app/Services/App/Cache/EventCacheService.php new file mode 100644 index 0000000..319a106 --- /dev/null +++ b/app/Services/App/Cache/EventCacheService.php @@ -0,0 +1,42 @@ +clearAllCacheByModel(); + + } + } + + public function clearAllCacheByModel(): void + { + $this->clearCacheByPrefix(CacheKeys::EVENT_PREFIX->value.'*'); + $this->clearAllEventsCache(); + } + + public function getCachedData(string $key) + { + return Cache::get($key); + } + + public function cacheData(string $key, $data, int $ttl = null): void + { + Cache::put($key, $data, $ttl ?? self::DEFAULT_TTL); + } + + + private function clearAllEventsCache(): void + { + Cache::forget(CacheKeys::EVENTS_PREFIX->value.'*'); + } +} \ No newline at end of file diff --git a/app/Services/App/Cache/FacultyCacheService.php b/app/Services/App/Cache/FacultyCacheService.php new file mode 100644 index 0000000..bd0011b --- /dev/null +++ b/app/Services/App/Cache/FacultyCacheService.php @@ -0,0 +1,42 @@ +clearAllCacheByModel(); + + } + } + + public function clearAllCacheByModel(): void + { + $this->clearCacheByPrefix(CacheKeys::FACULTY_PREFIX->value.'*'); + $this->clearAllFacultiesCache(); + } + + public function getCachedData(string $key) + { + return Cache::get($key); + } + + public function cacheData(string $key, $data, int $ttl = null): void + { + Cache::put($key, $data, $ttl ?? self::DEFAULT_TTL); + } + + + private function clearAllFacultiesCache(): void + { + Cache::forget(CacheKeys::FACULTIES_PREFIX->value.'*'); + } +} \ No newline at end of file diff --git a/app/Services/App/Cache/MainSectionCacheService.php b/app/Services/App/Cache/MainSectionCacheService.php index e48fe8f..ec74375 100644 --- a/app/Services/App/Cache/MainSectionCacheService.php +++ b/app/Services/App/Cache/MainSectionCacheService.php @@ -2,43 +2,36 @@ namespace App\Services\App\Cache; +use App\Enums\CacheKeys; use Illuminate\Support\Facades\Cache; class MainSectionCacheService extends AbstractCacheService implements CacheInterface { - /** - * Очищает кеш, связанный с постом. - * - * @param mixed $entity Пост или связанная сущность - * @return void - */ + private const DEFAULT_TTL = 3600; + public function clearCache($entity): void { - $this->clearCacheByPrefix('posts_'); + $this->clearNavigationCache(); } + public function clearAllCacheByModel(): void + { + $this->clearNavigationCache(); + } - /** - * Получает кешированные данные по ключу. - * - * @param string $key Ключ кеша - * @return mixed - */ public function getCachedData(string $key) { return Cache::get($key); } - /** - * Кеширует данные по ключу. - * - * @param string $key Ключ кеша - * @param mixed $data Данные для кеширования - * @param int $ttl Время жизни кеша в секундах - * @return void - */ - public function cacheData(string $key, $data, int $ttl = 3600): void + public function cacheData(string $key, $data, int $ttl = null): void { - Cache::put($key, $data, $ttl); + Cache::put($key, $data, $ttl ?? self::DEFAULT_TTL); + } + + + private function clearNavigationCache(): void + { + Cache::forget(CacheKeys::NAVIGATION_PREFIX->value); } } \ No newline at end of file diff --git a/app/Services/App/Cache/MainSliderCacheService.php b/app/Services/App/Cache/MainSliderCacheService.php deleted file mode 100644 index 8e9087c..0000000 --- a/app/Services/App/Cache/MainSliderCacheService.php +++ /dev/null @@ -1,48 +0,0 @@ -clearCacheByPrefix('active_sliders*'); - } - - - /** - * Получает кешированные данные по ключу. - * - * @param string $key Ключ кеша - * @return mixed - */ - public function getCachedData(string $key) - { - return Cache::get($key); - } - - /** - * Кеширует данные по ключу. - * - * @param string $key Ключ кеша - * @param mixed $data Данные для кеширования - * @param int $ttl Время жизни кеша в секундах - * @return void - */ - public function cacheData(string $key, $data, int $ttl = 3600): void - { - Cache::put($key, $data, $ttl); - } -} \ No newline at end of file diff --git a/app/Services/App/Cache/PageCacheService.php b/app/Services/App/Cache/PageCacheService.php index d916b9d..b2c0936 100644 --- a/app/Services/App/Cache/PageCacheService.php +++ b/app/Services/App/Cache/PageCacheService.php @@ -2,62 +2,42 @@ namespace App\Services\App\Cache; +use App\Enums\CacheKeys; use App\Models\Page; use Illuminate\Support\Facades\Cache; -use Illuminate\Support\Facades\Redis; class PageCacheService extends AbstractCacheService implements CacheInterface { - /** - * Очищает кеш, связанный с постом. - * - * @param mixed $entity Пост или связанная сущность - * @return void - */ + private const DEFAULT_TTL = 3600; + public function clearCache($entity): void { - $cacheKeyByPath = md5($entity->path); - $cacheKeyById = md5($entity->id); - - - - Cache::forget('page_' . $cacheKeyByPath); - Cache::forget('page_' . $cacheKeyById); - - Cache::forget('navigation'); - + if ($entity instanceof Page) { + $this->clearAllCacheByModel(); + $this->clearNavigationCache(); + } } public function clearAllCacheByModel(): void { - $this->clearCacheByPrefix('page_*'); - $this->clearCacheByPrefix('page_data_*'); - Cache::forget('navigation'); - + $this->clearCacheByPrefix(CacheKeys::PAGE_PREFIX->value.'*'); + $this->clearCacheByPrefix(CacheKeys::PAGE_DATA_PREFIX->value.'*'); + $this->clearNavigationCache(); } - - /** - * Получает кешированные данные по ключу. - * - * @param string $key Ключ кеша - * @return mixed - */ public function getCachedData(string $key) { return Cache::get($key); } - /** - * Кеширует данные по ключу. - * - * @param string $key Ключ кеша - * @param mixed $data Данные для кеширования - * @param int $ttl Время жизни кеша в секундах - * @return void - */ - public function cacheData(string $key, $data, int $ttl = 3600): void + public function cacheData(string $key, $data, int $ttl = null): void { - Cache::put($key, $data, $ttl); + Cache::put($key, $data, $ttl ?? self::DEFAULT_TTL); + } + + + private function clearNavigationCache(): void + { + Cache::forget(CacheKeys::NAVIGATION_PREFIX->value); } } \ No newline at end of file diff --git a/app/Services/App/Cache/PageReferenceListCacheService.php b/app/Services/App/Cache/PageReferenceListCacheService.php new file mode 100644 index 0000000..b58a0cc --- /dev/null +++ b/app/Services/App/Cache/PageReferenceListCacheService.php @@ -0,0 +1,42 @@ +clearAllCacheByModel(); + + } + } + + public function clearAllCacheByModel(): void + { + $this->clearCacheByPrefix(CacheKeys::PAGE_REFERENCE_LIST_PREFIX->value.'*'); + $this->clearAllReferencesCache(); + } + + public function getCachedData(string $key) + { + return Cache::get($key); + } + + public function cacheData(string $key, $data, int $ttl = null): void + { + Cache::put($key, $data, $ttl ?? self::DEFAULT_TTL); + } + + + private function clearAllReferencesCache(): void + { + Cache::forget(CacheKeys::PAGE_REFERENCE_LISTS_PREFIX->value.'*'); + } +} \ No newline at end of file diff --git a/app/Services/App/Cache/PostCacheService.php b/app/Services/App/Cache/PostCacheService.php index abb2dbb..539906a 100644 --- a/app/Services/App/Cache/PostCacheService.php +++ b/app/Services/App/Cache/PostCacheService.php @@ -2,59 +2,56 @@ namespace App\Services\App\Cache; +use App\Enums\CacheKeys; use Illuminate\Support\Facades\Cache; -use Illuminate\Support\Facades\Redis; class PostCacheService extends AbstractCacheService implements CacheInterface { + private const DEFAULT_TTL = 3600; + /** - * Очищает кеш, связанный с постом. - * - * @param mixed $entity Пост или связанная сущность - * @return void + * Clear cache for specific post */ public function clearCache($entity): void { - $cacheKeyBySlug = md5($entity->slug); - $cacheKeyById = md5($entity->id); - - Cache::forget('post_' .$cacheKeyBySlug); - Cache::forget('post_' .$cacheKeyById); - - $this->clearCacheByPrefix('recent_posts*'); - - } - - public function clearAllCacheByModel(): void - { - $this->clearCacheByPrefix('post_*'); - $this->clearCacheByPrefix('posts_*'); - $this->clearCacheByPrefix('recent_posts*'); + $this->forgetPostCache($entity->slug, $entity->id); + $this->clearRecentPostsCache(); } /** - * Получает кешированные данные по ключу. - * - * @param string $key Ключ кеша - * @return mixed + * Clear all cache related to posts */ + public function clearAllCacheByModel(): void + { + $this->clearCacheByPrefix(CacheKeys::POST_PREFIX->value.'*'); + $this->clearCacheByPrefix(CacheKeys::POSTS_PREFIX->value.'*'); + $this->clearRecentPostsCache(); + } + public function getCachedData(string $key) { return Cache::get($key); } - /** - * Кеширует данные по ключу. - * - * @param string $key Ключ кеша - * @param mixed $data Данные для кеширования - * @param int $ttl Время жизни кеша в секундах - * @return void - */ - public function cacheData(string $key, $data, int $ttl = 3600): void + public function cacheData(string $key, $data, int $ttl = null): void { - Cache::put($key, $data, $ttl); + Cache::put($key, $data, $ttl ?? self::DEFAULT_TTL); } + /** + * Forget cache for specific post by slug and id + */ + private function forgetPostCache(string $slug, int $id): void + { + Cache::forget(CacheKeys::POST_PREFIX->value.md5($slug)); + Cache::forget(CacheKeys::POST_PREFIX->value.md5($id)); + } + /** + * Clear recent posts cache + */ + private function clearRecentPostsCache(): void + { + $this->clearCacheByPrefix(CacheKeys::RECENT_POSTS_PREFIX->value.'*'); + } } \ No newline at end of file diff --git a/app/Services/App/Cache/ScheduleCacheService.php b/app/Services/App/Cache/ScheduleCacheService.php new file mode 100644 index 0000000..10dc0ca --- /dev/null +++ b/app/Services/App/Cache/ScheduleCacheService.php @@ -0,0 +1,46 @@ +clearAllCacheByModel(); + } + } + + public function clearAllCacheByModel(): void + { + $this->clearCacheByPrefix(CacheKeys::SCHEDULE_PREFIX->value.'*'); + $this->clearAllSchedulesCache(); + } + + public function getCachedData(string $key) + { + return Cache::get($key); + } + + public function cacheData(string $key, $data, int $ttl = null): void + { + Cache::put($key, $data, $ttl ?? self::DEFAULT_TTL); + } + + + private function forgetScheduleCache(int $id): void + { + Cache::forget($this->getCacheKey($id)); + } + + private function clearAllSchedulesCache(): void + { + Cache::forget(CacheKeys::SCHEDULES_PREFIX->value); + } +} \ No newline at end of file diff --git a/app/Services/App/Cache/SliderCacheService.php b/app/Services/App/Cache/SliderCacheService.php new file mode 100644 index 0000000..6200a0a --- /dev/null +++ b/app/Services/App/Cache/SliderCacheService.php @@ -0,0 +1,36 @@ +clearAllCacheByModel(); + } + + public function clearAllCacheByModel(): void + { + $this->clearSliderCache(); + } + + public function getCachedData(string $key) + { + return Cache::get($key); + } + + public function cacheData(string $key, $data, int $ttl = null): void + { + Cache::put($key, $data, $ttl ?? self::DEFAULT_TTL); + } + + private function clearSliderCache(): void + { + $this->clearCacheByPrefix(CacheKeys::SLIDER_PREFIX->value.'*'); + } +} \ No newline at end of file diff --git a/app/Services/App/Cache/SubSectionCacheService.php b/app/Services/App/Cache/SubSectionCacheService.php index 9262e01..3a2fd28 100644 --- a/app/Services/App/Cache/SubSectionCacheService.php +++ b/app/Services/App/Cache/SubSectionCacheService.php @@ -2,42 +2,38 @@ namespace App\Services\App\Cache; +use App\Enums\CacheKeys; use Illuminate\Support\Facades\Cache; class SubSectionCacheService extends AbstractCacheService implements CacheInterface { - /** - * Очищает кеш, связанный с постом. - * - * @param mixed $entity Пост или связанная сущность - * @return void - */ + private const DEFAULT_TTL = 3600; + public function clearCache($entity): void { - $this->clearCacheByPrefix('posts_'); + $this->clearAllCacheByModel(); + } + + public function clearAllCacheByModel(): void + { + $this->clearNavigationCache(); } - /** - * Получает кешированные данные по ключу. - * - * @param string $key Ключ кеша - * @return mixed - */ public function getCachedData(string $key) { return Cache::get($key); } - /** - * Кеширует данные по ключу. - * - * @param string $key Ключ кеша - * @param mixed $data Данные для кеширования - * @param int $ttl Время жизни кеша в секундах - * @return void - */ - public function cacheData(string $key, $data, int $ttl = 3600): void + public function cacheData(string $key, $data, int $ttl = null): void { - Cache::put($key, $data, $ttl); + Cache::put($key, $data, $ttl ?? self::DEFAULT_TTL); + } + + + + + private function clearNavigationCache(): void + { + Cache::forget(CacheKeys::NAVIGATION_PREFIX->value); } } \ No newline at end of file diff --git a/app/Services/App/Cache/TagCacheService.php b/app/Services/App/Cache/TagCacheService.php index 4cdc694..0fc97c1 100644 --- a/app/Services/App/Cache/TagCacheService.php +++ b/app/Services/App/Cache/TagCacheService.php @@ -2,49 +2,47 @@ namespace App\Services\App\Cache; +use App\Enums\CacheKeys; use Illuminate\Support\Facades\Cache; class TagCacheService extends AbstractCacheService implements CacheInterface { - /** - * Очищает кеш, связанный с постом. - * - * @param mixed $entity Пост или связанная сущность - * @return void - */ + private const DEFAULT_TTL = 3600; + public function clearCache($entity): void { + $this->clearAllCacheByModel(); } public function clearAllCacheByModel(): void { - $this->clearCacheByPrefix('tag_ids*'); - $this->clearCacheByPrefix('tags*'); - $this->clearCacheByPrefix('tag_content_*'); + $this->clearTagIdsCache(); + $this->clearTagsCache(); + $this->clearTagContentCache(); } - - /** - * Получает кешированные данные по ключу. - * - * @param string $key Ключ кеша - * @return mixed - */ public function getCachedData(string $key) { return Cache::get($key); } - /** - * Кеширует данные по ключу. - * - * @param string $key Ключ кеша - * @param mixed $data Данные для кеширования - * @param int $ttl Время жизни кеша в секундах - * @return void - */ - public function cacheData(string $key, $data, int $ttl = 3600): void + public function cacheData(string $key, $data, int $ttl = null): void { - Cache::put($key, $data, $ttl); + Cache::put($key, $data, $ttl ?? self::DEFAULT_TTL); + } + + private function clearTagIdsCache(): void + { + $this->clearCacheByPrefix(CacheKeys::TAG_IDS_PREFIX->value.'*'); + } + + private function clearTagsCache(): void + { + $this->clearCacheByPrefix(CacheKeys::TAGS_PREFIX->value.'*'); + } + + private function clearTagContentCache(): void + { + $this->clearCacheByPrefix(CacheKeys::TAG_CONTENT_PREFIX->value.'*'); } } \ No newline at end of file diff --git a/app/Services/App/Cache/UserCacheService.php b/app/Services/App/Cache/UserCacheService.php new file mode 100644 index 0000000..ae725ae --- /dev/null +++ b/app/Services/App/Cache/UserCacheService.php @@ -0,0 +1,37 @@ +clearAllCacheByModel(); + } + } + + public function clearAllCacheByModel(): void + { + $this->clearCacheByPrefix(CacheKeys::USER_PREFIX->value.'*'); + } + + public function getCachedData(string $key) + { + return Cache::get($key); + } + + public function cacheData(string $key, $data, int $ttl = null): void + { + Cache::put($key, $data, $ttl ?? self::DEFAULT_TTL); + } + + + +} \ No newline at end of file diff --git a/app/Services/App/Seo/SeoDescriptionInterface.php b/app/Services/App/Seo/SeoDescriptionInterface.php new file mode 100644 index 0000000..3426d2b --- /dev/null +++ b/app/Services/App/Seo/SeoDescriptionInterface.php @@ -0,0 +1,8 @@ +seo?->toArray(); + } + public function getSeoForCurrentPage(): ?array + { + $path = $this->getCurrentPath(); + + $page = Cache::remember( + CacheKeys::PAGE_PREFIX->value . $path, + now()->addHours(1), + fn() => Page::where('path', $path)->first() + ); + + return $page->seo?->toArray(); // Предполагается, что у модели Page есть поле `seo` (JSON или массив) + } + + private function getCurrentPath(): string + { + if (Route::currentRouteName() === 'page.view') { + return request()->path(); + } + + $routeUrl = route(Route::currentRouteName()); + return ltrim(parse_url($routeUrl, PHP_URL_PATH), '/'); + } +} \ No newline at end of file diff --git a/app/Services/Filament/Domain/Posts/PostSliderService.php b/app/Services/Filament/Domain/Posts/PostSliderService.php index 81c0945..2f48438 100644 --- a/app/Services/Filament/Domain/Posts/PostSliderService.php +++ b/app/Services/Filament/Domain/Posts/PostSliderService.php @@ -5,6 +5,7 @@ namespace App\Services\Filament\Domain\Posts; use App\Dto\MainSliderDTO; use App\Models\MainSlider; use App\Models\Post; +use App\Models\Slide; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Facades\Log; @@ -23,7 +24,8 @@ class PostSliderService public function create(): void { try { - $this->post->mainSlider()->create([ + $post = Post::find($this->post->id); + $slide = new Slide([ 'title' => $this->dto->title, 'content' => $this->dto->content, 'image' => $this->dto->image, @@ -33,11 +35,13 @@ class PostSliderService 'is_active' => $this->dto->is_active, 'start_time' => $this->dto->start_time, 'end_time' => $this->dto->end_time, + 'slider_id' => $this->dto->slider_id, ]); + $post->slide()->save($slide); - Log::info('MainSlider created successfully', ['postTitle' => $this->post->title]); + Log::info('Slide created successfully', ['postTitle' => $this->post->title]); } catch (\Exception $e) { - Log::error('Failed to create MainSlider', [ + Log::error('Failed to create slide', [ 'postTitle' => $this->post->title, 'error' => $e->getMessage(), ]); @@ -53,31 +57,20 @@ class PostSliderService public function update(): void { try { - // Retrieve the MainSlider instance - $mainSlider = $this->post->mainSlider; + $post = Post::find($this->post->id); + $post->slide()->update([ + 'title' => $this->dto->title, + 'content' => $this->dto->content, + 'image' => $this->dto->image, + 'link' => $this->generatePostLink(), + 'settings' => $this->dto->settings, + 'color_theme' => $this->dto->color_theme, + 'is_active' => $this->dto->is_active, + 'start_time' => $this->dto->start_time, + 'end_time' => $this->dto->end_time, + 'slider_id' => $this->dto->slider_id, + ]); - - if ($mainSlider) { - - // Update properties and save to trigger model events - $mainSlider->fill([ - 'title' => $this->dto->title, - 'content' => $this->dto->content, - 'image' => $this->dto->image, - 'link' => $this->generatePostLink(), - 'settings' => $this->dto->settings, - 'color_theme' => $this->dto->color_theme, - 'is_active' => $this->dto->is_active, - 'start_time' => $this->dto->start_time, - 'end_time' => $this->dto->end_time, - ]); - - $mainSlider->save(); // This will trigger updating and updated observer events - - Log::info('MainSlider updated successfully', ['postTitle' => $this->post->title]); - } else { - Log::warning('MainSlider not found for update', ['postTitle' => $this->post->title]); - } } catch (\Exception $e) { Log::error('Failed to update MainSlider', [ 'postTitle' => $this->post->title, diff --git a/app/Services/Filament/Domain/Seo/SeoGeneratorService.php b/app/Services/Filament/Domain/Seo/SeoGeneratorService.php new file mode 100644 index 0000000..df64657 --- /dev/null +++ b/app/Services/Filament/Domain/Seo/SeoGeneratorService.php @@ -0,0 +1,95 @@ + $this->extractSeoTitle($data), + 'description' => $this->extractSeoDescription($data['content']), + 'image' => $this->extractSeoImage($data), + ]; + } + + /** + * Извлекает SEO-заголовок. + * + * @param array $data + * @return string + */ + private function extractSeoTitle(array $data): string + { + return $data['title']; + } + + /** + * Извлекает SEO-описание. + * + * @param array $content + * @return string + */ + private function extractSeoDescription(array $content): string + { + $rowData = $this->getBlockBySeoActiveState('paragraph', $content); + if ($rowData === null) { + $rowData = $this->getFirstBlockByName('paragraph', $content); + } + + $description = $rowData ? html_entity_decode(strip_tags($rowData['data']['content'])) : ''; + return Str::limit(htmlspecialchars($description, ENT_QUOTES, 'UTF-8'), 160); + } + + /** + * Извлекает SEO-изображение. + * + * @param array $data + * @return string|null + */ + private function extractSeoImage(array $data): ?string + { + return $data['preview'] ?? null; + } + + /** + * Находит первый блок по имени. + * + * @param string $name + * @param array $content + * @return array|null + */ + private function getFirstBlockByName(string $name, array $content): ?array + { + foreach ($content as $block) { + if ($block['type'] === $name) { + return $block; + } + } + return null; + } + + /** + * Находит блок по SEO-активности. + * + * @param string $name + * @param array $content + * @return array|null + */ + private function getBlockBySeoActiveState(string $name, array $content): ?array + { + foreach ($content as $block) { + if ($block['type'] === $name && ($block['data']['seo_active'] ?? false)) { + return $block; + } + } + return null; + } +} \ No newline at end of file diff --git a/app/Services/Filament/Services/BreadcrumbFinderService.php b/app/Services/Filament/Services/BreadcrumbFinderService.php new file mode 100644 index 0000000..582686e --- /dev/null +++ b/app/Services/Filament/Services/BreadcrumbFinderService.php @@ -0,0 +1,37 @@ +getBreadcrumb($html); + } + + private function getBreadcrumb(string $html): ?string + { + $document = new Document($html); + $breadcrumbs = $document->first('ol.breadcrumb')?->find('li') ?? []; + + foreach ($breadcrumbs as $index => $breadcrumb) { + if ($index === 2) { // Индексация с 0 → третий элемент = 2 + return $breadcrumb->text(); + } + } + + return null; + } + + +} \ No newline at end of file diff --git a/app/Services/Filament/Services/CategoryFinderService.php b/app/Services/Filament/Services/CategoryFinderService.php new file mode 100644 index 0000000..94e091d --- /dev/null +++ b/app/Services/Filament/Services/CategoryFinderService.php @@ -0,0 +1,36 @@ +first('ul.dropdown-menu'); + $links = $dropdownMenu->find('a'); + $categories = []; + + foreach ($links as $link) { + $category = trim($link->text()); + $categories[] = $category; + } + + return $categories; + } + + +} \ No newline at end of file diff --git a/app/Services/Filament/Services/StaticFileSearch.php b/app/Services/Filament/Services/StaticFileSearch.php new file mode 100644 index 0000000..58fdb0b --- /dev/null +++ b/app/Services/Filament/Services/StaticFileSearch.php @@ -0,0 +1,198 @@ + null + ]; + } + $page = request()->input('page', 1); + try { + $index = $this->getIndex(); + $results = []; + $normalizedQuery = $this->normalizeText(Str::lower($query)); + + foreach ($index as $filePath => $content) { + if (stripos($content['content'], $normalizedQuery) !== false) { + $relativePath = str_replace(public_path() . '/', '', $filePath); + $results[] = [ + 'file' => $relativePath, + 'content' => $content['title'], + 'category' => trim($content['category']), + ]; + } + } + return $this->paginateResults($results, $page); + } catch (\Exception $e) { + Log::error('Search error: ' . $e->getMessage()); + return [ + 'data' => [], + 'meta' => [ + 'current_page' => 1, + 'total' => 0, + 'per_page' => self::PER_PAGE, + 'last_page' => 1 + ] + ]; + } + } + + protected function paginateResults(array $results, int $page): array + { + $total = count($results); + $lastPage = max(1, ceil($total / self::PER_PAGE)); + $page = max(1, min($page, $lastPage)); + + $offset = ($page - 1) * self::PER_PAGE; + $paginatedResults = array_slice($results, $offset, self::PER_PAGE); + + return [ + 'data' => $paginatedResults, + 'meta' => [ + 'current_page' => $page, + 'total' => $total, + 'per_page' => self::PER_PAGE, + 'last_page' => $lastPage + ] + ]; + } + + protected function getIndex(): array + { + return Cache::remember(self::CACHE_KEY, self::CACHE_TTL, function() { + try { + $index = []; + $directory = public_path(self::FILES_DIR); + + if (!is_dir($directory)) { + Log::error("Directory not found: {$directory}"); + return []; + } + + $iterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($directory, RecursiveDirectoryIterator::SKIP_DOTS), + RecursiveIteratorIterator::SELF_FIRST + ); + + foreach ($iterator as $file) { + if ($file->isFile() && $this->isHtmlFile($file)) { + $content = file_get_contents($file->getPathname()); + if ($content !== false) { + $breadcrumb = app(BreadcrumbFinderService::class)->isSameCategory($content); + $text = $this->normalizeText(strip_tags($content)); + $index[$file->getPathname()] = [ + 'title' => $this->getFirstH1Content($content), + 'content' => $text, + 'category' => $breadcrumb ?? null, + ]; + } + } + } + + return $index; + } catch (\Exception $e) { + Log::error('Index creation error: ' . $e->getMessage()); + return []; + } + }); + } + + protected function isHtmlFile(\SplFileInfo $file): bool + { + $extension = strtolower($file->getExtension()); + return in_array($extension, ['html', 'htm']); + } + + protected function normalizeText(string $text): string + { + $text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8'); + $text = preg_replace('/\s+/u', ' ', $text); + $text = trim($text); + return Str::lower($text); + } + + protected function findMatches(string $content, string $query): array + { + $matches = []; + $offset = 0; + $query = Str::lower($query); + $content = Str::lower($content); + $queryLength = mb_strlen($query, 'UTF-8'); + + while (($offset = mb_strpos($content, $query, $offset, 'UTF-8')) !== false) { + $start = max(0, $offset - 50); + $length = min(150, mb_strlen($content) - $start); + $excerpt = mb_substr($content, $start, $length, 'UTF-8'); + + $matches[] = str_replace( + $query, + '[[HIGHLIGHT]]'.$query.'[[/HIGHLIGHT]]', + $excerpt + ); + + $offset += $queryLength; + } + + return $matches; + } + + public function clearCache(): bool + { + try { + Cache::forget(self::CACHE_KEY); + return true; + } catch (\Exception $e) { + Log::error('Cache clear error: ' . $e->getMessage()); + return false; + } + } + + public function rebuildIndex(): array + { + $this->clearCache(); + return $this->getIndex(); + } + + public function getCacheStatus(): array + { + return [ + 'exists' => Cache::has(self::CACHE_KEY), + 'ttl' => Cache::get(self::CACHE_KEY.'_ttl', null), + 'driver' => config('cache.default'), + 'path' => public_path(self::FILES_DIR), + 'directory_exists' => is_dir(public_path(self::FILES_DIR)) + ]; + } + + public function getFirstH1Content(string $content): ?string + { + try { + if (preg_match('/]*>(.*?)<\/h1>/is', $content, $matches)) { + return $this->normalizeText($matches[1]); + } + + return null; + } catch (\Exception $e) { + Log::error('Failed to get H1: ' . $e->getMessage()); + return null; + } + } +} \ No newline at end of file diff --git a/app/Services/Filament/Traits/SeoGenerate.php b/app/Services/Filament/Traits/SeoGenerate.php new file mode 100644 index 0000000..0abf548 --- /dev/null +++ b/app/Services/Filament/Traits/SeoGenerate.php @@ -0,0 +1,34 @@ +seo()->create($this->generateSeo($record)); + } + + public function updateSeo($record): void + { + if ($record->seo()->exists()) { + $record->seo()->update($this->generateSeo($record)); + } else { + $this->createSeo($record); + } + } + + + private function generateSeo($record) { + return app(SeoGeneratorService::class)->generate([ + 'title' => $record->title, + 'content' => $record instanceof SeoDescriptionInterface + ? $record->getSeoDescription() + : $record->content, + 'preview' => $record->preview, + ]); + } +} \ No newline at end of file diff --git a/composer.json b/composer.json index aa9b20b..233b537 100644 --- a/composer.json +++ b/composer.json @@ -15,6 +15,7 @@ "filament/spatie-laravel-tags-plugin": "^3.2", "guava/filament-icon-picker": "^2.0", "guzzlehttp/guzzle": "^7.8", + "imangazaliev/didom": "^2.0", "inertiajs/inertia-laravel": "^1.3", "intervention/image": "^2.7", "joshembling/image-optimizer": "^1.4", diff --git a/composer.lock b/composer.lock index ec7261b..9bb7235 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "80caf6cd28399609a51515170b117048", + "content-hash": "8dfdeaeee9096f0841adf98508f78eea", "packages": [ { "name": "anourvalar/eloquent-serialize", @@ -2792,6 +2792,58 @@ ], "time": "2023-12-03T19:50:20+00:00" }, + { + "name": "imangazaliev/didom", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/Imangazaliev/DiDOM.git", + "reference": "50fa6595d14f22c0c984efed5c818485cf548136" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Imangazaliev/DiDOM/zipball/50fa6595d14f22c0c984efed5c818485cf548136", + "reference": "50fa6595d14f22c0c984efed5c818485cf548136", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-iconv": "*", + "php": ">=7.2" + }, + "require-dev": { + "phpunit/phpunit": "^8.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "DiDom\\": "src/DiDom/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Imangazaliev Muhammad", + "email": "imangazalievm@gmail.com" + } + ], + "description": "Simple and fast HTML parser", + "homepage": "https://github.com/Imangazaliev/DiDOM", + "keywords": [ + "didom", + "html", + "parser", + "xml" + ], + "support": { + "issues": "https://github.com/Imangazaliev/DiDOM/issues", + "source": "https://github.com/Imangazaliev/DiDOM/tree/2.0.1" + }, + "time": "2023-03-05T03:23:48+00:00" + }, { "name": "inertiajs/inertia-laravel", "version": "v1.3.0", diff --git a/database/migrations/2025_03_23_173349_add_morphs_to_slides_table.php b/database/migrations/2025_03_23_173349_add_morphs_to_slides_table.php new file mode 100644 index 0000000..f977da6 --- /dev/null +++ b/database/migrations/2025_03_23_173349_add_morphs_to_slides_table.php @@ -0,0 +1,29 @@ +unsignedBigInteger('slidable_id')->nullable(); // Поле для идентификатора + $table->string('slidable_type')->nullable(); // Поле для типа модели + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('slides', function (Blueprint $table) { + $table->dropColumn(['slidable_id', 'slidable_type']); + }); + } +}; diff --git a/database/migrations/2025_03_26_214117_add_settings_column_to_custom_forms_table.php b/database/migrations/2025_03_26_214117_add_settings_column_to_custom_forms_table.php new file mode 100644 index 0000000..0e3207a --- /dev/null +++ b/database/migrations/2025_03_26_214117_add_settings_column_to_custom_forms_table.php @@ -0,0 +1,28 @@ +text('settings')->nullable()->after('send_message'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('custom_forms', function (Blueprint $table) { + $table->dropColumn('settings'); + }); + } +}; diff --git a/public/logos/logo_urgpu.png b/public/logos/logo_urgpu.png new file mode 100644 index 0000000000000000000000000000000000000000..56f7fbe7e9dd2e7f48f362bd9eaa3f35775dd860 GIT binary patch literal 267101 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≫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⋘F-d;-BH7=zmu+HA?b0$$+;O)I& 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 n++l-7pG5hY@RBO>3KQj^t - + - + Поиск + - + + + + + Поиск + + + + + @@ -103,6 +117,7 @@ import {Link} from "@inertiajs/vue3"; import BasicIcon from "@/componentss/ui/icons/BasicIcon.vue"; import {helpers} from "@/mixins/Helpers.js"; +import {mapActions} from "vuex"; export default { @@ -122,11 +137,7 @@ export default { Link, }, methods: { - removeCookieBvi() { - if (this.getCookie('bvi_panelActive') === 'true') { - this.deleteCookiesWithPrefix('bvi') - } - } + ...mapActions(['toggleBvi']) }, diff --git a/resources/js/Navbars/MainPageNavbar.vue b/resources/js/Navbars/MainPageNavbar.vue index f246bac..b524d8a 100644 --- a/resources/js/Navbars/MainPageNavbar.vue +++ b/resources/js/Navbars/MainPageNavbar.vue @@ -1,184 +1,157 @@ + \ No newline at end of file diff --git a/resources/js/Navbars/MobileNavbar.vue b/resources/js/Navbars/MobileNavbar.vue index 8544523..97cc127 100644 --- a/resources/js/Navbars/MobileNavbar.vue +++ b/resources/js/Navbars/MobileNavbar.vue @@ -59,7 +59,7 @@ class="flex items-center gap-x-3 py-2 px-2.5 text-sm rounded-lg hover:bg-gray-[#EFEFEF] focus:outline-none focus:bg-gray-100" :class="(IS_SAME_ROUTE(page.path) ? 'text-blue-600 bg-white border border-[#E4E4E7]' : 'text-gray-700')" > - + {{ page.title }} @@ -72,8 +72,8 @@
  • - Расписание diff --git a/resources/js/Pages/Client/AcademicJournals/Index.vue b/resources/js/Pages/Client/AcademicJournals/Index.vue index 29fed8c..15ed8b4 100644 --- a/resources/js/Pages/Client/AcademicJournals/Index.vue +++ b/resources/js/Pages/Client/AcademicJournals/Index.vue @@ -3,15 +3,16 @@ import {Head, Link} from "@inertiajs/vue3"; import MainPageNavBar from "@/Navbars/MainPageNavbar.vue"; import MetaTags from "@/componentss/shared/SEO/MetaTags.vue"; import BasicFooter from "@/footers/BasicFooter.vue"; +import AcademicJournalsListBreadcrumbs + from "@/componentss/features/academicJournals/components/AcademicJournalsListBreadcrumbs.vue.vue"; export default { name: "Index", components: { + AcademicJournalsListBreadcrumbs, BasicFooter, MetaTags, MainPageNavBar, - ClientFooterDown, - ClientScrollTimeline, Link, Head, }, @@ -44,8 +45,8 @@ export default {
    -
    -
    +
    +
    @@ -54,6 +55,7 @@ export default {
    +

    Научные периодические издания НТГСПИ

    diff --git a/resources/js/Pages/Client/AcademicJournals/Show.vue b/resources/js/Pages/Client/AcademicJournals/Show.vue index 64bd66b..b14055e 100644 --- a/resources/js/Pages/Client/AcademicJournals/Show.vue +++ b/resources/js/Pages/Client/AcademicJournals/Show.vue @@ -5,10 +5,15 @@ import Builder from "@/componentss/shared/builder/pageBuilder/Builder.vue"; import BasicFooter from "@/footers/BasicFooter.vue"; import MetaTags from "@/componentss/shared/SEO/MetaTags.vue"; import AcademicJournalsTitle from "@/componentss/features/academicJournals/components/AcademicJournalsTitle.vue"; +import BasicPagination from "@/componentss/shared/paginate/BasicPagination.vue"; +import AcademicJournalsItemBreadcrumbs + from "@/componentss/features/academicJournals/components/AcademicJournalsItemBreadcrumbs.vue"; export default { name: "Show", components: { + AcademicJournalsItemBreadcrumbs, + BasicPagination, MetaTags, BasicFooter, Builder, @@ -66,6 +71,7 @@ export default {
    +
    -
    +

    Найдено мероприятий: {{ events.data.length }}

    @@ -107,26 +107,24 @@ export default {
    diff --git a/resources/js/Pages/Client/Events/Show.vue b/resources/js/Pages/Client/Events/Show.vue index def1a9d..86d5b68 100644 --- a/resources/js/Pages/Client/Events/Show.vue +++ b/resources/js/Pages/Client/Events/Show.vue @@ -70,28 +70,32 @@ export default {
    -
    +
    -
    -

    - Онлайн -

    -

    - {{ event.data.category }} -

    -
    - Дата начала: {{ event.data.event_date_start }}, {{ event.data.event_time_start }} -
    -
    - Адрес: {{ event.data.address }} -
    -
    -
    +
    + + Онлайн + + + {{ event.data.category.title }} + +
    + Дата начала: {{ event.data.event_date_start }}, {{ event.data.event_time_start }} +
    +
    + Адрес: {{ event.data.address }} +
    +
    +
    -
    +
    diff --git a/resources/js/Pages/Client/Posts/Index.vue b/resources/js/Pages/Client/Posts/Index.vue index 5d4dd5b..20b0c1c 100644 --- a/resources/js/Pages/Client/Posts/Index.vue +++ b/resources/js/Pages/Client/Posts/Index.vue @@ -76,12 +76,12 @@ export default {
    -
    +

    Новости НТГСПИ

    Узнайте последние новости любимого вуза

    - +
    @@ -93,9 +93,9 @@ export default { -
    +

    Найдено новостей: {{ posts.meta.total }}

    -
    +
    @@ -103,8 +103,8 @@ export default {
    -
    -
    +
    +
    diff --git a/resources/js/Pages/Dashboard/CreateSchedule.vue b/resources/js/Pages/Dashboard/CreateSchedule.vue deleted file mode 100644 index ff978c6..0000000 --- a/resources/js/Pages/Dashboard/CreateSchedule.vue +++ /dev/null @@ -1,125 +0,0 @@ - - - - - \ No newline at end of file diff --git a/resources/js/Pages/Main.vue b/resources/js/Pages/Main.vue index e300dd9..1652d47 100644 --- a/resources/js/Pages/Main.vue +++ b/resources/js/Pages/Main.vue @@ -2,7 +2,7 @@ - +

    Последние новости

    diff --git a/resources/js/componentss/features/academicJournals/components/AcademicJournalsItemBreadcrumbs.vue b/resources/js/componentss/features/academicJournals/components/AcademicJournalsItemBreadcrumbs.vue new file mode 100644 index 0000000..11e7320 --- /dev/null +++ b/resources/js/componentss/features/academicJournals/components/AcademicJournalsItemBreadcrumbs.vue @@ -0,0 +1,209 @@ + + + + + + \ No newline at end of file diff --git a/resources/js/componentss/features/academicJournals/components/AcademicJournalsListBreadcrumbs.vue.vue b/resources/js/componentss/features/academicJournals/components/AcademicJournalsListBreadcrumbs.vue.vue new file mode 100644 index 0000000..ba41933 --- /dev/null +++ b/resources/js/componentss/features/academicJournals/components/AcademicJournalsListBreadcrumbs.vue.vue @@ -0,0 +1,180 @@ + + + + + + \ No newline at end of file diff --git a/resources/js/componentss/features/pages/components/PageBreadcrumbs.vue b/resources/js/componentss/features/pages/components/PageBreadcrumbs.vue index 1da575d..db19e91 100644 --- a/resources/js/componentss/features/pages/components/PageBreadcrumbs.vue +++ b/resources/js/componentss/features/pages/components/PageBreadcrumbs.vue @@ -9,7 +9,7 @@
  • -
  • +
  • -
  • +
  • - -
    - +
    +
    -
    -