mirror of
https://github.com/pacnpal/thrillwiki_laravel.git
synced 2025-12-20 09:51:10 -05:00
feat: Implement rides management with CRUD functionality
- Added rides index view with search and filter options. - Created rides show view to display ride details. - Implemented API routes for rides. - Developed authentication routes for user registration, login, and email verification. - Created tests for authentication, email verification, password reset, and user profile management. - Added feature tests for rides and operators, including creation, updating, deletion, and searching. - Implemented soft deletes and caching for rides and operators. - Enhanced manufacturer and operator model tests for various functionalities.
This commit is contained in:
1163
app/Console/Commands/MakeThrillWikiCrud.php
Normal file
1163
app/Console/Commands/MakeThrillWikiCrud.php
Normal file
File diff suppressed because it is too large
Load Diff
392
app/Console/Commands/MakeThrillWikiLivewire.php
Normal file
392
app/Console/Commands/MakeThrillWikiLivewire.php
Normal file
@@ -0,0 +1,392 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Filesystem\Filesystem;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class MakeThrillWikiLivewire extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*/
|
||||
protected $signature = 'make:thrillwiki-livewire {name : The name of the component}
|
||||
{--reusable : Generate a reusable component with optimization traits}
|
||||
{--with-tests : Generate test files for the component}
|
||||
{--cached : Add caching optimization to the component}
|
||||
{--paginated : Add pagination support to the component}
|
||||
{--force : Overwrite existing files}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*/
|
||||
protected $description = 'Create a ThrillWiki-optimized Livewire component with built-in patterns and performance optimization';
|
||||
|
||||
protected Filesystem $files;
|
||||
|
||||
public function __construct(Filesystem $files)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->files = $files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$name = $this->argument('name');
|
||||
$className = Str::studly($name);
|
||||
$kebabName = Str::kebab($name);
|
||||
|
||||
$this->info("🚀 Generating ThrillWiki Livewire Component: {$className}");
|
||||
|
||||
// Generate the component class
|
||||
$this->generateComponent($className, $kebabName);
|
||||
|
||||
// Generate the view file
|
||||
$this->generateView($className, $kebabName);
|
||||
|
||||
// Generate tests if requested
|
||||
if ($this->option('with-tests')) {
|
||||
$this->generateTest($className);
|
||||
}
|
||||
|
||||
$this->displaySummary($className, $kebabName);
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
protected function generateComponent(string $className, string $kebabName): void
|
||||
{
|
||||
$componentPath = app_path("Livewire/{$className}.php");
|
||||
|
||||
if ($this->files->exists($componentPath) && !$this->option('force')) {
|
||||
$this->error("Component {$className} already exists! Use --force to overwrite.");
|
||||
return;
|
||||
}
|
||||
|
||||
$stub = $this->getComponentStub();
|
||||
$content = $this->replaceStubPlaceholders($stub, $className, $kebabName);
|
||||
|
||||
$this->files->ensureDirectoryExists(dirname($componentPath));
|
||||
$this->files->put($componentPath, $content);
|
||||
|
||||
$this->info("✅ Component created: app/Livewire/{$className}.php");
|
||||
}
|
||||
|
||||
protected function generateView(string $className, string $kebabName): void
|
||||
{
|
||||
$viewPath = resource_path("views/livewire/{$kebabName}.blade.php");
|
||||
|
||||
if ($this->files->exists($viewPath) && !$this->option('force')) {
|
||||
$this->error("View {$kebabName}.blade.php already exists! Use --force to overwrite.");
|
||||
return;
|
||||
}
|
||||
|
||||
$stub = $this->getViewStub();
|
||||
$content = $this->replaceViewPlaceholders($stub, $className, $kebabName);
|
||||
|
||||
$this->files->ensureDirectoryExists(dirname($viewPath));
|
||||
$this->files->put($viewPath, $content);
|
||||
|
||||
$this->info("✅ View created: resources/views/livewire/{$kebabName}.blade.php");
|
||||
}
|
||||
|
||||
protected function generateTest(string $className): void
|
||||
{
|
||||
$testPath = base_path("tests/Feature/Livewire/{$className}Test.php");
|
||||
|
||||
if ($this->files->exists($testPath) && !$this->option('force')) {
|
||||
$this->error("Test {$className}Test already exists! Use --force to overwrite.");
|
||||
return;
|
||||
}
|
||||
|
||||
$stub = $this->getTestStub();
|
||||
$content = $this->replaceTestPlaceholders($stub, $className);
|
||||
|
||||
$this->files->ensureDirectoryExists(dirname($testPath));
|
||||
$this->files->put($testPath, $content);
|
||||
|
||||
$this->info("✅ Test created: tests/Feature/Livewire/{$className}Test.php");
|
||||
}
|
||||
|
||||
protected function getComponentStub(): string
|
||||
{
|
||||
$traits = [];
|
||||
$imports = ['use Livewire\Component;'];
|
||||
$properties = [];
|
||||
$methods = [];
|
||||
|
||||
// Add pagination if requested
|
||||
if ($this->option('paginated')) {
|
||||
$imports[] = 'use Livewire\WithPagination;';
|
||||
$traits[] = 'WithPagination';
|
||||
$properties[] = ' protected $paginationTheme = \'tailwind\';';
|
||||
}
|
||||
|
||||
// Add caching optimization if requested
|
||||
if ($this->option('cached') || $this->option('reusable')) {
|
||||
$imports[] = 'use Illuminate\Support\Facades\Cache;';
|
||||
$methods[] = $this->getCachingMethods();
|
||||
}
|
||||
|
||||
// Build traits string
|
||||
$traitsString = empty($traits) ? '' : "\n use " . implode(', ', $traits) . ";\n";
|
||||
|
||||
// Build properties string
|
||||
$propertiesString = empty($properties) ? '' : "\n" . implode("\n", $properties) . "\n";
|
||||
|
||||
// Build methods string
|
||||
$methodsString = implode("\n\n", $methods);
|
||||
|
||||
return <<<PHP
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
{IMPORTS}
|
||||
|
||||
class {CLASS_NAME} extends Component
|
||||
{{TRAITS}{PROPERTIES}
|
||||
/**
|
||||
* Component initialization
|
||||
*/
|
||||
public function mount(): void
|
||||
{
|
||||
// Initialize component state
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the component
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.{VIEW_NAME}');
|
||||
}{METHODS}
|
||||
}
|
||||
PHP;
|
||||
}
|
||||
|
||||
protected function getViewStub(): string
|
||||
{
|
||||
if ($this->option('reusable')) {
|
||||
return <<<BLADE
|
||||
{{-- ThrillWiki Reusable Component: {CLASS_NAME} --}}
|
||||
<div class="thrillwiki-component"
|
||||
x-data="{ loading: false }"
|
||||
wire:loading.class="opacity-50">
|
||||
|
||||
{{-- Component Header --}}
|
||||
<div class="component-header mb-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{CLASS_NAME}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
{{-- Component Content --}}
|
||||
<div class="component-content">
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
ThrillWiki component content goes here.
|
||||
</p>
|
||||
|
||||
{{-- Example interactive element --}}
|
||||
<button wire:click="\$refresh"
|
||||
class="mt-4 px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors">
|
||||
Refresh Component
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{-- Loading State --}}
|
||||
<div wire:loading wire:target="\$refresh"
|
||||
class="absolute inset-0 bg-white bg-opacity-75 flex items-center justify-center">
|
||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
||||
</div>
|
||||
</div>
|
||||
BLADE;
|
||||
}
|
||||
|
||||
return <<<BLADE
|
||||
{{-- ThrillWiki Component: {CLASS_NAME} --}}
|
||||
<div class="thrillwiki-component">
|
||||
<h3 class="text-lg font-semibold mb-4 text-gray-900 dark:text-white">
|
||||
{CLASS_NAME}
|
||||
</h3>
|
||||
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
ThrillWiki component content goes here.
|
||||
</p>
|
||||
</div>
|
||||
BLADE;
|
||||
}
|
||||
|
||||
protected function getTestStub(): string
|
||||
{
|
||||
return <<<PHP
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Livewire;
|
||||
|
||||
use App\Livewire\{CLASS_NAME};
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
use Tests\TestCase;
|
||||
|
||||
class {CLASS_NAME}Test extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
/** @test */
|
||||
public function component_can_render(): void
|
||||
{
|
||||
Livewire::test({CLASS_NAME}::class)
|
||||
->assertStatus(200)
|
||||
->assertSee('{CLASS_NAME}');
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function component_can_mount_successfully(): void
|
||||
{
|
||||
Livewire::test({CLASS_NAME}::class)
|
||||
->assertStatus(200);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function component_follows_thrillwiki_patterns(): void
|
||||
{
|
||||
Livewire::test({CLASS_NAME}::class)
|
||||
->assertViewIs('livewire.{VIEW_NAME}');
|
||||
}
|
||||
}
|
||||
PHP;
|
||||
}
|
||||
|
||||
protected function getCachingMethods(): string
|
||||
{
|
||||
return <<<PHP
|
||||
/**
|
||||
* Get cache key for this component
|
||||
*/
|
||||
protected function getCacheKey(string \$suffix = ''): string
|
||||
{
|
||||
return 'thrillwiki.' . class_basename(static::class) . '.' . \$suffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remember data with caching
|
||||
*/
|
||||
protected function remember(string \$key, \$callback, int \$ttl = 3600)
|
||||
{
|
||||
return Cache::remember(\$this->getCacheKey(\$key), \$ttl, \$callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate component cache
|
||||
*/
|
||||
protected function invalidateCache(string \$key = null): void
|
||||
{
|
||||
if (\$key) {
|
||||
Cache::forget(\$this->getCacheKey(\$key));
|
||||
} else {
|
||||
// Clear all cache for this component
|
||||
Cache::flush();
|
||||
}
|
||||
}
|
||||
PHP;
|
||||
}
|
||||
|
||||
protected function replaceStubPlaceholders(string $stub, string $className, string $kebabName): string
|
||||
{
|
||||
$imports = ['use Livewire\Component;'];
|
||||
$traits = [];
|
||||
|
||||
if ($this->option('paginated')) {
|
||||
$imports[] = 'use Livewire\WithPagination;';
|
||||
$traits[] = 'WithPagination';
|
||||
}
|
||||
|
||||
if ($this->option('cached') || $this->option('reusable')) {
|
||||
$imports[] = 'use Illuminate\Support\Facades\Cache;';
|
||||
}
|
||||
|
||||
$traitsString = empty($traits) ? '' : "\n use " . implode(', ', $traits) . ";\n";
|
||||
$importsString = implode("\n", $imports);
|
||||
$methodsString = '';
|
||||
|
||||
if ($this->option('cached') || $this->option('reusable')) {
|
||||
$methodsString = "\n\n" . $this->getCachingMethods();
|
||||
}
|
||||
|
||||
return str_replace(
|
||||
['{IMPORTS}', '{CLASS_NAME}', '{VIEW_NAME}', '{TRAITS}', '{PROPERTIES}', '{METHODS}'],
|
||||
[$importsString, $className, $kebabName, $traitsString, '', $methodsString],
|
||||
$stub
|
||||
);
|
||||
}
|
||||
|
||||
protected function replaceViewPlaceholders(string $stub, string $className, string $kebabName): string
|
||||
{
|
||||
return str_replace(
|
||||
['{CLASS_NAME}', '{VIEW_NAME}'],
|
||||
[$className, $kebabName],
|
||||
$stub
|
||||
);
|
||||
}
|
||||
|
||||
protected function replaceTestPlaceholders(string $stub, string $className): string
|
||||
{
|
||||
return str_replace(
|
||||
['{CLASS_NAME}', '{VIEW_NAME}'],
|
||||
[$className, Str::kebab($className)],
|
||||
$stub
|
||||
);
|
||||
}
|
||||
|
||||
protected function displaySummary(string $className, string $kebabName): void
|
||||
{
|
||||
$this->newLine();
|
||||
$this->info("🎉 ThrillWiki Livewire Component '{$className}' created successfully!");
|
||||
$this->newLine();
|
||||
|
||||
$this->comment("📁 Files Generated:");
|
||||
$this->line(" • app/Livewire/{$className}.php");
|
||||
$this->line(" • resources/views/livewire/{$kebabName}.blade.php");
|
||||
|
||||
if ($this->option('with-tests')) {
|
||||
$this->line(" • tests/Feature/Livewire/{$className}Test.php");
|
||||
}
|
||||
|
||||
$this->newLine();
|
||||
$this->comment("🚀 Features Added:");
|
||||
|
||||
if ($this->option('reusable')) {
|
||||
$this->line(" • Reusable component patterns with optimization traits");
|
||||
}
|
||||
|
||||
if ($this->option('cached')) {
|
||||
$this->line(" • Caching optimization methods");
|
||||
}
|
||||
|
||||
if ($this->option('paginated')) {
|
||||
$this->line(" • Pagination support with Tailwind theme");
|
||||
}
|
||||
|
||||
if ($this->option('with-tests')) {
|
||||
$this->line(" • Automated test suite with ThrillWiki patterns");
|
||||
}
|
||||
|
||||
$this->newLine();
|
||||
$this->comment("📝 Next Steps:");
|
||||
$this->line(" 1. Customize the component logic in app/Livewire/{$className}.php");
|
||||
$this->line(" 2. Update the view template in resources/views/livewire/{$kebabName}.blade.php");
|
||||
$this->line(" 3. Include the component in your templates with <livewire:{$kebabName} />");
|
||||
|
||||
if ($this->option('with-tests')) {
|
||||
$this->line(" 4. Run tests with: php artisan test --filter {$className}Test");
|
||||
}
|
||||
|
||||
$this->newLine();
|
||||
$this->info("✨ Happy coding with ThrillWiki acceleration patterns!");
|
||||
}
|
||||
}
|
||||
857
app/Console/Commands/MakeThrillWikiModel.php
Normal file
857
app/Console/Commands/MakeThrillWikiModel.php
Normal file
@@ -0,0 +1,857 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
||||
class MakeThrillWikiModel extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'make:thrillwiki-model {name : The name of the model}
|
||||
{--migration : Generate a migration file}
|
||||
{--factory : Generate a model factory}
|
||||
{--with-relationships : Include common ThrillWiki relationships}
|
||||
{--cached : Add caching traits and methods}
|
||||
{--api-resource : Generate API resource class}
|
||||
{--with-tests : Generate model tests}
|
||||
{--force : Overwrite existing files}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Generate a ThrillWiki model with optimized patterns, traits, and optional related files';
|
||||
|
||||
/**
|
||||
* ThrillWiki traits for different model types
|
||||
*/
|
||||
protected array $thrillWikiTraits = [
|
||||
'HasLocation' => 'App\\Traits\\HasLocation',
|
||||
'HasSlugHistory' => 'App\\Traits\\HasSlugHistory',
|
||||
'HasStatistics' => 'App\\Traits\\HasStatistics',
|
||||
'HasCaching' => 'App\\Traits\\HasCaching',
|
||||
'HasSoftDeletes' => 'Illuminate\\Database\\Eloquent\\SoftDeletes',
|
||||
'HasFactory' => 'Illuminate\\Database\\Eloquent\\Factories\\HasFactory',
|
||||
];
|
||||
|
||||
/**
|
||||
* Common ThrillWiki relationships by model type
|
||||
*/
|
||||
protected array $relationshipPatterns = [
|
||||
'Park' => [
|
||||
'areas' => 'hasMany:ParkArea',
|
||||
'rides' => 'hasManyThrough:Ride,ParkArea',
|
||||
'operator' => 'belongsTo:Operator',
|
||||
'photos' => 'morphMany:Photo',
|
||||
'reviews' => 'morphMany:Review',
|
||||
],
|
||||
'Ride' => [
|
||||
'park' => 'belongsTo:Park',
|
||||
'area' => 'belongsTo:ParkArea',
|
||||
'manufacturer' => 'belongsTo:Manufacturer',
|
||||
'designer' => 'belongsTo:Designer',
|
||||
'photos' => 'morphMany:Photo',
|
||||
'reviews' => 'morphMany:Review',
|
||||
],
|
||||
'Operator' => [
|
||||
'parks' => 'hasMany:Park',
|
||||
],
|
||||
'Manufacturer' => [
|
||||
'rides' => 'hasMany:Ride,manufacturer_id',
|
||||
],
|
||||
'Designer' => [
|
||||
'rides' => 'hasMany:Ride,designer_id',
|
||||
],
|
||||
'Review' => [
|
||||
'user' => 'belongsTo:User',
|
||||
'reviewable' => 'morphTo',
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$this->info('🚀 Generating ThrillWiki Model for: ' . $this->argument('name'));
|
||||
|
||||
$name = $this->argument('name');
|
||||
$className = Str::studly($name);
|
||||
$tableName = Str::snake(Str::plural($name));
|
||||
|
||||
// Generate model
|
||||
$this->generateModel($className);
|
||||
|
||||
// Generate optional files
|
||||
if ($this->option('migration')) {
|
||||
$this->generateMigration($className, $tableName);
|
||||
}
|
||||
|
||||
if ($this->option('factory')) {
|
||||
$this->generateFactory($className);
|
||||
}
|
||||
|
||||
if ($this->option('api-resource')) {
|
||||
$this->generateApiResource($className);
|
||||
}
|
||||
|
||||
if ($this->option('with-tests')) {
|
||||
$this->generateTests($className);
|
||||
}
|
||||
|
||||
$this->displaySummary($className);
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the model file
|
||||
*/
|
||||
protected function generateModel(string $className): void
|
||||
{
|
||||
$modelPath = app_path("Models/{$className}.php");
|
||||
|
||||
if (File::exists($modelPath) && !$this->option('force')) {
|
||||
$this->error("Model {$className} already exists! Use --force to overwrite.");
|
||||
return;
|
||||
}
|
||||
|
||||
$modelContent = $this->buildModelContent($className);
|
||||
|
||||
$this->ensureDirectoryExists(dirname($modelPath));
|
||||
File::put($modelPath, $modelContent);
|
||||
|
||||
$this->line("✅ Model created: app/Models/{$className}.php");
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the model content with ThrillWiki patterns
|
||||
*/
|
||||
protected function buildModelContent(string $className): string
|
||||
{
|
||||
$tableName = Str::snake(Str::plural($className));
|
||||
$traits = $this->getTraitsForModel($className);
|
||||
$relationships = $this->getRelationshipsForModel($className);
|
||||
$cachingMethods = $this->option('cached') ? $this->getCachingMethods($className) : '';
|
||||
|
||||
$stub = <<<'PHP'
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
{TRAIT_IMPORTS}
|
||||
|
||||
/**
|
||||
* {CLASS_NAME} Model
|
||||
*
|
||||
* Generated by ThrillWiki Model Generator
|
||||
* Includes ThrillWiki optimization patterns and performance enhancements
|
||||
*/
|
||||
class {CLASS_NAME} extends Model
|
||||
{
|
||||
{TRAITS}
|
||||
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = '{TABLE_NAME}';
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'description',
|
||||
'is_active',
|
||||
// Add more fillable attributes as needed
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be cast.
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
protected $casts = [
|
||||
'is_active' => 'boolean',
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
// Add more casts as needed
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be hidden for arrays.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $hidden = [
|
||||
// Add hidden attributes if needed
|
||||
];
|
||||
|
||||
// Query Scopes
|
||||
|
||||
/**
|
||||
* Scope a query to only include active records.
|
||||
*/
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('is_active', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope for optimized queries with common relationships.
|
||||
*/
|
||||
public function scopeOptimized($query)
|
||||
{
|
||||
return $query->with($this->getOptimizedRelations());
|
||||
}
|
||||
|
||||
// ThrillWiki Methods
|
||||
|
||||
/**
|
||||
* Get optimized relations for this model.
|
||||
*/
|
||||
public function getOptimizedRelations(): array
|
||||
{
|
||||
return [
|
||||
// Define common relationships to eager load
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache key for this model instance.
|
||||
*/
|
||||
public function getCacheKey(string $suffix = ''): string
|
||||
{
|
||||
$key = strtolower(class_basename($this)) . '.' . $this->id;
|
||||
return $suffix ? $key . '.' . $suffix : $key;
|
||||
}
|
||||
|
||||
{RELATIONSHIPS}
|
||||
|
||||
{CACHING_METHODS}
|
||||
}
|
||||
PHP;
|
||||
|
||||
return str_replace([
|
||||
'{CLASS_NAME}',
|
||||
'{TABLE_NAME}',
|
||||
'{TRAIT_IMPORTS}',
|
||||
'{TRAITS}',
|
||||
'{RELATIONSHIPS}',
|
||||
'{CACHING_METHODS}',
|
||||
], [
|
||||
$className,
|
||||
$tableName,
|
||||
$this->buildTraitImports($traits),
|
||||
$this->buildTraitUses($traits),
|
||||
$relationships,
|
||||
$cachingMethods,
|
||||
], $stub);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get traits for the model based on options and model type
|
||||
*/
|
||||
protected function getTraitsForModel(string $className): array
|
||||
{
|
||||
$traits = ['HasFactory']; // Always include HasFactory
|
||||
|
||||
// Add SoftDeletes for most models
|
||||
$traits[] = 'HasSoftDeletes';
|
||||
|
||||
// Add caching if requested
|
||||
if ($this->option('cached')) {
|
||||
$traits[] = 'HasCaching';
|
||||
}
|
||||
|
||||
// Add location trait for location-based models
|
||||
if (in_array($className, ['Park', 'Company', 'ParkArea'])) {
|
||||
$traits[] = 'HasLocation';
|
||||
}
|
||||
|
||||
// Add slug history for main entities
|
||||
if (in_array($className, ['Park', 'Ride', 'Company', 'Designer'])) {
|
||||
$traits[] = 'HasSlugHistory';
|
||||
}
|
||||
|
||||
// Add statistics for countable entities
|
||||
if (in_array($className, ['Park', 'Ride', 'User'])) {
|
||||
$traits[] = 'HasStatistics';
|
||||
}
|
||||
|
||||
return $traits;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build trait import statements
|
||||
*/
|
||||
protected function buildTraitImports(array $traits): string
|
||||
{
|
||||
$imports = [];
|
||||
foreach ($traits as $trait) {
|
||||
if (isset($this->thrillWikiTraits[$trait])) {
|
||||
$imports[] = "use {$this->thrillWikiTraits[$trait]};";
|
||||
}
|
||||
}
|
||||
return implode("\n", $imports);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build trait use statements
|
||||
*/
|
||||
protected function buildTraitUses(array $traits): string
|
||||
{
|
||||
$uses = array_map(function($trait) {
|
||||
return " use {$trait};";
|
||||
}, $traits);
|
||||
|
||||
return implode("\n", $uses);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get relationships for the model
|
||||
*/
|
||||
protected function getRelationshipsForModel(string $className): string
|
||||
{
|
||||
if (!$this->option('with-relationships')) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!isset($this->relationshipPatterns[$className])) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$relationships = [];
|
||||
foreach ($this->relationshipPatterns[$className] as $method => $definition) {
|
||||
$relationships[] = $this->buildRelationshipMethod($method, $definition);
|
||||
}
|
||||
|
||||
return "\n // Relationships\n\n" . implode("\n\n", $relationships);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a relationship method
|
||||
*/
|
||||
protected function buildRelationshipMethod(string $method, string $definition): string
|
||||
{
|
||||
[$type, $model, $foreignKey] = array_pad(explode(',', str_replace(':', ',', $definition)), 3, null);
|
||||
|
||||
$methodBody = match($type) {
|
||||
'hasMany' => $foreignKey ?
|
||||
"return \$this->hasMany({$model}::class, '{$foreignKey}');" :
|
||||
"return \$this->hasMany({$model}::class);",
|
||||
'belongsTo' => $foreignKey ?
|
||||
"return \$this->belongsTo({$model}::class, '{$foreignKey}');" :
|
||||
"return \$this->belongsTo({$model}::class);",
|
||||
'hasManyThrough' => "return \$this->hasManyThrough({$model}::class, {$foreignKey}::class);",
|
||||
'morphMany' => "return \$this->morphMany({$model}::class, 'morphable');",
|
||||
'morphTo' => "return \$this->morphTo();",
|
||||
default => "return \$this->{$type}({$model}::class);"
|
||||
};
|
||||
|
||||
return " /**\n * Get the {$method} relationship.\n */\n public function {$method}()\n {\n {$methodBody}\n }";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get caching methods for the model
|
||||
*/
|
||||
protected function getCachingMethods(string $className): string
|
||||
{
|
||||
return <<<'PHP'
|
||||
|
||||
// Caching Methods
|
||||
|
||||
/**
|
||||
* Remember a value in cache with model-specific key.
|
||||
*/
|
||||
public function remember(string $key, $callback, int $ttl = 3600)
|
||||
{
|
||||
return cache()->remember($this->getCacheKey($key), $ttl, $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate cache for this model.
|
||||
*/
|
||||
public function invalidateCache(string $key = null): void
|
||||
{
|
||||
if ($key) {
|
||||
cache()->forget($this->getCacheKey($key));
|
||||
} else {
|
||||
// Clear all cache keys for this model
|
||||
cache()->forget($this->getCacheKey());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Boot method to handle cache invalidation.
|
||||
*/
|
||||
protected static function boot()
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
static::saved(function ($model) {
|
||||
$model->invalidateCache();
|
||||
});
|
||||
|
||||
static::deleted(function ($model) {
|
||||
$model->invalidateCache();
|
||||
});
|
||||
}
|
||||
PHP;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate migration file
|
||||
*/
|
||||
protected function generateMigration(string $className, string $tableName): void
|
||||
{
|
||||
$migrationName = 'create_' . $tableName . '_table';
|
||||
$timestamp = date('Y_m_d_His');
|
||||
$migrationFile = database_path("migrations/{$timestamp}_{$migrationName}.php");
|
||||
|
||||
$migrationContent = $this->buildMigrationContent($className, $tableName, $migrationName);
|
||||
|
||||
File::put($migrationFile, $migrationContent);
|
||||
$this->line("✅ Migration created: database/migrations/{$timestamp}_{$migrationName}.php");
|
||||
}
|
||||
|
||||
/**
|
||||
* Build migration content
|
||||
*/
|
||||
protected function buildMigrationContent(string $className, string $tableName, string $migrationName): string
|
||||
{
|
||||
$migrationClass = Str::studly($migrationName);
|
||||
|
||||
$stub = <<<'PHP'
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('{TABLE_NAME}', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->text('description')->nullable();
|
||||
$table->boolean('is_active')->default(true);
|
||||
|
||||
// Add common ThrillWiki fields
|
||||
$table->string('slug')->unique();
|
||||
|
||||
// Add indexes for performance
|
||||
$table->index(['is_active']);
|
||||
$table->index(['name']);
|
||||
$table->index(['slug']);
|
||||
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('{TABLE_NAME}');
|
||||
}
|
||||
};
|
||||
PHP;
|
||||
|
||||
return str_replace('{TABLE_NAME}', $tableName, $stub);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate factory file
|
||||
*/
|
||||
protected function generateFactory(string $className): void
|
||||
{
|
||||
$factoryPath = database_path("factories/{$className}Factory.php");
|
||||
|
||||
if (File::exists($factoryPath) && !$this->option('force')) {
|
||||
$this->error("Factory {$className}Factory already exists! Use --force to overwrite.");
|
||||
return;
|
||||
}
|
||||
|
||||
$factoryContent = $this->buildFactoryContent($className);
|
||||
|
||||
$this->ensureDirectoryExists(dirname($factoryPath));
|
||||
File::put($factoryPath, $factoryContent);
|
||||
|
||||
$this->line("✅ Factory created: database/factories/{$className}Factory.php");
|
||||
}
|
||||
|
||||
/**
|
||||
* Build factory content
|
||||
*/
|
||||
protected function buildFactoryContent(string $className): string
|
||||
{
|
||||
$stub = <<<'PHP'
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\{CLASS_NAME};
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\{CLASS_NAME}>
|
||||
*/
|
||||
class {CLASS_NAME}Factory extends Factory
|
||||
{
|
||||
/**
|
||||
* The name of the factory's corresponding model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $model = {CLASS_NAME}::class;
|
||||
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
$name = $this->faker->unique()->words(2, true);
|
||||
|
||||
return [
|
||||
'name' => $name,
|
||||
'slug' => Str::slug($name),
|
||||
'description' => $this->faker->paragraphs(2, true),
|
||||
'is_active' => $this->faker->boolean(90), // 90% chance of being active
|
||||
'created_at' => $this->faker->dateTimeBetween('-1 year', 'now'),
|
||||
'updated_at' => function (array $attributes) {
|
||||
return $this->faker->dateTimeBetween($attributes['created_at'], 'now');
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the model is active.
|
||||
*/
|
||||
public function active(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'is_active' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the model is inactive.
|
||||
*/
|
||||
public function inactive(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'is_active' => false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
PHP;
|
||||
|
||||
return str_replace('{CLASS_NAME}', $className, $stub);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate API resource
|
||||
*/
|
||||
protected function generateApiResource(string $className): void
|
||||
{
|
||||
$resourcePath = app_path("Http/Resources/{$className}Resource.php");
|
||||
|
||||
if (File::exists($resourcePath) && !$this->option('force')) {
|
||||
$this->error("Resource {$className}Resource already exists! Use --force to overwrite.");
|
||||
return;
|
||||
}
|
||||
|
||||
$resourceContent = $this->buildApiResourceContent($className);
|
||||
|
||||
$this->ensureDirectoryExists(dirname($resourcePath));
|
||||
File::put($resourcePath, $resourceContent);
|
||||
|
||||
$this->line("✅ API Resource created: app/Http/Resources/{$className}Resource.php");
|
||||
}
|
||||
|
||||
/**
|
||||
* Build API resource content
|
||||
*/
|
||||
protected function buildApiResourceContent(string $className): string
|
||||
{
|
||||
$stub = <<<'PHP'
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
/**
|
||||
* {CLASS_NAME} API Resource
|
||||
*
|
||||
* Transforms {CLASS_NAME} model data for API responses
|
||||
* Includes ThrillWiki optimization patterns
|
||||
*/
|
||||
class {CLASS_NAME}Resource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'slug' => $this->slug,
|
||||
'description' => $this->description,
|
||||
'is_active' => $this->is_active,
|
||||
'created_at' => $this->created_at?->toISOString(),
|
||||
'updated_at' => $this->updated_at?->toISOString(),
|
||||
|
||||
// Include relationships when loaded
|
||||
$this->mergeWhen($this->relationLoaded('relationships'), [
|
||||
// Add relationship data here
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get additional data that should be returned with the resource array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function with(Request $request): array
|
||||
{
|
||||
return [
|
||||
'meta' => [
|
||||
'model' => '{CLASS_NAME}',
|
||||
'generated_at' => now()->toISOString(),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
PHP;
|
||||
|
||||
return str_replace('{CLASS_NAME}', $className, $stub);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate test files
|
||||
*/
|
||||
protected function generateTests(string $className): void
|
||||
{
|
||||
$testPath = base_path("tests/Feature/{$className}Test.php");
|
||||
|
||||
if (File::exists($testPath) && !$this->option('force')) {
|
||||
$this->error("Test {$className}Test already exists! Use --force to overwrite.");
|
||||
return;
|
||||
}
|
||||
|
||||
$testContent = $this->buildTestContent($className);
|
||||
|
||||
$this->ensureDirectoryExists(dirname($testPath));
|
||||
File::put($testPath, $testContent);
|
||||
|
||||
$this->line("✅ Test created: tests/Feature/{$className}Test.php");
|
||||
}
|
||||
|
||||
/**
|
||||
* Build test content
|
||||
*/
|
||||
protected function buildTestContent(string $className): string
|
||||
{
|
||||
$stub = <<<'PHP'
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\{CLASS_NAME};
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Foundation\Testing\WithFaker;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* {CLASS_NAME} Model Feature Tests
|
||||
*
|
||||
* Tests for ThrillWiki {CLASS_NAME} model functionality
|
||||
*/
|
||||
class {CLASS_NAME}Test extends TestCase
|
||||
{
|
||||
use RefreshDatabase, WithFaker;
|
||||
|
||||
/**
|
||||
* Test model creation.
|
||||
*/
|
||||
public function test_can_create_{LOWER_CLASS_NAME}(): void
|
||||
{
|
||||
${LOWER_CLASS_NAME} = {CLASS_NAME}::factory()->create();
|
||||
|
||||
$this->assertDatabaseHas('{TABLE_NAME}', [
|
||||
'id' => ${LOWER_CLASS_NAME}->id,
|
||||
'name' => ${LOWER_CLASS_NAME}->name,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test model factory.
|
||||
*/
|
||||
public function test_{LOWER_CLASS_NAME}_factory_works(): void
|
||||
{
|
||||
${LOWER_CLASS_NAME} = {CLASS_NAME}::factory()->create();
|
||||
|
||||
$this->assertInstanceOf({CLASS_NAME}::class, ${LOWER_CLASS_NAME});
|
||||
$this->assertNotEmpty(${LOWER_CLASS_NAME}->name);
|
||||
$this->assertIsBool(${LOWER_CLASS_NAME}->is_active);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test active scope.
|
||||
*/
|
||||
public function test_active_scope_filters_correctly(): void
|
||||
{
|
||||
{CLASS_NAME}::factory()->active()->create();
|
||||
{CLASS_NAME}::factory()->inactive()->create();
|
||||
|
||||
$activeCount = {CLASS_NAME}::active()->count();
|
||||
$totalCount = {CLASS_NAME}::count();
|
||||
|
||||
$this->assertEquals(1, $activeCount);
|
||||
$this->assertEquals(2, $totalCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test cache key generation.
|
||||
*/
|
||||
public function test_cache_key_generation(): void
|
||||
{
|
||||
${LOWER_CLASS_NAME} = {CLASS_NAME}::factory()->create();
|
||||
|
||||
$cacheKey = ${LOWER_CLASS_NAME}->getCacheKey();
|
||||
$expectedKey = strtolower('{LOWER_CLASS_NAME}') . '.' . ${LOWER_CLASS_NAME}->id;
|
||||
|
||||
$this->assertEquals($expectedKey, $cacheKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test cache key with suffix.
|
||||
*/
|
||||
public function test_cache_key_with_suffix(): void
|
||||
{
|
||||
${LOWER_CLASS_NAME} = {CLASS_NAME}::factory()->create();
|
||||
|
||||
$cacheKey = ${LOWER_CLASS_NAME}->getCacheKey('details');
|
||||
$expectedKey = strtolower('{LOWER_CLASS_NAME}') . '.' . ${LOWER_CLASS_NAME}->id . '.details';
|
||||
|
||||
$this->assertEquals($expectedKey, $cacheKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test soft deletes.
|
||||
*/
|
||||
public function test_soft_deletes_work(): void
|
||||
{
|
||||
${LOWER_CLASS_NAME} = {CLASS_NAME}::factory()->create();
|
||||
${LOWER_CLASS_NAME}->delete();
|
||||
|
||||
$this->assertSoftDeleted(${LOWER_CLASS_NAME});
|
||||
|
||||
// Test that it's excluded from normal queries
|
||||
$this->assertEquals(0, {CLASS_NAME}::count());
|
||||
|
||||
// Test that it's included in withTrashed queries
|
||||
$this->assertEquals(1, {CLASS_NAME}::withTrashed()->count());
|
||||
}
|
||||
}
|
||||
PHP;
|
||||
|
||||
return str_replace([
|
||||
'{CLASS_NAME}',
|
||||
'{LOWER_CLASS_NAME}',
|
||||
'{TABLE_NAME}',
|
||||
], [
|
||||
$className,
|
||||
strtolower($className),
|
||||
Str::snake(Str::plural($className)),
|
||||
], $stub);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure directory exists
|
||||
*/
|
||||
protected function ensureDirectoryExists(string $directory): void
|
||||
{
|
||||
if (!File::isDirectory($directory)) {
|
||||
File::makeDirectory($directory, 0755, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display summary of generated files
|
||||
*/
|
||||
protected function displaySummary(string $className): void
|
||||
{
|
||||
$this->newLine();
|
||||
$this->info("🎉 ThrillWiki Model '{$className}' created successfully!");
|
||||
$this->newLine();
|
||||
|
||||
$this->line("📁 Files Generated:");
|
||||
$this->line(" • app/Models/{$className}.php");
|
||||
|
||||
if ($this->option('migration')) {
|
||||
$this->line(" • database/migrations/[timestamp]_create_" . Str::snake(Str::plural($className)) . "_table.php");
|
||||
}
|
||||
|
||||
if ($this->option('factory')) {
|
||||
$this->line(" • database/factories/{$className}Factory.php");
|
||||
}
|
||||
|
||||
if ($this->option('api-resource')) {
|
||||
$this->line(" • app/Http/Resources/{$className}Resource.php");
|
||||
}
|
||||
|
||||
if ($this->option('with-tests')) {
|
||||
$this->line(" • tests/Feature/{$className}Test.php");
|
||||
}
|
||||
|
||||
$this->newLine();
|
||||
$this->line("🚀 Next Steps:");
|
||||
|
||||
if ($this->option('migration')) {
|
||||
$this->line(" 1. Run migration: php artisan migrate");
|
||||
}
|
||||
|
||||
if ($this->option('with-tests')) {
|
||||
$this->line(" 2. Run tests: php artisan test --filter {$className}Test");
|
||||
}
|
||||
|
||||
$this->line(" 3. Customize model attributes and relationships");
|
||||
$this->line(" 4. Update migration with specific fields");
|
||||
|
||||
if ($this->option('factory')) {
|
||||
$this->line(" 5. Customize factory with realistic data");
|
||||
}
|
||||
|
||||
$this->newLine();
|
||||
}
|
||||
}
|
||||
162
app/Filament/Resources/RideResource.php
Normal file
162
app/Filament/Resources/RideResource.php
Normal file
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\RideResource\Pages;
|
||||
use App\Filament\Resources\RideResource\RelationManagers;
|
||||
use App\Models\Ride;
|
||||
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 RideResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Ride::class;
|
||||
|
||||
protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack';
|
||||
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('name')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
Forms\Components\TextInput::make('slug')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
Forms\Components\Textarea::make('description')
|
||||
->required()
|
||||
->columnSpanFull(),
|
||||
Forms\Components\Select::make('park_id')
|
||||
->relationship('park', 'name')
|
||||
->required(),
|
||||
Forms\Components\Select::make('park_area_id')
|
||||
->relationship('parkArea', 'name'),
|
||||
Forms\Components\Select::make('manufacturer_id')
|
||||
->relationship('manufacturer', 'name'),
|
||||
Forms\Components\Select::make('designer_id')
|
||||
->relationship('designer', 'name'),
|
||||
Forms\Components\Select::make('ride_model_id')
|
||||
->relationship('rideModel', 'name'),
|
||||
Forms\Components\TextInput::make('category')
|
||||
->required()
|
||||
->maxLength(2)
|
||||
->default(''),
|
||||
Forms\Components\TextInput::make('status')
|
||||
->required()
|
||||
->maxLength(20)
|
||||
->default('OPERATING'),
|
||||
Forms\Components\TextInput::make('post_closing_status')
|
||||
->maxLength(20),
|
||||
Forms\Components\DatePicker::make('opening_date'),
|
||||
Forms\Components\DatePicker::make('closing_date'),
|
||||
Forms\Components\DatePicker::make('status_since'),
|
||||
Forms\Components\TextInput::make('min_height_in')
|
||||
->numeric(),
|
||||
Forms\Components\TextInput::make('max_height_in')
|
||||
->numeric(),
|
||||
Forms\Components\TextInput::make('capacity_per_hour')
|
||||
->numeric(),
|
||||
Forms\Components\TextInput::make('ride_duration_seconds')
|
||||
->numeric(),
|
||||
Forms\Components\TextInput::make('average_rating')
|
||||
->numeric(),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('name')
|
||||
->searchable(),
|
||||
Tables\Columns\TextColumn::make('slug')
|
||||
->searchable(),
|
||||
Tables\Columns\TextColumn::make('park.name')
|
||||
->numeric()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('parkArea.name')
|
||||
->numeric()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('manufacturer.name')
|
||||
->numeric()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('designer.name')
|
||||
->numeric()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('rideModel.name')
|
||||
->numeric()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('category')
|
||||
->searchable(),
|
||||
Tables\Columns\TextColumn::make('status')
|
||||
->searchable(),
|
||||
Tables\Columns\TextColumn::make('post_closing_status')
|
||||
->searchable(),
|
||||
Tables\Columns\TextColumn::make('opening_date')
|
||||
->date()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('closing_date')
|
||||
->date()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('status_since')
|
||||
->date()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('min_height_in')
|
||||
->numeric()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('max_height_in')
|
||||
->numeric()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('capacity_per_hour')
|
||||
->numeric()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('ride_duration_seconds')
|
||||
->numeric()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('average_rating')
|
||||
->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(),
|
||||
])
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
Tables\Actions\DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListRides::route('/'),
|
||||
'create' => Pages\CreateRide::route('/create'),
|
||||
'edit' => Pages\EditRide::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
12
app/Filament/Resources/RideResource/Pages/CreateRide.php
Normal file
12
app/Filament/Resources/RideResource/Pages/CreateRide.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\RideResource\Pages;
|
||||
|
||||
use App\Filament\Resources\RideResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateRide extends CreateRecord
|
||||
{
|
||||
protected static string $resource = RideResource::class;
|
||||
}
|
||||
19
app/Filament/Resources/RideResource/Pages/EditRide.php
Normal file
19
app/Filament/Resources/RideResource/Pages/EditRide.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\RideResource\Pages;
|
||||
|
||||
use App\Filament\Resources\RideResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditRide extends EditRecord
|
||||
{
|
||||
protected static string $resource = RideResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
19
app/Filament/Resources/RideResource/Pages/ListRides.php
Normal file
19
app/Filament/Resources/RideResource/Pages/ListRides.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\RideResource\Pages;
|
||||
|
||||
use App\Filament\Resources\RideResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListRides extends ListRecords
|
||||
{
|
||||
protected static string $resource = RideResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
95
app/Http/Controllers/Api/OperatorController.php
Normal file
95
app/Http/Controllers/Api/OperatorController.php
Normal file
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Operator;
|
||||
use App\Http\Requests\OperatorRequest;
|
||||
use App\Http\Resources\OperatorResource;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class OperatorController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*/
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$query = Operator::query();
|
||||
|
||||
// Search functionality
|
||||
if ($request->filled('search')) {
|
||||
$search = $request->get('search');
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('name', 'ILIKE', "%{$search}%")
|
||||
->orWhere('description', 'ILIKE', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
// Filter by status
|
||||
if ($request->filled('status')) {
|
||||
$query->where('is_active', $request->get('status') === 'active');
|
||||
}
|
||||
|
||||
$operators = $query->latest()->paginate(15);
|
||||
|
||||
return response()->json([
|
||||
'data' => OperatorResource::collection($operators),
|
||||
'meta' => [
|
||||
'current_page' => $operators->currentPage(),
|
||||
'last_page' => $operators->lastPage(),
|
||||
'per_page' => $operators->perPage(),
|
||||
'total' => $operators->total(),
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*/
|
||||
public function store(OperatorRequest $request): JsonResponse
|
||||
{
|
||||
$operator = Operator::create($request->validated());
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Operator created successfully',
|
||||
'data' => new OperatorResource($operator)
|
||||
], 201);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified resource.
|
||||
*/
|
||||
public function show(Operator $operator): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'data' => new OperatorResource($operator)
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*/
|
||||
public function update(OperatorRequest $request, Operator $operator): JsonResponse
|
||||
{
|
||||
$operator->update($request->validated());
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Operator updated successfully',
|
||||
'data' => new OperatorResource($operator)
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified resource from storage.
|
||||
*/
|
||||
public function destroy(Operator $operator): JsonResponse
|
||||
{
|
||||
$operator->delete();
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Operator deleted successfully'
|
||||
]);
|
||||
}
|
||||
}
|
||||
95
app/Http/Controllers/Api/RideController.php
Normal file
95
app/Http/Controllers/Api/RideController.php
Normal file
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Ride;
|
||||
use App\Http\Requests\RideRequest;
|
||||
use App\Http\Resources\RideResource;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class RideController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*/
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$query = Ride::query();
|
||||
|
||||
// Search functionality
|
||||
if ($request->filled('search')) {
|
||||
$search = $request->get('search');
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('name', 'ILIKE', "%{$search}%")
|
||||
->orWhere('description', 'ILIKE', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
// Filter by status
|
||||
if ($request->filled('status')) {
|
||||
$query->where('is_active', $request->get('status') === 'active');
|
||||
}
|
||||
|
||||
$rides = $query->latest()->paginate(15);
|
||||
|
||||
return response()->json([
|
||||
'data' => RideResource::collection($rides),
|
||||
'meta' => [
|
||||
'current_page' => $rides->currentPage(),
|
||||
'last_page' => $rides->lastPage(),
|
||||
'per_page' => $rides->perPage(),
|
||||
'total' => $rides->total(),
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*/
|
||||
public function store(RideRequest $request): JsonResponse
|
||||
{
|
||||
$ride = Ride::create($request->validated());
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Ride created successfully',
|
||||
'data' => new RideResource($ride)
|
||||
], 201);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified resource.
|
||||
*/
|
||||
public function show(Ride $ride): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'data' => new RideResource($ride)
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*/
|
||||
public function update(RideRequest $request, Ride $ride): JsonResponse
|
||||
{
|
||||
$ride->update($request->validated());
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Ride updated successfully',
|
||||
'data' => new RideResource($ride)
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified resource from storage.
|
||||
*/
|
||||
public function destroy(Ride $ride): JsonResponse
|
||||
{
|
||||
$ride->delete();
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Ride deleted successfully'
|
||||
]);
|
||||
}
|
||||
}
|
||||
27
app/Http/Controllers/Auth/VerifyEmailController.php
Normal file
27
app/Http/Controllers/Auth/VerifyEmailController.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Auth\Events\Verified;
|
||||
use Illuminate\Foundation\Auth\EmailVerificationRequest;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
|
||||
class VerifyEmailController extends Controller
|
||||
{
|
||||
/**
|
||||
* Mark the authenticated user's email address as verified.
|
||||
*/
|
||||
public function __invoke(EmailVerificationRequest $request): RedirectResponse
|
||||
{
|
||||
if ($request->user()->hasVerifiedEmail()) {
|
||||
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
|
||||
}
|
||||
|
||||
if ($request->user()->markEmailAsVerified()) {
|
||||
event(new Verified($request->user()));
|
||||
}
|
||||
|
||||
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
|
||||
}
|
||||
}
|
||||
98
app/Http/Controllers/OperatorController.php
Normal file
98
app/Http/Controllers/OperatorController.php
Normal file
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Operator;
|
||||
use App\Http\Requests\OperatorRequest;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
|
||||
class OperatorController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*/
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$query = Operator::query();
|
||||
|
||||
// Search functionality
|
||||
if ($request->filled('search')) {
|
||||
$search = $request->get('search');
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('name', 'ILIKE', "%{$search}%")
|
||||
->orWhere('description', 'ILIKE', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
// Filter by status
|
||||
if ($request->filled('status')) {
|
||||
$query->where('is_active', $request->get('status') === 'active');
|
||||
}
|
||||
|
||||
$operators = $query->latest()->paginate(15)->withQueryString();
|
||||
|
||||
return view('operators.index', compact('operators'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for creating a new resource.
|
||||
*/
|
||||
public function create(): View
|
||||
{
|
||||
return view('operators.create');
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*/
|
||||
public function store(OperatorRequest $request): RedirectResponse
|
||||
{
|
||||
$operator = Operator::create($request->validated());
|
||||
|
||||
return redirect()
|
||||
->route('operators.show', $operator)
|
||||
->with('success', 'Operator created successfully!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified resource.
|
||||
*/
|
||||
public function show(Operator $operator): View
|
||||
{
|
||||
return view('operators.show', compact('operator'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified resource.
|
||||
*/
|
||||
public function edit(Operator $operator): View
|
||||
{
|
||||
return view('operators.edit', compact('operator'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*/
|
||||
public function update(OperatorRequest $request, Operator $operator): RedirectResponse
|
||||
{
|
||||
$operator->update($request->validated());
|
||||
|
||||
return redirect()
|
||||
->route('operators.show', $operator)
|
||||
->with('success', 'Operator updated successfully!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified resource from storage.
|
||||
*/
|
||||
public function destroy(Operator $operator): RedirectResponse
|
||||
{
|
||||
$operator->delete();
|
||||
|
||||
return redirect()
|
||||
->route('operators.index')
|
||||
->with('success', 'Operator deleted successfully!');
|
||||
}
|
||||
}
|
||||
57
app/Http/Controllers/ParkController.php
Normal file
57
app/Http/Controllers/ParkController.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Park;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class ParkController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of parks.
|
||||
*/
|
||||
public function index(): View
|
||||
{
|
||||
return view('parks.index');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for creating a new park.
|
||||
*/
|
||||
public function create(): View
|
||||
{
|
||||
return view('parks.create');
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified park.
|
||||
*/
|
||||
public function show(Park $park): View
|
||||
{
|
||||
// Load relationships for the park detail page
|
||||
$park->load([
|
||||
'operator',
|
||||
'location',
|
||||
'areas.rides' => function ($query) {
|
||||
$query->orderBy('position')->orderBy('name');
|
||||
},
|
||||
'areas' => function ($query) {
|
||||
$query->orderBy('position')->orderBy('name');
|
||||
},
|
||||
'photos' => function ($query) {
|
||||
$query->orderBy('is_featured', 'desc')->orderBy('created_at', 'desc');
|
||||
}
|
||||
]);
|
||||
|
||||
return view('parks.show', compact('park'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified park.
|
||||
*/
|
||||
public function edit(Park $park): View
|
||||
{
|
||||
return view('parks.edit', compact('park'));
|
||||
}
|
||||
}
|
||||
98
app/Http/Controllers/RideController.php
Normal file
98
app/Http/Controllers/RideController.php
Normal file
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Ride;
|
||||
use App\Http\Requests\RideRequest;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
|
||||
class RideController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*/
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$query = Ride::query();
|
||||
|
||||
// Search functionality
|
||||
if ($request->filled('search')) {
|
||||
$search = $request->get('search');
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('name', 'ILIKE', "%{$search}%")
|
||||
->orWhere('description', 'ILIKE', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
// Filter by status
|
||||
if ($request->filled('status')) {
|
||||
$query->where('is_active', $request->get('status') === 'active');
|
||||
}
|
||||
|
||||
$rides = $query->latest()->paginate(15)->withQueryString();
|
||||
|
||||
return view('rides.index', compact('rides'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for creating a new resource.
|
||||
*/
|
||||
public function create(): View
|
||||
{
|
||||
return view('rides.create');
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*/
|
||||
public function store(RideRequest $request): RedirectResponse
|
||||
{
|
||||
$ride = Ride::create($request->validated());
|
||||
|
||||
return redirect()
|
||||
->route('rides.show', $ride)
|
||||
->with('success', 'Ride created successfully!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified resource.
|
||||
*/
|
||||
public function show(Ride $ride): View
|
||||
{
|
||||
return view('rides.show', compact('ride'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified resource.
|
||||
*/
|
||||
public function edit(Ride $ride): View
|
||||
{
|
||||
return view('rides.edit', compact('ride'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*/
|
||||
public function update(RideRequest $request, Ride $ride): RedirectResponse
|
||||
{
|
||||
$ride->update($request->validated());
|
||||
|
||||
return redirect()
|
||||
->route('rides.show', $ride)
|
||||
->with('success', 'Ride updated successfully!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified resource from storage.
|
||||
*/
|
||||
public function destroy(Ride $ride): RedirectResponse
|
||||
{
|
||||
$ride->delete();
|
||||
|
||||
return redirect()
|
||||
->route('rides.index')
|
||||
->with('success', 'Ride deleted successfully!');
|
||||
}
|
||||
}
|
||||
48
app/Http/Requests/OperatorRequest.php
Normal file
48
app/Http/Requests/OperatorRequest.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class OperatorRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true; // Add authorization logic as needed
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
$rules = [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'description' => ['nullable', 'string'],
|
||||
'is_active' => ['boolean'],
|
||||
];
|
||||
|
||||
// For updates, make name unique except for current record
|
||||
if ($this->route('operator')) {
|
||||
$rules['name'][] = 'unique:operators,name,' . $this->route('operator')->id;
|
||||
} else {
|
||||
$rules['name'][] = 'unique:operators,name';
|
||||
}
|
||||
|
||||
return $rules;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom messages for validator errors.
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'name.required' => 'The operator name is required.',
|
||||
'name.unique' => 'A operator with this name already exists.',
|
||||
];
|
||||
}
|
||||
}
|
||||
48
app/Http/Requests/RideRequest.php
Normal file
48
app/Http/Requests/RideRequest.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class RideRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true; // Add authorization logic as needed
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
$rules = [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'description' => ['nullable', 'string'],
|
||||
'is_active' => ['boolean'],
|
||||
];
|
||||
|
||||
// For updates, make name unique except for current record
|
||||
if ($this->route('ride')) {
|
||||
$rules['name'][] = 'unique:rides,name,' . $this->route('ride')->id;
|
||||
} else {
|
||||
$rules['name'][] = 'unique:rides,name';
|
||||
}
|
||||
|
||||
return $rules;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom messages for validator errors.
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'name.required' => 'The ride name is required.',
|
||||
'name.unique' => 'A ride with this name already exists.',
|
||||
];
|
||||
}
|
||||
}
|
||||
53
app/Http/Resources/ManufacturerResource.php
Normal file
53
app/Http/Resources/ManufacturerResource.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
/**
|
||||
* Manufacturer API Resource
|
||||
*
|
||||
* Transforms Manufacturer model data for API responses
|
||||
* Includes ThrillWiki optimization patterns
|
||||
*/
|
||||
class ManufacturerResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'slug' => $this->slug,
|
||||
'description' => $this->description,
|
||||
'is_active' => $this->is_active,
|
||||
'created_at' => $this->created_at?->toISOString(),
|
||||
'updated_at' => $this->updated_at?->toISOString(),
|
||||
|
||||
// Include relationships when loaded
|
||||
$this->mergeWhen($this->relationLoaded('relationships'), [
|
||||
// Add relationship data here
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get additional data that should be returned with the resource array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function with(Request $request): array
|
||||
{
|
||||
return [
|
||||
'meta' => [
|
||||
'model' => 'Manufacturer',
|
||||
'generated_at' => now()->toISOString(),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
24
app/Http/Resources/OperatorResource.php
Normal file
24
app/Http/Resources/OperatorResource.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class OperatorResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'description' => $this->description,
|
||||
'is_active' => $this->is_active,
|
||||
'created_at' => $this->created_at,
|
||||
'updated_at' => $this->updated_at,
|
||||
];
|
||||
}
|
||||
}
|
||||
24
app/Http/Resources/RideResource.php
Normal file
24
app/Http/Resources/RideResource.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class RideResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'description' => $this->description,
|
||||
'is_active' => $this->is_active,
|
||||
'created_at' => $this->created_at,
|
||||
'updated_at' => $this->updated_at,
|
||||
];
|
||||
}
|
||||
}
|
||||
20
app/Livewire/Actions/Logout.php
Normal file
20
app/Livewire/Actions/Logout.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Actions;
|
||||
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Session;
|
||||
|
||||
class Logout
|
||||
{
|
||||
/**
|
||||
* Log the current user out of the application.
|
||||
*/
|
||||
public function __invoke(): void
|
||||
{
|
||||
Auth::guard('web')->logout();
|
||||
|
||||
Session::invalidate();
|
||||
Session::regenerateToken();
|
||||
}
|
||||
}
|
||||
72
app/Livewire/Forms/LoginForm.php
Normal file
72
app/Livewire/Forms/LoginForm.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Forms;
|
||||
|
||||
use Illuminate\Auth\Events\Lockout;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Form;
|
||||
|
||||
class LoginForm extends Form
|
||||
{
|
||||
#[Validate('required|string|email')]
|
||||
public string $email = '';
|
||||
|
||||
#[Validate('required|string')]
|
||||
public string $password = '';
|
||||
|
||||
#[Validate('boolean')]
|
||||
public bool $remember = false;
|
||||
|
||||
/**
|
||||
* Attempt to authenticate the request's credentials.
|
||||
*
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
*/
|
||||
public function authenticate(): void
|
||||
{
|
||||
$this->ensureIsNotRateLimited();
|
||||
|
||||
if (! Auth::attempt($this->only(['email', 'password']), $this->remember)) {
|
||||
RateLimiter::hit($this->throttleKey());
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'form.email' => trans('auth.failed'),
|
||||
]);
|
||||
}
|
||||
|
||||
RateLimiter::clear($this->throttleKey());
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the authentication request is not rate limited.
|
||||
*/
|
||||
protected function ensureIsNotRateLimited(): void
|
||||
{
|
||||
if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
|
||||
return;
|
||||
}
|
||||
|
||||
event(new Lockout(request()));
|
||||
|
||||
$seconds = RateLimiter::availableIn($this->throttleKey());
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'form.email' => trans('auth.throttle', [
|
||||
'seconds' => $seconds,
|
||||
'minutes' => ceil($seconds / 60),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the authentication rate limiting throttle key.
|
||||
*/
|
||||
protected function throttleKey(): string
|
||||
{
|
||||
return Str::transliterate(Str::lower($this->email).'|'.request()->ip());
|
||||
}
|
||||
}
|
||||
@@ -6,10 +6,11 @@ use App\Traits\HasSlugHistory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Manufacturer extends Model
|
||||
{
|
||||
use HasFactory, HasSlugHistory;
|
||||
use HasFactory, HasSlugHistory, SoftDeletes;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
@@ -24,15 +25,29 @@ class Manufacturer extends Model
|
||||
'description',
|
||||
'total_rides',
|
||||
'total_roller_coasters',
|
||||
'is_active',
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be cast.
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
protected $casts = [
|
||||
'total_rides' => 'integer',
|
||||
'total_roller_coasters' => 'integer',
|
||||
'is_active' => 'boolean',
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
'deleted_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the rides manufactured by this company.
|
||||
* Note: This relationship will be properly set up when we implement the Rides system.
|
||||
*/
|
||||
public function rides(): HasMany
|
||||
{
|
||||
return $this->hasMany(Ride::class);
|
||||
return $this->hasMany(Ride::class, 'manufacturer_id');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -42,7 +57,7 @@ class Manufacturer extends Model
|
||||
{
|
||||
$this->total_rides = $this->rides()->count();
|
||||
$this->total_roller_coasters = $this->rides()
|
||||
->where('type', 'roller_coaster')
|
||||
->where('category', 'RC')
|
||||
->count();
|
||||
$this->save();
|
||||
}
|
||||
@@ -88,6 +103,22 @@ class Manufacturer extends Model
|
||||
return $query->where('total_roller_coasters', '>', 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope a query to only include active manufacturers.
|
||||
*/
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('is_active', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope a query for optimized loading with statistics.
|
||||
*/
|
||||
public function scopeOptimized($query)
|
||||
{
|
||||
return $query->select(['id', 'name', 'slug', 'total_rides', 'total_roller_coasters', 'is_active']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the route key for the model.
|
||||
*/
|
||||
|
||||
@@ -4,26 +4,34 @@ namespace App\Models;
|
||||
|
||||
use App\Enums\RideCategory;
|
||||
use App\Enums\RideStatus;
|
||||
use App\Traits\HasSlugHistory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class Ride extends Model
|
||||
{
|
||||
use SoftDeletes, HasSlugHistory;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'slug',
|
||||
'description',
|
||||
'status',
|
||||
'category',
|
||||
'opening_date',
|
||||
'closing_date',
|
||||
'park_id',
|
||||
'park_area_id',
|
||||
'manufacturer_id',
|
||||
'designer_id',
|
||||
'ride_model_id',
|
||||
'category',
|
||||
'status',
|
||||
'post_closing_status',
|
||||
'status_since',
|
||||
'opening_date',
|
||||
'closing_date',
|
||||
'min_height_in',
|
||||
'max_height_in',
|
||||
'capacity_per_hour',
|
||||
@@ -35,13 +43,14 @@ class Ride extends Model
|
||||
'category' => RideCategory::class,
|
||||
'opening_date' => 'date',
|
||||
'closing_date' => 'date',
|
||||
'status_since' => 'date',
|
||||
'min_height_in' => 'integer',
|
||||
'max_height_in' => 'integer',
|
||||
'capacity_per_hour' => 'integer',
|
||||
'ride_duration_seconds' => 'integer',
|
||||
];
|
||||
|
||||
// Base Relationships
|
||||
// Core Relationships (Django Parity)
|
||||
public function park(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Park::class);
|
||||
@@ -54,7 +63,7 @@ class Ride extends Model
|
||||
|
||||
public function manufacturer(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Manufacturer::class);
|
||||
return $this->belongsTo(Manufacturer::class, 'manufacturer_id');
|
||||
}
|
||||
|
||||
public function designer(): BelongsTo
|
||||
@@ -67,23 +76,51 @@ class Ride extends Model
|
||||
return $this->belongsTo(RideModel::class);
|
||||
}
|
||||
|
||||
// Extended Relationships
|
||||
public function coasterStats(): HasOne
|
||||
{
|
||||
return $this->hasOne(RollerCoasterStats::class);
|
||||
}
|
||||
|
||||
// Photo Relationships (Polymorphic)
|
||||
public function photos(): MorphMany
|
||||
{
|
||||
return $this->morphMany(Photo::class, 'photosable');
|
||||
}
|
||||
|
||||
// Review Relationships
|
||||
public function reviews(): HasMany
|
||||
public function reviews(): MorphMany
|
||||
{
|
||||
return $this->hasMany(Review::class);
|
||||
return $this->morphMany(Review::class, 'reviewable');
|
||||
}
|
||||
|
||||
public function approvedReviews(): HasMany
|
||||
public function approvedReviews(): MorphMany
|
||||
{
|
||||
return $this->reviews()->approved();
|
||||
return $this->reviews()->where('status', 'approved');
|
||||
}
|
||||
|
||||
// Review Methods
|
||||
// Query Scopes
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('status', 'operating');
|
||||
}
|
||||
|
||||
public function scopeByCategory($query, $category)
|
||||
{
|
||||
return $query->where('category', $category);
|
||||
}
|
||||
|
||||
public function scopeInPark($query, $parkId)
|
||||
{
|
||||
return $query->where('park_id', $parkId);
|
||||
}
|
||||
|
||||
public function scopeByManufacturer($query, $manufacturerId)
|
||||
{
|
||||
return $query->where('manufacturer_id', $manufacturerId);
|
||||
}
|
||||
|
||||
// Attributes & Helper Methods
|
||||
public function getAverageRatingAttribute(): ?float
|
||||
{
|
||||
return $this->approvedReviews()->avg('rating');
|
||||
@@ -94,6 +131,32 @@ class Ride extends Model
|
||||
return $this->approvedReviews()->count();
|
||||
}
|
||||
|
||||
public function getDisplayNameAttribute(): string
|
||||
{
|
||||
return $this->name . ' at ' . $this->park->name;
|
||||
}
|
||||
|
||||
public function getIsOperatingAttribute(): bool
|
||||
{
|
||||
return $this->status === RideStatus::OPERATING;
|
||||
}
|
||||
|
||||
public function getHeightRequirementTextAttribute(): ?string
|
||||
{
|
||||
if (!$this->min_height_in) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$text = "Must be at least {$this->min_height_in}\" tall";
|
||||
|
||||
if ($this->max_height_in) {
|
||||
$text .= " and no taller than {$this->max_height_in}\" tall";
|
||||
}
|
||||
|
||||
return $text;
|
||||
}
|
||||
|
||||
// Review Management Methods
|
||||
public function canBeReviewedBy(?int $userId): bool
|
||||
{
|
||||
if (!$userId) {
|
||||
@@ -112,7 +175,32 @@ class Ride extends Model
|
||||
'rating' => $data['rating'],
|
||||
'title' => $data['title'] ?? null,
|
||||
'content' => $data['content'],
|
||||
'status' => ReviewStatus::PENDING,
|
||||
'status' => 'pending',
|
||||
]);
|
||||
}
|
||||
|
||||
// Cache Management (Future: will use HasCaching trait)
|
||||
public function getCacheKey(string $suffix = ''): string
|
||||
{
|
||||
return "ride:{$this->id}" . ($suffix ? ":{$suffix}" : '');
|
||||
}
|
||||
|
||||
public function clearRelatedCache(): void
|
||||
{
|
||||
cache()->forget($this->getCacheKey('reviews'));
|
||||
cache()->forget($this->getCacheKey('statistics'));
|
||||
cache()->forget($this->getCacheKey('photos'));
|
||||
}
|
||||
|
||||
// Statistics Management (Future: will use HasStatistics trait)
|
||||
public function updateStatistics(): void
|
||||
{
|
||||
// Placeholder for future HasStatistics trait integration
|
||||
// For now, manually manage statistics
|
||||
$totalReviews = $this->reviews()->count();
|
||||
$averageRating = $this->reviews()->avg('rating');
|
||||
|
||||
// Update any related statistics tracking
|
||||
$this->clearRelatedCache();
|
||||
}
|
||||
}
|
||||
@@ -53,9 +53,6 @@ class AdminPanelProvider extends PanelProvider
|
||||
->authMiddleware([
|
||||
Authenticate::class,
|
||||
])
|
||||
->resources([
|
||||
config('filament.resources.namespace') => app_path('Filament/Resources'),
|
||||
])
|
||||
->navigationGroups([
|
||||
'Company Management',
|
||||
'Attractions',
|
||||
|
||||
28
app/Providers/VoltServiceProvider.php
Normal file
28
app/Providers/VoltServiceProvider.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Livewire\Volt\Volt;
|
||||
|
||||
class VoltServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
Volt::mount([
|
||||
config('livewire.view_path', resource_path('views/livewire')),
|
||||
resource_path('views/pages'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
17
app/View/Components/AppLayout.php
Normal file
17
app/View/Components/AppLayout.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\View\Components;
|
||||
|
||||
use Illuminate\View\Component;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class AppLayout extends Component
|
||||
{
|
||||
/**
|
||||
* Get the view / contents that represents the component.
|
||||
*/
|
||||
public function render(): View
|
||||
{
|
||||
return view('layouts.app');
|
||||
}
|
||||
}
|
||||
17
app/View/Components/GuestLayout.php
Normal file
17
app/View/Components/GuestLayout.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\View\Components;
|
||||
|
||||
use Illuminate\View\Component;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class GuestLayout extends Component
|
||||
{
|
||||
/**
|
||||
* Get the view / contents that represents the component.
|
||||
*/
|
||||
public function render(): View
|
||||
{
|
||||
return view('layouts.guest');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user