feat: add module generator feature with controller, routing, and UI integration
This commit is contained in:
@@ -0,0 +1,138 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use App\Types;
|
||||||
|
use Illuminate\Support\Facades\Artisan;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Illuminate\Support\Facades\File;
|
||||||
|
|
||||||
|
class ModuleGeneratorController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->middleware('auth');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
return view('admin.module-generator');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function generate(Request $request)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'title' => 'required|string|max:255',
|
||||||
|
'columns' => 'required|array',
|
||||||
|
'columns.*.name' => 'required|string',
|
||||||
|
'columns.*.type' => 'required|string',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$title = $request->title;
|
||||||
|
$slug = Str::slug($title);
|
||||||
|
$tableName = str_replace('-', '_', $slug);
|
||||||
|
$modelName = Str::studly($slug);
|
||||||
|
|
||||||
|
// 1. Create Migration File
|
||||||
|
$timestamp = date('Y_m_d_His');
|
||||||
|
$migrationFilename = "{$timestamp}_create_{$tableName}_table.php";
|
||||||
|
$migrationPath = database_path("migrations/{$migrationFilename}");
|
||||||
|
|
||||||
|
$migrationContent = "<?php\n\nuse Illuminate\Database\Migrations\Migration;\nuse Illuminate\Database\Schema\Blueprint;\nuse Illuminate\Support\Facades\Schema;\n\nclass Create" . Str::studly($tableName) . "Table extends Migration\n{\n public function up()\n {\n Schema::create('{$tableName}', function (Blueprint \$table) {\n \$table->id();\n";
|
||||||
|
|
||||||
|
foreach ($request->columns as $column) {
|
||||||
|
$colName = Str::snake($column['name']);
|
||||||
|
$uiType = $column['type'];
|
||||||
|
|
||||||
|
$dbType = 'string';
|
||||||
|
switch ($uiType) {
|
||||||
|
case 'integer': $dbType = 'integer'; break;
|
||||||
|
case 'decimal': $dbType = 'decimal'; break;
|
||||||
|
case 'date': $dbType = 'date'; break;
|
||||||
|
case 'datetime': $dbType = 'dateTime'; break;
|
||||||
|
case 'long-text': $dbType = 'longText'; break;
|
||||||
|
case 'boolean': $dbType = 'boolean'; break;
|
||||||
|
default: $dbType = 'string'; break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($dbType == 'decimal') {
|
||||||
|
$migrationContent .= " \$table->decimal('{$colName}', 15, 2)->nullable();\n";
|
||||||
|
} else {
|
||||||
|
$migrationContent .= " \$table->{$dbType}('{$colName}')->nullable();\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$migrationContent .= " \$table->timestamps();\n \$table->softDeletes();\n });\n }\n\n public function down()\n {\n Schema::dropIfExists('{$tableName}');\n }\n}\n";
|
||||||
|
|
||||||
|
File::put($migrationPath, $migrationContent);
|
||||||
|
|
||||||
|
// Run the specific migration
|
||||||
|
Artisan::call('migrate', [
|
||||||
|
'--path' => "database/migrations/{$migrationFilename}",
|
||||||
|
'--force' => true
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 2. Create Model
|
||||||
|
$modelPath = app_path("Models/{$modelName}.php");
|
||||||
|
if (!File::exists($modelPath)) {
|
||||||
|
$modelContent = "<?php\n\nnamespace App\Models;\n\nuse Illuminate\Database\Eloquent\Model;\nuse Illuminate\Database\Eloquent\SoftDeletes;\n\nclass {$modelName} extends Model\n{\n use SoftDeletes;\n protected \$table = '{$tableName}';\n protected \$guarded = [];\n}\n";
|
||||||
|
File::put($modelPath, $modelContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Create Blade View
|
||||||
|
$viewPath = resource_path("views/admin/type/{$slug}.blade.php");
|
||||||
|
if (!File::exists($viewPath)) {
|
||||||
|
$templatePath = resource_path("views/admin/type/datagrid-module-template.blade.php");
|
||||||
|
if (File::exists($templatePath)) {
|
||||||
|
$templateContent = File::get($templatePath);
|
||||||
|
|
||||||
|
// Simple replacements in template
|
||||||
|
$templateContent = str_replace('use App\Models\RadiographicTest;', "use App\Models\\{$modelName};", $templateContent);
|
||||||
|
$templateContent = str_replace('$title = "Radiographic Test";', "\$title = \"{$title}\";", $templateContent);
|
||||||
|
$templateContent = str_replace('$tableName = "radiographic_tests";', "\$tableName = \"{$tableName}\";", $templateContent);
|
||||||
|
|
||||||
|
// Generate relationDatas
|
||||||
|
$relationDatasCode = "\$relationDatas = [\n";
|
||||||
|
foreach ($request->columns as $column) {
|
||||||
|
$colName = Str::snake($column['name']);
|
||||||
|
$colType = $column['type'];
|
||||||
|
$relationDatasCode .= " '{$colName}' => [\n 'type' => '{$colType}',\n ],\n";
|
||||||
|
}
|
||||||
|
$relationDatasCode .= "];";
|
||||||
|
|
||||||
|
// Replace the hardcoded relationDatas in template
|
||||||
|
$pattern = '/\$relationDatas = \[.*?\];/s';
|
||||||
|
$templateContent = preg_replace($pattern, $relationDatasCode, $templateContent);
|
||||||
|
|
||||||
|
File::put($viewPath, $templateContent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Handle Icon
|
||||||
|
$iconName = $slug;
|
||||||
|
if ($request->hasFile('icon')) {
|
||||||
|
$request->file('icon')->move(base_path('assets/icons'), $iconName . '.png');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Insert into types table
|
||||||
|
$type = Types::where('slug', $slug)->first();
|
||||||
|
if (!$type) {
|
||||||
|
$type = new Types();
|
||||||
|
$type->title = $title;
|
||||||
|
$type->slug = $slug;
|
||||||
|
$type->icon = $iconName;
|
||||||
|
$type->enabled = 1;
|
||||||
|
$type->full_control = "1"; // Default admin level
|
||||||
|
$type->write = "1";
|
||||||
|
$type->read = "1";
|
||||||
|
$type->modify = "1";
|
||||||
|
$type->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect("admin/types/{$slug}")->with('success', 'Module generated successfully!');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -195,6 +195,10 @@
|
|||||||
<a class="nav-submenu" href="{{ url('admin/new/types') }}"><i
|
<a class="nav-submenu" href="{{ url('admin/new/types') }}"><i
|
||||||
class="si si-layers"></i><span class="sidebar-mini-hide">{{e2('Modules')}}</span></a>
|
class="si si-layers"></i><span class="sidebar-mini-hide">{{e2('Modules')}}</span></a>
|
||||||
|
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="nav-submenu" href="{{ url('admin/module-generator') }}"><i
|
||||||
|
class="si si-magic-wand"></i><span class="sidebar-mini-hide">{{e2('Module Generator')}}</span></a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a class="nav-submenu" href="{{ url('admin/new/artisan') }}"><i
|
<a class="nav-submenu" href="{{ url('admin/new/artisan') }}"><i
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
@extends('admin.master')
|
||||||
|
@section("title", e2("Module Generator"))
|
||||||
|
@section("desc", e2("Create a new module with model, migration and view automatically."))
|
||||||
|
@section('content')
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<form action="{{ url('admin/module-generator/generate') }}" method="POST" enctype="multipart/form-data">
|
||||||
|
@csrf
|
||||||
|
<div class="block">
|
||||||
|
<div class="block-header block-header-default">
|
||||||
|
<h3 class="block-title">{{e2("Module Basic Information")}}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="block-content">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="title">{{e2("Module Title")}}</label>
|
||||||
|
<input type="text" name="title" id="title" class="form-control" placeholder="e.g. Products" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="icon">{{e2("Module Icon (PNG)")}}</label>
|
||||||
|
<input type="file" name="icon" id="icon" class="form-control" accept="image/png">
|
||||||
|
<small class="text-muted">Recommended size: 64x64px. Saved to assets/icons/</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="block">
|
||||||
|
<div class="block-header block-header-default">
|
||||||
|
<h3 class="block-title">{{e2("Database Columns")}}</h3>
|
||||||
|
<div class="block-options">
|
||||||
|
<button type="button" class="btn btn-sm btn-success" id="add-column">
|
||||||
|
<i class="fa fa-plus mr-1"></i> {{e2("Add Column")}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="block-content">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-bordered table-striped" id="columns-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{e2("Column Name")}}</th>
|
||||||
|
<th>{{e2("Type")}}</th>
|
||||||
|
<th class="text-center" style="width: 50px;">{{e2("Action")}}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="columns-body">
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<input type="text" name="columns[0][name]" class="form-control" placeholder="e.g. product_name" required>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<select name="columns[0][type]" class="form-control" required>
|
||||||
|
<option value="string">String (Text)</option>
|
||||||
|
<option value="integer">Integer (Number)</option>
|
||||||
|
<option value="decimal">Decimal (Price/Ratio)</option>
|
||||||
|
<option value="date">Date</option>
|
||||||
|
<option value="datetime">DateTime</option>
|
||||||
|
<option value="long-text">Long Text (Textarea)</option>
|
||||||
|
<option value="boolean">Boolean (Yes/No)</option>
|
||||||
|
<option value="select">Select (Dropdown)</option>
|
||||||
|
<option value="link-search">Link Search (File/PDF)</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<button type="button" class="btn btn-sm btn-danger remove-column">
|
||||||
|
<i class="fa fa-times"></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="py-3">
|
||||||
|
<button type="submit" class="btn btn-primary btn-lg btn-block">
|
||||||
|
<i class="fa fa-magic mr-1"></i> {{e2("Generate Module")}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@section('script')
|
||||||
|
<script>
|
||||||
|
$(function() {
|
||||||
|
let colIndex = 1;
|
||||||
|
|
||||||
|
$('#add-column').click(function() {
|
||||||
|
let html = `
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<input type="text" name="columns[${colIndex}][name]" class="form-control" placeholder="e.g. column_name" required>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<select name="columns[${colIndex}][type]" class="form-control" required>
|
||||||
|
<option value="string">String (Text)</option>
|
||||||
|
<option value="integer">Integer (Number)</option>
|
||||||
|
<option value="decimal">Decimal (Price/Ratio)</option>
|
||||||
|
<option value="date">Date</option>
|
||||||
|
<option value="datetime">DateTime</option>
|
||||||
|
<option value="long-text">Long Text (Textarea)</option>
|
||||||
|
<option value="boolean">Boolean (Yes/No)</option>
|
||||||
|
<option value="select">Select (Dropdown)</option>
|
||||||
|
<option value="link-search">Link Search (File/PDF)</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<button type="button" class="btn btn-sm btn-danger remove-column">
|
||||||
|
<i class="fa fa-times"></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
$('#columns-body').append(html);
|
||||||
|
colIndex++;
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on('click', '.remove-column', function() {
|
||||||
|
if ($('#columns-body tr').length > 1) {
|
||||||
|
$(this).closest('tr').remove();
|
||||||
|
} else {
|
||||||
|
alert('At least one column is required.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@endsection
|
||||||
@@ -234,6 +234,9 @@ Route::group(['prefix' => $hash, 'middleware' => 'auth'], function () {
|
|||||||
return back()->with('success', 'Statuses updated');
|
return back()->with('success', 'Statuses updated');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Route::get('/module-generator', [\App\Http\Controllers\ModuleGeneratorController::class, 'index']);
|
||||||
|
Route::post('/module-generator/generate', [\App\Http\Controllers\ModuleGeneratorController::class, 'generate']);
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Önbelleğe alınmış dashboard sayfası route'u
|
// Önbelleğe alınmış dashboard sayfası route'u
|
||||||
|
|||||||
+13
-3
@@ -1,11 +1,21 @@
|
|||||||
self.addEventListener('install', (event) => {
|
self.addEventListener('install', (event) => {
|
||||||
event.waitUntil(
|
event.waitUntil(
|
||||||
caches.open('app-cache').then((cache) => {
|
caches.open('app-cache').then((cache) => {
|
||||||
return cache.addAll([
|
// Use a more robust approach: attempt to cache but don't fail the whole installation if one fails
|
||||||
'/',
|
const urlsToCache = [
|
||||||
'/assets/manifest.json',
|
'/assets/manifest.json',
|
||||||
'/assets/favicon.png'
|
'/assets/favicon.png'
|
||||||
]);
|
];
|
||||||
|
|
||||||
|
return Promise.all(
|
||||||
|
urlsToCache.map(url => {
|
||||||
|
return fetch(url).then(response => {
|
||||||
|
if (response.ok) {
|
||||||
|
return cache.put(url, response);
|
||||||
|
}
|
||||||
|
}).catch(err => console.warn('SW: Failed to cache ' + url, err));
|
||||||
|
})
|
||||||
|
);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user