This commit is contained in:
F4ilji
2026-04-07 17:54:26 +05:00
parent 04adba6682
commit 936b1fb5b0
468 changed files with 43498 additions and 4176 deletions
@@ -9,6 +9,3 @@ Route::middleware('access-check')->group(function () {
Route::get('/news', IndexPostController::class)->name('client.post.index');
Route::get('/news/{slug}', ShowPostController::class)->name('client.post.show');
});
@@ -0,0 +1,32 @@
<?php
namespace App\Containers\Article\UI\WEB\Requests;
use Illuminate\Foundation\Http\FormRequest;
class CreateSlideRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'title' => 'nullable|string|max:255',
'content' => 'nullable|string|max:1000',
'image' => 'required|array',
'image.url' => 'required|string',
'link' => 'required|string|max:255',
'settings' => 'nullable|array',
'settings.text_position' => 'nullable|string|in:left,center,right',
'settings.link_text' => 'nullable|string|max:50',
'settings.shading' => 'nullable|string',
'color_theme' => 'required|string',
'is_active' => 'boolean',
'start_time' => 'nullable|date',
'end_time' => 'nullable|date|after_or_equal:start_time',
];
}
}
@@ -0,0 +1,32 @@
<?php
namespace App\Containers\Article\UI\WEB\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UpdateSlideRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'title' => 'nullable|string|max:255',
'content' => 'nullable|string|max:1000',
'image' => 'sometimes|array',
'image.url' => 'sometimes|required|string',
'link' => 'sometimes|required|string|max:255',
'settings' => 'nullable|array',
'settings.text_position' => 'nullable|string|in:left,center,right',
'settings.link_text' => 'nullable|string|max:50',
'settings.shading' => 'nullable|string',
'color_theme' => 'sometimes|required|string',
'is_active' => 'sometimes|boolean',
'start_time' => 'nullable|date',
'end_time' => 'nullable|date|after_or_equal:start_time',
];
}
}
@@ -0,0 +1,13 @@
<?php
namespace App\Containers\Dashboard\Actions\AcademicJournals;
use App\Containers\Science\Models\AcademicJournal;
class CreateAcademicJournalAction
{
public function run(array $data): AcademicJournal
{
return AcademicJournal::create($data);
}
}
@@ -0,0 +1,13 @@
<?php
namespace App\Containers\Dashboard\Actions\AcademicJournals;
use App\Containers\Science\Models\AcademicJournal;
class DeleteAcademicJournalAction
{
public function run(AcademicJournal $journal): bool
{
return $journal->delete();
}
}
@@ -0,0 +1,25 @@
<?php
namespace App\Containers\Dashboard\Actions\AcademicJournals;
use App\Containers\Science\Models\AcademicJournal;
class ListAcademicJournalsAction
{
public function run(array $filters = []): array
{
$query = AcademicJournal::query();
// Поиск по названию
if (!empty($filters['search'])) {
$query->where('title', 'like', '%' . $filters['search'] . '%');
}
$journals = $query->orderBy('created_at', 'desc')->paginate(20)->withQueryString();
return [
'journals' => $journals,
'filters' => $filters,
];
}
}
@@ -0,0 +1,14 @@
<?php
namespace App\Containers\Dashboard\Actions\AcademicJournals;
use App\Containers\Science\Models\AcademicJournal;
class UpdateAcademicJournalAction
{
public function run(AcademicJournal $journal, array $data): AcademicJournal
{
$journal->update($data);
return $journal->fresh();
}
}
@@ -0,0 +1,13 @@
<?php
namespace App\Containers\Dashboard\Actions\AdditionalEducations\Categories;
use App\Containers\AdditionalEducation\Models\AdditionalEducationCategory;
class CreateCategoryAction
{
public function run(array $data): AdditionalEducationCategory
{
return AdditionalEducationCategory::create($data);
}
}
@@ -0,0 +1,13 @@
<?php
namespace App\Containers\Dashboard\Actions\AdditionalEducations\Categories;
use App\Containers\AdditionalEducation\Models\AdditionalEducationCategory;
class DeleteCategoryAction
{
public function run(AdditionalEducationCategory $category): bool
{
return $category->delete();
}
}
@@ -0,0 +1,40 @@
<?php
namespace App\Containers\Dashboard\Actions\AdditionalEducations\Categories;
use App\Containers\AdditionalEducation\Models\AdditionalEducationCategory;
use App\Containers\AdditionalEducation\Models\DirectionAdditionalEducation;
class ListCategoriesAction
{
public function run(array $filters = []): array
{
$query = AdditionalEducationCategory::with(['direction']);
// Фильтр по направлению
if (!empty($filters['direction_id'])) {
$query->where('dir_addit_educat_id', $filters['direction_id']);
}
// Фильтр по активности
if (isset($filters['is_active']) && $filters['is_active'] !== '') {
$query->where('is_active', filter_var($filters['is_active'], FILTER_VALIDATE_BOOLEAN));
}
// Поиск по названию
if (!empty($filters['search'])) {
$search = $filters['search'];
$query->where('title', 'like', '%' . $search . '%');
}
$categories = $query->orderBy('title')->paginate(20)->withQueryString();
return [
'categories' => $categories,
'filters' => $filters,
'directions' => DirectionAdditionalEducation::where('is_active', true)
->orderBy('title')
->get(['id', 'title']),
];
}
}
@@ -0,0 +1,14 @@
<?php
namespace App\Containers\Dashboard\Actions\AdditionalEducations\Categories;
use App\Containers\AdditionalEducation\Models\AdditionalEducationCategory;
class UpdateCategoryAction
{
public function run(AdditionalEducationCategory $category, array $data): AdditionalEducationCategory
{
$category->update($data);
return $category;
}
}
@@ -0,0 +1,20 @@
<?php
namespace App\Containers\Dashboard\Actions\AdditionalEducations;
use App\Containers\AdditionalEducation\Models\AdditionalEducation;
use App\Containers\Dashboard\Tasks\AdditionalEducations\GenerateSearchDataTask;
class CreateAdditionalEducationAction
{
public function __construct(
private readonly GenerateSearchDataTask $generateSearchDataTask,
) {}
public function run(array $data): AdditionalEducation
{
$data['search_data'] = $this->generateSearchDataTask->run($data['content'] ?? []);
return AdditionalEducation::create($data);
}
}
@@ -0,0 +1,13 @@
<?php
namespace App\Containers\Dashboard\Actions\AdditionalEducations;
use App\Containers\AdditionalEducation\Models\AdditionalEducation;
class DeleteAdditionalEducationAction
{
public function run(AdditionalEducation $education): bool
{
return $education->delete();
}
}
@@ -0,0 +1,13 @@
<?php
namespace App\Containers\Dashboard\Actions\AdditionalEducations\Directions;
use App\Containers\AdditionalEducation\Models\DirectionAdditionalEducation;
class CreateDirectionAction
{
public function run(array $data): DirectionAdditionalEducation
{
return DirectionAdditionalEducation::create($data);
}
}
@@ -0,0 +1,13 @@
<?php
namespace App\Containers\Dashboard\Actions\AdditionalEducations\Directions;
use App\Containers\AdditionalEducation\Models\DirectionAdditionalEducation;
class DeleteDirectionAction
{
public function run(DirectionAdditionalEducation $direction): bool
{
return $direction->delete();
}
}
@@ -0,0 +1,31 @@
<?php
namespace App\Containers\Dashboard\Actions\AdditionalEducations\Directions;
use App\Containers\AdditionalEducation\Models\DirectionAdditionalEducation;
class ListDirectionsAction
{
public function run(array $filters = []): array
{
$query = DirectionAdditionalEducation::query();
// Фильтр по активности
if (isset($filters['is_active']) && $filters['is_active'] !== '') {
$query->where('is_active', filter_var($filters['is_active'], FILTER_VALIDATE_BOOLEAN));
}
// Поиск по названию
if (!empty($filters['search'])) {
$search = $filters['search'];
$query->where('title', 'like', '%' . $search . '%');
}
$directions = $query->orderBy('title')->paginate(20)->withQueryString();
return [
'directions' => $directions,
'filters' => $filters,
];
}
}
@@ -0,0 +1,14 @@
<?php
namespace App\Containers\Dashboard\Actions\AdditionalEducations\Directions;
use App\Containers\AdditionalEducation\Models\DirectionAdditionalEducation;
class UpdateDirectionAction
{
public function run(DirectionAdditionalEducation $direction, array $data): DirectionAdditionalEducation
{
$direction->update($data);
return $direction;
}
}
@@ -0,0 +1,55 @@
<?php
namespace App\Containers\Dashboard\Actions\AdditionalEducations;
use App\Containers\AdditionalEducation\Models\AdditionalEducation;
use App\Containers\AdditionalEducation\Models\AdditionalEducationCategory;
use App\Ship\Enums\Education\FormEducation;
class ListAdditionalEducationsAction
{
public function run(array $filters = []): array
{
$query = AdditionalEducation::with(['category']);
// Фильтр по категории
if (!empty($filters['category_id'])) {
$query->where('category_id', $filters['category_id']);
}
// Фильтр по форме обучения
if (!empty($filters['form_education'])) {
$query->where('form_education', $filters['form_education']);
}
// Фильтр по активности
if (isset($filters['is_active']) && $filters['is_active'] !== '') {
$query->where('is_active', filter_var($filters['is_active'], FILTER_VALIDATE_BOOLEAN));
}
// Поиск по названию или целевой аудитории
if (!empty($filters['search'])) {
$search = $filters['search'];
$query->where(function ($q) use ($search) {
$q->where('title', 'like', '%' . $search . '%')
->orWhere('target_group', 'like', '%' . $search . '%');
});
}
$educations = $query->orderBy('title')->paginate(20)->withQueryString();
return [
'educations' => $educations,
'filters' => $filters,
'categories' => AdditionalEducationCategory::where('is_active', true)
->orderBy('title')
->get(['id', 'title']),
'educationForms' => array_map(fn($form) => [
'value' => $form->value,
'label' => $form->getLabel(),
'color' => $form->getColor(),
'name' => $form->name,
], FormEducation::cases()),
];
}
}
@@ -0,0 +1,24 @@
<?php
namespace App\Containers\Dashboard\Actions\AdditionalEducations;
use App\Containers\AdditionalEducation\Models\AdditionalEducation;
use App\Containers\Dashboard\Tasks\AdditionalEducations\GenerateSearchDataTask;
class UpdateAdditionalEducationAction
{
public function __construct(
private readonly GenerateSearchDataTask $generateSearchDataTask,
) {}
public function run(AdditionalEducation $education, array $data): AdditionalEducation
{
if (isset($data['content'])) {
$data['search_data'] = $this->generateSearchDataTask->run($data['content']);
}
$education->update($data);
return $education;
}
}
@@ -0,0 +1,13 @@
<?php
namespace App\Containers\Dashboard\Actions\AdmissionCampaigns;
use App\Containers\Education\Models\AdmissionCampaign;
class CreateAdmissionCampaignAction
{
public function run(array $data): AdmissionCampaign
{
return AdmissionCampaign::create($data);
}
}
@@ -0,0 +1,13 @@
<?php
namespace App\Containers\Dashboard\Actions\AdmissionCampaigns;
use App\Containers\Education\Models\AdmissionCampaign;
class DeleteAdmissionCampaignAction
{
public function run(AdmissionCampaign $campaign): bool
{
return $campaign->delete();
}
}
@@ -0,0 +1,58 @@
<?php
namespace App\Containers\Dashboard\Actions\AdmissionCampaigns;
use App\Containers\Education\Models\AdmissionCampaign;
use App\Ship\Enums\Education\AdmissionCampaignStatus;
class ListAdmissionCampaignsAction
{
public function run(array $filters = []): array
{
$query = AdmissionCampaign::query();
// Фильтр по статусу
if (!empty($filters['status'])) {
$query->where('status', $filters['status']);
}
// Фильтр по учебному году
if (!empty($filters['academic_year'])) {
$query->where('academic_year', $filters['academic_year']);
}
// Поиск по названию
if (!empty($filters['search'])) {
$search = $filters['search'];
$query->where('name', 'like', '%' . $search . '%');
}
$campaigns = $query->orderBy('academic_year', 'desc')->paginate(20)->withQueryString();
return [
'campaigns' => $campaigns,
'filters' => $filters,
'statuses' => array_map(fn($status) => [
'value' => $status->value,
'label' => $status->getLabel(),
'color' => $status->getColor(),
], AdmissionCampaignStatus::cases()),
'academicYears' => $this->generateAcademicYears(),
];
}
private function generateAcademicYears(): array
{
$currentYear = (int) date('Y') - 5;
$yearsAhead = 10;
$academicYears = [];
for ($i = 0; $i < $yearsAhead; $i++) {
$startYear = $currentYear + $i;
$endYear = $startYear + 1;
$academicYears[] = "{$startYear}/{$endYear}";
}
return $academicYears;
}
}
@@ -0,0 +1,14 @@
<?php
namespace App\Containers\Dashboard\Actions\AdmissionCampaigns;
use App\Containers\Education\Models\AdmissionCampaign;
class UpdateAdmissionCampaignAction
{
public function run(AdmissionCampaign $campaign, array $data): AdmissionCampaign
{
$campaign->update($data);
return $campaign;
}
}
@@ -0,0 +1,13 @@
<?php
namespace App\Containers\Dashboard\Actions\AdmissionPlans;
use App\Containers\Education\Models\AdmissionPlan;
class CreateAdmissionPlanAction
{
public function run(array $data): AdmissionPlan
{
return AdmissionPlan::create($data);
}
}
@@ -0,0 +1,13 @@
<?php
namespace App\Containers\Dashboard\Actions\AdmissionPlans;
use App\Containers\Education\Models\AdmissionPlan;
class DeleteAdmissionPlanAction
{
public function run(AdmissionPlan $plan): bool
{
return $plan->delete();
}
}
@@ -0,0 +1,38 @@
<?php
namespace App\Containers\Dashboard\Actions\AdmissionPlans;
use App\Containers\Education\Models\AdmissionPlan;
use App\Containers\Education\Models\AdmissionCampaign;
use App\Containers\Education\Models\EducationalProgram;
use App\Ship\Enums\Education\EducationalProgramStatus;
class ListAdmissionPlansAction
{
public function run(array $filters = []): array
{
$query = AdmissionPlan::with(['educationalProgram', 'admissionCampaign']);
// Фильтр по приемной кампании
if (!empty($filters['admission_campaigns_id'])) {
$query->where('admission_campaigns_id', $filters['admission_campaigns_id']);
}
// Фильтр по образовательной программе
if (!empty($filters['educational_programs_id'])) {
$query->where('educational_programs_id', $filters['educational_programs_id']);
}
$plans = $query->orderBy('id', 'desc')->paginate(20)->withQueryString();
return [
'plans' => $plans,
'filters' => $filters,
'admissionCampaigns' => AdmissionCampaign::orderBy('name')->get(['id', 'name', 'academic_year']),
'educationalPrograms' => EducationalProgram::whereIn('status', [
EducationalProgramStatus::PUBLISHED,
EducationalProgramStatus::IN_PROGRESS
])->orderBy('name')->get(['id', 'name']),
];
}
}
@@ -0,0 +1,14 @@
<?php
namespace App\Containers\Dashboard\Actions\AdmissionPlans;
use App\Containers\Education\Models\AdmissionPlan;
class UpdateAdmissionPlanAction
{
public function run(AdmissionPlan $plan, array $data): AdmissionPlan
{
$plan->update($data);
return $plan;
}
}
@@ -0,0 +1,16 @@
<?php
namespace App\Containers\Dashboard\Actions\Categories;
use App\Containers\Article\Models\Category;
use Illuminate\Support\Str;
class CreateCategoryAction
{
public function run(array $data): Category
{
$data['slug'] = Str::slug($data['title']);
return Category::create($data);
}
}
@@ -0,0 +1,13 @@
<?php
namespace App\Containers\Dashboard\Actions\Categories;
use App\Containers\Article\Models\Category;
class DeleteCategoryAction
{
public function run(Category $category): bool
{
return $category->delete();
}
}
@@ -0,0 +1,30 @@
<?php
namespace App\Containers\Dashboard\Actions\Categories;
use App\Containers\Article\Models\Category;
class ListCategoriesAction
{
public function run(array $filters = []): array
{
$query = Category::query();
// Поиск по названию
if (!empty($filters['search'])) {
$query->where('title', 'like', '%' . $filters['search'] . '%');
}
// Фильтр по статусу
if (isset($filters['is_active']) && $filters['is_active'] !== '') {
$query->where('is_active', (bool) $filters['is_active']);
}
$categories = $query->orderBy('title')->paginate(20)->withQueryString();
return [
'categories' => $categories,
'filters' => $filters,
];
}
}
@@ -0,0 +1,19 @@
<?php
namespace App\Containers\Dashboard\Actions\Categories;
use App\Containers\Article\Models\Category;
use Illuminate\Support\Str;
class UpdateCategoryAction
{
public function run(Category $category, array $data): Category
{
if (isset($data['title'])) {
$data['slug'] = Str::slug($data['title']);
}
$category->update($data);
return $category->fresh();
}
}
@@ -0,0 +1,17 @@
<?php
namespace App\Containers\Dashboard\Actions\ContactWidgets;
use App\Containers\Widget\Models\ContactWidget;
use Illuminate\Support\Str;
class CreateContactWidgetAction
{
public function run(array $data): ContactWidget
{
$data['slug'] = $data['slug'] ?? Str::slug($data['title']);
$data['is_active'] = $data['is_active'] ?? true;
return ContactWidget::create($data);
}
}
@@ -0,0 +1,13 @@
<?php
namespace App\Containers\Dashboard\Actions\ContactWidgets;
use App\Containers\Widget\Models\ContactWidget;
class DeleteContactWidgetAction
{
public function run(ContactWidget $widget): bool
{
return $widget->delete();
}
}
@@ -0,0 +1,30 @@
<?php
namespace App\Containers\Dashboard\Actions\ContactWidgets;
use App\Containers\Widget\Models\ContactWidget;
class ListContactWidgetsAction
{
public function run(array $filters = []): array
{
$query = ContactWidget::query();
// Поиск по названию
if (!empty($filters['search'])) {
$query->where('title', 'like', '%' . $filters['search'] . '%');
}
// Фильтр по статусу
if (isset($filters['is_active']) && $filters['is_active'] !== '') {
$query->where('is_active', (bool) $filters['is_active']);
}
$widgets = $query->orderByDesc('created_at')->paginate(20)->withQueryString();
return [
'widgets' => $widgets,
'filters' => $filters,
];
}
}
@@ -0,0 +1,15 @@
<?php
namespace App\Containers\Dashboard\Actions\ContactWidgets;
use App\Containers\Widget\Models\ContactWidget;
class UpdateContactWidgetAction
{
public function run(ContactWidget $widget, array $data): ContactWidget
{
$widget->update($data);
return $widget;
}
}
@@ -0,0 +1,20 @@
<?php
namespace App\Containers\Dashboard\Actions\CustomForms;
use App\Containers\Widget\Models\CustomForm;
use Illuminate\Support\Str;
class CreateCustomFormAction
{
public function run(array $data): CustomForm
{
$data['form_id'] = $data['form_id'] ?? Str::slug($data['title']) . time();
$data['status'] = $data['status'] ?? 'published';
$data['settings'] = $data['settings'] ?? [];
$data['mail_settings'] = $data['mail_settings'] ?? [];
$data['columns'] = $data['columns'] ?? [];
return CustomForm::create($data);
}
}
@@ -0,0 +1,13 @@
<?php
namespace App\Containers\Dashboard\Actions\CustomForms;
use App\Containers\Widget\Models\CustomForm;
class DeleteCustomFormAction
{
public function run(CustomForm $form): bool
{
return $form->delete();
}
}
@@ -0,0 +1,13 @@
<?php
namespace App\Containers\Dashboard\Actions\CustomForms;
use App\Containers\Widget\Models\CustomFormResponse;
class DeleteFormResponseAction
{
public function run(CustomFormResponse $response): bool
{
return $response->delete();
}
}
@@ -0,0 +1,34 @@
<?php
namespace App\Containers\Dashboard\Actions\CustomForms;
use App\Containers\Widget\Models\CustomForm;
class ListCustomFormsAction
{
public function run(array $filters = []): array
{
$query = CustomForm::query();
// Поиск по названию
if (!empty($filters['search'])) {
$query->where('title', 'like', '%' . $filters['search'] . '%');
}
// Фильтр по статусу
if (!empty($filters['status'])) {
$query->where('status', $filters['status']);
}
$forms = $query->orderByDesc('created_at')->paginate(20)->withQueryString();
return [
'forms' => $forms,
'filters' => $filters,
'statuses' => [
['value' => 'published', 'label' => 'Опубликовано'],
['value' => 'hidden', 'label' => 'Скрыто'],
],
];
}
}
@@ -0,0 +1,53 @@
<?php
namespace App\Containers\Dashboard\Actions\CustomForms;
use App\Containers\Widget\Models\CustomForm;
class ListFormResponsesAction
{
public function run(CustomForm $form, array $filters = []): array
{
$query = $form->responses()->with('form');
// Фильтр по статусу просмотра
if (isset($filters['checked']) && $filters['checked'] !== '') {
$query->where('checked', (bool) $filters['checked']);
}
// Поиск по ID
if (!empty($filters['search'])) {
$query->where('id', 'like', '%' . $filters['search'] . '%');
}
$responses = $query->orderByDesc('created_at')->paginate(20)->withQueryString();
// Динамические колонки на основе полей формы
$columns = collect($form->columns ?? [])->map(function ($field) {
return [
'name' => $field['data']['name_field'] ?? '',
'title' => $field['data']['title_field'] ?? '',
'type' => $field['type'] ?? 'text',
'options' => $this->extractOptions($field),
];
})->filter(fn($col) => !empty($col['name']))->values()->toArray();
return [
'form' => $form,
'responses' => $responses,
'columns' => $columns,
'filters' => $filters,
];
}
private function extractOptions(array $field): array
{
if (!in_array($field['type'], ['single_choice', 'multiple_choice'])) {
return [];
}
return collect($field['data']['columns'] ?? [])
->mapWithKeys(fn($opt) => [$opt['name_field'] ?? '' => $opt['title_field'] ?? ''])
->toArray();
}
}
@@ -0,0 +1,15 @@
<?php
namespace App\Containers\Dashboard\Actions\CustomForms;
use App\Containers\Widget\Models\CustomForm;
class UpdateCustomFormAction
{
public function run(CustomForm $form, array $data): CustomForm
{
$form->update($data);
return $form;
}
}
@@ -0,0 +1,21 @@
<?php
namespace App\Containers\Dashboard\Actions\Departments;
use App\Containers\Education\Models\EducationalProgram;
use App\Containers\InstituteStructure\Models\Department;
use App\Services\App\Cache\DepartmentCacheService;
class AttachDepartmentProgramAction
{
public function __construct(
private readonly DepartmentCacheService $cacheService,
) {}
public function run(Department $department, EducationalProgram $program): void
{
$department->programs()->attach($program->id);
$this->cacheService->clearAllCacheByModel();
}
}
@@ -0,0 +1,27 @@
<?php
namespace App\Containers\Dashboard\Actions\Departments;
use App\Containers\InstituteStructure\Models\Department;
use App\Containers\User\Models\User;
use App\Services\App\Cache\DepartmentCacheService;
class AttachDepartmentTeacherAction
{
public function __construct(
private readonly DepartmentCacheService $cacheService,
) {}
public function run(Department $department, User $user, array $data): void
{
$department->teachers()->attach($user->id, [
'teaching_position' => $data['teaching_position'],
'service_email' => $data['service_email'] ?? null,
'service_phone' => $data['service_phone'] ?? null,
'cabinet' => $data['cabinet'] ?? null,
'sort' => $department->teachers()->count() + 1,
]);
$this->cacheService->clearAllCacheByModel();
}
}
@@ -0,0 +1,27 @@
<?php
namespace App\Containers\Dashboard\Actions\Departments;
use App\Containers\InstituteStructure\Models\Department;
use App\Containers\User\Models\User;
use App\Services\App\Cache\DepartmentCacheService;
class AttachDepartmentWorkerAction
{
public function __construct(
private readonly DepartmentCacheService $cacheService,
) {}
public function run(Department $department, User $user, array $data): void
{
$department->workers()->attach($user->id, [
'position' => $data['position'],
'service_email' => $data['service_email'] ?? null,
'service_phone' => $data['service_phone'] ?? null,
'cabinet' => $data['cabinet'] ?? null,
'sort' => $department->workers()->count() + 1,
]);
$this->cacheService->clearAllCacheByModel();
}
}
@@ -0,0 +1,55 @@
<?php
namespace App\Containers\Dashboard\Actions\Departments;
use App\Containers\Dashboard\Tasks\Content\GenerateSearchDataTask;
use App\Containers\InstituteStructure\Models\Department;
use Illuminate\Support\Str;
class CreateDepartmentAction
{
public function __construct(
private readonly GenerateSearchDataTask $generateSearchDataTask,
) {}
public function run(array $data): Department
{
// Генерируем slug если не передан
if (empty($data['slug'])) {
$data['slug'] = Str::slug($data['title']);
}
// Генерируем search_data из контента
if (!empty($data['content'])) {
$data['search_data'] = $this->generateSearchDataTask->run($data['content']);
}
$department = Department::create($data);
// Создаем SEO
$this->generateSeo($department, $data);
return $department;
}
private function generateSeo(Department $department, array $data): void
{
$title = $data['title'] ?? '';
$description = null;
// Извлекаем description из первого paragraph блока
if (!empty($data['content'])) {
foreach ($data['content'] as $block) {
if ($block['type'] === 'paragraph') {
$description = strip_tags($block['data']['content']);
break;
}
}
}
$department->seo()->create([
'title' => $title,
'description' => Str::limit(htmlspecialchars($description ?? '', ENT_QUOTES, 'UTF-8'), 160),
]);
}
}
@@ -0,0 +1,13 @@
<?php
namespace App\Containers\Dashboard\Actions\Departments;
use App\Containers\InstituteStructure\Models\Department;
class DeleteDepartmentAction
{
public function run(Department $department): void
{
$department->delete();
}
}
@@ -0,0 +1,21 @@
<?php
namespace App\Containers\Dashboard\Actions\Departments;
use App\Containers\Education\Models\EducationalProgram;
use App\Containers\InstituteStructure\Models\Department;
use App\Services\App\Cache\DepartmentCacheService;
class DetachDepartmentProgramAction
{
public function __construct(
private readonly DepartmentCacheService $cacheService,
) {}
public function run(Department $department, EducationalProgram $program): void
{
$department->programs()->detach($program->id);
$this->cacheService->clearAllCacheByModel();
}
}
@@ -0,0 +1,21 @@
<?php
namespace App\Containers\Dashboard\Actions\Departments;
use App\Containers\InstituteStructure\Models\Department;
use App\Containers\User\Models\User;
use App\Services\App\Cache\DepartmentCacheService;
class DetachDepartmentTeacherAction
{
public function __construct(
private readonly DepartmentCacheService $cacheService,
) {}
public function run(Department $department, User $user): void
{
$department->teachers()->detach($user->id);
$this->cacheService->clearAllCacheByModel();
}
}
@@ -0,0 +1,21 @@
<?php
namespace App\Containers\Dashboard\Actions\Departments;
use App\Containers\InstituteStructure\Models\Department;
use App\Containers\User\Models\User;
use App\Services\App\Cache\DepartmentCacheService;
class DetachDepartmentWorkerAction
{
public function __construct(
private readonly DepartmentCacheService $cacheService,
) {}
public function run(Department $department, User $user): void
{
$department->workers()->detach($user->id);
$this->cacheService->clearAllCacheByModel();
}
}
@@ -0,0 +1,31 @@
<?php
namespace App\Containers\Dashboard\Actions\Departments;
use App\Containers\InstituteStructure\Models\Department;
use App\Services\App\Cache\DepartmentCacheService;
class ListDepartmentProgramsAction
{
public function run(Department $department, array $filters = []): array
{
$query = $department->programs();
// Фильтр по статусу
if (!empty($filters['status'])) {
$query->where('status', $filters['status']);
}
// Поиск по названию
if (!empty($filters['search'])) {
$query->where('name', 'like', '%' . $filters['search'] . '%');
}
$programs = $query->orderBy('name')->paginate(20)->withQueryString();
return [
'programs' => $programs,
'filters' => $filters,
];
}
}
@@ -0,0 +1,30 @@
<?php
namespace App\Containers\Dashboard\Actions\Departments;
use App\Containers\InstituteStructure\Models\Department;
class ListDepartmentTeachersAction
{
public function run(Department $department, array $filters = []): array
{
$query = $department->teachers();
// Поиск по имени
if (!empty($filters['search'])) {
$query->where('name', 'like', '%' . $filters['search'] . '%');
}
// Фильтр по должности
if (!empty($filters['position'])) {
$query->where('teaching_position', 'like', '%' . $filters['position'] . '%');
}
$teachers = $query->orderBy('teachers_departments.sort')->paginate(20)->withQueryString();
return [
'teachers' => $teachers,
'filters' => $filters,
];
}
}
@@ -0,0 +1,31 @@
<?php
namespace App\Containers\Dashboard\Actions\Departments;
use App\Containers\InstituteStructure\Models\Department;
use App\Containers\User\Models\User;
class ListDepartmentWorkersAction
{
public function run(Department $department, array $filters = []): array
{
$query = $department->workers();
// Поиск по имени
if (!empty($filters['search'])) {
$query->where('name', 'like', '%' . $filters['search'] . '%');
}
// Фильтр по должности
if (!empty($filters['position'])) {
$query->where('position', 'like', '%' . $filters['position'] . '%');
}
$workers = $query->orderBy('workers_departments.sort')->paginate(20)->withQueryString();
return [
'workers' => $workers,
'filters' => $filters,
];
}
}
@@ -0,0 +1,35 @@
<?php
namespace App\Containers\Dashboard\Actions\Departments;
use App\Containers\InstituteStructure\Models\Department;
class ListDepartmentsAction
{
public function run(array $filters = []): array
{
$query = Department::query()->with(['faculty']);
// Фильтр по факультету
if (isset($filters['faculty_id']) && $filters['faculty_id'] !== '') {
$query->where('faculty_id', (int) $filters['faculty_id']);
}
// Фильтр по статусу
if (isset($filters['is_active']) && $filters['is_active'] !== '') {
$query->where('is_active', (bool) $filters['is_active']);
}
// Поиск по названию
if (!empty($filters['search'])) {
$query->where('title', 'like', '%' . $filters['search'] . '%');
}
$departments = $query->orderBy('title')->paginate(20)->withQueryString();
return [
'departments' => $departments,
'filters' => $filters,
];
}
}
@@ -0,0 +1,56 @@
<?php
namespace App\Containers\Dashboard\Actions\Departments;
use App\Containers\Dashboard\Tasks\Content\GenerateSearchDataTask;
use App\Containers\InstituteStructure\Models\Department;
use Illuminate\Support\Str;
class UpdateDepartmentAction
{
public function __construct(
private readonly GenerateSearchDataTask $generateSearchDataTask,
) {}
public function run(Department $department, array $data): Department
{
// Генерируем search_data из контента только если контент передан и не пуст
if (isset($data['content']) && !empty($data['content'])) {
$data['search_data'] = $this->generateSearchDataTask->run($data['content']);
}
$department->update($data);
// Обновляем SEO
$this->updateSeo($department, $data);
return $department;
}
private function updateSeo(Department $department, array $data): void
{
$title = $data['title'] ?? $department->title;
$description = null;
// Извлекаем description из первого paragraph блока
if (!empty($data['content'])) {
foreach ($data['content'] as $block) {
if ($block['type'] === 'paragraph') {
$description = strip_tags($block['data']['content']);
break;
}
}
}
$seoData = [
'title' => $title,
'description' => Str::limit(htmlspecialchars($description ?? '', ENT_QUOTES, 'UTF-8'), 160),
];
if ($department->seo) {
$department->seo->update($seoData);
} else {
$department->seo()->create($seoData);
}
}
}
@@ -0,0 +1,26 @@
<?php
namespace App\Containers\Dashboard\Actions\Departments;
use App\Containers\InstituteStructure\Models\Department;
use App\Containers\User\Models\User;
use App\Services\App\Cache\DepartmentCacheService;
class UpdateDepartmentTeacherAction
{
public function __construct(
private readonly DepartmentCacheService $cacheService,
) {}
public function run(Department $department, User $user, array $data): void
{
$department->teachers()->updateExistingPivot($user->id, [
'teaching_position' => $data['teaching_position'],
'service_email' => $data['service_email'] ?? null,
'service_phone' => $data['service_phone'] ?? null,
'cabinet' => $data['cabinet'] ?? null,
]);
$this->cacheService->clearAllCacheByModel();
}
}
@@ -0,0 +1,26 @@
<?php
namespace App\Containers\Dashboard\Actions\Departments;
use App\Containers\InstituteStructure\Models\Department;
use App\Containers\User\Models\User;
use App\Services\App\Cache\DepartmentCacheService;
class UpdateDepartmentWorkerAction
{
public function __construct(
private readonly DepartmentCacheService $cacheService,
) {}
public function run(Department $department, User $user, array $data): void
{
$department->workers()->updateExistingPivot($user->id, [
'position' => $data['position'],
'service_email' => $data['service_email'] ?? null,
'service_phone' => $data['service_phone'] ?? null,
'cabinet' => $data['cabinet'] ?? null,
]);
$this->cacheService->clearAllCacheByModel();
}
}
@@ -0,0 +1,13 @@
<?php
namespace App\Containers\Dashboard\Actions\DirectionStudies;
use App\Containers\Education\Models\DirectionStudy;
class CreateDirectionStudyAction
{
public function run(array $data): DirectionStudy
{
return DirectionStudy::create($data);
}
}
@@ -0,0 +1,13 @@
<?php
namespace App\Containers\Dashboard\Actions\DirectionStudies;
use App\Containers\Education\Models\DirectionStudy;
class DeleteDirectionStudyAction
{
public function run(DirectionStudy $direction): bool
{
return $direction->delete();
}
}
@@ -0,0 +1,40 @@
<?php
namespace App\Containers\Dashboard\Actions\DirectionStudies;
use App\Containers\Education\Models\DirectionStudy;
use App\Ship\Enums\Education\LevelEducational;
class ListDirectionStudiesAction
{
public function run(array $filters = []): array
{
$query = DirectionStudy::query();
// Фильтр по уровню образования
if (!empty($filters['lvl_edu'])) {
$query->where('lvl_edu', $filters['lvl_edu']);
}
// Поиск по коду или названию
if (!empty($filters['search'])) {
$search = $filters['search'];
$query->where(function ($q) use ($search) {
$q->where('code', 'like', '%' . $search . '%')
->orWhere('name', 'like', '%' . $search . '%');
});
}
$directions = $query->orderBy('code')->paginate(20)->withQueryString();
return [
'directions' => $directions,
'filters' => $filters,
'educationLevels' => array_map(fn($level) => [
'value' => $level->value,
'label' => $level->getLabel(),
'color' => $level->getColor(),
], LevelEducational::cases()),
];
}
}
@@ -0,0 +1,14 @@
<?php
namespace App\Containers\Dashboard\Actions\DirectionStudies;
use App\Containers\Education\Models\DirectionStudy;
class UpdateDirectionStudyAction
{
public function run(DirectionStudy $direction, array $data): DirectionStudy
{
$direction->update($data);
return $direction;
}
}
@@ -0,0 +1,27 @@
<?php
namespace App\Containers\Dashboard\Actions\Divisions;
use App\Containers\InstituteStructure\Models\Division;
use App\Containers\User\Models\User;
use App\Services\App\Cache\DivisionCacheService;
class AttachDivisionWorkerAction
{
public function __construct(
private readonly DivisionCacheService $cacheService,
) {}
public function run(Division $division, User $user, array $data): void
{
$division->workers()->attach($user->id, [
'administrativePosition' => $data['administrativePosition'],
'service_email' => $data['service_email'] ?? null,
'service_phone' => $data['service_phone'] ?? null,
'cabinet' => $data['cabinet'] ?? null,
'sort' => $data['sort'] ?? $division->workers()->count(),
]);
$this->cacheService->clearAllCacheByModel();
}
}
@@ -0,0 +1,55 @@
<?php
namespace App\Containers\Dashboard\Actions\Divisions;
use App\Containers\Dashboard\Tasks\Content\GenerateSearchDataTask;
use App\Containers\InstituteStructure\Models\Division;
use Illuminate\Support\Str;
class CreateDivisionAction
{
public function __construct(
private readonly GenerateSearchDataTask $generateSearchDataTask,
) {}
public function run(array $data): Division
{
// Генерируем slug если не передан
if (empty($data['slug'])) {
$data['slug'] = Str::slug($data['title']);
}
// Генерируем search_data из описания
if (!empty($data['description'])) {
$data['search_data'] = $this->generateSearchDataTask->run($data['description']);
}
$division = Division::create($data);
// Создаем SEO
$this->generateSeo($division, $data);
return $division;
}
private function generateSeo(Division $division, array $data): void
{
$title = $data['title'] ?? '';
$description = null;
// Извлекаем description из первого paragraph блока
if (!empty($data['description'])) {
foreach ($data['description'] as $block) {
if ($block['type'] === 'paragraph') {
$description = strip_tags($block['data']['content']);
break;
}
}
}
$division->seo()->create([
'title' => $title,
'description' => Str::limit(htmlspecialchars($description ?? '', ENT_QUOTES, 'UTF-8'), 160),
]);
}
}
@@ -0,0 +1,13 @@
<?php
namespace App\Containers\Dashboard\Actions\Divisions;
use App\Containers\InstituteStructure\Models\Division;
class DeleteDivisionAction
{
public function run(Division $division): void
{
$division->delete();
}
}
@@ -0,0 +1,20 @@
<?php
namespace App\Containers\Dashboard\Actions\Divisions;
use App\Containers\InstituteStructure\Models\Division;
use App\Containers\User\Models\User;
use App\Services\App\Cache\DivisionCacheService;
class DetachDivisionWorkerAction
{
public function __construct(
private readonly DivisionCacheService $cacheService,
) {}
public function run(Division $division, User $worker): void
{
$division->workers()->detach($worker->id);
$this->cacheService->clearAllCacheByModel();
}
}
@@ -0,0 +1,33 @@
<?php
namespace App\Containers\Dashboard\Actions\Divisions;
use App\Containers\InstituteStructure\Models\Division;
use Illuminate\Database\Eloquent\Builder;
class ListDivisionWorkersAction
{
public function run(Division $division, array $filters = []): array
{
$query = $division->workers()
->withPivot(['administrativePosition', 'sort', 'service_email', 'service_phone', 'cabinet'])
->whereHas('userDetail');
// Поиск по ФИО
if (!empty($filters['search'])) {
$query->where('name', 'like', '%' . $filters['search'] . '%');
}
// Фильтр по должности
if (!empty($filters['position'])) {
$query->where('division_user.administrativePosition', 'like', '%' . $filters['position'] . '%');
}
$workers = $query->orderBy('division_user.sort')->paginate(20)->withQueryString();
return [
'workers' => $workers,
'filters' => $filters,
];
}
}
@@ -0,0 +1,33 @@
<?php
namespace App\Containers\Dashboard\Actions\Divisions;
use App\Containers\InstituteStructure\Models\Division;
class ListDivisionsAction
{
public function run(array $filters = []): array
{
$query = Division::query();
// Фильтр по статусу (по умолчанию только активные)
if (isset($filters['is_active']) && $filters['is_active'] !== '') {
$query->where('is_active', (bool) $filters['is_active']);
} elseif (!isset($filters['is_active'])) {
$query->where('is_active', true);
}
// Поиск по названию
if (!empty($filters['search'])) {
$query->where('title', 'like', '%' . $filters['search'] . '%')
->orWhere('slug', 'like', '%' . $filters['search'] . '%');
}
$divisions = $query->orderBy('created_at', 'desc')->paginate(20)->withQueryString();
return [
'divisions' => $divisions,
'filters' => $filters,
];
}
}
@@ -0,0 +1,56 @@
<?php
namespace App\Containers\Dashboard\Actions\Divisions;
use App\Containers\Dashboard\Tasks\Content\GenerateSearchDataTask;
use App\Containers\InstituteStructure\Models\Division;
use Illuminate\Support\Str;
class UpdateDivisionAction
{
public function __construct(
private readonly GenerateSearchDataTask $generateSearchDataTask,
) {}
public function run(Division $division, array $data): Division
{
// Генерируем search_data из описания только если описание передано и не пусто
if (isset($data['description']) && !empty($data['description'])) {
$data['search_data'] = $this->generateSearchDataTask->run($data['description']);
}
$division->update($data);
// Обновляем SEO
$this->updateSeo($division, $data);
return $division;
}
private function updateSeo(Division $division, array $data): void
{
$title = $data['title'] ?? $division->title;
$description = null;
// Извлекаем description из первого paragraph блока
if (!empty($data['description'])) {
foreach ($data['description'] as $block) {
if ($block['type'] === 'paragraph') {
$description = strip_tags($block['data']['content']);
break;
}
}
}
$seoData = [
'title' => $title,
'description' => Str::limit(htmlspecialchars($description ?? '', ENT_QUOTES, 'UTF-8'), 160),
];
if ($division->seo) {
$division->seo->update($seoData);
} else {
$division->seo()->create($seoData);
}
}
}
@@ -0,0 +1,26 @@
<?php
namespace App\Containers\Dashboard\Actions\Divisions;
use App\Containers\InstituteStructure\Models\Division;
use App\Containers\User\Models\User;
use App\Services\App\Cache\DivisionCacheService;
class UpdateDivisionWorkerAction
{
public function __construct(
private readonly DivisionCacheService $cacheService,
) {}
public function run(Division $division, User $worker, array $data): void
{
$division->workers()->updateExistingPivot($worker->id, [
'administrativePosition' => $data['administrativePosition'],
'service_email' => $data['service_email'] ?? null,
'service_phone' => $data['service_phone'] ?? null,
'cabinet' => $data['cabinet'] ?? null,
]);
$this->cacheService->clearAllCacheByModel();
}
}
@@ -0,0 +1,13 @@
<?php
namespace App\Containers\Dashboard\Actions\EducationalGroups;
use App\Containers\Schedule\Models\EducationalGroup;
class CreateEducationalGroupAction
{
public function run(array $data): EducationalGroup
{
return EducationalGroup::create($data);
}
}
@@ -0,0 +1,13 @@
<?php
namespace App\Containers\Dashboard\Actions\EducationalGroups;
use App\Containers\Schedule\Models\EducationalGroup;
class DeleteEducationalGroupAction
{
public function run(EducationalGroup $group): bool
{
return $group->delete();
}
}
@@ -0,0 +1,44 @@
<?php
namespace App\Containers\Dashboard\Actions\EducationalGroups;
use App\Containers\Schedule\Models\EducationalGroup;
use App\Ship\Enums\Education\FormEducation;
use App\Containers\InstituteStructure\Models\Faculty;
class ListEducationalGroupsAction
{
public function run(array $filters = []): array
{
$query = EducationalGroup::with(['faculty']);
// Фильтр по факультету
if (!empty($filters['faculty_id'])) {
$query->where('faculty_id', $filters['faculty_id']);
}
// Фильтр по форме обучения
if (!empty($filters['education_form_id'])) {
$query->where('education_form_id', $filters['education_form_id']);
}
// Поиск по названию группы
if (!empty($filters['search'])) {
$query->where('title', 'like', '%' . $filters['search'] . '%');
}
$groups = $query->orderBy('title')->paginate(20)->withQueryString();
return [
'groups' => $groups,
'filters' => $filters,
'faculties' => Faculty::orderBy('title')->get(['id', 'title']),
'educationForms' => array_map(fn($form) => [
'value' => $form->value,
'label' => $form->getLabel(),
'color' => $form->getColor(),
'name' => $form->name,
], FormEducation::cases()),
];
}
}
@@ -0,0 +1,14 @@
<?php
namespace App\Containers\Dashboard\Actions\EducationalGroups;
use App\Containers\Schedule\Models\EducationalGroup;
class UpdateEducationalGroupAction
{
public function run(EducationalGroup $group, array $data): EducationalGroup
{
$group->update($data);
return $group->fresh();
}
}
@@ -0,0 +1,13 @@
<?php
namespace App\Containers\Dashboard\Actions\EducationalPrograms;
use App\Containers\Education\Models\EducationalProgram;
class CreateEducationalProgramAction
{
public function run(array $data): EducationalProgram
{
return EducationalProgram::create($data);
}
}
@@ -0,0 +1,13 @@
<?php
namespace App\Containers\Dashboard\Actions\EducationalPrograms;
use App\Containers\Education\Models\EducationalProgram;
class DeleteEducationalProgramAction
{
public function run(EducationalProgram $program): bool
{
return $program->delete();
}
}
@@ -0,0 +1,55 @@
<?php
namespace App\Containers\Dashboard\Actions\EducationalPrograms;
use App\Containers\Education\Models\EducationalProgram;
use App\Containers\Education\Models\DirectionStudy;
use App\Ship\Enums\Education\EducationalProgramStatus;
use App\Ship\Enums\Education\LevelEducational;
class ListEducationalProgramsAction
{
public function run(array $filters = []): array
{
$query = EducationalProgram::with(['directionStudy']);
// Фильтр по уровню образования
if (!empty($filters['lvl_edu'])) {
$query->where('lvl_edu', $filters['lvl_edu']);
}
// Фильтр по статусу
if (!empty($filters['status'])) {
$query->where('status', $filters['status']);
}
// Фильтр по направлению подготовки
if (!empty($filters['direction_study_id'])) {
$query->where('direction_study_id', $filters['direction_study_id']);
}
// Поиск по названию
if (!empty($filters['search'])) {
$search = $filters['search'];
$query->where('name', 'like', '%' . $search . '%');
}
$programs = $query->orderBy('name')->paginate(20)->withQueryString();
return [
'programs' => $programs,
'filters' => $filters,
'statuses' => array_map(fn($status) => [
'value' => $status->value,
'label' => $status->getLabel(),
'color' => $status->getColor(),
], EducationalProgramStatus::cases()),
'educationLevels' => array_map(fn($level) => [
'value' => $level->value,
'label' => $level->getLabel(),
'color' => $level->getColor(),
], LevelEducational::cases()),
'directionStudies' => DirectionStudy::orderBy('code')->get(['id', 'code', 'name']),
];
}
}
@@ -0,0 +1,14 @@
<?php
namespace App\Containers\Dashboard\Actions\EducationalPrograms;
use App\Containers\Education\Models\EducationalProgram;
class UpdateEducationalProgramAction
{
public function run(EducationalProgram $program, array $data): EducationalProgram
{
$program->update($data);
return $program;
}
}
@@ -189,6 +189,9 @@ class FetchEmailNewsAction
'subject' => $email['subject'] ?? 'unknown',
]);
// Помечаем письмо как прочитанное чтобы не обрабатывать повторно
$this->markEmail($email['message'], $folder);
return [
'success' => false,
'error' => 'Нет DOC/DOCX файла для извлечения текста',
@@ -0,0 +1,27 @@
<?php
namespace App\Containers\Dashboard\Actions\Faculties;
use App\Containers\InstituteStructure\Models\Faculty;
use App\Containers\User\Models\User;
use App\Services\App\Cache\FacultyCacheService;
class AttachFacultyWorkerAction
{
public function __construct(
private readonly FacultyCacheService $cacheService,
) {}
public function run(Faculty $faculty, User $user, array $data): void
{
$faculty->workers()->attach($user->id, [
'position' => $data['position'],
'service_email' => $data['service_email'] ?? null,
'service_phone' => $data['service_phone'] ?? null,
'cabinet' => $data['cabinet'] ?? null,
'sort' => $data['sort'] ?? $faculty->workers()->count(),
]);
$this->cacheService->clearAllCacheByModel();
}
}
@@ -0,0 +1,55 @@
<?php
namespace App\Containers\Dashboard\Actions\Faculties;
use App\Containers\Dashboard\Tasks\Content\GenerateSearchDataTask;
use App\Containers\InstituteStructure\Models\Faculty;
use Illuminate\Support\Str;
class CreateFacultyAction
{
public function __construct(
private readonly GenerateSearchDataTask $generateSearchDataTask,
) {}
public function run(array $data): Faculty
{
// Генерируем slug если не передан
if (empty($data['slug'])) {
$data['slug'] = Str::slug($data['title']);
}
// Генерируем search_data из контента
if (!empty($data['content'])) {
$data['search_data'] = $this->generateSearchDataTask->run($data['content']);
}
$faculty = Faculty::create($data);
// Создаем SEO
$this->generateSeo($faculty, $data);
return $faculty;
}
private function generateSeo(Faculty $faculty, array $data): void
{
$title = $data['title'] ?? '';
$description = null;
// Извлекаем description из первого paragraph блока
if (!empty($data['content'])) {
foreach ($data['content'] as $block) {
if ($block['type'] === 'paragraph') {
$description = strip_tags($block['data']['content']);
break;
}
}
}
$faculty->seo()->create([
'title' => $title,
'description' => Str::limit(htmlspecialchars($description ?? '', ENT_QUOTES, 'UTF-8'), 160),
]);
}
}
@@ -0,0 +1,13 @@
<?php
namespace App\Containers\Dashboard\Actions\Faculties;
use App\Containers\InstituteStructure\Models\Faculty;
class DeleteFacultyAction
{
public function run(Faculty $faculty): void
{
$faculty->delete();
}
}
@@ -0,0 +1,20 @@
<?php
namespace App\Containers\Dashboard\Actions\Faculties;
use App\Containers\InstituteStructure\Models\Faculty;
use App\Containers\User\Models\User;
use App\Services\App\Cache\FacultyCacheService;
class DetachFacultyWorkerAction
{
public function __construct(
private readonly FacultyCacheService $cacheService,
) {}
public function run(Faculty $faculty, User $worker): void
{
$faculty->workers()->detach($worker->id);
$this->cacheService->clearAllCacheByModel();
}
}
@@ -0,0 +1,30 @@
<?php
namespace App\Containers\Dashboard\Actions\Faculties;
use App\Containers\InstituteStructure\Models\Faculty;
class ListFacultiesAction
{
public function run(array $filters = []): array
{
$query = Faculty::query();
// Фильтр по статусу
if (isset($filters['is_active']) && $filters['is_active'] !== '') {
$query->where('is_active', (bool) $filters['is_active']);
}
// Поиск по названию
if (!empty($filters['search'])) {
$query->where('title', 'like', '%' . $filters['search'] . '%');
}
$faculties = $query->orderBy('title')->paginate(20)->withQueryString();
return [
'faculties' => $faculties,
'filters' => $filters,
];
}
}
@@ -0,0 +1,33 @@
<?php
namespace App\Containers\Dashboard\Actions\Faculties;
use App\Containers\InstituteStructure\Models\Faculty;
use Illuminate\Database\Eloquent\Builder;
class ListFacultyWorkersAction
{
public function run(Faculty $faculty, array $filters = []): array
{
$query = $faculty->workers()
->withPivot(['position', 'sort', 'service_email', 'service_phone', 'cabinet'])
->whereHas('userDetail');
// Поиск по ФИО
if (!empty($filters['search'])) {
$query->where('name', 'like', '%' . $filters['search'] . '%');
}
// Фильтр по должности
if (!empty($filters['position'])) {
$query->where('workers_faculties.position', 'like', '%' . $filters['position'] . '%');
}
$workers = $query->orderBy('workers_faculties.sort')->paginate(20)->withQueryString();
return [
'workers' => $workers,
'filters' => $filters,
];
}
}
@@ -0,0 +1,56 @@
<?php
namespace App\Containers\Dashboard\Actions\Faculties;
use App\Containers\Dashboard\Tasks\Content\GenerateSearchDataTask;
use App\Containers\InstituteStructure\Models\Faculty;
use Illuminate\Support\Str;
class UpdateFacultyAction
{
public function __construct(
private readonly GenerateSearchDataTask $generateSearchDataTask,
) {}
public function run(Faculty $faculty, array $data): Faculty
{
// Генерируем search_data из контента только если контент передан и не пуст
if (isset($data['content']) && !empty($data['content'])) {
$data['search_data'] = $this->generateSearchDataTask->run($data['content']);
}
$faculty->update($data);
// Обновляем SEO
$this->updateSeo($faculty, $data);
return $faculty;
}
private function updateSeo(Faculty $faculty, array $data): void
{
$title = $data['title'] ?? $faculty->title;
$description = null;
// Извлекаем description из первого paragraph блока
if (!empty($data['content'])) {
foreach ($data['content'] as $block) {
if ($block['type'] === 'paragraph') {
$description = strip_tags($block['data']['content']);
break;
}
}
}
$seoData = [
'title' => $title,
'description' => Str::limit(htmlspecialchars($description ?? '', ENT_QUOTES, 'UTF-8'), 160),
];
if ($faculty->seo) {
$faculty->seo->update($seoData);
} else {
$faculty->seo()->create($seoData);
}
}
}
@@ -0,0 +1,26 @@
<?php
namespace App\Containers\Dashboard\Actions\Faculties;
use App\Containers\InstituteStructure\Models\Faculty;
use App\Containers\User\Models\User;
use App\Services\App\Cache\FacultyCacheService;
class UpdateFacultyWorkerAction
{
public function __construct(
private readonly FacultyCacheService $cacheService,
) {}
public function run(Faculty $faculty, User $worker, array $data): void
{
$faculty->workers()->updateExistingPivot($worker->id, [
'position' => $data['position'],
'service_email' => $data['service_email'] ?? null,
'service_phone' => $data['service_phone'] ?? null,
'cabinet' => $data['cabinet'] ?? null,
]);
$this->cacheService->clearAllCacheByModel();
}
}
@@ -1,234 +0,0 @@
<?php
namespace App\Containers\Dashboard\Actions;
use App\Containers\Dashboard\Data\EmailAttachmentData;
use App\Containers\Dashboard\Exceptions\EmailFetchException;
use App\Containers\Dashboard\Tasks\ConnectToImapTask;
use App\Containers\Dashboard\Tasks\DownloadAttachmentsTask;
use App\Containers\Dashboard\Tasks\FetchUnreadEmailsTask;
use App\Containers\Dashboard\Tasks\FilterBySenderTask;
use App\Containers\Dashboard\Tasks\MarkEmailAsReadTask;
use App\Containers\Dashboard\Actions\ProcessMixedFilesAction;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Webklex\PHPIMAP\Client;
use Webklex\PHPIMAP\Folder;
/**
* Оркестрация процесса получения новостей из Email
*/
class FetchEmailNewsAction
{
public function __construct(
private readonly ConnectToImapTask $connectToImapTask,
private readonly FetchUnreadEmailsTask $fetchUnreadEmailsTask,
private readonly FilterBySenderTask $filterBySenderTask,
private readonly DownloadAttachmentsTask $downloadAttachmentsTask,
private readonly MarkEmailAsReadTask $markEmailAsReadTask,
private readonly ProcessMixedFilesAction $processMixedFilesAction,
) {}
/**
* Выполнить получение и обработку новостей из Email
*
* @return array Результат обработки
* @throws EmailFetchException
*/
public function run(): array
{
// Проверяем, включена ли функция
if (!config('email-news.enabled', true)) {
Log::warning('[FetchEmailNewsAction] Функция отключена в конфиге');
throw EmailFetchException::featureDisabled();
}
Log::info('[FetchEmailNewsAction] Начало получения новостей из Email');
$result = [
'processed_emails' => 0,
'skipped_emails' => 0,
'created_posts' => 0,
'errors' => [],
'posts' => [],
];
try {
// Подключаемся к IMAP
$client = $this->connectToImapTask->run();
// Получаем папку
$folder = $this->connectToImapTask->getFolder(
$client,
config('email-news.folder', 'INBOX')
);
// Получаем непрочитанные письма
$emails = $this->fetchUnreadEmailsTask->run($folder);
if (empty($emails)) {
Log::info('[FetchEmailNewsAction] Нет непрочитанных писем');
return $result;
}
// Фильтруем по отправителю
$filteredEmails = $this->filterBySenderTask->run($emails);
$result['skipped_emails'] = count($emails) - count($filteredEmails);
// Обрабатываем каждое письмо
foreach ($filteredEmails as $email) {
$emailResult = $this->processEmail($email, $folder);
if ($emailResult['success']) {
$result['created_posts']++;
$result['posts'][] = $emailResult['post'];
} else {
$result['errors'][] = [
'email_subject' => $email['subject'],
'error' => $emailResult['error'],
];
}
$result['processed_emails']++;
}
Log::info('[FetchEmailNewsAction] Завершено', [
'processed' => $result['processed_emails'],
'created_posts' => $result['created_posts'],
'skipped' => $result['skipped_emails'],
'errors_count' => count($result['errors']),
]);
return $result;
} catch (\Exception $e) {
Log::error('[FetchEmailNewsAction] Критическая ошибка', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
throw $e;
}
}
/**
* Обработать одно письмо
*
* @param array $email Данные письма
* @param Folder $folder IMAP папка
* @return array Результат обработки
*/
private function processEmail(array $email, Folder $folder): array
{
Log::info('[FetchEmailNewsAction:processEmail] Обработка письма', [
'subject' => $email['subject'],
'from' => $email['from_email'],
]);
try {
// Скачиваем вложения
$attachments = $this->downloadAttachmentsTask->run($email['message']);
// Проверяем, есть ли DOC/DOCX файл
$hasDocument = collect($attachments)->contains(fn($att) => $att->isDocument());
if (!$hasDocument) {
Log::warning('[FetchEmailNewsAction:processEmail] Нет DOC/DOCX файла во вложениях', [
'subject' => $email['subject'],
]);
return [
'success' => false,
'error' => 'Нет DOC/DOCX файла для извлечения текста',
];
}
// Конвертируем вложения в UploadedFile
$uploadedFiles = $this->convertToUploadedFiles($attachments);
// Обрабатываем через существующий ProcessMixedFilesAction
$postResult = $this->processMixedFilesAction->run($uploadedFiles);
// Помечаем письмо как прочитанное
$this->markEmail($email['message'], $folder);
Log::info('[FetchEmailNewsAction:processEmail] Письмо успешно обработано', [
'subject' => $email['subject'],
'post_id' => $postResult['post']->id,
]);
return [
'success' => true,
'post' => $postResult['post'],
'attachments_count' => count($attachments),
];
} catch (\Exception $e) {
Log::error('[FetchEmailNewsAction:processEmail] Ошибка обработки письма', [
'subject' => $email['subject'],
'error' => $e->getMessage(),
]);
return [
'success' => false,
'error' => $e->getMessage(),
];
}
}
/**
* Конвертировать EmailAttachmentData в UploadedFile
*
* @param array<EmailAttachmentData> $attachments
* @return \Illuminate\Support\Collection<UploadedFile>
*/
private function convertToUploadedFiles(array $attachments): \Illuminate\Support\Collection
{
$uploadedFiles = [];
foreach ($attachments as $attachment) {
$fullPath = storage_path('app/' . $attachment->path);
if (!file_exists($fullPath)) {
Log::warning('[FetchEmailNewsAction:convertToUploadedFiles] Файл не найден', [
'path' => $attachment->path,
]);
continue;
}
// Создаём UploadedFile из сохранённого файла
$uploadedFile = new UploadedFile(
$fullPath,
$attachment->filename,
$attachment->mimeType,
null,
true // test = false (файл валиден)
);
$uploadedFiles[] = $uploadedFile;
}
Log::info('[FetchEmailNewsAction:convertToUploadedFiles] Конвертировано файлов', [
'count' => count($uploadedFiles),
]);
return collect($uploadedFiles);
}
/**
* Пометить письмо как прочитанное (и возможно переместить)
*
* @param object $message IMAP сообщение
* @param Folder $folder Текущая папка
*/
private function markEmail(object $message, Folder $folder): void
{
$moveToFolder = config('email-news.move_to_folder');
if ($moveToFolder) {
$this->markEmailAsReadTask->markAndMove($message, $moveToFolder);
} elseif (config('email-news.mark_as_read', true)) {
$this->markEmailAsReadTask->run($message);
}
}
}
@@ -0,0 +1,13 @@
<?php
namespace App\Containers\Dashboard\Actions\JournalIssues;
use App\Containers\Science\Models\JournalIssue;
class CreateJournalIssueAction
{
public function run(array $data): JournalIssue
{
return JournalIssue::create($data);
}
}
@@ -0,0 +1,13 @@
<?php
namespace App\Containers\Dashboard\Actions\JournalIssues;
use App\Containers\Science\Models\JournalIssue;
class DeleteJournalIssueAction
{
public function run(JournalIssue $issue): bool
{
return $issue->delete();
}
}
@@ -0,0 +1,42 @@
<?php
namespace App\Containers\Dashboard\Actions\JournalIssues;
use App\Containers\Science\Models\JournalIssue;
class ListJournalIssuesAction
{
public function run(int $journalId, array $filters = []): array
{
$query = JournalIssue::where('academic_journal_id', $journalId);
// Фильтр по году
if (!empty($filters['year_publication'])) {
$query->where('year_publication', $filters['year_publication']);
}
// Фильтр по статусу
if (isset($filters['is_active']) && $filters['is_active'] !== '') {
$query->where('is_active', (bool) $filters['is_active']);
}
// Поиск по названию
if (!empty($filters['search'])) {
$query->where('title', 'like', '%' . $filters['search'] . '%');
}
$issues = $query->orderBy('sort')->orderBy('year_publication', 'desc')->paginate(20)->withQueryString();
// Получаем уникальные годы для фильтра (оптимизировано через groupBy)
$years = JournalIssue::where('academic_journal_id', $journalId)
->groupBy('year_publication')
->orderBy('year_publication', 'desc')
->pluck('year_publication');
return [
'issues' => $issues,
'filters' => $filters,
'years' => $years,
];
}
}
@@ -0,0 +1,14 @@
<?php
namespace App\Containers\Dashboard\Actions\JournalIssues;
use App\Containers\Science\Models\JournalIssue;
class UpdateJournalIssueAction
{
public function run(JournalIssue $issue, array $data): JournalIssue
{
$issue->update($data);
return $issue->fresh();
}
}
@@ -0,0 +1,25 @@
<?php
namespace App\Containers\Dashboard\Actions;
use App\Containers\Dashboard\Tasks\Posts\GetAiPreparedPostsTask;
use App\Containers\Dashboard\Tasks\Stats\GetDashboardStatsTask;
use App\Containers\Dashboard\Tasks\Stats\GetRecentActivityTask;
class LoadDashboardDataAction
{
public function __construct(
private readonly GetAiPreparedPostsTask $getAiPreparedPostsTask,
private readonly GetDashboardStatsTask $getDashboardStatsTask,
private readonly GetRecentActivityTask $getRecentActivityTask,
) {}
public function run(): array
{
return [
'aiPreparedPosts' => $this->getAiPreparedPostsTask->run(),
'stats' => $this->getDashboardStatsTask->run(),
'recentActivity' => $this->getRecentActivityTask->run(),
];
}
}
@@ -0,0 +1,16 @@
<?php
namespace App\Containers\Dashboard\Actions\MainSections;
use App\Containers\AppStructure\Models\MainSection;
class CreateMainSectionAction
{
public function run(array $data): MainSection
{
return MainSection::create([
'title' => $data['title'],
'slug' => $data['slug'],
]);
}
}
@@ -0,0 +1,13 @@
<?php
namespace App\Containers\Dashboard\Actions\MainSections;
use App\Containers\AppStructure\Models\MainSection;
class DeleteMainSectionAction
{
public function run(MainSection $mainSection): bool
{
return $mainSection->delete();
}
}
@@ -0,0 +1,23 @@
<?php
namespace App\Containers\Dashboard\Actions\MainSections;
use App\Containers\AppStructure\Models\MainSection;
class ListMainSectionsAction
{
public function run(array $filters): array
{
$query = MainSection::query()->with('subSections');
if (!empty($filters['search'])) {
$query->where('title', 'like', '%' . $filters['search'] . '%');
}
$mainSections = $query->orderBy('sort')->paginate(15);
return [
'mainSections' => $mainSections,
];
}
}
@@ -0,0 +1,18 @@
<?php
namespace App\Containers\Dashboard\Actions\MainSections;
use App\Containers\AppStructure\Models\MainSection;
class UpdateMainSectionAction
{
public function run(MainSection $mainSection, array $data): MainSection
{
$mainSection->update([
'title' => $data['title'],
'slug' => $data['slug'],
]);
return $mainSection->fresh();
}
}
@@ -0,0 +1,18 @@
<?php
namespace App\Containers\Dashboard\Actions\PageReferenceLists;
use App\Containers\Widget\Models\PageReferenceList;
use Illuminate\Support\Str;
class CreatePageReferenceListAction
{
public function run(array $data): PageReferenceList
{
$data['slug'] = $data['slug'] ?? Str::slug($data['title']);
$data['is_active'] = $data['is_active'] ?? true;
$data['content'] = $data['content'] ?? [];
return PageReferenceList::create($data);
}
}
@@ -0,0 +1,13 @@
<?php
namespace App\Containers\Dashboard\Actions\PageReferenceLists;
use App\Containers\Widget\Models\PageReferenceList;
class DeletePageReferenceListAction
{
public function run(PageReferenceList $list): bool
{
return $list->delete();
}
}
@@ -0,0 +1,28 @@
<?php
namespace App\Containers\Dashboard\Actions\PageReferenceLists;
use App\Containers\Widget\Models\PageReferenceList;
class ListPageReferenceListsAction
{
public function run(array $filters = []): array
{
$query = PageReferenceList::query();
if (!empty($filters['search'])) {
$query->where('title', 'like', '%' . $filters['search'] . '%');
}
if (isset($filters['is_active']) && $filters['is_active'] !== '') {
$query->where('is_active', (bool) $filters['is_active']);
}
$lists = $query->orderByDesc('created_at')->paginate(20)->withQueryString();
return [
'lists' => $lists,
'filters' => $filters,
];
}
}
@@ -0,0 +1,15 @@
<?php
namespace App\Containers\Dashboard\Actions\PageReferenceLists;
use App\Containers\Widget\Models\PageReferenceList;
class UpdatePageReferenceListAction
{
public function run(PageReferenceList $list, array $data): PageReferenceList
{
$list->update($data);
return $list;
}
}
@@ -0,0 +1,24 @@
<?php
namespace App\Containers\Dashboard\Actions\Pages;
use App\Containers\AppStructure\Models\Page;
use App\Containers\AppStructure\Models\SubSection;
class AttachPageToSubSectionAction
{
public function run(Page $page, SubSection $subSection): Page
{
// Pessimistic lock to prevent race conditions
$page = Page::lockForUpdate()->findOrFail($page->id);
if ($page->sub_section_id !== null) {
throw new \InvalidArgumentException('Страница уже принадлежит другому подразделу');
}
$page->section()->associate($subSection);
$page->save();
return $page;
}
}

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