Compare commits
13 Commits
6d2d4ad5ca
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| f5c059cd08 | |||
| 21620906dc | |||
| 14084e172a | |||
| 6fe63d70af | |||
| fdbd8f5148 | |||
| cd6043ed64 | |||
| eb05ec9b31 | |||
| db0cf0c2c4 | |||
| a6b1cfe498 | |||
| dff2cd752c | |||
| 94f4e53860 | |||
| 779702ca3f | |||
| ed7055edaa |
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/*
|
||||||
@@ -5,6 +5,21 @@
|
|||||||
|
|
||||||
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:
|
||||||
|
|||||||
16
README.md
16
README.md
@@ -75,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
|
||||||
@@ -208,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:
|
||||||
|
|||||||
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.
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
# PHP-FPM Application Server
|
# PHP-FPM Application Server
|
||||||
@@ -6,7 +5,6 @@ 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:
|
||||||
@@ -21,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"
|
||||||
@@ -30,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
|
||||||
|
|
||||||
@@ -43,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"
|
||||||
@@ -63,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"
|
||||||
@@ -83,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"
|
||||||
@@ -93,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"
|
||||||
@@ -105,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
|
||||||
@@ -120,7 +113,6 @@ 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:
|
||||||
@@ -140,7 +132,6 @@ 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:
|
||||||
@@ -156,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"
|
||||||
@@ -167,6 +157,8 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
laravel_network:
|
laravel_network:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
|
gateway:
|
||||||
|
external: true
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
mysql_data:
|
mysql_data:
|
||||||
|
|||||||
@@ -38,6 +38,11 @@ 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 && \
|
||||||
@@ -61,6 +66,13 @@ 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
|
# Run post-install scripts
|
||||||
USER devuser
|
USER devuser
|
||||||
RUN composer run-script post-autoload-dump 2>/dev/null || true
|
RUN composer run-script post-autoload-dump 2>/dev/null || true
|
||||||
|
|||||||
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
|
||||||
|
```
|
||||||
57
setup.sh
57
setup.sh
@@ -106,15 +106,15 @@ 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"
|
||||||
|
|
||||||
# Append port configurations to .env (for reference only, not used by Laravel)
|
# Create .env in project root for docker compose port mapping
|
||||||
# Note: DB_PORT is NOT appended - Laravel uses internal Docker port (3306/5432)
|
echo "# Docker Compose Port Configuration (auto-assigned by setup)" > .env
|
||||||
# These are for docker-compose external port mapping only
|
echo "APP_PORT=$APP_PORT" >> .env
|
||||||
echo "" >> src/.env
|
echo "MAIL_DASHBOARD_PORT=$MAIL_DASHBOARD_PORT" >> .env
|
||||||
echo "# Port Configuration (auto-assigned by setup)" >> src/.env
|
echo "MAIL_PORT=$MAIL_PORT" >> .env
|
||||||
echo "# APP_PORT=$APP_PORT" >> src/.env
|
echo "REDIS_PORT=$REDIS_PORT" >> .env
|
||||||
echo "# MAIL_DASHBOARD_PORT=$MAIL_DASHBOARD_PORT" >> src/.env
|
if [ -n "$DB_PORT" ]; then
|
||||||
echo "# MAIL_PORT=$MAIL_PORT" >> src/.env
|
echo "DB_PORT=$DB_PORT" >> .env
|
||||||
echo "# REDIS_PORT=$REDIS_PORT" >> src/.env
|
fi
|
||||||
|
|
||||||
echo -e "${GREEN}✓ Environment configured for ${DB}${NC}"
|
echo -e "${GREEN}✓ Environment configured for ${DB}${NC}"
|
||||||
else
|
else
|
||||||
@@ -124,13 +124,13 @@ fi
|
|||||||
|
|
||||||
# Step 3: Build containers
|
# Step 3: Build containers
|
||||||
echo -e "${YELLOW}→ Building Docker containers...${NC}"
|
echo -e "${YELLOW}→ Building Docker containers...${NC}"
|
||||||
docker-compose build
|
docker compose build
|
||||||
echo -e "${GREEN}✓ Containers built${NC}"
|
echo -e "${GREEN}✓ Containers built${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Step 4: 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 ""
|
||||||
|
|
||||||
@@ -140,11 +140,20 @@ sleep 3
|
|||||||
echo -e "${GREEN}✓ App container ready${NC}"
|
echo -e "${GREEN}✓ App container ready${NC}"
|
||||||
echo ""
|
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
|
# Step 6: Wait for database
|
||||||
if [ "$DB" = "mysql" ]; then
|
if [ "$DB" = "mysql" ]; then
|
||||||
echo -e "${YELLOW}→ Waiting for MySQL to be ready...${NC}"
|
echo -e "${YELLOW}→ Waiting for MySQL to be ready...${NC}"
|
||||||
for i in {1..30}; do
|
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
|
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}"
|
echo -e "${GREEN}✓ MySQL ready${NC}"
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
@@ -154,7 +163,7 @@ if [ "$DB" = "mysql" ]; then
|
|||||||
elif [ "$DB" = "pgsql" ]; then
|
elif [ "$DB" = "pgsql" ]; then
|
||||||
echo -e "${YELLOW}→ Waiting for PostgreSQL to be ready...${NC}"
|
echo -e "${YELLOW}→ Waiting for PostgreSQL to be ready...${NC}"
|
||||||
for i in {1..30}; do
|
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
|
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}"
|
echo -e "${GREEN}✓ PostgreSQL ready${NC}"
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
@@ -165,32 +174,32 @@ fi
|
|||||||
|
|
||||||
# Step 7: Generate app key
|
# 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 8: 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 9: 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 10: 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
|
# Step 11: Build frontend assets
|
||||||
echo -e "${YELLOW}→ Building frontend assets...${NC}"
|
echo -e "${YELLOW}→ Building frontend assets...${NC}"
|
||||||
docker-compose run --rm node npm install >/dev/null 2>&1
|
docker compose run --rm node npm install >/dev/null 2>&1
|
||||||
docker-compose run --rm node npm run build >/dev/null 2>&1
|
docker compose run --rm node npm run build >/dev/null 2>&1
|
||||||
echo -e "${GREEN}✓ Frontend assets built${NC}"
|
echo -e "${GREEN}✓ Frontend assets built${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
@@ -218,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 ""
|
||||||
|
|||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
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(),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,6 +22,10 @@ public function run(): void
|
|||||||
'users.delete',
|
'users.delete',
|
||||||
'settings.manage',
|
'settings.manage',
|
||||||
'audit.view',
|
'audit.view',
|
||||||
|
'menus.view',
|
||||||
|
'menus.create',
|
||||||
|
'menus.edit',
|
||||||
|
'menus.delete',
|
||||||
];
|
];
|
||||||
|
|
||||||
foreach ($corePermissions as $permission) {
|
foreach ($corePermissions as $permission) {
|
||||||
|
|||||||
2
src/package-lock.json
generated
2
src/package-lock.json
generated
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "app",
|
"name": "html",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
|
|||||||
@@ -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
|
||||||
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>
|
||||||
@@ -21,9 +21,16 @@
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<!-- Scripts -->
|
<!-- Scripts -->
|
||||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
@if(\App\Helpers\ViteHelper::manifestExists())
|
||||||
|
@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')
|
||||||
|
|
||||||
|
|||||||
@@ -21,9 +21,16 @@
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<!-- Scripts -->
|
<!-- Scripts -->
|
||||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
@if(\App\Helpers\ViteHelper::manifestExists())
|
||||||
|
@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 -->
|
||||||
|
|||||||
Reference in New Issue
Block a user