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