diff --git a/README.md b/README.md index 80163f2..692b05c 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,8 @@ A comprehensive Laravel development environment with Docker for local developmen │ ├── laravel-setup.md # Laravel setup guide │ ├── filament-admin.md # Admin panel docs │ ├── modules.md # Modular architecture guide +│ ├── module-generator.md # Admin UI module generator +│ ├── menu-management.md # Frontend menu system │ ├── audit-trail.md # Audit trail docs │ ├── site-settings.md # Appearance settings │ ├── testing.md # Pest testing guide diff --git a/docs/module-generator.md b/docs/module-generator.md new file mode 100644 index 0000000..30d07b7 --- /dev/null +++ b/docs/module-generator.md @@ -0,0 +1,173 @@ +# Module Generator (Admin UI) + +A visual tool for creating module skeletons through the Filament admin panel. + +> **Development Only**: This tool is only available when `APP_ENV=local`. + +## Access + +Navigate to: **Admin Panel → Development → Module Generator** + +URL: `/admin/module-generator` + +## Features + +- **Visual Form**: Create modules without command line +- **Auto Git Branch**: Optionally creates `module/{name}` branch and commits files +- **Skeleton Only**: Maximum flexibility - creates structure, you add the models +- **Instant Feedback**: Shows generation logs and next steps + +## What Gets Created + +``` +app/Modules/{ModuleName}/ +├── Config/{module_name}.php # Module configuration +├── Database/ +│ ├── Migrations/ # Empty, add your migrations +│ └── Seeders/ # Empty, add your seeders +├── Filament/Resources/ # Empty, add Filament resources +├── Http/ +│ ├── Controllers/{ModuleName}Controller.php +│ ├── Middleware/ # Empty +│ └── Requests/ # Empty +├── Models/ # Empty, add your models +├── Policies/ # Empty, add policies +├── Services/ # Empty, add services +├── Routes/ +│ ├── web.php # Basic index route +│ └── api.php # If API option selected +├── Resources/views/ +│ └── index.blade.php # Starter view +├── Permissions.php # CRUD permissions +├── {ModuleName}ServiceProvider.php # Auto-registered +└── README.md # Module documentation +``` + +## Form Options + +| Field | Description | +|-------|-------------| +| **Module Name** | PascalCase name (e.g., `Accounting`, `Inventory`) | +| **Description** | Brief description for README and config | +| **Create Git Branch** | Auto-create `module/{name}` branch and commit | +| **Include API Routes** | Add `Routes/api.php` with Sanctum auth | + +## Git Integration + +When "Create Git Branch" is enabled: + +1. **Checks** for uncommitted changes (fails if dirty) +2. **Creates** branch `module/{module-name}` +3. **Generates** all module files +4. **Commits** with message `feat: Add {ModuleName} module skeleton` +5. **Shows** push command: `git push -u origin module/{name}` + +### Requirements + +- Working directory must be clean (no uncommitted changes) +- Git must be installed in the container +- Repository must be initialized + +## After Generation + +1. **Run migrations** (if you add any): + ```bash + php artisan migrate + ``` + +2. **Seed permissions**: + ```bash + php artisan db:seed --class=RolePermissionSeeder + ``` + +3. **Clear caches**: + ```bash + php artisan optimize:clear + ``` + +4. **Push branch** (if Git was used): + ```bash + git push -u origin module/{name} + ``` + +## Adding Models + +After generating the skeleton, add models manually: + +```php +// app/Modules/Accounting/Models/Invoice.php +environment('local'); + } + + public static function shouldRegisterNavigation(): bool + { + return app()->environment('local'); + } + + public function mount(): void + { + $this->form->fill([ + 'create_git_branch' => true, + 'include_api' => false, + ]); + + $this->loadModules(); + } + + public function loadModules(): void + { + $discovery = new ModuleDiscoveryService(); + $this->modules = $discovery->discoverModules(); + $this->summary = $discovery->getModuleSummary(); + } + + public function toggleModule(string $moduleName): void + { + $this->expandedModule = $this->expandedModule === $moduleName ? null : $moduleName; + } + + public function form(Form $form): Form + { + return $form + ->schema([ + Section::make('Module Details') + ->description('Create a new module skeleton with all the necessary folders and files.') + ->schema([ + TextInput::make('name') + ->label('Module Name') + ->placeholder('e.g., Accounting, Inventory, HumanResources') + ->helperText('Use PascalCase. This will create app/Modules/YourModuleName/') + ->required() + ->maxLength(50) + ->rules(['regex:/^[A-Z][a-zA-Z0-9]*$/']) + ->validationMessages([ + 'regex' => 'Module name must be PascalCase (start with uppercase, letters and numbers only).', + ]) + ->live(onBlur: true) + ->afterStateUpdated(fn ($state, callable $set) => + $set('name', Str::studly($state)) + ), + + Textarea::make('description') + ->label('Description') + ->placeholder('Brief description of what this module does...') + ->rows(2) + ->maxLength(255), + ]), + + Section::make('Options') + ->schema([ + Toggle::make('create_git_branch') + ->label('Create Git Branch') + ->helperText('Automatically create and checkout a new branch: module/{module-name}') + ->default(true), + + Toggle::make('include_api') + ->label('Include API Routes') + ->helperText('Add Routes/api.php with Sanctum authentication') + ->default(false), + ]) + ->columns(2), + + Section::make('What Will Be Created') + ->schema([ + \Filament\Forms\Components\Placeholder::make('structure') + ->label('') + ->content(fn () => new \Illuminate\Support\HtmlString(' +
+
app/Modules/{ModuleName}/
+
+ ├── Config/{module_name}.php
+ ├── Database/Migrations/
+ ├── Database/Seeders/
+ ├── Filament/Resources/
+ ├── Http/Controllers/{ModuleName}Controller.php
+ ├── Http/Middleware/
+ ├── Http/Requests/
+ ├── Models/
+ ├── Policies/
+ ├── Services/
+ ├── Routes/web.php
+ ├── Resources/views/index.blade.php
+ ├── Permissions.php
+ ├── {ModuleName}ServiceProvider.php
+ └── README.md +
+
+ ')), + ]) + ->collapsible() + ->collapsed(), + ]) + ->statePath('data'); + } + + public function generate(): void + { + $data = $this->form->getState(); + + $service = new ModuleGeneratorService(); + $this->result = $service->generate($data['name'], [ + 'description' => $data['description'] ?? '', + 'create_git_branch' => $data['create_git_branch'] ?? true, + 'include_api' => $data['include_api'] ?? false, + ]); + + if ($this->result['success']) { + Notification::make() + ->title('Module Created Successfully!') + ->body("Module {$this->result['module_name']} has been created.") + ->success() + ->persistent() + ->send(); + + // Reset form for next module + $this->form->fill([ + 'name' => '', + 'description' => '', + 'create_git_branch' => true, + 'include_api' => false, + ]); + + // Reload modules list + $this->loadModules(); + } else { + Notification::make() + ->title('Module Creation Failed') + ->body($this->result['message']) + ->danger() + ->persistent() + ->send(); + } + } + + public function clearResult(): void + { + $this->result = null; + } +} diff --git a/src/app/Services/ModuleDiscoveryService.php b/src/app/Services/ModuleDiscoveryService.php new file mode 100644 index 0000000..19ba2ad --- /dev/null +++ b/src/app/Services/ModuleDiscoveryService.php @@ -0,0 +1,248 @@ +getModuleInfo($moduleName, $directory); + } + + return $modules; + } + + protected function getModuleInfo(string $name, string $path): array + { + $kebabName = Str::kebab($name); + $snakeName = Str::snake($name); + + return [ + 'name' => $name, + 'slug' => $kebabName, + 'path' => $path, + 'config' => $this->getConfig($path, $snakeName), + 'models' => $this->getModels($path), + 'views' => $this->getViews($path), + 'routes' => $this->getRoutes($path, $kebabName), + 'migrations' => $this->getMigrations($path), + 'filament_resources' => $this->getFilamentResources($path), + 'permissions' => $this->getPermissions($path), + 'has_api' => File::exists("{$path}/Routes/api.php"), + ]; + } + + protected function getConfig(string $path, string $snakeName): array + { + $configPath = "{$path}/Config/{$snakeName}.php"; + + if (File::exists($configPath)) { + return require $configPath; + } + + return []; + } + + protected function getModels(string $path): array + { + $modelsPath = "{$path}/Models"; + + if (!File::exists($modelsPath)) { + return []; + } + + $models = []; + $files = File::files($modelsPath); + + foreach ($files as $file) { + if ($file->getExtension() === 'php' && $file->getFilename() !== '.gitkeep') { + $modelName = $file->getFilenameWithoutExtension(); + $models[] = [ + 'name' => $modelName, + 'file' => $file->getFilename(), + 'path' => $file->getPathname(), + ]; + } + } + + return $models; + } + + protected function getViews(string $path): array + { + $viewsPath = "{$path}/Resources/views"; + + if (!File::exists($viewsPath)) { + return []; + } + + return $this->scanViewsRecursive($viewsPath, ''); + } + + protected function scanViewsRecursive(string $basePath, string $prefix): array + { + $views = []; + $items = File::files($basePath); + + foreach ($items as $file) { + if ($file->getExtension() === 'php' && Str::endsWith($file->getFilename(), '.blade.php')) { + $viewName = Str::replaceLast('.blade.php', '', $file->getFilename()); + $fullName = $prefix ? "{$prefix}.{$viewName}" : $viewName; + $views[] = [ + 'name' => $fullName, + 'file' => $file->getFilename(), + 'path' => $file->getPathname(), + ]; + } + } + + // Scan subdirectories + $directories = File::directories($basePath); + foreach ($directories as $dir) { + $dirName = basename($dir); + $subPrefix = $prefix ? "{$prefix}.{$dirName}" : $dirName; + $views = array_merge($views, $this->scanViewsRecursive($dir, $subPrefix)); + } + + return $views; + } + + protected function getRoutes(string $path, string $kebabName): array + { + $routes = []; + + // Get web routes + $webRoutesPath = "{$path}/Routes/web.php"; + if (File::exists($webRoutesPath)) { + $routes['web'] = $this->parseRouteFile($webRoutesPath, $kebabName); + } + + // Get API routes + $apiRoutesPath = "{$path}/Routes/api.php"; + if (File::exists($apiRoutesPath)) { + $routes['api'] = $this->parseRouteFile($apiRoutesPath, "api/{$kebabName}"); + } + + return $routes; + } + + protected function parseRouteFile(string $path, string $prefix): array + { + $content = File::get($path); + $routes = []; + + // Parse Route::get, Route::post, etc. + preg_match_all( + "/Route::(get|post|put|patch|delete|any)\s*\(\s*['\"]([^'\"]*)['\"].*?->name\s*\(\s*['\"]([^'\"]*)['\"]|Route::(get|post|put|patch|delete|any)\s*\(\s*['\"]([^'\"]*)['\"].*?\[.*?::class,\s*['\"](\w+)['\"]\]/", + $content, + $matches, + PREG_SET_ORDER + ); + + foreach ($matches as $match) { + $method = strtoupper($match[1] ?: $match[4]); + $uri = $match[2] ?: $match[5]; + $fullUri = $uri === '/' ? "/{$prefix}" : "/{$prefix}{$uri}"; + + $routes[] = [ + 'method' => $method, + 'uri' => $fullUri, + 'name' => $match[3] ?? null, + ]; + } + + return $routes; + } + + protected function getMigrations(string $path): array + { + $migrationsPath = "{$path}/Database/Migrations"; + + if (!File::exists($migrationsPath)) { + return []; + } + + $migrations = []; + $files = File::files($migrationsPath); + + foreach ($files as $file) { + if ($file->getExtension() === 'php') { + $migrations[] = [ + 'name' => $file->getFilename(), + 'path' => $file->getPathname(), + ]; + } + } + + return $migrations; + } + + protected function getFilamentResources(string $path): array + { + $resourcesPath = "{$path}/Filament/Resources"; + + if (!File::exists($resourcesPath)) { + return []; + } + + $resources = []; + $files = File::files($resourcesPath); + + foreach ($files as $file) { + if ($file->getExtension() === 'php' && Str::endsWith($file->getFilename(), 'Resource.php')) { + $resourceName = Str::replaceLast('Resource.php', '', $file->getFilename()); + $resources[] = [ + 'name' => $resourceName, + 'file' => $file->getFilename(), + 'path' => $file->getPathname(), + ]; + } + } + + return $resources; + } + + protected function getPermissions(string $path): array + { + $permissionsPath = "{$path}/Permissions.php"; + + if (File::exists($permissionsPath)) { + return require $permissionsPath; + } + + return []; + } + + public function getModuleSummary(): array + { + $modules = $this->discoverModules(); + + return [ + 'total' => count($modules), + 'with_models' => count(array_filter($modules, fn($m) => !empty($m['models']))), + 'with_filament' => count(array_filter($modules, fn($m) => !empty($m['filament_resources']))), + 'with_api' => count(array_filter($modules, fn($m) => $m['has_api'])), + ]; + } +} diff --git a/src/app/Services/ModuleGeneratorService.php b/src/app/Services/ModuleGeneratorService.php new file mode 100644 index 0000000..dcd0f6f --- /dev/null +++ b/src/app/Services/ModuleGeneratorService.php @@ -0,0 +1,492 @@ +studlyName = Str::studly($name); + $this->kebabName = Str::kebab($name); + $this->snakeName = Str::snake($name); + $this->modulePath = app_path("Modules/{$this->studlyName}"); + + $options = array_merge([ + 'description' => '', + 'create_git_branch' => true, + 'include_api' => false, + ], $options); + + // Check if module already exists + if (File::exists($this->modulePath)) { + return [ + 'success' => false, + 'message' => "Module {$this->studlyName} already exists!", + 'logs' => $this->logs, + ]; + } + + // Handle Git branching + $branchName = null; + if ($options['create_git_branch']) { + $gitResult = $this->createGitBranch(); + if (!$gitResult['success']) { + return $gitResult; + } + $branchName = $gitResult['branch']; + } + + // Generate module skeleton + $this->createDirectoryStructure(); + $this->createServiceProvider($options['description']); + $this->createConfig($options['description']); + $this->createPermissions(); + $this->createController(); + $this->createRoutes($options['include_api']); + $this->createViews(); + $this->createReadme($options['description']); + $this->registerServiceProvider(); + + // Git commit + if ($options['create_git_branch'] && $this->repo) { + $this->commitChanges(); + } + + return [ + 'success' => true, + 'message' => "Module {$this->studlyName} created successfully!", + 'module_name' => $this->studlyName, + 'module_path' => $this->modulePath, + 'branch' => $branchName, + 'logs' => $this->logs, + 'next_steps' => $this->getNextSteps($branchName), + ]; + } + + protected function createGitBranch(): array + { + try { + $git = new Git(); + $repoPath = base_path(); + + // Check if we're in a git repository + if (!File::exists($repoPath . '/.git')) { + $this->log('⚠ No Git repository found, skipping branch creation'); + return ['success' => true, 'branch' => null]; + } + + $this->repo = $git->open($repoPath); + + // Check for uncommitted changes + if ($this->repo->hasChanges()) { + return [ + 'success' => false, + 'message' => 'Git working directory has uncommitted changes. Please commit or stash them first.', + 'logs' => $this->logs, + ]; + } + + $branchName = "module/{$this->kebabName}"; + + // Create and checkout new branch + $this->repo->createBranch($branchName, true); + $this->log("✓ Created and checked out branch: {$branchName}"); + + return ['success' => true, 'branch' => $branchName]; + } catch (\Exception $e) { + return [ + 'success' => false, + 'message' => 'Git error: ' . $e->getMessage(), + 'logs' => $this->logs, + ]; + } + } + + protected function commitChanges(): void + { + try { + $this->repo->addAllChanges(); + $this->repo->commit("feat: Add {$this->studlyName} module skeleton"); + $this->log("✓ Committed changes to Git"); + } catch (\Exception $e) { + $this->log("⚠ Git commit failed: " . $e->getMessage()); + } + } + + protected function createDirectoryStructure(): void + { + $directories = [ + '', + '/Config', + '/Database/Migrations', + '/Database/Seeders', + '/Filament/Resources', + '/Http/Controllers', + '/Http/Middleware', + '/Http/Requests', + '/Models', + '/Policies', + '/Services', + '/Routes', + '/Resources/views', + ]; + + foreach ($directories as $dir) { + File::makeDirectory("{$this->modulePath}{$dir}", 0755, true); + } + + // Create .gitkeep files in empty directories + $emptyDirs = [ + '/Database/Migrations', + '/Database/Seeders', + '/Filament/Resources', + '/Http/Middleware', + '/Http/Requests', + '/Models', + '/Policies', + '/Services', + ]; + + foreach ($emptyDirs as $dir) { + File::put("{$this->modulePath}{$dir}/.gitkeep", ''); + } + + $this->log("✓ Created directory structure"); + } + + protected function createServiceProvider(string $description): void + { + $stub = <<studlyName}; + +use Illuminate\Support\ServiceProvider; +use Illuminate\Support\Facades\Route; + +class {$this->studlyName}ServiceProvider extends ServiceProvider +{ + public function register(): void + { + \$this->mergeConfigFrom( + __DIR__ . '/Config/{$this->snakeName}.php', + '{$this->snakeName}' + ); + } + + public function boot(): void + { + \$this->loadMigrationsFrom(__DIR__ . '/Database/Migrations'); + \$this->loadViewsFrom(__DIR__ . '/Resources/views', '{$this->kebabName}'); + + \$this->registerRoutes(); + \$this->registerPermissions(); + } + + protected function registerRoutes(): void + { + Route::middleware(['web', 'auth']) + ->prefix('{$this->kebabName}') + ->name('{$this->kebabName}.') + ->group(__DIR__ . '/Routes/web.php'); + + if (file_exists(__DIR__ . '/Routes/api.php')) { + Route::middleware(['api', 'auth:sanctum']) + ->prefix('api/{$this->kebabName}') + ->name('api.{$this->kebabName}.') + ->group(__DIR__ . '/Routes/api.php'); + } + } + + protected function registerPermissions(): void + { + // Permissions are registered via RolePermissionSeeder + // See: Permissions.php in this module + } +} +PHP; + + File::put("{$this->modulePath}/{$this->studlyName}ServiceProvider.php", $stub); + $this->log("✓ Created ServiceProvider"); + } + + protected function createConfig(string $description): void + { + $stub = << '{$this->studlyName}', + 'slug' => '{$this->kebabName}', + 'description' => '{$description}', + 'version' => '1.0.0', + + 'audit' => [ + 'enabled' => true, + 'strategy' => 'all', // 'all', 'include', 'exclude', 'none' + 'include' => [], + 'exclude' => [], + ], +]; +PHP; + + File::put("{$this->modulePath}/Config/{$this->snakeName}.php", $stub); + $this->log("✓ Created Config"); + } + + protected function createPermissions(): void + { + $stub = <<snakeName}.view' => 'View {$this->studlyName}', + '{$this->snakeName}.create' => 'Create {$this->studlyName} records', + '{$this->snakeName}.edit' => 'Edit {$this->studlyName} records', + '{$this->snakeName}.delete' => 'Delete {$this->studlyName} records', +]; +PHP; + + File::put("{$this->modulePath}/Permissions.php", $stub); + $this->log("✓ Created Permissions"); + } + + protected function createController(): void + { + $stub = <<studlyName}\Http\Controllers; + +use App\Http\Controllers\Controller; +use Illuminate\Http\Request; + +class {$this->studlyName}Controller extends Controller +{ + public function index() + { + \$this->authorize('{$this->snakeName}.view'); + + return view('{$this->kebabName}::index'); + } +} +PHP; + + File::put("{$this->modulePath}/Http/Controllers/{$this->studlyName}Controller.php", $stub); + $this->log("✓ Created Controller"); + } + + protected function createRoutes(bool $includeApi): void + { + // Web routes + $webStub = <<studlyName}\Http\Controllers\\{$this->studlyName}Controller; +use Illuminate\Support\Facades\Route; + +Route::get('/', [{$this->studlyName}Controller::class, 'index'])->name('index'); + +// Add more routes here: +// Route::get('/create', [{$this->studlyName}Controller::class, 'create'])->name('create'); +// Route::post('/', [{$this->studlyName}Controller::class, 'store'])->name('store'); +// Route::get('/{id}', [{$this->studlyName}Controller::class, 'show'])->name('show'); +// Route::get('/{id}/edit', [{$this->studlyName}Controller::class, 'edit'])->name('edit'); +// Route::put('/{id}', [{$this->studlyName}Controller::class, 'update'])->name('update'); +// Route::delete('/{id}', [{$this->studlyName}Controller::class, 'destroy'])->name('destroy'); +PHP; + + File::put("{$this->modulePath}/Routes/web.php", $webStub); + + // API routes (if requested) + if ($includeApi) { + $apiStub = <<group(function () { + // API routes here + // Route::get('/', fn() => response()->json(['message' => '{$this->studlyName} API'])); +}); +PHP; + + File::put("{$this->modulePath}/Routes/api.php", $apiStub); + $this->log("✓ Created Routes (web + api)"); + } else { + $this->log("✓ Created Routes (web)"); + } + } + + protected function createViews(): void + { + $stub = << + +

+ {{ __('{$this->studlyName}') }} +

+
+ +
+
+
+
+

{{ __('{$this->studlyName} Module') }}

+

+ This is your new module. Start building by adding models, controllers, and views. +

+ +
+

Quick Start:

+
    +
  • Add models in app/Modules/{$this->studlyName}/Models/
  • +
  • Add migrations in app/Modules/{$this->studlyName}/Database/Migrations/
  • +
  • Add Filament resources in app/Modules/{$this->studlyName}/Filament/Resources/
  • +
  • Extend routes in app/Modules/{$this->studlyName}/Routes/web.php
  • +
+
+
+
+
+
+ +BLADE; + + File::put("{$this->modulePath}/Resources/views/index.blade.php", $stub); + $this->log("✓ Created Views"); + } + + protected function createReadme(string $description): void + { + $stub = <<studlyName} Module + +{$description} + +## Structure + +``` +{$this->studlyName}/ +├── Config/{$this->snakeName}.php # Module configuration +├── Database/ +│ ├── Migrations/ # Database migrations +│ └── Seeders/ # Database seeders +├── Filament/Resources/ # Admin panel resources +├── Http/ +│ ├── Controllers/ # HTTP controllers +│ ├── Middleware/ # Module middleware +│ └── Requests/ # Form requests +├── Models/ # Eloquent models +├── Policies/ # Authorization policies +├── Services/ # Business logic services +├── Routes/ +│ ├── web.php # Web routes +│ └── api.php # API routes (if enabled) +├── Resources/views/ # Blade templates +├── Permissions.php # Module permissions +├── {$this->studlyName}ServiceProvider.php +└── README.md +``` + +## Permissions + +| Permission | Description | +|------------|-------------| +| `{$this->snakeName}.view` | View {$this->studlyName} | +| `{$this->snakeName}.create` | Create records | +| `{$this->snakeName}.edit` | Edit records | +| `{$this->snakeName}.delete` | Delete records | + +## Routes + +| Method | URI | Name | Description | +|--------|-----|------|-------------| +| GET | `/{$this->kebabName}` | `{$this->kebabName}.index` | Module index | + +## Getting Started + +1. **Add a Model:** + ```bash + # Create model manually or use artisan + php artisan make:model Modules/{$this->studlyName}/Models/YourModel -m + ``` + +2. **Run Migrations:** + ```bash + php artisan migrate + ``` + +3. **Seed Permissions:** + ```bash + php artisan db:seed --class=RolePermissionSeeder + ``` + +4. **Add Filament Resource:** + Create resources in `Filament/Resources/` following the Filament documentation. + +## Configuration + +Edit `Config/{$this->snakeName}.php` to customize module settings including audit behavior. +MD; + + File::put("{$this->modulePath}/README.md", $stub); + $this->log("✓ Created README.md"); + } + + protected function registerServiceProvider(): void + { + $providersPath = base_path('bootstrap/providers.php'); + + if (File::exists($providersPath)) { + $content = File::get($providersPath); + $providerClass = "App\\Modules\\{$this->studlyName}\\{$this->studlyName}ServiceProvider::class"; + + if (!str_contains($content, $providerClass)) { + $content = preg_replace( + '/(\];)/', + " {$providerClass},\n$1", + $content + ); + File::put($providersPath, $content); + $this->log("✓ Registered ServiceProvider"); + } + } else { + $this->log("⚠ Could not auto-register ServiceProvider"); + } + } + + protected function getNextSteps(?string $branchName): array + { + $steps = [ + "Run migrations: `php artisan migrate`", + "Seed permissions: `php artisan db:seed --class=RolePermissionSeeder`", + "Clear caches: `php artisan optimize:clear`", + "Access frontend: `/{$this->kebabName}`", + ]; + + if ($branchName) { + $steps[] = "Push branch: `git push -u origin {$branchName}`"; + $steps[] = "Create merge request when ready"; + } + + return $steps; + } + + protected function log(string $message): void + { + $this->logs[] = $message; + } +} diff --git a/src/composer.json b/src/composer.json index 13a86e0..7a0223e 100644 --- a/src/composer.json +++ b/src/composer.json @@ -17,6 +17,7 @@ "spatie/laravel-permission": "^6.24" }, "require-dev": { + "czproject/git-php": "^4.6", "fakerphp/faker": "^1.23", "laravel/breeze": "^2.3", "laravel/pail": "^1.1", diff --git a/src/composer.lock b/src/composer.lock index a06361f..3c8231e 100644 --- a/src/composer.lock +++ b/src/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "1f84406b8f4e254677813bb043dd37c3", + "content-hash": "384bae7218ddf4c1e2b18a8c99946102", "packages": [ { "name": "anourvalar/eloquent-serialize", @@ -8305,6 +8305,70 @@ ], "time": "2026-01-08T08:02:38+00:00" }, + { + "name": "czproject/git-php", + "version": "v4.6.0", + "source": { + "type": "git", + "url": "https://github.com/czproject/git-php.git", + "reference": "1f1ecc92aea9ee31120f4f5b759f5aa947420b0a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/czproject/git-php/zipball/1f1ecc92aea9ee31120f4f5b759f5aa947420b0a", + "reference": "1f1ecc92aea9ee31120f4f5b759f5aa947420b0a", + "shasum": "" + }, + "require": { + "php": "8.0 - 8.5" + }, + "require-dev": { + "nette/tester": "^2.5" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jan Pecha", + "email": "janpecha@email.cz" + } + ], + "description": "Library for work with Git repository in PHP.", + "keywords": [ + "git" + ], + "support": { + "issues": "https://github.com/czproject/git-php/issues", + "source": "https://github.com/czproject/git-php/tree/v4.6.0" + }, + "funding": [ + { + "url": "https://github.com/sponsors/janpecha", + "type": "github" + }, + { + "url": "https://www.janpecha.cz/donate/git-php/", + "type": "other" + }, + { + "url": "https://donate.stripe.com/7sIcO2a9maTSg2A9AA", + "type": "stripe" + }, + { + "url": "https://thanks.dev/u/gh/czproject", + "type": "thanks.dev" + } + ], + "time": "2025-11-10T07:24:07+00:00" + }, { "name": "fakerphp/faker", "version": "v1.24.1", diff --git a/src/resources/views/filament/pages/module-generator.blade.php b/src/resources/views/filament/pages/module-generator.blade.php new file mode 100644 index 0000000..256dc64 --- /dev/null +++ b/src/resources/views/filament/pages/module-generator.blade.php @@ -0,0 +1,315 @@ + +
+ {{-- Warning Banner --}} +
+
+ +
+ Development Only: This tool is only available in the local environment. + It creates module skeletons and optionally manages Git branches. +
+
+
+ + {{-- Registered Modules Section --}} +
+
+
+

+ + Registered Modules +

+
+ + {{ $summary['total'] ?? 0 }} Total + + + {{ $summary['with_models'] ?? 0 }} with Models + + + {{ $summary['with_filament'] ?? 0 }} with Filament + + + {{ $summary['with_api'] ?? 0 }} with API + +
+
+
+ + @if(empty($modules)) +
+ +

No modules registered yet. Create your first module below!

+
+ @else +
+ @foreach($modules as $module) +
+ {{-- Module Header --}} + + + {{-- Module Details (Expandable) --}} + @if($expandedModule === $module['name']) +
+
+ {{-- Models --}} +
+

+ + Models +

+ @if(count($module['models']) > 0) +
    + @foreach($module['models'] as $model) +
  • {{ $model['name'] }}
  • + @endforeach +
+ @else +

No models

+ @endif +
+ + {{-- Views --}} +
+

+ + Views +

+ @if(count($module['views']) > 0) +
    + @foreach($module['views'] as $view) +
  • {{ $module['slug'] }}::{{ $view['name'] }}
  • + @endforeach +
+ @else +

No views

+ @endif +
+ + {{-- Filament Resources --}} +
+

+ + Filament Resources +

+ @if(count($module['filament_resources']) > 0) +
    + @foreach($module['filament_resources'] as $resource) +
  • {{ $resource['name'] }}Resource
  • + @endforeach +
+ @else +

No Filament resources

+ @endif +
+ + {{-- Routes --}} +
+

+ + Routes +

+ @if(!empty($module['routes']['web'])) +
    + @foreach($module['routes']['web'] as $route) +
  • + {{ $route['method'] }} + {{ $route['uri'] }} +
  • + @endforeach +
+ @else +

No web routes parsed

+ @endif +
+ + {{-- Migrations --}} +
+

+ + Migrations +

+ @if(count($module['migrations']) > 0) +
    + @foreach($module['migrations'] as $migration) +
  • {{ $migration['name'] }}
  • + @endforeach +
+ @else +

No migrations

+ @endif +
+ + {{-- Permissions --}} +
+

+ + Permissions +

+ @if(count($module['permissions']) > 0) +
    + @foreach($module['permissions'] as $key => $label) +
  • {{ $key }}
  • + @endforeach +
+ @else +

No permissions

+ @endif +
+
+ + {{-- Module Path --}} +
+ Path: {{ $module['path'] }} +
+
+ @endif +
+ @endforeach +
+ @endif +
+ + {{-- Create Module Form --}} +
+

+ + Create New Module +

+ +
+ {{ $this->form }} + +
+ + + Generate Module + +
+
+
+ + {{-- Result Panel --}} + @if($result) +
+ @if($result['success']) +
+
+ +

+ Module Created Successfully! +

+
+ +
+
+ Module: + {{ $result['module_name'] }} +
+ +
+ Path: + {{ $result['module_path'] }} +
+ + @if($result['branch']) +
+ Git Branch: + {{ $result['branch'] }} +
+ @endif + + {{-- Logs --}} +
+ Generation Log: +
+ @foreach($result['logs'] as $log) +
{{ $log }}
+ @endforeach +
+
+ + {{-- Next Steps --}} +
+ Next Steps: +
    + @foreach($result['next_steps'] as $step) +
  1. {!! preg_replace('/`([^`]+)`/', '$1', e($step)) !!}
  2. + @endforeach +
+
+
+ +
+ + Create Another Module + +
+
+ @else +
+
+ +

+ Module Creation Failed +

+
+ +

{{ $result['message'] }}

+ + @if(!empty($result['logs'])) +
+ @foreach($result['logs'] as $log) +
{{ $log }}
+ @endforeach +
+ @endif + +
+ + Try Again + +
+
+ @endif +
+ @endif +
+