new admin design with mnodules
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Administration\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Modules\Audit\Models\AuditLog;
|
||||
use Modules\Media\Models\Media;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
final class DashboardController extends Controller
|
||||
{
|
||||
public function __invoke()
|
||||
{
|
||||
return view('administration::dashboard', [
|
||||
'userCount' => User::count(),
|
||||
'roleCount' => Schema::hasTable('roles') ? Role::count() : 0,
|
||||
'mediaCount' => Schema::hasTable('media') ? Media::count() : 0,
|
||||
'auditCount' => Schema::hasTable('audit_logs') ? AuditLog::count() : 0,
|
||||
'recentAudit' => Schema::hasTable('audit_logs') ? AuditLog::query()->latest('created_at')->limit(8)->get() : collect(),
|
||||
'modules' => $this->modules(),
|
||||
]);
|
||||
}
|
||||
|
||||
private function modules(): array
|
||||
{
|
||||
$statuses = json_decode((string) file_get_contents(base_path('modules_statuses.json')), true) ?: [];
|
||||
$modules = [];
|
||||
foreach (glob(base_path('Modules/*/module.json')) ?: [] as $manifest) {
|
||||
$data = json_decode((string) file_get_contents($manifest), true) ?: [];
|
||||
$name = $data['name'] ?? basename(dirname($manifest));
|
||||
$modules[] = ['name' => $name, 'version' => $data['version'] ?? '1.0.0', 'enabled' => (bool) ($statuses[$name] ?? false)];
|
||||
}
|
||||
usort($modules, fn ($a, $b) => strcasecmp($a['name'], $b['name']));
|
||||
return $modules;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Administration\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
|
||||
final class ModuleController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$statuses = json_decode((string) file_get_contents(base_path('modules_statuses.json')), true) ?: [];
|
||||
$modules = collect(glob(base_path('Modules/*/module.json')) ?: [])->map(function (string $manifest) use ($statuses): array {
|
||||
$data = json_decode((string) file_get_contents($manifest), true) ?: [];
|
||||
$name = $data['name'] ?? basename(dirname($manifest));
|
||||
return [
|
||||
'name' => $name,
|
||||
'description' => $data['description'] ?? 'Laravel application module.',
|
||||
'version' => $data['version'] ?? '1.0.0',
|
||||
'enabled' => (bool) ($statuses[$name] ?? false),
|
||||
'providers' => $data['providers'] ?? [],
|
||||
];
|
||||
})->sortBy('name', SORT_NATURAL | SORT_FLAG_CASE)->values();
|
||||
|
||||
return view('administration::modules.index', compact('modules'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<x-layouts.app :title="__('Administration')">
|
||||
<div class="space-y-6">
|
||||
<div><h1 class="text-2xl font-semibold text-zinc-950 dark:text-zinc-100">Administration</h1><p class="mt-1 text-sm text-zinc-600 dark:text-zinc-400">Platform health and recent activity.</p></div>
|
||||
<div class="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
|
||||
@foreach([['Users',$userCount,'users'],['Roles',$roleCount,'shield-check'],['Media',$mediaCount,'photo'],['Audit events',$auditCount,'clipboard-document-list']] as [$label,$count,$icon])
|
||||
<div class="rounded-xl border border-zinc-200 bg-white p-5 shadow-sm dark:border-zinc-700 dark:bg-zinc-900"><flux:icon :name="$icon" class="size-5 text-zinc-500"/><div class="mt-4 text-3xl font-semibold">{{ $count }}</div><div class="text-sm text-zinc-500">{{ $label }}</div></div>
|
||||
@endforeach
|
||||
</div>
|
||||
<div class="grid gap-6 xl:grid-cols-2">
|
||||
<div class="rounded-xl border border-zinc-200 bg-white shadow-sm dark:border-zinc-700 dark:bg-zinc-900"><div class="border-b border-zinc-200 px-5 py-4 font-semibold dark:border-zinc-700">Modules</div><div class="divide-y divide-zinc-200 dark:divide-zinc-800">@foreach($modules as $module)<div class="flex items-center justify-between px-5 py-3"><div><div class="font-medium">{{ $module['name'] }}</div><div class="text-xs text-zinc-500">v{{ $module['version'] }}</div></div><span class="rounded-full px-2.5 py-1 text-xs {{ $module['enabled'] ? 'bg-emerald-100 text-emerald-800 dark:bg-emerald-950 dark:text-emerald-200' : 'bg-zinc-100 text-zinc-600 dark:bg-zinc-800 dark:text-zinc-300' }}">{{ $module['enabled'] ? 'Enabled' : 'Disabled' }}</span></div>@endforeach</div></div>
|
||||
<div class="rounded-xl border border-zinc-200 bg-white shadow-sm dark:border-zinc-700 dark:bg-zinc-900"><div class="border-b border-zinc-200 px-5 py-4 font-semibold dark:border-zinc-700">Recent audit activity</div><div class="divide-y divide-zinc-200 dark:divide-zinc-800">@forelse($recentAudit as $log)<div class="px-5 py-3"><div class="font-medium">{{ $log->event }}</div><div class="text-xs text-zinc-500">{{ optional($log->created_at)->diffForHumans() }}</div></div>@empty<div class="px-5 py-8 text-center text-sm text-zinc-500">No audit events yet.</div>@endforelse</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</x-layouts.app>
|
||||
@@ -0,0 +1 @@
|
||||
<x-layouts.app :title="__('Modules')"><div class="space-y-6"><div><h1 class="text-2xl font-semibold">Modules</h1><p class="mt-1 text-sm text-zinc-500">Installed platform modules and runtime status.</p></div><div class="overflow-hidden rounded-xl border border-zinc-200 bg-white dark:border-zinc-700 dark:bg-zinc-900"><table class="min-w-full divide-y divide-zinc-200 text-sm dark:divide-zinc-700"><thead class="bg-zinc-50 text-left text-xs uppercase text-zinc-500 dark:bg-zinc-950"><tr><th class="px-4 py-3">Module</th><th class="px-4 py-3">Version</th><th class="px-4 py-3">Status</th><th class="px-4 py-3">Providers</th></tr></thead><tbody class="divide-y divide-zinc-200 dark:divide-zinc-800">@foreach($modules as $module)<tr><td class="px-4 py-3"><div class="font-medium">{{ $module['name'] }}</div><div class="text-xs text-zinc-500">{{ $module['description'] }}</div></td><td class="px-4 py-3">{{ $module['version'] }}</td><td class="px-4 py-3">{{ $module['enabled'] ? 'Enabled' : 'Disabled' }}</td><td class="px-4 py-3 text-xs text-zinc-500">{{ implode(', ', $module['providers']) ?: '—' }}</td></tr>@endforeach</tbody></table></div></div></x-layouts.app>
|
||||
@@ -1,5 +1,10 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Modules\Administration\Http\Controllers\DashboardController;
|
||||
use Modules\Administration\Http\Controllers\ModuleController;
|
||||
|
||||
// Module routes are intentionally registered by each feature as it is enabled.
|
||||
Route::middleware(['auth', 'verified'])->prefix('administration')->name('administration.')->group(function (): void {
|
||||
Route::get('/', DashboardController::class)->name('dashboard');
|
||||
Route::get('/modules', [ModuleController::class, 'index'])->name('modules.index');
|
||||
});
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<?php
|
||||
namespace Modules\Audit\Http\Controllers;
|
||||
use App\Http\Controllers\Controller; use Modules\Audit\Models\AuditLog;
|
||||
final class AuditController extends Controller { public function index(){ return view('audit::index',['logs'=>AuditLog::query()->latest('created_at')->paginate(50)]); } }
|
||||
@@ -0,0 +1 @@
|
||||
<x-layouts.app :title="__('Audit log')"><div class="space-y-6"><div><h1 class="text-2xl font-semibold">Audit log</h1><p class="mt-1 text-sm text-zinc-500">Recorded platform activity.</p></div><div class="overflow-hidden rounded-xl border border-zinc-200 bg-white dark:border-zinc-700 dark:bg-zinc-900"><table class="min-w-full divide-y divide-zinc-200 text-sm dark:divide-zinc-700"><thead><tr><th class="px-4 py-3 text-left">Event</th><th class="px-4 py-3 text-left">Actor</th><th class="px-4 py-3 text-left">IP</th><th class="px-4 py-3 text-left">Date</th></tr></thead><tbody class="divide-y divide-zinc-200 dark:divide-zinc-800">@forelse($logs as $log)<tr><td class="px-4 py-3 font-medium">{{ $log->event }}</td><td class="px-4 py-3">{{ $log->actor_type ? class_basename($log->actor_type).' #'.$log->actor_id : 'System' }}</td><td class="px-4 py-3">{{ $log->ip_address ?: '—' }}</td><td class="px-4 py-3">{{ optional($log->created_at)->format('d M Y H:i') }}</td></tr>@empty<tr><td colspan="4" class="px-4 py-10 text-center text-zinc-500">No audit events recorded.</td></tr>@endforelse</tbody></table></div>{{ $logs->links() }}</div></x-layouts.app>
|
||||
@@ -1,5 +1,3 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
// Module routes are intentionally registered by each feature as it is enabled.
|
||||
use Illuminate\Support\Facades\Route; use Modules\Audit\Http\Controllers\AuditController;
|
||||
Route::middleware(['auth','verified'])->get('administration/audit',[AuditController::class,'index'])->name('administration.audit.index');
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
namespace Modules\Media\Http\Controllers;
|
||||
use App\Http\Controllers\Controller; use Illuminate\Http\Request; use Illuminate\Support\Facades\Storage; use Modules\Media\Models\Media;
|
||||
final class MediaController extends Controller {
|
||||
public function index(){ return view('media::index',['media'=>Media::query()->latest()->paginate(30)]); }
|
||||
public function store(Request $request){ $request->validate(['file'=>['required','file','max:20480'],'title'=>['nullable','string','max:255'],'alt_text'=>['nullable','string','max:255']]); $file=$request->file('file'); $path=$file->store('media','public'); Media::create(['disk'=>'public','path'=>$path,'original_name'=>$file->getClientOriginalName(),'mime_type'=>$file->getMimeType(),'size'=>$file->getSize(),'title'=>$request->string('title')->toString() ?: pathinfo($file->getClientOriginalName(),PATHINFO_FILENAME),'alt_text'=>$request->string('alt_text')->toString() ?: null,'uploaded_by'=>$request->user()->id]); return back()->with('status','Media uploaded.'); }
|
||||
public function destroy(Media $medium){ Storage::disk($medium->disk)->delete($medium->path); $medium->delete(); return back()->with('status','Media deleted.'); }
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<x-layouts.app :title="__('Media library')"><div class="space-y-6"><div><h1 class="text-2xl font-semibold">Media library</h1><p class="mt-1 text-sm text-zinc-500">Upload and manage reusable files.</p></div>@if(session('status'))<div class="rounded-xl bg-emerald-50 p-3 text-sm text-emerald-800">{{ session('status') }}</div>@endif<form method="POST" enctype="multipart/form-data" action="{{ route('administration.media.store') }}" class="grid gap-4 rounded-xl border border-zinc-200 bg-white p-5 sm:grid-cols-3 dark:border-zinc-700 dark:bg-zinc-900">@csrf<flux:input type="file" name="file" label="File" required/><flux:input name="title" label="Title"/><flux:input name="alt_text" label="Alternative text"/><div class="sm:col-span-3"><flux:button type="submit" variant="primary">Upload</flux:button></div></form><div class="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">@forelse($media as $item)<article class="overflow-hidden rounded-xl border border-zinc-200 bg-white dark:border-zinc-700 dark:bg-zinc-900">@if(str_starts_with((string)$item->mime_type,'image/'))<img src="{{ Storage::disk($item->disk)->url($item->path) }}" alt="{{ $item->alt_text }}" class="h-40 w-full object-cover">@else<div class="flex h-40 items-center justify-center bg-zinc-100 text-zinc-500 dark:bg-zinc-800"><flux:icon.document class="size-10"/></div>@endif<div class="p-4"><div class="truncate font-medium">{{ $item->title ?: $item->original_name }}</div><div class="mt-1 text-xs text-zinc-500">{{ Number::fileSize($item->size) }}</div><form method="POST" action="{{ route('administration.media.destroy',$item) }}" class="mt-3">@csrf @method('DELETE')<flux:button size="sm" variant="danger" type="submit">Delete</flux:button></form></div></article>@empty<div class="col-span-full rounded-xl border border-dashed border-zinc-300 p-10 text-center text-sm text-zinc-500">No media uploaded.</div>@endforelse</div>{{ $media->links() }}</div></x-layouts.app>
|
||||
@@ -1,5 +1,3 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
// Module routes are intentionally registered by each feature as it is enabled.
|
||||
use Illuminate\Support\Facades\Route; use Modules\Media\Http\Controllers\MediaController;
|
||||
Route::middleware(['auth','verified'])->prefix('administration/media')->name('administration.media.')->group(function (): void { Route::get('/',[MediaController::class,'index'])->name('index'); Route::post('/',[MediaController::class,'store'])->name('store'); Route::delete('/{medium}',[MediaController::class,'destroy'])->name('destroy'); });
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Roles\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
final class RoleController extends Controller
|
||||
{
|
||||
public function index() { return view('roles::index',['roles'=>Role::withCount(['users','permissions'])->orderBy('name')->get()]); }
|
||||
public function create() { return view('roles::form',['role'=>new Role(['guard_name'=>'web']),'permissions'=>Permission::orderBy('name')->get()]); }
|
||||
public function store(Request $request) { $data=$this->validated($request); $role=Role::create(['name'=>$data['name'],'guard_name'=>'web']); $role->syncPermissions($data['permissions']??[]); return redirect()->route('administration.roles.index')->with('status','Role created.'); }
|
||||
public function edit(Role $role) { return view('roles::form',['role'=>$role->load('permissions'),'permissions'=>Permission::orderBy('name')->get()]); }
|
||||
public function update(Request $request, Role $role) { $data=$this->validated($request,$role); $role->update(['name'=>$data['name']]); $role->syncPermissions($data['permissions']??[]); return redirect()->route('administration.roles.index')->with('status','Role updated.'); }
|
||||
public function destroy(Role $role) { abort_if($role->name==='admin',422,'The administrator role cannot be deleted.'); $role->delete(); return back()->with('status','Role deleted.'); }
|
||||
private function validated(Request $request, ?Role $role=null): array { return $request->validate(['name'=>['required','string','max:100',Rule::unique('roles','name')->ignore($role)],'permissions'=>['array'],'permissions.*'=>['string','exists:permissions,name']]); }
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<x-layouts.app :title="$role->exists ? __('Edit role') : __('Create role')"><div class="max-w-3xl space-y-6"><div><h1 class="text-2xl font-semibold">{{ $role->exists ? 'Edit role' : 'Create role' }}</h1></div><form method="POST" action="{{ $role->exists ? route('administration.roles.update',$role) : route('administration.roles.store') }}" class="space-y-6 rounded-xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-900">@csrf @if($role->exists)@method('PUT')@endif<flux:input name="name" label="Name" :value="old('name',$role->name)" required/><div><div class="mb-3 font-medium">Permissions</div><div class="grid gap-3 sm:grid-cols-2">@foreach($permissions as $permission)<label class="flex items-center gap-3 rounded-lg border border-zinc-200 p-3 dark:border-zinc-700"><input type="checkbox" name="permissions[]" value="{{ $permission->name }}" @checked(in_array($permission->name,old('permissions',$role->exists ? $role->permissions->pluck('name')->all() : [])))><span class="text-sm">{{ $permission->name }}</span></label>@endforeach</div></div><div class="flex justify-end gap-2"><flux:button :href="route('administration.roles.index')">Cancel</flux:button><flux:button type="submit" variant="primary">Save</flux:button></div></form></div></x-layouts.app>
|
||||
@@ -0,0 +1 @@
|
||||
<x-layouts.app :title="__('Roles')"><div class="space-y-6"><div class="flex items-start justify-between"><div><h1 class="text-2xl font-semibold">Roles & permissions</h1><p class="mt-1 text-sm text-zinc-500">Control administrative access.</p></div><flux:button variant="primary" :href="route('administration.roles.create')" wire:navigate>Create role</flux:button></div>@if(session('status'))<div class="rounded-xl bg-emerald-50 p-3 text-sm text-emerald-800">{{ session('status') }}</div>@endif<div class="rounded-xl border border-zinc-200 bg-white dark:border-zinc-700 dark:bg-zinc-900"><div class="divide-y divide-zinc-200 dark:divide-zinc-800">@foreach($roles as $role)<div class="flex items-center justify-between p-4"><div><div class="font-medium">{{ $role->name }}</div><div class="text-xs text-zinc-500">{{ $role->users_count }} users · {{ $role->permissions_count }} permissions</div></div><div class="flex gap-2"><flux:button size="sm" :href="route('administration.roles.edit',$role)" wire:navigate>Edit</flux:button>@if($role->name!=='admin')<form method="POST" action="{{ route('administration.roles.destroy',$role) }}">@csrf @method('DELETE')<flux:button size="sm" variant="danger" type="submit">Delete</flux:button></form>@endif</div></div>@endforeach</div></div></div></x-layouts.app>
|
||||
@@ -1,5 +1,3 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
// Module routes are intentionally registered by each feature as it is enabled.
|
||||
use Illuminate\Support\Facades\Route; use Modules\Roles\Http\Controllers\RoleController;
|
||||
Route::middleware(['auth','verified'])->prefix('administration/roles')->name('administration.roles.')->group(function (): void { Route::resource('', RoleController::class)->parameters([''=>'role'])->except(['show']); });
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<?php
|
||||
namespace Modules\Search\Http\Controllers;
|
||||
use App\Http\Controllers\Controller; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB;
|
||||
final class SearchController extends Controller { public function index(Request $request){ $q=trim((string)$request->query('q')); $results=DB::table('search_index')->when($q!== '',fn($query)=>$query->where(fn($w)=>$w->where('title','like',"%{$q}%")->orWhere('summary','like',"%{$q}%")->orWhere('content','like',"%{$q}%")))->latest('updated_at')->paginate(25)->withQueryString(); return view('search::index',compact('results','q')); } }
|
||||
@@ -0,0 +1 @@
|
||||
<x-layouts.app :title="__('Search index')"><div class="space-y-6"><div><h1 class="text-2xl font-semibold">Search index</h1><p class="mt-1 text-sm text-zinc-500">Inspect content currently available to platform search.</p></div><form method="GET" class="flex gap-2"><flux:input name="q" :value="$q" placeholder="Search indexed content..." class="flex-1"/><flux:button type="submit" variant="primary">Search</flux:button></form><div class="rounded-xl border border-zinc-200 bg-white dark:border-zinc-700 dark:bg-zinc-900"><div class="divide-y divide-zinc-200 dark:divide-zinc-800">@forelse($results as $result)<div class="p-4"><div class="font-medium">{{ $result->title }}</div><div class="mt-1 text-sm text-zinc-500">{{ str($result->summary ?: $result->content)->limit(180) }}</div><div class="mt-2 text-xs text-zinc-400">{{ $result->searchable_type }} #{{ $result->searchable_id }}</div></div>@empty<div class="p-10 text-center text-sm text-zinc-500">No indexed content found.</div>@endforelse</div></div>{{ $results->links() }}</div></x-layouts.app>
|
||||
@@ -1,5 +1,3 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
// Module routes are intentionally registered by each feature as it is enabled.
|
||||
use Illuminate\Support\Facades\Route; use Modules\Search\Http\Controllers\SearchController;
|
||||
Route::middleware(['auth','verified'])->get('administration/search',[SearchController::class,'index'])->name('administration.search.index');
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
namespace Modules\Seo\Http\Controllers;
|
||||
use App\Http\Controllers\Controller; use Illuminate\Http\Request; use Modules\Settings\Models\Setting;
|
||||
final class SeoController extends Controller {
|
||||
public function index(){ $values=Setting::where('group','seo')->pluck('value','key'); return view('seo::index',compact('values')); }
|
||||
public function update(Request $request){ $data=$request->validate(['site_title'=>['nullable','string','max:255'],'meta_description'=>['nullable','string','max:500'],'robots'=>['required','string','max:100'],'canonical_base'=>['nullable','url','max:255']]); foreach($data as $key=>$value) Setting::updateOrCreate(['group'=>'seo','key'=>$key],['value'=>$value,'type'=>'string','is_public'=>true]); return back()->with('status','SEO defaults saved.'); }
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<x-layouts.app :title="__('SEO')"><div class="max-w-3xl space-y-6"><div><h1 class="text-2xl font-semibold">SEO defaults</h1><p class="mt-1 text-sm text-zinc-500">Site-wide metadata used when content does not provide its own values.</p></div>@if(session('status'))<div class="rounded-xl bg-emerald-50 p-3 text-sm text-emerald-800">{{ session('status') }}</div>@endif<form method="POST" action="{{ route('administration.seo.update') }}" class="space-y-5 rounded-xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-900">@csrf @method('PUT')<flux:input name="site_title" label="Site title" :value="old('site_title',$values['site_title']??'')"/><flux:textarea name="meta_description" label="Default meta description">{{ old('meta_description',$values['meta_description']??'') }}</flux:textarea><flux:input name="robots" label="Robots" :value="old('robots',$values['robots']??'index,follow')" required/><flux:input name="canonical_base" label="Canonical base URL" :value="old('canonical_base',$values['canonical_base']??'')"/><div class="flex justify-end"><flux:button type="submit" variant="primary">Save SEO defaults</flux:button></div></form></div></x-layouts.app>
|
||||
@@ -1,5 +1,3 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
// Module routes are intentionally registered by each feature as it is enabled.
|
||||
use Illuminate\Support\Facades\Route; use Modules\Seo\Http\Controllers\SeoController;
|
||||
Route::middleware(['auth','verified'])->prefix('administration/seo')->name('administration.seo.')->group(function (): void { Route::get('/',[SeoController::class,'index'])->name('index'); Route::put('/',[SeoController::class,'update'])->name('update'); });
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
namespace Modules\Settings\Http\Controllers;
|
||||
use App\Http\Controllers\Controller; use Illuminate\Http\Request; use Modules\Settings\Models\Setting;
|
||||
final class SettingController extends Controller {
|
||||
public function index(){ $settings=Setting::query()->orderBy('group')->orderBy('key')->get(); return view('settings::index',compact('settings')); }
|
||||
public function update(Request $request){ $data=$request->validate(['settings'=>['array'],'settings.*'=>['nullable','string','max:5000']]); foreach($data['settings']??[] as $id=>$value){ Setting::whereKey($id)->update(['value'=>$value]); } return back()->with('status','Settings saved.'); }
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<x-layouts.app :title="__('Settings')"><div class="max-w-4xl space-y-6"><div><h1 class="text-2xl font-semibold">Platform settings</h1><p class="mt-1 text-sm text-zinc-500">Shared configuration stored by the Settings module.</p></div>@if(session('status'))<div class="rounded-xl bg-emerald-50 p-3 text-sm text-emerald-800">{{ session('status') }}</div>@endif<form method="POST" action="{{ route('administration.settings.update') }}" class="space-y-4">@csrf @method('PUT')@forelse($settings->groupBy('group') as $group=>$items)<section class="rounded-xl border border-zinc-200 bg-white p-5 dark:border-zinc-700 dark:bg-zinc-900"><h2 class="mb-4 font-semibold capitalize">{{ $group }}</h2><div class="space-y-4">@foreach($items as $setting)<flux:input name="settings[{{ $setting->id }}]" :label="str($setting->key)->replace('_',' ')->title()" :value="$setting->value"/>@endforeach</div></section>@empty<div class="rounded-xl border border-dashed border-zinc-300 p-10 text-center text-sm text-zinc-500">No settings have been registered yet. SEO defaults can be configured from the SEO screen.</div>@endforelse<div class="flex justify-end"><flux:button type="submit" variant="primary">Save settings</flux:button></div></form></div></x-layouts.app>
|
||||
@@ -1,5 +1,3 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
// Module routes are intentionally registered by each feature as it is enabled.
|
||||
use Illuminate\Support\Facades\Route; use Modules\Settings\Http\Controllers\SettingController;
|
||||
Route::middleware(['auth','verified'])->prefix('administration/settings')->name('administration.settings.')->group(function (): void { Route::get('/',[SettingController::class,'index'])->name('index'); Route::put('/',[SettingController::class,'update'])->name('update'); });
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Users\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
final class UserController extends Controller
|
||||
{
|
||||
public function index() { return view('stylist-theme-admin::administration.users.index', ['users' => User::with('roles')->latest()->paginate(20)]); }
|
||||
public function create() { return view('stylist-theme-admin::administration.users.create', ['roles' => Role::orderBy('name')->pluck('name')]); }
|
||||
public function store(Request $request) {
|
||||
$data=$request->validate(['name'=>['required','string','max:255'],'email'=>['required','email','max:255','unique:users,email'],'password'=>['required','string','min:8','confirmed'],'roles'=>['array'],'roles.*'=>['string','exists:roles,name']]);
|
||||
$user=User::create($data); $user->syncRoles($data['roles'] ?? []);
|
||||
return redirect()->route('administration.users.index')->with('status','User created.');
|
||||
}
|
||||
public function edit(User $user) { return view('stylist-theme-admin::administration.users.edit', ['user'=>$user->load('roles'),'roles'=>Role::orderBy('name')->pluck('name')]); }
|
||||
public function update(Request $request, User $user) {
|
||||
$data=$request->validate(['name'=>['required','string','max:255'],'email'=>['required','email','max:255',Rule::unique('users','email')->ignore($user)],'password'=>['nullable','string','min:8','confirmed'],'roles'=>['array'],'roles.*'=>['string','exists:roles,name']]);
|
||||
if (blank($data['password'] ?? null)) unset($data['password']);
|
||||
$user->update($data); $user->syncRoles($data['roles'] ?? []);
|
||||
return redirect()->route('administration.users.index')->with('status','User updated.');
|
||||
}
|
||||
public function destroy(User $user) {
|
||||
abort_if($user->is(auth()->user()),422,'You cannot delete your own account.');
|
||||
$user->delete(); return redirect()->route('administration.users.index')->with('status','User deleted.');
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
// Module routes are intentionally registered by each feature as it is enabled.
|
||||
use Illuminate\Support\Facades\Route; use Modules\Users\Http\Controllers\UserController;
|
||||
Route::middleware(['auth','verified'])->prefix('administration/users')->name('administration.users.')->group(function (): void { Route::resource('', UserController::class)->parameters([''=>'user'])->except(['show']); });
|
||||
|
||||
+2
-1
@@ -14,6 +14,7 @@ use Illuminate\Support\Str;
|
||||
use Laravel\Fortify\Contracts\PasskeyUser;
|
||||
use Laravel\Fortify\PasskeyAuthenticatable;
|
||||
use Laravel\Fortify\TwoFactorAuthenticatable;
|
||||
use Spatie\Permission\Traits\HasRoles;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
@@ -33,7 +34,7 @@ use Laravel\Fortify\TwoFactorAuthenticatable;
|
||||
class User extends Authenticatable implements PasskeyUser
|
||||
{
|
||||
/** @use HasFactory<UserFactory> */
|
||||
use HasFactory, Notifiable, PasskeyAuthenticatable, TwoFactorAuthenticatable;
|
||||
use HasFactory, HasRoles, Notifiable, PasskeyAuthenticatable, TwoFactorAuthenticatable;
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
|
||||
@@ -12,23 +12,27 @@
|
||||
|
||||
<flux:sidebar.nav>
|
||||
<flux:sidebar.group :heading="__('Platform')" class="grid">
|
||||
<flux:sidebar.item icon="home" :href="route('dashboard')" :current="request()->routeIs('dashboard')" wire:navigate>
|
||||
{{ __('Dashboard') }}
|
||||
</flux:sidebar.item>
|
||||
<flux:sidebar.item icon="home" :href="route('dashboard')" :current="request()->routeIs('dashboard')" wire:navigate>{{ __('Dashboard') }}</flux:sidebar.item>
|
||||
<flux:sidebar.item icon="squares-2x2" :href="route('administration.dashboard')" :current="request()->routeIs('administration.dashboard')" wire:navigate>{{ __('Administration') }}</flux:sidebar.item>
|
||||
</flux:sidebar.group>
|
||||
|
||||
<flux:sidebar.group :heading="__('Management')" class="grid">
|
||||
<flux:sidebar.item icon="users" :href="route('administration.users.index')" :current="request()->routeIs('administration.users.*')" wire:navigate>{{ __('Users') }}</flux:sidebar.item>
|
||||
<flux:sidebar.item icon="shield-check" :href="route('administration.roles.index')" :current="request()->routeIs('administration.roles.*')" wire:navigate>{{ __('Roles & Permissions') }}</flux:sidebar.item>
|
||||
<flux:sidebar.item icon="photo" :href="route('administration.media.index')" :current="request()->routeIs('administration.media.*')" wire:navigate>{{ __('Media') }}</flux:sidebar.item>
|
||||
<flux:sidebar.item icon="magnifying-glass" :href="route('administration.search.index')" :current="request()->routeIs('administration.search.*')" wire:navigate>{{ __('Search') }}</flux:sidebar.item>
|
||||
</flux:sidebar.group>
|
||||
|
||||
<flux:sidebar.group :heading="__('Configuration')" class="grid">
|
||||
<flux:sidebar.item icon="globe-alt" :href="route('administration.seo.index')" :current="request()->routeIs('administration.seo.*')" wire:navigate>{{ __('SEO') }}</flux:sidebar.item>
|
||||
<flux:sidebar.item icon="cog-6-tooth" :href="route('administration.settings.index')" :current="request()->routeIs('administration.settings.*')" wire:navigate>{{ __('Settings') }}</flux:sidebar.item>
|
||||
<flux:sidebar.item icon="cube" :href="route('administration.modules.index')" :current="request()->routeIs('administration.modules.*')" wire:navigate>{{ __('Modules') }}</flux:sidebar.item>
|
||||
<flux:sidebar.item icon="clipboard-document-list" :href="route('administration.audit.index')" :current="request()->routeIs('administration.audit.*')" wire:navigate>{{ __('Audit Log') }}</flux:sidebar.item>
|
||||
</flux:sidebar.group>
|
||||
</flux:sidebar.nav>
|
||||
|
||||
<flux:spacer />
|
||||
|
||||
<flux:sidebar.nav>
|
||||
<flux:sidebar.item icon="folder" href="https://github.com/laravel/livewire-starter-kit" target="_blank">
|
||||
{{ __('Repository') }}
|
||||
</flux:sidebar.item>
|
||||
|
||||
<flux:sidebar.item icon="book-open" href="https://laravel.com/docs/starter-kits#livewire" target="_blank">
|
||||
{{ __('Documentation') }}
|
||||
</flux:sidebar.item>
|
||||
</flux:sidebar.nav>
|
||||
|
||||
<x-stylist-theme-common::desktop-user-menu class="hidden lg:block" :name="auth()->user()->name" />
|
||||
</flux:sidebar>
|
||||
|
||||
Reference in New Issue
Block a user