Compare commits
42 Commits
7af4f1d14a
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| f5c059cd08 | |||
| 21620906dc | |||
| 14084e172a | |||
| 6fe63d70af | |||
| fdbd8f5148 | |||
| cd6043ed64 | |||
| eb05ec9b31 | |||
| db0cf0c2c4 | |||
| a6b1cfe498 | |||
| dff2cd752c | |||
| 94f4e53860 | |||
| 779702ca3f | |||
| ed7055edaa | |||
| 6d2d4ad5ca | |||
| 551024cc32 | |||
| 1995f58056 | |||
| bcb7997ba0 | |||
| a0a722971d | |||
| 754153ed1c | |||
| a1a0d6a6c3 | |||
| 1d83a3c830 | |||
| fa4c787beb | |||
| c39a04315c | |||
| e380721938 | |||
| f903914c2b | |||
| 2b153f1759 | |||
| ac16dbc153 | |||
| 12027d9e98 | |||
| e4c7d09109 | |||
| cf6079d58c | |||
| 4b2ff91ac4 | |||
| e274d41f19 | |||
| 2ff7a24736 | |||
| 119eaf1873 | |||
| 306413ca56 | |||
| b1453ff249 | |||
| ac11580eb3 | |||
| 0d2506f3ef | |||
| 73a4cd8c40 | |||
| e7fcaa35e1 | |||
| 83131d8432 | |||
| 97acc5e8ea |
34
.dockerignore
Normal file
34
.dockerignore
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# Dependencies
|
||||||
|
src/node_modules
|
||||||
|
src/vendor
|
||||||
|
|
||||||
|
# Git
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Build artifacts
|
||||||
|
src/public/build
|
||||||
|
src/public/hot
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
src/coverage
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
src/storage/logs/*
|
||||||
|
|
||||||
|
# Cache
|
||||||
|
src/bootstrap/cache/*
|
||||||
|
src/storage/framework/cache/*
|
||||||
|
src/storage/framework/sessions/*
|
||||||
|
src/storage/framework/views/*
|
||||||
@@ -1,7 +1,25 @@
|
|||||||
# AI Assistant Context
|
# AI Assistant Context
|
||||||
|
|
||||||
|
> **🚨 CRITICAL: For detailed module development instructions, see [CLAUDE.md](CLAUDE.md)**
|
||||||
|
> That document contains EXACT file templates, naming conventions, and step-by-step instructions.
|
||||||
|
|
||||||
This document provides context for AI coding assistants working on projects built with this template.
|
This document provides context for AI coding assistants working on projects built with this template.
|
||||||
|
|
||||||
|
## Development Workflow Rules
|
||||||
|
|
||||||
|
### Git Branching Strategy
|
||||||
|
|
||||||
|
**Rule**: The AI agent must create a new Git branch for every new feature, module, or significant change. It should never commit or push directly to the `main` branch.
|
||||||
|
|
||||||
|
**Rationale**: This practice is crucial for maintaining a clean and stable main branch, facilitating code reviews, and making it easier for human developers to collaborate and manage the project's history.
|
||||||
|
|
||||||
|
**Example Workflow**:
|
||||||
|
1. `git checkout -b feature/new-company-module`
|
||||||
|
2. *...perform all work related to the new module...*
|
||||||
|
3. `git add .`
|
||||||
|
4. `git commit -m "feat: Create new company module"`
|
||||||
|
5. *...agent informs the user that the feature is complete on the new branch...*
|
||||||
|
|
||||||
## Template Overview
|
## Template Overview
|
||||||
|
|
||||||
This is a **ready-to-use Laravel Docker Development Template** with everything pre-installed:
|
This is a **ready-to-use Laravel Docker Development Template** with everything pre-installed:
|
||||||
@@ -331,6 +349,7 @@ make restore file=... # Restore database
|
|||||||
|
|
||||||
## Documentation Links
|
## Documentation Links
|
||||||
|
|
||||||
|
- **[CLAUDE.md](CLAUDE.md)** - 🚨 **AI AGENTS: START HERE** - Exact module templates
|
||||||
- [GETTING_STARTED.md](GETTING_STARTED.md) - Setup walkthrough
|
- [GETTING_STARTED.md](GETTING_STARTED.md) - Setup walkthrough
|
||||||
- [README.md](README.md) - Overview
|
- [README.md](README.md) - Overview
|
||||||
- [docs/modules.md](docs/modules.md) - Module system
|
- [docs/modules.md](docs/modules.md) - Module system
|
||||||
|
|||||||
782
CLAUDE.md
Normal file
782
CLAUDE.md
Normal file
@@ -0,0 +1,782 @@
|
|||||||
|
# AI Agent Instructions for Laravel Module Development
|
||||||
|
|
||||||
|
> **CRITICAL**: This document provides EXACT specifications for building modules in this Laravel template.
|
||||||
|
> Follow these instructions PRECISELY. Do NOT deviate, guess, or improvise.
|
||||||
|
> When in doubt, READ THE EXISTING CODE before making changes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚨 BEFORE YOU START - MANDATORY STEPS
|
||||||
|
|
||||||
|
### 1. Always Check Existing Patterns First
|
||||||
|
```bash
|
||||||
|
# List existing modules to see patterns
|
||||||
|
ls -la src/app/Modules/
|
||||||
|
|
||||||
|
# Read an existing module's structure
|
||||||
|
find src/app/Modules/[ExistingModule]/ -type f -name "*.php"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Always Check Logs After Errors
|
||||||
|
```bash
|
||||||
|
docker-compose exec app tail -n 100 storage/logs/laravel.log
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Never Guess at Solutions
|
||||||
|
- If you encounter an error, READ the full stack trace
|
||||||
|
- Find the EXACT file and line number causing the issue
|
||||||
|
- Understand what the code is trying to do before fixing
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📁 PROJECT STRUCTURE - EXACT PATHS
|
||||||
|
|
||||||
|
```
|
||||||
|
Laravel-Docker-Dev-Template/
|
||||||
|
├── src/ # ← ALL Laravel code goes here
|
||||||
|
│ ├── app/
|
||||||
|
│ │ ├── Filament/
|
||||||
|
│ │ │ ├── Pages/
|
||||||
|
│ │ │ │ └── Settings.php # Site settings page
|
||||||
|
│ │ │ └── Resources/ # Core Filament resources
|
||||||
|
│ │ │ ├── UserResource.php
|
||||||
|
│ │ │ ├── RoleResource.php
|
||||||
|
│ │ │ └── PermissionResource.php
|
||||||
|
│ │ ├── Http/
|
||||||
|
│ │ │ ├── Controllers/
|
||||||
|
│ │ │ └── Middleware/
|
||||||
|
│ │ ├── Models/
|
||||||
|
│ │ │ ├── User.php
|
||||||
|
│ │ │ └── Setting.php
|
||||||
|
│ │ ├── Modules/ # ← ALL modules go here
|
||||||
|
│ │ │ └── [ModuleName]/ # ← Each module is a folder
|
||||||
|
│ │ ├── Providers/
|
||||||
|
│ │ │ └── AppServiceProvider.php
|
||||||
|
│ │ └── Traits/
|
||||||
|
│ │ └── ModuleAuditable.php
|
||||||
|
│ ├── bootstrap/
|
||||||
|
│ │ └── app.php # Middleware registration
|
||||||
|
│ ├── config/
|
||||||
|
│ ├── database/
|
||||||
|
│ │ ├── migrations/
|
||||||
|
│ │ └── seeders/
|
||||||
|
│ │ ├── DatabaseSeeder.php
|
||||||
|
│ │ └── RolePermissionSeeder.php
|
||||||
|
│ ├── resources/
|
||||||
|
│ │ └── views/
|
||||||
|
│ │ ├── layouts/
|
||||||
|
│ │ │ ├── app.blade.php # Authenticated layout
|
||||||
|
│ │ │ └── guest.blade.php # Guest layout
|
||||||
|
│ │ └── components/
|
||||||
|
│ └── routes/
|
||||||
|
│ ├── web.php
|
||||||
|
│ ├── api.php
|
||||||
|
│ └── auth.php
|
||||||
|
├── docker-compose.yml
|
||||||
|
├── setup.bat # Windows setup
|
||||||
|
└── setup.sh # Linux/Mac setup
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 MODULE STRUCTURE - EXACT SPECIFICATION
|
||||||
|
|
||||||
|
When creating a module named `[ModuleName]` (e.g., `StockManagement`):
|
||||||
|
|
||||||
|
```
|
||||||
|
src/app/Modules/[ModuleName]/
|
||||||
|
├── Config/
|
||||||
|
│ └── [module_name].php # snake_case filename
|
||||||
|
├── Database/
|
||||||
|
│ ├── Migrations/
|
||||||
|
│ │ └── YYYY_MM_DD_HHMMSS_create_[table_name]_table.php
|
||||||
|
│ └── Seeders/
|
||||||
|
│ └── [ModuleName]Seeder.php
|
||||||
|
├── Filament/
|
||||||
|
│ └── Resources/
|
||||||
|
│ ├── [ModelName]Resource.php
|
||||||
|
│ └── [ModelName]Resource/
|
||||||
|
│ └── Pages/
|
||||||
|
│ ├── List[ModelName]s.php
|
||||||
|
│ ├── Create[ModelName].php
|
||||||
|
│ └── Edit[ModelName].php
|
||||||
|
├── Http/
|
||||||
|
│ ├── Controllers/
|
||||||
|
│ │ └── [ModuleName]Controller.php
|
||||||
|
│ ├── Middleware/
|
||||||
|
│ └── Requests/
|
||||||
|
│ └── [ModelName]Request.php
|
||||||
|
├── Models/
|
||||||
|
│ └── [ModelName].php
|
||||||
|
├── Policies/
|
||||||
|
│ └── [ModelName]Policy.php
|
||||||
|
├── Services/
|
||||||
|
│ └── [ModelName]Service.php
|
||||||
|
├── Routes/
|
||||||
|
│ ├── web.php
|
||||||
|
│ └── api.php
|
||||||
|
├── Resources/
|
||||||
|
│ └── views/
|
||||||
|
│ ├── index.blade.php
|
||||||
|
│ └── layouts/
|
||||||
|
│ └── module.blade.php
|
||||||
|
├── Permissions.php
|
||||||
|
└── [ModuleName]ServiceProvider.php
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 EXACT FILE TEMPLATES
|
||||||
|
|
||||||
|
### 1. Service Provider (REQUIRED)
|
||||||
|
|
||||||
|
**File**: `src/app/Modules/[ModuleName]/[ModuleName]ServiceProvider.php`
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Modules\[ModuleName];
|
||||||
|
|
||||||
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
|
class [ModuleName]ServiceProvider extends ServiceProvider
|
||||||
|
{
|
||||||
|
public function register(): void
|
||||||
|
{
|
||||||
|
$this->mergeConfigFrom(
|
||||||
|
__DIR__ . '/Config/[module_name].php',
|
||||||
|
'[module_name]'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function boot(): void
|
||||||
|
{
|
||||||
|
$this->loadMigrationsFrom(__DIR__ . '/Database/Migrations');
|
||||||
|
$this->loadViewsFrom(__DIR__ . '/Resources/views', '[module-slug]');
|
||||||
|
|
||||||
|
$this->registerRoutes();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function registerRoutes(): void
|
||||||
|
{
|
||||||
|
Route::middleware(['web', 'auth'])
|
||||||
|
->prefix('[module-slug]')
|
||||||
|
->name('[module-slug].')
|
||||||
|
->group(__DIR__ . '/Routes/web.php');
|
||||||
|
|
||||||
|
if (file_exists(__DIR__ . '/Routes/api.php')) {
|
||||||
|
Route::middleware(['api', 'auth:sanctum'])
|
||||||
|
->prefix('api/[module-slug]')
|
||||||
|
->name('api.[module-slug].')
|
||||||
|
->group(__DIR__ . '/Routes/api.php');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**NAMING RULES**:
|
||||||
|
- `[ModuleName]` = PascalCase (e.g., `StockManagement`)
|
||||||
|
- `[module_name]` = snake_case (e.g., `stock_management`)
|
||||||
|
- `[module-slug]` = kebab-case (e.g., `stock-management`)
|
||||||
|
|
||||||
|
### 2. Config File (REQUIRED)
|
||||||
|
|
||||||
|
**File**: `src/app/Modules/[ModuleName]/Config/[module_name].php`
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
'name' => '[Module Display Name]',
|
||||||
|
'slug' => '[module-slug]',
|
||||||
|
'version' => '1.0.0',
|
||||||
|
|
||||||
|
'audit' => [
|
||||||
|
'enabled' => true,
|
||||||
|
'strategy' => 'all', // 'all', 'include', 'exclude', 'none'
|
||||||
|
'include' => [],
|
||||||
|
'exclude' => [],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Model (REQUIRED for data modules)
|
||||||
|
|
||||||
|
**File**: `src/app/Modules/[ModuleName]/Models/[ModelName].php`
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Modules\[ModuleName]\Models;
|
||||||
|
|
||||||
|
use App\Traits\ModuleAuditable;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use OwenIt\Auditing\Contracts\Auditable;
|
||||||
|
|
||||||
|
class [ModelName] extends Model implements Auditable
|
||||||
|
{
|
||||||
|
use HasFactory, ModuleAuditable;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
// List ALL fields that can be mass-assigned
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
// Type casts for fields
|
||||||
|
];
|
||||||
|
|
||||||
|
// Define relationships here
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Migration (REQUIRED for data modules)
|
||||||
|
|
||||||
|
**File**: `src/app/Modules/[ModuleName]/Database/Migrations/YYYY_MM_DD_HHMMSS_create_[table_name]_table.php`
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('[table_name]', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
// Define ALL columns here
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('[table_name]');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Permissions (REQUIRED) - AUTO-LOADED
|
||||||
|
|
||||||
|
**File**: `src/app/Modules/[ModuleName]/Permissions.php`
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
'[module_name].view' => 'View [Module Name]',
|
||||||
|
'[module_name].create' => 'Create [Module Name] records',
|
||||||
|
'[module_name].edit' => 'Edit [Module Name] records',
|
||||||
|
'[module_name].delete' => 'Delete [Module Name] records',
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
> **✅ AUTO-LOADED**: `RolePermissionSeeder` automatically scans all `app/Modules/*/Permissions.php`
|
||||||
|
> files and registers them. Just run `php artisan db:seed --class=RolePermissionSeeder`.
|
||||||
|
|
||||||
|
### 6. Filament Resource (REQUIRED for admin)
|
||||||
|
|
||||||
|
**File**: `src/app/Modules/[ModuleName]/Filament/Resources/[ModelName]Resource.php`
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Modules\[ModuleName]\Filament\Resources;
|
||||||
|
|
||||||
|
use App\Modules\[ModuleName]\Filament\Resources\[ModelName]Resource\Pages;
|
||||||
|
use App\Modules\[ModuleName]\Models\[ModelName];
|
||||||
|
use Filament\Forms;
|
||||||
|
use Filament\Forms\Form;
|
||||||
|
use Filament\Resources\Resource;
|
||||||
|
use Filament\Tables;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
|
||||||
|
class [ModelName]Resource extends Resource
|
||||||
|
{
|
||||||
|
protected static ?string $model = [ModelName]::class;
|
||||||
|
|
||||||
|
protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack';
|
||||||
|
|
||||||
|
protected static ?string $navigationGroup = '[Module Display Name]';
|
||||||
|
|
||||||
|
protected static ?int $navigationSort = 1;
|
||||||
|
|
||||||
|
public static function canAccess(): bool
|
||||||
|
{
|
||||||
|
return auth()->user()?->can('[module_name].view') ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function form(Form $form): Form
|
||||||
|
{
|
||||||
|
return $form
|
||||||
|
->schema([
|
||||||
|
Forms\Components\Section::make('[Model Name] Details')
|
||||||
|
->schema([
|
||||||
|
// Add form fields here
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function table(Table $table): Table
|
||||||
|
{
|
||||||
|
return $table
|
||||||
|
->columns([
|
||||||
|
// Add table columns here
|
||||||
|
])
|
||||||
|
->filters([
|
||||||
|
//
|
||||||
|
])
|
||||||
|
->actions([
|
||||||
|
Tables\Actions\EditAction::make(),
|
||||||
|
Tables\Actions\DeleteAction::make(),
|
||||||
|
])
|
||||||
|
->bulkActions([
|
||||||
|
Tables\Actions\BulkActionGroup::make([
|
||||||
|
Tables\Actions\DeleteBulkAction::make(),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getRelations(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
//
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getPages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'index' => Pages\List[ModelName]s::route('/'),
|
||||||
|
'create' => Pages\Create[ModelName]::route('/create'),
|
||||||
|
'edit' => Pages\Edit[ModelName]::route('/{record}/edit'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Filament Resource Pages (REQUIRED)
|
||||||
|
|
||||||
|
**File**: `src/app/Modules/[ModuleName]/Filament/Resources/[ModelName]Resource/Pages/List[ModelName]s.php`
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Modules\[ModuleName]\Filament\Resources\[ModelName]Resource\Pages;
|
||||||
|
|
||||||
|
use App\Modules\[ModuleName]\Filament\Resources\[ModelName]Resource;
|
||||||
|
use Filament\Actions;
|
||||||
|
use Filament\Resources\Pages\ListRecords;
|
||||||
|
|
||||||
|
class List[ModelName]s extends ListRecords
|
||||||
|
{
|
||||||
|
protected static string $resource = [ModelName]Resource::class;
|
||||||
|
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Actions\CreateAction::make(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**File**: `src/app/Modules/[ModuleName]/Filament/Resources/[ModelName]Resource/Pages/Create[ModelName].php`
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Modules\[ModuleName]\Filament\Resources\[ModelName]Resource\Pages;
|
||||||
|
|
||||||
|
use App\Modules\[ModuleName]\Filament\Resources\[ModelName]Resource;
|
||||||
|
use Filament\Resources\Pages\CreateRecord;
|
||||||
|
|
||||||
|
class Create[ModelName] extends CreateRecord
|
||||||
|
{
|
||||||
|
protected static string $resource = [ModelName]Resource::class;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**File**: `src/app/Modules/[ModuleName]/Filament/Resources/[ModelName]Resource/Pages/Edit[ModelName].php`
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Modules\[ModuleName]\Filament\Resources\[ModelName]Resource\Pages;
|
||||||
|
|
||||||
|
use App\Modules\[ModuleName]\Filament\Resources\[ModelName]Resource;
|
||||||
|
use Filament\Actions;
|
||||||
|
use Filament\Resources\Pages\EditRecord;
|
||||||
|
|
||||||
|
class Edit[ModelName] extends EditRecord
|
||||||
|
{
|
||||||
|
protected static string $resource = [ModelName]Resource::class;
|
||||||
|
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Actions\DeleteAction::make(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8. Routes (REQUIRED)
|
||||||
|
|
||||||
|
**File**: `src/app/Modules/[ModuleName]/Routes/web.php`
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Modules\[ModuleName]\Http\Controllers\[ModuleName]Controller;
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
|
Route::get('/', [[ModuleName]Controller::class, 'index'])->name('index');
|
||||||
|
```
|
||||||
|
|
||||||
|
**File**: `src/app/Modules/[ModuleName]/Routes/api.php` (if API needed)
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
|
Route::middleware('auth:sanctum')->group(function () {
|
||||||
|
// API routes here
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9. Controller (REQUIRED)
|
||||||
|
|
||||||
|
**File**: `src/app/Modules/[ModuleName]/Http/Controllers/[ModuleName]Controller.php`
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Modules\[ModuleName]\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class [ModuleName]Controller extends Controller
|
||||||
|
{
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$this->authorize('[module_name].view');
|
||||||
|
|
||||||
|
return view('[module-slug]::index');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 10. Index View (REQUIRED)
|
||||||
|
|
||||||
|
**File**: `src/app/Modules/[ModuleName]/Resources/views/index.blade.php`
|
||||||
|
```blade
|
||||||
|
<x-app-layout>
|
||||||
|
<x-slot name="header">
|
||||||
|
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
|
||||||
|
{{ __('[Module Display Name]') }}
|
||||||
|
</h2>
|
||||||
|
</x-slot>
|
||||||
|
|
||||||
|
<div class="py-12">
|
||||||
|
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||||
|
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
|
||||||
|
<div class="p-6 text-gray-900 dark:text-gray-100">
|
||||||
|
{{ __('[Module Display Name] content goes here.') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</x-app-layout>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ MODULE REGISTRATION
|
||||||
|
|
||||||
|
After creating all files, register the module in `src/config/app.php` providers array:
|
||||||
|
|
||||||
|
```php
|
||||||
|
'providers' => ServiceProvider::defaultProviders()->merge([
|
||||||
|
// ...existing providers...
|
||||||
|
App\Modules\[ModuleName]\[ModuleName]ServiceProvider::class,
|
||||||
|
])->toArray(),
|
||||||
|
```
|
||||||
|
|
||||||
|
Or add to `src/bootstrap/providers.php`:
|
||||||
|
```php
|
||||||
|
return [
|
||||||
|
App\Providers\AppServiceProvider::class,
|
||||||
|
// ...existing providers...
|
||||||
|
App\Modules\[ModuleName]\[ModuleName]ServiceProvider::class,
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 AFTER MODULE CREATION - REQUIRED COMMANDS
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Run migrations
|
||||||
|
docker-compose exec app php artisan migrate
|
||||||
|
|
||||||
|
# 2. Clear all caches
|
||||||
|
docker-compose exec app php artisan optimize:clear
|
||||||
|
|
||||||
|
# 3. Seed permissions (if using permissions)
|
||||||
|
docker-compose exec app php artisan db:seed --class=RolePermissionSeeder
|
||||||
|
|
||||||
|
# 4. Reset permission cache
|
||||||
|
docker-compose exec app php artisan permission:cache-reset
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ COMMON MISTAKES TO AVOID
|
||||||
|
|
||||||
|
### 1. Wrong Namespace
|
||||||
|
❌ `namespace App\Modules\StockManagement\Models;`
|
||||||
|
✅ `namespace App\Modules\StockManagement\Models;`
|
||||||
|
|
||||||
|
The namespace MUST match the folder structure EXACTLY.
|
||||||
|
|
||||||
|
### 2. Wrong View Namespace
|
||||||
|
❌ `return view('stockmanagement::index');`
|
||||||
|
❌ `return view('StockManagement::index');`
|
||||||
|
✅ `return view('stock-management::index');`
|
||||||
|
|
||||||
|
View namespace is ALWAYS kebab-case.
|
||||||
|
|
||||||
|
### 3. Wrong Permission Names
|
||||||
|
❌ `'StockManagement.view'`
|
||||||
|
❌ `'stock-management.view'`
|
||||||
|
✅ `'stock_management.view'`
|
||||||
|
|
||||||
|
Permissions are ALWAYS snake_case.
|
||||||
|
|
||||||
|
### 4. Forgetting to Register Provider
|
||||||
|
The module WILL NOT LOAD if you forget to add its ServiceProvider.
|
||||||
|
|
||||||
|
### 5. Wrong Import Paths
|
||||||
|
❌ `use App\Models\Product;`
|
||||||
|
✅ `use App\Modules\StockManagement\Models\Product;`
|
||||||
|
|
||||||
|
Module models are in the MODULE namespace, not the core App namespace.
|
||||||
|
|
||||||
|
### 6. Missing Fillable Array
|
||||||
|
❌ Empty `$fillable` array causes mass-assignment errors
|
||||||
|
✅ List ALL fields that should be mass-assignable
|
||||||
|
|
||||||
|
### 7. Forgetting to Run Migrations
|
||||||
|
Always run `php artisan migrate` after creating migrations.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 FILAMENT FORM FIELD REFERENCE
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Text
|
||||||
|
Forms\Components\TextInput::make('name')
|
||||||
|
->required()
|
||||||
|
->maxLength(255);
|
||||||
|
|
||||||
|
// Email
|
||||||
|
Forms\Components\TextInput::make('email')
|
||||||
|
->email()
|
||||||
|
->required();
|
||||||
|
|
||||||
|
// Password
|
||||||
|
Forms\Components\TextInput::make('password')
|
||||||
|
->password()
|
||||||
|
->required();
|
||||||
|
|
||||||
|
// Textarea
|
||||||
|
Forms\Components\Textarea::make('description')
|
||||||
|
->rows(3);
|
||||||
|
|
||||||
|
// Select
|
||||||
|
Forms\Components\Select::make('status')
|
||||||
|
->options([
|
||||||
|
'active' => 'Active',
|
||||||
|
'inactive' => 'Inactive',
|
||||||
|
])
|
||||||
|
->required();
|
||||||
|
|
||||||
|
// Checkbox
|
||||||
|
Forms\Components\Toggle::make('is_active')
|
||||||
|
->default(true);
|
||||||
|
|
||||||
|
// Date
|
||||||
|
Forms\Components\DatePicker::make('date');
|
||||||
|
|
||||||
|
// DateTime
|
||||||
|
Forms\Components\DateTimePicker::make('published_at');
|
||||||
|
|
||||||
|
// Number
|
||||||
|
Forms\Components\TextInput::make('price')
|
||||||
|
->numeric()
|
||||||
|
->prefix('$');
|
||||||
|
|
||||||
|
// File Upload
|
||||||
|
Forms\Components\FileUpload::make('image')
|
||||||
|
->image()
|
||||||
|
->directory('uploads');
|
||||||
|
|
||||||
|
// Relationship Select
|
||||||
|
Forms\Components\Select::make('category_id')
|
||||||
|
->relationship('category', 'name')
|
||||||
|
->searchable()
|
||||||
|
->preload();
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 FILAMENT TABLE COLUMN REFERENCE
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Text
|
||||||
|
Tables\Columns\TextColumn::make('name')
|
||||||
|
->searchable()
|
||||||
|
->sortable();
|
||||||
|
|
||||||
|
// Badge
|
||||||
|
Tables\Columns\TextColumn::make('status')
|
||||||
|
->badge()
|
||||||
|
->color(fn (string $state): string => match ($state) {
|
||||||
|
'active' => 'success',
|
||||||
|
'inactive' => 'danger',
|
||||||
|
default => 'gray',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Boolean Icon
|
||||||
|
Tables\Columns\IconColumn::make('is_active')
|
||||||
|
->boolean();
|
||||||
|
|
||||||
|
// Image
|
||||||
|
Tables\Columns\ImageColumn::make('avatar')
|
||||||
|
->circular();
|
||||||
|
|
||||||
|
// Date
|
||||||
|
Tables\Columns\TextColumn::make('created_at')
|
||||||
|
->dateTime()
|
||||||
|
->sortable();
|
||||||
|
|
||||||
|
// Money
|
||||||
|
Tables\Columns\TextColumn::make('price')
|
||||||
|
->money('USD');
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 EXISTING CORE RESOURCES
|
||||||
|
|
||||||
|
### User Management (already exists in template)
|
||||||
|
- `src/app/Filament/Resources/UserResource.php` - User CRUD with role assignment
|
||||||
|
- `src/app/Filament/Resources/RoleResource.php` - Role CRUD with permissions
|
||||||
|
- `src/app/Filament/Resources/PermissionResource.php` - Permission CRUD
|
||||||
|
|
||||||
|
### Settings (already exists)
|
||||||
|
- `src/app/Filament/Pages/Settings.php` - Site settings (name, logo, colors, registration toggle)
|
||||||
|
- `src/app/Models/Setting.php` - Settings model with get/set helpers
|
||||||
|
|
||||||
|
### Authentication (pre-installed)
|
||||||
|
- Laravel Breeze with Blade templates
|
||||||
|
- Login: `/login`
|
||||||
|
- Register: `/register` (controlled by enable_registration setting)
|
||||||
|
- Admin: `/admin`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 TESTING MODULES
|
||||||
|
|
||||||
|
Create tests in `src/tests/Feature/Modules/[ModuleName]/`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature\Modules\[ModuleName];
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Modules\[ModuleName]\Models\[ModelName];
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class [ModelName]Test extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
public function test_user_can_view_[model_name]s(): void
|
||||||
|
{
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$user->givePermissionTo('[module_name].view');
|
||||||
|
|
||||||
|
$this->actingAs($user)
|
||||||
|
->get('/[module-slug]')
|
||||||
|
->assertStatus(200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 CHECKLIST FOR NEW MODULES
|
||||||
|
|
||||||
|
Before considering a module complete:
|
||||||
|
|
||||||
|
- [ ] ServiceProvider created and registered
|
||||||
|
- [ ] Config file created
|
||||||
|
- [ ] Model created with fillable and casts
|
||||||
|
- [ ] Migration created and run
|
||||||
|
- [ ] Permissions.php created
|
||||||
|
- [ ] Filament Resource created with all pages
|
||||||
|
- [ ] Controller created
|
||||||
|
- [ ] Routes (web.php and optionally api.php) created
|
||||||
|
- [ ] Index view created
|
||||||
|
- [ ] `php artisan migrate` run
|
||||||
|
- [ ] `php artisan optimize:clear` run
|
||||||
|
- [ ] `php artisan db:seed --class=RolePermissionSeeder` run
|
||||||
|
- [ ] Module accessible at `/[module-slug]`
|
||||||
|
- [ ] Admin panel shows module in navigation
|
||||||
|
- [ ] CRUD operations work in admin
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🆘 DEBUGGING
|
||||||
|
|
||||||
|
### Module Not Loading
|
||||||
|
```bash
|
||||||
|
# Check if provider is registered
|
||||||
|
docker-compose exec app php artisan about
|
||||||
|
|
||||||
|
# Clear everything
|
||||||
|
docker-compose exec app php artisan optimize:clear
|
||||||
|
```
|
||||||
|
|
||||||
|
### Filament Resource Not Showing
|
||||||
|
```bash
|
||||||
|
# Clear Filament cache
|
||||||
|
docker-compose exec app php artisan filament:cache-components
|
||||||
|
|
||||||
|
# Check canAccess() method returns true
|
||||||
|
# Check user has required permission
|
||||||
|
```
|
||||||
|
|
||||||
|
### Permission Denied
|
||||||
|
```bash
|
||||||
|
# Reset permission cache
|
||||||
|
docker-compose exec app php artisan permission:cache-reset
|
||||||
|
|
||||||
|
# Verify permissions exist
|
||||||
|
docker-compose exec app php artisan tinker
|
||||||
|
>>> \Spatie\Permission\Models\Permission::pluck('name');
|
||||||
|
```
|
||||||
|
|
||||||
|
### View Not Found
|
||||||
|
```bash
|
||||||
|
# Verify view namespace (must be kebab-case)
|
||||||
|
# Check file exists in Resources/views/
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Remember**: When in doubt, look at existing code in the codebase. The patterns are already established - follow them exactly.
|
||||||
27
README.md
27
README.md
@@ -4,7 +4,8 @@ A comprehensive Laravel development environment with Docker for local developmen
|
|||||||
|
|
||||||
> **New here?** Start with [GETTING_STARTED.md](GETTING_STARTED.md) for a step-by-step setup guide.
|
> **New here?** Start with [GETTING_STARTED.md](GETTING_STARTED.md) for a step-by-step setup guide.
|
||||||
>
|
>
|
||||||
> **AI Assistant?** See [AI_CONTEXT.md](AI_CONTEXT.md) for project context and conventions.
|
> **AI Assistant?** See [CLAUDE.md](CLAUDE.md) for EXACT module templates and step-by-step instructions.
|
||||||
|
> Also see [AI_CONTEXT.md](AI_CONTEXT.md) for project context and conventions.
|
||||||
|
|
||||||
## Architecture Overview
|
## Architecture Overview
|
||||||
|
|
||||||
@@ -74,6 +75,8 @@ A comprehensive Laravel development environment with Docker for local developmen
|
|||||||
│ ├── laravel-setup.md # Laravel setup guide
|
│ ├── laravel-setup.md # Laravel setup guide
|
||||||
│ ├── filament-admin.md # Admin panel docs
|
│ ├── filament-admin.md # Admin panel docs
|
||||||
│ ├── modules.md # Modular architecture guide
|
│ ├── modules.md # Modular architecture guide
|
||||||
|
│ ├── module-generator.md # Admin UI module generator
|
||||||
|
│ ├── menu-management.md # Frontend menu system
|
||||||
│ ├── audit-trail.md # Audit trail docs
|
│ ├── audit-trail.md # Audit trail docs
|
||||||
│ ├── site-settings.md # Appearance settings
|
│ ├── site-settings.md # Appearance settings
|
||||||
│ ├── testing.md # Pest testing guide
|
│ ├── testing.md # Pest testing guide
|
||||||
@@ -139,8 +142,8 @@ setup.bat # Windows
|
|||||||
- Mailpit: http://localhost:8025
|
- Mailpit: http://localhost:8025
|
||||||
|
|
||||||
**Admin Login:**
|
**Admin Login:**
|
||||||
- Email: admin@example.com
|
Interactive (prompts for name and password)
|
||||||
- Password: password
|
php artisan make:admin admin@example.com
|
||||||
|
|
||||||
### Manual Setup (Alternative)
|
### Manual Setup (Alternative)
|
||||||
|
|
||||||
@@ -207,6 +210,20 @@ If you prefer manual control:
|
|||||||
| `make setup-laravel` | Configure auth, API, middleware |
|
| `make setup-laravel` | Configure auth, API, middleware |
|
||||||
| `make setup-all` | Run both setup scripts |
|
| `make setup-all` | Run both setup scripts |
|
||||||
|
|
||||||
|
### Frontend Assets (Vite)
|
||||||
|
|
||||||
|
Build frontend CSS/JS assets:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build assets for production
|
||||||
|
docker-compose run --rm node npm run build
|
||||||
|
|
||||||
|
# Or run Vite dev server (hot reload)
|
||||||
|
docker-compose --profile frontend up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Note:** The template includes a resilient fallback - if assets aren't built, basic styling is provided and a development warning is shown. This prevents the `ViteManifestNotFoundException` error from breaking the app.
|
||||||
|
|
||||||
## Laravel Setup (Auth, API, Middleware)
|
## Laravel Setup (Auth, API, Middleware)
|
||||||
|
|
||||||
After installing Laravel, run the interactive setup:
|
After installing Laravel, run the interactive setup:
|
||||||
@@ -376,7 +393,9 @@ See [docs/laravel-setup.md](docs/laravel-setup.md) for detailed configuration.
|
|||||||
# Edit .env with your settings
|
# Edit .env with your settings
|
||||||
php artisan key:generate
|
php artisan key:generate
|
||||||
php artisan migrate
|
php artisan migrate
|
||||||
npm ci && npm run build
|
npm install && npm run build
|
||||||
|
php artisan db:seed
|
||||||
|
php artisan make:admin user@domain.com
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Configure Nginx**
|
2. **Configure Nginx**
|
||||||
|
|||||||
2
REPO_MANAGEMENT.md
Normal file
2
REPO_MANAGEMENT.md
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
Commits must be againsts the module being currently worked on.
|
||||||
|
Commits must be to a branch that has the exact same name as the module being worked on.
|
||||||
16
TEMPLATE_FIX_SUGGESTIONS.md
Normal file
16
TEMPLATE_FIX_SUGGESTIONS.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# Template Improvement Suggestions
|
||||||
|
|
||||||
|
This document contains suggestions for improving the template based on recurring development issues.
|
||||||
|
|
||||||
|
## 1. Mandatory Module Settings Page
|
||||||
|
|
||||||
|
**Rule**: When creating a new module, the AI agent must always create a corresponding settings page within the admin panel.
|
||||||
|
|
||||||
|
**Minimum Requirements**: This settings page must, at a minimum, allow an administrator to configure which user roles have Create, Read, Update, and Delete (CRUD) permissions for that module.
|
||||||
|
|
||||||
|
**Rationale**: This has been a recurring issue. Automating the creation of a permission management UI for each module makes the template more robust, secure, and user-friendly out-of-the-box. It prevents situations where new modules are added without any way for an admin to control access to them.
|
||||||
|
|
||||||
|
**Example Implementation**:
|
||||||
|
- When a `Blog` module is created, a `Blog Settings` page should also be created.
|
||||||
|
- This page should contain a form with checkboxes or a multi-select dropdown for each CRUD permission (`blog.view`, `blog.create`, `blog.edit`, `blog.delete`).
|
||||||
|
- An administrator can then select which roles (e.g., 'admin', 'editor', 'viewer') are granted each of these permissions.
|
||||||
420
TEST_SETUP.md
Normal file
420
TEST_SETUP.md
Normal file
@@ -0,0 +1,420 @@
|
|||||||
|
# Testing the 2-Minute Setup
|
||||||
|
|
||||||
|
This document verifies that the Laravel Docker Dev Template works as advertised.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Test 1: Fresh Local Setup (Docker)
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
- Docker Desktop running
|
||||||
|
- Git installed
|
||||||
|
- No existing containers from this project
|
||||||
|
|
||||||
|
### Steps
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Clone to a fresh directory
|
||||||
|
cd /tmp # or C:\temp on Windows
|
||||||
|
git clone https://git.radapps.co.za/theradcoza/Laravel-Docker-Dev-Template.git test-setup
|
||||||
|
cd test-setup
|
||||||
|
|
||||||
|
# 2. Run setup (should take ~2 minutes)
|
||||||
|
./setup.sh mysql # or setup.bat mysql on Windows
|
||||||
|
|
||||||
|
# 3. Verify containers are running
|
||||||
|
docker-compose ps
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected Output:**
|
||||||
|
- `app` container: Up
|
||||||
|
- `nginx` container: Up
|
||||||
|
- `mysql` container: Up
|
||||||
|
- `redis` container: Up
|
||||||
|
- `mailpit` container: Up
|
||||||
|
|
||||||
|
### Verification Checklist
|
||||||
|
|
||||||
|
**Database:**
|
||||||
|
```bash
|
||||||
|
docker-compose exec app php artisan tinker
|
||||||
|
```
|
||||||
|
```php
|
||||||
|
// Check admin user exists
|
||||||
|
App\Models\User::where('email', 'admin@example.com')->exists(); // Should return: true
|
||||||
|
|
||||||
|
// Check roles exist
|
||||||
|
Spatie\Permission\Models\Role::count(); // Should return: 3
|
||||||
|
|
||||||
|
// Check permissions exist
|
||||||
|
Spatie\Permission\Models\Permission::count(); // Should return: 5
|
||||||
|
|
||||||
|
// Check admin has admin role
|
||||||
|
$admin = App\Models\User::where('email', 'admin@example.com')->first();
|
||||||
|
$admin->hasRole('admin'); // Should return: true
|
||||||
|
|
||||||
|
exit
|
||||||
|
```
|
||||||
|
|
||||||
|
**Web Access:**
|
||||||
|
1. Visit http://localhost:8080 - Should show Laravel welcome page
|
||||||
|
2. Visit http://localhost:8080/admin - Should show Filament login
|
||||||
|
3. Login with:
|
||||||
|
- Email: admin@example.com
|
||||||
|
- Password: password
|
||||||
|
4. Should successfully login and see admin dashboard
|
||||||
|
5. Should see "Users" menu item
|
||||||
|
6. Should see "Settings" menu item
|
||||||
|
|
||||||
|
**Settings Page:**
|
||||||
|
1. Click "Settings" in admin panel
|
||||||
|
2. Should load without errors
|
||||||
|
3. Should show form fields:
|
||||||
|
- Site Name
|
||||||
|
- Site Logo (file upload)
|
||||||
|
- Primary Color
|
||||||
|
- Secondary Color
|
||||||
|
- Accent Color
|
||||||
|
- Site Description
|
||||||
|
- Contact Email
|
||||||
|
- Maintenance Mode (toggle)
|
||||||
|
4. Try saving - should show success notification
|
||||||
|
|
||||||
|
**Users Management:**
|
||||||
|
1. Click "Users" in admin panel
|
||||||
|
2. Should see admin user in list
|
||||||
|
3. Click "New User"
|
||||||
|
4. Create test user
|
||||||
|
5. Should save successfully
|
||||||
|
|
||||||
|
**Mailpit:**
|
||||||
|
1. Visit http://localhost:8025
|
||||||
|
2. Should show Mailpit interface
|
||||||
|
3. Send a test email (e.g., password reset)
|
||||||
|
4. Email should appear in Mailpit
|
||||||
|
|
||||||
|
### Success Criteria
|
||||||
|
|
||||||
|
✅ Setup completes in under 3 minutes
|
||||||
|
✅ All containers start successfully
|
||||||
|
✅ Admin user exists with correct credentials
|
||||||
|
✅ 3 roles created (admin, editor, viewer)
|
||||||
|
✅ 5 permissions created
|
||||||
|
✅ Admin login works
|
||||||
|
✅ Settings page loads and saves
|
||||||
|
✅ Users management works
|
||||||
|
✅ No 500 errors
|
||||||
|
✅ No 419 CSRF errors
|
||||||
|
✅ No "Class Redis not found" errors
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Test 2: Production Deployment Simulation
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
- Ubuntu 24.04 server (or VM)
|
||||||
|
- Root or sudo access
|
||||||
|
- Clean database
|
||||||
|
|
||||||
|
### Steps
|
||||||
|
|
||||||
|
Follow `PRODUCTION_DEPLOYMENT.md` exactly:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Install PHP and extensions
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install -y php8.3 php8.3-fpm php8.3-cli php8.3-mysql \
|
||||||
|
php8.3-redis php8.3-curl php8.3-mbstring php8.3-xml \
|
||||||
|
php8.3-zip php8.3-bcmath php8.3-gd
|
||||||
|
|
||||||
|
# 2. Verify Redis extension
|
||||||
|
php -m | grep redis # Should output: redis
|
||||||
|
|
||||||
|
# 3. Clone repo
|
||||||
|
cd /var/www
|
||||||
|
sudo git clone https://git.radapps.co.za/theradcoza/Laravel-Docker-Dev-Template.git test-app
|
||||||
|
cd test-app/src
|
||||||
|
|
||||||
|
# 4. Install dependencies
|
||||||
|
composer install --no-dev --optimize-autoloader
|
||||||
|
npm install && npm run build
|
||||||
|
|
||||||
|
# 5. Configure environment
|
||||||
|
cp .env.mysql .env
|
||||||
|
nano .env # Set database credentials, APP_URL, etc.
|
||||||
|
|
||||||
|
# 6. Generate key
|
||||||
|
php artisan key:generate --force
|
||||||
|
|
||||||
|
# 7. Run migrations
|
||||||
|
php artisan migrate --force
|
||||||
|
|
||||||
|
# 8. CRITICAL: Run seeders
|
||||||
|
php artisan db:seed --force
|
||||||
|
|
||||||
|
# 9. Set permissions
|
||||||
|
sudo chown -R www-data:www-data storage bootstrap/cache
|
||||||
|
sudo chmod -R 775 storage bootstrap/cache
|
||||||
|
|
||||||
|
# 10. Create storage link
|
||||||
|
php artisan storage:link
|
||||||
|
|
||||||
|
# 11. Cache config
|
||||||
|
php artisan config:cache
|
||||||
|
php artisan route:cache
|
||||||
|
php artisan view:cache
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verification
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check admin user exists
|
||||||
|
php artisan tinker
|
||||||
|
```
|
||||||
|
```php
|
||||||
|
App\Models\User::where('email', 'admin@example.com')->exists(); // true
|
||||||
|
exit
|
||||||
|
```
|
||||||
|
|
||||||
|
**Configure web server** (Apache or Nginx) per `PRODUCTION_DEPLOYMENT.md`
|
||||||
|
|
||||||
|
**Test web access:**
|
||||||
|
1. Visit https://your-domain.com
|
||||||
|
2. Visit https://your-domain.com/admin
|
||||||
|
3. Login with admin@example.com / password
|
||||||
|
4. Should work without errors
|
||||||
|
|
||||||
|
### Success Criteria
|
||||||
|
|
||||||
|
✅ PHP Redis extension installed
|
||||||
|
✅ Migrations run successfully
|
||||||
|
✅ Seeders run successfully
|
||||||
|
✅ Admin user created
|
||||||
|
✅ Roles and permissions created
|
||||||
|
✅ No "Class Redis not found" errors
|
||||||
|
✅ No 419 CSRF errors
|
||||||
|
✅ Admin login works
|
||||||
|
✅ Settings page works
|
||||||
|
✅ No 500 errors
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Test 3: Common Error Scenarios
|
||||||
|
|
||||||
|
### Scenario 1: Missing Redis Extension
|
||||||
|
|
||||||
|
**Simulate:**
|
||||||
|
```bash
|
||||||
|
# On production server, DON'T install php-redis
|
||||||
|
composer install
|
||||||
|
php artisan migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected:**
|
||||||
|
- Should fail with clear error message
|
||||||
|
- `PRODUCTION_DEPLOYMENT.md` should have solution
|
||||||
|
|
||||||
|
**Fix:**
|
||||||
|
```bash
|
||||||
|
sudo apt install php8.3-redis
|
||||||
|
sudo systemctl restart php8.3-fpm
|
||||||
|
```
|
||||||
|
|
||||||
|
### Scenario 2: Forgot to Run Seeders
|
||||||
|
|
||||||
|
**Simulate:**
|
||||||
|
```bash
|
||||||
|
php artisan migrate --force
|
||||||
|
# Skip: php artisan db:seed --force
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected:**
|
||||||
|
- Admin user doesn't exist
|
||||||
|
- Roles don't exist
|
||||||
|
- Can't login
|
||||||
|
|
||||||
|
**Fix:**
|
||||||
|
```bash
|
||||||
|
php artisan db:seed --force
|
||||||
|
```
|
||||||
|
|
||||||
|
### Scenario 3: Permission Issues
|
||||||
|
|
||||||
|
**Simulate:**
|
||||||
|
```bash
|
||||||
|
# Wrong permissions on storage
|
||||||
|
sudo chmod -R 755 storage
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected:**
|
||||||
|
- 500 errors
|
||||||
|
- Can't write logs
|
||||||
|
|
||||||
|
**Fix:**
|
||||||
|
```bash
|
||||||
|
sudo chown -R www-data:www-data storage bootstrap/cache
|
||||||
|
sudo chmod -R 775 storage bootstrap/cache
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Automated Test Script
|
||||||
|
|
||||||
|
Create `test-setup.sh`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo "🧪 Testing Laravel Docker Dev Template Setup..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Test 1: Check containers
|
||||||
|
echo "✓ Checking containers..."
|
||||||
|
if ! docker-compose ps | grep -q "Up"; then
|
||||||
|
echo "❌ Containers not running"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "✅ Containers running"
|
||||||
|
|
||||||
|
# Test 2: Check admin user
|
||||||
|
echo "✓ Checking admin user..."
|
||||||
|
ADMIN_EXISTS=$(docker-compose exec -T app php artisan tinker --execute="echo App\Models\User::where('email', 'admin@example.com')->exists() ? 'true' : 'false';")
|
||||||
|
if [[ ! "$ADMIN_EXISTS" =~ "true" ]]; then
|
||||||
|
echo "❌ Admin user not found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "✅ Admin user exists"
|
||||||
|
|
||||||
|
# Test 3: Check roles
|
||||||
|
echo "✓ Checking roles..."
|
||||||
|
ROLE_COUNT=$(docker-compose exec -T app php artisan tinker --execute="echo Spatie\Permission\Models\Role::count();")
|
||||||
|
if [[ ! "$ROLE_COUNT" =~ "3" ]]; then
|
||||||
|
echo "❌ Expected 3 roles, found: $ROLE_COUNT"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "✅ 3 roles created"
|
||||||
|
|
||||||
|
# Test 4: Check permissions
|
||||||
|
echo "✓ Checking permissions..."
|
||||||
|
PERM_COUNT=$(docker-compose exec -T app php artisan tinker --execute="echo Spatie\Permission\Models\Permission::count();")
|
||||||
|
if [[ ! "$PERM_COUNT" =~ "5" ]]; then
|
||||||
|
echo "❌ Expected 5 permissions, found: $PERM_COUNT"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "✅ 5 permissions created"
|
||||||
|
|
||||||
|
# Test 5: Check admin has admin role
|
||||||
|
echo "✓ Checking admin role assignment..."
|
||||||
|
HAS_ROLE=$(docker-compose exec -T app php artisan tinker --execute="echo App\Models\User::where('email', 'admin@example.com')->first()->hasRole('admin') ? 'true' : 'false';")
|
||||||
|
if [[ ! "$HAS_ROLE" =~ "true" ]]; then
|
||||||
|
echo "❌ Admin user doesn't have admin role"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "✅ Admin has admin role"
|
||||||
|
|
||||||
|
# Test 6: Check web access
|
||||||
|
echo "✓ Checking web access..."
|
||||||
|
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8080)
|
||||||
|
if [ "$HTTP_CODE" != "200" ]; then
|
||||||
|
echo "❌ Web server not responding (HTTP $HTTP_CODE)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "✅ Web server responding"
|
||||||
|
|
||||||
|
# Test 7: Check admin panel
|
||||||
|
echo "✓ Checking admin panel..."
|
||||||
|
ADMIN_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/admin)
|
||||||
|
if [ "$ADMIN_CODE" != "200" ]; then
|
||||||
|
echo "❌ Admin panel not responding (HTTP $ADMIN_CODE)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "✅ Admin panel responding"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "🎉 All tests passed!"
|
||||||
|
echo ""
|
||||||
|
echo "Admin Login:"
|
||||||
|
echo " URL: http://localhost:8080/admin"
|
||||||
|
echo " Email: admin@example.com"
|
||||||
|
echo " Password: password"
|
||||||
|
```
|
||||||
|
|
||||||
|
Make executable:
|
||||||
|
```bash
|
||||||
|
chmod +x test-setup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Run:
|
||||||
|
```bash
|
||||||
|
./test-setup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Test (30 seconds)
|
||||||
|
|
||||||
|
If you just want to verify the basics work:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# After running setup.sh
|
||||||
|
docker-compose exec app php artisan tinker --execute="
|
||||||
|
echo 'Admin exists: ' . (App\Models\User::where('email', 'admin@example.com')->exists() ? 'YES' : 'NO') . PHP_EOL;
|
||||||
|
echo 'Roles count: ' . Spatie\Permission\Models\Role::count() . PHP_EOL;
|
||||||
|
echo 'Permissions count: ' . Spatie\Permission\Models\Permission::count() . PHP_EOL;
|
||||||
|
echo 'Admin has role: ' . (App\Models\User::where('email', 'admin@example.com')->first()->hasRole('admin') ? 'YES' : 'NO') . PHP_EOL;
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected output:**
|
||||||
|
```
|
||||||
|
Admin exists: YES
|
||||||
|
Roles count: 3
|
||||||
|
Permissions count: 5
|
||||||
|
Admin has role: YES
|
||||||
|
```
|
||||||
|
|
||||||
|
Then visit http://localhost:8080/admin and login.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Reporting Issues
|
||||||
|
|
||||||
|
If any test fails:
|
||||||
|
|
||||||
|
1. **Capture logs:**
|
||||||
|
```bash
|
||||||
|
docker-compose logs app > app-logs.txt
|
||||||
|
docker-compose exec app cat storage/logs/laravel.log > laravel-logs.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Capture database state:**
|
||||||
|
```bash
|
||||||
|
docker-compose exec app php artisan tinker --execute="
|
||||||
|
echo 'Users: ' . App\Models\User::count() . PHP_EOL;
|
||||||
|
echo 'Roles: ' . Spatie\Permission\Models\Role::count() . PHP_EOL;
|
||||||
|
echo 'Permissions: ' . Spatie\Permission\Models\Permission::count() . PHP_EOL;
|
||||||
|
" > db-state.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Report with:**
|
||||||
|
- Which test failed
|
||||||
|
- Error messages
|
||||||
|
- Log files
|
||||||
|
- Steps to reproduce
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Success Metrics
|
||||||
|
|
||||||
|
The template is considered "working correctly" if:
|
||||||
|
|
||||||
|
- ✅ Fresh setup completes in under 3 minutes
|
||||||
|
- ✅ Zero manual intervention required
|
||||||
|
- ✅ Admin user auto-created with correct credentials
|
||||||
|
- ✅ All roles and permissions seeded
|
||||||
|
- ✅ Admin panel accessible immediately
|
||||||
|
- ✅ Settings page works without errors
|
||||||
|
- ✅ No Redis errors
|
||||||
|
- ✅ No CSRF errors
|
||||||
|
- ✅ Production deployment follows clear guide
|
||||||
|
- ✅ All common errors documented with solutions
|
||||||
@@ -36,7 +36,7 @@ composer install --no-dev --optimize-autoloader --no-interaction
|
|||||||
|
|
||||||
# Install and build frontend assets
|
# Install and build frontend assets
|
||||||
echo "[4/9] Installing Node dependencies..."
|
echo "[4/9] Installing Node dependencies..."
|
||||||
npm ci --production=false
|
npm install
|
||||||
|
|
||||||
echo "[5/9] Building frontend assets..."
|
echo "[5/9] Building frontend assets..."
|
||||||
npm run build
|
npm run build
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
# PHP-FPM Application Server
|
# PHP-FPM Application Server
|
||||||
@@ -6,11 +5,11 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: docker/php/Dockerfile
|
dockerfile: docker/php/Dockerfile
|
||||||
container_name: laravel_app
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
working_dir: /var/www/html
|
working_dir: /var/www/html
|
||||||
volumes:
|
volumes:
|
||||||
- ./src:/var/www/html:cached
|
- ./src:/var/www/html:cached
|
||||||
|
- vendor_data:/var/www/html/vendor
|
||||||
- ./docker/php/local.ini:/usr/local/etc/php/conf.d/local.ini
|
- ./docker/php/local.ini:/usr/local/etc/php/conf.d/local.ini
|
||||||
networks:
|
networks:
|
||||||
- laravel_network
|
- laravel_network
|
||||||
@@ -20,7 +19,6 @@ services:
|
|||||||
# Nginx Web Server
|
# Nginx Web Server
|
||||||
nginx:
|
nginx:
|
||||||
image: nginx:alpine
|
image: nginx:alpine
|
||||||
container_name: laravel_nginx
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "${APP_PORT:-8080}:80"
|
- "${APP_PORT:-8080}:80"
|
||||||
@@ -29,6 +27,7 @@ services:
|
|||||||
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf
|
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf
|
||||||
networks:
|
networks:
|
||||||
- laravel_network
|
- laravel_network
|
||||||
|
- gateway
|
||||||
depends_on:
|
depends_on:
|
||||||
- app
|
- app
|
||||||
|
|
||||||
@@ -42,7 +41,6 @@ services:
|
|||||||
# MySQL Database
|
# MySQL Database
|
||||||
mysql:
|
mysql:
|
||||||
image: mysql:8.0
|
image: mysql:8.0
|
||||||
container_name: laravel_mysql
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "${DB_PORT:-3306}:3306"
|
- "${DB_PORT:-3306}:3306"
|
||||||
@@ -62,7 +60,6 @@ services:
|
|||||||
# PostgreSQL Database
|
# PostgreSQL Database
|
||||||
pgsql:
|
pgsql:
|
||||||
image: postgres:16-alpine
|
image: postgres:16-alpine
|
||||||
container_name: laravel_pgsql
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "${DB_PORT:-5432}:5432"
|
- "${DB_PORT:-5432}:5432"
|
||||||
@@ -82,7 +79,6 @@ services:
|
|||||||
# This is a dummy service to enable the sqlite profile
|
# This is a dummy service to enable the sqlite profile
|
||||||
sqlite:
|
sqlite:
|
||||||
image: alpine:latest
|
image: alpine:latest
|
||||||
container_name: laravel_sqlite_init
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./src/database:/data
|
- ./src/database:/data
|
||||||
command: sh -c "touch /data/database.sqlite && chmod 666 /data/database.sqlite"
|
command: sh -c "touch /data/database.sqlite && chmod 666 /data/database.sqlite"
|
||||||
@@ -92,7 +88,6 @@ services:
|
|||||||
# Redis Cache
|
# Redis Cache
|
||||||
redis:
|
redis:
|
||||||
image: redis:alpine
|
image: redis:alpine
|
||||||
container_name: laravel_redis
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "${REDIS_PORT:-6379}:6379"
|
- "${REDIS_PORT:-6379}:6379"
|
||||||
@@ -104,7 +99,6 @@ services:
|
|||||||
# Node.js for frontend assets (Vite/Mix)
|
# Node.js for frontend assets (Vite/Mix)
|
||||||
node:
|
node:
|
||||||
image: node:20-alpine
|
image: node:20-alpine
|
||||||
container_name: laravel_node
|
|
||||||
working_dir: /var/www/html
|
working_dir: /var/www/html
|
||||||
volumes:
|
volumes:
|
||||||
- ./src:/var/www/html
|
- ./src:/var/www/html
|
||||||
@@ -119,11 +113,11 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: docker/php/Dockerfile
|
dockerfile: docker/php/Dockerfile
|
||||||
container_name: laravel_queue
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
working_dir: /var/www/html
|
working_dir: /var/www/html
|
||||||
volumes:
|
volumes:
|
||||||
- ./src:/var/www/html
|
- ./src:/var/www/html
|
||||||
|
- vendor_data:/var/www/html/vendor
|
||||||
- ./docker/php/local.ini:/usr/local/etc/php/conf.d/local.ini
|
- ./docker/php/local.ini:/usr/local/etc/php/conf.d/local.ini
|
||||||
networks:
|
networks:
|
||||||
- laravel_network
|
- laravel_network
|
||||||
@@ -138,11 +132,11 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: docker/php/Dockerfile
|
dockerfile: docker/php/Dockerfile
|
||||||
container_name: laravel_scheduler
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
working_dir: /var/www/html
|
working_dir: /var/www/html
|
||||||
volumes:
|
volumes:
|
||||||
- ./src:/var/www/html
|
- ./src:/var/www/html
|
||||||
|
- vendor_data:/var/www/html/vendor
|
||||||
- ./docker/php/local.ini:/usr/local/etc/php/conf.d/local.ini
|
- ./docker/php/local.ini:/usr/local/etc/php/conf.d/local.ini
|
||||||
networks:
|
networks:
|
||||||
- laravel_network
|
- laravel_network
|
||||||
@@ -153,7 +147,6 @@ services:
|
|||||||
# Mailpit for local email testing
|
# Mailpit for local email testing
|
||||||
mailpit:
|
mailpit:
|
||||||
image: axllent/mailpit
|
image: axllent/mailpit
|
||||||
container_name: laravel_mailpit
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "${MAIL_PORT:-1025}:1025"
|
- "${MAIL_PORT:-1025}:1025"
|
||||||
@@ -164,8 +157,11 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
laravel_network:
|
laravel_network:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
|
gateway:
|
||||||
|
external: true
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
mysql_data:
|
mysql_data:
|
||||||
pgsql_data:
|
pgsql_data:
|
||||||
redis_data:
|
redis_data:
|
||||||
|
vendor_data:
|
||||||
|
|||||||
6
docker/.dockerignore
Normal file
6
docker/.dockerignore
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
node_modules
|
||||||
|
src/node_modules
|
||||||
|
src/vendor
|
||||||
|
.git
|
||||||
|
.env
|
||||||
|
*.log
|
||||||
@@ -38,12 +38,27 @@ RUN pecl install redis && docker-php-ext-enable redis
|
|||||||
# Install Composer
|
# Install Composer
|
||||||
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
|
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
|
||||||
|
|
||||||
|
# Install Node.js for asset building
|
||||||
|
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
|
||||||
|
&& apt-get install -y nodejs \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Create system user to run Composer and Artisan commands
|
# Create system user to run Composer and Artisan commands
|
||||||
RUN useradd -G www-data,root -u 1000 -d /home/devuser devuser
|
RUN useradd -G www-data,root -u 1000 -d /home/devuser devuser
|
||||||
RUN mkdir -p /home/devuser/.composer && \
|
RUN mkdir -p /home/devuser/.composer && \
|
||||||
chown -R devuser:devuser /home/devuser
|
chown -R devuser:devuser /home/devuser
|
||||||
|
|
||||||
# Copy existing application directory permissions
|
# Copy composer files first
|
||||||
|
COPY --chown=devuser:devuser ./src/composer.json ./src/composer.lock /var/www/html/
|
||||||
|
|
||||||
|
# Install composer dependencies as devuser
|
||||||
|
USER devuser
|
||||||
|
RUN composer install --no-interaction --no-dev --optimize-autoloader --no-scripts
|
||||||
|
|
||||||
|
# Switch back to root to copy rest of files
|
||||||
|
USER root
|
||||||
|
|
||||||
|
# Copy existing application directory
|
||||||
COPY --chown=devuser:devuser ./src /var/www/html
|
COPY --chown=devuser:devuser ./src /var/www/html
|
||||||
|
|
||||||
# Set proper permissions
|
# Set proper permissions
|
||||||
@@ -51,6 +66,17 @@ RUN chown -R devuser:www-data /var/www/html \
|
|||||||
&& chmod -R 775 /var/www/html/storage 2>/dev/null || true \
|
&& chmod -R 775 /var/www/html/storage 2>/dev/null || true \
|
||||||
&& chmod -R 775 /var/www/html/bootstrap/cache 2>/dev/null || true
|
&& chmod -R 775 /var/www/html/bootstrap/cache 2>/dev/null || true
|
||||||
|
|
||||||
|
# Build frontend assets (if package.json exists)
|
||||||
|
RUN if [ -f "package.json" ]; then \
|
||||||
|
npm ci --ignore-scripts && \
|
||||||
|
npm run build && \
|
||||||
|
rm -rf node_modules; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Run post-install scripts
|
||||||
|
USER devuser
|
||||||
|
RUN composer run-script post-autoload-dump 2>/dev/null || true
|
||||||
|
|
||||||
USER devuser
|
USER devuser
|
||||||
|
|
||||||
EXPOSE 9000
|
EXPOSE 9000
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ composer install --no-dev --optimize-autoloader
|
|||||||
php artisan migrate --force
|
php artisan migrate --force
|
||||||
|
|
||||||
# Build assets
|
# Build assets
|
||||||
npm ci
|
npm install
|
||||||
npm run build
|
npm run build
|
||||||
|
|
||||||
# Clear caches
|
# Clear caches
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
# Filament Admin Panel
|
# Filament Admin Panel
|
||||||
|
|
||||||
|
> **🤖 AI Agents**: For Filament form/table component syntax, see [CLAUDE.md](../CLAUDE.md#-filament-form-field-reference).
|
||||||
|
|
||||||
This template includes optional [Filament](https://filamentphp.com/) admin panel setup for user management and administration.
|
This template includes optional [Filament](https://filamentphp.com/) admin panel setup for user management and administration.
|
||||||
|
|
||||||
## What is Filament?
|
## What is Filament?
|
||||||
|
|||||||
130
docs/menu-management.md
Normal file
130
docs/menu-management.md
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
# Frontend Menu Management
|
||||||
|
|
||||||
|
This template includes a dynamic menu management system that allows administrators to create and manage frontend navigation menus through the admin panel.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Dynamic Menus**: Create multiple menus (header, footer, sidebar, etc.)
|
||||||
|
- **Nested Items**: Support for parent-child menu items (dropdowns)
|
||||||
|
- **Multiple Link Types**:
|
||||||
|
- External links (URLs)
|
||||||
|
- Named routes
|
||||||
|
- Module links (auto-generates route from module slug)
|
||||||
|
- **Permission-Based Filtering**: Menu items are automatically filtered based on user permissions
|
||||||
|
- **Drag & Drop Reordering**: Reorder menu items in the admin panel
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
### Permission Filtering
|
||||||
|
|
||||||
|
Menu items are automatically filtered based on the current user's permissions:
|
||||||
|
|
||||||
|
1. **Module Items**: If a menu item links to a module (e.g., `companies`), the user must have `{module}.view` permission (e.g., `companies.view`) to see that item.
|
||||||
|
|
||||||
|
2. **Permission Items**: If a menu item has a specific `permission` set, the user must have that exact permission.
|
||||||
|
|
||||||
|
3. **Admin Users**: Users with the `admin` role can see all menu items.
|
||||||
|
|
||||||
|
4. **Guest Users**: Only see items with no permission or module restrictions.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### In Blade Templates
|
||||||
|
|
||||||
|
Use the `<x-frontend-menu>` component to render a menu:
|
||||||
|
|
||||||
|
```blade
|
||||||
|
{{-- Desktop navigation --}}
|
||||||
|
<x-frontend-menu menu="header" />
|
||||||
|
|
||||||
|
{{-- Mobile/responsive navigation --}}
|
||||||
|
<x-frontend-menu-responsive menu="header" />
|
||||||
|
|
||||||
|
{{-- By location --}}
|
||||||
|
<x-frontend-menu menu="footer" />
|
||||||
|
```
|
||||||
|
|
||||||
|
### Programmatic Access
|
||||||
|
|
||||||
|
```php
|
||||||
|
use App\Services\MenuService;
|
||||||
|
|
||||||
|
$menuService = app(MenuService::class);
|
||||||
|
|
||||||
|
// Get menu items (already filtered for current user)
|
||||||
|
$items = $menuService->getMenu('header');
|
||||||
|
|
||||||
|
// Get available modules for menu items
|
||||||
|
$modules = $menuService->getAvailableModules();
|
||||||
|
|
||||||
|
// Get available routes for menu items
|
||||||
|
$routes = $menuService->getAvailableRoutes();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Admin Panel
|
||||||
|
|
||||||
|
Navigate to **Settings > Menus** in the admin panel to:
|
||||||
|
|
||||||
|
1. Create new menus
|
||||||
|
2. Add/edit/delete menu items
|
||||||
|
3. Set item types (link, route, module)
|
||||||
|
4. Configure permissions for each item
|
||||||
|
5. Reorder items via drag & drop
|
||||||
|
|
||||||
|
## Database Structure
|
||||||
|
|
||||||
|
### `menus` table
|
||||||
|
- `id` - Primary key
|
||||||
|
- `name` - Display name
|
||||||
|
- `slug` - Unique identifier (used in templates)
|
||||||
|
- `location` - Optional location hint (header, footer, sidebar)
|
||||||
|
- `is_active` - Toggle menu visibility
|
||||||
|
|
||||||
|
### `menu_items` table
|
||||||
|
- `id` - Primary key
|
||||||
|
- `menu_id` - Foreign key to menus
|
||||||
|
- `parent_id` - For nested items
|
||||||
|
- `title` - Display text
|
||||||
|
- `type` - link, route, or module
|
||||||
|
- `url` - For external links
|
||||||
|
- `route` - For named routes
|
||||||
|
- `route_params` - JSON parameters for routes
|
||||||
|
- `module` - Module slug for permission checking
|
||||||
|
- `permission` - Specific permission required
|
||||||
|
- `icon` - Heroicon name
|
||||||
|
- `target` - _self or _blank
|
||||||
|
- `order` - Sort order
|
||||||
|
- `is_active` - Toggle visibility
|
||||||
|
|
||||||
|
## Adding Menu Items for New Modules
|
||||||
|
|
||||||
|
When creating a new module, add a menu item to link to it:
|
||||||
|
|
||||||
|
1. Go to **Settings > Menus** in admin
|
||||||
|
2. Edit the "Header Navigation" menu
|
||||||
|
3. Click "New" in the Items section
|
||||||
|
4. Set:
|
||||||
|
- **Title**: Your module name
|
||||||
|
- **Type**: Module
|
||||||
|
- **Module**: Select your module from dropdown
|
||||||
|
5. Save
|
||||||
|
|
||||||
|
The menu item will automatically:
|
||||||
|
- Generate the correct URL (`/{module-slug}`)
|
||||||
|
- Check for `{module}.view` permission
|
||||||
|
- Hide from users without permission
|
||||||
|
|
||||||
|
## Customization
|
||||||
|
|
||||||
|
### Custom Menu Component
|
||||||
|
|
||||||
|
Copy and modify the default component:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp resources/views/components/frontend-menu.blade.php \
|
||||||
|
resources/views/components/my-custom-menu.blade.php
|
||||||
|
```
|
||||||
|
|
||||||
|
### Styling
|
||||||
|
|
||||||
|
The default components use Tailwind CSS classes. Modify the component templates to match your design.
|
||||||
173
docs/module-generator.md
Normal file
173
docs/module-generator.md
Normal file
@@ -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
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Modules\Accounting\Models;
|
||||||
|
|
||||||
|
use App\Traits\ModuleAuditable;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use OwenIt\Auditing\Contracts\Auditable;
|
||||||
|
|
||||||
|
class Invoice extends Model implements Auditable
|
||||||
|
{
|
||||||
|
use ModuleAuditable;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'number',
|
||||||
|
'customer_id',
|
||||||
|
'total',
|
||||||
|
'status',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Adding Filament Resources
|
||||||
|
|
||||||
|
Create admin CRUD in `Filament/Resources/`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
// app/Modules/Accounting/Filament/Resources/InvoiceResource.php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Modules\Accounting\Filament\Resources;
|
||||||
|
|
||||||
|
use App\Modules\Accounting\Models\Invoice;
|
||||||
|
use Filament\Resources\Resource;
|
||||||
|
// ... standard Filament resource implementation
|
||||||
|
```
|
||||||
|
|
||||||
|
## Comparison: UI vs CLI
|
||||||
|
|
||||||
|
| Feature | UI Generator | `make:module` CLI |
|
||||||
|
|---------|--------------|-------------------|
|
||||||
|
| Skeleton only | ✅ | ❌ (includes model) |
|
||||||
|
| Git integration | ✅ Auto-branch | ❌ Manual |
|
||||||
|
| Model generation | ❌ | ✅ With `--model=` |
|
||||||
|
| Filament resource | ❌ | ✅ With `--model=` |
|
||||||
|
| Visual feedback | ✅ | Terminal output |
|
||||||
|
| Environment | Local only | Any |
|
||||||
|
|
||||||
|
**Use UI Generator when**: You need a blank canvas for complex modules with multiple models.
|
||||||
|
|
||||||
|
**Use CLI when**: You want a quick single-model module with all files generated.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### "Git working directory has uncommitted changes"
|
||||||
|
|
||||||
|
Commit or stash your changes first:
|
||||||
|
```bash
|
||||||
|
git add . && git commit -m "WIP"
|
||||||
|
# or
|
||||||
|
git stash
|
||||||
|
```
|
||||||
|
|
||||||
|
### Module not showing in admin
|
||||||
|
|
||||||
|
Clear caches:
|
||||||
|
```bash
|
||||||
|
php artisan optimize:clear
|
||||||
|
```
|
||||||
|
|
||||||
|
### Permissions not working
|
||||||
|
|
||||||
|
Re-seed permissions:
|
||||||
|
```bash
|
||||||
|
php artisan db:seed --class=RolePermissionSeeder
|
||||||
|
```
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
# Modular Architecture
|
# Modular Architecture
|
||||||
|
|
||||||
|
> **🤖 AI Agents**: For EXACT file templates and copy-paste code, see [CLAUDE.md](../CLAUDE.md).
|
||||||
|
> This document explains concepts; CLAUDE.md provides strict implementation patterns.
|
||||||
|
|
||||||
This template uses a modular architecture to organize features into self-contained modules. Each module has its own admin panel, routes, views, and permissions.
|
This template uses a modular architecture to organize features into self-contained modules. Each module has its own admin panel, routes, views, and permissions.
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|||||||
193
setup.bat
193
setup.bat
@@ -24,64 +24,199 @@ echo.
|
|||||||
echo Setting up with %DB%...
|
echo Setting up with %DB%...
|
||||||
echo.
|
echo.
|
||||||
|
|
||||||
REM Step 1: Configure environment
|
REM Step 1: Check port availability and auto-assign alternatives
|
||||||
|
echo Checking port availability...
|
||||||
|
|
||||||
|
REM Function to find next available port
|
||||||
|
REM Usage: call :find_port <start_port> <result_var>
|
||||||
|
goto :skip_functions
|
||||||
|
|
||||||
|
:find_port
|
||||||
|
set /a port=%~1
|
||||||
|
:find_port_loop
|
||||||
|
netstat -ano | findstr ":%port% " >nul 2>&1
|
||||||
|
if %errorlevel% equ 0 (
|
||||||
|
set /a port+=1
|
||||||
|
goto :find_port_loop
|
||||||
|
)
|
||||||
|
set %~2=%port%
|
||||||
|
exit /b 0
|
||||||
|
|
||||||
|
:skip_functions
|
||||||
|
|
||||||
|
REM Check and assign APP_PORT (Web Server)
|
||||||
|
netstat -ano | findstr ":8080" >nul 2>&1
|
||||||
|
if %errorlevel% equ 0 (
|
||||||
|
call :find_port 8081 APP_PORT
|
||||||
|
echo [!] Port 8080 in use - using !APP_PORT! for Web Server
|
||||||
|
) else (
|
||||||
|
set APP_PORT=8080
|
||||||
|
echo [OK] Port 8080 ^(Web Server^) - Available
|
||||||
|
)
|
||||||
|
|
||||||
|
REM Check and assign MAIL_DASHBOARD_PORT (Mailpit Dashboard)
|
||||||
|
netstat -ano | findstr ":8025" >nul 2>&1
|
||||||
|
if %errorlevel% equ 0 (
|
||||||
|
call :find_port 8026 MAIL_DASHBOARD_PORT
|
||||||
|
echo [!] Port 8025 in use - using !MAIL_DASHBOARD_PORT! for Mailpit Dashboard
|
||||||
|
) else (
|
||||||
|
set MAIL_DASHBOARD_PORT=8025
|
||||||
|
echo [OK] Port 8025 ^(Mailpit Dashboard^) - Available
|
||||||
|
)
|
||||||
|
|
||||||
|
REM Check and assign MAIL_PORT (Mailpit SMTP)
|
||||||
|
netstat -ano | findstr ":1025" >nul 2>&1
|
||||||
|
if %errorlevel% equ 0 (
|
||||||
|
call :find_port 1026 MAIL_PORT
|
||||||
|
echo [!] Port 1025 in use - using !MAIL_PORT! for Mailpit SMTP
|
||||||
|
) else (
|
||||||
|
set MAIL_PORT=1025
|
||||||
|
echo [OK] Port 1025 ^(Mailpit SMTP^) - Available
|
||||||
|
)
|
||||||
|
|
||||||
|
REM Check and assign REDIS_PORT
|
||||||
|
netstat -ano | findstr ":6379" >nul 2>&1
|
||||||
|
if %errorlevel% equ 0 (
|
||||||
|
call :find_port 6380 REDIS_PORT
|
||||||
|
echo [!] Port 6379 in use - using !REDIS_PORT! for Redis
|
||||||
|
) else (
|
||||||
|
set REDIS_PORT=6379
|
||||||
|
echo [OK] Port 6379 ^(Redis^) - Available
|
||||||
|
)
|
||||||
|
|
||||||
|
REM Check and assign DB_PORT based on database selection
|
||||||
|
if "%DB%"=="mysql" (
|
||||||
|
netstat -ano | findstr ":3306" >nul 2>&1
|
||||||
|
if !errorlevel! equ 0 (
|
||||||
|
call :find_port 3307 DB_PORT
|
||||||
|
echo [!] Port 3306 in use - using !DB_PORT! for MySQL
|
||||||
|
) else (
|
||||||
|
set DB_PORT=3306
|
||||||
|
echo [OK] Port 3306 ^(MySQL^) - Available
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%DB%"=="pgsql" (
|
||||||
|
netstat -ano | findstr ":5432" >nul 2>&1
|
||||||
|
if !errorlevel! equ 0 (
|
||||||
|
call :find_port 5433 DB_PORT
|
||||||
|
echo [!] Port 5432 in use - using !DB_PORT! for PostgreSQL
|
||||||
|
) else (
|
||||||
|
set DB_PORT=5432
|
||||||
|
echo [OK] Port 5432 ^(PostgreSQL^) - Available
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
echo.
|
||||||
|
|
||||||
|
REM Step 2: Configure environment
|
||||||
echo Configuring environment...
|
echo Configuring environment...
|
||||||
if exist "src\.env.%DB%" (
|
if exist "src\.env.%DB%" (
|
||||||
copy /y "src\.env.%DB%" "src\.env" >nul
|
copy /y "src\.env.%DB%" "src\.env" >nul
|
||||||
|
|
||||||
|
REM Append port configurations to .env (for reference only, not used by Laravel)
|
||||||
|
REM Note: DB_PORT is NOT appended - Laravel uses internal Docker port (3306/5432)
|
||||||
|
REM These are for docker-compose external port mapping only
|
||||||
|
echo. >> src\.env
|
||||||
|
echo # Port Configuration ^(auto-assigned by setup^) >> src\.env
|
||||||
|
echo # APP_PORT=%APP_PORT% >> src\.env
|
||||||
|
echo # MAIL_DASHBOARD_PORT=%MAIL_DASHBOARD_PORT% >> src\.env
|
||||||
|
echo # MAIL_PORT=%MAIL_PORT% >> src\.env
|
||||||
|
echo # REDIS_PORT=%REDIS_PORT% >> src\.env
|
||||||
|
|
||||||
echo Environment configured for %DB%
|
echo Environment configured for %DB%
|
||||||
) else (
|
) else (
|
||||||
echo ERROR: Template file src\.env.%DB% not found
|
echo ERROR: Template file src\.env.%DB% not found
|
||||||
exit /b 1
|
exit /b 1
|
||||||
)
|
)
|
||||||
|
|
||||||
REM Step 2: Install composer dependencies
|
REM Step 3: Build containers
|
||||||
echo Installing composer dependencies...
|
echo Building Docker containers...
|
||||||
docker-compose build
|
docker-compose build
|
||||||
docker-compose --profile %DB% run --rm app composer install --no-interaction
|
echo Containers built
|
||||||
echo Dependencies installed
|
|
||||||
echo.
|
echo.
|
||||||
|
|
||||||
REM Step 3: Start containers
|
REM Step 4: Start containers
|
||||||
echo Starting Docker containers...
|
echo Starting Docker containers...
|
||||||
docker-compose --profile %DB% up -d
|
docker-compose --profile %DB% up -d
|
||||||
echo Containers started
|
echo Containers started
|
||||||
echo.
|
echo.
|
||||||
|
|
||||||
REM Step 4: Wait for database
|
REM Step 5: Wait for app container to be ready
|
||||||
|
echo Waiting for app container...
|
||||||
|
timeout /t 3 /nobreak >nul
|
||||||
|
echo App container ready
|
||||||
|
echo.
|
||||||
|
|
||||||
|
REM Step 6: Wait for database
|
||||||
if "%DB%"=="mysql" (
|
if "%DB%"=="mysql" (
|
||||||
echo Waiting for database...
|
echo Waiting for MySQL to be ready...
|
||||||
timeout /t 5 /nobreak >nul
|
set DB_READY=0
|
||||||
echo Database ready
|
for /L %%i in (1,1,30) do (
|
||||||
|
docker-compose exec -T app php -r "new PDO('mysql:host=mysql;dbname=laravel', 'laravel', 'secret');" >nul 2>&1
|
||||||
|
if !errorlevel! equ 0 (
|
||||||
|
set DB_READY=1
|
||||||
|
goto :mysql_ready
|
||||||
|
)
|
||||||
|
timeout /t 1 /nobreak >nul
|
||||||
|
)
|
||||||
|
:mysql_ready
|
||||||
|
if !DB_READY! equ 1 (
|
||||||
|
echo MySQL ready
|
||||||
|
) else (
|
||||||
|
echo WARNING: MySQL may not be fully ready yet
|
||||||
|
)
|
||||||
echo.
|
echo.
|
||||||
)
|
)
|
||||||
if "%DB%"=="pgsql" (
|
if "%DB%"=="pgsql" (
|
||||||
echo Waiting for database...
|
echo Waiting for PostgreSQL to be ready...
|
||||||
timeout /t 5 /nobreak >nul
|
set DB_READY=0
|
||||||
echo Database ready
|
for /L %%i in (1,1,30) do (
|
||||||
|
docker-compose exec -T app php -r "new PDO('pgsql:host=pgsql;dbname=laravel', 'laravel', 'secret');" >nul 2>&1
|
||||||
|
if !errorlevel! equ 0 (
|
||||||
|
set DB_READY=1
|
||||||
|
goto :pgsql_ready
|
||||||
|
)
|
||||||
|
timeout /t 1 /nobreak >nul
|
||||||
|
)
|
||||||
|
:pgsql_ready
|
||||||
|
if !DB_READY! equ 1 (
|
||||||
|
echo PostgreSQL ready
|
||||||
|
) else (
|
||||||
|
echo WARNING: PostgreSQL may not be fully ready yet
|
||||||
|
)
|
||||||
echo.
|
echo.
|
||||||
)
|
)
|
||||||
|
|
||||||
REM Step 5: Generate app key
|
REM Step 7: Generate app key
|
||||||
echo Generating application key...
|
echo Generating application key...
|
||||||
docker-compose exec app php artisan key:generate --force
|
docker-compose exec -T app php artisan key:generate --force
|
||||||
echo App key generated
|
echo App key generated
|
||||||
echo.
|
echo.
|
||||||
|
|
||||||
REM Step 6: Run migrations
|
REM Step 8: Run migrations
|
||||||
echo → Running database migrations...
|
echo Running database migrations...
|
||||||
docker-compose exec app php artisan migrate --force
|
docker-compose exec -T app php artisan migrate --force
|
||||||
echo ✓ Migrations completed
|
echo Migrations completed
|
||||||
echo.
|
echo.
|
||||||
|
|
||||||
echo → Seeding database (roles, permissions, admin user)...
|
REM Step 9: Seed database
|
||||||
docker-compose exec app php artisan db:seed --force
|
echo Seeding database (roles, permissions, admin user)...
|
||||||
echo ✓ Database seeded
|
docker-compose exec -T app php artisan db:seed --force
|
||||||
|
echo Database seeded
|
||||||
echo.
|
echo.
|
||||||
|
|
||||||
REM Step 7: Create storage link
|
REM Step 10: Create storage link
|
||||||
echo → Creating storage symlink...
|
echo Creating storage symlink...
|
||||||
docker-compose exec app php artisan storage:link
|
docker-compose exec -T app php artisan storage:link
|
||||||
echo ✓ Storage linked
|
echo Storage linked
|
||||||
|
echo.
|
||||||
|
|
||||||
|
REM Step 11: Build frontend assets
|
||||||
|
echo Building frontend assets...
|
||||||
|
docker-compose run --rm node npm install >nul 2>&1
|
||||||
|
docker-compose run --rm node npm run build >nul 2>&1
|
||||||
|
echo Frontend assets built
|
||||||
echo.
|
echo.
|
||||||
|
|
||||||
REM Done
|
REM Done
|
||||||
@@ -92,9 +227,9 @@ echo ========================================================
|
|||||||
echo.
|
echo.
|
||||||
echo Your Laravel application is ready!
|
echo Your Laravel application is ready!
|
||||||
echo.
|
echo.
|
||||||
echo Laravel App: http://localhost:8080
|
echo Laravel App: http://localhost:%APP_PORT%
|
||||||
echo Admin Panel: http://localhost:8080/admin
|
echo Admin Panel: http://localhost:%APP_PORT%/admin
|
||||||
echo Mailpit: http://localhost:8025
|
echo Mailpit: http://localhost:%MAIL_DASHBOARD_PORT%
|
||||||
echo.
|
echo.
|
||||||
echo Admin Login:
|
echo Admin Login:
|
||||||
echo Email: admin@example.com
|
echo Email: admin@example.com
|
||||||
|
|||||||
174
setup.sh
174
setup.sh
@@ -32,61 +32,177 @@ fi
|
|||||||
echo -e "${YELLOW}Setting up with ${DB}...${NC}"
|
echo -e "${YELLOW}Setting up with ${DB}...${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Step 1: Configure environment
|
# Step 1: Check port availability and auto-assign alternatives
|
||||||
|
echo -e "${YELLOW}→ Checking port availability...${NC}"
|
||||||
|
|
||||||
|
# Function to find next available port
|
||||||
|
find_port() {
|
||||||
|
local port=$1
|
||||||
|
while lsof -Pi :$port -sTCP:LISTEN -t >/dev/null 2>&1 || netstat -tuln 2>/dev/null | grep -q ":$port "; do
|
||||||
|
port=$((port + 1))
|
||||||
|
done
|
||||||
|
echo $port
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check and assign APP_PORT (Web Server)
|
||||||
|
if lsof -Pi :8080 -sTCP:LISTEN -t >/dev/null 2>&1 || netstat -tuln 2>/dev/null | grep -q ":8080 "; then
|
||||||
|
APP_PORT=$(find_port 8081)
|
||||||
|
echo -e "${YELLOW}[!] Port 8080 in use - using $APP_PORT for Web Server${NC}"
|
||||||
|
else
|
||||||
|
APP_PORT=8080
|
||||||
|
echo -e "${GREEN}[✓] Port 8080 (Web Server) - Available${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check and assign MAIL_DASHBOARD_PORT (Mailpit Dashboard)
|
||||||
|
if lsof -Pi :8025 -sTCP:LISTEN -t >/dev/null 2>&1 || netstat -tuln 2>/dev/null | grep -q ":8025 "; then
|
||||||
|
MAIL_DASHBOARD_PORT=$(find_port 8026)
|
||||||
|
echo -e "${YELLOW}[!] Port 8025 in use - using $MAIL_DASHBOARD_PORT for Mailpit Dashboard${NC}"
|
||||||
|
else
|
||||||
|
MAIL_DASHBOARD_PORT=8025
|
||||||
|
echo -e "${GREEN}[✓] Port 8025 (Mailpit Dashboard) - Available${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check and assign MAIL_PORT (Mailpit SMTP)
|
||||||
|
if lsof -Pi :1025 -sTCP:LISTEN -t >/dev/null 2>&1 || netstat -tuln 2>/dev/null | grep -q ":1025 "; then
|
||||||
|
MAIL_PORT=$(find_port 1026)
|
||||||
|
echo -e "${YELLOW}[!] Port 1025 in use - using $MAIL_PORT for Mailpit SMTP${NC}"
|
||||||
|
else
|
||||||
|
MAIL_PORT=1025
|
||||||
|
echo -e "${GREEN}[✓] Port 1025 (Mailpit SMTP) - Available${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check and assign REDIS_PORT
|
||||||
|
if lsof -Pi :6379 -sTCP:LISTEN -t >/dev/null 2>&1 || netstat -tuln 2>/dev/null | grep -q ":6379 "; then
|
||||||
|
REDIS_PORT=$(find_port 6380)
|
||||||
|
echo -e "${YELLOW}[!] Port 6379 in use - using $REDIS_PORT for Redis${NC}"
|
||||||
|
else
|
||||||
|
REDIS_PORT=6379
|
||||||
|
echo -e "${GREEN}[✓] Port 6379 (Redis) - Available${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check and assign DB_PORT based on database selection
|
||||||
|
if [ "$DB" = "mysql" ]; then
|
||||||
|
if lsof -Pi :3306 -sTCP:LISTEN -t >/dev/null 2>&1 || netstat -tuln 2>/dev/null | grep -q ":3306 "; then
|
||||||
|
DB_PORT=$(find_port 3307)
|
||||||
|
echo -e "${YELLOW}[!] Port 3306 in use - using $DB_PORT for MySQL${NC}"
|
||||||
|
else
|
||||||
|
DB_PORT=3306
|
||||||
|
echo -e "${GREEN}[✓] Port 3306 (MySQL) - Available${NC}"
|
||||||
|
fi
|
||||||
|
elif [ "$DB" = "pgsql" ]; then
|
||||||
|
if lsof -Pi :5432 -sTCP:LISTEN -t >/dev/null 2>&1 || netstat -tuln 2>/dev/null | grep -q ":5432 "; then
|
||||||
|
DB_PORT=$(find_port 5433)
|
||||||
|
echo -e "${YELLOW}[!] Port 5432 in use - using $DB_PORT for PostgreSQL${NC}"
|
||||||
|
else
|
||||||
|
DB_PORT=5432
|
||||||
|
echo -e "${GREEN}[✓] Port 5432 (PostgreSQL) - Available${NC}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Step 2: Configure environment
|
||||||
echo -e "${YELLOW}→ Configuring environment...${NC}"
|
echo -e "${YELLOW}→ Configuring environment...${NC}"
|
||||||
if [ -f "src/.env.${DB}" ]; then
|
if [ -f "src/.env.${DB}" ]; then
|
||||||
cp "src/.env.${DB}" "src/.env"
|
cp "src/.env.${DB}" "src/.env"
|
||||||
|
|
||||||
|
# Create .env in project root for docker compose port mapping
|
||||||
|
echo "# Docker Compose Port Configuration (auto-assigned by setup)" > .env
|
||||||
|
echo "APP_PORT=$APP_PORT" >> .env
|
||||||
|
echo "MAIL_DASHBOARD_PORT=$MAIL_DASHBOARD_PORT" >> .env
|
||||||
|
echo "MAIL_PORT=$MAIL_PORT" >> .env
|
||||||
|
echo "REDIS_PORT=$REDIS_PORT" >> .env
|
||||||
|
if [ -n "$DB_PORT" ]; then
|
||||||
|
echo "DB_PORT=$DB_PORT" >> .env
|
||||||
|
fi
|
||||||
|
|
||||||
echo -e "${GREEN}✓ Environment configured for ${DB}${NC}"
|
echo -e "${GREEN}✓ Environment configured for ${DB}${NC}"
|
||||||
else
|
else
|
||||||
echo -e "${RED}✗ Template file src/.env.${DB} not found${NC}"
|
echo -e "${RED}✗ Template file src/.env.${DB} not found${NC}"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Step 2: Install composer dependencies
|
# Step 3: Build containers
|
||||||
echo -e "${YELLOW}→ Installing composer dependencies...${NC}"
|
echo -e "${YELLOW}→ Building Docker containers...${NC}"
|
||||||
docker-compose build
|
docker compose build
|
||||||
docker-compose --profile ${DB} run --rm app composer install --no-interaction
|
echo -e "${GREEN}✓ Containers built${NC}"
|
||||||
echo -e "${GREEN}✓ Dependencies installed${NC}"
|
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Step 3: Start containers
|
# Step 4: Start containers
|
||||||
echo -e "${YELLOW}→ Starting Docker containers...${NC}"
|
echo -e "${YELLOW}→ Starting Docker containers...${NC}"
|
||||||
docker-compose --profile ${DB} up -d
|
docker compose --profile ${DB} up -d
|
||||||
echo -e "${GREEN}✓ Containers started${NC}"
|
echo -e "${GREEN}✓ Containers started${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Step 4: Wait for database
|
# Step 5: Wait for app container to be ready
|
||||||
if [ "$DB" = "mysql" ] || [ "$DB" = "pgsql" ]; then
|
echo -e "${YELLOW}→ Waiting for app container...${NC}"
|
||||||
echo -e "${YELLOW}→ Waiting for database...${NC}"
|
sleep 3
|
||||||
sleep 5
|
echo -e "${GREEN}✓ App container ready${NC}"
|
||||||
echo -e "${GREEN}✓ Database ready${NC}"
|
echo ""
|
||||||
|
|
||||||
|
# Step 5.5: Fix permissions on storage, bootstrap/cache, public, and .env
|
||||||
|
echo -e "${YELLOW}→ Setting directory permissions...${NC}"
|
||||||
|
chmod -R 777 src/storage src/bootstrap/cache src/public 2>/dev/null || true
|
||||||
|
chmod 666 src/.env 2>/dev/null || true
|
||||||
|
docker compose exec -T app chmod -R 777 storage bootstrap/cache public 2>/dev/null || true
|
||||||
|
docker compose exec -T app chmod 666 .env 2>/dev/null || true
|
||||||
|
echo -e "${GREEN}✓ Permissions set${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Step 6: Wait for database
|
||||||
|
if [ "$DB" = "mysql" ]; then
|
||||||
|
echo -e "${YELLOW}→ Waiting for MySQL to be ready...${NC}"
|
||||||
|
for i in {1..30}; do
|
||||||
|
if docker compose exec -T app php -r "new PDO('mysql:host=mysql;dbname=laravel', 'laravel', 'secret');" >/dev/null 2>&1; then
|
||||||
|
echo -e "${GREEN}✓ MySQL ready${NC}"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
elif [ "$DB" = "pgsql" ]; then
|
||||||
|
echo -e "${YELLOW}→ Waiting for PostgreSQL to be ready...${NC}"
|
||||||
|
for i in {1..30}; do
|
||||||
|
if docker compose exec -T app php -r "new PDO('pgsql:host=pgsql;dbname=laravel', 'laravel', 'secret');" >/dev/null 2>&1; then
|
||||||
|
echo -e "${GREEN}✓ PostgreSQL ready${NC}"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
echo ""
|
echo ""
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Step 5: Generate app key if needed
|
# Step 7: Generate app key
|
||||||
echo -e "${YELLOW}→ Generating application key...${NC}"
|
echo -e "${YELLOW}→ Generating application key...${NC}"
|
||||||
docker-compose exec app php artisan key:generate --force
|
docker compose exec app php artisan key:generate --force
|
||||||
echo -e "${GREEN}✓ App key generated${NC}"
|
echo -e "${GREEN}✓ App key generated${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Step 6: Run migrations
|
# Step 8: Run migrations
|
||||||
echo -e "${YELLOW}→ Running database migrations...${NC}"
|
echo -e "${YELLOW}→ Running database migrations...${NC}"
|
||||||
docker-compose exec app php artisan migrate --force
|
docker compose exec app php artisan migrate --force
|
||||||
echo -e "${GREEN}✓ Migrations completed${NC}"
|
echo -e "${GREEN}✓ Migrations completed${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Step 7: Seed database (roles, permissions, admin user)
|
# Step 9: Seed database (roles, permissions, admin user)
|
||||||
echo -e "${YELLOW}→ Seeding database (roles, permissions, admin user)...${NC}"
|
echo -e "${YELLOW}→ Seeding database (roles, permissions, admin user)...${NC}"
|
||||||
docker-compose exec app php artisan db:seed --force
|
docker compose exec app php artisan db:seed --force
|
||||||
echo -e "${GREEN}✓ Database seeded${NC}"
|
echo -e "${GREEN}✓ Database seeded${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Step 8: Create storage link
|
# Step 10: Create storage link
|
||||||
echo -e "${YELLOW}→ Creating storage symlink...${NC}"
|
echo -e "${YELLOW}→ Creating storage symlink...${NC}"
|
||||||
docker-compose exec app php artisan storage:link
|
docker compose exec app php artisan storage:link
|
||||||
echo -e "${GREEN}✓ Storage linked${NC}"
|
echo -e "${GREEN}✓ Storage linked${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
|
# Step 11: Build frontend assets
|
||||||
|
echo -e "${YELLOW}→ Building frontend assets...${NC}"
|
||||||
|
docker compose run --rm node npm install >/dev/null 2>&1
|
||||||
|
docker compose run --rm node npm run build >/dev/null 2>&1
|
||||||
|
echo -e "${GREEN}✓ Frontend assets built${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
# Done!
|
# Done!
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${GREEN}╔════════════════════════════════════════════════════════╗${NC}"
|
echo -e "${GREEN}╔════════════════════════════════════════════════════════╗${NC}"
|
||||||
@@ -95,9 +211,9 @@ echo -e "${GREEN}╚════════════════════
|
|||||||
echo ""
|
echo ""
|
||||||
echo -e "${GREEN}Your Laravel application is ready!${NC}"
|
echo -e "${GREEN}Your Laravel application is ready!${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " 📱 Laravel App: ${GREEN}http://localhost:8080${NC}"
|
echo -e " 📱 Laravel App: ${GREEN}http://localhost:$APP_PORT${NC}"
|
||||||
echo -e " 🔐 Admin Panel: ${GREEN}http://localhost:8080/admin${NC}"
|
echo -e " 🔐 Admin Panel: ${GREEN}http://localhost:$APP_PORT/admin${NC}"
|
||||||
echo -e " 📧 Mailpit: ${GREEN}http://localhost:8025${NC}"
|
echo -e " 📧 Mailpit: ${GREEN}http://localhost:$MAIL_DASHBOARD_PORT${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${YELLOW}Admin Login:${NC}"
|
echo -e "${YELLOW}Admin Login:${NC}"
|
||||||
echo -e " Email: admin@example.com"
|
echo -e " Email: admin@example.com"
|
||||||
@@ -111,11 +227,11 @@ echo -e " ✓ Laravel Pint code style"
|
|||||||
echo -e " ✓ Queue workers & scheduler (optional profiles)"
|
echo -e " ✓ Queue workers & scheduler (optional profiles)"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${YELLOW}Common Commands:${NC}"
|
echo -e "${YELLOW}Common Commands:${NC}"
|
||||||
echo -e " docker-compose exec app php artisan <command>"
|
echo -e " docker compose exec app php artisan <command>"
|
||||||
echo -e " docker-compose exec app composer <command>"
|
echo -e " docker compose exec app composer <command>"
|
||||||
echo -e " docker-compose exec app ./vendor/bin/pest"
|
echo -e " docker compose exec app ./vendor/bin/pest"
|
||||||
echo -e " docker-compose logs -f app"
|
echo -e " docker compose logs -f app"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${YELLOW}Stop containers:${NC}"
|
echo -e "${YELLOW}Stop containers:${NC}"
|
||||||
echo -e " docker-compose down"
|
echo -e " docker compose down"
|
||||||
echo ""
|
echo ""
|
||||||
|
|||||||
71
src/app/Console/Commands/MakeAdminCommand.php
Normal file
71
src/app/Console/Commands/MakeAdminCommand.php
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Support\Facades\Validator;
|
||||||
|
|
||||||
|
class MakeAdminCommand extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'make:admin
|
||||||
|
{email : The email address for the admin user}
|
||||||
|
{--name= : The name for the admin user}
|
||||||
|
{--password= : The password (will prompt if not provided)}';
|
||||||
|
|
||||||
|
protected $description = 'Create a new admin user with full permissions';
|
||||||
|
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
$email = $this->argument('email');
|
||||||
|
$name = $this->option('name') ?? $this->ask('Enter admin name', 'Admin');
|
||||||
|
$password = $this->option('password') ?? $this->secret('Enter password');
|
||||||
|
|
||||||
|
// Validate email
|
||||||
|
$validator = Validator::make(
|
||||||
|
['email' => $email],
|
||||||
|
['email' => 'required|email|unique:users,email']
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
$this->error('Validation failed:');
|
||||||
|
foreach ($validator->errors()->all() as $error) {
|
||||||
|
$this->error(" - {$error}");
|
||||||
|
}
|
||||||
|
return Command::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate password
|
||||||
|
if (strlen($password) < 8) {
|
||||||
|
$this->error('Password must be at least 8 characters.');
|
||||||
|
return Command::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create user
|
||||||
|
$user = User::create([
|
||||||
|
'name' => $name,
|
||||||
|
'email' => $email,
|
||||||
|
'password' => Hash::make($password),
|
||||||
|
'email_verified_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Assign admin role
|
||||||
|
$user->assignRole('admin');
|
||||||
|
|
||||||
|
$this->info("✅ Admin user created successfully!");
|
||||||
|
$this->table(
|
||||||
|
['Field', 'Value'],
|
||||||
|
[
|
||||||
|
['Name', $name],
|
||||||
|
['Email', $email],
|
||||||
|
['Role', 'admin'],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->newLine();
|
||||||
|
$this->info("Login at: /admin");
|
||||||
|
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,82 +8,322 @@
|
|||||||
|
|
||||||
class MakeModuleCommand extends Command
|
class MakeModuleCommand extends Command
|
||||||
{
|
{
|
||||||
protected $signature = 'make:module {name : The name of the module}';
|
protected $signature = 'make:module
|
||||||
|
{name : The name of the module (PascalCase)}
|
||||||
|
{--model= : Create a model with the given name}
|
||||||
|
{--api : Include API routes}
|
||||||
|
{--no-filament : Skip Filament resource generation}';
|
||||||
|
|
||||||
protected $description = 'Create a new module with standard structure';
|
protected $description = 'Create a new module with full structure (ServiceProvider, Config, Routes, Views, Permissions)';
|
||||||
|
|
||||||
|
protected string $studlyName;
|
||||||
|
protected string $kebabName;
|
||||||
|
protected string $snakeName;
|
||||||
|
protected string $modulePath;
|
||||||
|
protected ?string $modelName;
|
||||||
|
|
||||||
public function handle(): int
|
public function handle(): int
|
||||||
{
|
{
|
||||||
$name = $this->argument('name');
|
$name = $this->argument('name');
|
||||||
$studlyName = Str::studly($name);
|
$this->studlyName = Str::studly($name);
|
||||||
$kebabName = Str::kebab($name);
|
$this->kebabName = Str::kebab($name);
|
||||||
|
$this->snakeName = Str::snake($name);
|
||||||
|
$this->modelName = $this->option('model') ? Str::studly($this->option('model')) : null;
|
||||||
|
|
||||||
$modulePath = app_path("Modules/{$studlyName}");
|
$this->modulePath = app_path("Modules/{$this->studlyName}");
|
||||||
|
|
||||||
if (File::exists($modulePath)) {
|
if (File::exists($this->modulePath)) {
|
||||||
$this->error("Module {$studlyName} already exists!");
|
$this->error("Module {$this->studlyName} already exists!");
|
||||||
return self::FAILURE;
|
return self::FAILURE;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->info("Creating module: {$studlyName}");
|
$this->info("Creating module: {$this->studlyName}");
|
||||||
|
$this->newLine();
|
||||||
|
|
||||||
$this->createDirectories($modulePath);
|
$this->createDirectoryStructure();
|
||||||
$this->createModel($modulePath, $studlyName);
|
$this->createServiceProvider();
|
||||||
$this->createMigration($studlyName);
|
$this->createConfig();
|
||||||
$this->createController($modulePath, $studlyName);
|
$this->createPermissions();
|
||||||
$this->createRoutes($modulePath, $studlyName, $kebabName);
|
$this->createController();
|
||||||
$this->createViews($modulePath, $studlyName, $kebabName);
|
$this->createRoutes();
|
||||||
$this->createFilamentResource($studlyName);
|
$this->createViews();
|
||||||
$this->createTests($studlyName);
|
|
||||||
|
|
||||||
$this->info("\n✅ Module {$studlyName} created successfully!");
|
if ($this->modelName) {
|
||||||
$this->info("\nNext steps:");
|
$this->createModel();
|
||||||
$this->info("1. Run migrations: php artisan migrate");
|
$this->createMigration();
|
||||||
$this->info("2. Register routes in routes/web.php:");
|
|
||||||
$this->info(" require __DIR__.'/../app/Modules/{$studlyName}/routes.php';");
|
if (!$this->option('no-filament')) {
|
||||||
$this->info("3. Access at: /{$kebabName}");
|
$this->createFilamentResource();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->createTests();
|
||||||
|
$this->registerServiceProvider();
|
||||||
|
|
||||||
|
$this->newLine();
|
||||||
|
$this->info("✅ Module {$this->studlyName} created successfully!");
|
||||||
|
$this->newLine();
|
||||||
|
$this->warn("Next steps:");
|
||||||
|
$this->line(" 1. Run migrations: <info>php artisan migrate</info>");
|
||||||
|
$this->line(" 2. Seed permissions: <info>php artisan db:seed --class=RolePermissionSeeder</info>");
|
||||||
|
$this->line(" 3. Clear caches: <info>php artisan optimize:clear</info>");
|
||||||
|
$this->newLine();
|
||||||
|
$this->line(" Access at:");
|
||||||
|
$this->line(" - Frontend: <info>/{$this->kebabName}</info>");
|
||||||
|
if ($this->modelName && !$this->option('no-filament')) {
|
||||||
|
$this->line(" - Admin: <info>/admin → {$this->studlyName}</info>");
|
||||||
|
}
|
||||||
|
if ($this->option('api')) {
|
||||||
|
$this->line(" - API: <info>/api/{$this->kebabName}</info>");
|
||||||
|
}
|
||||||
|
|
||||||
return self::SUCCESS;
|
return self::SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function createDirectories(string $path): void
|
protected function createDirectoryStructure(): void
|
||||||
{
|
{
|
||||||
File::makeDirectory($path, 0755, true);
|
$directories = [
|
||||||
File::makeDirectory("{$path}/Models", 0755, true);
|
'',
|
||||||
File::makeDirectory("{$path}/Controllers", 0755, true);
|
'/Config',
|
||||||
File::makeDirectory("{$path}/Views", 0755, true);
|
'/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);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function createModel(string $path, string $name): void
|
$this->info("✓ Created directory structure");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function createServiceProvider(): void
|
||||||
{
|
{
|
||||||
$stub = <<<PHP
|
$stub = <<<PHP
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Modules\\{$name}\Models;
|
namespace App\Modules\\{$this->studlyName};
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Support\ServiceProvider;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
class {$name} extends Model
|
class {$this->studlyName}ServiceProvider extends ServiceProvider
|
||||||
{
|
{
|
||||||
use HasFactory;
|
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->info("✓ Created ServiceProvider");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function createConfig(): void
|
||||||
|
{
|
||||||
|
$stub = <<<PHP
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
'name' => '{$this->studlyName}',
|
||||||
|
'slug' => '{$this->kebabName}',
|
||||||
|
'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->info("✓ Created Config");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function createPermissions(): void
|
||||||
|
{
|
||||||
|
$stub = <<<PHP
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
'{$this->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->info("✓ Created Permissions");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function createController(): void
|
||||||
|
{
|
||||||
|
$stub = <<<PHP
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Modules\\{$this->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->info("✓ Created Controller");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function createRoutes(): void
|
||||||
|
{
|
||||||
|
// Web routes
|
||||||
|
$webStub = <<<PHP
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Modules\\{$this->studlyName}\Http\Controllers\\{$this->studlyName}Controller;
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
|
Route::get('/', [{$this->studlyName}Controller::class, 'index'])->name('index');
|
||||||
|
PHP;
|
||||||
|
|
||||||
|
File::put("{$this->modulePath}/Routes/web.php", $webStub);
|
||||||
|
|
||||||
|
// API routes (if requested)
|
||||||
|
if ($this->option('api')) {
|
||||||
|
$apiStub = <<<PHP
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
|
Route::middleware('auth:sanctum')->group(function () {
|
||||||
|
// API routes here
|
||||||
|
});
|
||||||
|
PHP;
|
||||||
|
|
||||||
|
File::put("{$this->modulePath}/Routes/api.php", $apiStub);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info("✓ Created Routes" . ($this->option('api') ? ' (web + api)' : ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function createViews(): void
|
||||||
|
{
|
||||||
|
$stub = <<<BLADE
|
||||||
|
<x-app-layout>
|
||||||
|
<x-slot name="header">
|
||||||
|
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
|
||||||
|
{{ __('{$this->studlyName}') }}
|
||||||
|
</h2>
|
||||||
|
</x-slot>
|
||||||
|
|
||||||
|
<div class="py-12">
|
||||||
|
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||||
|
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
|
||||||
|
<div class="p-6 text-gray-900 dark:text-gray-100">
|
||||||
|
{{ __('{$this->studlyName} module is working!') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</x-app-layout>
|
||||||
|
BLADE;
|
||||||
|
|
||||||
|
File::put("{$this->modulePath}/Resources/views/index.blade.php", $stub);
|
||||||
|
$this->info("✓ Created Views");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function createModel(): void
|
||||||
|
{
|
||||||
|
$tableName = Str::snake(Str::plural($this->modelName));
|
||||||
|
|
||||||
|
$stub = <<<PHP
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Modules\\{$this->studlyName}\Models;
|
||||||
|
|
||||||
|
use App\Traits\ModuleAuditable;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use OwenIt\Auditing\Contracts\Auditable;
|
||||||
|
|
||||||
|
class {$this->modelName} extends Model implements Auditable
|
||||||
|
{
|
||||||
|
use HasFactory, ModuleAuditable;
|
||||||
|
|
||||||
|
protected \$table = '{$tableName}';
|
||||||
|
|
||||||
protected \$fillable = [
|
protected \$fillable = [
|
||||||
'name',
|
'name',
|
||||||
'description',
|
'description',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
protected \$casts = [
|
||||||
|
//
|
||||||
|
];
|
||||||
}
|
}
|
||||||
PHP;
|
PHP;
|
||||||
|
|
||||||
File::put("{$path}/Models/{$name}.php", $stub);
|
File::put("{$this->modulePath}/Models/{$this->modelName}.php", $stub);
|
||||||
$this->info("✓ Created Model: {$name}");
|
$this->info("✓ Created Model: {$this->modelName}");
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function createMigration(string $name): void
|
protected function createMigration(): void
|
||||||
{
|
{
|
||||||
$table = Str::snake(Str::plural($name));
|
$tableName = Str::snake(Str::plural($this->modelName));
|
||||||
$timestamp = date('Y_m_d_His');
|
$timestamp = date('Y_m_d_His');
|
||||||
$migrationName = "create_{$table}_table";
|
$migrationName = "create_{$tableName}_table";
|
||||||
|
|
||||||
$stub = <<<PHP
|
$stub = <<<PHP
|
||||||
<?php
|
<?php
|
||||||
@@ -96,7 +336,7 @@ protected function createMigration(string $name): void
|
|||||||
{
|
{
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::create('{$table}', function (Blueprint \$table) {
|
Schema::create('{$tableName}', function (Blueprint \$table) {
|
||||||
\$table->id();
|
\$table->id();
|
||||||
\$table->string('name');
|
\$table->string('name');
|
||||||
\$table->text('description')->nullable();
|
\$table->text('description')->nullable();
|
||||||
@@ -106,231 +346,252 @@ public function up(): void
|
|||||||
|
|
||||||
public function down(): void
|
public function down(): void
|
||||||
{
|
{
|
||||||
Schema::dropIfExists('{$table}');
|
Schema::dropIfExists('{$tableName}');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
PHP;
|
PHP;
|
||||||
|
|
||||||
$migrationPath = database_path("migrations/{$timestamp}_{$migrationName}.php");
|
$migrationPath = "{$this->modulePath}/Database/Migrations/{$timestamp}_{$migrationName}.php";
|
||||||
File::put($migrationPath, $stub);
|
File::put($migrationPath, $stub);
|
||||||
$this->info("✓ Created Migration: {$migrationName}");
|
$this->info("✓ Created Migration: {$migrationName}");
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function createController(string $path, string $name): void
|
protected function createFilamentResource(): void
|
||||||
{
|
{
|
||||||
$plural = Str::plural($name);
|
$modelClass = "App\\Modules\\{$this->studlyName}\\Models\\{$this->modelName}";
|
||||||
$variable = Str::camel($name);
|
$tableName = Str::snake(Str::plural($this->modelName));
|
||||||
$pluralVariable = Str::camel($plural);
|
|
||||||
|
|
||||||
$stub = <<<PHP
|
// Create the resource
|
||||||
|
$resourceStub = <<<PHP
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Modules\\{$name}\Controllers;
|
namespace App\Modules\\{$this->studlyName}\Filament\Resources;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Modules\\{$this->studlyName}\Filament\Resources\\{$this->modelName}Resource\Pages;
|
||||||
use App\Modules\\{$name}\Models\\{$name};
|
use App\Modules\\{$this->studlyName}\Models\\{$this->modelName};
|
||||||
use Illuminate\Http\Request;
|
use Filament\Forms;
|
||||||
|
use Filament\Forms\Form;
|
||||||
|
use Filament\Resources\Resource;
|
||||||
|
use Filament\Tables;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
|
||||||
class {$name}Controller extends Controller
|
class {$this->modelName}Resource extends Resource
|
||||||
{
|
{
|
||||||
public function index()
|
protected static ?string \$model = {$this->modelName}::class;
|
||||||
|
|
||||||
|
protected static ?string \$navigationIcon = 'heroicon-o-rectangle-stack';
|
||||||
|
|
||||||
|
protected static ?string \$navigationGroup = '{$this->studlyName}';
|
||||||
|
|
||||||
|
protected static ?int \$navigationSort = 1;
|
||||||
|
|
||||||
|
public static function canAccess(): bool
|
||||||
{
|
{
|
||||||
\${$pluralVariable} = {$name}::latest()->paginate(15);
|
return auth()->user()?->can('{$this->snakeName}.view') ?? false;
|
||||||
return view('{$name}::index', compact('{$pluralVariable}'));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function create()
|
public static function form(Form \$form): Form
|
||||||
{
|
{
|
||||||
return view('{$name}::create');
|
return \$form
|
||||||
}
|
->schema([
|
||||||
|
Forms\Components\Section::make('{$this->modelName} Details')
|
||||||
public function store(Request \$request)
|
->schema([
|
||||||
{
|
Forms\Components\TextInput::make('name')
|
||||||
\$validated = \$request->validate([
|
->required()
|
||||||
'name' => 'required|string|max:255',
|
->maxLength(255),
|
||||||
'description' => 'nullable|string',
|
Forms\Components\Textarea::make('description')
|
||||||
|
->rows(3),
|
||||||
|
]),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
{$name}::create(\$validated);
|
|
||||||
|
|
||||||
return redirect()->route('{$variable}.index')
|
|
||||||
->with('success', '{$name} created successfully.');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function show({$name} \${$variable})
|
public static function table(Table \$table): Table
|
||||||
{
|
{
|
||||||
return view('{$name}::show', compact('{$variable}'));
|
return \$table
|
||||||
}
|
->columns([
|
||||||
|
Tables\Columns\TextColumn::make('name')
|
||||||
public function edit({$name} \${$variable})
|
->searchable()
|
||||||
{
|
->sortable(),
|
||||||
return view('{$name}::edit', compact('{$variable}'));
|
Tables\Columns\TextColumn::make('description')
|
||||||
}
|
->limit(50),
|
||||||
|
Tables\Columns\TextColumn::make('created_at')
|
||||||
public function update(Request \$request, {$name} \${$variable})
|
->dateTime()
|
||||||
{
|
->sortable()
|
||||||
\$validated = \$request->validate([
|
->toggleable(isToggledHiddenByDefault: true),
|
||||||
'name' => 'required|string|max:255',
|
])
|
||||||
'description' => 'nullable|string',
|
->filters([
|
||||||
|
//
|
||||||
|
])
|
||||||
|
->actions([
|
||||||
|
Tables\Actions\EditAction::make(),
|
||||||
|
Tables\Actions\DeleteAction::make(),
|
||||||
|
])
|
||||||
|
->bulkActions([
|
||||||
|
Tables\Actions\BulkActionGroup::make([
|
||||||
|
Tables\Actions\DeleteBulkAction::make(),
|
||||||
|
]),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
\${$variable}->update(\$validated);
|
|
||||||
|
|
||||||
return redirect()->route('{$variable}.index')
|
|
||||||
->with('success', '{$name} updated successfully.');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function destroy({$name} \${$variable})
|
public static function getRelations(): array
|
||||||
{
|
{
|
||||||
\${$variable}->delete();
|
return [
|
||||||
|
//
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
return redirect()->route('{$variable}.index')
|
public static function getPages(): array
|
||||||
->with('success', '{$name} deleted successfully.');
|
{
|
||||||
|
return [
|
||||||
|
'index' => Pages\List{$this->modelName}s::route('/'),
|
||||||
|
'create' => Pages\Create{$this->modelName}::route('/create'),
|
||||||
|
'edit' => Pages\Edit{$this->modelName}::route('/{record}/edit'),
|
||||||
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
PHP;
|
PHP;
|
||||||
|
|
||||||
File::put("{$path}/Controllers/{$name}Controller.php", $stub);
|
File::put("{$this->modulePath}/Filament/Resources/{$this->modelName}Resource.php", $resourceStub);
|
||||||
$this->info("✓ Created Controller: {$name}Controller");
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function createRoutes(string $path, string $name, string $kebabName): void
|
// Create Pages directory
|
||||||
{
|
File::makeDirectory("{$this->modulePath}/Filament/Resources/{$this->modelName}Resource/Pages", 0755, true);
|
||||||
$variable = Str::camel($name);
|
|
||||||
|
|
||||||
$stub = <<<PHP
|
// Create List page
|
||||||
|
$listStub = <<<PHP
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Illuminate\Support\Facades\Route;
|
namespace App\Modules\\{$this->studlyName}\Filament\Resources\\{$this->modelName}Resource\Pages;
|
||||||
use App\Modules\\{$name}\Controllers\\{$name}Controller;
|
|
||||||
|
|
||||||
Route::middleware(['web', 'auth'])->group(function () {
|
use App\Modules\\{$this->studlyName}\Filament\Resources\\{$this->modelName}Resource;
|
||||||
Route::resource('{$kebabName}', {$name}Controller::class)->names('{$variable}');
|
use Filament\Actions;
|
||||||
});
|
use Filament\Resources\Pages\ListRecords;
|
||||||
|
|
||||||
|
class List{$this->modelName}s extends ListRecords
|
||||||
|
{
|
||||||
|
protected static string \$resource = {$this->modelName}Resource::class;
|
||||||
|
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Actions\CreateAction::make(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
PHP;
|
PHP;
|
||||||
|
|
||||||
File::put("{$path}/routes.php", $stub);
|
File::put("{$this->modulePath}/Filament/Resources/{$this->modelName}Resource/Pages/List{$this->modelName}s.php", $listStub);
|
||||||
$this->info("✓ Created Routes");
|
|
||||||
|
// Create Create page
|
||||||
|
$createStub = <<<PHP
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Modules\\{$this->studlyName}\Filament\Resources\\{$this->modelName}Resource\Pages;
|
||||||
|
|
||||||
|
use App\Modules\\{$this->studlyName}\Filament\Resources\\{$this->modelName}Resource;
|
||||||
|
use Filament\Resources\Pages\CreateRecord;
|
||||||
|
|
||||||
|
class Create{$this->modelName} extends CreateRecord
|
||||||
|
{
|
||||||
|
protected static string \$resource = {$this->modelName}Resource::class;
|
||||||
|
}
|
||||||
|
PHP;
|
||||||
|
|
||||||
|
File::put("{$this->modulePath}/Filament/Resources/{$this->modelName}Resource/Pages/Create{$this->modelName}.php", $createStub);
|
||||||
|
|
||||||
|
// Create Edit page
|
||||||
|
$editStub = <<<PHP
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Modules\\{$this->studlyName}\Filament\Resources\\{$this->modelName}Resource\Pages;
|
||||||
|
|
||||||
|
use App\Modules\\{$this->studlyName}\Filament\Resources\\{$this->modelName}Resource;
|
||||||
|
use Filament\Actions;
|
||||||
|
use Filament\Resources\Pages\EditRecord;
|
||||||
|
|
||||||
|
class Edit{$this->modelName} extends EditRecord
|
||||||
|
{
|
||||||
|
protected static string \$resource = {$this->modelName}Resource::class;
|
||||||
|
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Actions\DeleteAction::make(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PHP;
|
||||||
|
|
||||||
|
File::put("{$this->modulePath}/Filament/Resources/{$this->modelName}Resource/Pages/Edit{$this->modelName}.php", $editStub);
|
||||||
|
|
||||||
|
$this->info("✓ Created Filament Resource: {$this->modelName}Resource");
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function createViews(string $path, string $name, string $kebabName): void
|
protected function createTests(): void
|
||||||
{
|
{
|
||||||
$plural = Str::plural($name);
|
$testPath = base_path("tests/Feature/Modules/{$this->studlyName}");
|
||||||
$variable = Str::camel($name);
|
File::makeDirectory($testPath, 0755, true);
|
||||||
$pluralVariable = Str::camel($plural);
|
|
||||||
|
|
||||||
$indexView = <<<BLADE
|
|
||||||
<x-app-layout>
|
|
||||||
<x-slot name="header">
|
|
||||||
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
|
|
||||||
{{ __('{$plural}') }}
|
|
||||||
</h2>
|
|
||||||
</x-slot>
|
|
||||||
|
|
||||||
<div class="py-12">
|
|
||||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
|
||||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
|
|
||||||
<div class="p-6 text-gray-900">
|
|
||||||
<div class="flex justify-between mb-4">
|
|
||||||
<h3 class="text-lg font-semibold">{$plural} List</h3>
|
|
||||||
<a href="{{ route('{$variable}.create') }}" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
|
|
||||||
Create New
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if(session('success'))
|
|
||||||
<div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded mb-4">
|
|
||||||
{{ session('success') }}
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
|
|
||||||
<table class="min-w-full divide-y divide-gray-200">
|
|
||||||
<thead class="bg-gray-50">
|
|
||||||
<tr>
|
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Name</th>
|
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Description</th>
|
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody class="bg-white divide-y divide-gray-200">
|
|
||||||
@forelse(\${$pluralVariable} as \${$variable})
|
|
||||||
<tr>
|
|
||||||
<td class="px-6 py-4">{{ \${$variable}->name }}</td>
|
|
||||||
<td class="px-6 py-4">{{ \${$variable}->description }}</td>
|
|
||||||
<td class="px-6 py-4">
|
|
||||||
<a href="{{ route('{$variable}.edit', \${$variable}) }}" class="text-blue-600 hover:text-blue-900">Edit</a>
|
|
||||||
<form action="{{ route('{$variable}.destroy', \${$variable}) }}" method="POST" class="inline">
|
|
||||||
@csrf
|
|
||||||
@method('DELETE')
|
|
||||||
<button type="submit" class="text-red-600 hover:text-red-900 ml-2">Delete</button>
|
|
||||||
</form>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
@empty
|
|
||||||
<tr>
|
|
||||||
<td colspan="3" class="px-6 py-4 text-center">No {$pluralVariable} found.</td>
|
|
||||||
</tr>
|
|
||||||
@endforelse
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<div class="mt-4">
|
|
||||||
{{ \${$pluralVariable}->links() }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</x-app-layout>
|
|
||||||
BLADE;
|
|
||||||
|
|
||||||
File::put("{$path}/Views/index.blade.php", $indexView);
|
|
||||||
$this->info("✓ Created Views");
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function createFilamentResource(string $name): void
|
|
||||||
{
|
|
||||||
$this->call('make:filament-resource', [
|
|
||||||
'name' => $name,
|
|
||||||
'--generate' => true,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function createTests(string $name): void
|
|
||||||
{
|
|
||||||
$variable = Str::camel($name);
|
|
||||||
|
|
||||||
$stub = <<<PHP
|
$stub = <<<PHP
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Modules\\{$name}\Models\\{$name};
|
namespace Tests\Feature\Modules\\{$this->studlyName};
|
||||||
|
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
it('can list {$variable}s', function () {
|
class {$this->studlyName}Test extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
public function test_user_can_view_{$this->snakeName}_index(): void
|
||||||
|
{
|
||||||
\$user = User::factory()->create();
|
\$user = User::factory()->create();
|
||||||
{$name}::factory()->count(3)->create();
|
\$user->givePermissionTo('{$this->snakeName}.view');
|
||||||
|
|
||||||
\$response = \$this->actingAs(\$user)->get(route('{$variable}.index'));
|
\$response = \$this->actingAs(\$user)
|
||||||
|
->get('/{$this->kebabName}');
|
||||||
|
|
||||||
\$response->assertStatus(200);
|
\$response->assertStatus(200);
|
||||||
});
|
}
|
||||||
|
|
||||||
it('can create {$variable}', function () {
|
public function test_unauthorized_user_cannot_view_{$this->snakeName}(): void
|
||||||
|
{
|
||||||
\$user = User::factory()->create();
|
\$user = User::factory()->create();
|
||||||
|
|
||||||
\$response = \$this->actingAs(\$user)->post(route('{$variable}.store'), [
|
\$response = \$this->actingAs(\$user)
|
||||||
'name' => 'Test {$name}',
|
->get('/{$this->kebabName}');
|
||||||
'description' => 'Test description',
|
|
||||||
]);
|
|
||||||
|
|
||||||
\$response->assertRedirect(route('{$variable}.index'));
|
\$response->assertStatus(403);
|
||||||
\$this->assertDatabaseHas('{$variable}s', ['name' => 'Test {$name}']);
|
}
|
||||||
});
|
}
|
||||||
PHP;
|
PHP;
|
||||||
|
|
||||||
$testPath = base_path("tests/Modules/{$name}");
|
File::put("{$testPath}/{$this->studlyName}Test.php", $stub);
|
||||||
File::makeDirectory($testPath, 0755, true);
|
|
||||||
File::put("{$testPath}/{$name}Test.php", $stub);
|
|
||||||
$this->info("✓ Created Tests");
|
$this->info("✓ Created Tests");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)) {
|
||||||
|
// Add the provider before the closing bracket
|
||||||
|
$content = preg_replace(
|
||||||
|
'/(\];)/',
|
||||||
|
" {$providerClass},\n$1",
|
||||||
|
$content
|
||||||
|
);
|
||||||
|
File::put($providersPath, $content);
|
||||||
|
$this->info("✓ Registered ServiceProvider in bootstrap/providers.php");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->warn("⚠ Could not auto-register ServiceProvider. Add manually to config/app.php:");
|
||||||
|
$this->line(" App\\Modules\\{$this->studlyName}\\{$this->studlyName}ServiceProvider::class");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
186
src/app/Filament/Pages/ModuleGenerator.php
Normal file
186
src/app/Filament/Pages/ModuleGenerator.php
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Pages;
|
||||||
|
|
||||||
|
use App\Services\ModuleDiscoveryService;
|
||||||
|
use App\Services\ModuleGeneratorService;
|
||||||
|
use Filament\Forms\Components\Section;
|
||||||
|
use Filament\Forms\Components\TextInput;
|
||||||
|
use Filament\Forms\Components\Textarea;
|
||||||
|
use Filament\Forms\Components\Toggle;
|
||||||
|
use Filament\Forms\Concerns\InteractsWithForms;
|
||||||
|
use Filament\Forms\Contracts\HasForms;
|
||||||
|
use Filament\Forms\Form;
|
||||||
|
use Filament\Notifications\Notification;
|
||||||
|
use Filament\Pages\Page;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class ModuleGenerator extends Page implements HasForms
|
||||||
|
{
|
||||||
|
use InteractsWithForms;
|
||||||
|
|
||||||
|
protected static ?string $navigationIcon = 'heroicon-o-cube';
|
||||||
|
protected static ?string $navigationLabel = 'Module Generator';
|
||||||
|
protected static ?string $navigationGroup = 'Development';
|
||||||
|
protected static ?int $navigationSort = 100;
|
||||||
|
protected static string $view = 'filament.pages.module-generator';
|
||||||
|
|
||||||
|
public ?array $data = [];
|
||||||
|
public ?array $result = null;
|
||||||
|
public array $modules = [];
|
||||||
|
public array $summary = [];
|
||||||
|
public ?string $expandedModule = null;
|
||||||
|
|
||||||
|
public static function canAccess(): bool
|
||||||
|
{
|
||||||
|
// Only available in local environment
|
||||||
|
return app()->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('
|
||||||
|
<div class="text-sm text-gray-600 dark:text-gray-400 font-mono bg-gray-50 dark:bg-gray-900 p-4 rounded-lg">
|
||||||
|
<div class="font-bold mb-2">app/Modules/{ModuleName}/</div>
|
||||||
|
<div class="ml-4">
|
||||||
|
├── Config/{module_name}.php<br>
|
||||||
|
├── Database/Migrations/<br>
|
||||||
|
├── Database/Seeders/<br>
|
||||||
|
├── Filament/Resources/<br>
|
||||||
|
├── Http/Controllers/{ModuleName}Controller.php<br>
|
||||||
|
├── Http/Middleware/<br>
|
||||||
|
├── Http/Requests/<br>
|
||||||
|
├── Models/<br>
|
||||||
|
├── Policies/<br>
|
||||||
|
├── Services/<br>
|
||||||
|
├── Routes/web.php<br>
|
||||||
|
├── Resources/views/index.blade.php<br>
|
||||||
|
├── Permissions.php<br>
|
||||||
|
├── {ModuleName}ServiceProvider.php<br>
|
||||||
|
└── README.md
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
')),
|
||||||
|
])
|
||||||
|
->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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -31,6 +31,7 @@ public function mount(): void
|
|||||||
$siteDescription = Setting::get('site_description');
|
$siteDescription = Setting::get('site_description');
|
||||||
$contactEmail = Setting::get('contact_email');
|
$contactEmail = Setting::get('contact_email');
|
||||||
$maintenanceMode = Setting::get('maintenance_mode', false);
|
$maintenanceMode = Setting::get('maintenance_mode', false);
|
||||||
|
$enableRegistration = Setting::get('enable_registration', false);
|
||||||
|
|
||||||
$this->form->fill([
|
$this->form->fill([
|
||||||
'site_name' => is_string($siteName) ? $siteName : config('app.name'),
|
'site_name' => is_string($siteName) ? $siteName : config('app.name'),
|
||||||
@@ -41,6 +42,7 @@ public function mount(): void
|
|||||||
'site_description' => is_string($siteDescription) || is_null($siteDescription) ? $siteDescription : '',
|
'site_description' => is_string($siteDescription) || is_null($siteDescription) ? $siteDescription : '',
|
||||||
'contact_email' => is_string($contactEmail) || is_null($contactEmail) ? $contactEmail : '',
|
'contact_email' => is_string($contactEmail) || is_null($contactEmail) ? $contactEmail : '',
|
||||||
'maintenance_mode' => is_bool($maintenanceMode) ? $maintenanceMode : false,
|
'maintenance_mode' => is_bool($maintenanceMode) ? $maintenanceMode : false,
|
||||||
|
'enable_registration' => is_bool($enableRegistration) ? $enableRegistration : false,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,6 +89,10 @@ public function form(Form $form): Form
|
|||||||
|
|
||||||
Forms\Components\Section::make('System')
|
Forms\Components\Section::make('System')
|
||||||
->schema([
|
->schema([
|
||||||
|
Forms\Components\Toggle::make('enable_registration')
|
||||||
|
->label('Enable User Registration')
|
||||||
|
->helperText('Allow new users to register. When disabled, only admins can create users.'),
|
||||||
|
|
||||||
Forms\Components\Toggle::make('maintenance_mode')
|
Forms\Components\Toggle::make('maintenance_mode')
|
||||||
->label('Maintenance Mode')
|
->label('Maintenance Mode')
|
||||||
->helperText('Enable to put the site in maintenance mode'),
|
->helperText('Enable to put the site in maintenance mode'),
|
||||||
@@ -116,6 +122,7 @@ public function save(): void
|
|||||||
Setting::set('site_description', $data['site_description'] ?? '');
|
Setting::set('site_description', $data['site_description'] ?? '');
|
||||||
Setting::set('contact_email', $data['contact_email'] ?? '');
|
Setting::set('contact_email', $data['contact_email'] ?? '');
|
||||||
Setting::set('maintenance_mode', $data['maintenance_mode'] ?? false, 'boolean');
|
Setting::set('maintenance_mode', $data['maintenance_mode'] ?? false, 'boolean');
|
||||||
|
Setting::set('enable_registration', $data['enable_registration'] ?? false, 'boolean');
|
||||||
|
|
||||||
Notification::make()
|
Notification::make()
|
||||||
->title('Settings saved successfully')
|
->title('Settings saved successfully')
|
||||||
|
|||||||
231
src/app/Filament/Resources/AuditResource.php
Normal file
231
src/app/Filament/Resources/AuditResource.php
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources;
|
||||||
|
|
||||||
|
use App\Filament\Resources\AuditResource\Pages;
|
||||||
|
use App\Models\Audit;
|
||||||
|
use Filament\Forms;
|
||||||
|
use Filament\Forms\Form;
|
||||||
|
use Filament\Resources\Resource;
|
||||||
|
use Filament\Tables;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
use Filament\Infolists;
|
||||||
|
use Filament\Infolists\Infolist;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
|
||||||
|
class AuditResource extends Resource
|
||||||
|
{
|
||||||
|
protected static ?string $model = Audit::class;
|
||||||
|
|
||||||
|
protected static ?string $navigationIcon = 'heroicon-o-clipboard-document-list';
|
||||||
|
|
||||||
|
protected static ?string $navigationGroup = 'System';
|
||||||
|
|
||||||
|
protected static ?string $navigationLabel = 'Audit Trail';
|
||||||
|
|
||||||
|
protected static ?int $navigationSort = 100;
|
||||||
|
|
||||||
|
protected static ?string $modelLabel = 'Audit Log';
|
||||||
|
|
||||||
|
protected static ?string $pluralModelLabel = 'Audit Trail';
|
||||||
|
|
||||||
|
public static function canAccess(): bool
|
||||||
|
{
|
||||||
|
return auth()->user()?->can('audit.view') ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function canCreate(): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function canEdit($record): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function canDelete($record): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function form(Form $form): Form
|
||||||
|
{
|
||||||
|
return $form->schema([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function table(Table $table): Table
|
||||||
|
{
|
||||||
|
return $table
|
||||||
|
->columns([
|
||||||
|
Tables\Columns\TextColumn::make('created_at')
|
||||||
|
->label('Date/Time')
|
||||||
|
->dateTime('M j, Y H:i:s')
|
||||||
|
->sortable(),
|
||||||
|
|
||||||
|
Tables\Columns\TextColumn::make('user.name')
|
||||||
|
->label('User')
|
||||||
|
->default('System')
|
||||||
|
->searchable()
|
||||||
|
->sortable(),
|
||||||
|
|
||||||
|
Tables\Columns\BadgeColumn::make('event')
|
||||||
|
->label('Action')
|
||||||
|
->colors([
|
||||||
|
'success' => 'created',
|
||||||
|
'warning' => 'updated',
|
||||||
|
'danger' => 'deleted',
|
||||||
|
'info' => 'restored',
|
||||||
|
])
|
||||||
|
->formatStateUsing(fn (string $state): string => ucfirst($state)),
|
||||||
|
|
||||||
|
Tables\Columns\TextColumn::make('auditable_type')
|
||||||
|
->label('Model')
|
||||||
|
->formatStateUsing(fn (string $state): string => class_basename($state))
|
||||||
|
->sortable(),
|
||||||
|
|
||||||
|
Tables\Columns\TextColumn::make('auditable_id')
|
||||||
|
->label('Record ID')
|
||||||
|
->sortable(),
|
||||||
|
|
||||||
|
Tables\Columns\TextColumn::make('ip_address')
|
||||||
|
->label('IP Address')
|
||||||
|
->toggleable(isToggledHiddenByDefault: true),
|
||||||
|
|
||||||
|
Tables\Columns\TextColumn::make('url')
|
||||||
|
->label('URL')
|
||||||
|
->limit(30)
|
||||||
|
->toggleable(isToggledHiddenByDefault: true),
|
||||||
|
])
|
||||||
|
->defaultSort('created_at', 'desc')
|
||||||
|
->filters([
|
||||||
|
Tables\Filters\SelectFilter::make('event')
|
||||||
|
->label('Action')
|
||||||
|
->options([
|
||||||
|
'created' => 'Created',
|
||||||
|
'updated' => 'Updated',
|
||||||
|
'deleted' => 'Deleted',
|
||||||
|
'restored' => 'Restored',
|
||||||
|
]),
|
||||||
|
|
||||||
|
Tables\Filters\SelectFilter::make('user_id')
|
||||||
|
->label('User')
|
||||||
|
->relationship('user', 'name')
|
||||||
|
->searchable()
|
||||||
|
->preload(),
|
||||||
|
|
||||||
|
Tables\Filters\SelectFilter::make('auditable_type')
|
||||||
|
->label('Model')
|
||||||
|
->options(fn () => Audit::query()
|
||||||
|
->distinct()
|
||||||
|
->pluck('auditable_type')
|
||||||
|
->mapWithKeys(fn ($type) => [$type => class_basename($type)])
|
||||||
|
->toArray()
|
||||||
|
),
|
||||||
|
|
||||||
|
Tables\Filters\Filter::make('created_at')
|
||||||
|
->form([
|
||||||
|
Forms\Components\DatePicker::make('from')
|
||||||
|
->label('From Date'),
|
||||||
|
Forms\Components\DatePicker::make('until')
|
||||||
|
->label('Until Date'),
|
||||||
|
])
|
||||||
|
->query(function (Builder $query, array $data): Builder {
|
||||||
|
return $query
|
||||||
|
->when(
|
||||||
|
$data['from'],
|
||||||
|
fn (Builder $query, $date): Builder => $query->whereDate('created_at', '>=', $date),
|
||||||
|
)
|
||||||
|
->when(
|
||||||
|
$data['until'],
|
||||||
|
fn (Builder $query, $date): Builder => $query->whereDate('created_at', '<=', $date),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
->indicateUsing(function (array $data): array {
|
||||||
|
$indicators = [];
|
||||||
|
if ($data['from'] ?? null) {
|
||||||
|
$indicators['from'] = 'From: ' . $data['from'];
|
||||||
|
}
|
||||||
|
if ($data['until'] ?? null) {
|
||||||
|
$indicators['until'] = 'Until: ' . $data['until'];
|
||||||
|
}
|
||||||
|
return $indicators;
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
->actions([
|
||||||
|
Tables\Actions\ViewAction::make(),
|
||||||
|
])
|
||||||
|
->bulkActions([])
|
||||||
|
->poll('60s');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function infolist(Infolist $infolist): Infolist
|
||||||
|
{
|
||||||
|
return $infolist
|
||||||
|
->schema([
|
||||||
|
Infolists\Components\Section::make('Audit Details')
|
||||||
|
->schema([
|
||||||
|
Infolists\Components\Grid::make(3)
|
||||||
|
->schema([
|
||||||
|
Infolists\Components\TextEntry::make('created_at')
|
||||||
|
->label('Date/Time')
|
||||||
|
->dateTime('F j, Y H:i:s'),
|
||||||
|
|
||||||
|
Infolists\Components\TextEntry::make('user.name')
|
||||||
|
->label('User')
|
||||||
|
->default('System'),
|
||||||
|
|
||||||
|
Infolists\Components\TextEntry::make('event')
|
||||||
|
->label('Action')
|
||||||
|
->badge()
|
||||||
|
->color(fn (string $state): string => match ($state) {
|
||||||
|
'created' => 'success',
|
||||||
|
'updated' => 'warning',
|
||||||
|
'deleted' => 'danger',
|
||||||
|
'restored' => 'info',
|
||||||
|
default => 'gray',
|
||||||
|
})
|
||||||
|
->formatStateUsing(fn (string $state): string => ucfirst($state)),
|
||||||
|
]),
|
||||||
|
|
||||||
|
Infolists\Components\Grid::make(3)
|
||||||
|
->schema([
|
||||||
|
Infolists\Components\TextEntry::make('auditable_type')
|
||||||
|
->label('Model')
|
||||||
|
->formatStateUsing(fn (string $state): string => class_basename($state)),
|
||||||
|
|
||||||
|
Infolists\Components\TextEntry::make('auditable_id')
|
||||||
|
->label('Record ID'),
|
||||||
|
|
||||||
|
Infolists\Components\TextEntry::make('ip_address')
|
||||||
|
->label('IP Address')
|
||||||
|
->default('N/A'),
|
||||||
|
]),
|
||||||
|
|
||||||
|
Infolists\Components\TextEntry::make('url')
|
||||||
|
->label('URL')
|
||||||
|
->columnSpanFull(),
|
||||||
|
]),
|
||||||
|
|
||||||
|
Infolists\Components\Section::make('Changes')
|
||||||
|
->schema([
|
||||||
|
Infolists\Components\ViewEntry::make('changes')
|
||||||
|
->view('filament.infolists.entries.audit-changes')
|
||||||
|
->columnSpanFull(),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getRelations(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getPages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'index' => Pages\ListAudits::route('/'),
|
||||||
|
'view' => Pages\ViewAudit::route('/{record}'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\AuditResource\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\AuditResource;
|
||||||
|
use Filament\Resources\Pages\ListRecords;
|
||||||
|
|
||||||
|
class ListAudits extends ListRecords
|
||||||
|
{
|
||||||
|
protected static string $resource = AuditResource::class;
|
||||||
|
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/app/Filament/Resources/AuditResource/Pages/ViewAudit.php
Normal file
16
src/app/Filament/Resources/AuditResource/Pages/ViewAudit.php
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\AuditResource\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\AuditResource;
|
||||||
|
use Filament\Resources\Pages\ViewRecord;
|
||||||
|
|
||||||
|
class ViewAudit extends ViewRecord
|
||||||
|
{
|
||||||
|
protected static string $resource = AuditResource::class;
|
||||||
|
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
146
src/app/Filament/Resources/MenuResource.php
Normal file
146
src/app/Filament/Resources/MenuResource.php
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources;
|
||||||
|
|
||||||
|
use App\Filament\Resources\MenuResource\Pages;
|
||||||
|
use App\Filament\Resources\MenuResource\RelationManagers;
|
||||||
|
use App\Models\Menu;
|
||||||
|
use Filament\Forms;
|
||||||
|
use Filament\Forms\Form;
|
||||||
|
use Filament\Resources\Resource;
|
||||||
|
use Filament\Tables;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class MenuResource extends Resource
|
||||||
|
{
|
||||||
|
protected static ?string $model = Menu::class;
|
||||||
|
|
||||||
|
protected static ?string $navigationIcon = 'heroicon-o-bars-3';
|
||||||
|
|
||||||
|
protected static ?string $navigationGroup = 'Settings';
|
||||||
|
|
||||||
|
protected static ?int $navigationSort = 10;
|
||||||
|
|
||||||
|
public static function canAccess(): bool
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
return $user?->hasRole('admin') || $user?->can('menus.view');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function canCreate(): bool
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
return $user?->hasRole('admin') || $user?->can('menus.create');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function canEdit($record): bool
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
return $user?->hasRole('admin') || $user?->can('menus.edit');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function canDelete($record): bool
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
return $user?->hasRole('admin') || $user?->can('menus.delete');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function form(Form $form): Form
|
||||||
|
{
|
||||||
|
return $form
|
||||||
|
->schema([
|
||||||
|
Forms\Components\Section::make('Menu Details')
|
||||||
|
->schema([
|
||||||
|
Forms\Components\TextInput::make('name')
|
||||||
|
->required()
|
||||||
|
->maxLength(255)
|
||||||
|
->live(onBlur: true)
|
||||||
|
->afterStateUpdated(fn ($state, Forms\Set $set) =>
|
||||||
|
$set('slug', Str::slug($state))
|
||||||
|
),
|
||||||
|
Forms\Components\TextInput::make('slug')
|
||||||
|
->required()
|
||||||
|
->maxLength(255)
|
||||||
|
->unique(ignoreRecord: true),
|
||||||
|
Forms\Components\Select::make('location')
|
||||||
|
->options([
|
||||||
|
'header' => 'Header Navigation',
|
||||||
|
'footer' => 'Footer Navigation',
|
||||||
|
'sidebar' => 'Sidebar Navigation',
|
||||||
|
])
|
||||||
|
->placeholder('Select a location'),
|
||||||
|
Forms\Components\Toggle::make('is_active')
|
||||||
|
->label('Active')
|
||||||
|
->default(true),
|
||||||
|
])->columns(2),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function table(Table $table): Table
|
||||||
|
{
|
||||||
|
return $table
|
||||||
|
->columns([
|
||||||
|
Tables\Columns\TextColumn::make('name')
|
||||||
|
->searchable()
|
||||||
|
->sortable(),
|
||||||
|
Tables\Columns\TextColumn::make('slug')
|
||||||
|
->searchable()
|
||||||
|
->badge()
|
||||||
|
->color('gray'),
|
||||||
|
Tables\Columns\TextColumn::make('location')
|
||||||
|
->badge()
|
||||||
|
->color(fn (?string $state) => match ($state) {
|
||||||
|
'header' => 'success',
|
||||||
|
'footer' => 'warning',
|
||||||
|
'sidebar' => 'info',
|
||||||
|
default => 'gray',
|
||||||
|
}),
|
||||||
|
Tables\Columns\TextColumn::make('allItems_count')
|
||||||
|
->label('Items')
|
||||||
|
->counts('allItems')
|
||||||
|
->badge(),
|
||||||
|
Tables\Columns\IconColumn::make('is_active')
|
||||||
|
->label('Active')
|
||||||
|
->boolean(),
|
||||||
|
Tables\Columns\TextColumn::make('updated_at')
|
||||||
|
->dateTime()
|
||||||
|
->sortable()
|
||||||
|
->toggleable(isToggledHiddenByDefault: true),
|
||||||
|
])
|
||||||
|
->filters([
|
||||||
|
Tables\Filters\SelectFilter::make('location')
|
||||||
|
->options([
|
||||||
|
'header' => 'Header',
|
||||||
|
'footer' => 'Footer',
|
||||||
|
'sidebar' => 'Sidebar',
|
||||||
|
]),
|
||||||
|
Tables\Filters\TernaryFilter::make('is_active')
|
||||||
|
->label('Active'),
|
||||||
|
])
|
||||||
|
->actions([
|
||||||
|
Tables\Actions\EditAction::make(),
|
||||||
|
])
|
||||||
|
->bulkActions([
|
||||||
|
Tables\Actions\BulkActionGroup::make([
|
||||||
|
Tables\Actions\DeleteBulkAction::make(),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getRelations(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
RelationManagers\ItemsRelationManager::class,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getPages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'index' => Pages\ListMenus::route('/'),
|
||||||
|
'create' => Pages\CreateMenu::route('/create'),
|
||||||
|
'edit' => Pages\EditMenu::route('/{record}/edit'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/app/Filament/Resources/MenuResource/Pages/CreateMenu.php
Normal file
11
src/app/Filament/Resources/MenuResource/Pages/CreateMenu.php
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\MenuResource\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\MenuResource;
|
||||||
|
use Filament\Resources\Pages\CreateRecord;
|
||||||
|
|
||||||
|
class CreateMenu extends CreateRecord
|
||||||
|
{
|
||||||
|
protected static string $resource = MenuResource::class;
|
||||||
|
}
|
||||||
19
src/app/Filament/Resources/MenuResource/Pages/EditMenu.php
Normal file
19
src/app/Filament/Resources/MenuResource/Pages/EditMenu.php
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\MenuResource\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\MenuResource;
|
||||||
|
use Filament\Actions;
|
||||||
|
use Filament\Resources\Pages\EditRecord;
|
||||||
|
|
||||||
|
class EditMenu extends EditRecord
|
||||||
|
{
|
||||||
|
protected static string $resource = MenuResource::class;
|
||||||
|
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Actions\DeleteAction::make(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/app/Filament/Resources/MenuResource/Pages/ListMenus.php
Normal file
19
src/app/Filament/Resources/MenuResource/Pages/ListMenus.php
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\MenuResource\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\MenuResource;
|
||||||
|
use Filament\Actions;
|
||||||
|
use Filament\Resources\Pages\ListRecords;
|
||||||
|
|
||||||
|
class ListMenus extends ListRecords
|
||||||
|
{
|
||||||
|
protected static string $resource = MenuResource::class;
|
||||||
|
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Actions\CreateAction::make(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\MenuResource\RelationManagers;
|
||||||
|
|
||||||
|
use App\Models\MenuItem;
|
||||||
|
use App\Services\MenuService;
|
||||||
|
use Filament\Forms;
|
||||||
|
use Filament\Forms\Form;
|
||||||
|
use Filament\Resources\RelationManagers\RelationManager;
|
||||||
|
use Filament\Tables;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
use Spatie\Permission\Models\Permission;
|
||||||
|
|
||||||
|
class ItemsRelationManager extends RelationManager
|
||||||
|
{
|
||||||
|
protected static string $relationship = 'allItems';
|
||||||
|
|
||||||
|
protected static ?string $title = 'Menu Items';
|
||||||
|
|
||||||
|
public function form(Form $form): Form
|
||||||
|
{
|
||||||
|
$menuService = app(MenuService::class);
|
||||||
|
|
||||||
|
return $form
|
||||||
|
->schema([
|
||||||
|
Forms\Components\Section::make('Item Details')
|
||||||
|
->schema([
|
||||||
|
Forms\Components\TextInput::make('title')
|
||||||
|
->required()
|
||||||
|
->maxLength(255),
|
||||||
|
Forms\Components\Select::make('type')
|
||||||
|
->options([
|
||||||
|
'link' => 'External Link',
|
||||||
|
'route' => 'Named Route',
|
||||||
|
'module' => 'Module',
|
||||||
|
])
|
||||||
|
->default('link')
|
||||||
|
->required()
|
||||||
|
->live(),
|
||||||
|
Forms\Components\TextInput::make('url')
|
||||||
|
->label('URL')
|
||||||
|
->url()
|
||||||
|
->visible(fn (Forms\Get $get) => $get('type') === 'link')
|
||||||
|
->required(fn (Forms\Get $get) => $get('type') === 'link'),
|
||||||
|
Forms\Components\Select::make('route')
|
||||||
|
->label('Route Name')
|
||||||
|
->options(fn () => $menuService->getAvailableRoutes())
|
||||||
|
->searchable()
|
||||||
|
->visible(fn (Forms\Get $get) => $get('type') === 'route')
|
||||||
|
->required(fn (Forms\Get $get) => $get('type') === 'route'),
|
||||||
|
Forms\Components\Select::make('module')
|
||||||
|
->label('Module')
|
||||||
|
->options(fn () => $menuService->getAvailableModules())
|
||||||
|
->searchable()
|
||||||
|
->visible(fn (Forms\Get $get) => $get('type') === 'module')
|
||||||
|
->required(fn (Forms\Get $get) => $get('type') === 'module')
|
||||||
|
->helperText('Users need view permission for this module to see this item'),
|
||||||
|
])->columns(2),
|
||||||
|
|
||||||
|
Forms\Components\Section::make('Permissions & Display')
|
||||||
|
->schema([
|
||||||
|
Forms\Components\Select::make('parent_id')
|
||||||
|
->label('Parent Item')
|
||||||
|
->options(fn () => MenuItem::where('menu_id', $this->ownerRecord->id)
|
||||||
|
->whereNull('parent_id')
|
||||||
|
->pluck('title', 'id'))
|
||||||
|
->placeholder('None (Top Level)')
|
||||||
|
->searchable(),
|
||||||
|
Forms\Components\Select::make('permission')
|
||||||
|
->label('Required Permission')
|
||||||
|
->options(fn () => Permission::pluck('name', 'name'))
|
||||||
|
->searchable()
|
||||||
|
->placeholder('No specific permission required')
|
||||||
|
->helperText('If set, user must have this permission to see this item'),
|
||||||
|
Forms\Components\TextInput::make('icon')
|
||||||
|
->placeholder('heroicon-o-home')
|
||||||
|
->helperText('Heroicon name or custom icon class'),
|
||||||
|
Forms\Components\Select::make('target')
|
||||||
|
->options([
|
||||||
|
'_self' => 'Same Window',
|
||||||
|
'_blank' => 'New Tab',
|
||||||
|
])
|
||||||
|
->default('_self'),
|
||||||
|
Forms\Components\TextInput::make('order')
|
||||||
|
->numeric()
|
||||||
|
->default(0)
|
||||||
|
->helperText('Lower numbers appear first'),
|
||||||
|
Forms\Components\Toggle::make('is_active')
|
||||||
|
->label('Active')
|
||||||
|
->default(true),
|
||||||
|
])->columns(2),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function table(Table $table): Table
|
||||||
|
{
|
||||||
|
return $table
|
||||||
|
->recordTitleAttribute('title')
|
||||||
|
->reorderable('order')
|
||||||
|
->defaultSort('order')
|
||||||
|
->columns([
|
||||||
|
Tables\Columns\TextColumn::make('title')
|
||||||
|
->searchable()
|
||||||
|
->sortable(),
|
||||||
|
Tables\Columns\TextColumn::make('parent.title')
|
||||||
|
->label('Parent')
|
||||||
|
->placeholder('—')
|
||||||
|
->badge()
|
||||||
|
->color('gray'),
|
||||||
|
Tables\Columns\TextColumn::make('type')
|
||||||
|
->badge()
|
||||||
|
->color(fn (string $state) => match ($state) {
|
||||||
|
'link' => 'info',
|
||||||
|
'route' => 'success',
|
||||||
|
'module' => 'warning',
|
||||||
|
default => 'gray',
|
||||||
|
}),
|
||||||
|
Tables\Columns\TextColumn::make('module')
|
||||||
|
->placeholder('—')
|
||||||
|
->toggleable(),
|
||||||
|
Tables\Columns\TextColumn::make('permission')
|
||||||
|
->placeholder('—')
|
||||||
|
->toggleable(isToggledHiddenByDefault: true),
|
||||||
|
Tables\Columns\TextColumn::make('order')
|
||||||
|
->sortable(),
|
||||||
|
Tables\Columns\IconColumn::make('is_active')
|
||||||
|
->label('Active')
|
||||||
|
->boolean(),
|
||||||
|
])
|
||||||
|
->filters([
|
||||||
|
Tables\Filters\SelectFilter::make('type')
|
||||||
|
->options([
|
||||||
|
'link' => 'External Link',
|
||||||
|
'route' => 'Named Route',
|
||||||
|
'module' => 'Module',
|
||||||
|
]),
|
||||||
|
Tables\Filters\TernaryFilter::make('is_active')
|
||||||
|
->label('Active'),
|
||||||
|
])
|
||||||
|
->headerActions([
|
||||||
|
Tables\Actions\CreateAction::make(),
|
||||||
|
])
|
||||||
|
->actions([
|
||||||
|
Tables\Actions\EditAction::make(),
|
||||||
|
Tables\Actions\DeleteAction::make(),
|
||||||
|
])
|
||||||
|
->bulkActions([
|
||||||
|
Tables\Actions\BulkActionGroup::make([
|
||||||
|
Tables\Actions\DeleteBulkAction::make(),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
96
src/app/Filament/Resources/PermissionResource.php
Normal file
96
src/app/Filament/Resources/PermissionResource.php
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources;
|
||||||
|
|
||||||
|
use App\Filament\Resources\PermissionResource\Pages;
|
||||||
|
use Filament\Forms;
|
||||||
|
use Filament\Forms\Form;
|
||||||
|
use Filament\Resources\Resource;
|
||||||
|
use Filament\Tables;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
use Spatie\Permission\Models\Permission;
|
||||||
|
|
||||||
|
class PermissionResource extends Resource
|
||||||
|
{
|
||||||
|
protected static ?string $model = Permission::class;
|
||||||
|
|
||||||
|
protected static ?string $navigationIcon = 'heroicon-o-key';
|
||||||
|
|
||||||
|
protected static ?string $navigationGroup = 'User Management';
|
||||||
|
|
||||||
|
protected static ?int $navigationSort = 3;
|
||||||
|
|
||||||
|
public static function form(Form $form): Form
|
||||||
|
{
|
||||||
|
return $form
|
||||||
|
->schema([
|
||||||
|
Forms\Components\Section::make('Permission Details')
|
||||||
|
->schema([
|
||||||
|
Forms\Components\TextInput::make('name')
|
||||||
|
->required()
|
||||||
|
->unique(ignoreRecord: true)
|
||||||
|
->maxLength(255)
|
||||||
|
->helperText('Use format: resource.action (e.g., users.create, posts.edit)'),
|
||||||
|
Forms\Components\Select::make('guard_name')
|
||||||
|
->options([
|
||||||
|
'web' => 'Web',
|
||||||
|
'api' => 'API',
|
||||||
|
])
|
||||||
|
->default('web')
|
||||||
|
->required(),
|
||||||
|
])->columns(2),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function table(Table $table): Table
|
||||||
|
{
|
||||||
|
return $table
|
||||||
|
->columns([
|
||||||
|
Tables\Columns\TextColumn::make('name')
|
||||||
|
->badge()
|
||||||
|
->color('info')
|
||||||
|
->searchable()
|
||||||
|
->sortable(),
|
||||||
|
Tables\Columns\TextColumn::make('guard_name')
|
||||||
|
->badge()
|
||||||
|
->color('gray'),
|
||||||
|
Tables\Columns\TextColumn::make('roles_count')
|
||||||
|
->counts('roles')
|
||||||
|
->label('Roles')
|
||||||
|
->badge()
|
||||||
|
->color('success'),
|
||||||
|
Tables\Columns\TextColumn::make('created_at')
|
||||||
|
->dateTime()
|
||||||
|
->sortable()
|
||||||
|
->toggleable(isToggledHiddenByDefault: true),
|
||||||
|
])
|
||||||
|
->filters([
|
||||||
|
//
|
||||||
|
])
|
||||||
|
->actions([
|
||||||
|
Tables\Actions\EditAction::make(),
|
||||||
|
Tables\Actions\DeleteAction::make(),
|
||||||
|
])
|
||||||
|
->bulkActions([
|
||||||
|
Tables\Actions\BulkActionGroup::make([
|
||||||
|
Tables\Actions\DeleteBulkAction::make(),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getRelations(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
//
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getPages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'index' => Pages\ListPermissions::route('/'),
|
||||||
|
'create' => Pages\CreatePermission::route('/create'),
|
||||||
|
'edit' => Pages\EditPermission::route('/{record}/edit'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\PermissionResource\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\PermissionResource;
|
||||||
|
use Filament\Resources\Pages\CreateRecord;
|
||||||
|
|
||||||
|
class CreatePermission extends CreateRecord
|
||||||
|
{
|
||||||
|
protected static string $resource = PermissionResource::class;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\PermissionResource\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\PermissionResource;
|
||||||
|
use Filament\Actions;
|
||||||
|
use Filament\Resources\Pages\EditRecord;
|
||||||
|
|
||||||
|
class EditPermission extends EditRecord
|
||||||
|
{
|
||||||
|
protected static string $resource = PermissionResource::class;
|
||||||
|
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Actions\DeleteAction::make(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\PermissionResource\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\PermissionResource;
|
||||||
|
use Filament\Actions;
|
||||||
|
use Filament\Resources\Pages\ListRecords;
|
||||||
|
|
||||||
|
class ListPermissions extends ListRecords
|
||||||
|
{
|
||||||
|
protected static string $resource = PermissionResource::class;
|
||||||
|
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Actions\CreateAction::make(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
117
src/app/Filament/Resources/RoleResource.php
Normal file
117
src/app/Filament/Resources/RoleResource.php
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources;
|
||||||
|
|
||||||
|
use App\Filament\Resources\RoleResource\Pages;
|
||||||
|
use Filament\Forms;
|
||||||
|
use Filament\Forms\Form;
|
||||||
|
use Filament\Resources\Resource;
|
||||||
|
use Filament\Tables;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
use Spatie\Permission\Models\Role;
|
||||||
|
|
||||||
|
class RoleResource extends Resource
|
||||||
|
{
|
||||||
|
protected static ?string $model = Role::class;
|
||||||
|
|
||||||
|
protected static ?string $navigationIcon = 'heroicon-o-shield-check';
|
||||||
|
|
||||||
|
protected static ?string $navigationGroup = 'User Management';
|
||||||
|
|
||||||
|
protected static ?int $navigationSort = 2;
|
||||||
|
|
||||||
|
public static function form(Form $form): Form
|
||||||
|
{
|
||||||
|
return $form
|
||||||
|
->schema([
|
||||||
|
Forms\Components\Section::make('Role Details')
|
||||||
|
->schema([
|
||||||
|
Forms\Components\TextInput::make('name')
|
||||||
|
->required()
|
||||||
|
->unique(ignoreRecord: true)
|
||||||
|
->maxLength(255),
|
||||||
|
Forms\Components\Select::make('guard_name')
|
||||||
|
->options([
|
||||||
|
'web' => 'Web',
|
||||||
|
'api' => 'API',
|
||||||
|
])
|
||||||
|
->default('web')
|
||||||
|
->required(),
|
||||||
|
])->columns(2),
|
||||||
|
|
||||||
|
Forms\Components\Section::make('Permissions')
|
||||||
|
->schema([
|
||||||
|
Forms\Components\CheckboxList::make('permissions')
|
||||||
|
->relationship('permissions', 'name')
|
||||||
|
->columns(3)
|
||||||
|
->helperText('Select permissions for this role'),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function table(Table $table): Table
|
||||||
|
{
|
||||||
|
return $table
|
||||||
|
->columns([
|
||||||
|
Tables\Columns\TextColumn::make('name')
|
||||||
|
->badge()
|
||||||
|
->color(fn (string $state): string => match ($state) {
|
||||||
|
'admin' => 'danger',
|
||||||
|
'editor' => 'warning',
|
||||||
|
default => 'success',
|
||||||
|
})
|
||||||
|
->searchable()
|
||||||
|
->sortable(),
|
||||||
|
Tables\Columns\TextColumn::make('guard_name')
|
||||||
|
->badge()
|
||||||
|
->color('gray'),
|
||||||
|
Tables\Columns\TextColumn::make('permissions_count')
|
||||||
|
->counts('permissions')
|
||||||
|
->label('Permissions')
|
||||||
|
->badge()
|
||||||
|
->color('info'),
|
||||||
|
Tables\Columns\TextColumn::make('users_count')
|
||||||
|
->counts('users')
|
||||||
|
->label('Users')
|
||||||
|
->badge()
|
||||||
|
->color('success'),
|
||||||
|
Tables\Columns\TextColumn::make('created_at')
|
||||||
|
->dateTime()
|
||||||
|
->sortable()
|
||||||
|
->toggleable(isToggledHiddenByDefault: true),
|
||||||
|
])
|
||||||
|
->filters([
|
||||||
|
//
|
||||||
|
])
|
||||||
|
->actions([
|
||||||
|
Tables\Actions\EditAction::make(),
|
||||||
|
Tables\Actions\DeleteAction::make()
|
||||||
|
->before(function (Role $record) {
|
||||||
|
if ($record->name === 'admin') {
|
||||||
|
throw new \Exception('Cannot delete the admin role.');
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
->bulkActions([
|
||||||
|
Tables\Actions\BulkActionGroup::make([
|
||||||
|
Tables\Actions\DeleteBulkAction::make(),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getRelations(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
//
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getPages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'index' => Pages\ListRoles::route('/'),
|
||||||
|
'create' => Pages\CreateRole::route('/create'),
|
||||||
|
'edit' => Pages\EditRole::route('/{record}/edit'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/app/Filament/Resources/RoleResource/Pages/CreateRole.php
Normal file
11
src/app/Filament/Resources/RoleResource/Pages/CreateRole.php
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\RoleResource\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\RoleResource;
|
||||||
|
use Filament\Resources\Pages\CreateRecord;
|
||||||
|
|
||||||
|
class CreateRole extends CreateRecord
|
||||||
|
{
|
||||||
|
protected static string $resource = RoleResource::class;
|
||||||
|
}
|
||||||
19
src/app/Filament/Resources/RoleResource/Pages/EditRole.php
Normal file
19
src/app/Filament/Resources/RoleResource/Pages/EditRole.php
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\RoleResource\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\RoleResource;
|
||||||
|
use Filament\Actions;
|
||||||
|
use Filament\Resources\Pages\EditRecord;
|
||||||
|
|
||||||
|
class EditRole extends EditRecord
|
||||||
|
{
|
||||||
|
protected static string $resource = RoleResource::class;
|
||||||
|
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Actions\DeleteAction::make(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/app/Filament/Resources/RoleResource/Pages/ListRoles.php
Normal file
19
src/app/Filament/Resources/RoleResource/Pages/ListRoles.php
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\RoleResource\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\RoleResource;
|
||||||
|
use Filament\Actions;
|
||||||
|
use Filament\Resources\Pages\ListRecords;
|
||||||
|
|
||||||
|
class ListRoles extends ListRecords
|
||||||
|
{
|
||||||
|
protected static string $resource = RoleResource::class;
|
||||||
|
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Actions\CreateAction::make(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,25 +3,30 @@
|
|||||||
namespace App\Filament\Resources;
|
namespace App\Filament\Resources;
|
||||||
|
|
||||||
use App\Filament\Resources\UserResource\Pages;
|
use App\Filament\Resources\UserResource\Pages;
|
||||||
use App\Filament\Resources\UserResource\RelationManagers;
|
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Filament\Forms;
|
use Filament\Forms;
|
||||||
use Filament\Forms\Form;
|
use Filament\Forms\Form;
|
||||||
use Filament\Resources\Resource;
|
use Filament\Resources\Resource;
|
||||||
use Filament\Tables;
|
use Filament\Tables;
|
||||||
use Filament\Tables\Table;
|
use Filament\Tables\Table;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Support\Facades\Hash;
|
||||||
use Illuminate\Database\Eloquent\SoftDeletingScope;
|
use Spatie\Permission\Models\Role;
|
||||||
|
|
||||||
class UserResource extends Resource
|
class UserResource extends Resource
|
||||||
{
|
{
|
||||||
protected static ?string $model = User::class;
|
protected static ?string $model = User::class;
|
||||||
|
|
||||||
protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack';
|
protected static ?string $navigationIcon = 'heroicon-o-users';
|
||||||
|
|
||||||
|
protected static ?string $navigationGroup = 'User Management';
|
||||||
|
|
||||||
|
protected static ?int $navigationSort = 1;
|
||||||
|
|
||||||
public static function form(Form $form): Form
|
public static function form(Form $form): Form
|
||||||
{
|
{
|
||||||
return $form
|
return $form
|
||||||
|
->schema([
|
||||||
|
Forms\Components\Section::make('User Information')
|
||||||
->schema([
|
->schema([
|
||||||
Forms\Components\TextInput::make('name')
|
Forms\Components\TextInput::make('name')
|
||||||
->required()
|
->required()
|
||||||
@@ -29,12 +34,34 @@ public static function form(Form $form): Form
|
|||||||
Forms\Components\TextInput::make('email')
|
Forms\Components\TextInput::make('email')
|
||||||
->email()
|
->email()
|
||||||
->required()
|
->required()
|
||||||
|
->unique(ignoreRecord: true)
|
||||||
->maxLength(255),
|
->maxLength(255),
|
||||||
Forms\Components\DateTimePicker::make('email_verified_at'),
|
Forms\Components\DateTimePicker::make('email_verified_at')
|
||||||
|
->label('Email Verified At'),
|
||||||
|
])->columns(2),
|
||||||
|
|
||||||
|
Forms\Components\Section::make('Password')
|
||||||
|
->schema([
|
||||||
Forms\Components\TextInput::make('password')
|
Forms\Components\TextInput::make('password')
|
||||||
->password()
|
->password()
|
||||||
->required()
|
->dehydrateStateUsing(fn ($state) => Hash::make($state))
|
||||||
->maxLength(255),
|
->dehydrated(fn ($state) => filled($state))
|
||||||
|
->required(fn (string $context): bool => $context === 'create')
|
||||||
|
->maxLength(255)
|
||||||
|
->confirmed(),
|
||||||
|
Forms\Components\TextInput::make('password_confirmation')
|
||||||
|
->password()
|
||||||
|
->maxLength(255)
|
||||||
|
->dehydrated(false),
|
||||||
|
])->columns(2),
|
||||||
|
|
||||||
|
Forms\Components\Section::make('Roles')
|
||||||
|
->schema([
|
||||||
|
Forms\Components\CheckboxList::make('roles')
|
||||||
|
->relationship('roles', 'name')
|
||||||
|
->columns(3)
|
||||||
|
->helperText('Assign roles to this user'),
|
||||||
|
]),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,26 +70,34 @@ public static function table(Table $table): Table
|
|||||||
return $table
|
return $table
|
||||||
->columns([
|
->columns([
|
||||||
Tables\Columns\TextColumn::make('name')
|
Tables\Columns\TextColumn::make('name')
|
||||||
->searchable(),
|
->searchable()
|
||||||
Tables\Columns\TextColumn::make('email')
|
|
||||||
->searchable(),
|
|
||||||
Tables\Columns\TextColumn::make('email_verified_at')
|
|
||||||
->dateTime()
|
|
||||||
->sortable(),
|
->sortable(),
|
||||||
|
Tables\Columns\TextColumn::make('email')
|
||||||
|
->searchable()
|
||||||
|
->sortable(),
|
||||||
|
Tables\Columns\TextColumn::make('roles.name')
|
||||||
|
->badge()
|
||||||
|
->color('success')
|
||||||
|
->separator(','),
|
||||||
|
Tables\Columns\IconColumn::make('email_verified_at')
|
||||||
|
->label('Verified')
|
||||||
|
->boolean()
|
||||||
|
->trueIcon('heroicon-o-check-circle')
|
||||||
|
->falseIcon('heroicon-o-x-circle'),
|
||||||
Tables\Columns\TextColumn::make('created_at')
|
Tables\Columns\TextColumn::make('created_at')
|
||||||
->dateTime()
|
->dateTime()
|
||||||
->sortable()
|
->sortable()
|
||||||
->toggleable(isToggledHiddenByDefault: true),
|
->toggleable(isToggledHiddenByDefault: true),
|
||||||
Tables\Columns\TextColumn::make('updated_at')
|
|
||||||
->dateTime()
|
|
||||||
->sortable()
|
|
||||||
->toggleable(isToggledHiddenByDefault: true),
|
|
||||||
])
|
])
|
||||||
->filters([
|
->filters([
|
||||||
//
|
Tables\Filters\SelectFilter::make('roles')
|
||||||
|
->relationship('roles', 'name')
|
||||||
|
->multiple()
|
||||||
|
->preload(),
|
||||||
])
|
])
|
||||||
->actions([
|
->actions([
|
||||||
Tables\Actions\EditAction::make(),
|
Tables\Actions\EditAction::make(),
|
||||||
|
Tables\Actions\DeleteAction::make(),
|
||||||
])
|
])
|
||||||
->bulkActions([
|
->bulkActions([
|
||||||
Tables\Actions\BulkActionGroup::make([
|
Tables\Actions\BulkActionGroup::make([
|
||||||
|
|||||||
88
src/app/Helpers/ViteHelper.php
Normal file
88
src/app/Helpers/ViteHelper.php
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Helpers;
|
||||||
|
|
||||||
|
class ViteHelper
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Check if the Vite manifest exists (assets have been built).
|
||||||
|
*/
|
||||||
|
public static function manifestExists(): bool
|
||||||
|
{
|
||||||
|
return file_exists(public_path('build/manifest.json'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get fallback CSS for when Vite assets are not built.
|
||||||
|
* This provides basic styling so the app remains usable.
|
||||||
|
*/
|
||||||
|
public static function fallbackStyles(): string
|
||||||
|
{
|
||||||
|
return <<<'CSS'
|
||||||
|
<style>
|
||||||
|
/* Fallback styles when Vite assets are not built */
|
||||||
|
*, *::before, *::after { box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
font-family: ui-sans-serif, system-ui, sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
.dark body { background: #111827; color: #f9fafb; }
|
||||||
|
.min-h-screen { min-height: 100vh; }
|
||||||
|
.bg-gray-100 { background: #f3f4f6; }
|
||||||
|
.bg-white { background: #fff; }
|
||||||
|
.shadow { box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
|
||||||
|
.max-w-7xl { max-width: 80rem; margin: 0 auto; }
|
||||||
|
.px-4 { padding-left: 1rem; padding-right: 1rem; }
|
||||||
|
.py-6 { padding-top: 1.5rem; padding-bottom: 1.5rem; }
|
||||||
|
.font-semibold { font-weight: 600; }
|
||||||
|
.text-xl { font-size: 1.25rem; }
|
||||||
|
.text-gray-800 { color: #1f2937; }
|
||||||
|
a { color: #3b82f6; text-decoration: none; }
|
||||||
|
a:hover { text-decoration: underline; }
|
||||||
|
.hidden { display: none; }
|
||||||
|
.flex { display: flex; }
|
||||||
|
.items-center { align-items: center; }
|
||||||
|
.justify-between { justify-content: space-between; }
|
||||||
|
.space-x-4 > * + * { margin-left: 1rem; }
|
||||||
|
nav { background: #fff; border-bottom: 1px solid #e5e7eb; padding: 1rem; }
|
||||||
|
.container { max-width: 80rem; margin: 0 auto; padding: 0 1rem; }
|
||||||
|
button, .btn {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
background: #3b82f6;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
button:hover, .btn:hover { background: #2563eb; }
|
||||||
|
input, select, textarea {
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.alert { padding: 1rem; border-radius: 0.375rem; margin-bottom: 1rem; }
|
||||||
|
.alert-warning { background: #fef3c7; border: 1px solid #f59e0b; color: #92400e; }
|
||||||
|
</style>
|
||||||
|
CSS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a warning banner HTML for development.
|
||||||
|
*/
|
||||||
|
public static function devWarningBanner(): string
|
||||||
|
{
|
||||||
|
if (app()->environment('production')) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return <<<'HTML'
|
||||||
|
<div class="alert alert-warning" style="margin:1rem;padding:1rem;background:#fef3c7;border:1px solid #f59e0b;border-radius:0.5rem;color:#92400e;">
|
||||||
|
<strong>⚠️ Development Notice:</strong> Vite assets are not built.
|
||||||
|
Run <code style="background:#fde68a;padding:0.125rem 0.25rem;border-radius:0.25rem;">docker-compose run --rm node npm run build</code> to build assets.
|
||||||
|
</div>
|
||||||
|
HTML;
|
||||||
|
}
|
||||||
|
}
|
||||||
26
src/app/Http/Middleware/CheckRegistrationEnabled.php
Normal file
26
src/app/Http/Middleware/CheckRegistrationEnabled.php
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use App\Models\Setting;
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
class CheckRegistrationEnabled
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handle an incoming request.
|
||||||
|
* Block registration routes if registration is disabled in settings.
|
||||||
|
*/
|
||||||
|
public function handle(Request $request, Closure $next): Response
|
||||||
|
{
|
||||||
|
$registrationEnabled = Setting::get('enable_registration', false);
|
||||||
|
|
||||||
|
if (!$registrationEnabled) {
|
||||||
|
abort(403, 'User registration is currently disabled. Please contact an administrator.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
}
|
||||||
111
src/app/Models/Audit.php
Normal file
111
src/app/Models/Audit.php
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||||
|
|
||||||
|
class Audit extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'audits';
|
||||||
|
|
||||||
|
protected $guarded = [];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'old_values' => 'array',
|
||||||
|
'new_values' => 'array',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the user that performed the action.
|
||||||
|
*/
|
||||||
|
public function user(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the auditable model.
|
||||||
|
*/
|
||||||
|
public function auditable(): MorphTo
|
||||||
|
{
|
||||||
|
return $this->morphTo();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the event badge color.
|
||||||
|
*/
|
||||||
|
public function getEventColorAttribute(): string
|
||||||
|
{
|
||||||
|
return match ($this->event) {
|
||||||
|
'created' => 'success',
|
||||||
|
'updated' => 'warning',
|
||||||
|
'deleted' => 'danger',
|
||||||
|
'restored' => 'info',
|
||||||
|
default => 'gray',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a human-readable model name.
|
||||||
|
*/
|
||||||
|
public function getModelNameAttribute(): string
|
||||||
|
{
|
||||||
|
$class = class_basename($this->auditable_type);
|
||||||
|
return preg_replace('/(?<!^)[A-Z]/', ' $0', $class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get formatted changes for display.
|
||||||
|
*/
|
||||||
|
public function getFormattedChangesAttribute(): array
|
||||||
|
{
|
||||||
|
$changes = [];
|
||||||
|
$oldValues = $this->old_values ?? [];
|
||||||
|
$newValues = $this->new_values ?? [];
|
||||||
|
|
||||||
|
$allKeys = array_unique(array_merge(array_keys($oldValues), array_keys($newValues)));
|
||||||
|
|
||||||
|
foreach ($allKeys as $key) {
|
||||||
|
$old = $oldValues[$key] ?? null;
|
||||||
|
$new = $newValues[$key] ?? null;
|
||||||
|
|
||||||
|
if ($old !== $new) {
|
||||||
|
$changes[] = [
|
||||||
|
'field' => $key,
|
||||||
|
'old' => $this->formatValue($old),
|
||||||
|
'new' => $this->formatValue($new),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $changes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a value for display.
|
||||||
|
*/
|
||||||
|
protected function formatValue($value): string
|
||||||
|
{
|
||||||
|
if (is_null($value)) {
|
||||||
|
return '(empty)';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_bool($value)) {
|
||||||
|
return $value ? 'Yes' : 'No';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_array($value)) {
|
||||||
|
return json_encode($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
$stringValue = (string) $value;
|
||||||
|
|
||||||
|
if (strlen($stringValue) > 100) {
|
||||||
|
return substr($stringValue, 0, 100) . '...';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $stringValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
40
src/app/Models/Menu.php
Normal file
40
src/app/Models/Menu.php
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
|
class Menu extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'name',
|
||||||
|
'slug',
|
||||||
|
'location',
|
||||||
|
'is_active',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'is_active' => 'boolean',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function items(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(MenuItem::class)->whereNull('parent_id')->orderBy('order');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function allItems(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(MenuItem::class)->orderBy('order');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function findBySlug(string $slug): ?self
|
||||||
|
{
|
||||||
|
return static::where('slug', $slug)->where('is_active', true)->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function findByLocation(string $location): ?self
|
||||||
|
{
|
||||||
|
return static::where('location', $location)->where('is_active', true)->first();
|
||||||
|
}
|
||||||
|
}
|
||||||
90
src/app/Models/MenuItem.php
Normal file
90
src/app/Models/MenuItem.php
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
||||||
|
class MenuItem extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'menu_id',
|
||||||
|
'parent_id',
|
||||||
|
'title',
|
||||||
|
'type',
|
||||||
|
'url',
|
||||||
|
'route',
|
||||||
|
'route_params',
|
||||||
|
'module',
|
||||||
|
'permission',
|
||||||
|
'icon',
|
||||||
|
'target',
|
||||||
|
'order',
|
||||||
|
'is_active',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'route_params' => 'array',
|
||||||
|
'is_active' => 'boolean',
|
||||||
|
'order' => 'integer',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function menu(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Menu::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function parent(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(MenuItem::class, 'parent_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function children(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(MenuItem::class, 'parent_id')->orderBy('order');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUrlAttribute(): ?string
|
||||||
|
{
|
||||||
|
return match ($this->type) {
|
||||||
|
'link' => $this->attributes['url'],
|
||||||
|
'route' => $this->route ? route($this->route, $this->route_params ?? []) : null,
|
||||||
|
'module' => $this->module ? route("{$this->module}.index") : null,
|
||||||
|
default => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isVisibleToUser(?User $user = null): bool
|
||||||
|
{
|
||||||
|
if (!$this->is_active) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = $user ?? Auth::user();
|
||||||
|
|
||||||
|
if (!$user) {
|
||||||
|
return !$this->permission && !$this->module;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($user->hasRole('admin')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->permission) {
|
||||||
|
return $user->can($this->permission);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->module) {
|
||||||
|
return $user->can("{$this->module}.view");
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getVisibleChildren(?User $user = null): \Illuminate\Support\Collection
|
||||||
|
{
|
||||||
|
return $this->children->filter(fn (MenuItem $item) => $item->isVisibleToUser($user));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,17 +3,24 @@
|
|||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||||
|
use Filament\Models\Contracts\FilamentUser;
|
||||||
|
use Filament\Panel;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
use Illuminate\Notifications\Notifiable;
|
use Illuminate\Notifications\Notifiable;
|
||||||
use Laravel\Sanctum\HasApiTokens;
|
use Laravel\Sanctum\HasApiTokens;
|
||||||
use Spatie\Permission\Traits\HasRoles;
|
use Spatie\Permission\Traits\HasRoles;
|
||||||
|
|
||||||
class User extends Authenticatable
|
class User extends Authenticatable implements FilamentUser
|
||||||
{
|
{
|
||||||
/** @use HasFactory<\Database\Factories\UserFactory> */
|
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||||
use HasFactory, Notifiable, HasRoles, HasApiTokens;
|
use HasFactory, Notifiable, HasRoles, HasApiTokens;
|
||||||
|
|
||||||
|
public function canAccessPanel(Panel $panel): bool
|
||||||
|
{
|
||||||
|
return $this->hasRole('admin');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The attributes that are mass assignable.
|
* The attributes that are mass assignable.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,94 +1,131 @@
|
|||||||
# Module System
|
# Module System
|
||||||
|
|
||||||
|
> **🤖 AI Agents**: For EXACT file templates, see [CLAUDE.md](../../../CLAUDE.md)
|
||||||
|
|
||||||
This Laravel application uses a modular architecture to organize features into self-contained modules.
|
This Laravel application uses a modular architecture to organize features into self-contained modules.
|
||||||
|
|
||||||
## Creating a New Module
|
## Creating a New Module
|
||||||
|
|
||||||
Use the artisan command to scaffold a complete module:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# Basic module (no model)
|
||||||
php artisan make:module ProductCatalog
|
php artisan make:module ProductCatalog
|
||||||
|
|
||||||
|
# With a model + Filament resource
|
||||||
|
php artisan make:module ProductCatalog --model=Product
|
||||||
|
|
||||||
|
# With API routes
|
||||||
|
php artisan make:module ProductCatalog --model=Product --api
|
||||||
|
|
||||||
|
# Without Filament admin
|
||||||
|
php artisan make:module ProductCatalog --model=Product --no-filament
|
||||||
```
|
```
|
||||||
|
|
||||||
This creates:
|
## What Gets Created
|
||||||
- **Model**: `app/Modules/ProductCatalog/Models/ProductCatalog.php`
|
|
||||||
- **Controller**: `app/Modules/ProductCatalog/Controllers/ProductCatalogController.php`
|
|
||||||
- **Routes**: `app/Modules/ProductCatalog/routes.php`
|
|
||||||
- **Views**: `app/Modules/ProductCatalog/Views/`
|
|
||||||
- **Migration**: `database/migrations/YYYY_MM_DD_HHMMSS_create_product_catalogs_table.php`
|
|
||||||
- **Filament Resource**: `app/Filament/Resources/ProductCatalogResource.php`
|
|
||||||
- **Tests**: `tests/Modules/ProductCatalog/ProductCatalogTest.php`
|
|
||||||
|
|
||||||
## Module Structure
|
|
||||||
|
|
||||||
|
### Basic module (`make:module Name`):
|
||||||
```
|
```
|
||||||
app/Modules/
|
app/Modules/Name/
|
||||||
└── ProductCatalog/
|
├── Config/name.php # Module config
|
||||||
|
├── Database/Migrations/ # Module migrations
|
||||||
|
├── Database/Seeders/
|
||||||
|
├── Filament/Resources/ # Admin resources
|
||||||
|
├── Http/Controllers/NameController.php
|
||||||
|
├── Http/Middleware/
|
||||||
|
├── Http/Requests/
|
||||||
├── Models/
|
├── Models/
|
||||||
│ └── ProductCatalog.php
|
├── Policies/
|
||||||
├── Controllers/
|
├── Services/
|
||||||
│ └── ProductCatalogController.php
|
├── Routes/web.php # Frontend routes
|
||||||
├── Views/
|
├── Resources/views/index.blade.php
|
||||||
│ ├── index.blade.php
|
├── Permissions.php # Module permissions
|
||||||
│ ├── create.blade.php
|
└── NameServiceProvider.php # Auto-registered
|
||||||
│ ├── edit.blade.php
|
|
||||||
│ └── show.blade.php
|
|
||||||
└── routes.php
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Registering Module Routes
|
### With `--model=Product`:
|
||||||
|
Adds:
|
||||||
|
- `Models/Product.php` (with auditing)
|
||||||
|
- `Database/Migrations/create_products_table.php`
|
||||||
|
- `Filament/Resources/ProductResource.php` (full CRUD)
|
||||||
|
|
||||||
After creating a module, register its routes in `routes/web.php`:
|
### With `--api`:
|
||||||
|
Adds:
|
||||||
```php
|
- `Routes/api.php` (Sanctum-protected)
|
||||||
require __DIR__.'/../app/Modules/ProductCatalog/routes.php';
|
|
||||||
```
|
|
||||||
|
|
||||||
## Module Features
|
## Module Features
|
||||||
|
|
||||||
Each module includes:
|
✅ **ServiceProvider** - Auto-registered, loads routes/views/migrations
|
||||||
|
✅ **Config** - Module-specific settings with audit configuration
|
||||||
✅ **CRUD Operations** - Full Create, Read, Update, Delete functionality
|
✅ **Permissions** - Pre-defined CRUD permissions
|
||||||
✅ **Authentication** - Routes protected by auth middleware
|
✅ **Filament Admin** - Full CRUD resource with navigation group
|
||||||
✅ **Filament Admin** - Auto-generated admin panel resource
|
✅ **Auditing** - Track all changes via ModuleAuditable trait
|
||||||
✅ **Blade Views** - Responsive Tailwind CSS templates
|
✅ **Tests** - Feature tests with permission checks
|
||||||
✅ **Tests** - Pest test suite for module functionality
|
|
||||||
✅ **Permissions** - Ready for role-based access control
|
|
||||||
|
|
||||||
## Example Usage
|
## Example Usage
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Create an Inventory module
|
# Create a Stock Management module with Product model
|
||||||
php artisan make:module Inventory
|
php artisan make:module StockManagement --model=Product --api
|
||||||
|
|
||||||
# Run migrations
|
# Run migrations
|
||||||
php artisan migrate
|
php artisan migrate
|
||||||
|
|
||||||
# Register routes in routes/web.php
|
# Seed permissions
|
||||||
require __DIR__.'/../app/Modules/Inventory/routes.php';
|
php artisan db:seed --class=RolePermissionSeeder
|
||||||
|
|
||||||
# Access at: http://localhost:8080/inventory
|
# Clear caches
|
||||||
# Admin panel: http://localhost:8080/admin/inventories
|
php artisan optimize:clear
|
||||||
```
|
```
|
||||||
|
|
||||||
## Adding Permissions
|
**Access:**
|
||||||
|
- Frontend: `http://localhost:8080/stock-management`
|
||||||
|
- Admin: `http://localhost:8080/admin` → Stock Management section
|
||||||
|
- API: `http://localhost:8080/api/stock-management`
|
||||||
|
|
||||||
To add module-specific permissions:
|
## Permissions
|
||||||
|
|
||||||
|
Each module creates these permissions in `Permissions.php`:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
// In database/seeders/RolePermissionSeeder.php
|
return [
|
||||||
$permissions = [
|
'stock_management.view' => 'View Stock Management',
|
||||||
'inventory.view',
|
'stock_management.create' => 'Create Stock Management records',
|
||||||
'inventory.create',
|
'stock_management.edit' => 'Edit Stock Management records',
|
||||||
'inventory.edit',
|
'stock_management.delete' => 'Delete Stock Management records',
|
||||||
'inventory.delete',
|
|
||||||
];
|
];
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Permissions are auto-loaded!** When you run `php artisan db:seed --class=RolePermissionSeeder`,
|
||||||
|
it scans all `app/Modules/*/Permissions.php` files and registers them automatically.
|
||||||
|
|
||||||
|
Use in Blade:
|
||||||
|
```blade
|
||||||
|
@can('stock_management.view')
|
||||||
|
<a href="{{ route('stock-management.index') }}">Stock</a>
|
||||||
|
@endcan
|
||||||
|
```
|
||||||
|
|
||||||
|
Use in Controller:
|
||||||
|
```php
|
||||||
|
$this->authorize('stock_management.view');
|
||||||
|
```
|
||||||
|
|
||||||
|
## Audit Configuration
|
||||||
|
|
||||||
|
Edit `Config/module_name.php`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
'audit' => [
|
||||||
|
'enabled' => true,
|
||||||
|
'strategy' => 'all', // 'all', 'include', 'exclude', 'none'
|
||||||
|
'include' => [], // Fields to audit (if strategy='include')
|
||||||
|
'exclude' => ['password'], // Fields to exclude
|
||||||
|
],
|
||||||
|
```
|
||||||
|
|
||||||
## Best Practices
|
## Best Practices
|
||||||
|
|
||||||
1. **Keep modules self-contained** - Each module should be independent
|
1. **Keep modules independent** - Avoid tight coupling between modules
|
||||||
2. **Use namespaces** - Follow the `App\Modules\{ModuleName}` pattern
|
2. **Use Services** - Put business logic in `Services/`, not controllers
|
||||||
3. **Test your modules** - Run `php artisan test` after creating
|
3. **Define clear permissions** - One permission per action
|
||||||
4. **Document changes** - Update module README if you modify structure
|
4. **Test your modules** - Run `php artisan test` after creating
|
||||||
5. **Use Filament** - Leverage the admin panel for quick CRUD interfaces
|
5. **Use Filament** - Leverage admin panel for quick CRUD
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
namespace App\Providers;
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use App\Models\Setting;
|
||||||
|
use Illuminate\Support\Facades\View;
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
|
||||||
class AppServiceProvider extends ServiceProvider
|
class AppServiceProvider extends ServiceProvider
|
||||||
@@ -19,6 +21,28 @@ public function register(): void
|
|||||||
*/
|
*/
|
||||||
public function boot(): void
|
public function boot(): void
|
||||||
{
|
{
|
||||||
//
|
View::composer('*', function ($view) {
|
||||||
|
try {
|
||||||
|
$view->with('siteSettings', [
|
||||||
|
'name' => Setting::get('site_name', config('app.name', 'Laravel')),
|
||||||
|
'logo' => Setting::get('site_logo'),
|
||||||
|
'primary_color' => Setting::get('primary_color', '#3b82f6'),
|
||||||
|
'secondary_color' => Setting::get('secondary_color', '#8b5cf6'),
|
||||||
|
'accent_color' => Setting::get('accent_color', '#10b981'),
|
||||||
|
'description' => Setting::get('site_description'),
|
||||||
|
'enable_registration' => Setting::get('enable_registration', false),
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$view->with('siteSettings', [
|
||||||
|
'name' => config('app.name', 'Laravel'),
|
||||||
|
'logo' => null,
|
||||||
|
'primary_color' => '#3b82f6',
|
||||||
|
'secondary_color' => '#8b5cf6',
|
||||||
|
'accent_color' => '#10b981',
|
||||||
|
'description' => null,
|
||||||
|
'enable_registration' => false,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
117
src/app/Services/MenuService.php
Normal file
117
src/app/Services/MenuService.php
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\Menu;
|
||||||
|
use App\Models\MenuItem;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
|
||||||
|
class MenuService
|
||||||
|
{
|
||||||
|
public function getMenu(string $slugOrLocation, ?User $user = null): ?array
|
||||||
|
{
|
||||||
|
$menu = Menu::findBySlug($slugOrLocation) ?? Menu::findByLocation($slugOrLocation);
|
||||||
|
|
||||||
|
if (!$menu) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->buildMenuTree($menu, $user);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function buildMenuTree(Menu $menu, ?User $user = null): array
|
||||||
|
{
|
||||||
|
$user = $user ?? Auth::user();
|
||||||
|
|
||||||
|
$items = $menu->items()
|
||||||
|
->with(['children' => fn ($q) => $q->orderBy('order')])
|
||||||
|
->where('is_active', true)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return $this->filterAndMapItems($items, $user);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function filterAndMapItems(Collection $items, ?User $user): array
|
||||||
|
{
|
||||||
|
return $items
|
||||||
|
->filter(fn (MenuItem $item) => $item->isVisibleToUser($user))
|
||||||
|
->map(fn (MenuItem $item) => $this->mapItem($item, $user))
|
||||||
|
->values()
|
||||||
|
->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function mapItem(MenuItem $item, ?User $user): array
|
||||||
|
{
|
||||||
|
$children = $item->children->count() > 0
|
||||||
|
? $this->filterAndMapItems($item->children, $user)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => $item->id,
|
||||||
|
'title' => $item->title,
|
||||||
|
'url' => $item->url,
|
||||||
|
'icon' => $item->icon,
|
||||||
|
'target' => $item->target,
|
||||||
|
'is_active' => $this->isActiveUrl($item->url),
|
||||||
|
'children' => $children,
|
||||||
|
'has_children' => count($children) > 0,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function isActiveUrl(?string $url): bool
|
||||||
|
{
|
||||||
|
if (!$url) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$currentUrl = request()->url();
|
||||||
|
$currentPath = request()->path();
|
||||||
|
|
||||||
|
if ($url === $currentUrl) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$urlPath = parse_url($url, PHP_URL_PATH) ?? '';
|
||||||
|
|
||||||
|
return $urlPath && str_starts_with('/' . ltrim($currentPath, '/'), $urlPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAvailableModules(): array
|
||||||
|
{
|
||||||
|
$modules = [];
|
||||||
|
$modulesPath = app_path('Modules');
|
||||||
|
|
||||||
|
if (!is_dir($modulesPath)) {
|
||||||
|
return $modules;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (scandir($modulesPath) as $module) {
|
||||||
|
if ($module === '.' || $module === '..' || !is_dir($modulesPath . '/' . $module)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$slug = strtolower(preg_replace('/(?<!^)[A-Z]/', '-$0', $module));
|
||||||
|
$modules[$slug] = $module;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $modules;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAvailableRoutes(): array
|
||||||
|
{
|
||||||
|
$routes = [];
|
||||||
|
|
||||||
|
foreach (app('router')->getRoutes() as $route) {
|
||||||
|
$name = $route->getName();
|
||||||
|
if ($name && !str_starts_with($name, 'filament.') && !str_starts_with($name, 'livewire.')) {
|
||||||
|
$routes[$name] = $name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ksort($routes);
|
||||||
|
return $routes;
|
||||||
|
}
|
||||||
|
}
|
||||||
248
src/app/Services/ModuleDiscoveryService.php
Normal file
248
src/app/Services/ModuleDiscoveryService.php
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\File;
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class ModuleDiscoveryService
|
||||||
|
{
|
||||||
|
public function discoverModules(): array
|
||||||
|
{
|
||||||
|
$modulesPath = app_path('Modules');
|
||||||
|
|
||||||
|
if (!File::exists($modulesPath)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$modules = [];
|
||||||
|
$directories = File::directories($modulesPath);
|
||||||
|
|
||||||
|
foreach ($directories as $directory) {
|
||||||
|
$moduleName = basename($directory);
|
||||||
|
|
||||||
|
// Skip non-module directories (like README files)
|
||||||
|
if (!File::exists("{$directory}/{$moduleName}ServiceProvider.php")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$modules[] = $this->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'])),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
492
src/app/Services/ModuleGeneratorService.php
Normal file
492
src/app/Services/ModuleGeneratorService.php
Normal file
@@ -0,0 +1,492 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use CzProject\GitPhp\Git;
|
||||||
|
use CzProject\GitPhp\GitRepository;
|
||||||
|
use Illuminate\Support\Facades\File;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class ModuleGeneratorService
|
||||||
|
{
|
||||||
|
protected string $studlyName;
|
||||||
|
protected string $kebabName;
|
||||||
|
protected string $snakeName;
|
||||||
|
protected string $modulePath;
|
||||||
|
protected array $logs = [];
|
||||||
|
protected ?GitRepository $repo = null;
|
||||||
|
|
||||||
|
public function generate(string $name, array $options = []): array
|
||||||
|
{
|
||||||
|
$this->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 = <<<PHP
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Modules\\{$this->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 = <<<PHP
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
'name' => '{$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 = <<<PHP
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
'{$this->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 = <<<PHP
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Modules\\{$this->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 = <<<PHP
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Modules\\{$this->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 = <<<PHP
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
|
Route::middleware('auth:sanctum')->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 = <<<BLADE
|
||||||
|
<x-app-layout>
|
||||||
|
<x-slot name="header">
|
||||||
|
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
|
||||||
|
{{ __('{$this->studlyName}') }}
|
||||||
|
</h2>
|
||||||
|
</x-slot>
|
||||||
|
|
||||||
|
<div class="py-12">
|
||||||
|
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||||
|
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
|
||||||
|
<div class="p-6 text-gray-900 dark:text-gray-100">
|
||||||
|
<h3 class="text-lg font-medium mb-4">{{ __('{$this->studlyName} Module') }}</h3>
|
||||||
|
<p class="text-gray-600 dark:text-gray-400">
|
||||||
|
This is your new module. Start building by adding models, controllers, and views.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="mt-6 p-4 bg-gray-50 dark:bg-gray-900 rounded-lg">
|
||||||
|
<h4 class="font-medium mb-2">Quick Start:</h4>
|
||||||
|
<ul class="list-disc list-inside text-sm space-y-1">
|
||||||
|
<li>Add models in <code>app/Modules/{$this->studlyName}/Models/</code></li>
|
||||||
|
<li>Add migrations in <code>app/Modules/{$this->studlyName}/Database/Migrations/</code></li>
|
||||||
|
<li>Add Filament resources in <code>app/Modules/{$this->studlyName}/Filament/Resources/</code></li>
|
||||||
|
<li>Extend routes in <code>app/Modules/{$this->studlyName}/Routes/web.php</code></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</x-app-layout>
|
||||||
|
BLADE;
|
||||||
|
|
||||||
|
File::put("{$this->modulePath}/Resources/views/index.blade.php", $stub);
|
||||||
|
$this->log("✓ Created Views");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function createReadme(string $description): void
|
||||||
|
{
|
||||||
|
$stub = <<<MD
|
||||||
|
# {$this->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;
|
||||||
|
}
|
||||||
|
}
|
||||||
77
src/app/Traits/ModuleAuditable.php
Normal file
77
src/app/Traits/ModuleAuditable.php
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Traits;
|
||||||
|
|
||||||
|
use OwenIt\Auditing\Auditable;
|
||||||
|
|
||||||
|
trait ModuleAuditable
|
||||||
|
{
|
||||||
|
use Auditable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the audit configuration from module config.
|
||||||
|
*/
|
||||||
|
public function getAuditConfig(): array
|
||||||
|
{
|
||||||
|
$moduleName = $this->getModuleName();
|
||||||
|
|
||||||
|
return config("{$moduleName}.audit", [
|
||||||
|
'enabled' => true,
|
||||||
|
'strategy' => 'all',
|
||||||
|
'include' => [],
|
||||||
|
'exclude' => [],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if auditing is enabled for this model.
|
||||||
|
*/
|
||||||
|
public function isAuditingEnabled(): bool
|
||||||
|
{
|
||||||
|
$config = $this->getAuditConfig();
|
||||||
|
|
||||||
|
return $config['enabled'] ?? true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get fields to include in audit (if strategy is 'include').
|
||||||
|
*/
|
||||||
|
public function getAuditInclude(): array
|
||||||
|
{
|
||||||
|
$config = $this->getAuditConfig();
|
||||||
|
|
||||||
|
if (($config['strategy'] ?? 'all') === 'include') {
|
||||||
|
return $config['include'] ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get fields to exclude from audit.
|
||||||
|
*/
|
||||||
|
public function getAuditExclude(): array
|
||||||
|
{
|
||||||
|
$config = $this->getAuditConfig();
|
||||||
|
|
||||||
|
return array_merge(
|
||||||
|
$this->auditExclude ?? [],
|
||||||
|
$config['exclude'] ?? []
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine the module name from the model's namespace.
|
||||||
|
*/
|
||||||
|
protected function getModuleName(): string
|
||||||
|
{
|
||||||
|
$class = get_class($this);
|
||||||
|
|
||||||
|
// Extract module name from namespace: App\Modules\{ModuleName}\Models\...
|
||||||
|
if (preg_match('/App\\\\Modules\\\\([^\\\\]+)\\\\/', $class, $matches)) {
|
||||||
|
return \Illuminate\Support\Str::snake($matches[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'app';
|
||||||
|
}
|
||||||
|
}
|
||||||
30
src/app/View/Components/FrontendMenu.php
Normal file
30
src/app/View/Components/FrontendMenu.php
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\View\Components;
|
||||||
|
|
||||||
|
use App\Services\MenuService;
|
||||||
|
use Illuminate\Contracts\View\View;
|
||||||
|
use Illuminate\View\Component;
|
||||||
|
|
||||||
|
class FrontendMenu extends Component
|
||||||
|
{
|
||||||
|
public array $items = [];
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
public string $menu = 'header',
|
||||||
|
public string $class = '',
|
||||||
|
) {
|
||||||
|
$menuService = app(MenuService::class);
|
||||||
|
$this->items = $menuService->getMenu($menu) ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render(): View
|
||||||
|
{
|
||||||
|
return view('components.frontend-menu');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasItems(): bool
|
||||||
|
{
|
||||||
|
return count($this->items) > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
30
src/app/View/Components/FrontendMenuResponsive.php
Normal file
30
src/app/View/Components/FrontendMenuResponsive.php
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\View\Components;
|
||||||
|
|
||||||
|
use App\Services\MenuService;
|
||||||
|
use Illuminate\Contracts\View\View;
|
||||||
|
use Illuminate\View\Component;
|
||||||
|
|
||||||
|
class FrontendMenuResponsive extends Component
|
||||||
|
{
|
||||||
|
public array $items = [];
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
public string $menu = 'header',
|
||||||
|
public string $class = '',
|
||||||
|
) {
|
||||||
|
$menuService = app(MenuService::class);
|
||||||
|
$this->items = $menuService->getMenu($menu) ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render(): View
|
||||||
|
{
|
||||||
|
return view('components.frontend-menu-responsive');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasItems(): bool
|
||||||
|
{
|
||||||
|
return count($this->items) > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,7 +12,9 @@
|
|||||||
health: '/up',
|
health: '/up',
|
||||||
)
|
)
|
||||||
->withMiddleware(function (Middleware $middleware) {
|
->withMiddleware(function (Middleware $middleware) {
|
||||||
//
|
$middleware->alias([
|
||||||
|
'registration.enabled' => \App\Http\Middleware\CheckRegistrationEnabled::class,
|
||||||
|
]);
|
||||||
})
|
})
|
||||||
->withExceptions(function (Exceptions $exceptions) {
|
->withExceptions(function (Exceptions $exceptions) {
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
"spatie/laravel-permission": "^6.24"
|
"spatie/laravel-permission": "^6.24"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
|
"czproject/git-php": "^4.6",
|
||||||
"fakerphp/faker": "^1.23",
|
"fakerphp/faker": "^1.23",
|
||||||
"laravel/breeze": "^2.3",
|
"laravel/breeze": "^2.3",
|
||||||
"laravel/pail": "^1.1",
|
"laravel/pail": "^1.1",
|
||||||
|
|||||||
66
src/composer.lock
generated
66
src/composer.lock
generated
@@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "1f84406b8f4e254677813bb043dd37c3",
|
"content-hash": "384bae7218ddf4c1e2b18a8c99946102",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "anourvalar/eloquent-serialize",
|
"name": "anourvalar/eloquent-serialize",
|
||||||
@@ -8305,6 +8305,70 @@
|
|||||||
],
|
],
|
||||||
"time": "2026-01-08T08:02:38+00:00"
|
"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",
|
"name": "fakerphp/faker",
|
||||||
"version": "v1.24.1",
|
"version": "v1.24.1",
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('menus', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('name');
|
||||||
|
$table->string('slug')->unique();
|
||||||
|
$table->string('location')->nullable()->comment('e.g., header, footer, sidebar');
|
||||||
|
$table->boolean('is_active')->default(true);
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('menus');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('menu_items', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('menu_id')->constrained()->cascadeOnDelete();
|
||||||
|
$table->foreignId('parent_id')->nullable()->constrained('menu_items')->cascadeOnDelete();
|
||||||
|
$table->string('title');
|
||||||
|
$table->string('type')->default('link')->comment('link, module, route');
|
||||||
|
$table->string('url')->nullable()->comment('For external links');
|
||||||
|
$table->string('route')->nullable()->comment('For named routes');
|
||||||
|
$table->json('route_params')->nullable()->comment('Route parameters as JSON');
|
||||||
|
$table->string('module')->nullable()->comment('Module slug for permission checking');
|
||||||
|
$table->string('permission')->nullable()->comment('Specific permission required');
|
||||||
|
$table->string('icon')->nullable()->comment('Icon class or name');
|
||||||
|
$table->string('target')->default('_self')->comment('_self, _blank');
|
||||||
|
$table->integer('order')->default(0);
|
||||||
|
$table->boolean('is_active')->default(true);
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('menu_items');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -12,8 +12,20 @@
|
|||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::create('audits', function (Blueprint $table) {
|
Schema::create('audits', function (Blueprint $table) {
|
||||||
$table->id();
|
$table->bigIncrements('id');
|
||||||
|
$table->string('user_type')->nullable();
|
||||||
|
$table->unsignedBigInteger('user_id')->nullable();
|
||||||
|
$table->string('event');
|
||||||
|
$table->morphs('auditable');
|
||||||
|
$table->text('old_values')->nullable();
|
||||||
|
$table->text('new_values')->nullable();
|
||||||
|
$table->text('url')->nullable();
|
||||||
|
$table->ipAddress('ip_address')->nullable();
|
||||||
|
$table->string('user_agent', 1023)->nullable();
|
||||||
|
$table->string('tags')->nullable();
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->index(['user_id', 'user_type']);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Database\Seeders;
|
namespace Database\Seeders;
|
||||||
|
|
||||||
@@ -13,6 +13,7 @@ public function run(): void
|
|||||||
{
|
{
|
||||||
$this->call([
|
$this->call([
|
||||||
RolePermissionSeeder::class,
|
RolePermissionSeeder::class,
|
||||||
|
MenuSeeder::class,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
33
src/database/seeders/MenuSeeder.php
Normal file
33
src/database/seeders/MenuSeeder.php
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use App\Models\Menu;
|
||||||
|
use App\Models\MenuItem;
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
|
||||||
|
class MenuSeeder extends Seeder
|
||||||
|
{
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
$headerMenu = Menu::firstOrCreate(
|
||||||
|
['slug' => 'header'],
|
||||||
|
[
|
||||||
|
'name' => 'Header Navigation',
|
||||||
|
'location' => 'header',
|
||||||
|
'is_active' => true,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
MenuItem::firstOrCreate(
|
||||||
|
['menu_id' => $headerMenu->id, 'title' => 'Dashboard'],
|
||||||
|
[
|
||||||
|
'type' => 'route',
|
||||||
|
'route' => 'dashboard',
|
||||||
|
'icon' => 'heroicon-o-home',
|
||||||
|
'order' => 1,
|
||||||
|
'is_active' => true,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
namespace Database\Seeders;
|
namespace Database\Seeders;
|
||||||
|
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
|
use Illuminate\Support\Facades\File;
|
||||||
use Spatie\Permission\Models\Role;
|
use Spatie\Permission\Models\Role;
|
||||||
use Spatie\Permission\Models\Permission;
|
use Spatie\Permission\Models\Permission;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
@@ -13,30 +14,77 @@ public function run(): void
|
|||||||
{
|
{
|
||||||
app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions();
|
app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions();
|
||||||
|
|
||||||
$permissions = [
|
// Core permissions
|
||||||
|
$corePermissions = [
|
||||||
'users.view',
|
'users.view',
|
||||||
'users.create',
|
'users.create',
|
||||||
'users.edit',
|
'users.edit',
|
||||||
'users.delete',
|
'users.delete',
|
||||||
'settings.manage',
|
'settings.manage',
|
||||||
|
'audit.view',
|
||||||
|
'menus.view',
|
||||||
|
'menus.create',
|
||||||
|
'menus.edit',
|
||||||
|
'menus.delete',
|
||||||
];
|
];
|
||||||
|
|
||||||
foreach ($permissions as $permission) {
|
foreach ($corePermissions as $permission) {
|
||||||
Permission::create(['name' => $permission]);
|
Permission::firstOrCreate(['name' => $permission]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$adminRole = Role::create(['name' => 'admin']);
|
// Auto-load module permissions from app/Modules/*/Permissions.php
|
||||||
$adminRole->givePermissionTo(Permission::all());
|
$this->loadModulePermissions();
|
||||||
|
|
||||||
$editorRole = Role::create(['name' => 'editor']);
|
// Create roles
|
||||||
$editorRole->givePermissionTo(['users.view', 'users.edit']);
|
$adminRole = Role::firstOrCreate(['name' => 'admin']);
|
||||||
|
$adminRole->syncPermissions(Permission::all());
|
||||||
|
|
||||||
$viewerRole = Role::create(['name' => 'viewer']);
|
$editorRole = Role::firstOrCreate(['name' => 'editor']);
|
||||||
$viewerRole->givePermissionTo(['users.view']);
|
$editorRole->syncPermissions(['users.view', 'users.edit']);
|
||||||
|
|
||||||
$admin = User::where('email', 'admin@example.com')->first();
|
$viewerRole = Role::firstOrCreate(['name' => 'viewer']);
|
||||||
if ($admin) {
|
$viewerRole->syncPermissions(['users.view']);
|
||||||
|
|
||||||
|
// Create admin user if not exists
|
||||||
|
$admin = User::firstOrCreate(
|
||||||
|
['email' => 'admin@example.com'],
|
||||||
|
[
|
||||||
|
'name' => 'Admin',
|
||||||
|
'password' => bcrypt('password'),
|
||||||
|
'email_verified_at' => now(),
|
||||||
|
]
|
||||||
|
);
|
||||||
$admin->assignRole('admin');
|
$admin->assignRole('admin');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scan all module Permissions.php files and register permissions.
|
||||||
|
*/
|
||||||
|
protected function loadModulePermissions(): void
|
||||||
|
{
|
||||||
|
$modulesPath = app_path('Modules');
|
||||||
|
|
||||||
|
if (!File::isDirectory($modulesPath)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$modules = File::directories($modulesPath);
|
||||||
|
|
||||||
|
foreach ($modules as $modulePath) {
|
||||||
|
$permissionsFile = $modulePath . '/Permissions.php';
|
||||||
|
|
||||||
|
if (File::exists($permissionsFile)) {
|
||||||
|
$permissions = require $permissionsFile;
|
||||||
|
|
||||||
|
if (is_array($permissions)) {
|
||||||
|
foreach ($permissions as $permissionName => $description) {
|
||||||
|
// Handle both formats:
|
||||||
|
// ['permission.name' => 'Description'] or ['permission.name']
|
||||||
|
$name = is_string($permissionName) ? $permissionName : $description;
|
||||||
|
Permission::firstOrCreate(['name' => $name]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
2852
src/package-lock.json
generated
Normal file
2852
src/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,7 @@
|
|||||||
|
@if(isset($siteSettings['logo']) && $siteSettings['logo'])
|
||||||
|
<img src="{{ asset('storage/' . $siteSettings['logo']) }}" alt="{{ $siteSettings['name'] ?? 'Logo' }}" {{ $attributes->merge(['class' => 'object-contain']) }}>
|
||||||
|
@else
|
||||||
<svg viewBox="0 0 316 316" xmlns="http://www.w3.org/2000/svg" {{ $attributes }}>
|
<svg viewBox="0 0 316 316" xmlns="http://www.w3.org/2000/svg" {{ $attributes }}>
|
||||||
<path d="M305.8 81.125C305.77 80.995 305.69 80.885 305.65 80.755C305.56 80.525 305.49 80.285 305.37 80.075C305.29 79.935 305.17 79.815 305.07 79.685C304.94 79.515 304.83 79.325 304.68 79.175C304.55 79.045 304.39 78.955 304.25 78.845C304.09 78.715 303.95 78.575 303.77 78.475L251.32 48.275C249.97 47.495 248.31 47.495 246.96 48.275L194.51 78.475C194.33 78.575 194.19 78.725 194.03 78.845C193.89 78.955 193.73 79.045 193.6 79.175C193.45 79.325 193.34 79.515 193.21 79.685C193.11 79.815 192.99 79.935 192.91 80.075C192.79 80.285 192.71 80.525 192.63 80.755C192.58 80.875 192.51 80.995 192.48 81.125C192.38 81.495 192.33 81.875 192.33 82.265V139.625L148.62 164.795V52.575C148.62 52.185 148.57 51.805 148.47 51.435C148.44 51.305 148.36 51.195 148.32 51.065C148.23 50.835 148.16 50.595 148.04 50.385C147.96 50.245 147.84 50.125 147.74 49.995C147.61 49.825 147.5 49.635 147.35 49.485C147.22 49.355 147.06 49.265 146.92 49.155C146.76 49.025 146.62 48.885 146.44 48.785L93.99 18.585C92.64 17.805 90.98 17.805 89.63 18.585L37.18 48.785C37 48.885 36.86 49.035 36.7 49.155C36.56 49.265 36.4 49.355 36.27 49.485C36.12 49.635 36.01 49.825 35.88 49.995C35.78 50.125 35.66 50.245 35.58 50.385C35.46 50.595 35.38 50.835 35.3 51.065C35.25 51.185 35.18 51.305 35.15 51.435C35.05 51.805 35 52.185 35 52.575V232.235C35 233.795 35.84 235.245 37.19 236.025L142.1 296.425C142.33 296.555 142.58 296.635 142.82 296.725C142.93 296.765 143.04 296.835 143.16 296.865C143.53 296.965 143.9 297.015 144.28 297.015C144.66 297.015 145.03 296.965 145.4 296.865C145.5 296.835 145.59 296.775 145.69 296.745C145.95 296.655 146.21 296.565 146.45 296.435L251.36 236.035C252.72 235.255 253.55 233.815 253.55 232.245V174.885L303.81 145.945C305.17 145.165 306 143.725 306 142.155V82.265C305.95 81.875 305.89 81.495 305.8 81.125ZM144.2 227.205L100.57 202.515L146.39 176.135L196.66 147.195L240.33 172.335L208.29 190.625L144.2 227.205ZM244.75 114.995V164.795L226.39 154.225L201.03 139.625V89.825L219.39 100.395L244.75 114.995ZM249.12 57.105L292.81 82.265L249.12 107.425L205.43 82.265L249.12 57.105ZM114.49 184.425L96.13 194.995V85.305L121.49 70.705L139.85 60.135V169.815L114.49 184.425ZM91.76 27.425L135.45 52.585L91.76 77.745L48.07 52.585L91.76 27.425ZM43.67 60.135L62.03 70.705L87.39 85.305V202.545V202.555V202.565C87.39 202.735 87.44 202.895 87.46 203.055C87.49 203.265 87.49 203.485 87.55 203.695V203.705C87.6 203.875 87.69 204.035 87.76 204.195C87.84 204.375 87.89 204.575 87.99 204.745C87.99 204.745 87.99 204.755 88 204.755C88.09 204.905 88.22 205.035 88.33 205.175C88.45 205.335 88.55 205.495 88.69 205.635L88.7 205.645C88.82 205.765 88.98 205.855 89.12 205.965C89.28 206.085 89.42 206.225 89.59 206.325C89.6 206.325 89.6 206.325 89.61 206.335C89.62 206.335 89.62 206.345 89.63 206.345L139.87 234.775V285.065L43.67 229.705V60.135ZM244.75 229.705L148.58 285.075V234.775L219.8 194.115L244.75 179.875V229.705ZM297.2 139.625L253.49 164.795V114.995L278.85 100.395L297.21 89.825V139.625H297.2Z"/>
|
<path d="M305.8 81.125C305.77 80.995 305.69 80.885 305.65 80.755C305.56 80.525 305.49 80.285 305.37 80.075C305.29 79.935 305.17 79.815 305.07 79.685C304.94 79.515 304.83 79.325 304.68 79.175C304.55 79.045 304.39 78.955 304.25 78.845C304.09 78.715 303.95 78.575 303.77 78.475L251.32 48.275C249.97 47.495 248.31 47.495 246.96 48.275L194.51 78.475C194.33 78.575 194.19 78.725 194.03 78.845C193.89 78.955 193.73 79.045 193.6 79.175C193.45 79.325 193.34 79.515 193.21 79.685C193.11 79.815 192.99 79.935 192.91 80.075C192.79 80.285 192.71 80.525 192.63 80.755C192.58 80.875 192.51 80.995 192.48 81.125C192.38 81.495 192.33 81.875 192.33 82.265V139.625L148.62 164.795V52.575C148.62 52.185 148.57 51.805 148.47 51.435C148.44 51.305 148.36 51.195 148.32 51.065C148.23 50.835 148.16 50.595 148.04 50.385C147.96 50.245 147.84 50.125 147.74 49.995C147.61 49.825 147.5 49.635 147.35 49.485C147.22 49.355 147.06 49.265 146.92 49.155C146.76 49.025 146.62 48.885 146.44 48.785L93.99 18.585C92.64 17.805 90.98 17.805 89.63 18.585L37.18 48.785C37 48.885 36.86 49.035 36.7 49.155C36.56 49.265 36.4 49.355 36.27 49.485C36.12 49.635 36.01 49.825 35.88 49.995C35.78 50.125 35.66 50.245 35.58 50.385C35.46 50.595 35.38 50.835 35.3 51.065C35.25 51.185 35.18 51.305 35.15 51.435C35.05 51.805 35 52.185 35 52.575V232.235C35 233.795 35.84 235.245 37.19 236.025L142.1 296.425C142.33 296.555 142.58 296.635 142.82 296.725C142.93 296.765 143.04 296.835 143.16 296.865C143.53 296.965 143.9 297.015 144.28 297.015C144.66 297.015 145.03 296.965 145.4 296.865C145.5 296.835 145.59 296.775 145.69 296.745C145.95 296.655 146.21 296.565 146.45 296.435L251.36 236.035C252.72 235.255 253.55 233.815 253.55 232.245V174.885L303.81 145.945C305.17 145.165 306 143.725 306 142.155V82.265C305.95 81.875 305.89 81.495 305.8 81.125ZM144.2 227.205L100.57 202.515L146.39 176.135L196.66 147.195L240.33 172.335L208.29 190.625L144.2 227.205ZM244.75 114.995V164.795L226.39 154.225L201.03 139.625V89.825L219.39 100.395L244.75 114.995ZM249.12 57.105L292.81 82.265L249.12 107.425L205.43 82.265L249.12 57.105ZM114.49 184.425L96.13 194.995V85.305L121.49 70.705L139.85 60.135V169.815L114.49 184.425ZM91.76 27.425L135.45 52.585L91.76 77.745L48.07 52.585L91.76 27.425ZM43.67 60.135L62.03 70.705L87.39 85.305V202.545V202.555V202.565C87.39 202.735 87.44 202.895 87.46 203.055C87.49 203.265 87.49 203.485 87.55 203.695V203.705C87.6 203.875 87.69 204.035 87.76 204.195C87.84 204.375 87.89 204.575 87.99 204.745C87.99 204.745 87.99 204.755 88 204.755C88.09 204.905 88.22 205.035 88.33 205.175C88.45 205.335 88.55 205.495 88.69 205.635L88.7 205.645C88.82 205.765 88.98 205.855 89.12 205.965C89.28 206.085 89.42 206.225 89.59 206.325C89.6 206.325 89.6 206.325 89.61 206.335C89.62 206.335 89.62 206.345 89.63 206.345L139.87 234.775V285.065L43.67 229.705V60.135ZM244.75 229.705L148.58 285.075V234.775L219.8 194.115L244.75 179.875V229.705ZM297.2 139.625L253.49 164.795V114.995L278.85 100.395L297.21 89.825V139.625H297.2Z"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
@endif
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.2 KiB |
@@ -0,0 +1,47 @@
|
|||||||
|
@if($hasItems())
|
||||||
|
<div {{ $attributes->merge(['class' => $class]) }}>
|
||||||
|
@foreach($items as $item)
|
||||||
|
@if($item['has_children'])
|
||||||
|
{{-- Collapsible menu item with children --}}
|
||||||
|
<div x-data="{ open: false }">
|
||||||
|
<button type="button"
|
||||||
|
@click="open = !open"
|
||||||
|
class="w-full flex items-center justify-between px-4 py-2 text-base font-medium text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 {{ $item['is_active'] ? 'text-gray-800 dark:text-gray-200 bg-gray-50 dark:bg-gray-700' : '' }}">
|
||||||
|
<span class="flex items-center">
|
||||||
|
@if($item['icon'])
|
||||||
|
<x-dynamic-component :component="$item['icon']" class="w-5 h-5 mr-2" />
|
||||||
|
@endif
|
||||||
|
{{ $item['title'] }}
|
||||||
|
</span>
|
||||||
|
<svg class="w-4 h-4 transition-transform" :class="{ 'rotate-180': open }" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div x-show="open" x-collapse class="pl-4">
|
||||||
|
@foreach($item['children'] as $child)
|
||||||
|
<a href="{{ $child['url'] }}"
|
||||||
|
target="{{ $child['target'] }}"
|
||||||
|
class="block px-4 py-2 text-base font-medium text-gray-500 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 {{ $child['is_active'] ? 'text-gray-800 dark:text-gray-200 bg-gray-50 dark:bg-gray-700' : '' }}">
|
||||||
|
@if($child['icon'])
|
||||||
|
<x-dynamic-component :component="$child['icon']" class="inline w-4 h-4 mr-1" />
|
||||||
|
@endif
|
||||||
|
{{ $child['title'] }}
|
||||||
|
</a>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
{{-- Regular menu item --}}
|
||||||
|
<a href="{{ $item['url'] }}"
|
||||||
|
target="{{ $item['target'] }}"
|
||||||
|
class="block px-4 py-2 text-base font-medium text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 {{ $item['is_active'] ? 'text-gray-800 dark:text-gray-200 bg-gray-50 dark:bg-gray-700 border-l-4 border-indigo-500' : '' }}">
|
||||||
|
@if($item['icon'])
|
||||||
|
<x-dynamic-component :component="$item['icon']" class="inline w-5 h-5 mr-2" />
|
||||||
|
@endif
|
||||||
|
{{ $item['title'] }}
|
||||||
|
</a>
|
||||||
|
@endif
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
60
src/resources/views/components/frontend-menu.blade.php
Normal file
60
src/resources/views/components/frontend-menu.blade.php
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
@if($hasItems())
|
||||||
|
<nav {{ $attributes->merge(['class' => $class]) }}>
|
||||||
|
<ul class="flex items-center space-x-4">
|
||||||
|
@foreach($items as $item)
|
||||||
|
<li class="relative group">
|
||||||
|
@if($item['has_children'])
|
||||||
|
{{-- Dropdown menu item --}}
|
||||||
|
<button type="button"
|
||||||
|
class="inline-flex items-center px-3 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 focus:outline-none transition duration-150 ease-in-out {{ $item['is_active'] ? 'text-gray-900 dark:text-gray-100' : '' }}"
|
||||||
|
x-data="{ open: false }"
|
||||||
|
@click="open = !open"
|
||||||
|
@click.away="open = false">
|
||||||
|
@if($item['icon'])
|
||||||
|
<x-dynamic-component :component="$item['icon']" class="w-4 h-4 mr-1" />
|
||||||
|
@endif
|
||||||
|
{{ $item['title'] }}
|
||||||
|
<svg class="ml-1 w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{{-- Dropdown content --}}
|
||||||
|
<div x-show="open"
|
||||||
|
x-transition:enter="transition ease-out duration-100"
|
||||||
|
x-transition:enter-start="transform opacity-0 scale-95"
|
||||||
|
x-transition:enter-end="transform opacity-100 scale-100"
|
||||||
|
x-transition:leave="transition ease-in duration-75"
|
||||||
|
x-transition:leave-start="transform opacity-100 scale-100"
|
||||||
|
x-transition:leave-end="transform opacity-0 scale-95"
|
||||||
|
class="absolute left-0 mt-2 w-48 rounded-md shadow-lg bg-white dark:bg-gray-800 ring-1 ring-black ring-opacity-5 z-50"
|
||||||
|
style="display: none;">
|
||||||
|
<div class="py-1">
|
||||||
|
@foreach($item['children'] as $child)
|
||||||
|
<a href="{{ $child['url'] }}"
|
||||||
|
target="{{ $child['target'] }}"
|
||||||
|
class="block px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 {{ $child['is_active'] ? 'bg-gray-100 dark:bg-gray-700' : '' }}">
|
||||||
|
@if($child['icon'])
|
||||||
|
<x-dynamic-component :component="$child['icon']" class="inline w-4 h-4 mr-1" />
|
||||||
|
@endif
|
||||||
|
{{ $child['title'] }}
|
||||||
|
</a>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
{{-- Regular menu item --}}
|
||||||
|
<a href="{{ $item['url'] }}"
|
||||||
|
target="{{ $item['target'] }}"
|
||||||
|
class="inline-flex items-center px-3 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 transition duration-150 ease-in-out {{ $item['is_active'] ? 'text-gray-900 dark:text-gray-100 border-b-2 border-indigo-500' : '' }}">
|
||||||
|
@if($item['icon'])
|
||||||
|
<x-dynamic-component :component="$item['icon']" class="w-4 h-4 mr-1" />
|
||||||
|
@endif
|
||||||
|
{{ $item['title'] }}
|
||||||
|
</a>
|
||||||
|
@endif
|
||||||
|
</li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
@endif
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
<div class="space-y-4">
|
||||||
|
@php
|
||||||
|
$oldValues = $getRecord()->old_values ?? [];
|
||||||
|
$newValues = $getRecord()->new_values ?? [];
|
||||||
|
$allKeys = array_unique(array_merge(array_keys($oldValues), array_keys($newValues)));
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
@if(count($allKeys) === 0)
|
||||||
|
<p class="text-sm text-gray-500 dark:text-gray-400 italic">No changes recorded.</p>
|
||||||
|
@else
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="w-full text-sm border-collapse">
|
||||||
|
<thead>
|
||||||
|
<tr class="bg-gray-50 dark:bg-gray-800">
|
||||||
|
<th class="px-4 py-2 text-left font-medium text-gray-600 dark:text-gray-300 border dark:border-gray-700">Field</th>
|
||||||
|
<th class="px-4 py-2 text-left font-medium text-gray-600 dark:text-gray-300 border dark:border-gray-700">Old Value</th>
|
||||||
|
<th class="px-4 py-2 text-left font-medium text-gray-600 dark:text-gray-300 border dark:border-gray-700">New Value</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach($allKeys as $key)
|
||||||
|
@php
|
||||||
|
$oldValue = $oldValues[$key] ?? null;
|
||||||
|
$newValue = $newValues[$key] ?? null;
|
||||||
|
$hasChanged = $oldValue !== $newValue;
|
||||||
|
@endphp
|
||||||
|
<tr class="{{ $hasChanged ? 'bg-yellow-50 dark:bg-yellow-900/20' : '' }}">
|
||||||
|
<td class="px-4 py-2 font-medium text-gray-700 dark:text-gray-200 border dark:border-gray-700">
|
||||||
|
{{ \Illuminate\Support\Str::headline($key) }}
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-2 text-gray-600 dark:text-gray-300 border dark:border-gray-700">
|
||||||
|
@if(is_null($oldValue))
|
||||||
|
<span class="text-gray-400 italic">(empty)</span>
|
||||||
|
@elseif(is_array($oldValue))
|
||||||
|
<code class="text-xs bg-gray-100 dark:bg-gray-700 px-1 py-0.5 rounded">{{ json_encode($oldValue) }}</code>
|
||||||
|
@elseif(is_bool($oldValue))
|
||||||
|
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium {{ $oldValue ? 'bg-green-100 text-green-800 dark:bg-green-800 dark:text-green-100' : 'bg-red-100 text-red-800 dark:bg-red-800 dark:text-red-100' }}">
|
||||||
|
{{ $oldValue ? 'Yes' : 'No' }}
|
||||||
|
</span>
|
||||||
|
@else
|
||||||
|
{{ \Illuminate\Support\Str::limit((string) $oldValue, 100) }}
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-2 text-gray-600 dark:text-gray-300 border dark:border-gray-700">
|
||||||
|
@if(is_null($newValue))
|
||||||
|
<span class="text-gray-400 italic">(empty)</span>
|
||||||
|
@elseif(is_array($newValue))
|
||||||
|
<code class="text-xs bg-gray-100 dark:bg-gray-700 px-1 py-0.5 rounded">{{ json_encode($newValue) }}</code>
|
||||||
|
@elseif(is_bool($newValue))
|
||||||
|
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium {{ $newValue ? 'bg-green-100 text-green-800 dark:bg-green-800 dark:text-green-100' : 'bg-red-100 text-red-800 dark:bg-red-800 dark:text-red-100' }}">
|
||||||
|
{{ $newValue ? 'Yes' : 'No' }}
|
||||||
|
</span>
|
||||||
|
@else
|
||||||
|
{{ \Illuminate\Support\Str::limit((string) $newValue, 100) }}
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
315
src/resources/views/filament/pages/module-generator.blade.php
Normal file
315
src/resources/views/filament/pages/module-generator.blade.php
Normal file
@@ -0,0 +1,315 @@
|
|||||||
|
<x-filament-panels::page>
|
||||||
|
<div class="space-y-6">
|
||||||
|
{{-- Warning Banner --}}
|
||||||
|
<div class="p-4 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg">
|
||||||
|
<div class="flex items-start gap-3">
|
||||||
|
<x-heroicon-o-exclamation-triangle class="w-5 h-5 text-amber-600 dark:text-amber-400 flex-shrink-0 mt-0.5" />
|
||||||
|
<div class="text-sm text-amber-800 dark:text-amber-200">
|
||||||
|
<strong>Development Only:</strong> This tool is only available in the local environment.
|
||||||
|
It creates module skeletons and optionally manages Git branches.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Registered Modules Section --}}
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700">
|
||||||
|
<div class="p-4 border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
|
||||||
|
<x-heroicon-o-cube-transparent class="w-5 h-5" />
|
||||||
|
Registered Modules
|
||||||
|
</h3>
|
||||||
|
<div class="flex gap-4 text-sm">
|
||||||
|
<span class="px-2 py-1 bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 rounded">
|
||||||
|
{{ $summary['total'] ?? 0 }} Total
|
||||||
|
</span>
|
||||||
|
<span class="px-2 py-1 bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300 rounded">
|
||||||
|
{{ $summary['with_models'] ?? 0 }} with Models
|
||||||
|
</span>
|
||||||
|
<span class="px-2 py-1 bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 rounded">
|
||||||
|
{{ $summary['with_filament'] ?? 0 }} with Filament
|
||||||
|
</span>
|
||||||
|
<span class="px-2 py-1 bg-orange-100 dark:bg-orange-900/30 text-orange-700 dark:text-orange-300 rounded">
|
||||||
|
{{ $summary['with_api'] ?? 0 }} with API
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if(empty($modules))
|
||||||
|
<div class="p-8 text-center text-gray-500 dark:text-gray-400">
|
||||||
|
<x-heroicon-o-inbox class="w-12 h-12 mx-auto mb-3 opacity-50" />
|
||||||
|
<p>No modules registered yet. Create your first module below!</p>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
|
@foreach($modules as $module)
|
||||||
|
<div class="group">
|
||||||
|
{{-- Module Header --}}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
wire:click="toggleModule('{{ $module['name'] }}')"
|
||||||
|
class="w-full p-4 flex items-center justify-between hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<x-heroicon-o-cube class="w-5 h-5 text-gray-400" />
|
||||||
|
<div class="text-left">
|
||||||
|
<div class="font-medium text-gray-900 dark:text-white">{{ $module['name'] }}</div>
|
||||||
|
<div class="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
/{{ $module['slug'] }}
|
||||||
|
@if($module['has_api'])
|
||||||
|
<span class="ml-2 text-xs px-1.5 py-0.5 bg-orange-100 dark:bg-orange-900/30 text-orange-600 dark:text-orange-400 rounded">API</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div class="flex gap-2 text-xs">
|
||||||
|
@if(count($module['models']) > 0)
|
||||||
|
<span class="px-2 py-1 bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300 rounded">
|
||||||
|
{{ count($module['models']) }} {{ Str::plural('Model', count($module['models'])) }}
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
@if(count($module['views']) > 0)
|
||||||
|
<span class="px-2 py-1 bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 rounded">
|
||||||
|
{{ count($module['views']) }} {{ Str::plural('View', count($module['views'])) }}
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
@if(count($module['filament_resources']) > 0)
|
||||||
|
<span class="px-2 py-1 bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 rounded">
|
||||||
|
{{ count($module['filament_resources']) }} {{ Str::plural('Resource', count($module['filament_resources'])) }}
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
<x-heroicon-o-chevron-down class="w-5 h-5 text-gray-400 transition-transform {{ $expandedModule === $module['name'] ? 'rotate-180' : '' }}" />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{{-- Module Details (Expandable) --}}
|
||||||
|
@if($expandedModule === $module['name'])
|
||||||
|
<div class="px-4 pb-4 bg-gray-50 dark:bg-gray-900/50">
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 pt-4">
|
||||||
|
{{-- Models --}}
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg p-3 border border-gray-200 dark:border-gray-700">
|
||||||
|
<h4 class="font-medium text-sm text-gray-700 dark:text-gray-300 mb-2 flex items-center gap-2">
|
||||||
|
<x-heroicon-o-circle-stack class="w-4 h-4" />
|
||||||
|
Models
|
||||||
|
</h4>
|
||||||
|
@if(count($module['models']) > 0)
|
||||||
|
<ul class="text-sm space-y-1">
|
||||||
|
@foreach($module['models'] as $model)
|
||||||
|
<li class="text-gray-600 dark:text-gray-400 font-mono text-xs">{{ $model['name'] }}</li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
@else
|
||||||
|
<p class="text-xs text-gray-400 italic">No models</p>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Views --}}
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg p-3 border border-gray-200 dark:border-gray-700">
|
||||||
|
<h4 class="font-medium text-sm text-gray-700 dark:text-gray-300 mb-2 flex items-center gap-2">
|
||||||
|
<x-heroicon-o-eye class="w-4 h-4" />
|
||||||
|
Views
|
||||||
|
</h4>
|
||||||
|
@if(count($module['views']) > 0)
|
||||||
|
<ul class="text-sm space-y-1">
|
||||||
|
@foreach($module['views'] as $view)
|
||||||
|
<li class="text-gray-600 dark:text-gray-400 font-mono text-xs">{{ $module['slug'] }}::{{ $view['name'] }}</li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
@else
|
||||||
|
<p class="text-xs text-gray-400 italic">No views</p>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Filament Resources --}}
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg p-3 border border-gray-200 dark:border-gray-700">
|
||||||
|
<h4 class="font-medium text-sm text-gray-700 dark:text-gray-300 mb-2 flex items-center gap-2">
|
||||||
|
<x-heroicon-o-rectangle-group class="w-4 h-4" />
|
||||||
|
Filament Resources
|
||||||
|
</h4>
|
||||||
|
@if(count($module['filament_resources']) > 0)
|
||||||
|
<ul class="text-sm space-y-1">
|
||||||
|
@foreach($module['filament_resources'] as $resource)
|
||||||
|
<li class="text-gray-600 dark:text-gray-400 font-mono text-xs">{{ $resource['name'] }}Resource</li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
@else
|
||||||
|
<p class="text-xs text-gray-400 italic">No Filament resources</p>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Routes --}}
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg p-3 border border-gray-200 dark:border-gray-700">
|
||||||
|
<h4 class="font-medium text-sm text-gray-700 dark:text-gray-300 mb-2 flex items-center gap-2">
|
||||||
|
<x-heroicon-o-arrow-path class="w-4 h-4" />
|
||||||
|
Routes
|
||||||
|
</h4>
|
||||||
|
@if(!empty($module['routes']['web']))
|
||||||
|
<ul class="text-sm space-y-1">
|
||||||
|
@foreach($module['routes']['web'] as $route)
|
||||||
|
<li class="text-gray-600 dark:text-gray-400 font-mono text-xs">
|
||||||
|
<span class="text-blue-600 dark:text-blue-400">{{ $route['method'] }}</span>
|
||||||
|
{{ $route['uri'] }}
|
||||||
|
</li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
@else
|
||||||
|
<p class="text-xs text-gray-400 italic">No web routes parsed</p>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Migrations --}}
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg p-3 border border-gray-200 dark:border-gray-700">
|
||||||
|
<h4 class="font-medium text-sm text-gray-700 dark:text-gray-300 mb-2 flex items-center gap-2">
|
||||||
|
<x-heroicon-o-table-cells class="w-4 h-4" />
|
||||||
|
Migrations
|
||||||
|
</h4>
|
||||||
|
@if(count($module['migrations']) > 0)
|
||||||
|
<ul class="text-sm space-y-1">
|
||||||
|
@foreach($module['migrations'] as $migration)
|
||||||
|
<li class="text-gray-600 dark:text-gray-400 font-mono text-xs truncate" title="{{ $migration['name'] }}">{{ $migration['name'] }}</li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
@else
|
||||||
|
<p class="text-xs text-gray-400 italic">No migrations</p>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Permissions --}}
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg p-3 border border-gray-200 dark:border-gray-700">
|
||||||
|
<h4 class="font-medium text-sm text-gray-700 dark:text-gray-300 mb-2 flex items-center gap-2">
|
||||||
|
<x-heroicon-o-key class="w-4 h-4" />
|
||||||
|
Permissions
|
||||||
|
</h4>
|
||||||
|
@if(count($module['permissions']) > 0)
|
||||||
|
<ul class="text-sm space-y-1">
|
||||||
|
@foreach($module['permissions'] as $key => $label)
|
||||||
|
<li class="text-gray-600 dark:text-gray-400 font-mono text-xs">{{ $key }}</li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
@else
|
||||||
|
<p class="text-xs text-gray-400 italic">No permissions</p>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Module Path --}}
|
||||||
|
<div class="mt-4 p-2 bg-gray-100 dark:bg-gray-800 rounded text-xs font-mono text-gray-500 dark:text-gray-400">
|
||||||
|
Path: {{ $module['path'] }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Create Module Form --}}
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 p-6">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||||
|
<x-heroicon-o-plus-circle class="w-5 h-5" />
|
||||||
|
Create New Module
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<form wire:submit="generate">
|
||||||
|
{{ $this->form }}
|
||||||
|
|
||||||
|
<div class="mt-6 flex gap-3">
|
||||||
|
<x-filament::button type="submit" size="lg">
|
||||||
|
<x-heroicon-o-sparkles class="w-5 h-5 mr-2" />
|
||||||
|
Generate Module
|
||||||
|
</x-filament::button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Result Panel --}}
|
||||||
|
@if($result)
|
||||||
|
<div class="mt-8">
|
||||||
|
@if($result['success'])
|
||||||
|
<div class="p-6 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg">
|
||||||
|
<div class="flex items-center gap-3 mb-4">
|
||||||
|
<x-heroicon-o-check-circle class="w-6 h-6 text-green-600 dark:text-green-400" />
|
||||||
|
<h3 class="text-lg font-semibold text-green-800 dark:text-green-200">
|
||||||
|
Module Created Successfully!
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-4 text-sm">
|
||||||
|
<div>
|
||||||
|
<span class="font-medium text-green-700 dark:text-green-300">Module:</span>
|
||||||
|
<code class="ml-2 px-2 py-1 bg-green-100 dark:bg-green-800 rounded">{{ $result['module_name'] }}</code>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span class="font-medium text-green-700 dark:text-green-300">Path:</span>
|
||||||
|
<code class="ml-2 px-2 py-1 bg-green-100 dark:bg-green-800 rounded text-xs">{{ $result['module_path'] }}</code>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if($result['branch'])
|
||||||
|
<div>
|
||||||
|
<span class="font-medium text-green-700 dark:text-green-300">Git Branch:</span>
|
||||||
|
<code class="ml-2 px-2 py-1 bg-green-100 dark:bg-green-800 rounded">{{ $result['branch'] }}</code>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
{{-- Logs --}}
|
||||||
|
<div class="mt-4">
|
||||||
|
<span class="font-medium text-green-700 dark:text-green-300">Generation Log:</span>
|
||||||
|
<div class="mt-2 p-3 bg-white dark:bg-gray-800 rounded border border-green-200 dark:border-green-700 font-mono text-xs">
|
||||||
|
@foreach($result['logs'] as $log)
|
||||||
|
<div class="py-0.5">{{ $log }}</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Next Steps --}}
|
||||||
|
<div class="mt-4">
|
||||||
|
<span class="font-medium text-green-700 dark:text-green-300">Next Steps:</span>
|
||||||
|
<ol class="mt-2 list-decimal list-inside space-y-1 text-green-800 dark:text-green-200">
|
||||||
|
@foreach($result['next_steps'] as $step)
|
||||||
|
<li>{!! preg_replace('/`([^`]+)`/', '<code class="px-1 py-0.5 bg-green-100 dark:bg-green-800 rounded text-xs">$1</code>', e($step)) !!}</li>
|
||||||
|
@endforeach
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-6">
|
||||||
|
<x-filament::button color="gray" wire:click="clearResult">
|
||||||
|
Create Another Module
|
||||||
|
</x-filament::button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="p-6 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
|
||||||
|
<div class="flex items-center gap-3 mb-4">
|
||||||
|
<x-heroicon-o-x-circle class="w-6 h-6 text-red-600 dark:text-red-400" />
|
||||||
|
<h3 class="text-lg font-semibold text-red-800 dark:text-red-200">
|
||||||
|
Module Creation Failed
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-red-700 dark:text-red-300">{{ $result['message'] }}</p>
|
||||||
|
|
||||||
|
@if(!empty($result['logs']))
|
||||||
|
<div class="mt-4 p-3 bg-white dark:bg-gray-800 rounded border border-red-200 dark:border-red-700 font-mono text-xs">
|
||||||
|
@foreach($result['logs'] as $log)
|
||||||
|
<div class="py-0.5">{{ $log }}</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="mt-6">
|
||||||
|
<x-filament::button color="gray" wire:click="clearResult">
|
||||||
|
Try Again
|
||||||
|
</x-filament::button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</x-filament-panels::page>
|
||||||
@@ -5,16 +5,32 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||||
|
|
||||||
<title>{{ config('app.name', 'Laravel') }}</title>
|
<title>{{ $siteSettings['name'] ?? config('app.name', 'Laravel') }}</title>
|
||||||
|
|
||||||
<!-- Fonts -->
|
<!-- Fonts -->
|
||||||
<link rel="preconnect" href="https://fonts.bunny.net">
|
<link rel="preconnect" href="https://fonts.bunny.net">
|
||||||
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
|
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
|
||||||
|
|
||||||
|
<!-- Dynamic Colors from Settings -->
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--primary-color: {{ $siteSettings['primary_color'] ?? '#3b82f6' }};
|
||||||
|
--secondary-color: {{ $siteSettings['secondary_color'] ?? '#8b5cf6' }};
|
||||||
|
--accent-color: {{ $siteSettings['accent_color'] ?? '#10b981' }};
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<!-- Scripts -->
|
<!-- Scripts -->
|
||||||
|
@if(\App\Helpers\ViteHelper::manifestExists())
|
||||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||||
|
@else
|
||||||
|
{!! \App\Helpers\ViteHelper::fallbackStyles() !!}
|
||||||
|
@endif
|
||||||
</head>
|
</head>
|
||||||
<body class="font-sans antialiased">
|
<body class="font-sans antialiased">
|
||||||
|
@if(!\App\Helpers\ViteHelper::manifestExists())
|
||||||
|
{!! \App\Helpers\ViteHelper::devWarningBanner() !!}
|
||||||
|
@endif
|
||||||
<div class="min-h-screen bg-gray-100 dark:bg-gray-900">
|
<div class="min-h-screen bg-gray-100 dark:bg-gray-900">
|
||||||
@include('layouts.navigation')
|
@include('layouts.navigation')
|
||||||
|
|
||||||
|
|||||||
@@ -5,16 +5,32 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||||
|
|
||||||
<title>{{ config('app.name', 'Laravel') }}</title>
|
<title>{{ $siteSettings['name'] ?? config('app.name', 'Laravel') }}</title>
|
||||||
|
|
||||||
<!-- Fonts -->
|
<!-- Fonts -->
|
||||||
<link rel="preconnect" href="https://fonts.bunny.net">
|
<link rel="preconnect" href="https://fonts.bunny.net">
|
||||||
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
|
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
|
||||||
|
|
||||||
|
<!-- Dynamic Colors from Settings -->
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--primary-color: {{ $siteSettings['primary_color'] ?? '#3b82f6' }};
|
||||||
|
--secondary-color: {{ $siteSettings['secondary_color'] ?? '#8b5cf6' }};
|
||||||
|
--accent-color: {{ $siteSettings['accent_color'] ?? '#10b981' }};
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<!-- Scripts -->
|
<!-- Scripts -->
|
||||||
|
@if(\App\Helpers\ViteHelper::manifestExists())
|
||||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||||
|
@else
|
||||||
|
{!! \App\Helpers\ViteHelper::fallbackStyles() !!}
|
||||||
|
@endif
|
||||||
</head>
|
</head>
|
||||||
<body class="font-sans text-gray-900 antialiased">
|
<body class="font-sans text-gray-900 antialiased">
|
||||||
|
@if(!\App\Helpers\ViteHelper::manifestExists())
|
||||||
|
{!! \App\Helpers\ViteHelper::devWarningBanner() !!}
|
||||||
|
@endif
|
||||||
<div class="min-h-screen flex flex-col sm:justify-center items-center pt-6 sm:pt-0 bg-gray-100 dark:bg-gray-900">
|
<div class="min-h-screen flex flex-col sm:justify-center items-center pt-6 sm:pt-0 bg-gray-100 dark:bg-gray-900">
|
||||||
<div>
|
<div>
|
||||||
<a href="/">
|
<a href="/">
|
||||||
|
|||||||
@@ -11,10 +11,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Navigation Links -->
|
<!-- Navigation Links -->
|
||||||
<div class="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex">
|
<div class="hidden sm:-my-px sm:ms-10 sm:flex sm:items-center">
|
||||||
<x-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
|
<x-frontend-menu menu="header" />
|
||||||
{{ __('Dashboard') }}
|
|
||||||
</x-nav-link>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -67,9 +65,7 @@
|
|||||||
<!-- Responsive Navigation Menu -->
|
<!-- Responsive Navigation Menu -->
|
||||||
<div :class="{'block': open, 'hidden': ! open}" class="hidden sm:hidden">
|
<div :class="{'block': open, 'hidden': ! open}" class="hidden sm:hidden">
|
||||||
<div class="pt-2 pb-3 space-y-1">
|
<div class="pt-2 pb-3 space-y-1">
|
||||||
<x-responsive-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
|
<x-frontend-menu-responsive menu="header" />
|
||||||
{{ __('Dashboard') }}
|
|
||||||
</x-responsive-nav-link>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Responsive Settings Options -->
|
<!-- Responsive Settings Options -->
|
||||||
|
|||||||
@@ -12,10 +12,12 @@
|
|||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
Route::middleware('guest')->group(function () {
|
Route::middleware('guest')->group(function () {
|
||||||
|
Route::middleware('registration.enabled')->group(function () {
|
||||||
Route::get('register', [RegisteredUserController::class, 'create'])
|
Route::get('register', [RegisteredUserController::class, 'create'])
|
||||||
->name('register');
|
->name('register');
|
||||||
|
|
||||||
Route::post('register', [RegisteredUserController::class, 'store']);
|
Route::post('register', [RegisteredUserController::class, 'store']);
|
||||||
|
});
|
||||||
|
|
||||||
Route::get('login', [AuthenticatedSessionController::class, 'create'])
|
Route::get('login', [AuthenticatedSessionController::class, 'create'])
|
||||||
->name('login');
|
->name('login');
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
Route::get('/', function () {
|
Route::get('/', function () {
|
||||||
return view('welcome');
|
return redirect()->route('login');
|
||||||
});
|
});
|
||||||
|
|
||||||
Route::get('/dashboard', function () {
|
Route::get('/dashboard', function () {
|
||||||
|
|||||||
Reference in New Issue
Block a user