From 17392cbe2519f7e18fde7982c737cdc6641e6e84 Mon Sep 17 00:00:00 2001 From: F4ilji Date: Sat, 14 Jun 2025 19:27:37 +0500 Subject: [PATCH] added new constructor block and schedule upload page --- .../Forms/ItemForm/Blocks/FastFilesBlock.php | 45 ++++ .../Pages/UploadSchedules.php | 245 ++++++++++++++++++ 2 files changed, 290 insertions(+) create mode 100644 app/Filament/Components/Forms/ItemForm/Blocks/FastFilesBlock.php create mode 100644 app/Filament/Resources/ScheduleResource/Pages/ScheduleResource/Pages/UploadSchedules.php diff --git a/app/Filament/Components/Forms/ItemForm/Blocks/FastFilesBlock.php b/app/Filament/Components/Forms/ItemForm/Blocks/FastFilesBlock.php new file mode 100644 index 0000000..981a3ac --- /dev/null +++ b/app/Filament/Components/Forms/ItemForm/Blocks/FastFilesBlock.php @@ -0,0 +1,45 @@ +label('Файл') + ->required() + ->multiple() + ->helperText('Поддерживаются PDF, Word, Excel, PowerPoint и ZIP файлы') + ->getUploadedFileNameForStorageUsing( + fn (TemporaryUploadedFile $file): string => + // Генерируем уникальное имя файла для хранения, используя slug из оригинального имени и временную метку + str(pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME) . '-' . Carbon::now()->format('d-m-Y') . '.' . $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) // 512MB + ->disk('public') + ->directory('files') + ->downloadable() + + ->visibility('public'), + ]; + } +} \ No newline at end of file diff --git a/app/Filament/Resources/ScheduleResource/Pages/ScheduleResource/Pages/UploadSchedules.php b/app/Filament/Resources/ScheduleResource/Pages/ScheduleResource/Pages/UploadSchedules.php new file mode 100644 index 0000000..caa2adc --- /dev/null +++ b/app/Filament/Resources/ScheduleResource/Pages/ScheduleResource/Pages/UploadSchedules.php @@ -0,0 +1,245 @@ +form->fill(); если нет начальных данных для формы. + // FileUpload управляет своим состоянием сам. + $this->processedFiles = []; + $this->failedFiles = []; + $this->schedules_files = []; // Инициализируем как пустой массив при монтировании + } + + public function form(Form $form): Form + { + return $form + ->schema([ + Section::make('Загрузка расписаний') + ->description('Загрузите несколько файлов расписаний одновременно. Система автоматически определит группы и создаст записи.') + ->schema([ + FileUpload::make('schedules_files') + ->label('Файлы расписаний') + ->multiple() + ->required() + ->acceptedFileTypes(['application/pdf']) + ->maxSize(10000) // 10MB + ->disk('public') + ->directory('schedules') + ->helperText('Загрузите PDF файлы. Название файла должно содержать название группы (например, "Нт-102 мТМОД Экзамены.pdf").') + // Удаляем afterStateUpdated, так как обработка будет в save() + // ->afterStateUpdated(function ($state, callable $set) { /* ... */ }), + ]), + ]) + // Важно: если FileUpload напрямую привязан к публичному свойству $schedules_files, + // то statePath('data') не нужен для этого конкретного поля. + // Если у вас есть другие поля, которые вы хотите привязать к $data, оставьте statePath('data'). + // Но для FileUpload, привязанного к $schedules_files, он будет напрямую изменять это свойство. + // Если вы хотите, чтобы все поля были в $data, то тогда FileUpload должен быть 'data.schedules_files' + // в схеме формы, и вы будете обращаться к $this->data['schedules_files']. + // Для простоты, оставим его напрямую привязанным к $schedules_files. + // Если у вас нет других полей формы, можете вообще удалить statePath. + // В данном случае, поскольку у нас только один FileUpload, его лучше не привязывать к $data. + // $this->data = $form->getState(); // Это не нужно при такой привязке + ; + } + + /** + * Логика обработки одиночного загруженного файла. + * Теперь вызывается только из метода save(). + * @param TemporaryUploadedFile $file + * @return bool Успех обработки + */ + protected function processUploadedFile(TemporaryUploadedFile $file): bool + { + try { + $originalFileNameWithExtension = $file->getClientOriginalName(); + $originalFileName = pathinfo($originalFileNameWithExtension, PATHINFO_FILENAME); + $extension = $file->getClientOriginalExtension(); + + // Проверяем, был ли файл уже успешно обработан в этой сессии + // Это дополнительная защита от дублирования, хотя основной баг исправлен переносом логики. + if (in_array($originalFileNameWithExtension, $this->processedFiles)) { + return true; // Уже обработан, ничего не делаем + } + + $groupTitleParts = explode(' ', $originalFileName, 3); + $groupSearchTitle = implode(' ', array_slice($groupTitleParts, 0, 2)); + + $educationalGroup = EducationalGroup::where('title', 'like', $groupSearchTitle . '%') + ->first(); + + if (!$educationalGroup) { + $this->failedFiles[] = $originalFileNameWithExtension; + Notification::make() + ->title('Группа не найдена') + ->body("Не удалось найти группу для файла: `{$originalFileNameWithExtension}`. Проверьте название файла.") + ->warning() + ->send(); + return false; + } + + // Проверяем наличие расписания для данной группы с похожим названием файла + // Это предотвратит создание дубликатов в базе данных, если пользователь загрузит тот же файл. + $existingSchedule = Schedule::where('educational_group_id', $educationalGroup->id) + ->whereJsonContains('file->0->title', $originalFileName) + ->first(); + + if ($existingSchedule) { + $this->failedFiles[] = $originalFileNameWithExtension . " (уже существует)"; + Notification::make() + ->title('Расписание уже существует') + ->body("Расписание для группы `{$educationalGroup->title}` с файлом `{$originalFileNameWithExtension}` уже существует.") + ->info() + ->send(); + return false; + } + + + // Перемещаем файл в публичное хранилище + $sluggedFileName = Str::slug($originalFileName); + // Добавляем timestamp для уникальности, но убедимся, что он достаточно уникален + $uniqueFileName = $sluggedFileName . '-' . md5(uniqid(rand(), true)) . '.' . $extension; // Более уникальный ID + $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[] = $originalFileNameWithExtension; + Notification::make() + ->title('Расписание успешно загружено') + ->body("Файл `{$originalFileNameWithExtension}` для группы `{$educationalGroup->title}`") + ->success() + ->send(); + return true; + + } catch (\Throwable $e) { + $this->failedFiles[] = $originalFileNameWithExtension . " (Ошибка: " . $e->getMessage() . ")"; + Notification::make() + ->title('Ошибка при обработке файла') + ->body("Не удалось обработать файл `{$originalFileNameWithExtension}`: " . $e->getMessage()) + ->danger() + ->send(); + report($e); + return false; + } + } + + public function save(): void + { + // Сбрасываем списки обработанных/необработанных файлов перед новой попыткой сохранения. + $this->processedFiles = []; + $this->failedFiles = []; + + // Проверяем, есть ли файлы для обработки + if (empty($this->schedules_files)) { + Notification::make() + ->title('Нет файлов для обработки') + ->warning() + ->send(); + return; + } + + // Проходим по всем загруженным файлам и обрабатываем их + foreach ($this->schedules_files as $file) { + if ($file instanceof TemporaryUploadedFile) { + $this->processUploadedFile($file); + } + } + + // Отправляем итоговое уведомление + if (!empty($this->processedFiles) || !empty($this->failedFiles)) { + $message = ''; + if (!empty($this->processedFiles)) { + $message .= 'Успешно обработаны: ' . implode(', ', $this->processedFiles) . '. '; + } + if (!empty($this->failedFiles)) { + $message .= 'Не удалось обработать: ' . implode(', ', $this->failedFiles) . '. '; + } + + Notification::make() + ->title('Результаты загрузки файлов') + ->body($message) + ->info() + ->send(); + } + + // Очищаем форму и связанные свойства после обработки + // Важно очистить $schedules_files, чтобы избежать повторной обработки при следующей отправке + $this->schedules_files = []; + // Если у вас есть statePath, можно вызвать $this->form->fill(); + // В данном случае, так как $schedules_files напрямую привязан, достаточно обнулить его. + } + + protected function getFormActions(): array + { + return [ + Action::make('save') + ->label('Загрузить и показать результаты') // Изменил текст для ясности + ->submit('save') + ->color('primary') + ->icon('heroicon-o-cloud-arrow-up') + // Кнопка всегда видна, чтобы пользователь мог инициировать обработку + // даже если он просто выбрал файлы, но они еще не были обработаны. + // Также полезно для вывода результатов после загрузки. + // ->hidden(fn () => empty($this->processedFiles) && empty($this->failedFiles)) + ]; + } + + + public static function getNavigationLabel(): string + { + return __('Расписание и группы'); + } +} \ No newline at end of file