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
@@ -0,0 +1,13 @@
<?php
namespace App\Containers\Dashboard\Actions\Schedules;
use App\Containers\Schedule\Models\Schedule;
class CreateScheduleAction
{
public function run(array $data): Schedule
{
return Schedule::create($data);
}
}
@@ -0,0 +1,13 @@
<?php
namespace App\Containers\Dashboard\Actions\Schedules;
use App\Containers\Schedule\Models\Schedule;
class DeleteScheduleAction
{
public function run(Schedule $schedule): bool
{
return $schedule->delete();
}
}
@@ -0,0 +1,48 @@
<?php
namespace App\Containers\Dashboard\Actions\Schedules;
use App\Containers\Schedule\Models\Schedule;
use App\Containers\Schedule\Models\EducationalGroup;
use App\Ship\Enums\Education\FormEducation;
class ListSchedulesAction
{
public function run(array $filters = []): array
{
$query = Schedule::with(['educationalGroup.faculty']);
// Фильтр по учебной группе
if (!empty($filters['educational_group_id'])) {
$query->where('educational_group_id', $filters['educational_group_id']);
}
// Фильтр по форме обучения (через таблицу educational_groups)
if (!empty($filters['education_form_id'])) {
$query->whereHas('educationalGroup', function ($q) use ($filters) {
$q->where('education_form_id', $filters['education_form_id']);
});
}
// Поиск по названию группы
if (!empty($filters['search'])) {
$query->whereHas('educationalGroup', function ($q) use ($filters) {
$q->where('title', 'like', '%' . $filters['search'] . '%');
});
}
$schedules = $query->orderByDesc('updated_at')->paginate(20)->withQueryString();
return [
'schedules' => $schedules,
'filters' => $filters,
'educationalGroups' => EducationalGroup::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\Schedules;
use App\Containers\Schedule\Models\Schedule;
class UpdateScheduleAction
{
public function run(Schedule $schedule, array $data): Schedule
{
$schedule->update($data);
return $schedule->fresh();
}
}
@@ -0,0 +1,197 @@
<?php
namespace App\Containers\Dashboard\Actions\Schedules;
use App\Containers\Schedule\Models\EducationalGroup;
use App\Containers\Schedule\Models\Schedule;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Carbon;
class UploadMultipleSchedulesAction
{
private array $processedFiles = [];
private array $failedFiles = [];
/**
* Обрабатывает множественную загрузку файлов расписаний
*
* @param array $files Массив загруженных файлов
* @return array Результаты обработки
*/
public function run(array $files): array
{
$this->processedFiles = [];
$this->failedFiles = [];
foreach ($files as $file) {
$this->processFile($file);
}
return [
'processed' => $this->processedFiles,
'failed' => $this->failedFiles,
'processed_count' => count($this->processedFiles),
'failed_count' => count($this->failedFiles),
];
}
/**
* Обрабатывает одиночный файл
*/
private function processFile($file): void
{
try {
$originalFileNameWithExtension = $file->getClientOriginalName();
$originalFileName = pathinfo($originalFileNameWithExtension, PATHINFO_FILENAME);
// Проверяем, был ли файл уже обработан
if (in_array($originalFileNameWithExtension, array_column($this->processedFiles, 'filename'))) {
return;
}
$educationalGroup = $this->findEducationalGroupByFileName($originalFileName);
if (!$educationalGroup) {
// Извлекаем очищенное название для более понятного сообщения об ошибке
$fileNameWithoutExt = pathinfo($originalFileName, PATHINFO_FILENAME);
$cleanedName = preg_replace('/\s+с\s+\d{2}\.\d{2}(\.\d{4})?\s+по\s+\d{2}\.\d{2}(\.\d{4})?/ui', '', $fileNameWithoutExt);
$cleanedName = preg_replace('/\s*(Экзамены|расписание|сессия|контрольная|\d{4}).*$/ui', '', $cleanedName);
$cleanedName = trim(preg_replace('/\s+/', ' ', $cleanedName));
$this->failedFiles[] = [
'filename' => $originalFileNameWithExtension,
'error' => "Группа '{$cleanedName}' не найдена в базе. Проверьте правильность названия группы.",
];
return;
}
// Проверяем наличие расписания для данной группы
$existingSchedule = Schedule::where('educational_group_id', $educationalGroup->id)
->whereJsonContains('file->0->title', $originalFileName)
->first();
if ($existingSchedule) {
$this->failedFiles[] = [
'filename' => $originalFileNameWithExtension,
'error' => 'Расписание уже существует',
];
return;
}
// Сохраняем файл
$uniqueFileName = Str::slug($originalFileName) . '-' . Carbon::now()->timestamp . '.' . $file->getClientOriginalExtension();
$path = $file->storeAs('schedules', $uniqueFileName, 'public');
// Создаем запись
DB::transaction(function () use ($educationalGroup, $originalFileName, $path) {
Schedule::create([
'educational_group_id' => $educationalGroup->id,
'file' => [
[
'title' => $originalFileName,
'path' => $path,
],
],
]);
});
$this->processedFiles[] = [
'filename' => $originalFileNameWithExtension,
'group' => $educationalGroup->title,
];
} catch (\Throwable $e) {
$this->failedFiles[] = [
'filename' => $file->getClientOriginalName(),
'error' => $e->getMessage(),
];
}
}
/**
* Находит образовательную группу по названию файла
*
* Алгоритм:
* 1. Очищаем название файла от лишней информации (даты, слова "Экзамены", "расписание" и т.д.)
* 2. Извлекаем код группы по паттернам
* 3. Ищем точное совпадение в базе
*/
private function findEducationalGroupByFileName(string $fileName): ?EducationalGroup
{
$fileNameWithoutExt = pathinfo($fileName, PATHINFO_FILENAME);
// Нормализуем имя файла - убираем лишние пробелы
$fileNameWithoutExt = preg_replace('/\s+/', ' ', trim($fileNameWithoutExt));
// ========== ШАГ 1: Очищаем название файла от лишней информации ==========
// Удаляем даты в форматах: "с 06.04 по 09.04.2026", "с 10.01.2026 по 15.01.2026"
$cleanedName = preg_replace('/\s+с\s+\d{2}\.\d{2}(\.\d{4})?\s+по\s+\d{2}\.\d{2}(\.\d{4})?/ui', '', $fileNameWithoutExt);
// Удаляем слова "Экзамены", "расписание", "сессия" и годы
$cleanedName = preg_replace('/\s*(Экзамены|расписание|сессия|контрольная|\d{4}).*$/ui', '', $cleanedName);
// Финальная очистка - убираем лишние пробелы
$cleanedName = trim(preg_replace('/\s+/', ' ', $cleanedName));
// ========== ШАГ 2: Извлечение кода группы по паттернам ==========
// Паттерн 1: "Нт-XXXо YYY" или "Нт-XXXоYYY" (очная форма, например: Нт-101о ИРЭ, Нт-211Со Экл)
if (preg_match('/^(Нт-\d{2,3}[а-яА-ЯёЁ]?\s*[а-яА-ЯёЁ]{2,5})/u', $cleanedName, $matches)) {
$groupCode = trim($matches[1]);
// Нормализуем: убираем лишние пробелы между номером и буквами
$groupCode = preg_replace('/(\d)(\s+)([а-яА-ЯёЁ])/u', '$1$3', $groupCode);
// Ищем ТОЧНОЕ совпадение
$group = EducationalGroup::where('title', $groupCode)->first();
if ($group) {
return $group;
}
}
// Паттерн 2: "Нт-XXX YYY" (заочная форма, например: Нт-404 ИП, Нт-501 СР)
if (preg_match('/^(Нт-\d{3}\s+[А-Я]{2,4})(?![а-яА-Я])/u', $cleanedName, $matches)) {
$groupCode = trim($matches[1]);
$group = EducationalGroup::where('title', $groupCode)->first();
if ($group) {
return $group;
}
}
// Паттерн 3: "нXXX YY-о3" (например: н101 ГД-о3, н303 ФК-По3)
if (preg_match('/^(н\d{3}\s+[А-Яа-я\-]+-о[34])/u', $cleanedName, $matches)) {
$groupCode = trim($matches[1]);
$group = EducationalGroup::where('title', $groupCode)->first();
if ($group) {
return $group;
}
}
// Паттерн 4: "Нт-XXX мYYY" (магистратура, например: Нт-102 мТМОД)
if (preg_match('/^(Нт-\d{3}\s+м[А-Я]{2,5})/u', $cleanedName, $matches)) {
$groupCode = trim($matches[1]);
$group = EducationalGroup::where('title', $groupCode)->first();
if ($group) {
return $group;
}
}
// Паттерн 5: "БФКф-XXXX" (например: БФКф-2531)
if (preg_match('/^(БФКф-\d{4})/u', $cleanedName, $matches)) {
$groupCode = trim($matches[1]);
$group = EducationalGroup::where('title', $groupCode)->first();
if ($group) {
return $group;
}
}
// ========== ШАГ 3: Точное совпадение по очищенному имени файла ==========
$exactMatch = EducationalGroup::where('title', $cleanedName)->first();
if ($exactMatch) {
return $exactMatch;
}
return null;
}
}