added new constructor block and schedule upload page
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Components\Forms\ItemForm\Blocks;
|
||||
|
||||
use App\Filament\Components\Forms\ItemForm\Blocks\BlockSchema;
|
||||
use App\Ship\Helpers\ByteConverter;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\Hidden;
|
||||
use Filament\Forms\Components\Repeater;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Str;
|
||||
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
|
||||
|
||||
class FastFilesBlock implements BlockSchema
|
||||
{
|
||||
public static function schema(): array
|
||||
{
|
||||
return [
|
||||
FileUpload::make('path')
|
||||
->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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
+245
@@ -0,0 +1,245 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\ScheduleResource\Pages\ScheduleResource\Pages;
|
||||
|
||||
use App\Filament\Resources\ScheduleResource;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Resources\Pages\Page;
|
||||
|
||||
use App\Containers\Schedule\Models\EducationalGroup;
|
||||
use App\Containers\Schedule\Models\Schedule;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Notifications\Notification;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile; // Убедитесь, что этот use-оператор присутствует
|
||||
|
||||
class UploadSchedules extends Page
|
||||
{
|
||||
protected static string $resource = ScheduleResource::class;
|
||||
|
||||
protected static ?string $title = 'Быстрая загрузка расписания';
|
||||
|
||||
protected static string $view = 'filament.resources.schedule-resource.pages.schedule-resource.pages.upload-schedules';
|
||||
|
||||
protected static ?string $navigationIcon = 'heroicon-o-arrow-up-on-square';
|
||||
|
||||
protected static ?string $navigationLabel = 'Быстрая загрузка';
|
||||
|
||||
// protected static ?int $navigationSort = 1;
|
||||
|
||||
// `public ?array $data = [];` - это не нужно, если вы используете $schedules_files напрямую для FileUpload
|
||||
|
||||
/**
|
||||
* Публичное свойство для FileUpload.
|
||||
* Livewire/Filament ожидает его наличия для корректной работы компонента формы.
|
||||
* Будет содержать массив TemporaryUploadedFile или null.
|
||||
*/
|
||||
public $schedules_files = []; // Инициализируем как пустой массив
|
||||
|
||||
public array $processedFiles = [];
|
||||
public array $failedFiles = [];
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
// Не нужно вызывать $this->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 __('Расписание и группы');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user