Постановка задачи:
Нужно создать базу данных «Автовокзал» в используя фреймворк laravel, админку filament и mysql, заполнить ее данными и создать отчет по таблицам «Маршруты» и «Рейсы».
Схема данных
Миграции для создании таблиц в БД
Выполним команды в консоли:
1 2 3 4 5 6 |
php artisan make:model Destination -m php artisan make:model Schedule -m php artisan make:model Route -m php artisan make:model Stop -m php artisan make:model Bus -m php artisan make:model Trip -m |
Для создания миграций в Laravel, соответствующих моделям автовокзала, можно использовать следующие примеры:
Миграция для таблицы destinations
(Пункты назначения)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class CreateDestinationsTable extends Migration { public function up() { Schema::create('destinations', function (Blueprint $table) { $table->id(); $table->string('name'); // Название пункта назначения $table->timestamps(); }); } public function down() { Schema::dropIfExists('destinations'); } } |
Миграция для таблицы schedules
(Дни движения)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class CreateSchedulesTable extends Migration { public function up() { Schema::create('schedules', function (Blueprint $table) { $table->id(); $table->string('days'); // Дни движения $table->timestamps(); }); } public function down() { Schema::dropIfExists('schedules'); } } |
Миграция для таблицы routes
(Маршруты)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class CreateRoutesTable extends Migration { public function up() { Schema::create('routes', function (Blueprint $table) { $table->id(); $table->foreignId('departure_id')->constrained('destinations'); // Пункт отправления $table->foreignId('destination_id')->constrained('destinations'); // Пункт назначения $table->foreignId('schedule_id')->constrained('schedules'); // Дни движения $table->time('travel_time'); // Время в пути $table->timestamps(); }); } public function down() { Schema::dropIfExists('routes'); } } |
Миграция для таблицы stops
(Остановки)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class CreateStopsTable extends Migration { public function up() { Schema::create('stops', function (Blueprint $table) { $table->id(); $table->string('name'); // Название остановки $table->foreignId('route_id')->constrained('routes'); // Маршрут_id $table->timestamps(); }); } public function down() { Schema::dropIfExists('stops'); } } |
Миграция для таблицы buses
(Автобусы)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class CreateBusesTable extends Migration { public function up() { Schema::create('buses', function (Blueprint $table) { $table->id(); $table->string('brand'); // Марка автобуса $table->string('number'); // Номер автобуса $table->string('driver'); // Водитель $table->integer('seats'); // Число мест $table->timestamps(); }); } public function down() { Schema::dropIfExists('buses'); } } |
Миграция для таблицы trips
(Рейсы)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class CreateTripsTable extends Migration { public function up() { Schema::create('trips', function (Blueprint $table) { $table->id(); $table->time('departure_time'); // Время отправления $table->time('arrival_time'); // Время прибытия $table->foreignId('route_id')->constrained('routes'); // Маршрут_id $table->foreignId('bus_id')->constrained('buses'); // Автобус_id $table->decimal('ticket_price', 8, 2); // Цена билета $table->timestamps(); }); } public function down() { Schema::dropIfExists('trips'); } } |
Модели для Laravel
Для реализации сущностей автовокзала в Laravel, можно создать следующие модели:
Destination (Пункт назначения)
1 2 3 4 5 6 7 8 9 10 11 |
namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class Destination extends Model { use HasFactory; protected $fillable = ['name']; // Название пункта назначения } |
Schedule (Дни движения)
1 2 3 4 5 6 7 8 9 10 11 |
namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class Schedule extends Model { use HasFactory; protected $fillable = ['days']; // Дни движения } |
Route (Маршрут)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class Route extends Model { use HasFactory; protected $fillable = [ 'departure_id', // Пункт отправления 'destination_id', // Пункт назначения 'schedule_id', // Дни движения 'travel_time' // Время в пути ]; public function departure() { return $this->belongsTo(Destination::class, 'departure_id'); } public function destination() { return $this->belongsTo(Destination::class, 'destination_id'); } public function schedule() { return $this->belongsTo(Schedule::class, 'schedule_id'); } public function routeName(): Attribute { return new Attribute( get: fn () => $this->departure->name .' - ' . $this->destination->name ); } } |
Stop (Остановка)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class Stop extends Model { use HasFactory; protected $fillable = ['name', 'route_id']; // Название остановки и маршрут_id public function route() { return $this->belongsTo(Route::class); } public function routeName(): Attribute { return new Attribute( get: fn () => $this->route->departure->name .' - ' . $this->route->destination->name ); } } |
Bus (Автобус)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class Bus extends Model { use HasFactory; protected $fillable = ['brand', 'number', 'driver', 'seats']; // Марка, номер, водитель, число мест public function busName(): Attribute { return new Attribute( get: fn () => $this->brand .' - ' . $this->number ); } } |
Trip (Рейс)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class Trip extends Model { use HasFactory; protected $fillable = [ 'departure_time', // Время отправления 'arrival_time', // Время прибытия 'route_id', // Маршрут_id 'bus_id', // Автобус_id 'ticket_price' // Цена билета ]; public function route() { return $this->belongsTo(Route::class); } public function bus() { return $this->belongsTo(Bus::class); } } |
Запускаем миграцию
1 |
php artisan migrate |
База данных создана. Приступим к её наполнению. Создадим интерфейс для ввода информации
Установка Filament 3
Устанавливаем конструктор панелей Filament, выполнив следующие команды в каталоге вашего проекта Laravel:
1 2 |
composer require filament/filament -W php artisan filament:install --panels |
Создайте пользователя
Вы можете создать новую учетную запись пользователя с помощью следующей команды:
1 |
php artisan make:filament-user |
Откройте /admin в своем веб-браузере, войдите в систему и начните создавать свое приложение!
Генератор ресурсов
Выполним в консоли следующие команды, которые создадут нам ресурсы для редактирования наших моделей
1 2 3 4 5 6 |
php artisan make:filament-resource Stop --generate --simple php artisan make:filament-resource Bus --generate --simple php artisan make:filament-resource Trip --generate --simple php artisan make:filament-resource Destination --generate --simple php artisan make:filament-resource Stop --generate --simple php artisan make:filament-resource Route --generate |
После создания ресурсов так будет выглядеть наше меню
Отредактируем сгенерированные ресурсы
В ресурсах автобусов все поля сгенерировались автоматически, их менять не нужно
Поменяем только название, иконку и группу ресурса
1 2 3 |
protected static ?string $navigationIcon = 'heroicon-c-truck'; protected static ?string $pluralModelLabel = 'Автобусы'; protected static ?string $navigationGroup = 'Справочники'; |
В пунктах назначения все поля сгенерировались автоматически, меняем только название и иконку
1 2 3 |
protected static ?string $navigationIcon = 'heroicon-s-building-storefront'; protected static ?string $pluralModelLabel = 'Пункты назаначения'; protected static ?string $navigationGroup = 'Справочники'; |
В таблицах маршрутов заменим сгенерированные столбцы на:
- TextColumn::make(‘departure.name’)
- TextColumn::make(‘destination.name’)
- TextColumn::make(‘schedule.days’)
В форме редактирования заменим сгенерированные поля на наши:
- Пункт отправления выбирается из справочника:
1 2 3 |
Forms\Components\Select::make('departure_id') ->relationship(name: 'destination', titleAttribute: 'name') ->required(), |
- Пункт назначения выбирается из справочника
1 2 3 |
Forms\Components\Select::make('destination_id') ->relationship(name: 'destination', titleAttribute: 'name') ->required(), |
- Дни движения можно будет не только выбрать из бд, но и добавить новую и отредактировать
1 2 3 4 5 6 7 |
Forms\Components\Select::make('schedule_id') ->relationship(name:'schedule',titleAttribute:'days') ->createOptionForm([ Forms\Components\TextInput::make('days') ->required(), ]) ->required(), |
- Ну и время в пути поменяем на поле выбора времени
1 2 |
Forms\Components\TimePicker::make('travel_time') ->required(), |
app\Filament\Resources\RouteResource.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 |
<?php namespace App\Filament\Resources; use App\Filament\Resources\RouteResource\Pages; use App\Filament\Resources\RouteResource\RelationManagers; use App\Models\Route; use Filament\Forms; use Filament\Forms\Form; use Filament\Resources\Resource; use Filament\Tables; use Filament\Tables\Table; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\SoftDeletingScope; class RouteResource extends Resource { protected static ?string $model = Route::class; protected static ?string $navigationIcon = 'heroicon-c-rectangle-group'; protected static ?string $pluralModelLabel = 'Маршруты'; public static function form(Form $form): Form { return $form ->schema([ Forms\Components\Select::make('departure_id') ->relationship(name: 'destination', titleAttribute: 'name') ->required(), Forms\Components\Select::make('destination_id') ->relationship(name: 'destination', titleAttribute: 'name') ->required(), Forms\Components\Select::make('schedule_id') ->relationship(name:'schedule',titleAttribute:'days') ->createOptionForm([ Forms\Components\TextInput::make('days') ->required(), ]) ->required(), Forms\Components\TimePicker::make('travel_time') ->required(), ]); } public static function table(Table $table): Table { return $table ->columns([ Tables\Columns\TextColumn::make('departure.name') ->sortable(), Tables\Columns\TextColumn::make('destination.name') ->sortable(), Tables\Columns\TextColumn::make('schedule.days') ->sortable(), Tables\Columns\TextColumn::make('travel_time'), Tables\Columns\TextColumn::make('created_at') ->dateTime() ->sortable() ->toggleable(isToggledHiddenByDefault: true), Tables\Columns\TextColumn::make('updated_at') ->dateTime() ->sortable() ->toggleable(isToggledHiddenByDefault: true), ]) ->filters([ // ]) ->actions([ Tables\Actions\EditAction::make(), ]) ->bulkActions([ Tables\Actions\BulkActionGroup::make([ Tables\Actions\DeleteBulkAction::make(), ]), ]); } public static function getRelations(): array { return [ // ]; } public static function getPages(): array { return [ 'index' => Pages\ListRoutes::route('/'), 'create' => Pages\CreateRoute::route('/create'), 'edit' => Pages\EditRoute::route('/{record}/edit'), ]; } } |
В ресурсах остановки в таблицах меняем поля route_id
с TextInput на Select
со связью relationship(name: 'route')
и отображением сборного имении через функцию getOptionLabelFromRecordUsing()
В табличном виде заменяем имя маршрута на TextColumn::make(‘routeName’)
app\Filament\Resources\StopResource.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
<?php namespace App\Filament\Resources; use App\Filament\Resources\StopResource\Pages; use App\Filament\Resources\StopResource\RelationManagers; use App\Models\Route; use App\Models\Stop; use Filament\Forms; use Filament\Forms\Form; use Filament\Resources\Resource; use Filament\Tables; use Filament\Tables\Table; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\SoftDeletingScope; class StopResource extends Resource { protected static ?string $model = Stop::class; protected static ?string $navigationIcon = 'heroicon-c-building-office'; protected static ?string $pluralModelLabel = 'Остановки'; protected static ?string $navigationGroup = 'Справочники'; public static function form(Form $form): Form { return $form ->schema([ Forms\Components\TextInput::make('name') ->required() ->maxLength(255), Forms\Components\Select::make('route_id') ->required() ->relationship(name: 'route') ->getOptionLabelFromRecordUsing(fn (Route $record) => "{$record->departure->name} - {$record->destination->name}"), ]); } public static function table(Table $table): Table { return $table ->columns([ Tables\Columns\TextColumn::make('name') ->searchable(), Tables\Columns\TextColumn::make('routeName') ->sortable(), Tables\Columns\TextColumn::make('created_at') ->dateTime() ->sortable() ->toggleable(isToggledHiddenByDefault: true), Tables\Columns\TextColumn::make('updated_at') ->dateTime() ->sortable() ->toggleable(isToggledHiddenByDefault: true), ]) ->filters([ // ]) ->actions([ Tables\Actions\EditAction::make(), Tables\Actions\DeleteAction::make(), ]) ->bulkActions([ Tables\Actions\BulkActionGroup::make([ Tables\Actions\DeleteBulkAction::make(), ]), ]); } public static function getPages(): array { return [ 'index' => Pages\ManageStops::route('/'), ]; } } |
В рейсах мы добавили вывод добавленного атрибута в моделях. На форме есть реактивное обновление полей дата прибытия меняется в зависимости от маршрута и времени выезда. Так же были изменены обычные textinput на select со связью с объектами
app\Filament\Resources\TripResource.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 |
<?php namespace App\Filament\Resources; use App\Filament\Resources\TripResource\Pages; use App\Filament\Resources\TripResource\RelationManagers; use App\Models\Bus; use App\Models\Route; use App\Models\Trip; use Filament\Forms; use Filament\Forms\Form; use Filament\Forms\Get; use Filament\Forms\Set; use Filament\Resources\Resource; use Filament\Tables; use Filament\Tables\Table; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\SoftDeletingScope; use Illuminate\Support\Carbon; class TripResource extends Resource { protected static ?string $model = Trip::class; protected static ?string $navigationIcon = 'heroicon-c-arrow-up-on-square-stack'; protected static ?string $pluralModelLabel = 'Рейсы'; public static function form(Form $form): Form { return $form ->schema([ Forms\Components\Select::make('route_id') ->relationship(name:'route') ->live() ->getOptionLabelFromRecordUsing(fn (Route $record) => "{$record->departure->name} - {$record->destination->name}") ->afterStateUpdated(function ($state, Get $get, Set $set) { if(is_null($get('departure_time'))) return; $carbonTime1 = Carbon::createFromFormat('H:i:s', $get('departure_time')); $carbonTime2 = Carbon::createFromFormat('H:i:s', Route::find($state)?->travel_time ?? '00:00:00'); $resultTime = $carbonTime1->copy()->addHours($carbonTime2->hour) ->addMinutes($carbonTime2->minute) ->addSeconds($carbonTime2->second); $set('arrival_time', $resultTime->format('H:i:s')); }) ->required(), Forms\Components\Select::make('bus_id') ->getOptionLabelFromRecordUsing(fn (Bus $record) => "{$record->brand} ({$record->number})") ->relationship(name:'bus') ->required(), Forms\Components\TimePicker::make('departure_time') ->live(onBlur: true) ->afterStateUpdated(function ($state, Get $get, Set $set) { $carbonTime1 = Carbon::createFromFormat('H:i:s', $state); $carbonTime2 = Carbon::createFromFormat('H:i:s', Route::find($get('route_id'))?->travel_time ?? '00:00:00'); $resultTime = $carbonTime1->copy()->addHours($carbonTime2->hour) ->addMinutes($carbonTime2->minute) ->addSeconds($carbonTime2->second); $set('arrival_time', $resultTime->format('H:i:s')); }) ->required(), Forms\Components\TimePicker::make('arrival_time') ->readOnly() ->required(), Forms\Components\TextInput::make('ticket_price') ->required() ->numeric(), ]); } public static function table(Table $table): Table { return $table ->columns([ Tables\Columns\TextColumn::make('departure_time'), Tables\Columns\TextColumn::make('arrival_time'), Tables\Columns\TextColumn::make('route.routeName') ->sortable(), Tables\Columns\TextColumn::make('bus.busName') ->numeric() ->sortable(), Tables\Columns\TextColumn::make('ticket_price') ->numeric() ->sortable(), Tables\Columns\TextColumn::make('created_at') ->dateTime() ->sortable() ->toggleable(isToggledHiddenByDefault: true), Tables\Columns\TextColumn::make('updated_at') ->dateTime() ->sortable() ->toggleable(isToggledHiddenByDefault: true), ]) ->filters([ // ]) ->actions([ Tables\Actions\EditAction::make(), Tables\Actions\DeleteAction::make(), ]) ->bulkActions([ Tables\Actions\BulkActionGroup::make([ Tables\Actions\DeleteBulkAction::make(), ]), ]); } public static function getPages(): array { return [ 'index' => Pages\ManageTrips::route('/'), ]; } } |
Настройка Dashboard
Создадим числовой виджет
Командой создадим виджет статистики
1 |
php artisan make:filament-widget StatsOverview --stats-overview |
в созданном файле добавим
1 2 3 4 5 6 7 8 9 |
protected function getStats(): array { return [ Stat::make('Всего маршрутов', Route::query()->count()), Stat::make('Всего рейсов', Trip::query()->count()), Stat::make('Всего автобусов', Bus::query()->count()), Stat::make('Всего пунктов назначения', Destination::query()->count()), ]; } |
Отчет Маршруты и Рейсы
Добавим кнопку выгрузить отчет в ресурс маршруты
Установим через composer библиотеку для формирования pdf отчетов — barryvdh / laravel-dompdf
1 |
composer require barryvdh/laravel-dompdf |
Создадим blade шаблон для нашего отчета
resources\views\tripreport.blade.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 |
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Отчет по поездке</title> <style type="text/css"> * { /*font-family: Helvetica, sans-serif;*/ font-family: "DejaVu Sans", sans-serif; } .table-container { overflow: hidden; /* Скрывает переполнение */ border-radius: 10px; /* Закругление углов контейнера */ border: 1px solid #ccc; /* Рамка вокруг таблицы */ } table { width: 100%; border-collapse: separate; /* Разделение границ ячеек */ border-spacing: 0; /* Убирает промежутки между ячейками */ } th, td { padding: 15px; text-align: left; border-bottom: 1px solid #ddd; /* Граница между строками */ } th { background-color: #f2f2f2; /* Цвет фона заголовка */ } tr:hover { background-color: #f5f5f5; /* Цвет фона при наведении на строку */ } /* Закругление углов для первой и последней строки */ thead th:first-child { border-top-left-radius: 10px; /* Закругление верхнего левого угла */ } thead th:last-child { border-top-right-radius: 10px; /* Закругление верхнего правого угла */ } tbody tr:last-child td:first-child { border-bottom-left-radius: 10px; /* Закругление нижнего левого угла */ } tbody tr:last-child td:last-child { border-bottom-right-radius: 10px; /* Закругление нижнего правого угла */ } </style> </head> <body> <div style="text-align: center;"> <h2>Маршруты и рейсы</h2> </div> <div class="table-container"> <table> <thead> <tr> <th>Время отправления</th> <th>Пункт отправления - Пункт назначения</th> <th>Дни движения</th> <th>Время прибытия</th> <th>Время в пути</th> <th>Автобус</th> <th>Цена билета</th> </tr> </thead> <tbody> @foreach($tripData as $trip) <tr> <td>{{ \Carbon\Carbon::parse($trip->departure_time)->format('H:i') ?? 'Нет данных' }}</td> <td>{{ $trip->route->routeName ?? 'Нет данных' }}</td> <td>{{ $trip->route->schedule->days ?? 'Нет данных' }}</td> <td>{{ \Carbon\Carbon::parse($trip->arrival_time)->format('H:i') ?? 'Нет данных' }}</td> <td>{{ \Carbon\Carbon::parse($trip->route->travel_time)->format('H:i') ?? 'Нет данных' }}</td> <td>{{ $trip->bus->busName ?? 'Нет данных' }}</td> <td>{{ $trip->ticket_price.'р.' ?? 'Нет данных' }}</td> </tr> @endforeach </tbody> </table> </div> </body> </html> |
Теперь можно добавить кнопку и действие для выгрузки отчета. В файле app\Filament\Resources\TripResource\Pages\ManageTrips.php
добавим новый Action, который запросит все рейсы и передаст их в наш шаблон
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
Actions\Action::make('Отчет_маршруты_и_рейсы') ->action(function () { $trip = Trip::with(['bus', 'route','route.schedule'])->get(); return response()->streamDownload(function () use ($trip) { echo Pdf::loadHtml( Blade::render('tripreport', ['tripData'=>$trip]) ) ->setPaper('a4', 'landscape') ->stream(); }, "report112" . '.pdf'); Notification::make() ->title('Отчет успешно сохранен') ->icon('heroicon-o-document-text') ->success() ->send(); }) |
После нажатия кнопки будет сохранен документ pdf и выйдет уведомление.
Вот пример отчета:
Заключение
Созданный проект позволяет вносить данные в базу данных и сохранять отчет в PDF. Это приложение было создано в ознакомительных целях, для изучения возможностей Laravel и Filament 3.