Initial commit of base infrastructure
This commit is contained in:
commit
747b6b4004
18
.editorconfig
Normal file
18
.editorconfig
Normal file
@ -0,0 +1,18 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 4
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.{yml,yaml}]
|
||||
indent_size = 2
|
||||
|
||||
[docker-compose.yml]
|
||||
indent_size = 4
|
||||
59
.env.example
Normal file
59
.env.example
Normal file
@ -0,0 +1,59 @@
|
||||
APP_NAME=Laravel
|
||||
APP_ENV=local
|
||||
APP_KEY=
|
||||
APP_DEBUG=true
|
||||
APP_URL=http://localhost
|
||||
|
||||
LOG_CHANNEL=stack
|
||||
LOG_DEPRECATIONS_CHANNEL=null
|
||||
LOG_LEVEL=debug
|
||||
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=laravel
|
||||
DB_USERNAME=root
|
||||
DB_PASSWORD=
|
||||
|
||||
BROADCAST_DRIVER=log
|
||||
CACHE_DRIVER=file
|
||||
FILESYSTEM_DISK=local
|
||||
QUEUE_CONNECTION=sync
|
||||
SESSION_DRIVER=file
|
||||
SESSION_LIFETIME=120
|
||||
|
||||
MEMCACHED_HOST=127.0.0.1
|
||||
|
||||
REDIS_HOST=127.0.0.1
|
||||
REDIS_PASSWORD=null
|
||||
REDIS_PORT=6379
|
||||
|
||||
MAIL_MAILER=smtp
|
||||
MAIL_HOST=mailpit
|
||||
MAIL_PORT=1025
|
||||
MAIL_USERNAME=null
|
||||
MAIL_PASSWORD=null
|
||||
MAIL_ENCRYPTION=null
|
||||
MAIL_FROM_ADDRESS="hello@example.com"
|
||||
MAIL_FROM_NAME="${APP_NAME}"
|
||||
|
||||
AWS_ACCESS_KEY_ID=
|
||||
AWS_SECRET_ACCESS_KEY=
|
||||
AWS_DEFAULT_REGION=us-east-1
|
||||
AWS_BUCKET=
|
||||
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||
|
||||
PUSHER_APP_ID=
|
||||
PUSHER_APP_KEY=
|
||||
PUSHER_APP_SECRET=
|
||||
PUSHER_HOST=
|
||||
PUSHER_PORT=443
|
||||
PUSHER_SCHEME=https
|
||||
PUSHER_APP_CLUSTER=mt1
|
||||
|
||||
VITE_APP_NAME="${APP_NAME}"
|
||||
VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
|
||||
VITE_PUSHER_HOST="${PUSHER_HOST}"
|
||||
VITE_PUSHER_PORT="${PUSHER_PORT}"
|
||||
VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
|
||||
VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
|
||||
11
.gitattributes
vendored
Normal file
11
.gitattributes
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
* text=auto eol=lf
|
||||
|
||||
*.blade.php diff=html
|
||||
*.css diff=css
|
||||
*.html diff=html
|
||||
*.md diff=markdown
|
||||
*.php diff=php
|
||||
|
||||
/.github export-ignore
|
||||
CHANGELOG.md export-ignore
|
||||
.styleci.yml export-ignore
|
||||
19
.gitignore
vendored
Normal file
19
.gitignore
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
/.phpunit.cache
|
||||
/node_modules
|
||||
/public/build
|
||||
/public/hot
|
||||
/public/storage
|
||||
/storage/*.key
|
||||
/vendor
|
||||
.env
|
||||
.env.backup
|
||||
.env.production
|
||||
.phpunit.result.cache
|
||||
Homestead.json
|
||||
Homestead.yaml
|
||||
auth.json
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
/.fleet
|
||||
/.idea
|
||||
/.vscode
|
||||
20
README.md
Normal file
20
README.md
Normal file
@ -0,0 +1,20 @@
|
||||
# New Infrastructure
|
||||
|
||||
A Laravel 11 based infrastructure providing component-based architecture for building web applications with:
|
||||
- Admin interface for project management
|
||||
- API infrastructure for mobile applications
|
||||
- Data extraction components for third-party integration
|
||||
|
||||
## Status
|
||||
This project is under active development and is not yet ready for production use.
|
||||
|
||||
## Requirements
|
||||
- PHP 8.3+
|
||||
- Laravel 11
|
||||
- PostgreSQL 17
|
||||
|
||||
## Disclaimer
|
||||
This software is provided as-is, without any guarantees or warranties. Use at your own risk. While issues and pull requests are welcome, there is no guarantee of timely responses or updates.
|
||||
|
||||
## License
|
||||
MIT
|
||||
47
app/Components/Admin/Helpers/AuthorizationHelper.php
Normal file
47
app/Components/Admin/Helpers/AuthorizationHelper.php
Normal file
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Components\Admin\Helpers;
|
||||
|
||||
class AuthorizationHelper
|
||||
{
|
||||
/**
|
||||
* List of system-critical permissions that require extra protection
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static array $criticalPermissions = [
|
||||
'manage_system_settings',
|
||||
'manage_security',
|
||||
'manage_roles',
|
||||
'manage_permissions',
|
||||
'manage_users',
|
||||
'manage_authentication',
|
||||
'manage_authorization'
|
||||
];
|
||||
|
||||
/**
|
||||
* Check if a permission is considered system-critical
|
||||
*
|
||||
* @param string $permissionName
|
||||
* @return bool
|
||||
*/
|
||||
public static function isSystemCriticalPermission(string $permissionName): bool
|
||||
{
|
||||
return in_array(
|
||||
strtolower($permissionName),
|
||||
self::$criticalPermissions
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given role name is a system role
|
||||
*
|
||||
* @param string $roleName
|
||||
* @return bool
|
||||
*/
|
||||
public static function isSystemRole(string $roleName): bool
|
||||
{
|
||||
$systemRoles = ['administrator', 'super admin', 'system admin'];
|
||||
return in_array(strtolower($roleName), $systemRoles);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
namespace App\Components\Admin\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use App\Models\User;
|
||||
use App\Models\Role;
|
||||
use App\Models\ResourceType;
|
||||
use App\Models\ResourceTypeMapping;
|
||||
use Illuminate\View\View;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class AdminDashboardController extends Controller
|
||||
{
|
||||
public function index(): View
|
||||
{
|
||||
// System Statistics
|
||||
$systemStats = [
|
||||
'users_count' => User::count(),
|
||||
'roles_count' => Role::count(),
|
||||
'user_roles_count' => DB::table('auth.user_roles')->count(),
|
||||
'resources_count' => ResourceType::count(),
|
||||
'resource_mappings_count' => ResourceTypeMapping::count(),
|
||||
];
|
||||
|
||||
// Database Statistics
|
||||
$dbStats = DB::select("
|
||||
SELECT
|
||||
pg_size_pretty(pg_database_size(current_database())) as db_size,
|
||||
(SELECT count(*) FROM information_schema.schemata
|
||||
WHERE schema_name NOT IN ('information_schema', 'pg_catalog')) as schema_count,
|
||||
(SELECT count(*) FROM information_schema.tables
|
||||
WHERE table_schema NOT IN ('information_schema', 'pg_catalog')
|
||||
AND table_type = 'BASE TABLE') as table_count,
|
||||
(SELECT count(*) FROM information_schema.views
|
||||
WHERE table_schema NOT IN ('information_schema', 'pg_catalog')
|
||||
AND table_name NOT LIKE 'pg_%') as view_count,
|
||||
(SELECT count(*) FROM pg_matviews) as materialized_view_count,
|
||||
(SELECT count(*) FROM pg_indexes
|
||||
WHERE schemaname NOT IN ('information_schema', 'pg_catalog')) as index_count
|
||||
");
|
||||
|
||||
return view('admin::admin.dashboard', compact('systemStats', 'dbStats'));
|
||||
}
|
||||
|
||||
public function getDatabaseIO(): JsonResponse
|
||||
{
|
||||
// Get current stats
|
||||
$stats = DB::select("
|
||||
SELECT
|
||||
blks_read,
|
||||
blks_hit,
|
||||
tup_returned,
|
||||
tup_fetched,
|
||||
tup_inserted,
|
||||
tup_updated,
|
||||
tup_deleted,
|
||||
EXTRACT(EPOCH FROM now()) as timestamp,
|
||||
xact_commit,
|
||||
xact_rollback
|
||||
FROM pg_stat_database
|
||||
WHERE datname = current_database()
|
||||
");
|
||||
|
||||
// Store the stats in cache with timestamp
|
||||
$previousStats = cache()->get('database_stats');
|
||||
$currentStats = $stats[0];
|
||||
cache()->put('database_stats', $currentStats, now()->addMinutes(5));
|
||||
|
||||
// If we have previous stats, calculate the differences
|
||||
if ($previousStats) {
|
||||
$timeDiff = $currentStats->timestamp - $previousStats->timestamp;
|
||||
|
||||
$response = [
|
||||
// Calculate rates per second
|
||||
'blks_read' => ($currentStats->blks_read - $previousStats->blks_read) / $timeDiff,
|
||||
'blks_hit' => ($currentStats->blks_hit - $previousStats->blks_hit) / $timeDiff,
|
||||
'tup_returned' => ($currentStats->tup_returned - $previousStats->tup_returned) / $timeDiff,
|
||||
'tup_inserted' => ($currentStats->tup_inserted - $previousStats->tup_inserted) / $timeDiff,
|
||||
'tup_updated' => ($currentStats->tup_updated - $previousStats->tup_updated) / $timeDiff,
|
||||
'tup_deleted' => ($currentStats->tup_deleted - $previousStats->tup_deleted) / $timeDiff,
|
||||
'xact_commit' => ($currentStats->xact_commit - $previousStats->xact_commit) / $timeDiff,
|
||||
'xact_rollback' => ($currentStats->xact_rollback - $previousStats->xact_rollback) / $timeDiff,
|
||||
];
|
||||
} else {
|
||||
// For the first call, return zeros
|
||||
$response = [
|
||||
'blks_read' => 0,
|
||||
'blks_hit' => 0,
|
||||
'tup_returned' => 0,
|
||||
'tup_inserted' => 0,
|
||||
'tup_updated' => 0,
|
||||
'tup_deleted' => 0,
|
||||
'xact_commit' => 0,
|
||||
'xact_rollback' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
return response()->json($response);
|
||||
}
|
||||
}
|
||||
153
app/Components/Admin/Http/Controllers/MenuTypesController.php
Normal file
153
app/Components/Admin/Http/Controllers/MenuTypesController.php
Normal file
@ -0,0 +1,153 @@
|
||||
<?php
|
||||
|
||||
namespace App\Components\Admin\Http\Controllers;
|
||||
|
||||
use App\Components\Admin\Http\Requests\MenuTypeRequest;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\MenuType;
|
||||
use App\Traits\WebPageAuthorization;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class MenuTypesController extends Controller
|
||||
{
|
||||
use WebPageAuthorization;
|
||||
|
||||
private const RESOURCE_TYPE = 'web_pages';
|
||||
private const RESOURCE_VALUE = 'admin.menu-types.php';
|
||||
|
||||
protected string $resourceType;
|
||||
protected string $resourceValue;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->resourceType = self::RESOURCE_TYPE;
|
||||
$this->resourceValue = self::RESOURCE_VALUE;
|
||||
}
|
||||
|
||||
public function index(Request $request): View|RedirectResponse
|
||||
{
|
||||
if (!$this->checkResourcePermission($this->resourceType, $this->resourceValue, 'view')) {
|
||||
abort(403, 'Unauthorized to view menu types');
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($request) {
|
||||
$menuTypes = MenuType::query()
|
||||
->search($request->input('search'))
|
||||
->sort(
|
||||
$request->input('sort', 'name'),
|
||||
$request->input('direction', 'asc')
|
||||
)
|
||||
->withCount('navigationItems')
|
||||
->paginate(10)
|
||||
->withQueryString();
|
||||
|
||||
return view('admin::menu-types.index', [
|
||||
'menuTypes' => $menuTypes,
|
||||
'thisResourceType' => $this->resourceType,
|
||||
'thisResourceValue' => $this->resourceValue
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
public function create(): View
|
||||
{
|
||||
if (!$this->checkResourcePermission($this->resourceType, $this->resourceValue, 'create')) {
|
||||
abort(403, 'Unauthorized to create menu types');
|
||||
}
|
||||
|
||||
return view('admin::menu-types.create', [
|
||||
'thisResourceType' => $this->resourceType,
|
||||
'thisResourceValue' => $this->resourceValue
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(MenuTypeRequest $request): RedirectResponse
|
||||
{
|
||||
if (!$this->checkResourcePermission($this->resourceType, $this->resourceValue, 'create')) {
|
||||
abort(403, 'Unauthorized to create menu types');
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($request) {
|
||||
$menuType = MenuType::create($request->validated());
|
||||
|
||||
Log::info('Menu type created successfully', [
|
||||
'id' => $menuType->id,
|
||||
'name' => $menuType->name,
|
||||
'created_by' => auth()->id()
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('admin.menu-types.index')
|
||||
->with('success', 'Menu type created successfully');
|
||||
});
|
||||
}
|
||||
|
||||
public function edit(MenuType $menuType): View
|
||||
{
|
||||
if (!$this->checkResourcePermission($this->resourceType, $this->resourceValue, 'edit')) {
|
||||
abort(403, 'Unauthorized to edit menu types');
|
||||
}
|
||||
|
||||
return view('admin::menu-types.edit', [
|
||||
'menuType' => $menuType,
|
||||
'thisResourceType' => $this->resourceType,
|
||||
'thisResourceValue' => $this->resourceValue
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(MenuTypeRequest $request, MenuType $menuType): RedirectResponse
|
||||
{
|
||||
if (!$this->checkResourcePermission($this->resourceType, $this->resourceValue, 'edit')) {
|
||||
abort(403, 'Unauthorized to edit menu types');
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($request, $menuType) {
|
||||
$menuType->update($request->validated());
|
||||
|
||||
Log::info('Menu type updated successfully', [
|
||||
'id' => $menuType->id,
|
||||
'name' => $menuType->name,
|
||||
'updated_by' => auth()->id()
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('admin.menu-types.index')
|
||||
->with('success', 'Menu type updated successfully');
|
||||
});
|
||||
}
|
||||
|
||||
public function destroy(MenuType $menuType): RedirectResponse
|
||||
{
|
||||
if (!$this->checkResourcePermission($this->resourceType, $this->resourceValue, 'delete')) {
|
||||
abort(403, 'Unauthorized to delete menu types');
|
||||
}
|
||||
|
||||
if ($menuType->navigationItems()->exists()) {
|
||||
return back()->withErrors([
|
||||
'error' => 'Cannot delete menu type: It has associated navigation items'
|
||||
]);
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($menuType) {
|
||||
$menuTypeDetails = [
|
||||
'id' => $menuType->id,
|
||||
'name' => $menuType->name
|
||||
];
|
||||
|
||||
$menuType->delete();
|
||||
|
||||
Log::info('Menu type deleted successfully', [
|
||||
...$menuTypeDetails,
|
||||
'deleted_by' => auth()->id()
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('admin.menu-types.index')
|
||||
->with('success', 'Menu type deleted successfully');
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace App\Components\Admin\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Migration;
|
||||
use App\Traits\WebPageAuthorization;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class MigrationsController extends Controller
|
||||
{
|
||||
use WebPageAuthorization;
|
||||
|
||||
private const RESOURCE_TYPE = 'web_pages';
|
||||
private const RESOURCE_VALUE = 'admin.migrations.php';
|
||||
|
||||
protected string $resourceType;
|
||||
protected string $resourceValue;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->resourceType = self::RESOURCE_TYPE;
|
||||
$this->resourceValue = self::RESOURCE_VALUE;
|
||||
}
|
||||
|
||||
public function index(Request $request): View|RedirectResponse
|
||||
{
|
||||
if (!$this->checkResourcePermission($this->resourceType, $this->resourceValue, 'view')) {
|
||||
abort(403, 'Unauthorized to view migrations');
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($request) {
|
||||
$migrations = Migration::query()
|
||||
->when($request->input('search'), function ($query, $search) {
|
||||
return $query->search($search);
|
||||
})
|
||||
->orderBy($request->input('sort', 'id'), $request->input('direction', 'desc'))
|
||||
->paginate(10)
|
||||
->withQueryString();
|
||||
|
||||
return view('admin::migrations.index', [
|
||||
'migrations' => $migrations,
|
||||
'thisResourceType' => $this->resourceType,
|
||||
'thisResourceValue' => $this->resourceValue
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
public function show(int $id): View|RedirectResponse
|
||||
{
|
||||
if (!$this->checkResourcePermission($this->resourceType, $this->resourceValue, 'view')) {
|
||||
abort(403, 'Unauthorized to view migration details');
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($id) {
|
||||
$migration = Migration::findOrFail($id);
|
||||
|
||||
Log::info('Migration details accessed', [
|
||||
'migration_id' => $id,
|
||||
'user_id' => auth()->id()
|
||||
]);
|
||||
|
||||
return view('admin::migrations.show', [
|
||||
'migration' => $migration,
|
||||
'thisResourceType' => $this->resourceType,
|
||||
'thisResourceValue' => $this->resourceValue
|
||||
]);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,171 @@
|
||||
<?php
|
||||
|
||||
namespace App\Components\Admin\Http\Controllers;
|
||||
|
||||
use App\Components\Admin\Http\Requests\NavigationItemRequest;
|
||||
use App\Helpers\RouteHelper;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\MenuType;
|
||||
use App\Models\NavigationItem;
|
||||
use App\Traits\WebPageAuthorization;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class NavigationItemsController extends Controller
|
||||
{
|
||||
use WebPageAuthorization;
|
||||
|
||||
private const RESOURCE_TYPE = 'web_pages';
|
||||
private const RESOURCE_VALUE = 'admin.navigation-items.php';
|
||||
|
||||
protected string $resourceType;
|
||||
protected string $resourceValue;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->resourceType = self::RESOURCE_TYPE;
|
||||
$this->resourceValue = self::RESOURCE_VALUE;
|
||||
}
|
||||
|
||||
public function index(Request $request): View|RedirectResponse
|
||||
{
|
||||
if (!$this->checkResourcePermission($this->resourceType, $this->resourceValue, 'view')) {
|
||||
abort(403, 'Unauthorized to view navigation items');
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($request) {
|
||||
$navItems = NavigationItem::query()
|
||||
->with(['menuType', 'parent'])
|
||||
->search($request->input('search'))
|
||||
->byMenuType($request->input('menu_type_id'))
|
||||
->ordered()
|
||||
->paginate(10)
|
||||
->withQueryString();
|
||||
|
||||
$menuTypes = MenuType::orderBy('name')->get();
|
||||
$activeNavItems = NavigationItem::active()->ordered()->get();
|
||||
|
||||
return view('admin::navigation-items.index', [
|
||||
'navItems' => $navItems,
|
||||
'menuTypes' => $menuTypes,
|
||||
'activeNavItems' => $activeNavItems,
|
||||
'thisResourceType' => $this->resourceType,
|
||||
'thisResourceValue' => $this->resourceValue
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
public function create(): View
|
||||
{
|
||||
if (!$this->checkResourcePermission($this->resourceType, $this->resourceValue, 'create')) {
|
||||
abort(403, 'Unauthorized to create navigation items');
|
||||
}
|
||||
|
||||
return DB::transaction(function () {
|
||||
return view('admin::navigation-items.create', [
|
||||
'menuTypes' => MenuType::orderBy('name')->get(),
|
||||
'parentItems' => NavigationItem::whereNull('parent_id')
|
||||
->orderBy('name')
|
||||
->get(),
|
||||
'activeNavItems' => NavigationItem::active()->ordered()->get(),
|
||||
'availableRoutes' => RouteHelper::getAdminNamedRoutes(),
|
||||
'thisResourceType' => $this->resourceType,
|
||||
'thisResourceValue' => $this->resourceValue
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
public function store(NavigationItemRequest $request): RedirectResponse
|
||||
{
|
||||
if (!$this->checkResourcePermission($this->resourceType, $this->resourceValue, 'create')) {
|
||||
abort(403, 'Unauthorized to create navigation items');
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($request) {
|
||||
$navigationItem = NavigationItem::create($request->validated());
|
||||
|
||||
Log::info('Navigation item created successfully', [
|
||||
'id' => $navigationItem->id,
|
||||
'name' => $navigationItem->name,
|
||||
'user_id' => auth()->id()
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('admin.navigation-items.index')
|
||||
->with('success', 'Navigation item created successfully');
|
||||
});
|
||||
}
|
||||
|
||||
public function edit(NavigationItem $navigationItem): View
|
||||
{
|
||||
if (!$this->checkResourcePermission($this->resourceType, $this->resourceValue, 'edit')) {
|
||||
abort(403, 'Unauthorized to edit navigation items');
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($navigationItem) {
|
||||
return view('admin::navigation-items.edit', [
|
||||
'navigationItem' => $navigationItem,
|
||||
'menuTypes' => MenuType::orderBy('name')->get(),
|
||||
'parentItems' => NavigationItem::where('id', '!=', $navigationItem->id)
|
||||
->whereNull('parent_id')
|
||||
->orderBy('name')
|
||||
->get(),
|
||||
'activeNavItems' => NavigationItem::active()->ordered()->get(),
|
||||
'availableRoutes' => RouteHelper::getAdminNamedRoutes(),
|
||||
'thisResourceType' => $this->resourceType,
|
||||
'thisResourceValue' => $this->resourceValue
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
public function update(NavigationItemRequest $request, NavigationItem $navigationItem): RedirectResponse
|
||||
{
|
||||
if (!$this->checkResourcePermission($this->resourceType, $this->resourceValue, 'edit')) {
|
||||
abort(403, 'Unauthorized to edit navigation items');
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($request, $navigationItem) {
|
||||
$navigationItem->update($request->validated());
|
||||
|
||||
Log::info('Navigation item updated successfully', [
|
||||
'id' => $navigationItem->id,
|
||||
'name' => $navigationItem->name,
|
||||
'user_id' => auth()->id()
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('admin.navigation-items.index')
|
||||
->with('success', 'Navigation item updated successfully');
|
||||
});
|
||||
}
|
||||
|
||||
public function destroy(NavigationItem $navigationItem): RedirectResponse
|
||||
{
|
||||
if (!$this->checkResourcePermission($this->resourceType, $this->resourceValue, 'delete')) {
|
||||
abort(403, 'Unauthorized to delete navigation items');
|
||||
}
|
||||
|
||||
if ($navigationItem->children()->exists()) {
|
||||
return back()->withErrors([
|
||||
'error' => 'Cannot delete navigation item with child items'
|
||||
]);
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($navigationItem) {
|
||||
$navigationItem->delete();
|
||||
|
||||
Log::info('Navigation item deleted successfully', [
|
||||
'id' => $navigationItem->id,
|
||||
'name' => $navigationItem->name,
|
||||
'user_id' => auth()->id()
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('admin.navigation-items.index')
|
||||
->with('success', 'Navigation item deleted successfully');
|
||||
});
|
||||
}
|
||||
}
|
||||
147
app/Components/Admin/Http/Controllers/PermissionsController.php
Normal file
147
app/Components/Admin/Http/Controllers/PermissionsController.php
Normal file
@ -0,0 +1,147 @@
|
||||
<?php
|
||||
|
||||
namespace App\Components\Admin\Http\Controllers;
|
||||
|
||||
use App\Components\Admin\Http\Requests\PermissionRequest;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Permission;
|
||||
use App\Traits\WebPageAuthorization;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class PermissionsController extends Controller
|
||||
{
|
||||
use WebPageAuthorization;
|
||||
|
||||
private const RESOURCE_TYPE = 'web_pages';
|
||||
private const RESOURCE_VALUE = 'admin.permissions.php';
|
||||
|
||||
protected string $resourceType;
|
||||
protected string $resourceValue;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->resourceType = self::RESOURCE_TYPE;
|
||||
$this->resourceValue = self::RESOURCE_VALUE;
|
||||
}
|
||||
|
||||
public function index(Request $request): View|RedirectResponse
|
||||
{
|
||||
if (!$this->checkResourcePermission($this->resourceType, $this->resourceValue, 'view')) {
|
||||
abort(403, 'Unauthorized to view permissions');
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($request) {
|
||||
$permissions = Permission::query()
|
||||
->search($request->input('search'))
|
||||
->orderBy($request->input('sort', 'name'))
|
||||
->paginate(10)
|
||||
->withQueryString();
|
||||
|
||||
return view('admin::permissions.index', [
|
||||
'permissions' => $permissions,
|
||||
'thisResourceType' => $this->resourceType,
|
||||
'thisResourceValue' => $this->resourceValue
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
public function create(): View
|
||||
{
|
||||
if (!$this->checkResourcePermission($this->resourceType, $this->resourceValue, 'create')) {
|
||||
abort(403, 'Unauthorized to create permissions');
|
||||
}
|
||||
|
||||
return view('admin::permissions.create', [
|
||||
'thisResourceType' => $this->resourceType,
|
||||
'thisResourceValue' => $this->resourceValue
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(PermissionRequest $request): RedirectResponse
|
||||
{
|
||||
if (!$this->checkResourcePermission($this->resourceType, $this->resourceValue, 'create')) {
|
||||
abort(403, 'Unauthorized to create permissions');
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($request) {
|
||||
$permission = Permission::create($request->validated());
|
||||
|
||||
Log::info('Permission created successfully', [
|
||||
'id' => $permission->id,
|
||||
'name' => $permission->name,
|
||||
'user_id' => auth()->id()
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('admin.permissions.index')
|
||||
->with('success', 'Permission created successfully');
|
||||
});
|
||||
}
|
||||
|
||||
public function edit(Permission $permission): View
|
||||
{
|
||||
if (!$this->checkResourcePermission($this->resourceType, $this->resourceValue, 'edit')) {
|
||||
abort(403, 'Unauthorized to edit this permission');
|
||||
}
|
||||
|
||||
return view('admin::permissions.edit', [
|
||||
'permission' => $permission,
|
||||
'thisResourceType' => $this->resourceType,
|
||||
'thisResourceValue' => $this->resourceValue
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(PermissionRequest $request, Permission $permission): RedirectResponse
|
||||
{
|
||||
if (!$this->checkResourcePermission($this->resourceType, $this->resourceValue, 'edit')) {
|
||||
abort(403, 'Unauthorized to edit this permission');
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($request, $permission) {
|
||||
$permission->update($request->validated());
|
||||
|
||||
Log::info('Permission updated successfully', [
|
||||
'id' => $permission->id,
|
||||
'name' => $permission->name,
|
||||
'user_id' => auth()->id()
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('admin.permissions.index')
|
||||
->with('success', 'Permission updated successfully');
|
||||
});
|
||||
}
|
||||
|
||||
public function destroy(Permission $permission): RedirectResponse
|
||||
{
|
||||
if (!$this->checkResourcePermission($this->resourceType, $this->resourceValue, 'delete')) {
|
||||
abort(403, 'Unauthorized to delete permissions');
|
||||
}
|
||||
|
||||
if ($permission->roles()->exists()) {
|
||||
return back()->withErrors([
|
||||
'error' => 'Cannot delete permission that is assigned to roles'
|
||||
]);
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($permission) {
|
||||
$pageInfo = [
|
||||
'id' => $permission->id,
|
||||
'name' => $permission->name,
|
||||
'user_id' => auth()->id()
|
||||
];
|
||||
|
||||
$permission->delete();
|
||||
|
||||
Log::info('Permission deleted successfully', $pageInfo);
|
||||
|
||||
return redirect()
|
||||
->route('admin.permissions.index')
|
||||
->with('success', 'Permission deleted successfully');
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,252 @@
|
||||
<?php
|
||||
|
||||
namespace App\Components\Admin\Http\Controllers;
|
||||
|
||||
use App\Components\Admin\Http\Requests\ResourceAssociationRequest;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\ResourceAssociation;
|
||||
use App\Models\ResourceType;
|
||||
use App\Models\Role;
|
||||
use App\Models\User;
|
||||
use App\Traits\WebPageAuthorization;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class ResourceAssociationsController extends Controller
|
||||
{
|
||||
use WebPageAuthorization;
|
||||
|
||||
private const RESOURCE_TYPE = 'web_pages';
|
||||
private const RESOURCE_VALUE = 'admin.resource-associations.php';
|
||||
|
||||
protected string $resourceType;
|
||||
protected string $resourceValue;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->resourceType = self::RESOURCE_TYPE;
|
||||
$this->resourceValue = self::RESOURCE_VALUE;
|
||||
}
|
||||
|
||||
public function index(Request $request): View|RedirectResponse
|
||||
{
|
||||
if (!$this->checkResourcePermission($this->resourceType, $this->resourceValue, 'view')) {
|
||||
abort(403, 'Unauthorized to view resource associations');
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($request) {
|
||||
$resourceAssociations = ResourceAssociation::with(['user', 'resourceType', 'role'])
|
||||
->search($request->input('search'))
|
||||
->sort(
|
||||
$request->input('sort', 'created_at'),
|
||||
$request->input('direction', 'desc')
|
||||
)
|
||||
->paginate(10)
|
||||
->withQueryString();
|
||||
|
||||
$resourceMappings = DB::table('auth.tbl_resource_type_mappings')
|
||||
->pluck('resource_value_column', 'resource_type_id');
|
||||
|
||||
foreach ($resourceAssociations as $association) {
|
||||
$this->loadResourceValue($association, $resourceMappings);
|
||||
}
|
||||
|
||||
return view('admin::resource-associations.index', [
|
||||
'resourceAssociations' => $resourceAssociations,
|
||||
'thisResourceType' => $this->resourceType,
|
||||
'thisResourceValue' => $this->resourceValue
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
public function create(): View
|
||||
{
|
||||
if (!$this->checkResourcePermission($this->resourceType, $this->resourceValue, 'create')) {
|
||||
abort(403, 'Unauthorized to create resource associations');
|
||||
}
|
||||
|
||||
return DB::transaction(function () {
|
||||
return view('admin::resource-associations.create', [
|
||||
'users' => User::where('active', true)->orderBy('name')->get(),
|
||||
'resourceTypes' => ResourceType::orderBy('name')->get(),
|
||||
'thisResourceType' => $this->resourceType,
|
||||
'thisResourceValue' => $this->resourceValue
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
public function store(ResourceAssociationRequest $request): RedirectResponse
|
||||
{
|
||||
if (!$this->checkResourcePermission($this->resourceType, $this->resourceValue, 'create')) {
|
||||
abort(403, 'Unauthorized to create resource associations');
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($request) {
|
||||
$resourceAssociation = ResourceAssociation::create($request->validated());
|
||||
|
||||
Log::info('Resource association created successfully', [
|
||||
'id' => $resourceAssociation->id,
|
||||
'user_id' => $resourceAssociation->user_id,
|
||||
'resource_type_id' => $resourceAssociation->resource_type_id,
|
||||
'role_id' => $resourceAssociation->role_id
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('admin.resource-associations.index')
|
||||
->with('success', 'Resource association created successfully');
|
||||
});
|
||||
}
|
||||
|
||||
public function edit(ResourceAssociation $resourceAssociation): View
|
||||
{
|
||||
if (!$this->checkResourcePermission($this->resourceType, $this->resourceValue, 'edit')) {
|
||||
abort(403, 'Unauthorized to edit resource associations');
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($resourceAssociation) {
|
||||
$resources = [];
|
||||
if ($resourceAssociation->resource_type_id) {
|
||||
$resources = DB::select(
|
||||
"SELECT * FROM auth.get_resource_query(?)",
|
||||
[$resourceAssociation->resource_type_id]
|
||||
);
|
||||
}
|
||||
|
||||
return view('admin::resource-associations.edit', [
|
||||
'resourceAssociation' => $resourceAssociation,
|
||||
'users' => User::where('active', true)->orderBy('name')->get(),
|
||||
'resourceTypes' => ResourceType::orderBy('name')->get(),
|
||||
'roles' => Role::orderBy('name')->get(),
|
||||
'resources' => $resources,
|
||||
'thisResourceType' => $this->resourceType,
|
||||
'thisResourceValue' => $this->resourceValue
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
public function update(ResourceAssociationRequest $request, ResourceAssociation $resourceAssociation): RedirectResponse
|
||||
{
|
||||
if (!$this->checkResourcePermission($this->resourceType, $this->resourceValue, 'edit')) {
|
||||
abort(403, 'Unauthorized to edit resource associations');
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($request, $resourceAssociation) {
|
||||
$resourceAssociation->update($request->validated());
|
||||
|
||||
Log::info('Resource association updated successfully', [
|
||||
'id' => $resourceAssociation->id,
|
||||
'user_id' => $resourceAssociation->user_id,
|
||||
'resource_type_id' => $resourceAssociation->resource_type_id,
|
||||
'role_id' => $resourceAssociation->role_id
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('admin.resource-associations.index')
|
||||
->with('success', 'Resource association updated successfully');
|
||||
});
|
||||
}
|
||||
|
||||
public function destroy(ResourceAssociation $resourceAssociation): RedirectResponse
|
||||
{
|
||||
if (!$this->checkResourcePermission($this->resourceType, $this->resourceValue, 'delete')) {
|
||||
abort(403, 'Unauthorized to delete resource associations');
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($resourceAssociation) {
|
||||
$resourceAssociation->delete();
|
||||
|
||||
Log::info('Resource association deleted successfully', [
|
||||
'id' => $resourceAssociation->id,
|
||||
'user_id' => $resourceAssociation->user_id,
|
||||
'resource_type_id' => $resourceAssociation->resource_type_id,
|
||||
'role_id' => $resourceAssociation->role_id
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('admin.resource-associations.index')
|
||||
->with('success', 'Resource association deleted successfully');
|
||||
});
|
||||
}
|
||||
|
||||
public function getRoles(Request $request): JsonResponse
|
||||
{
|
||||
if (!$this->checkResourcePermission($this->resourceType, $this->resourceValue, 'view')) {
|
||||
return response()->json(['error' => 'Unauthorized'], 403);
|
||||
}
|
||||
|
||||
$userId = $request->get('user_id');
|
||||
if (!$userId) {
|
||||
return response()->json(['error' => 'User ID is required'], 400);
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($userId) {
|
||||
$roles = DB::table('auth.tbl_user_roles as ur')
|
||||
->join('auth.tbl_roles as r', 'ur.role_id', '=', 'r.id')
|
||||
->where('ur.user_id', $userId)
|
||||
->whereNull('ur.deleted_at')
|
||||
->select('r.id', 'r.name', 'r.description')
|
||||
->orderBy('r.name')
|
||||
->get();
|
||||
|
||||
return response()->json($roles);
|
||||
});
|
||||
}
|
||||
|
||||
public function getResources(Request $request): JsonResponse
|
||||
{
|
||||
if (!$this->checkResourcePermission($this->resourceType, $this->resourceValue, 'view')) {
|
||||
return response()->json(['error' => 'Unauthorized'], 403);
|
||||
}
|
||||
|
||||
$resourceTypeId = $request->get('resource_type_id');
|
||||
if (!$resourceTypeId) {
|
||||
return response()->json(['error' => 'Resource type ID is required'], 400);
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($resourceTypeId) {
|
||||
$mapping = DB::table('auth.tbl_resource_type_mappings')
|
||||
->where('resource_type_id', $resourceTypeId)
|
||||
->first();
|
||||
|
||||
if (!$mapping) {
|
||||
return response()->json(['error' => 'No mapping found for this resource type'], 404);
|
||||
}
|
||||
|
||||
$query = "SELECT id, {$mapping->resource_value_column} as value
|
||||
FROM {$mapping->table_schema}.{$mapping->table_name}
|
||||
WHERE deleted_at IS NULL
|
||||
ORDER BY {$mapping->resource_value_column}";
|
||||
|
||||
return response()->json(DB::select($query));
|
||||
});
|
||||
}
|
||||
|
||||
private function loadResourceValue(ResourceAssociation $association, $resourceMappings): void
|
||||
{
|
||||
if ($association->resource_id && isset($resourceMappings[$association->resource_type_id])) {
|
||||
$mapping = DB::table('auth.tbl_resource_type_mappings')
|
||||
->where('resource_type_id', $association->resource_type_id)
|
||||
->first();
|
||||
|
||||
if ($mapping) {
|
||||
$query = "SELECT {$mapping->resource_value_column} as value
|
||||
FROM {$mapping->table_schema}.{$mapping->table_name}
|
||||
WHERE id = ?";
|
||||
|
||||
$result = DB::selectOne($query, [$association->resource_id]);
|
||||
$association->resource_value = $result
|
||||
? "{$mapping->resource_value_column}: {$result->value}"
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isset($association->resource_value)) {
|
||||
$mapping = $resourceMappings[$association->resource_type_id] ?? null;
|
||||
$association->resource_value = $mapping ? "{$mapping}: all" : 'all';
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,234 @@
|
||||
<?php
|
||||
|
||||
namespace App\Components\Admin\Http\Controllers;
|
||||
|
||||
use App\Components\Admin\Http\Requests\ResourceTypeMappingRequest;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\ResourceType;
|
||||
use App\Models\ResourceTypeMapping;
|
||||
use App\Traits\WebPageAuthorization;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class ResourceTypeMappingsController extends Controller
|
||||
{
|
||||
use WebPageAuthorization;
|
||||
|
||||
private const RESOURCE_TYPE = 'web_pages';
|
||||
private const RESOURCE_VALUE = 'admin.resource-type-mappings.php';
|
||||
|
||||
protected string $resourceType;
|
||||
protected string $resourceValue;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->resourceType = self::RESOURCE_TYPE;
|
||||
$this->resourceValue = self::RESOURCE_VALUE;
|
||||
}
|
||||
|
||||
public function index(Request $request): View|RedirectResponse
|
||||
{
|
||||
if (!$this->checkResourcePermission($this->resourceType, $this->resourceValue, 'view')) {
|
||||
abort(403, 'Unauthorized to view resource type mappings');
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($request) {
|
||||
$resourceTypeMappings = ResourceTypeMapping::query()
|
||||
->with('resourceType')
|
||||
->search($request->input('search'))
|
||||
->bySchema($request->input('schema'))
|
||||
->ordered($request->input('sort'), $request->input('direction'))
|
||||
->paginate(10)
|
||||
->withQueryString();
|
||||
|
||||
return view('admin::resource-type-mappings.index', [
|
||||
'resource_type_mappings' => $resourceTypeMappings,
|
||||
'thisResourceType' => $this->resourceType,
|
||||
'thisResourceValue' => $this->resourceValue
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
public function create(): View
|
||||
{
|
||||
if (!$this->checkResourcePermission($this->resourceType, $this->resourceValue, 'create')) {
|
||||
abort(403, 'Unauthorized to create resource type mappings');
|
||||
}
|
||||
|
||||
return DB::transaction(function () {
|
||||
$resourceTypes = ResourceType::whereNotIn('id', function($query) {
|
||||
$query->select('resource_type_id')
|
||||
->from('auth.tbl_resource_type_mappings')
|
||||
->whereNull('deleted_at');
|
||||
})->orderBy('name')->get();
|
||||
|
||||
return view('admin::resource-type-mappings.create', [
|
||||
'resourceTypes' => $resourceTypes,
|
||||
'schemas' => $this->getAvailableSchemas(),
|
||||
'thisResourceType' => $this->resourceType,
|
||||
'thisResourceValue' => $this->resourceValue
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
public function store(ResourceTypeMappingRequest $request): RedirectResponse
|
||||
{
|
||||
if (!$this->checkResourcePermission($this->resourceType, $this->resourceValue, 'create')) {
|
||||
abort(403, 'Unauthorized to create resource type mappings');
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($request) {
|
||||
$resourceTypeMapping = ResourceTypeMapping::create($request->validated());
|
||||
|
||||
Log::info('Resource type mapping created successfully', [
|
||||
'resource_type_id' => $resourceTypeMapping->resource_type_id,
|
||||
'table' => $resourceTypeMapping->getFullTableName(),
|
||||
'user_id' => auth()->id()
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('admin.resource-type-mappings.index')
|
||||
->with('success', 'Resource type mapping created successfully');
|
||||
});
|
||||
}
|
||||
|
||||
public function edit(ResourceTypeMapping $resourceTypeMapping): View
|
||||
{
|
||||
if (!$this->checkResourcePermission($this->resourceType, $this->resourceValue, 'edit')) {
|
||||
abort(403, 'Unauthorized to edit resource type mappings');
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($resourceTypeMapping) {
|
||||
$resourceTypes = ResourceType::whereNotIn('id', function($query) use ($resourceTypeMapping) {
|
||||
$query->select('resource_type_id')
|
||||
->from('auth.tbl_resource_type_mappings')
|
||||
->where('resource_type_id', '!=', $resourceTypeMapping->resource_type_id)
|
||||
->whereNull('deleted_at');
|
||||
})->orderBy('name')->get();
|
||||
|
||||
return view('admin::resource-type-mappings.edit', [
|
||||
'resource_type_mapping' => $resourceTypeMapping,
|
||||
'resourceTypes' => $resourceTypes,
|
||||
'schemas' => $this->getAvailableSchemas(),
|
||||
'thisResourceType' => $this->resourceType,
|
||||
'thisResourceValue' => $this->resourceValue
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
public function update(ResourceTypeMappingRequest $request, ResourceTypeMapping $resourceTypeMapping): RedirectResponse
|
||||
{
|
||||
if (!$this->checkResourcePermission($this->resourceType, $this->resourceValue, 'edit')) {
|
||||
abort(403, 'Unauthorized to edit resource type mappings');
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($request, $resourceTypeMapping) {
|
||||
$resourceTypeMapping->update($request->validated());
|
||||
|
||||
Log::info('Resource type mapping updated successfully', [
|
||||
'resource_type_id' => $resourceTypeMapping->resource_type_id,
|
||||
'table' => $resourceTypeMapping->getFullTableName(),
|
||||
'user_id' => auth()->id()
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('admin.resource-type-mappings.index')
|
||||
->with('success', 'Resource type mapping updated successfully');
|
||||
});
|
||||
}
|
||||
|
||||
public function destroy(ResourceTypeMapping $resourceTypeMapping): RedirectResponse
|
||||
{
|
||||
if (!$this->checkResourcePermission($this->resourceType, $this->resourceValue, 'delete')) {
|
||||
abort(403, 'Unauthorized to delete resource type mappings');
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($resourceTypeMapping) {
|
||||
$resourceTypeMapping->delete();
|
||||
|
||||
Log::info('Resource type mapping deleted successfully', [
|
||||
'resource_type_id' => $resourceTypeMapping->resource_type_id,
|
||||
'table' => $resourceTypeMapping->getFullTableName(),
|
||||
'user_id' => auth()->id()
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('admin.resource-type-mappings.index')
|
||||
->with('success', 'Resource type mapping deleted successfully');
|
||||
});
|
||||
}
|
||||
|
||||
public function getTables(Request $request): JsonResponse
|
||||
{
|
||||
if (!$this->checkResourcePermission($this->resourceType, $this->resourceValue, 'view')) {
|
||||
return response()->json(['error' => 'Unauthorized access'], 403);
|
||||
}
|
||||
|
||||
$schema = $request->get('schema');
|
||||
if (!$schema) {
|
||||
return response()->json(['error' => 'Schema is required'], 400);
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($schema) {
|
||||
$tables = $this->getTablesForSchema($schema);
|
||||
|
||||
return response()->json(array_map(function($table) {
|
||||
return ['table_name' => $table->table_name];
|
||||
}, $tables));
|
||||
});
|
||||
}
|
||||
|
||||
public function getColumns(Request $request): JsonResponse
|
||||
{
|
||||
if (!$this->checkResourcePermission($this->resourceType, $this->resourceValue, 'view')) {
|
||||
return response()->json(['error' => 'Unauthorized access'], 403);
|
||||
}
|
||||
|
||||
$schema = $request->get('schema');
|
||||
$table = $request->get('table');
|
||||
|
||||
if (!$schema || !$table) {
|
||||
return response()->json(['error' => 'Schema and table are required'], 400);
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($schema, $table) {
|
||||
return response()->json($this->getColumnsForTable($schema, $table));
|
||||
});
|
||||
}
|
||||
|
||||
private function getAvailableSchemas(): array
|
||||
{
|
||||
return DB::select("
|
||||
SELECT schema_name
|
||||
FROM information_schema.schemata
|
||||
WHERE schema_name NOT IN ('information_schema', 'pg_catalog')
|
||||
ORDER BY schema_name
|
||||
");
|
||||
}
|
||||
|
||||
private function getTablesForSchema(string $schema): array
|
||||
{
|
||||
return DB::select("
|
||||
SELECT table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = ?
|
||||
AND table_type = 'BASE TABLE'
|
||||
ORDER BY table_name
|
||||
", [$schema]);
|
||||
}
|
||||
|
||||
private function getColumnsForTable(string $schema, string $table): array
|
||||
{
|
||||
return DB::select("
|
||||
SELECT column_name
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = ?
|
||||
AND table_name = ?
|
||||
ORDER BY ordinal_position
|
||||
", [$schema, $table]);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
namespace App\Components\Admin\Http\Controllers;
|
||||
|
||||
use App\Components\Admin\Http\Requests\ResourceTypeRequest;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\ResourceType;
|
||||
use App\Traits\WebPageAuthorization;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class ResourceTypesController extends Controller
|
||||
{
|
||||
use WebPageAuthorization;
|
||||
|
||||
private const RESOURCE_TYPE = 'web_pages';
|
||||
private const RESOURCE_VALUE = 'admin.resource-types.php';
|
||||
|
||||
protected string $resourceType;
|
||||
protected string $resourceValue;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->resourceType = self::RESOURCE_TYPE;
|
||||
$this->resourceValue = self::RESOURCE_VALUE;
|
||||
}
|
||||
|
||||
public function index(Request $request): View|RedirectResponse
|
||||
{
|
||||
if (!$this->checkResourcePermission($this->resourceType, $this->resourceValue, 'view')) {
|
||||
abort(403, 'Unauthorized to view resource types');
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($request) {
|
||||
$resourceTypes = ResourceType::query()
|
||||
->search($request->input('search'))
|
||||
->orderBy($request->input('sort', 'name'))
|
||||
->paginate(10)
|
||||
->withQueryString();
|
||||
|
||||
return view('admin::resource-types.index', [
|
||||
'resourceTypes' => $resourceTypes,
|
||||
'thisResourceType' => $this->resourceType,
|
||||
'thisResourceValue' => $this->resourceValue
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
public function create(): View
|
||||
{
|
||||
if (!$this->checkResourcePermission($this->resourceType, $this->resourceValue, 'create')) {
|
||||
abort(403, 'Unauthorized to create resource types');
|
||||
}
|
||||
|
||||
return view('admin::resource-types.create', [
|
||||
'thisResourceType' => $this->resourceType,
|
||||
'thisResourceValue' => $this->resourceValue
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(ResourceTypeRequest $request): RedirectResponse
|
||||
{
|
||||
if (!$this->checkResourcePermission($this->resourceType, $this->resourceValue, 'create')) {
|
||||
abort(403, 'Unauthorized to create resource types');
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($request) {
|
||||
$resourceType = ResourceType::create($request->validated());
|
||||
|
||||
Log::info('Resource type created successfully', [
|
||||
'id' => $resourceType->id,
|
||||
'name' => $resourceType->name,
|
||||
'user_id' => auth()->id()
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('admin.resource-types.index')
|
||||
->with('success', 'Resource type created successfully');
|
||||
});
|
||||
}
|
||||
|
||||
public function edit(ResourceType $resourceType): View
|
||||
{
|
||||
if (!$this->checkResourcePermission($this->resourceType, $this->resourceValue, 'edit')) {
|
||||
abort(403, 'Unauthorized to edit resource types');
|
||||
}
|
||||
|
||||
return view('admin::resource-types.edit', [
|
||||
'resourceType' => $resourceType,
|
||||
'thisResourceType' => $this->resourceType,
|
||||
'thisResourceValue' => $this->resourceValue
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(ResourceTypeRequest $request, ResourceType $resourceType): RedirectResponse
|
||||
{
|
||||
if (!$this->checkResourcePermission($this->resourceType, $this->resourceValue, 'edit')) {
|
||||
abort(403, 'Unauthorized to edit resource types');
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($request, $resourceType) {
|
||||
$resourceType->update($request->validated());
|
||||
|
||||
Log::info('Resource type updated successfully', [
|
||||
'id' => $resourceType->id,
|
||||
'name' => $resourceType->name,
|
||||
'user_id' => auth()->id()
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('admin.resource-types.index')
|
||||
->with('success', 'Resource type updated successfully');
|
||||
});
|
||||
}
|
||||
|
||||
public function destroy(ResourceType $resourceType): RedirectResponse
|
||||
{
|
||||
if (!$this->checkResourcePermission($this->resourceType, $this->resourceValue, 'delete')) {
|
||||
abort(403, 'Unauthorized to delete resource types');
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($resourceType) {
|
||||
$pageInfo = [
|
||||
'id' => $resourceType->id,
|
||||
'name' => $resourceType->name,
|
||||
'user_id' => auth()->id()
|
||||
];
|
||||
|
||||
$resourceType->delete();
|
||||
|
||||
Log::info('Resource type deleted successfully', $pageInfo);
|
||||
|
||||
return redirect()
|
||||
->route('admin.resource-types.index')
|
||||
->with('success', 'Resource type deleted successfully');
|
||||
});
|
||||
}
|
||||
}
|
||||
215
app/Components/Admin/Http/Controllers/RolesController.php
Normal file
215
app/Components/Admin/Http/Controllers/RolesController.php
Normal file
@ -0,0 +1,215 @@
|
||||
<?php
|
||||
|
||||
namespace App\Components\Admin\Http\Controllers;
|
||||
|
||||
use App\Components\Admin\Helpers\AuthorizationHelper;
|
||||
use App\Components\Admin\Http\Requests\RoleRequest;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Role;
|
||||
use App\Traits\WebPageAuthorization;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class RolesController extends Controller
|
||||
{
|
||||
use WebPageAuthorization;
|
||||
|
||||
private const RESOURCE_TYPE = 'web_pages';
|
||||
private const RESOURCE_VALUE = 'admin.roles.php';
|
||||
|
||||
protected string $resourceType;
|
||||
protected string $resourceValue;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->resourceType = self::RESOURCE_TYPE;
|
||||
$this->resourceValue = self::RESOURCE_VALUE;
|
||||
}
|
||||
|
||||
public function index(Request $request): View|RedirectResponse
|
||||
{
|
||||
if (!$this->checkResourcePermission($this->resourceType, $this->resourceValue, 'view')) {
|
||||
abort(403, 'Unauthorized to view roles');
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($request) {
|
||||
$roles = Role::query()
|
||||
->search($request->input('search'))
|
||||
->orderBy($request->input('sort', 'name'))
|
||||
->paginate(10)
|
||||
->withQueryString();
|
||||
|
||||
return view('admin::roles.index', [
|
||||
'roles' => $roles,
|
||||
'thisResourceType' => $this->resourceType,
|
||||
'thisResourceValue' => $this->resourceValue
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
public function create(): View
|
||||
{
|
||||
if (!$this->checkResourcePermission($this->resourceType, $this->resourceValue, 'create')) {
|
||||
abort(403, 'Unauthorized to create roles');
|
||||
}
|
||||
|
||||
return view('admin::roles.create', [
|
||||
'thisResourceType' => $this->resourceType,
|
||||
'thisResourceValue' => $this->resourceValue
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(RoleRequest $request): RedirectResponse
|
||||
{
|
||||
if (!$this->checkResourcePermission($this->resourceType, $this->resourceValue, 'create')) {
|
||||
abort(403, 'Unauthorized to create roles');
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($request) {
|
||||
$role = Role::create($request->validated());
|
||||
|
||||
Log::info('Role created successfully', [
|
||||
'id' => $role->id,
|
||||
'name' => $role->name,
|
||||
'user_id' => auth()->id()
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('admin.roles.index')
|
||||
->with('success', 'Role created successfully');
|
||||
});
|
||||
}
|
||||
|
||||
public function edit(Role $role): View
|
||||
{
|
||||
if (!$this->checkResourcePermission($this->resourceType, $this->resourceValue, 'edit')) {
|
||||
abort(403, 'Unauthorized to edit roles');
|
||||
}
|
||||
|
||||
return view('admin::roles.edit', [
|
||||
'role' => $role,
|
||||
'thisResourceType' => $this->resourceType,
|
||||
'thisResourceValue' => $this->resourceValue
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(RoleRequest $request, Role $role): RedirectResponse
|
||||
{
|
||||
if (!$this->checkResourcePermission($this->resourceType, $this->resourceValue, 'edit')) {
|
||||
abort(403, 'Unauthorized to edit roles');
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($request, $role) {
|
||||
$role->update($request->validated());
|
||||
|
||||
Log::info('Role updated successfully', [
|
||||
'id' => $role->id,
|
||||
'name' => $role->name,
|
||||
'user_id' => auth()->id()
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('admin.roles.index')
|
||||
->with('success', 'Role updated successfully');
|
||||
});
|
||||
}
|
||||
|
||||
public function managePermissions(Role $role): View
|
||||
{
|
||||
if (!$this->checkResourcePermission($this->resourceType, $this->resourceValue, 'edit')) {
|
||||
abort(403, 'Unauthorized to manage role permissions');
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($role) {
|
||||
$permissions = Permission::orderBy('name')->get();
|
||||
$rolePermissionIds = $role->permissions->pluck('id')->toArray();
|
||||
|
||||
$criticalPermissions = $permissions->mapWithKeys(function ($permission) {
|
||||
return [$permission->id => AuthorizationHelper::isSystemCriticalPermission($permission->name)];
|
||||
})->toArray();
|
||||
|
||||
$isSuperAdmin = auth()->user()->hasRole('super_admin');
|
||||
$isSystemRole = AuthorizationHelper::isSystemRole($role->name);
|
||||
|
||||
if ($isSystemRole) {
|
||||
Log::info('System role permissions being accessed', [
|
||||
'role_id' => $role->id,
|
||||
'role_name' => $role->name,
|
||||
'user_id' => auth()->id(),
|
||||
'user_email' => auth()->user()->email
|
||||
]);
|
||||
}
|
||||
|
||||
return view('admin::roles.manage-permissions', [
|
||||
'role' => $role,
|
||||
'permissions' => $permissions,
|
||||
'rolePermissionIds' => $rolePermissionIds,
|
||||
'thisResourceType' => $this->resourceType,
|
||||
'thisResourceValue' => $this->resourceValue,
|
||||
'criticalPermissions' => $criticalPermissions,
|
||||
'isSuperAdmin' => $isSuperAdmin,
|
||||
'isSystemRole' => $isSystemRole
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
public function updatePermissions(Request $request, Role $role): RedirectResponse
|
||||
{
|
||||
if (!$this->checkResourcePermission($this->resourceType, $this->resourceValue, 'edit')) {
|
||||
abort(403, 'Unauthorized to update role permissions');
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
'permissions' => 'array|nullable',
|
||||
'permissions.*' => [Rule::exists('pgsql.auth.permissions', 'id')]
|
||||
]);
|
||||
|
||||
return DB::transaction(function () use ($validated, $role) {
|
||||
$permissions = $validated['permissions'] ?? [];
|
||||
$role->permissions()->sync($permissions);
|
||||
|
||||
Log::info('Role permissions updated successfully', [
|
||||
'role_id' => $role->id,
|
||||
'permissions' => $permissions,
|
||||
'user_id' => auth()->id()
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('admin.roles.index')
|
||||
->with('success', 'Permissions updated successfully');
|
||||
});
|
||||
}
|
||||
|
||||
public function destroy(Role $role): RedirectResponse
|
||||
{
|
||||
if (!$this->checkResourcePermission($this->resourceType, $this->resourceValue, 'delete')) {
|
||||
abort(403, 'Unauthorized to delete roles');
|
||||
}
|
||||
|
||||
if ($role->users()->exists()) {
|
||||
return back()->withErrors([
|
||||
'error' => 'Cannot delete role that is assigned to users'
|
||||
]);
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($role) {
|
||||
$role->permissions()->detach();
|
||||
$role->delete();
|
||||
|
||||
Log::info('Role deleted successfully', [
|
||||
'id' => $role->id,
|
||||
'name' => $role->name,
|
||||
'user_id' => auth()->id()
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('admin.roles.index')
|
||||
->with('success', 'Role deleted successfully');
|
||||
});
|
||||
}
|
||||
}
|
||||
155
app/Components/Admin/Http/Controllers/UserRolesController.php
Normal file
155
app/Components/Admin/Http/Controllers/UserRolesController.php
Normal file
@ -0,0 +1,155 @@
|
||||
<?php
|
||||
|
||||
namespace App\Components\Admin\Http\Controllers;
|
||||
|
||||
use App\Components\Admin\Http\Requests\UserRoleRequest;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Role;
|
||||
use App\Models\User;
|
||||
use App\Models\UserRole;
|
||||
use App\Traits\WebPageAuthorization;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class UserRolesController extends Controller
|
||||
{
|
||||
use WebPageAuthorization;
|
||||
|
||||
private const RESOURCE_TYPE = 'web_pages';
|
||||
private const RESOURCE_VALUE = 'admin.user-roles.php';
|
||||
|
||||
protected string $resourceType;
|
||||
protected string $resourceValue;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->resourceType = self::RESOURCE_TYPE;
|
||||
$this->resourceValue = self::RESOURCE_VALUE;
|
||||
}
|
||||
|
||||
public function index(Request $request): View|RedirectResponse
|
||||
{
|
||||
if (!$this->checkResourcePermission($this->resourceType, $this->resourceValue, 'view')) {
|
||||
abort(403, 'Unauthorized to view user roles');
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($request) {
|
||||
$userRoles = UserRole::with(['user', 'role'])
|
||||
->search($request->input('search'))
|
||||
->sort(
|
||||
$request->input('sort', 'created_at'),
|
||||
$request->input('direction', 'desc')
|
||||
)
|
||||
->paginate(10)
|
||||
->withQueryString();
|
||||
|
||||
return view('admin::user-roles.index', [
|
||||
'userRoles' => $userRoles,
|
||||
'thisResourceType' => $this->resourceType,
|
||||
'thisResourceValue' => $this->resourceValue
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
public function create(): View
|
||||
{
|
||||
if (!$this->checkResourcePermission($this->resourceType, $this->resourceValue, 'create')) {
|
||||
abort(403, 'Unauthorized to create user roles');
|
||||
}
|
||||
|
||||
return DB::transaction(function () {
|
||||
return view('admin::user-roles.create', [
|
||||
'users' => User::orderBy('name')->get(['id', 'name']),
|
||||
'roles' => Role::orderBy('name')->get(['id', 'name']),
|
||||
'thisResourceType' => $this->resourceType,
|
||||
'thisResourceValue' => $this->resourceValue
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
public function store(UserRoleRequest $request): RedirectResponse
|
||||
{
|
||||
if (!$this->checkResourcePermission($this->resourceType, $this->resourceValue, 'create')) {
|
||||
abort(403, 'Unauthorized to create user roles');
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($request) {
|
||||
$userRole = UserRole::create($request->validated());
|
||||
|
||||
Log::info('User role created successfully', [
|
||||
'id' => $userRole->id,
|
||||
'user_id' => $userRole->user_id,
|
||||
'role_id' => $userRole->role_id,
|
||||
'created_by' => auth()->id()
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('admin.user-roles.index')
|
||||
->with('success', 'User role assigned successfully');
|
||||
});
|
||||
}
|
||||
|
||||
public function edit(UserRole $userRole): View
|
||||
{
|
||||
if (!$this->checkResourcePermission($this->resourceType, $this->resourceValue, 'edit')) {
|
||||
abort(403, 'Unauthorized to edit user roles');
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($userRole) {
|
||||
return view('admin::user-roles.edit', [
|
||||
'userRole' => $userRole,
|
||||
'users' => User::orderBy('name')->get(['id', 'name']),
|
||||
'roles' => Role::orderBy('name')->get(['id', 'name']),
|
||||
'thisResourceType' => $this->resourceType,
|
||||
'thisResourceValue' => $this->resourceValue
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
public function update(UserRoleRequest $request, UserRole $userRole): RedirectResponse
|
||||
{
|
||||
if (!$this->checkResourcePermission($this->resourceType, $this->resourceValue, 'edit')) {
|
||||
abort(403, 'Unauthorized to edit user roles');
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($request, $userRole) {
|
||||
$userRole->update($request->validated());
|
||||
|
||||
Log::info('User role updated successfully', [
|
||||
'id' => $userRole->id,
|
||||
'user_id' => $userRole->user_id,
|
||||
'role_id' => $userRole->role_id,
|
||||
'updated_by' => auth()->id()
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('admin.user-roles.index')
|
||||
->with('success', 'User role updated successfully');
|
||||
});
|
||||
}
|
||||
|
||||
public function destroy(UserRole $userRole): RedirectResponse
|
||||
{
|
||||
if (!$this->checkResourcePermission($this->resourceType, $this->resourceValue, 'delete')) {
|
||||
abort(403, 'Unauthorized to delete user roles');
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($userRole) {
|
||||
$userRole->delete();
|
||||
|
||||
Log::info('User role deleted successfully', [
|
||||
'id' => $userRole->id,
|
||||
'user_id' => $userRole->user_id,
|
||||
'role_id' => $userRole->role_id,
|
||||
'deleted_by' => auth()->id()
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('admin.user-roles.index')
|
||||
->with('success', 'User role removed successfully');
|
||||
});
|
||||
}
|
||||
}
|
||||
154
app/Components/Admin/Http/Controllers/UsersController.php
Normal file
154
app/Components/Admin/Http/Controllers/UsersController.php
Normal file
@ -0,0 +1,154 @@
|
||||
<?php
|
||||
|
||||
namespace App\Components\Admin\Http\Controllers;
|
||||
|
||||
use App\Components\Admin\Http\Requests\UserRequest;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use App\Traits\WebPageAuthorization;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class UsersController extends Controller
|
||||
{
|
||||
use WebPageAuthorization;
|
||||
|
||||
private const RESOURCE_TYPE = 'web_pages';
|
||||
private const RESOURCE_VALUE = 'admin.users.php';
|
||||
|
||||
protected string $resourceType;
|
||||
protected string $resourceValue;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->resourceType = self::RESOURCE_TYPE;
|
||||
$this->resourceValue = self::RESOURCE_VALUE;
|
||||
}
|
||||
|
||||
public function index(Request $request): View|RedirectResponse
|
||||
{
|
||||
if (!$this->checkResourcePermission($this->resourceType, $this->resourceValue, 'view')) {
|
||||
abort(403, 'Unauthorized to view users');
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($request) {
|
||||
$query = User::query()
|
||||
->search($request->input('search'))
|
||||
->filterByStatus($request->input('status'))
|
||||
->orderBy($request->input('sort', 'name'));
|
||||
|
||||
Log::info('User query:', [
|
||||
'sql' => $query->toSql(),
|
||||
'bindings' => $query->getBindings()
|
||||
]);
|
||||
|
||||
$users = $query->paginate(10)->withQueryString();
|
||||
|
||||
return view('admin::users.index', [
|
||||
'users' => $users,
|
||||
'thisResourceType' => $this->resourceType,
|
||||
'thisResourceValue' => $this->resourceValue
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
public function create(): View|RedirectResponse
|
||||
{
|
||||
if (!$this->checkResourcePermission($this->resourceType, $this->resourceValue, 'create')) {
|
||||
abort(403, 'Unauthorized to create users');
|
||||
}
|
||||
|
||||
return view('admin::users.create', [
|
||||
'thisResourceType' => $this->resourceType,
|
||||
'thisResourceValue' => $this->resourceValue
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(UserRequest $request): RedirectResponse
|
||||
{
|
||||
if (!$this->checkResourcePermission($this->resourceType, $this->resourceValue, 'create')) {
|
||||
abort(403, 'Unauthorized to create users');
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($request) {
|
||||
$validated = $request->validated();
|
||||
$validated['password'] = Hash::make($validated['password']);
|
||||
|
||||
$user = User::create($validated);
|
||||
|
||||
Log::info('User created successfully', [
|
||||
'id' => $user->id,
|
||||
'name' => $user->name
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('admin.users.index')
|
||||
->with('success', 'User created successfully');
|
||||
});
|
||||
}
|
||||
|
||||
public function edit(User $user): View|RedirectResponse
|
||||
{
|
||||
if (!$this->checkResourcePermission($this->resourceType, $this->resourceValue, 'edit')) {
|
||||
abort(403, 'Unauthorized to edit users');
|
||||
}
|
||||
|
||||
return view('admin::users.edit', [
|
||||
'user' => $user,
|
||||
'thisResourceType' => $this->resourceType,
|
||||
'thisResourceValue' => $this->resourceValue
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(UserRequest $request, User $user): RedirectResponse
|
||||
{
|
||||
if (!$this->checkResourcePermission($this->resourceType, $this->resourceValue, 'edit')) {
|
||||
abort(403, 'Unauthorized to edit users');
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($request, $user) {
|
||||
$validated = $request->validated();
|
||||
|
||||
if (isset($validated['password'])) {
|
||||
$validated['password'] = Hash::make($validated['password']);
|
||||
} else {
|
||||
unset($validated['password']);
|
||||
}
|
||||
|
||||
$user->update($validated);
|
||||
|
||||
Log::info('User updated successfully', [
|
||||
'id' => $user->id,
|
||||
'name' => $user->name
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('admin.users.index')
|
||||
->with('success', 'User updated successfully');
|
||||
});
|
||||
}
|
||||
|
||||
public function destroy(User $user): RedirectResponse
|
||||
{
|
||||
if (!$this->checkResourcePermission($this->resourceType, $this->resourceValue, 'delete')) {
|
||||
abort(403, 'Unauthorized to delete users');
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($user) {
|
||||
$user->delete();
|
||||
|
||||
Log::info('User deleted successfully', [
|
||||
'id' => $user->id,
|
||||
'name' => $user->name
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('admin.users.index')
|
||||
->with('success', 'User deleted successfully');
|
||||
});
|
||||
}
|
||||
}
|
||||
141
app/Components/Admin/Http/Controllers/WebPagesController.php
Normal file
141
app/Components/Admin/Http/Controllers/WebPagesController.php
Normal file
@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
namespace App\Components\Admin\Http\Controllers;
|
||||
|
||||
use App\Components\Admin\Http\Requests\WebPageRequest;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\WebPage;
|
||||
use App\Traits\WebPageAuthorization;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class WebPagesController extends Controller
|
||||
{
|
||||
use WebPageAuthorization;
|
||||
|
||||
private const RESOURCE_TYPE = 'web_pages';
|
||||
private const RESOURCE_VALUE = 'admin.web-pages.php';
|
||||
|
||||
protected string $resourceType;
|
||||
protected string $resourceValue;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->resourceType = self::RESOURCE_TYPE;
|
||||
$this->resourceValue = self::RESOURCE_VALUE;
|
||||
}
|
||||
|
||||
public function index(Request $request): View|RedirectResponse
|
||||
{
|
||||
if (!$this->checkResourcePermission($this->resourceType, $this->resourceValue, 'view')) {
|
||||
abort(403, 'Unauthorized to view web pages');
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($request) {
|
||||
$webPages = WebPage::query()
|
||||
->search($request->input('search'))
|
||||
->orderBy($request->input('sort', 'url'))
|
||||
->paginate(10)
|
||||
->withQueryString();
|
||||
|
||||
return view('admin::web-pages.index', [
|
||||
'webPages' => $webPages,
|
||||
'thisResourceType' => $this->resourceType,
|
||||
'thisResourceValue' => $this->resourceValue
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
public function create(): View
|
||||
{
|
||||
if (!$this->checkResourcePermission($this->resourceType, $this->resourceValue, 'create')) {
|
||||
abort(403, 'Unauthorized to create web pages');
|
||||
}
|
||||
|
||||
return view('admin::web-pages.create', [
|
||||
'thisResourceType' => $this->resourceType,
|
||||
'thisResourceValue' => $this->resourceValue
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(WebPageRequest $request): RedirectResponse
|
||||
{
|
||||
if (!$this->checkResourcePermission($this->resourceType, $this->resourceValue, 'create')) {
|
||||
abort(403, 'Unauthorized to create web pages');
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($request) {
|
||||
$webPage = WebPage::create($request->validated());
|
||||
|
||||
Log::info('Web page created successfully', [
|
||||
'id' => $webPage->id,
|
||||
'url' => $webPage->url,
|
||||
'user_id' => auth()->id()
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('admin.web-pages.index')
|
||||
->with('success', 'Web page created successfully');
|
||||
});
|
||||
}
|
||||
|
||||
public function edit(WebPage $webPage): View
|
||||
{
|
||||
if (!$this->checkResourcePermission($this->resourceType, $this->resourceValue, 'edit')) {
|
||||
abort(403, 'Unauthorized to edit this web page');
|
||||
}
|
||||
|
||||
return view('admin::web-pages.edit', [
|
||||
'webPage' => $webPage,
|
||||
'thisResourceType' => $this->resourceType,
|
||||
'thisResourceValue' => $this->resourceValue
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(WebPageRequest $request, WebPage $webPage): RedirectResponse
|
||||
{
|
||||
if (!$this->checkResourcePermission($this->resourceType, $this->resourceValue, 'edit')) {
|
||||
abort(403, 'Unauthorized to edit this web page');
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($request, $webPage) {
|
||||
$webPage->update($request->validated());
|
||||
|
||||
Log::info('Web page updated successfully', [
|
||||
'id' => $webPage->id,
|
||||
'url' => $webPage->url,
|
||||
'user_id' => auth()->id()
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('admin.web-pages.index')
|
||||
->with('success', 'Web page updated successfully');
|
||||
});
|
||||
}
|
||||
|
||||
public function destroy(WebPage $webPage): RedirectResponse
|
||||
{
|
||||
if (!$this->checkResourcePermission($this->resourceType, $this->resourceValue, 'delete')) {
|
||||
abort(403, 'Unauthorized to delete this web page');
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($webPage) {
|
||||
$pageInfo = [
|
||||
'id' => $webPage->id,
|
||||
'url' => $webPage->url,
|
||||
'user_id' => auth()->id()
|
||||
];
|
||||
|
||||
$webPage->delete();
|
||||
|
||||
Log::info('Web page deleted successfully', $pageInfo);
|
||||
|
||||
return redirect()
|
||||
->route('admin.web-pages.index')
|
||||
->with('success', 'Web page deleted successfully');
|
||||
});
|
||||
}
|
||||
}
|
||||
14
app/Components/Admin/Http/Middleware/AdminAuthentication.php
Normal file
14
app/Components/Admin/Http/Middleware/AdminAuthentication.php
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Components\Admin\Http\Middleware;
|
||||
|
||||
use App\Http\Middleware\BaseAuthentication;
|
||||
|
||||
class AdminAuthentication extends BaseAuthentication
|
||||
{
|
||||
protected function getLoginRoute(): string
|
||||
{
|
||||
return 'login';
|
||||
}
|
||||
}
|
||||
|
||||
67
app/Components/Admin/Http/Requests/AutogroupRequest.php
Normal file
67
app/Components/Admin/Http/Requests/AutogroupRequest.php
Normal file
@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace App\Components\Admin\Http\Requests;
|
||||
|
||||
use App\Traits\WebPageAuthorization;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class AutogroupRequest extends FormRequest
|
||||
{
|
||||
use WebPageAuthorization;
|
||||
|
||||
protected string $resourceType = 'web_pages';
|
||||
protected string $resourceValue = 'admin.autogroups.php';
|
||||
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
$autogroupId = $this->route('autogroup')?->id;
|
||||
$action = $autogroupId ? 'edit' : 'create';
|
||||
|
||||
return $this->checkResourcePermission($this->resourceType, $this->resourceValue, $action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
$autogroupId = $this->route('autogroup')?->id;
|
||||
|
||||
return [
|
||||
'name' => [
|
||||
'required',
|
||||
'string',
|
||||
'max:255',
|
||||
Rule::unique('pgsql.core.tbl_autogroups', 'name')
|
||||
->ignore($autogroupId, 'id')
|
||||
->whereNull('deleted_at')
|
||||
],
|
||||
'description' => [
|
||||
'nullable',
|
||||
'string',
|
||||
'max:255'
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom messages for validator errors.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'name.required' => 'The autogroup name is required.',
|
||||
'name.unique' => 'This autogroup name is already in use.',
|
||||
'name.max' => 'The autogroup name cannot exceed 255 characters.',
|
||||
'description.max' => 'The description cannot exceed 255 characters.',
|
||||
];
|
||||
}
|
||||
}
|
||||
61
app/Components/Admin/Http/Requests/MenuTypeRequest.php
Normal file
61
app/Components/Admin/Http/Requests/MenuTypeRequest.php
Normal file
@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace App\Components\Admin\Http\Requests;
|
||||
|
||||
use App\Traits\WebPageAuthorization;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class MenuTypeRequest extends FormRequest
|
||||
{
|
||||
use WebPageAuthorization;
|
||||
|
||||
protected string $resourceType = 'web_pages';
|
||||
protected string $resourceValue = 'admin.menu-types.php';
|
||||
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
$menuTypeId = $this->route('menu_type')?->id;
|
||||
$action = $menuTypeId ? 'edit' : 'create';
|
||||
return $this->checkResourcePermission($this->resourceType, $this->resourceValue, $action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => [
|
||||
'required',
|
||||
'string',
|
||||
'max:100',
|
||||
Rule::unique('pgsql.config.tbl_menu_types', 'name')
|
||||
->ignore($this->route('menu_type')),
|
||||
'regex:/^[\w\s-]+$/'
|
||||
],
|
||||
'description' => ['nullable', 'string', 'max:255']
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom messages for validator errors.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'name.required' => 'A menu type name is required',
|
||||
'name.max' => 'The menu type name cannot be longer than 100 characters',
|
||||
'name.regex' => 'The name may only contain letters, numbers, spaces, hyphens, and underscores',
|
||||
'name.unique' => 'This menu type name is already in use',
|
||||
'description.max' => 'The description cannot be longer than 255 characters'
|
||||
];
|
||||
}
|
||||
}
|
||||
92
app/Components/Admin/Http/Requests/MigrationRequest.php
Normal file
92
app/Components/Admin/Http/Requests/MigrationRequest.php
Normal file
@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace App\Components\Admin\Http\Requests;
|
||||
|
||||
use App\Traits\WebPageAuthorization;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class MigrationRequest extends FormRequest
|
||||
{
|
||||
use WebPageAuthorization;
|
||||
|
||||
/**
|
||||
* The resource type for authorization.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $resourceType = 'web_pages';
|
||||
|
||||
/**
|
||||
* The resource value for authorization.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $resourceValue = 'admin.migrations.php';
|
||||
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
// We'll only allow view permission since migrations shouldn't be modified through the UI
|
||||
return $this->checkResourcePermission($this->resourceType, $this->resourceValue, 'view');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'sort' => ['sometimes', 'string', 'in:id,migration,batch'],
|
||||
'direction' => ['sometimes', 'string', 'in:asc,desc'],
|
||||
'search' => ['sometimes', 'nullable', 'string', 'max:255'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom messages for validator errors.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'sort.in' => 'The sort field must be either id, migration, or batch.',
|
||||
'direction.in' => 'The direction must be either ascending or descending.',
|
||||
'search.max' => 'The search term cannot exceed 255 characters.',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom attributes for validator errors.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function attributes(): array
|
||||
{
|
||||
return [
|
||||
'sort' => 'sort field',
|
||||
'direction' => 'sort direction',
|
||||
'search' => 'search term',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the data for validation.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
// Ensure sort and direction have default values if not provided
|
||||
$this->merge([
|
||||
'sort' => $this->input('sort', 'id'),
|
||||
'direction' => $this->input('direction', 'desc'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
110
app/Components/Admin/Http/Requests/NavigationItemRequest.php
Normal file
110
app/Components/Admin/Http/Requests/NavigationItemRequest.php
Normal file
@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
namespace App\Components\Admin\Http\Requests;
|
||||
|
||||
use App\Helpers\IconHelper;
|
||||
use App\Traits\WebPageAuthorization;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class NavigationItemRequest extends FormRequest
|
||||
{
|
||||
use WebPageAuthorization;
|
||||
|
||||
protected string $resourceType = 'web_pages';
|
||||
protected string $resourceValue = 'admin.navigation-items.php';
|
||||
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
// Determine the action based on whether we're updating an existing item
|
||||
$isEdit = $this->route('navigation_item') !== null;
|
||||
$action = $isEdit ? 'edit' : 'create';
|
||||
|
||||
return $this->checkResourcePermission($this->resourceType, $this->resourceValue, $action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'menu_type_id' => [
|
||||
'required',
|
||||
Rule::exists('pgsql.config.tbl_menu_types', 'id')
|
||||
],
|
||||
'name' => [
|
||||
'required',
|
||||
'string',
|
||||
'max:255'
|
||||
],
|
||||
'route' => [
|
||||
'required',
|
||||
'string',
|
||||
'max:255'
|
||||
],
|
||||
'icon' => [
|
||||
'nullable',
|
||||
'string',
|
||||
'max:100',
|
||||
function ($attribute, $value, $fail) {
|
||||
if (!empty($value) && !IconHelper::isValidIcon($value)) {
|
||||
$fail('The selected icon is invalid.');
|
||||
}
|
||||
}
|
||||
],
|
||||
'order_index' => [
|
||||
'nullable',
|
||||
'integer',
|
||||
'min:0'
|
||||
],
|
||||
'parent_id' => [
|
||||
'nullable',
|
||||
Rule::exists('pgsql.config.tbl_navigation_items', 'id')
|
||||
->whereNull('deleted_at')
|
||||
->where(function ($query) {
|
||||
$query->whereNull('parent_id')
|
||||
->when($this->route('navigation_item'), function ($query) {
|
||||
$query->where('id', '!=', $this->route('navigation_item')->id);
|
||||
});
|
||||
})
|
||||
],
|
||||
'is_active' => [
|
||||
'boolean'
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom messages for validator errors.
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'menu_type_id.required' => 'The menu type is required.',
|
||||
'menu_type_id.exists' => 'The selected menu type is invalid.',
|
||||
'name.required' => 'The navigation item name is required.',
|
||||
'name.max' => 'The navigation item name cannot exceed 255 characters.',
|
||||
'route.required' => 'The route is required.',
|
||||
'route.max' => 'The route cannot exceed 255 characters.',
|
||||
'icon.max' => 'The icon cannot exceed 100 characters.',
|
||||
'order_index.integer' => 'The order index must be a number.',
|
||||
'order_index.min' => 'The order index must be 0 or greater.',
|
||||
'parent_id.exists' => 'The selected parent item is invalid.',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the data for validation.
|
||||
*/
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
$this->merge([
|
||||
'is_active' => $this->boolean('is_active'),
|
||||
'order_index' => $this->input('order_index') ?? 0,
|
||||
]);
|
||||
}
|
||||
}
|
||||
49
app/Components/Admin/Http/Requests/PermissionRequest.php
Normal file
49
app/Components/Admin/Http/Requests/PermissionRequest.php
Normal file
@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Components\Admin\Http\Requests;
|
||||
|
||||
use App\Traits\WebPageAuthorization;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class PermissionRequest extends FormRequest
|
||||
{
|
||||
use WebPageAuthorization;
|
||||
|
||||
protected string $resourceType = 'web_pages';
|
||||
protected string $resourceValue = 'admin.permissions.php';
|
||||
|
||||
public function authorize(): bool
|
||||
{
|
||||
// dd($this->route('permission'));
|
||||
|
||||
$permissionId = $this->route('permission')?->id;
|
||||
$action = $permissionId ? 'edit' : 'create';
|
||||
|
||||
return $this->checkResourcePermission($this->resourceType, $this->resourceValue, $action);
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => [
|
||||
'required',
|
||||
'string',
|
||||
'max:255',
|
||||
Rule::unique('pgsql.auth.tbl_permissions', 'name')
|
||||
->ignore($this->route('permission')),
|
||||
],
|
||||
'description' => 'nullable|string|max:1000',
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'name.required' => 'The permission name is required',
|
||||
'name.unique' => 'This permission name already exists',
|
||||
'name.max' => 'The permission name cannot exceed 255 characters',
|
||||
'description.max' => 'The description cannot exceed 1000 characters',
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
namespace App\Components\Admin\Http\Requests;
|
||||
|
||||
use App\Traits\WebPageAuthorization;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class ResourceAssociationRequest extends FormRequest
|
||||
{
|
||||
use WebPageAuthorization;
|
||||
|
||||
protected string $resourceType = 'web_pages';
|
||||
protected string $resourceValue = 'admin.resource-associations.php';
|
||||
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
// Check if this is an update request
|
||||
$isEdit = $this->route('resource_association') !== null;
|
||||
|
||||
// Determine the required permission based on the request type
|
||||
$permission = $isEdit ? 'edit' : 'create';
|
||||
|
||||
return $this->checkResourcePermission($this->resourceType, $this->resourceValue, $permission);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'user_id' => [
|
||||
'required',
|
||||
'exists:users,id,active,true'
|
||||
],
|
||||
'resource_type_id' => [
|
||||
'required',
|
||||
'exists:pgsql.auth.tbl_resource_types,id'
|
||||
],
|
||||
'role_id' => [
|
||||
'required',
|
||||
'exists:pgsql.auth.tbl_roles,id',
|
||||
Rule::unique('pgsql.auth.tbl_resource_associations')
|
||||
->where(function ($query) {
|
||||
return $query->where('user_id', $this->user_id)
|
||||
->where('resource_type_id', $this->resource_type_id)
|
||||
->where('resource_id', $this->resource_id)
|
||||
->whereNull('deleted_at');
|
||||
})
|
||||
->ignore($this->route('resource_association'))
|
||||
],
|
||||
'resource_id' => [
|
||||
'nullable',
|
||||
'integer',
|
||||
function ($attribute, $value, $fail) {
|
||||
if ($value !== null) {
|
||||
// Verify the resource exists in the mapped table
|
||||
$mapping = \DB::table('auth.tbl_resource_type_mappings')
|
||||
->where('resource_type_id', $this->resource_type_id)
|
||||
->first();
|
||||
|
||||
if ($mapping) {
|
||||
$exists = \DB::table("{$mapping->table_schema}.{$mapping->table_name}")
|
||||
->where('id', $value)
|
||||
->whereNull('deleted_at')
|
||||
->exists();
|
||||
|
||||
if (!$exists) {
|
||||
$fail('The selected resource does not exist.');
|
||||
}
|
||||
} else {
|
||||
$fail('No mapping found for the selected resource type.');
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
'description' => [
|
||||
'nullable',
|
||||
'string',
|
||||
'max:255'
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom messages for validator errors.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'user_id.required' => 'A user must be selected.',
|
||||
'user_id.exists' => 'The selected user must be active and valid.',
|
||||
'resource_type_id.required' => 'A resource type must be selected.',
|
||||
'resource_type_id.exists' => 'The selected resource type is invalid.',
|
||||
'role_id.required' => 'A role must be selected.',
|
||||
'role_id.exists' => 'The selected role is invalid.',
|
||||
'role_id.unique' => 'This user already has this role for this resource.',
|
||||
'resource_id.integer' => 'The resource ID must be a number.',
|
||||
'description.max' => 'The description cannot exceed 255 characters.'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the data for validation.
|
||||
*/
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
// If no resource_id is provided, set it to null explicitly
|
||||
if ($this->input('resource_id') === '') {
|
||||
$this->merge([
|
||||
'resource_id' => null
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
namespace App\Components\Admin\Http\Requests;
|
||||
|
||||
use App\Traits\WebPageAuthorization;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class ResourceTypeMappingRequest extends FormRequest
|
||||
{
|
||||
use WebPageAuthorization;
|
||||
|
||||
protected string $resourceType = 'web_pages';
|
||||
protected string $resourceValue = 'admin.resource-type-mappings.php';
|
||||
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
$isEdit = $this->route('resource_type_mapping') !== null;
|
||||
$action = $isEdit ? 'edit' : 'create';
|
||||
|
||||
return $this->checkResourcePermission($this->resourceType, $this->resourceValue, $action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'resource_type_id' => [
|
||||
'required',
|
||||
'exists:pgsql.auth.tbl_resource_types,id',
|
||||
Rule::unique('pgsql.auth.tbl_resource_type_mappings', 'resource_type_id')
|
||||
->ignore($this->route('resource_type_mapping'), 'resource_type_id')
|
||||
],
|
||||
'table_schema' => [
|
||||
'required',
|
||||
'string',
|
||||
'max:255',
|
||||
function ($attribute, $value, $fail) {
|
||||
$exists = DB::select("
|
||||
SELECT EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.schemata
|
||||
WHERE schema_name = ?
|
||||
) as exists
|
||||
", [$value])[0]->exists;
|
||||
|
||||
if (!$exists) {
|
||||
$fail("The selected schema does not exist.");
|
||||
}
|
||||
}
|
||||
],
|
||||
'table_name' => [
|
||||
'required',
|
||||
'string',
|
||||
'max:255',
|
||||
function ($attribute, $value, $fail) {
|
||||
if (!$this->table_schema) {
|
||||
return;
|
||||
}
|
||||
|
||||
$exists = DB::select("
|
||||
SELECT EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = ?
|
||||
AND table_name = ?
|
||||
) as exists
|
||||
", [$this->table_schema, $value])[0]->exists;
|
||||
|
||||
if (!$exists) {
|
||||
$fail("The selected table does not exist in the specified schema.");
|
||||
}
|
||||
}
|
||||
],
|
||||
'resource_value_column' => [
|
||||
'required',
|
||||
'string',
|
||||
'max:255',
|
||||
function ($attribute, $value, $fail) {
|
||||
if (!$this->table_schema || !$this->table_name) {
|
||||
return;
|
||||
}
|
||||
|
||||
$exists = DB::select("
|
||||
SELECT EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = ?
|
||||
AND table_name = ?
|
||||
AND column_name = ?
|
||||
) as exists
|
||||
", [$this->table_schema, $this->table_name, $value])[0]->exists;
|
||||
|
||||
if (!$exists) {
|
||||
$fail("The selected column does not exist in the specified table.");
|
||||
}
|
||||
}
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom messages for validator errors.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'resource_type_id.required' => 'A resource type must be selected.',
|
||||
'resource_type_id.exists' => 'The selected resource type is invalid.',
|
||||
'resource_type_id.unique' => 'This resource type already has a mapping.',
|
||||
'table_schema.required' => 'A database schema must be selected.',
|
||||
'table_schema.max' => 'The schema name cannot exceed 255 characters.',
|
||||
'table_name.required' => 'A database table must be selected.',
|
||||
'table_name.max' => 'The table name cannot exceed 255 characters.',
|
||||
'resource_value_column.required' => 'A value column must be selected.',
|
||||
'resource_value_column.max' => 'The column name cannot exceed 255 characters.'
|
||||
];
|
||||
}
|
||||
}
|
||||
45
app/Components/Admin/Http/Requests/ResourceTypeRequest.php
Normal file
45
app/Components/Admin/Http/Requests/ResourceTypeRequest.php
Normal file
@ -0,0 +1,45 @@
|
||||
<?php
|
||||
namespace App\Components\Admin\Http\Requests;
|
||||
|
||||
use App\Traits\WebPageAuthorization;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class ResourceTypeRequest extends FormRequest
|
||||
{
|
||||
use WebPageAuthorization;
|
||||
|
||||
protected string $resourceType = 'web_pages';
|
||||
protected string $resourceValue = 'admin.resource-types.php';
|
||||
|
||||
public function authorize(): bool
|
||||
{
|
||||
$resourceTypeId = $this->route('resource_type')?->id;
|
||||
$action = $resourceTypeId ? 'edit' : 'create';
|
||||
|
||||
// $action = $this->route('resource_type') ? 'edit' : 'create';
|
||||
return $this->checkResourcePermission($this->resourceType, $this->resourceValue, $action);
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => [
|
||||
'required',
|
||||
'string',
|
||||
'max:255',
|
||||
Rule::unique('pgsql.auth.tbl_resource_types', 'name')->ignore($this->route('resource_type'))
|
||||
],
|
||||
'description' => ['nullable', 'string']
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'name.required' => 'The resource type name is required.',
|
||||
'name.unique' => 'This resource type name is already taken.',
|
||||
'name.max' => 'The resource type name cannot exceed 255 characters.'
|
||||
];
|
||||
}
|
||||
}
|
||||
47
app/Components/Admin/Http/Requests/RoleRequest.php
Normal file
47
app/Components/Admin/Http/Requests/RoleRequest.php
Normal file
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Components\Admin\Http\Requests;
|
||||
|
||||
use App\Traits\WebPageAuthorization;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class RoleRequest extends FormRequest
|
||||
{
|
||||
use WebPageAuthorization;
|
||||
|
||||
protected string $resourceType = 'web_pages';
|
||||
protected string $resourceValue = 'admin.roles.php';
|
||||
|
||||
public function authorize(): bool
|
||||
{
|
||||
// Determine if this is a create or update request
|
||||
$roleId = $this->route('role')?->id;
|
||||
$action = $roleId ? 'edit' : 'create';
|
||||
|
||||
return $this->checkResourcePermission($this->resourceType, $this->resourceValue, $action);
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => [
|
||||
'required',
|
||||
'string',
|
||||
'max:255',
|
||||
Rule::unique('pgsql.auth.tbl_roles', 'name')
|
||||
->ignore($this->route('role'))
|
||||
],
|
||||
'description' => ['nullable', 'string']
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'name.required' => 'The role name is required.',
|
||||
'name.unique' => 'This role name is already taken.',
|
||||
'name.max' => 'The role name cannot exceed 255 characters.'
|
||||
];
|
||||
}
|
||||
}
|
||||
87
app/Components/Admin/Http/Requests/UserRequest.php
Normal file
87
app/Components/Admin/Http/Requests/UserRequest.php
Normal file
@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace App\Components\Admin\Http\Requests;
|
||||
|
||||
use App\Traits\WebPageAuthorization;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UserRequest extends FormRequest
|
||||
{
|
||||
use WebPageAuthorization;
|
||||
|
||||
protected string $resourceType = 'web_pages';
|
||||
protected string $resourceValue = 'admin.users.php';
|
||||
|
||||
public function authorize(): bool
|
||||
{
|
||||
$modelId = $this->route('user')?->id;
|
||||
$action = $modelId ? 'edit' : 'create';
|
||||
|
||||
return $this->checkResourcePermission($this->resourceType, $this->resourceValue, $action);
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$rules = [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'email' => [
|
||||
'required',
|
||||
'string',
|
||||
'email',
|
||||
'max:255',
|
||||
Rule::unique('users', 'email')
|
||||
],
|
||||
'active' => ['boolean'],
|
||||
];
|
||||
|
||||
// Add password rules for create
|
||||
if ($this->isMethod('POST')) {
|
||||
$rules['password'] = ['required', 'string', 'min:8', 'confirmed'];
|
||||
}
|
||||
|
||||
// Modify password rules for update
|
||||
if ($this->isMethod('PUT') || $this->isMethod('PATCH')) {
|
||||
$rules['password'] = ['nullable', 'string', 'min:8', 'confirmed'];
|
||||
$rules['email'][4] = Rule::unique('users', 'email')->ignore($this->route('user')->id);
|
||||
}
|
||||
|
||||
return $rules;
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'name.required' => 'A name is required',
|
||||
'name.max' => 'The name cannot be longer than 255 characters',
|
||||
'email.required' => 'An email address is required',
|
||||
'email.email' => 'Please enter a valid email address',
|
||||
'email.unique' => 'This email address is already in use',
|
||||
'password.required' => 'A password is required',
|
||||
'password.min' => 'The password must be at least 8 characters',
|
||||
'password.confirmed' => 'The password confirmation does not match',
|
||||
];
|
||||
}
|
||||
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
$this->merge([
|
||||
'active' => $this->has('active')
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the error messages that apply to the request parameters.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function attributes(): array
|
||||
{
|
||||
return [
|
||||
'name' => 'user name',
|
||||
'email' => 'email address',
|
||||
'password' => 'password',
|
||||
'active' => 'active status',
|
||||
];
|
||||
}
|
||||
}
|
||||
80
app/Components/Admin/Http/Requests/UserRoleRequest.php
Normal file
80
app/Components/Admin/Http/Requests/UserRoleRequest.php
Normal file
@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace App\Components\Admin\Http\Requests;
|
||||
|
||||
use App\Traits\WebPageAuthorization;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UserRoleRequest extends FormRequest
|
||||
{
|
||||
use WebPageAuthorization;
|
||||
|
||||
protected string $resourceType = 'web_pages';
|
||||
protected string $resourceValue = 'admin.user-roles.php';
|
||||
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
$isEdit = $this->route('userRole') !== null;
|
||||
$action = $isEdit ? 'edit' : 'create';
|
||||
|
||||
return $this->checkResourcePermission($this->resourceType, $this->resourceValue, $action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'user_id' => [
|
||||
'required',
|
||||
'integer',
|
||||
'exists:users,id',
|
||||
Rule::unique('pgsql.auth.tbl_user_roles', 'user_id')
|
||||
->where('role_id', $this->input('role_id'))
|
||||
->ignore($this->route('userRole'))
|
||||
],
|
||||
'role_id' => [
|
||||
'required',
|
||||
'integer',
|
||||
'exists:pgsql.auth.tbl_roles,id'
|
||||
],
|
||||
'description' => [
|
||||
'nullable',
|
||||
'string',
|
||||
'max:1000'
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom messages for validator errors.
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'user_id.required' => 'A user must be selected.',
|
||||
'user_id.exists' => 'The selected user is invalid.',
|
||||
'user_id.unique' => 'This user already has been assigned this role.',
|
||||
'role_id.required' => 'A role must be selected.',
|
||||
'role_id.exists' => 'The selected role is invalid.',
|
||||
'description.max' => 'The description cannot exceed 1000 characters.'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom attributes for validator errors.
|
||||
*/
|
||||
public function attributes(): array
|
||||
{
|
||||
return [
|
||||
'user_id' => 'user',
|
||||
'role_id' => 'role',
|
||||
'description' => 'description'
|
||||
];
|
||||
}
|
||||
}
|
||||
44
app/Components/Admin/Http/Requests/WebPageRequest.php
Normal file
44
app/Components/Admin/Http/Requests/WebPageRequest.php
Normal file
@ -0,0 +1,44 @@
|
||||
<?php
|
||||
namespace App\Components\Admin\Http\Requests;
|
||||
|
||||
use App\Traits\WebPageAuthorization;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class WebPageRequest extends FormRequest
|
||||
{
|
||||
use WebPageAuthorization;
|
||||
protected string $resourceType = 'web_pages';
|
||||
protected string $resourceValue = 'admin.web-pages.php';
|
||||
|
||||
public function authorize(): bool
|
||||
{
|
||||
// return true;
|
||||
$webPageId = $this->route('web_page')?->id;
|
||||
$action = $webPageId ? 'edit' : 'create';
|
||||
|
||||
return $this->checkResourcePermission($this->resourceType, $this->resourceValue, $action);
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'url' => [
|
||||
'required',
|
||||
'string',
|
||||
'max:255',
|
||||
Rule::unique('pgsql.config.tbl_web_pages', 'url')->ignore($this->route('web_page'))
|
||||
],
|
||||
'description' => ['nullable', 'string']
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'url.required' => 'The URL is required.',
|
||||
'url.max' => 'The URL cannot exceed 255 characters.',
|
||||
'url.unique' => 'This URL is already in use.',
|
||||
];
|
||||
}
|
||||
}
|
||||
46
app/Components/Admin/Providers/AdminServiceProvider.php
Normal file
46
app/Components/Admin/Providers/AdminServiceProvider.php
Normal file
@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Components\Admin\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use App\Components\Admin\Http\Middleware\AdminAuthentication;
|
||||
use App\Components\Admin\View\Composers\NavigationComposer;
|
||||
use Illuminate\Support\Facades\View;
|
||||
|
||||
class AdminServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function register(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
public function boot(): void
|
||||
{
|
||||
// Update route model binding to new Laravel 11 syntax
|
||||
Route::bind('resource_type_mapping', function ($value) {
|
||||
return \App\Models\ResourceTypeMapping::findOrFail($value);
|
||||
});
|
||||
|
||||
// Load routes
|
||||
$this->loadRoutesFrom(__DIR__ . '/../routes/web.php');
|
||||
|
||||
// Load views with namespace
|
||||
$this->loadViewsFrom(__DIR__ . '/../resources/views', 'admin');
|
||||
|
||||
// Register middleware using new Laravel 11 method
|
||||
$this->app['router']->middlewareGroup('admin', [
|
||||
AdminAuthentication::class,
|
||||
]);
|
||||
|
||||
// Register view composer
|
||||
View::composer('admin::layouts.admin', NavigationComposer::class);
|
||||
|
||||
// Publishing assets
|
||||
if ($this->app->runningInConsole()) {
|
||||
$this->publishes([
|
||||
__DIR__ . '/../resources/views' => resource_path('views/vendor/admin'),
|
||||
], 'admin-views');
|
||||
}
|
||||
}
|
||||
}
|
||||
91
app/Components/Admin/Traits/ResourceAuthorization.php
Normal file
91
app/Components/Admin/Traits/ResourceAuthorization.php
Normal file
@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
namespace App\Components\Admin\Traits;
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
trait ResourceAuthorization
|
||||
{
|
||||
/**
|
||||
* Check if the current user has permission for a specific resource and action
|
||||
*
|
||||
* @param string $resourceType The type of resource (e.g., 'web_pages')
|
||||
* @param string $permission The permission to check (e.g., 'view', 'edit')
|
||||
* @param int|null $resourceId Specific resource ID if checking a single resource
|
||||
* @return bool
|
||||
*/
|
||||
public function checkResourcePermission(string $resourceType, string $permission, ?int $resourceId = null): bool
|
||||
{
|
||||
try {
|
||||
// Base query to check permissions
|
||||
$query = DB::table('auth.vw_user_authorizations')
|
||||
->where('user_id', auth()->id())
|
||||
->where('resource_type', $resourceType)
|
||||
->where('permission_name', $permission);
|
||||
|
||||
// If resourceId is provided, check specific resource or null (all resources)
|
||||
if ($resourceId !== null) {
|
||||
$query->where(function($q) use ($resourceId) {
|
||||
$q->where('resource_id', $resourceId)
|
||||
->orWhereNull('resource_id');
|
||||
});
|
||||
}
|
||||
|
||||
$hasPermission = $query->exists();
|
||||
|
||||
// Log authorization check
|
||||
Log::debug('Authorization check', [
|
||||
'user_id' => auth()->id(),
|
||||
'resource_type' => $resourceType,
|
||||
'permission' => $permission,
|
||||
'resource_id' => $resourceId,
|
||||
'granted' => $hasPermission
|
||||
]);
|
||||
|
||||
return $hasPermission;
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Authorization check failed', [
|
||||
'error' => $e->getMessage(),
|
||||
'user_id' => auth()->id(),
|
||||
'resource_type' => $resourceType,
|
||||
'permission' => $permission,
|
||||
'resource_id' => $resourceId
|
||||
]);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all resources of a specific type that the user has permission to access
|
||||
*
|
||||
* @param string $resourceType
|
||||
* @param string $permission
|
||||
* @return array
|
||||
*/
|
||||
public function getAuthorizedResourceIds(string $resourceType, string $permission): array
|
||||
{
|
||||
try {
|
||||
return DB::table('auth.vw_user_authorizations')
|
||||
->where('user_id', auth()->id())
|
||||
->where('resource_type', $resourceType)
|
||||
->where('permission_name', $permission)
|
||||
->pluck('resource_id')
|
||||
->filter()
|
||||
->toArray();
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Failed to get authorized resource IDs', [
|
||||
'error' => $e->getMessage(),
|
||||
'user_id' => auth()->id(),
|
||||
'resource_type' => $resourceType,
|
||||
'permission' => $permission
|
||||
]);
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
36
app/Components/Admin/Traits/UserAuthorization.php
Normal file
36
app/Components/Admin/Traits/UserAuthorization.php
Normal file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Components\Admin\Traits;
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
trait UserAuthorization
|
||||
{
|
||||
/**
|
||||
* Check if the user has permission for a specific resource and action
|
||||
*
|
||||
* @param string $resourceType
|
||||
* @param string $resourceValue
|
||||
* @param string $permission
|
||||
* @param int|null $resourceId
|
||||
* @return bool
|
||||
*/
|
||||
// public function checkResourcePermission(string $resourceType, string $permission, ?int $resourceId = null): bool
|
||||
public function checkResourcePermission(string $resourceType, string $resourceValue, $permission): bool
|
||||
{
|
||||
$query = DB::table('auth.vw_user_authorizations')
|
||||
->where('user_id', $this->id)
|
||||
->where('resource_type', $resourceType)
|
||||
->where('resource_value', $resourceValue)
|
||||
->where('permission_name', $permission);
|
||||
|
||||
// if ($resourceId !== null) {
|
||||
// $query->where(function($q) use ($resourceId) {
|
||||
// $q->where('resource_id', $resourceId)
|
||||
// ->orWhereNull('resource_id');
|
||||
// });
|
||||
// }
|
||||
|
||||
return $query->exists();
|
||||
}
|
||||
}
|
||||
20
app/Components/Admin/View/Composers/NavigationComposer.php
Normal file
20
app/Components/Admin/View/Composers/NavigationComposer.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Components\Admin\View\Composers;
|
||||
|
||||
use App\Models\NavigationItem;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class NavigationComposer
|
||||
{
|
||||
public function compose(View $view)
|
||||
{
|
||||
$navigationItems = NavigationItem::with(['children'])
|
||||
->whereNull('parent_id')
|
||||
->where('is_active', true)
|
||||
->orderBy('order_index')
|
||||
->get();
|
||||
|
||||
$view->with('navigationItems', $navigationItems);
|
||||
}
|
||||
}
|
||||
205
app/Components/Admin/resources/views/admin/dashboard.blade.php
Normal file
205
app/Components/Admin/resources/views/admin/dashboard.blade.php
Normal file
@ -0,0 +1,205 @@
|
||||
@extends('admin::layouts.admin')
|
||||
@section('title', 'Admin Dashboard')
|
||||
|
||||
@push('scripts')
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
<main class="flex-1">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-bold">Admin Dashboard</h1>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
|
||||
<!-- System Statistics -->
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<h2 class="text-xl font-semibold mb-4">System Statistics</h2>
|
||||
<div class="space-y-4">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-gray-600">Users</span>
|
||||
<span class="font-semibold">{{ $systemStats['users_count'] }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-gray-600">Roles</span>
|
||||
<span class="font-semibold">{{ $systemStats['roles_count'] }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-gray-600">User Roles</span>
|
||||
<span class="font-semibold">{{ $systemStats['user_roles_count'] }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-gray-600">Resource Types</span>
|
||||
<span class="font-semibold">{{ $systemStats['resources_count'] }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-gray-600">Resource Type Mappings</span>
|
||||
<span class="font-semibold">{{ $systemStats['resource_mappings_count'] }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Database Statistics -->
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<h2 class="text-xl font-semibold mb-4">Database Statistics</h2>
|
||||
<div class="space-y-4">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-gray-600">Database Size</span>
|
||||
<span class="font-semibold">{{ $dbStats[0]->db_size }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-gray-600">Schemas</span>
|
||||
<span class="font-semibold">{{ $dbStats[0]->schema_count }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-gray-600">Tables</span>
|
||||
<span class="font-semibold">{{ $dbStats[0]->table_count }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-gray-600">Views</span>
|
||||
<span class="font-semibold">{{ $dbStats[0]->view_count }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-gray-600">Materialized Views</span>
|
||||
<span class="font-semibold">{{ $dbStats[0]->materialized_view_count }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-gray-600">Indexes</span>
|
||||
<span class="font-semibold">{{ $dbStats[0]->index_count }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Database I/O Monitor -->
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<h2 class="text-xl font-semibold mb-4">Database I/O Activity</h2>
|
||||
<div class="h-64">
|
||||
<canvas id="ioChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Database Operations Monitor -->
|
||||
<div class="bg-white rounded-lg shadow p-6 mt-6">
|
||||
<h2 class="text-xl font-semibold mb-4">Database Operations</h2>
|
||||
<div class="h-64">
|
||||
<canvas id="operationsChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize the I/O chart
|
||||
const ctx = document.getElementById('ioChart').getContext('2d');
|
||||
const ioChart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [{
|
||||
label: 'Blocks Read/sec',
|
||||
borderColor: '#EF4444',
|
||||
data: [],
|
||||
fill: false
|
||||
}, {
|
||||
label: 'Cache Hits/sec',
|
||||
borderColor: '#10B981',
|
||||
data: [],
|
||||
fill: false
|
||||
}, {
|
||||
label: 'Tuples Returned/sec',
|
||||
borderColor: '#3B82F6',
|
||||
data: [],
|
||||
fill: false
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true
|
||||
}
|
||||
},
|
||||
animation: false
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize the operations chart
|
||||
const opCtx = document.getElementById('operationsChart').getContext('2d');
|
||||
const operationsChart = new Chart(opCtx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [{
|
||||
label: 'Inserts/sec',
|
||||
borderColor: '#10B981',
|
||||
data: [],
|
||||
fill: false
|
||||
}, {
|
||||
label: 'Updates/sec',
|
||||
borderColor: '#F59E0B',
|
||||
data: [],
|
||||
fill: false
|
||||
}, {
|
||||
label: 'Deletes/sec',
|
||||
borderColor: '#EF4444',
|
||||
data: [],
|
||||
fill: false
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true
|
||||
}
|
||||
},
|
||||
animation: false
|
||||
}
|
||||
});
|
||||
|
||||
// Function to update both charts
|
||||
async function updateCharts() {
|
||||
try {
|
||||
const response = await fetch('{{ route('admin.database.io') }}');
|
||||
const data = await response.json();
|
||||
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
|
||||
// Update I/O Chart
|
||||
ioChart.data.labels.push(timestamp);
|
||||
ioChart.data.datasets[0].data.push(data.blks_read);
|
||||
ioChart.data.datasets[1].data.push(data.blks_hit);
|
||||
ioChart.data.datasets[2].data.push(data.tup_returned);
|
||||
|
||||
// Update Operations Chart
|
||||
operationsChart.data.labels.push(timestamp);
|
||||
operationsChart.data.datasets[0].data.push(data.tup_inserted);
|
||||
operationsChart.data.datasets[1].data.push(data.tup_updated);
|
||||
operationsChart.data.datasets[2].data.push(data.tup_deleted);
|
||||
|
||||
// Keep only last 20 points for both charts
|
||||
if (ioChart.data.labels.length > 20) {
|
||||
ioChart.data.labels.shift();
|
||||
ioChart.data.datasets.forEach(dataset => dataset.data.shift());
|
||||
|
||||
operationsChart.data.labels.shift();
|
||||
operationsChart.data.datasets.forEach(dataset => dataset.data.shift());
|
||||
}
|
||||
|
||||
// Update both charts
|
||||
ioChart.update();
|
||||
operationsChart.update();
|
||||
} catch (error) {
|
||||
console.error('Error fetching database stats:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Update every 2 seconds
|
||||
setInterval(updateCharts, 2000);
|
||||
// Initial update
|
||||
updateCharts();
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
128
app/Components/Admin/resources/views/layouts/admin.blade.php
Normal file
128
app/Components/Admin/resources/views/layouts/admin.blade.php
Normal file
@ -0,0 +1,128 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Admin - @yield('title')</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
|
||||
</head>
|
||||
<body class="bg-gray-100 h-screen flex">
|
||||
<!-- Sidebar -->
|
||||
<div x-data="{ open: false }"
|
||||
class="fixed inset-y-0 left-0 z-30 w-96 bg-gray-800 text-white transform transition-transform duration-300 ease-in-out
|
||||
md:relative md:translate-x-0
|
||||
{{ $errors->any() ? '-translate-x-full' : '' }}"
|
||||
:class="{ '-translate-x-full': !open }">
|
||||
<!-- Sidebar Header -->
|
||||
<div class="flex items-center justify-between p-4 border-b border-gray-700">
|
||||
<div class="flex items-center">
|
||||
<a href="{{ route('admin.admin.dashboard') }}">
|
||||
<img src="{{ asset('images/pawpaw_logo.png') }}" alt="Admin Logo" class="w-full h-auto object-contain">
|
||||
</a>
|
||||
</div>
|
||||
<button @click="open = false" class="md:hidden">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Admin Panel Title -->
|
||||
<div class="flex items-center justify-between p-4 border-b border-gray-700">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-shield-alt mr-2"></i>
|
||||
<span class="text-xl font-bold">Admin Panel</span>
|
||||
</div>
|
||||
<button @click="open = false" class="md:hidden">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Navigation Links -->
|
||||
<nav class="mt-5">
|
||||
@foreach($navigationItems as $item)
|
||||
<a href="{{ route($item->route) }}"
|
||||
class="flex items-center py-2 px-4 hover:bg-gray-700 {{ request()->routeIs($item->route . '*') ? 'bg-gray-700' : '' }}">
|
||||
@if($item->icon)
|
||||
<i class="{{ \App\Helpers\IconHelper::getIconClasses($item->icon) }} w-6 mr-2"></i>
|
||||
@endif
|
||||
<span>{{ $item->name }}</span>
|
||||
</a>
|
||||
@endforeach
|
||||
</nav>
|
||||
|
||||
<!-- Profile Section -->
|
||||
<div class="absolute bottom-0 left-0 right-0 p-4 border-t border-gray-700">
|
||||
<div x-data="{ profileOpen: false }" class="relative">
|
||||
<button @click="profileOpen = !profileOpen"
|
||||
class="w-full flex items-center justify-between py-2 px-4 hover:bg-gray-700 focus:outline-none">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-user-circle mr-2"></i>
|
||||
<span>{{ Auth::user()->name }}</span>
|
||||
</div>
|
||||
<svg class="w-4 h-4 transition-transform duration-200"
|
||||
:class="{ 'transform rotate-180': profileOpen }"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 9l-7 7-7-7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div x-show="profileOpen"
|
||||
@click.away="profileOpen = false"
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 scale-95"
|
||||
x-transition:enter-end="opacity-100 scale-100"
|
||||
x-transition:leave="transition ease-in duration-75"
|
||||
x-transition:leave-start="opacity-100 scale-100"
|
||||
x-transition:leave-end="opacity-0 scale-95"
|
||||
class="absolute bottom-full left-0 right-0 bg-gray-700 rounded-t-lg shadow-lg">
|
||||
<a href="{{ route('profile.edit') }}"
|
||||
class="flex items-center px-4 py-2 hover:bg-gray-600">
|
||||
<i class="fas fa-user-edit mr-2"></i>
|
||||
<span>My Profile</span>
|
||||
</a>
|
||||
<form method="POST" action="{{ route('logout') }}">
|
||||
@csrf
|
||||
<button type="submit"
|
||||
class="w-full flex items-center px-4 py-2 hover:bg-gray-600 text-left">
|
||||
<i class="fas fa-sign-out-alt mr-2"></i>
|
||||
<span>Logout</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Hamburger Button -->
|
||||
<button @click="open = true"
|
||||
class="fixed top-4 left-4 z-40 md:hidden bg-white p-2 rounded-md shadow-md">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Main Content Area -->
|
||||
<main class="flex-1 overflow-y-auto p-8 ml-0 md:ml-16 mt-0">
|
||||
@if(session('success'))
|
||||
<div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded relative mb-4">
|
||||
{{ session('success') }}
|
||||
</div>
|
||||
@endif
|
||||
@yield('content')
|
||||
</main>
|
||||
|
||||
@stack('scripts')
|
||||
</body>
|
||||
</html>
|
||||
@ -0,0 +1,79 @@
|
||||
@extends('admin::layouts.admin')
|
||||
@section('title', 'Create Menu Type')
|
||||
@section('content')
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-bold">Create Menu Type</h1>
|
||||
<x-auth-check :permission="['type' => $thisResourceType, 'value' => $thisResourceValue, 'action' => 'view']">
|
||||
<a href="{{ route('admin.menu-types.index') }}"
|
||||
class="bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded">
|
||||
Back to List
|
||||
</a>
|
||||
</x-auth-check>
|
||||
</div>
|
||||
|
||||
@if($errors->any())
|
||||
<x-alert type="error" message="Please fix the following errors:">
|
||||
<ul class="mt-2">
|
||||
@foreach($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</x-alert>
|
||||
@endif
|
||||
|
||||
<div class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
|
||||
<x-auth-check :permission="['type' => $thisResourceType, 'value' => $thisResourceValue, 'action' => 'create']">
|
||||
<x-form :action="route('admin.menu-types.store')" method="POST">
|
||||
<x-input-field
|
||||
name="name"
|
||||
label="Name"
|
||||
:value="old('name')"
|
||||
required
|
||||
maxlength="100"
|
||||
placeholder="Enter menu type name"
|
||||
autofocus
|
||||
>
|
||||
<p class="text-gray-600 text-xs mt-1">
|
||||
The name must be unique and can only contain letters, numbers, spaces, hyphens, and underscores.
|
||||
Maximum 100 characters.
|
||||
</p>
|
||||
</x-input-field>
|
||||
|
||||
<div class="mb-6">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="description">
|
||||
Description
|
||||
</label>
|
||||
<textarea
|
||||
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline @error('description') border-red-500 @enderror"
|
||||
id="description"
|
||||
name="description"
|
||||
rows="4"
|
||||
maxlength="255"
|
||||
placeholder="Enter menu type description">{{ old('description') }}</textarea>
|
||||
@error('description')
|
||||
<p class="text-red-500 text-xs italic mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
<p class="text-gray-600 text-xs mt-1">
|
||||
Provide a clear description of what this menu type represents. Maximum 255 characters.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<x-auth-check :permission="['type' => $thisResourceType, 'value' => $thisResourceValue, 'action' => 'view']">
|
||||
<a href="{{ route('admin.menu-types.index') }}"
|
||||
class="text-gray-600 hover:text-gray-800">
|
||||
Cancel
|
||||
</a>
|
||||
</x-auth-check>
|
||||
|
||||
<button type="submit"
|
||||
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">
|
||||
Create Menu Type
|
||||
</button>
|
||||
</div>
|
||||
</x-form>
|
||||
</x-auth-check>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
115
app/Components/Admin/resources/views/menu-types/edit.blade.php
Normal file
115
app/Components/Admin/resources/views/menu-types/edit.blade.php
Normal file
@ -0,0 +1,115 @@
|
||||
@extends('admin::layouts.admin')
|
||||
@section('title', 'Edit Menu Type')
|
||||
@section('content')
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-bold">Edit Menu Type</h1>
|
||||
<x-auth-check :permission="['type' => $thisResourceType, 'value' => $thisResourceValue, 'action' => 'view']">
|
||||
<a href="{{ route('admin.menu-types.index') }}"
|
||||
class="bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded">
|
||||
Back to List
|
||||
</a>
|
||||
</x-auth-check>
|
||||
</div>
|
||||
|
||||
@if($errors->any())
|
||||
<x-alert type="error" message="Please fix the following errors:">
|
||||
<ul class="mt-2">
|
||||
@foreach($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</x-alert>
|
||||
@endif
|
||||
|
||||
<div class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
|
||||
<x-form
|
||||
:action="route('admin.menu-types.update', $menuType)"
|
||||
method="PUT"
|
||||
>
|
||||
<x-input-field
|
||||
name="name"
|
||||
label="Name"
|
||||
:value="old('name', $menuType->name)"
|
||||
required
|
||||
maxlength="100"
|
||||
placeholder="Enter menu type name"
|
||||
>
|
||||
<p class="text-gray-600 text-xs mt-1">
|
||||
The name must be unique and can only contain letters, numbers, spaces, hyphens, and underscores.
|
||||
Maximum 100 characters.
|
||||
</p>
|
||||
</x-input-field>
|
||||
|
||||
<div class="mb-6">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="description">
|
||||
Description
|
||||
</label>
|
||||
<textarea
|
||||
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline @error('description') border-red-500 @enderror"
|
||||
id="description"
|
||||
name="description"
|
||||
rows="4"
|
||||
maxlength="255"
|
||||
placeholder="Enter menu type description">{{ old('description', $menuType->description) }}</textarea>
|
||||
@error('description')
|
||||
<p class="text-red-500 text-xs italic mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
<p class="text-gray-600 text-xs mt-1">
|
||||
Provide a clear description of what this menu type represents. Maximum 255 characters.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<x-auth-check :permission="['type' => $thisResourceType, 'value' => $thisResourceValue, 'action' => 'edit']">
|
||||
<div class="flex items-center justify-end">
|
||||
<button type="submit"
|
||||
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">
|
||||
Update Menu Type
|
||||
</button>
|
||||
</div>
|
||||
</x-auth-check>
|
||||
</x-form>
|
||||
|
||||
<x-auth-check :permission="['type' => $thisResourceType, 'value' => $thisResourceValue, 'action' => 'delete']">
|
||||
<div class="mt-6 pt-6 border-t border-gray-200">
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold text-red-600">Danger Zone</h2>
|
||||
<p class="text-sm text-gray-600 mt-1">
|
||||
@if($menuType->navigation_items_count > 0)
|
||||
This menu type has {{ $menuType->navigation_items_count }} associated navigation items and cannot be deleted.
|
||||
@else
|
||||
Once you delete a menu type, it cannot be recovered.
|
||||
@endif
|
||||
</p>
|
||||
</div>
|
||||
<x-form
|
||||
:action="route('admin.menu-types.destroy', $menuType)"
|
||||
method="DELETE"
|
||||
onsubmit="return confirm('Are you sure you want to delete this menu type? This action cannot be undone.');"
|
||||
>
|
||||
<button type="submit"
|
||||
class="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline {{ $menuType->navigation_items_count > 0 ? 'opacity-50 cursor-not-allowed' : '' }}"
|
||||
{{ $menuType->navigation_items_count > 0 ? 'disabled' : '' }}>
|
||||
Delete Menu Type
|
||||
</button>
|
||||
</x-form>
|
||||
</div>
|
||||
</div>
|
||||
</x-auth-check>
|
||||
</div>
|
||||
|
||||
@if($menuType->navigation_items_count > 0)
|
||||
<div class="bg-white shadow-md rounded px-8 pt-6 pb-8">
|
||||
<h2 class="text-lg font-semibold mb-4">Associated Navigation Items</h2>
|
||||
<p class="text-sm text-gray-600 mb-2">
|
||||
This menu type is currently being used by {{ $menuType->navigation_items_count }} navigation items.
|
||||
You must remove or reassign these items before this menu type can be deleted.
|
||||
</p>
|
||||
<a href="#" class="text-blue-600 hover:text-blue-800 text-sm">
|
||||
View Navigation Items
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endsection
|
||||
153
app/Components/Admin/resources/views/menu-types/index.blade.php
Normal file
153
app/Components/Admin/resources/views/menu-types/index.blade.php
Normal file
@ -0,0 +1,153 @@
|
||||
@extends('admin::layouts.admin')
|
||||
@section('title', 'Menu Types')
|
||||
@section('content')
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-bold">Menu Types</h1>
|
||||
<x-auth-check :permission="['type' => $thisResourceType, 'value' => $thisResourceValue, 'action' => 'create']">
|
||||
<a href="{{ route('admin.menu-types.create') }}"
|
||||
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
|
||||
Create New Menu Type
|
||||
</a>
|
||||
</x-auth-check>
|
||||
</div>
|
||||
|
||||
<x-alert type="success" :message="session('success')" />
|
||||
<x-alert type="error" :message="session('error')" />
|
||||
|
||||
<!-- Search Section -->
|
||||
<div class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
|
||||
<x-form
|
||||
:action="route('admin.menu-types.index')"
|
||||
method="GET"
|
||||
class="flex gap-4 items-end"
|
||||
>
|
||||
<div class="flex-1">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="search">
|
||||
Search
|
||||
</label>
|
||||
<input type="text"
|
||||
id="search"
|
||||
name="search"
|
||||
value="{{ request('search') }}"
|
||||
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
||||
placeholder="Search menu types...">
|
||||
</div>
|
||||
<div class="w-48">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="sort">
|
||||
Sort By
|
||||
</label>
|
||||
<select id="sort"
|
||||
name="sort"
|
||||
class="shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline">
|
||||
<option value="name" {{ request('sort', 'name') === 'name' ? 'selected' : '' }}>Name</option>
|
||||
<option value="created_at" {{ request('sort') === 'created_at' ? 'selected' : '' }}>Date Created</option>
|
||||
<option value="updated_at" {{ request('sort') === 'updated_at' ? 'selected' : '' }}>Last Updated</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="w-48">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="direction">
|
||||
Order
|
||||
</label>
|
||||
<select id="direction"
|
||||
name="direction"
|
||||
class="shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline">
|
||||
<option value="asc" {{ request('direction', 'asc') === 'asc' ? 'selected' : '' }}>Ascending</option>
|
||||
<option value="desc" {{ request('direction') === 'desc' ? 'selected' : '' }}>Descending</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex items-end space-x-2">
|
||||
<button type="submit"
|
||||
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">
|
||||
Filter
|
||||
</button>
|
||||
@if(request('search') || request('sort') || request('direction'))
|
||||
<a href="{{ route('admin.menu-types.index') }}"
|
||||
class="bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">
|
||||
Clear
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
</x-form>
|
||||
</div>
|
||||
|
||||
<div class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
|
||||
@if($menuTypes->isEmpty())
|
||||
<div class="text-center py-4">
|
||||
<p class="text-gray-500">No menu types found.</p>
|
||||
</div>
|
||||
@else
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Name
|
||||
</th>
|
||||
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Description
|
||||
</th>
|
||||
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Navigation Items
|
||||
</th>
|
||||
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Last Updated
|
||||
</th>
|
||||
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
@foreach($menuTypes as $menuType)
|
||||
<tr>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm font-medium text-gray-900">
|
||||
{{ $menuType->name }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="text-sm text-gray-900">
|
||||
{{ $menuType->description ?: 'No description' }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm text-gray-900">
|
||||
{{ $menuType->navigation_items_count }} items
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{{ $menuType->updated_at->format('Y-m-d H:i:s') }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
<x-auth-check :permission="['type' => $thisResourceType, 'value' => $thisResourceValue, 'action' => 'edit']">
|
||||
<a href="{{ route('admin.menu-types.edit', $menuType) }}"
|
||||
class="text-indigo-600 hover:text-indigo-900 mr-3">
|
||||
Edit
|
||||
</a>
|
||||
</x-auth-check>
|
||||
|
||||
<x-auth-check :permission="['type' => $thisResourceType, 'value' => $thisResourceValue, 'action' => 'delete']">
|
||||
<x-form
|
||||
:action="route('admin.menu-types.destroy', $menuType)"
|
||||
method="DELETE"
|
||||
class="inline-block"
|
||||
onsubmit="return confirm('Are you sure you want to delete this menu type? This action cannot be undone.');"
|
||||
>
|
||||
<button type="submit"
|
||||
class="text-red-600 hover:text-red-900 {{ $menuType->navigation_items_count > 0 ? 'opacity-50 cursor-not-allowed' : '' }}"
|
||||
{{ $menuType->navigation_items_count > 0 ? 'disabled' : '' }}>
|
||||
Delete
|
||||
</button>
|
||||
</x-form>
|
||||
</x-auth-check>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="mt-4">
|
||||
{{ $menuTypes->links() }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endsection
|
||||
123
app/Components/Admin/resources/views/migrations/index.blade.php
Normal file
123
app/Components/Admin/resources/views/migrations/index.blade.php
Normal file
@ -0,0 +1,123 @@
|
||||
@extends('admin::layouts.admin')
|
||||
@section('title', 'Migrations')
|
||||
@section('content')
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-bold">Database Migrations</h1>
|
||||
</div>
|
||||
|
||||
<x-alert type="success" :message="session('success')" />
|
||||
<x-alert type="error" :message="session('error')" />
|
||||
|
||||
<!-- Search Section -->
|
||||
<div class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
|
||||
<x-form :action="route('admin.migrations.index')" method="GET" class="flex gap-4 items-end">
|
||||
<div class="flex-1">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="search">
|
||||
Search
|
||||
</label>
|
||||
<input type="text"
|
||||
id="search"
|
||||
name="search"
|
||||
value="{{ request('search') }}"
|
||||
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
||||
placeholder="Search migrations...">
|
||||
</div>
|
||||
<div class="w-48">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="sort">
|
||||
Sort By
|
||||
</label>
|
||||
<select id="sort"
|
||||
name="sort"
|
||||
class="shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline">
|
||||
<option value="id" {{ request('sort', 'id') === 'id' ? 'selected' : '' }}>ID</option>
|
||||
<option value="migration" {{ request('sort') === 'migration' ? 'selected' : '' }}>Migration Name</option>
|
||||
<option value="batch" {{ request('sort') === 'batch' ? 'selected' : '' }}>Batch</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="w-48">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="direction">
|
||||
Direction
|
||||
</label>
|
||||
<select id="direction"
|
||||
name="direction"
|
||||
class="shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline">
|
||||
<option value="desc" {{ request('direction', 'desc') === 'desc' ? 'selected' : '' }}>Newest First</option>
|
||||
<option value="asc" {{ request('direction') === 'asc' ? 'selected' : '' }}>Oldest First</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit"
|
||||
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">
|
||||
Filter
|
||||
</button>
|
||||
@if(request()->hasAny(['search', 'sort', 'direction']))
|
||||
<a href="{{ route('admin.migrations.index') }}"
|
||||
class="ml-2 text-gray-600 hover:text-gray-900">
|
||||
Clear
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
</x-form>
|
||||
</div>
|
||||
|
||||
<div class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
|
||||
@if($migrations->isEmpty())
|
||||
<div class="text-center py-4">
|
||||
<p class="text-gray-500">No migrations found.</p>
|
||||
</div>
|
||||
@else
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
ID
|
||||
</th>
|
||||
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Migration
|
||||
</th>
|
||||
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Batch
|
||||
</th>
|
||||
@if(auth()->user()->checkResourcePermission($thisResourceType, $thisResourceValue, 'view'))
|
||||
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Actions
|
||||
</th>
|
||||
@endif
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
@foreach($migrations as $migration)
|
||||
<tr>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
{{ $migration->id }}
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="text-sm text-gray-900">
|
||||
{{ $migration->formatted_name }}
|
||||
</div>
|
||||
<div class="text-sm text-gray-500">
|
||||
{{ $migration->migration }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
{{ $migration->batch }}
|
||||
</td>
|
||||
@if(auth()->user()->checkResourcePermission($thisResourceType, $thisResourceValue, 'view'))
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
<a href="{{ route('admin.migrations.show', $migration->id) }}"
|
||||
class="text-indigo-600 hover:text-indigo-900">
|
||||
Details
|
||||
</a>
|
||||
</td>
|
||||
@endif
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="mt-4">
|
||||
{{ $migrations->links() }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endsection
|
||||
120
app/Components/Admin/resources/views/migrations/show.blade.php
Normal file
120
app/Components/Admin/resources/views/migrations/show.blade.php
Normal file
@ -0,0 +1,120 @@
|
||||
@extends('admin::layouts.admin')
|
||||
@section('title', 'Migration Details')
|
||||
@section('content')
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-bold">Migration Details</h1>
|
||||
@if(auth()->user()->checkResourcePermission($thisResourceType, $thisResourceValue, 'view'))
|
||||
<a href="{{ route('admin.migrations.index') }}"
|
||||
class="bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded">
|
||||
Back to List
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<x-alert type="error" :message="session('error')" />
|
||||
|
||||
<div class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
|
||||
<div class="grid grid-cols-1 gap-6">
|
||||
<!-- ID -->
|
||||
<div>
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2">
|
||||
ID
|
||||
</label>
|
||||
<p class="text-gray-900">
|
||||
{{ $migration->id }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Migration Name -->
|
||||
<div>
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2">
|
||||
Migration Name
|
||||
</label>
|
||||
<p class="text-gray-900">
|
||||
{{ $migration->formatted_name }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Full Migration Name -->
|
||||
<div>
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2">
|
||||
Full Migration Name
|
||||
</label>
|
||||
<p class="text-gray-500 font-mono text-sm">
|
||||
{{ $migration->migration }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Batch -->
|
||||
<div>
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2">
|
||||
Batch Number
|
||||
</label>
|
||||
<p class="text-gray-900">
|
||||
{{ $migration->batch }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Timestamp Info -->
|
||||
<div>
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2">
|
||||
Timestamp
|
||||
</label>
|
||||
<p class="text-gray-500">
|
||||
@php
|
||||
$timestamp = preg_match('/^\d{4}_\d{2}_\d{2}_\d{6}/', $migration->migration, $matches)
|
||||
? $matches[0]
|
||||
: null;
|
||||
if ($timestamp) {
|
||||
$datetime = \Carbon\Carbon::createFromFormat('Y_m_d_His', $timestamp);
|
||||
echo $datetime->format('F j, Y g:i:s A');
|
||||
} else {
|
||||
echo 'No timestamp available';
|
||||
}
|
||||
@endphp
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Migration Type -->
|
||||
<div>
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2">
|
||||
Migration Type
|
||||
</label>
|
||||
<p class="text-gray-900">
|
||||
@php
|
||||
$type = 'Unknown';
|
||||
if (str_contains(strtolower($migration->migration), 'create')) {
|
||||
$type = 'Create Table';
|
||||
} elseif (str_contains(strtolower($migration->migration), 'add')) {
|
||||
$type = 'Add Column';
|
||||
} elseif (str_contains(strtolower($migration->migration), 'update')) {
|
||||
$type = 'Update Table';
|
||||
} elseif (str_contains(strtolower($migration->migration), 'alter')) {
|
||||
$type = 'Alter Table';
|
||||
}
|
||||
@endphp
|
||||
{{ $type }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Navigation -->
|
||||
<div class="mt-6 flex justify-between">
|
||||
@if($previousMigration = \App\Models\Migration::where('id', '<', $migration->id)->orderBy('id', 'desc')->first())
|
||||
<a href="{{ route('admin.migrations.show', $previousMigration->id) }}"
|
||||
class="inline-flex items-center px-4 py-2 bg-gray-600 hover:bg-gray-700 text-white font-bold rounded">
|
||||
← Previous Migration
|
||||
</a>
|
||||
@else
|
||||
<div></div>
|
||||
@endif
|
||||
|
||||
@if($nextMigration = \App\Models\Migration::where('id', '>', $migration->id)->orderBy('id')->first())
|
||||
<a href="{{ route('admin.migrations.show', $nextMigration->id) }}"
|
||||
class="inline-flex items-center px-4 py-2 bg-gray-600 hover:bg-gray-700 text-white font-bold rounded">
|
||||
Next Migration →
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
@endsection
|
||||
@ -0,0 +1,231 @@
|
||||
@extends('admin::layouts.admin')
|
||||
@section('title', 'Create Navigation Item')
|
||||
|
||||
@section('content')
|
||||
<div class="w-full max-w-4xl">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-bold">Create Navigation Item</h1>
|
||||
<x-auth-check
|
||||
:permission="['type' => $thisResourceType, 'value' => $thisResourceValue, 'action' => 'view']">
|
||||
<a href="{{ route('admin.navigation-items.index') }}"
|
||||
class="bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded">
|
||||
Back to List
|
||||
</a>
|
||||
</x-auth-check>
|
||||
</div>
|
||||
|
||||
@if($errors->any())
|
||||
<x-alert type="error" message="Please fix the following errors:">
|
||||
<ul class="mt-2">
|
||||
@foreach($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</x-alert>
|
||||
@endif
|
||||
|
||||
<div class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
|
||||
<x-form :action="route('admin.navigation-items.store')" method="POST">
|
||||
<!-- Menu Type -->
|
||||
<div class="mb-6">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="menu_type_id">
|
||||
Menu Type <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<select
|
||||
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline @error('menu_type_id') border-red-500 @enderror"
|
||||
id="menu_type_id"
|
||||
name="menu_type_id"
|
||||
required>
|
||||
<option value="">Select Menu Type</option>
|
||||
@foreach($menuTypes as $menuType)
|
||||
<option
|
||||
value="{{ $menuType->id }}" {{ old('menu_type_id') == $menuType->id ? 'selected' : '' }}>
|
||||
{{ $menuType->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('menu_type_id')
|
||||
<p class="text-red-500 text-xs italic mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
<p class="text-gray-600 text-xs mt-1">Select the type of menu this item belongs to.</p>
|
||||
</div>
|
||||
|
||||
<x-input-field
|
||||
name="name"
|
||||
label="Name"
|
||||
:value="old('name')"
|
||||
required
|
||||
maxlength="255"
|
||||
placeholder="Enter navigation item name"
|
||||
>
|
||||
<p class="text-gray-600 text-xs mt-1">The display name of the navigation item.</p>
|
||||
</x-input-field>
|
||||
|
||||
<!-- Route Selection -->
|
||||
<div class="mb-6">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="route">
|
||||
Route <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<select
|
||||
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline @error('route') border-red-500 @enderror"
|
||||
id="route"
|
||||
name="route"
|
||||
required>
|
||||
<option value="">Select a Route</option>
|
||||
@foreach($availableRoutes as $group => $routes)
|
||||
<optgroup label="{{ Str::title($group) }}">
|
||||
@foreach($routes as $route)
|
||||
<option value="{{ $route['name'] }}"
|
||||
{{ old('route') == $route['name'] ? 'selected' : '' }}
|
||||
title="{{ $route['uri'] }}">
|
||||
{{ $route['name'] }} ({{ $route['uri'] }})
|
||||
</option>
|
||||
@endforeach
|
||||
</optgroup>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('route')
|
||||
<p class="text-red-500 text-xs italic mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
<p class="text-gray-600 text-xs mt-1">Select the route that this navigation item should link to.</p>
|
||||
</div>
|
||||
|
||||
<!-- Icon Selection -->
|
||||
<div class="mb-6">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="icon">
|
||||
Icon
|
||||
</label>
|
||||
<div class="relative">
|
||||
<select
|
||||
class="shadow appearance-none border rounded w-full py-2 pl-10 pr-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline @error('icon') border-red-500 @enderror"
|
||||
id="icon"
|
||||
name="icon">
|
||||
<option value="">No Icon</option>
|
||||
@foreach(\App\Helpers\IconHelper::getCommonIcons() as $icon)
|
||||
<option value="{{ $icon['value'] }}"
|
||||
{{ old('icon') == $icon['value'] ? 'selected' : '' }}
|
||||
data-icon-class="{{ $icon['classes'] }}">
|
||||
{{ $icon['name'] }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
|
||||
<i id="selectedIcon" class="text-gray-500"></i>
|
||||
</div>
|
||||
</div>
|
||||
@error('icon')
|
||||
<p class="text-red-500 text-xs italic mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
<p class="text-gray-600 text-xs mt-1">Select an icon for this navigation item</p>
|
||||
|
||||
<!-- Icon Preview -->
|
||||
<div class="mt-4">
|
||||
<p class="text-sm font-bold mb-2">Available Icons:</p>
|
||||
<div class="grid grid-cols-4 gap-4">
|
||||
@foreach(\App\Helpers\IconHelper::getCommonIcons() as $previewIcon)
|
||||
<div class="flex items-center p-2 cursor-pointer hover:bg-gray-100 rounded"
|
||||
onclick="document.getElementById('icon').value='{{ $previewIcon['value'] }}'; updateIconPreview();">
|
||||
<i class="{{ $previewIcon['classes'] }} mr-2"></i>
|
||||
<span class="text-sm">{{ $previewIcon['name'] }}</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<x-input-field
|
||||
type="number"
|
||||
name="order_index"
|
||||
label="Order Index"
|
||||
:value="old('order_index', 0)"
|
||||
min="0"
|
||||
step="1"
|
||||
>
|
||||
<p class="text-gray-600 text-xs mt-1">Determines the display order of the navigation item (0 =
|
||||
first).</p>
|
||||
</x-input-field>
|
||||
|
||||
<!-- Parent Item -->
|
||||
<div class="mb-6">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="parent_id">
|
||||
Parent Item
|
||||
</label>
|
||||
<select
|
||||
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline @error('parent_id') border-red-500 @enderror"
|
||||
id="parent_id"
|
||||
name="parent_id">
|
||||
<option value="">No Parent</option>
|
||||
@foreach($parentItems as $parentItem)
|
||||
<option
|
||||
value="{{ $parentItem->id }}" {{ old('parent_id') == $parentItem->id ? 'selected' : '' }}>
|
||||
{{ $parentItem->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('parent_id')
|
||||
<p class="text-red-500 text-xs italic mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
<p class="text-gray-600 text-xs mt-1">Optional parent item for creating nested navigation.</p>
|
||||
</div>
|
||||
|
||||
<!-- Active Status -->
|
||||
<div class="mb-6">
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox"
|
||||
name="is_active"
|
||||
value="1"
|
||||
{{ old('is_active', true) ? 'checked' : '' }}
|
||||
class="form-checkbox h-4 w-4 text-blue-600 transition duration-150 ease-in-out">
|
||||
<span class="ml-2 text-gray-700">Active</span>
|
||||
</label>
|
||||
@error('is_active')
|
||||
<p class="text-red-500 text-xs italic mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
<p class="text-gray-600 text-xs mt-1">Whether this navigation item should be visible in the
|
||||
menu.</p>
|
||||
</div>
|
||||
|
||||
<!-- Form Actions -->
|
||||
<div class="flex items-center justify-between">
|
||||
<x-auth-check
|
||||
:permission="['type' => $thisResourceType, 'value' => $thisResourceValue, 'action' => 'view']">
|
||||
<a href="{{ route('admin.navigation-items.index') }}"
|
||||
class="text-gray-600 hover:text-gray-800">
|
||||
Cancel
|
||||
</a>
|
||||
</x-auth-check>
|
||||
|
||||
<button type="submit"
|
||||
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">
|
||||
Create Navigation Item
|
||||
</button>
|
||||
</div>
|
||||
</x-form>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Update icon preview initially
|
||||
updateIconPreview();
|
||||
|
||||
// Add change event listener
|
||||
document.getElementById('icon').addEventListener('change', updateIconPreview);
|
||||
});
|
||||
|
||||
function updateIconPreview() {
|
||||
const select = document.getElementById('icon');
|
||||
const selectedIcon = document.getElementById('selectedIcon');
|
||||
const option = select.options[select.selectedIndex];
|
||||
|
||||
if (option && option.value) {
|
||||
const iconClass = option.getAttribute('data-icon-class');
|
||||
selectedIcon.className = iconClass;
|
||||
} else {
|
||||
selectedIcon.className = '';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
@ -0,0 +1,246 @@
|
||||
@extends('admin::layouts.admin')
|
||||
@section('title', 'Edit Navigation Item')
|
||||
|
||||
@section('content')
|
||||
<div class="w-full">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-bold">Edit Navigation Item</h1>
|
||||
<x-auth-check
|
||||
:permission="['type' => $thisResourceType, 'value' => $thisResourceValue, 'action' => 'view']">
|
||||
<a href="{{ route('admin.navigation-items.index') }}"
|
||||
class="bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded">
|
||||
Back to List
|
||||
</a>
|
||||
</x-auth-check>
|
||||
</div>
|
||||
|
||||
@if($errors->any())
|
||||
<x-alert type="error" message="Please fix the following errors:">
|
||||
<ul class="mt-2">
|
||||
@foreach($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</x-alert>
|
||||
@endif
|
||||
|
||||
<div class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
|
||||
<x-form :action="route('admin.navigation-items.update', $navigationItem)" method="PUT">
|
||||
<!-- Menu Type -->
|
||||
<div class="mb-6">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="menu_type_id">
|
||||
Menu Type <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<select
|
||||
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline @error('menu_type_id') border-red-500 @enderror"
|
||||
id="menu_type_id"
|
||||
name="menu_type_id"
|
||||
required>
|
||||
<option value="">Select Menu Type</option>
|
||||
@foreach($menuTypes as $menuType)
|
||||
<option
|
||||
value="{{ $menuType->id }}" {{ old('menu_type_id', $navigationItem->menu_type_id) == $menuType->id ? 'selected' : '' }}>
|
||||
{{ $menuType->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('menu_type_id')
|
||||
<p class="text-red-500 text-xs italic mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
<p class="text-gray-600 text-xs mt-1">Select the type of menu this item belongs to.</p>
|
||||
</div>
|
||||
|
||||
<x-input-field
|
||||
name="name"
|
||||
label="Name"
|
||||
:value="old('name', $navigationItem->name)"
|
||||
required
|
||||
maxlength="255"
|
||||
placeholder="Enter navigation item name"
|
||||
>
|
||||
<p class="text-gray-600 text-xs mt-1">The display name of the navigation item.</p>
|
||||
</x-input-field>
|
||||
|
||||
<!-- Route Selection -->
|
||||
<div class="mb-6">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="route">
|
||||
Route <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<select
|
||||
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline @error('route') border-red-500 @enderror"
|
||||
id="route"
|
||||
name="route"
|
||||
required>
|
||||
<option value="">Select a Route</option>
|
||||
@foreach($availableRoutes as $group => $routes)
|
||||
<optgroup label="{{ Str::title($group) }}">
|
||||
@foreach($routes as $route)
|
||||
<option value="{{ $route['name'] }}"
|
||||
{{ old('route', $navigationItem->route) == $route['name'] ? 'selected' : '' }}
|
||||
title="{{ $route['uri'] }}">
|
||||
{{ $route['name'] }} ({{ $route['uri'] }})
|
||||
</option>
|
||||
@endforeach
|
||||
</optgroup>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('route')
|
||||
<p class="text-red-500 text-xs italic mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
<p class="text-gray-600 text-xs mt-1">Select the route that this navigation item should link to.</p>
|
||||
</div>
|
||||
|
||||
<!-- Icon Selection -->
|
||||
<div class="mb-6">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="icon">
|
||||
Icon
|
||||
</label>
|
||||
<div class="relative">
|
||||
<select
|
||||
class="shadow appearance-none border rounded w-full py-2 pl-10 pr-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline @error('icon') border-red-500 @enderror"
|
||||
id="icon"
|
||||
name="icon">
|
||||
<option value="">No Icon</option>
|
||||
@foreach(\App\Helpers\IconHelper::getCommonIcons() as $icon)
|
||||
<option value="{{ $icon['value'] }}"
|
||||
{{ old('icon', $navigationItem->icon) == $icon['value'] ? 'selected' : '' }}
|
||||
data-icon-class="{{ $icon['classes'] }}">
|
||||
{{ $icon['name'] }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
|
||||
<i id="selectedIcon" class="text-gray-500"></i>
|
||||
</div>
|
||||
</div>
|
||||
@error('icon')
|
||||
<p class="text-red-500 text-xs italic mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
<p class="text-gray-600 text-xs mt-1">Select an icon for this navigation item</p>
|
||||
|
||||
<!-- Icon Preview -->
|
||||
<div class="mt-4">
|
||||
<p class="text-sm font-bold mb-2">Available Icons:</p>
|
||||
<div class="grid grid-cols-4 gap-4">
|
||||
@foreach(\App\Helpers\IconHelper::getCommonIcons() as $previewIcon)
|
||||
<div class="flex items-center p-2 cursor-pointer hover:bg-gray-100 rounded"
|
||||
onclick="document.getElementById('icon').value='{{ $previewIcon['value'] }}'; updateIconPreview();">
|
||||
<i class="{{ $previewIcon['classes'] }} mr-2"></i>
|
||||
<span class="text-sm">{{ $previewIcon['name'] }}</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<x-input-field
|
||||
type="number"
|
||||
name="order_index"
|
||||
label="Order Index"
|
||||
:value="old('order_index', $navigationItem->order_index)"
|
||||
min="0"
|
||||
step="1"
|
||||
>
|
||||
<p class="text-gray-600 text-xs mt-1">Determines the display order of the navigation item (0 =
|
||||
first).</p>
|
||||
</x-input-field>
|
||||
|
||||
<!-- Parent Item -->
|
||||
<div class="mb-6">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="parent_id">
|
||||
Parent Item
|
||||
</label>
|
||||
<select
|
||||
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline @error('parent_id') border-red-500 @enderror"
|
||||
id="parent_id"
|
||||
name="parent_id">
|
||||
<option value="">No Parent</option>
|
||||
@foreach($parentItems as $parentItem)
|
||||
<option
|
||||
value="{{ $parentItem->id }}" {{ old('parent_id', $navigationItem->parent_id) == $parentItem->id ? 'selected' : '' }}>
|
||||
{{ $parentItem->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('parent_id')
|
||||
<p class="text-red-500 text-xs italic mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
<p class="text-gray-600 text-xs mt-1">Optional parent item for creating nested navigation.</p>
|
||||
</div>
|
||||
|
||||
<!-- Active Status -->
|
||||
<div class="mb-6">
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox"
|
||||
name="is_active"
|
||||
value="1"
|
||||
{{ old('is_active', $navigationItem->is_active) ? 'checked' : '' }}
|
||||
class="form-checkbox h-4 w-4 text-blue-600 transition duration-150 ease-in-out">
|
||||
<span class="ml-2 text-gray-700">Active</span>
|
||||
</label>
|
||||
@error('is_active')
|
||||
<p class="text-red-500 text-xs italic mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
<p class="text-gray-600 text-xs mt-1">Whether this navigation item should be visible in the
|
||||
menu.</p>
|
||||
</div>
|
||||
|
||||
<!-- Form Actions -->
|
||||
<div class="flex items-center justify-end">
|
||||
<button type="submit"
|
||||
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">
|
||||
Update Navigation Item
|
||||
</button>
|
||||
</div>
|
||||
</x-form>
|
||||
|
||||
<!-- Danger Zone -->
|
||||
<x-auth-check
|
||||
:permission="['type' => $thisResourceType, 'value' => $thisResourceValue, 'action' => 'delete']">
|
||||
<div class="mt-6 pt-6 border-t border-gray-200">
|
||||
<div class="flex justify-between items-center">
|
||||
<h2 class="text-lg font-semibold text-red-600">Danger Zone</h2>
|
||||
<x-form
|
||||
:action="route('admin.navigation-items.destroy', $navigationItem)"
|
||||
method="DELETE"
|
||||
onsubmit="return confirm('Are you sure you want to delete this navigation item? This action cannot be undone.');"
|
||||
>
|
||||
<button type="submit"
|
||||
class="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">
|
||||
Delete Navigation Item
|
||||
</button>
|
||||
</x-form>
|
||||
</div>
|
||||
<p class="text-gray-600 text-sm mt-2">
|
||||
Once you delete a navigation item, it cannot be recovered. Please be certain.
|
||||
</p>
|
||||
</div>
|
||||
</x-auth-check>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Update icon preview initially
|
||||
updateIconPreview();
|
||||
|
||||
// Add change event listener
|
||||
document.getElementById('icon').addEventListener('change', updateIconPreview);
|
||||
});
|
||||
|
||||
function updateIconPreview() {
|
||||
const select = document.getElementById('icon');
|
||||
const selectedIcon = document.getElementById('selectedIcon');
|
||||
const option = select.options[select.selectedIndex];
|
||||
|
||||
if (option && option.value) {
|
||||
const iconClass = option.getAttribute('data-icon-class');
|
||||
selectedIcon.className = iconClass;
|
||||
} else {
|
||||
selectedIcon.className = '';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
@endsection
|
||||
@ -0,0 +1,181 @@
|
||||
@extends('admin::layouts.admin')
|
||||
@section('title', 'Navigation Items')
|
||||
|
||||
@section('content')
|
||||
<div class="w-full">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-bold">Navigation Items</h1>
|
||||
<x-auth-check
|
||||
:permission="['type' => $thisResourceType, 'value' => $thisResourceValue, 'action' => 'create']">
|
||||
<a href="{{ route('admin.navigation-items.create') }}"
|
||||
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
|
||||
Create New Item
|
||||
</a>
|
||||
</x-auth-check>
|
||||
</div>
|
||||
|
||||
<x-alert type="success" :message="session('success')"/>
|
||||
<x-alert type="error" :message="session('error')"/>
|
||||
|
||||
<!-- Search Section -->
|
||||
<div class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
|
||||
<x-form
|
||||
:action="route('admin.navigation-items.index')"
|
||||
method="GET"
|
||||
class="flex gap-4 items-end"
|
||||
>
|
||||
<div class="flex-1">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="search">
|
||||
Search
|
||||
</label>
|
||||
<input type="text"
|
||||
id="search"
|
||||
name="search"
|
||||
value="{{ request('search') }}"
|
||||
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
||||
placeholder="Search by name or route...">
|
||||
</div>
|
||||
|
||||
<div class="w-64">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="menu_type_id">
|
||||
Menu Type
|
||||
</label>
|
||||
<select id="menu_type_id"
|
||||
name="menu_type_id"
|
||||
class="shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline">
|
||||
<option value="">All Menu Types</option>
|
||||
@foreach($menuTypes as $menuType)
|
||||
<option
|
||||
value="{{ $menuType->id }}" {{ request('menu_type_id') == $menuType->id ? 'selected' : '' }}>
|
||||
{{ $menuType->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="flex items-end space-x-2">
|
||||
<button type="submit"
|
||||
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">
|
||||
Filter
|
||||
</button>
|
||||
@if(request('search') || request('menu_type_id'))
|
||||
<a href="{{ route('admin.navigation-items.index') }}"
|
||||
class="bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">
|
||||
Clear
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
</x-form>
|
||||
</div>
|
||||
|
||||
<div class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
|
||||
@if($navItems->isEmpty())
|
||||
<div class="text-center py-4">
|
||||
<p class="text-gray-500">No navigation items found.</p>
|
||||
</div>
|
||||
@else
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Menu Type
|
||||
</th>
|
||||
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Name
|
||||
</th>
|
||||
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Icon
|
||||
</th>
|
||||
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Route
|
||||
</th>
|
||||
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Order
|
||||
</th>
|
||||
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Parent
|
||||
</th>
|
||||
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Status
|
||||
</th>
|
||||
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
@foreach($navItems as $item)
|
||||
<tr>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
{{ $item->menuType->name }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="flex items-center">
|
||||
@if($item->icon)
|
||||
<i class="{{ \App\Helpers\IconHelper::getIconClasses($item->icon) }} mr-2"></i>
|
||||
@endif
|
||||
{{ $item->name }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
@if($item->icon)
|
||||
{{ $item->icon }}
|
||||
@else
|
||||
<span class="text-gray-400">-</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="text-sm">{{ $item->route }}</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-center">
|
||||
{{ $item->order_index }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
{{ $item->parent?->name ?: '-' }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span
|
||||
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full {{ $item->is_active ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800' }}">
|
||||
{{ $item->is_active ? 'Active' : 'Inactive' }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
<div class="flex space-x-3">
|
||||
<x-auth-check
|
||||
:permission="['type' => $thisResourceType, 'value' => $thisResourceValue, 'action' => 'edit']">
|
||||
<a href="{{ route('admin.navigation-items.edit', $item) }}"
|
||||
class="text-indigo-600 hover:text-indigo-900">
|
||||
Edit
|
||||
</a>
|
||||
</x-auth-check>
|
||||
|
||||
<x-auth-check
|
||||
:permission="['type' => $thisResourceType, 'value' => $thisResourceValue, 'action' => 'delete']">
|
||||
<x-form
|
||||
:action="route('admin.navigation-items.destroy', $item)"
|
||||
method="DELETE"
|
||||
class="inline-block"
|
||||
onsubmit="return confirm('Are you sure you want to delete this navigation item? This action cannot be undone.');"
|
||||
>
|
||||
<button type="submit"
|
||||
class="text-red-600 hover:text-red-900">
|
||||
Delete
|
||||
</button>
|
||||
</x-form>
|
||||
</x-auth-check>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
{{ $navItems->links() }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@ -0,0 +1,55 @@
|
||||
@extends('admin::layouts.admin')
|
||||
@section('title', 'Create Permission')
|
||||
@section('content')
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-bold">Create Permission</h1>
|
||||
<a href="{{ route('admin.permissions.index') }}"
|
||||
class="bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded">
|
||||
Back to List
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@if($errors->any())
|
||||
<x-alert type="error">
|
||||
<ul>
|
||||
@foreach($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</x-alert>
|
||||
@endif
|
||||
|
||||
<div class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
|
||||
<x-form :action="route('admin.permissions.store')" method="POST">
|
||||
<x-input-field
|
||||
name="name"
|
||||
label="Name"
|
||||
:value="old('name')"
|
||||
required
|
||||
/>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="description">
|
||||
Description
|
||||
</label>
|
||||
<textarea
|
||||
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline @error('description') border-red-500 @enderror"
|
||||
id="description"
|
||||
name="description"
|
||||
rows="3">{{ old('description') }}</textarea>
|
||||
@error('description')
|
||||
<p class="text-red-500 text-xs italic">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end">
|
||||
<button type="submit"
|
||||
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">
|
||||
Create Permission
|
||||
</button>
|
||||
</div>
|
||||
</x-form>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@ -0,0 +1,75 @@
|
||||
@extends('admin::layouts.admin')
|
||||
@section('title', 'Edit Permission')
|
||||
@section('content')
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-bold">Edit Permission</h1>
|
||||
<a href="{{ route('admin.permissions.index') }}"
|
||||
class="bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded">
|
||||
Back to List
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@if($errors->any())
|
||||
<x-alert type="error">
|
||||
<ul>
|
||||
@foreach($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</x-alert>
|
||||
@endif
|
||||
|
||||
<div class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
|
||||
<x-form :action="route('admin.permissions.update', $permission)" method="PUT">
|
||||
<x-input-field
|
||||
name="name"
|
||||
label="Name"
|
||||
:value="old('name', $permission->name)"
|
||||
required
|
||||
/>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="description">
|
||||
Description
|
||||
</label>
|
||||
<textarea
|
||||
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline @error('description') border-red-500 @enderror"
|
||||
id="description"
|
||||
name="description"
|
||||
rows="3">{{ old('description', $permission->description) }}</textarea>
|
||||
@error('description')
|
||||
<p class="text-red-500 text-xs italic">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end">
|
||||
<button type="submit"
|
||||
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">
|
||||
Update Permission
|
||||
</button>
|
||||
</div>
|
||||
</x-form>
|
||||
|
||||
<!-- Danger Zone -->
|
||||
<div class="mt-6 pt-6 border-t border-gray-200">
|
||||
<div class="flex justify-between items-center">
|
||||
<h2 class="text-lg font-semibold text-red-600">Danger Zone</h2>
|
||||
<x-form
|
||||
:action="route('admin.permissions.destroy', $permission)"
|
||||
method="DELETE"
|
||||
onsubmit="return confirm('Are you sure you want to delete this permission? This action cannot be undone.');"
|
||||
>
|
||||
<button type="submit"
|
||||
class="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">
|
||||
Delete Permission
|
||||
</button>
|
||||
</x-form>
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-gray-600">
|
||||
Once you delete this permission, there is no going back. Please be certain.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
122
app/Components/Admin/resources/views/permissions/index.blade.php
Normal file
122
app/Components/Admin/resources/views/permissions/index.blade.php
Normal file
@ -0,0 +1,122 @@
|
||||
@extends('admin::layouts.admin')
|
||||
@section('title', 'Permissions')
|
||||
@section('content')
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-bold">Permissions</h1>
|
||||
<x-auth-check :permission="['type' => $thisResourceType, 'value' => $thisResourceValue, 'action' => 'create']">
|
||||
<a href="{{ route('admin.permissions.create') }}"
|
||||
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
|
||||
Create New Permission
|
||||
</a>
|
||||
</x-auth-check>
|
||||
</div>
|
||||
|
||||
<x-alert type="success" :message="session('success')" />
|
||||
<x-alert type="error" :message="session('error')" />
|
||||
|
||||
<!-- Search Section -->
|
||||
<div class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
|
||||
<x-form
|
||||
:action="route('admin.permissions.index')"
|
||||
method="GET"
|
||||
class="flex gap-4 items-end"
|
||||
>
|
||||
<div class="flex-1">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="search">
|
||||
Search
|
||||
</label>
|
||||
<input type="text"
|
||||
id="search"
|
||||
name="search"
|
||||
value="{{ request('search') }}"
|
||||
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
||||
placeholder="Search permissions...">
|
||||
</div>
|
||||
<div class="w-48">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="sort">
|
||||
Sort By
|
||||
</label>
|
||||
<select id="sort"
|
||||
name="sort"
|
||||
class="shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline">
|
||||
<option value="name" {{ request('sort', 'name') === 'name' ? 'selected' : '' }}>Name</option>
|
||||
<option value="created_at" {{ request('sort') === 'created_at' ? 'selected' : '' }}>Date Created</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit"
|
||||
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">
|
||||
Filter
|
||||
</button>
|
||||
@if(request('search') || request('sort'))
|
||||
<a href="{{ route('admin.permissions.index') }}"
|
||||
class="ml-2 text-gray-600 hover:text-gray-900">
|
||||
Clear
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
</x-form>
|
||||
</div>
|
||||
|
||||
<div class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
|
||||
@if($permissions->isEmpty())
|
||||
<div class="text-center py-4">
|
||||
<p class="text-gray-500">No permissions found.</p>
|
||||
</div>
|
||||
@else
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Name
|
||||
</th>
|
||||
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Description
|
||||
</th>
|
||||
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
@foreach($permissions as $permission)
|
||||
<tr>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
{{ $permission->name }}
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="text-sm text-gray-900">
|
||||
{{ $permission->description ?: 'No description' }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
<x-auth-check :permission="['type' => $thisResourceType, 'value' => $thisResourceValue, 'action' => 'edit']">
|
||||
<a href="{{ route('admin.permissions.edit', $permission) }}"
|
||||
class="text-indigo-600 hover:text-indigo-900 mr-3">Edit</a>
|
||||
</x-auth-check>
|
||||
|
||||
<x-auth-check :permission="['type' => $thisResourceType, 'value' => $thisResourceValue, 'action' => 'delete']">
|
||||
<x-form
|
||||
:action="route('admin.permissions.destroy', $permission)"
|
||||
method="DELETE"
|
||||
class="inline-block"
|
||||
onsubmit="return confirm('Are you sure you want to delete this permission?');"
|
||||
>
|
||||
<button type="submit"
|
||||
class="text-red-600 hover:text-red-900">
|
||||
Delete
|
||||
</button>
|
||||
</x-form>
|
||||
</x-auth-check>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="mt-4">
|
||||
{{ $permissions->links() }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endsection
|
||||
@ -0,0 +1,250 @@
|
||||
@extends('admin::layouts.admin')
|
||||
@section('title', 'Create Resource Association')
|
||||
@section('content')
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-bold">Create Resource Association</h1>
|
||||
@if(auth()->user()->checkResourcePermission($thisResourceType, $thisResourceValue, 'view'))
|
||||
<a href="{{ route('admin.resource-associations.index') }}"
|
||||
class="bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded">
|
||||
Back to List
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if($errors->any())
|
||||
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative mb-4">
|
||||
<strong class="font-bold">Please fix the following errors:</strong>
|
||||
<ul class="mt-2">
|
||||
@foreach($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
|
||||
@if(auth()->user()->checkResourcePermission($thisResourceType, $thisResourceValue, 'create'))
|
||||
<x-form :action="route('admin.resource-associations.store')" method="POST">
|
||||
<div class="mb-6">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="user_id">
|
||||
User <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<select class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline @error('user_id') border-red-500 @enderror"
|
||||
id="user_id"
|
||||
name="user_id"
|
||||
required>
|
||||
<option value="">Select a user</option>
|
||||
@foreach($users as $user)
|
||||
<option value="{{ $user->id }}" {{ old('user_id') == $user->id ? 'selected' : '' }}>
|
||||
{{ $user->name }} ({{ $user->email }})
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('user_id')
|
||||
<p class="text-red-500 text-xs italic mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="resource_type_id">
|
||||
Resource Type <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<select class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline @error('resource_type_id') border-red-500 @enderror"
|
||||
id="resource_type_id"
|
||||
name="resource_type_id"
|
||||
required>
|
||||
<option value="">Select a resource type</option>
|
||||
@foreach($resourceTypes as $resourceType)
|
||||
<option value="{{ $resourceType->id }}" {{ old('resource_type_id') == $resourceType->id ? 'selected' : '' }}>
|
||||
{{ $resourceType->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('resource_type_id')
|
||||
<p class="text-red-500 text-xs italic mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="role_id">
|
||||
Role <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<select class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline @error('role_id') border-red-500 @enderror"
|
||||
id="role_id"
|
||||
name="role_id"
|
||||
required
|
||||
disabled>
|
||||
<option value="">Select a user first</option>
|
||||
</select>
|
||||
@error('role_id')
|
||||
<p class="text-red-500 text-xs italic mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
<p class="text-gray-600 text-xs mt-1">Only roles assigned to the selected user will be shown.</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="resource_id">
|
||||
Resource
|
||||
</label>
|
||||
<select class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline @error('resource_id') border-red-500 @enderror"
|
||||
id="resource_id"
|
||||
name="resource_id"
|
||||
disabled>
|
||||
<option value="">Select a resource type first</option>
|
||||
</select>
|
||||
@error('resource_id')
|
||||
<p class="text-red-500 text-xs italic mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
<p class="text-gray-600 text-xs mt-1">Select a resource type first to load available resources.</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="description">
|
||||
Description
|
||||
</label>
|
||||
<textarea
|
||||
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline @error('description') border-red-500 @enderror"
|
||||
id="description"
|
||||
name="description"
|
||||
rows="3"
|
||||
placeholder="Enter a description">{{ old('description') }}</textarea>
|
||||
@error('description')
|
||||
<p class="text-red-500 text-xs italic mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
@if(auth()->user()->checkResourcePermission($thisResourceType, $thisResourceValue, 'view'))
|
||||
<a href="{{ route('admin.resource-associations.index') }}"
|
||||
class="text-gray-600 hover:text-gray-800">
|
||||
Cancel
|
||||
</a>
|
||||
@else
|
||||
<div></div>
|
||||
@endif
|
||||
<button type="submit"
|
||||
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">
|
||||
Create Resource Association
|
||||
</button>
|
||||
</div>
|
||||
</x-form>
|
||||
@else
|
||||
<div class="text-red-600 text-center py-4">
|
||||
You do not have permission to create resource associations.
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const userSelect = document.getElementById('user_id');
|
||||
const roleSelect = document.getElementById('role_id');
|
||||
const resourceTypeSelect = document.getElementById('resource_type_id');
|
||||
const resourceSelect = document.getElementById('resource_id');
|
||||
|
||||
// Function to load roles for selected user
|
||||
const loadRoles = async () => {
|
||||
const userId = userSelect.value;
|
||||
roleSelect.disabled = true;
|
||||
|
||||
if (!userId) {
|
||||
roleSelect.innerHTML = '<option value="">Select a user first</option>';
|
||||
roleSelect.disabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
roleSelect.innerHTML = '<option value="">Loading...</option>';
|
||||
|
||||
const response = await fetch(`/admin/resource-associations/roles?user_id=${userId}`);
|
||||
if (!response.ok) {
|
||||
if (response.status === 403) {
|
||||
roleSelect.innerHTML = '<option value="">Unauthorized to view roles</option>';
|
||||
return;
|
||||
}
|
||||
throw new Error('Failed to fetch roles');
|
||||
}
|
||||
|
||||
const roles = await response.json();
|
||||
|
||||
roleSelect.innerHTML = '<option value="">Select a role</option>';
|
||||
roles.forEach(role => {
|
||||
const option = document.createElement('option');
|
||||
option.value = role.id;
|
||||
option.textContent = role.name;
|
||||
option.title = role.description || '';
|
||||
if (role.id == {{ old('role_id', 0) }}) {
|
||||
option.selected = true;
|
||||
}
|
||||
roleSelect.appendChild(option);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error loading roles:', error);
|
||||
roleSelect.innerHTML = '<option value="">Error loading roles</option>';
|
||||
} finally {
|
||||
roleSelect.disabled = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Function to load resources for selected resource type
|
||||
const loadResources = async () => {
|
||||
const resourceTypeId = resourceTypeSelect.value;
|
||||
resourceSelect.disabled = true;
|
||||
|
||||
if (!resourceTypeId) {
|
||||
resourceSelect.innerHTML = '<option value="">Select a resource type first</option>';
|
||||
resourceSelect.disabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
resourceSelect.innerHTML = '<option value="">Loading...</option>';
|
||||
|
||||
const response = await fetch(`/admin/resource-associations/resources?resource_type_id=${resourceTypeId}`);
|
||||
if (!response.ok) {
|
||||
if (response.status === 403) {
|
||||
resourceSelect.innerHTML = '<option value="">Unauthorized to view resources</option>';
|
||||
return;
|
||||
}
|
||||
throw new Error('Failed to fetch resources');
|
||||
}
|
||||
|
||||
const resources = await response.json();
|
||||
|
||||
resourceSelect.innerHTML = '<option value="">Select a resource</option>';
|
||||
resources.forEach(resource => {
|
||||
const option = document.createElement('option');
|
||||
option.value = resource.id;
|
||||
option.textContent = resource.value || `Resource ${resource.id}`;
|
||||
if (resource.id === {{ old('resource_id', 0) }}) {
|
||||
option.selected = true;
|
||||
}
|
||||
resourceSelect.appendChild(option);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error loading resources:', error);
|
||||
resourceSelect.innerHTML = '<option value="">Error loading resources</option>';
|
||||
} finally {
|
||||
resourceSelect.disabled = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Load initial values if they exist
|
||||
if (userSelect.value) {
|
||||
loadRoles();
|
||||
}
|
||||
|
||||
if (resourceTypeSelect.value) {
|
||||
loadResources();
|
||||
}
|
||||
|
||||
// Add event listeners
|
||||
userSelect.addEventListener('change', loadRoles);
|
||||
resourceTypeSelect.addEventListener('change', loadResources);
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
@endsection
|
||||
@ -0,0 +1,264 @@
|
||||
@extends('admin::layouts.admin')
|
||||
@section('title', 'Edit Resource Association')
|
||||
@section('content')
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-bold">Edit Resource Association</h1>
|
||||
@if(auth()->user()->checkResourcePermission($thisResourceType, $thisResourceValue, 'view'))
|
||||
<a href="{{ route('admin.resource-associations.index') }}"
|
||||
class="bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded">
|
||||
Back to List
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if($errors->any())
|
||||
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative mb-4">
|
||||
<strong class="font-bold">Please fix the following errors:</strong>
|
||||
<ul class="mt-2">
|
||||
@foreach($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
|
||||
@if(auth()->user()->checkResourcePermission($thisResourceType, $thisResourceValue, 'edit'))
|
||||
<x-form :action="route('admin.resource-associations.update', $resourceAssociation)" method="PUT">
|
||||
<div class="mb-6">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="user_id">
|
||||
User <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<select class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline @error('user_id') border-red-500 @enderror"
|
||||
id="user_id"
|
||||
name="user_id"
|
||||
required>
|
||||
<option value="">Select a user</option>
|
||||
@foreach($users as $user)
|
||||
<option value="{{ $user->id }}" {{ old('user_id', $resourceAssociation->user_id) == $user->id ? 'selected' : '' }}>
|
||||
{{ $user->name }} ({{ $user->email }})
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('user_id')
|
||||
<p class="text-red-500 text-xs italic mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="resource_type_id">
|
||||
Resource Type <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<select class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline @error('resource_type_id') border-red-500 @enderror"
|
||||
id="resource_type_id"
|
||||
name="resource_type_id"
|
||||
required>
|
||||
<option value="">Select a resource type</option>
|
||||
@foreach($resourceTypes as $resourceType)
|
||||
<option value="{{ $resourceType->id }}" {{ old('resource_type_id', $resourceAssociation->resource_type_id) == $resourceType->id ? 'selected' : '' }}>
|
||||
{{ $resourceType->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('resource_type_id')
|
||||
<p class="text-red-500 text-xs italic mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="role_id">
|
||||
Role <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<select class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline @error('role_id') border-red-500 @enderror"
|
||||
id="role_id"
|
||||
name="role_id"
|
||||
required
|
||||
disabled>
|
||||
<option value="">Loading roles...</option>
|
||||
</select>
|
||||
@error('role_id')
|
||||
<p class="text-red-500 text-xs italic mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
<p class="text-gray-600 text-xs mt-1">Only roles assigned to the selected user will be shown.</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="resource_id">
|
||||
Resource
|
||||
</label>
|
||||
<select class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline @error('resource_id') border-red-500 @enderror"
|
||||
id="resource_id"
|
||||
name="resource_id"
|
||||
disabled>
|
||||
<option value="">Loading resources...</option>
|
||||
</select>
|
||||
@error('resource_id')
|
||||
<p class="text-red-500 text-xs italic mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
<p class="text-gray-600 text-xs mt-1">Select a resource type first to load available resources.</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="description">
|
||||
Description
|
||||
</label>
|
||||
<textarea
|
||||
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline @error('description') border-red-500 @enderror"
|
||||
id="description"
|
||||
name="description"
|
||||
rows="3"
|
||||
placeholder="Enter a description">{{ old('description', $resourceAssociation->description) }}</textarea>
|
||||
@error('description')
|
||||
<p class="text-red-500 text-xs italic mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
@if(auth()->user()->checkResourcePermission($thisResourceType, $thisResourceValue, 'view'))
|
||||
<a href="{{ route('admin.resource-associations.index') }}"
|
||||
class="text-gray-600 hover:text-gray-800">
|
||||
Cancel
|
||||
</a>
|
||||
@else
|
||||
<div></div>
|
||||
@endif
|
||||
<button type="submit"
|
||||
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">
|
||||
Update Resource Association
|
||||
</button>
|
||||
</div>
|
||||
</x-form>
|
||||
|
||||
@if(auth()->user()->checkResourcePermission($thisResourceType, $thisResourceValue, 'delete'))
|
||||
<div class="mt-6 pt-6 border-t border-gray-200">
|
||||
<div class="flex justify-between items-center">
|
||||
<h2 class="text-lg font-semibold text-red-600">Danger Zone</h2>
|
||||
<x-form
|
||||
:action="route('admin.resource-associations.destroy', $resourceAssociation)"
|
||||
method="DELETE"
|
||||
onsubmit="return confirm('Are you sure you want to delete this resource association? This action cannot be undone.');">
|
||||
<button type="submit"
|
||||
class="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">
|
||||
Delete Resource Association
|
||||
</button>
|
||||
</x-form>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@else
|
||||
<div class="text-red-600 text-center py-4">
|
||||
You do not have permission to edit resource associations.
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const userSelect = document.getElementById('user_id');
|
||||
const roleSelect = document.getElementById('role_id');
|
||||
const resourceTypeSelect = document.getElementById('resource_type_id');
|
||||
const resourceSelect = document.getElementById('resource_id');
|
||||
|
||||
// Function to load roles for selected user
|
||||
const loadRoles = async () => {
|
||||
const userId = userSelect.value;
|
||||
roleSelect.disabled = true;
|
||||
|
||||
if (!userId) {
|
||||
roleSelect.innerHTML = '<option value="">Select a user first</option>';
|
||||
roleSelect.disabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
roleSelect.innerHTML = '<option value="">Loading...</option>';
|
||||
|
||||
const response = await fetch(`/admin/resource-associations/roles?user_id=${userId}`);
|
||||
if (!response.ok) {
|
||||
if (response.status === 403) {
|
||||
roleSelect.innerHTML = '<option value="">Unauthorized to view roles</option>';
|
||||
return;
|
||||
}
|
||||
throw new Error('Failed to fetch roles');
|
||||
}
|
||||
|
||||
const roles = await response.json();
|
||||
|
||||
roleSelect.innerHTML = '<option value="">Select a role</option>';
|
||||
roles.forEach(role => {
|
||||
const option = document.createElement('option');
|
||||
option.value = role.id;
|
||||
option.textContent = role.name;
|
||||
option.title = role.description || '';
|
||||
if (role.id == {{ old('role_id', $resourceAssociation->role_id) }}) {
|
||||
option.selected = true;
|
||||
}
|
||||
roleSelect.appendChild(option);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error loading roles:', error);
|
||||
roleSelect.innerHTML = '<option value="">Error loading roles</option>';
|
||||
} finally {
|
||||
roleSelect.disabled = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Function to load resources for selected resource type
|
||||
const loadResources = async () => {
|
||||
const resourceTypeId = resourceTypeSelect.value;
|
||||
resourceSelect.disabled = true;
|
||||
|
||||
if (!resourceTypeId) {
|
||||
resourceSelect.innerHTML = '<option value="">Select a resource type first</option>';
|
||||
resourceSelect.disabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
resourceSelect.innerHTML = '<option value="">Loading...</option>';
|
||||
|
||||
const response = await fetch(`/admin/resource-associations/resources?resource_type_id=${resourceTypeId}`);
|
||||
if (!response.ok) {
|
||||
if (response.status === 403) {
|
||||
resourceSelect.innerHTML = '<option value="">Unauthorized to view resources</option>';
|
||||
return;
|
||||
}
|
||||
throw new Error('Failed to fetch resources');
|
||||
}
|
||||
|
||||
const resources = await response.json();
|
||||
|
||||
resourceSelect.innerHTML = '<option value="">Select a resource</option>';
|
||||
resources.forEach(resource => {
|
||||
const option = document.createElement('option');
|
||||
option.value = resource.id;
|
||||
option.textContent = resource.value || `Resource ${resource.id}`;
|
||||
if (resource.id == {{ old('resource_id', $resourceAssociation->resource_id ?? 0) }}) {
|
||||
option.selected = true;
|
||||
}
|
||||
resourceSelect.appendChild(option);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error loading resources:', error);
|
||||
resourceSelect.innerHTML = '<option value="">Error loading resources</option>';
|
||||
} finally {
|
||||
resourceSelect.disabled = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Load initial values
|
||||
loadRoles();
|
||||
if (resourceTypeSelect.value) {
|
||||
loadResources();
|
||||
}
|
||||
|
||||
// Add event listeners
|
||||
userSelect.addEventListener('change', loadRoles);
|
||||
resourceTypeSelect.addEventListener('change', loadResources);
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
@endsection
|
||||
@ -0,0 +1,153 @@
|
||||
@extends('admin::layouts.admin')
|
||||
@section('title', 'Resource Associations')
|
||||
@section('content')
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-bold">Resource Associations</h1>
|
||||
<x-auth-check :permission="['type' => $thisResourceType, 'value' => $thisResourceValue, 'action' => 'create']">
|
||||
<a href="{{ route('admin.resource-associations.create') }}"
|
||||
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
|
||||
Create New Resource Association
|
||||
</a>
|
||||
</x-auth-check>
|
||||
</div>
|
||||
|
||||
<x-alert type="success" :message="session('success')" />
|
||||
<x-alert type="error" :message="session('error')" />
|
||||
|
||||
<!-- Search Section -->
|
||||
<div class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
|
||||
<x-form :action="route('admin.resource-associations.index')" method="GET" class="flex gap-4 items-end">
|
||||
<div class="flex-1">
|
||||
<x-input-field
|
||||
name="search"
|
||||
label="Search"
|
||||
:value="request('search')"
|
||||
placeholder="Search by user, resource type, role or description..."
|
||||
/>
|
||||
</div>
|
||||
<div class="w-48">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="sort">
|
||||
Sort By
|
||||
</label>
|
||||
<select id="sort"
|
||||
name="sort"
|
||||
class="shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline">
|
||||
<option value="created_at" {{ request('sort', 'created_at') === 'created_at' ? 'selected' : '' }}>Date Created</option>
|
||||
<option value="updated_at" {{ request('sort') === 'updated_at' ? 'selected' : '' }}>Date Updated</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="w-48">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="direction">
|
||||
Order
|
||||
</label>
|
||||
<select id="direction"
|
||||
name="direction"
|
||||
class="shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline">
|
||||
<option value="desc" {{ request('direction', 'desc') === 'desc' ? 'selected' : '' }}>Newest First</option>
|
||||
<option value="asc" {{ request('direction') === 'asc' ? 'selected' : '' }}>Oldest First</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">
|
||||
Filter
|
||||
</button>
|
||||
@if(request()->hasAny(['search', 'sort', 'direction']))
|
||||
<a href="{{ route('admin.resource-associations.index') }}"
|
||||
class="ml-2 text-gray-600 hover:text-gray-900">
|
||||
Clear
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
</x-form>
|
||||
</div>
|
||||
|
||||
<div class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
|
||||
@if($resourceAssociations->isEmpty())
|
||||
<div class="text-center py-4">
|
||||
<p class="text-gray-500">No resource associations found.</p>
|
||||
</div>
|
||||
@else
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
User
|
||||
</th>
|
||||
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Resource Type
|
||||
</th>
|
||||
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Role
|
||||
</th>
|
||||
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Resource
|
||||
</th>
|
||||
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Description
|
||||
</th>
|
||||
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Created At
|
||||
</th>
|
||||
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
@foreach($resourceAssociations as $resourceAssociation)
|
||||
<tr>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm font-medium text-gray-900">
|
||||
{{ $resourceAssociation->user->name }}
|
||||
</div>
|
||||
<div class="text-sm text-gray-500">
|
||||
{{ $resourceAssociation->user->email }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
{{ $resourceAssociation->resourceType->name }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
{{ $resourceAssociation->role->name }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
{{ $resourceAssociation->resource_value }}
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="text-sm text-gray-900">
|
||||
{{ $resourceAssociation->description ?: 'No description' }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{{ $resourceAssociation->created_at->format('Y-m-d H:i:s') }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
<x-auth-check :permission="['type' => $thisResourceType, 'value' => $thisResourceValue, 'action' => 'edit']">
|
||||
<a href="{{ route('admin.resource-associations.edit', $resourceAssociation) }}"
|
||||
class="text-indigo-600 hover:text-indigo-900 mr-3">Edit</a>
|
||||
</x-auth-check>
|
||||
|
||||
<x-auth-check :permission="['type' => $thisResourceType, 'value' => $thisResourceValue, 'action' => 'delete']">
|
||||
<x-form
|
||||
:action="route('admin.resource-associations.destroy', $resourceAssociation)"
|
||||
method="DELETE"
|
||||
class="inline-block"
|
||||
onsubmit="return confirm('Are you sure you want to delete this resource association?');"
|
||||
>
|
||||
<button type="submit" class="text-red-600 hover:text-red-900">
|
||||
Delete
|
||||
</button>
|
||||
</x-form>
|
||||
</x-auth-check>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="mt-4">
|
||||
{{ $resourceAssociations->links() }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endsection
|
||||
@ -0,0 +1,248 @@
|
||||
@extends('admin::layouts.admin')
|
||||
@section('title', 'Create Resource Type Mapping')
|
||||
|
||||
@section('content')
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-bold">Create Resource Type Mapping</h1>
|
||||
<x-auth-check :permission="['type' => $thisResourceType, 'value' => $thisResourceValue, 'action' => 'view']">
|
||||
<a href="{{ route('admin.resource-type-mappings.index') }}"
|
||||
class="bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded">
|
||||
Back to List
|
||||
</a>
|
||||
</x-auth-check>
|
||||
</div>
|
||||
|
||||
@if($errors->any())
|
||||
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative mb-4" role="alert">
|
||||
<strong class="font-bold">Please fix the following errors:</strong>
|
||||
<ul class="mt-2 list-disc list-inside">
|
||||
@foreach($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
<button type="button" class="absolute top-0 bottom-0 right-0 px-4 py-3" onclick="this.parentElement.style.display='none'">
|
||||
<span class="sr-only">Close</span>
|
||||
<svg class="h-6 w-6 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
|
||||
@if(auth()->user()->checkResourcePermission($thisResourceType, $thisResourceValue, 'create'))
|
||||
<x-form :action="route('admin.resource-type-mappings.store')" method="POST" id="createForm">
|
||||
<!-- Resource Type Selection -->
|
||||
<div class="mb-6">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="resource_type_id">
|
||||
Resource Type <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<select class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline @error('resource_type_id') border-red-500 @enderror"
|
||||
id="resource_type_id"
|
||||
name="resource_type_id"
|
||||
required>
|
||||
<option value="">Select Resource Type</option>
|
||||
@foreach($resourceTypes as $resourceType)
|
||||
<option value="{{ $resourceType->id }}" {{ old('resource_type_id') == $resourceType->id ? 'selected' : '' }}>
|
||||
{{ $resourceType->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('resource_type_id')
|
||||
<p class="text-red-500 text-xs italic mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
<p class="text-gray-600 text-xs mt-1">Select the resource type to map to a database table</p>
|
||||
</div>
|
||||
|
||||
<!-- Schema Selection -->
|
||||
<div class="mb-6">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="table_schema">
|
||||
Database Schema <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<select class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline @error('table_schema') border-red-500 @enderror"
|
||||
id="table_schema"
|
||||
name="table_schema"
|
||||
required>
|
||||
<option value="">Select Schema</option>
|
||||
@foreach($schemas as $schema)
|
||||
<option value="{{ $schema->schema_name }}" {{ old('table_schema') == $schema->schema_name ? 'selected' : '' }}>
|
||||
{{ $schema->schema_name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('table_schema')
|
||||
<p class="text-red-500 text-xs italic mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
<p class="text-gray-600 text-xs mt-1">Select the database schema containing the target table</p>
|
||||
</div>
|
||||
|
||||
<!-- Table Selection -->
|
||||
<div class="mb-6">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="table_name">
|
||||
Table Name <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<select class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline @error('table_name') border-red-500 @enderror"
|
||||
id="table_name"
|
||||
name="table_name"
|
||||
required
|
||||
{{ !old('table_schema') ? 'disabled' : '' }}>
|
||||
<option value="">Select Table</option>
|
||||
</select>
|
||||
@error('table_name')
|
||||
<p class="text-red-500 text-xs italic mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
<p class="text-gray-600 text-xs mt-1">Select the table to map the resource type to</p>
|
||||
</div>
|
||||
|
||||
<!-- Column Selection -->
|
||||
<div class="mb-6">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="resource_value_column">
|
||||
Value Column <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<select class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline @error('resource_value_column') border-red-500 @enderror"
|
||||
id="resource_value_column"
|
||||
name="resource_value_column"
|
||||
required
|
||||
{{ !old('table_name') ? 'disabled' : '' }}>
|
||||
<option value="">Select Column</option>
|
||||
</select>
|
||||
@error('resource_value_column')
|
||||
<p class="text-red-500 text-xs italic mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
<p class="text-gray-600 text-xs mt-1">Select the column that contains the resource identifier</p>
|
||||
</div>
|
||||
|
||||
<!-- Form Actions -->
|
||||
<div class="flex items-center justify-end space-x-4">
|
||||
<x-auth-check :permission="['type' => $thisResourceType, 'value' => $thisResourceValue, 'action' => 'view']">
|
||||
<button type="button"
|
||||
onclick="window.location.href='{{ route('admin.resource-type-mappings.index') }}'"
|
||||
class="bg-gray-300 hover:bg-gray-400 text-gray-800 font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">
|
||||
Cancel
|
||||
</button>
|
||||
</x-auth-check>
|
||||
<button type="submit"
|
||||
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">
|
||||
Create Mapping
|
||||
</button>
|
||||
</div>
|
||||
</x-form>
|
||||
@else
|
||||
<div class="text-red-500 text-center py-4">
|
||||
You do not have permission to create resource type mappings.
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@push('scripts')
|
||||
<script type="text/javascript">
|
||||
function initializeDynamicSelects() {
|
||||
console.log('Initializing dynamic selects');
|
||||
|
||||
const schemaSelect = document.getElementById('table_schema');
|
||||
const tableSelect = document.getElementById('table_name');
|
||||
const columnSelect = document.getElementById('resource_value_column');
|
||||
|
||||
// Helper function to show loading state
|
||||
function setLoading(select, loading) {
|
||||
select.disabled = loading;
|
||||
if (loading) {
|
||||
select.innerHTML = '<option value="">Loading...</option>';
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to handle errors
|
||||
function handleError(message) {
|
||||
console.error('Error:', message);
|
||||
alert(message);
|
||||
}
|
||||
|
||||
// Load tables when schema changes
|
||||
schemaSelect.addEventListener('change', async function() {
|
||||
const schema = this.value;
|
||||
|
||||
// Reset dependent fields
|
||||
tableSelect.disabled = !schema;
|
||||
columnSelect.disabled = true;
|
||||
columnSelect.innerHTML = '<option value="">Select Column</option>';
|
||||
|
||||
if (schema) {
|
||||
try {
|
||||
setLoading(tableSelect, true);
|
||||
|
||||
const response = await fetch('/admin/resource-type-mappings/ajax/tables?schema=' + encodeURIComponent(schema));
|
||||
if (!response.ok) throw new Error(`Failed to fetch tables: ${response.status}`);
|
||||
|
||||
const tables = await response.json();
|
||||
|
||||
tableSelect.innerHTML = '<option value="">Select Table</option>';
|
||||
tables.forEach(table => {
|
||||
const option = new Option(table.table_name, table.table_name);
|
||||
if (table.table_name === '{{ old('table_name') }}') {
|
||||
option.selected = true;
|
||||
}
|
||||
tableSelect.add(option);
|
||||
});
|
||||
|
||||
// If we have an old table value, trigger the columns load
|
||||
if ('{{ old('table_name') }}') {
|
||||
tableSelect.dispatchEvent(new Event('change'));
|
||||
}
|
||||
} catch (error) {
|
||||
handleError('Error loading tables: ' + error.message);
|
||||
} finally {
|
||||
tableSelect.disabled = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Load columns when table changes
|
||||
tableSelect.addEventListener('change', async function() {
|
||||
const schema = schemaSelect.value;
|
||||
const table = this.value;
|
||||
|
||||
columnSelect.disabled = !table;
|
||||
|
||||
if (schema && table) {
|
||||
try {
|
||||
setLoading(columnSelect, true);
|
||||
|
||||
const response = await fetch('/admin/resource-type-mappings/ajax/columns?schema=' +
|
||||
encodeURIComponent(schema) + '&table=' + encodeURIComponent(table));
|
||||
if (!response.ok) throw new Error(`Failed to fetch columns: ${response.status}`);
|
||||
|
||||
const columns = await response.json();
|
||||
|
||||
columnSelect.innerHTML = '<option value="">Select Column</option>';
|
||||
columns.forEach(column => {
|
||||
const option = new Option(column.column_name, column.column_name);
|
||||
if (column.column_name === '{{ old('resource_value_column') }}') {
|
||||
option.selected = true;
|
||||
}
|
||||
columnSelect.add(option);
|
||||
});
|
||||
} catch (error) {
|
||||
handleError('Error loading columns: ' + error.message);
|
||||
} finally {
|
||||
columnSelect.disabled = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Trigger initial load if we have old values
|
||||
if ('{{ old('table_schema') }}') {
|
||||
schemaSelect.dispatchEvent(new Event('change'));
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize when the document is ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initializeDynamicSelects);
|
||||
} else {
|
||||
initializeDynamicSelects();
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
@endsection
|
||||
@ -0,0 +1,280 @@
|
||||
@extends('admin::layouts.admin')
|
||||
@section('title', 'Edit Resource Type Mapping')
|
||||
|
||||
@section('content')
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-bold">Edit Resource Type Mapping</h1>
|
||||
@if(auth()->user()->checkResourcePermission($thisResourceType, $thisResourceValue, 'view'))
|
||||
<a href="{{ route('admin.resource-type-mappings.index') }}"
|
||||
class="bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded">
|
||||
Back to List
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if($errors->any())
|
||||
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative mb-4" role="alert">
|
||||
<strong class="font-bold">Please fix the following errors:</strong>
|
||||
<ul class="mt-2 list-disc list-inside">
|
||||
@foreach($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
<button type="button" class="absolute top-0 bottom-0 right-0 px-4 py-3" onclick="this.parentElement.style.display='none'">
|
||||
<span class="sr-only">Close</span>
|
||||
<svg class="h-6 w-6 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
|
||||
@if(auth()->user()->checkResourcePermission($thisResourceType, $thisResourceValue, 'edit'))
|
||||
<x-form
|
||||
:action="route('admin.resource-type-mappings.update', $resource_type_mapping->resource_type_id)"
|
||||
method="PUT"
|
||||
id="editForm">
|
||||
|
||||
<!-- Resource Type Selection -->
|
||||
<div class="mb-6">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="resource_type_id">
|
||||
Resource Type <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<select class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline @error('resource_type_id') border-red-500 @enderror"
|
||||
id="resource_type_id"
|
||||
name="resource_type_id"
|
||||
required>
|
||||
<option value="">Select Resource Type</option>
|
||||
@foreach($resourceTypes as $resourceType)
|
||||
<option value="{{ $resourceType->id }}"
|
||||
{{ (old('resource_type_id', $resource_type_mapping->resource_type_id) == $resourceType->id) ? 'selected' : '' }}>
|
||||
{{ $resourceType->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('resource_type_id')
|
||||
<p class="text-red-500 text-xs italic mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
<p class="text-gray-600 text-xs mt-1">Select the resource type to map to a database table</p>
|
||||
</div>
|
||||
|
||||
<!-- Schema Selection -->
|
||||
<div class="mb-6">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="table_schema">
|
||||
Database Schema <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<select class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline @error('table_schema') border-red-500 @enderror"
|
||||
id="table_schema"
|
||||
name="table_schema"
|
||||
required>
|
||||
<option value="">Select Schema</option>
|
||||
@foreach($schemas as $schema)
|
||||
<option value="{{ $schema->schema_name }}"
|
||||
{{ old('table_schema', $resource_type_mapping->table_schema) == $schema->schema_name ? 'selected' : '' }}>
|
||||
{{ $schema->schema_name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('table_schema')
|
||||
<p class="text-red-500 text-xs italic mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
<p class="text-gray-600 text-xs mt-1">Select the database schema containing the target table</p>
|
||||
</div>
|
||||
|
||||
<!-- Table Selection -->
|
||||
<div class="mb-6">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="table_name">
|
||||
Table Name <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<select class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline @error('table_name') border-red-500 @enderror"
|
||||
id="table_name"
|
||||
name="table_name"
|
||||
required>
|
||||
<option value="">Select Table</option>
|
||||
@if($resource_type_mapping->table_name)
|
||||
<option value="{{ $resource_type_mapping->table_name }}" selected>
|
||||
{{ $resource_type_mapping->table_name }}
|
||||
</option>
|
||||
@endif
|
||||
</select>
|
||||
@error('table_name')
|
||||
<p class="text-red-500 text-xs italic mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
<p class="text-gray-600 text-xs mt-1">Select the table to map the resource type to</p>
|
||||
</div>
|
||||
|
||||
<!-- Column Selection -->
|
||||
<div class="mb-6">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="resource_value_column">
|
||||
Value Column <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<select class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline @error('resource_value_column') border-red-500 @enderror"
|
||||
id="resource_value_column"
|
||||
name="resource_value_column"
|
||||
required>
|
||||
<option value="">Select Column</option>
|
||||
@if($resource_type_mapping->resource_value_column)
|
||||
<option value="{{ $resource_type_mapping->resource_value_column }}" selected>
|
||||
{{ $resource_type_mapping->resource_value_column }}
|
||||
</option>
|
||||
@endif
|
||||
</select>
|
||||
@error('resource_value_column')
|
||||
<p class="text-red-500 text-xs italic mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
<p class="text-gray-600 text-xs mt-1">Select the column that contains the resource identifier</p>
|
||||
</div>
|
||||
|
||||
<!-- Form Actions -->
|
||||
<div class="flex items-center justify-end space-x-4">
|
||||
@if(auth()->user()->checkResourcePermission($thisResourceType, $thisResourceValue, 'view'))
|
||||
<button type="button"
|
||||
onclick="window.location.href='{{ route('admin.resource-type-mappings.index') }}'"
|
||||
class="bg-gray-300 hover:bg-gray-400 text-gray-800 font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">
|
||||
Cancel
|
||||
</button>
|
||||
@endif
|
||||
<button type="submit"
|
||||
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">
|
||||
Update Mapping
|
||||
</button>
|
||||
</div>
|
||||
</x-form>
|
||||
|
||||
<!-- Delete Section -->
|
||||
@if(auth()->user()->checkResourcePermission($thisResourceType, $thisResourceValue, 'delete'))
|
||||
<div class="mt-8 pt-6 border-t border-gray-200">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-lg font-semibold text-red-600">Danger Zone</h2>
|
||||
<x-form
|
||||
:action="route('admin.resource-type-mappings.destroy', $resource_type_mapping->resource_type_id)"
|
||||
method="DELETE"
|
||||
onsubmit="return confirm('Are you sure you want to delete this mapping? This action cannot be undone.');">
|
||||
<button type="submit"
|
||||
class="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">
|
||||
Delete Mapping
|
||||
</button>
|
||||
</x-form>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@else
|
||||
<div class="text-red-500 text-center py-4">
|
||||
You do not have permission to edit resource type mappings.
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@push('scripts')
|
||||
<script type="text/javascript">
|
||||
function initializeDynamicSelects() {
|
||||
console.log('Initializing dynamic selects');
|
||||
|
||||
const schemaSelect = document.getElementById('table_schema');
|
||||
const tableSelect = document.getElementById('table_name');
|
||||
const columnSelect = document.getElementById('resource_value_column');
|
||||
|
||||
// Helper function to show loading state
|
||||
function setLoading(select, loading) {
|
||||
select.disabled = loading;
|
||||
if (loading) {
|
||||
select.innerHTML = '<option value="">Loading...</option>';
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to handle errors
|
||||
function handleError(message) {
|
||||
console.error('Error:', message);
|
||||
alert(message);
|
||||
}
|
||||
|
||||
// Load tables when schema changes
|
||||
schemaSelect.addEventListener('change', async function() {
|
||||
const schema = this.value;
|
||||
|
||||
// Reset and disable dependent fields
|
||||
tableSelect.disabled = !schema;
|
||||
columnSelect.disabled = true;
|
||||
columnSelect.innerHTML = '<option value="">Select Column</option>';
|
||||
|
||||
if (schema) {
|
||||
try {
|
||||
setLoading(tableSelect, true);
|
||||
|
||||
const response = await fetch('/admin/resource-type-mappings/ajax/tables?schema=' + encodeURIComponent(schema));
|
||||
if (!response.ok) throw new Error(`Failed to fetch tables: ${response.status}`);
|
||||
|
||||
const tables = await response.json();
|
||||
|
||||
tableSelect.innerHTML = '<option value="">Select Table</option>';
|
||||
tables.forEach(table => {
|
||||
const option = new Option(table.table_name, table.table_name);
|
||||
if (table.table_name === '{{ $resource_type_mapping->table_name }}') {
|
||||
option.selected = true;
|
||||
}
|
||||
tableSelect.add(option);
|
||||
});
|
||||
|
||||
tableSelect.disabled = false;
|
||||
|
||||
// If we have a selected table, trigger the columns load
|
||||
if (tableSelect.value) {
|
||||
tableSelect.dispatchEvent(new Event('change'));
|
||||
}
|
||||
} catch (error) {
|
||||
handleError('Error loading tables: ' + error.message);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Load columns when table changes
|
||||
tableSelect.addEventListener('change', async function() {
|
||||
const schema = schemaSelect.value;
|
||||
const table = this.value;
|
||||
|
||||
columnSelect.disabled = !table;
|
||||
|
||||
if (schema && table) {
|
||||
try {
|
||||
setLoading(columnSelect, true);
|
||||
|
||||
const response = await fetch('/admin/resource-type-mappings/ajax/columns?schema=' +
|
||||
encodeURIComponent(schema) + '&table=' + encodeURIComponent(table));
|
||||
if (!response.ok) throw new Error(`Failed to fetch columns: ${response.status}`);
|
||||
|
||||
const columns = await response.json();
|
||||
|
||||
columnSelect.innerHTML = '<option value="">Select Column</option>';
|
||||
columns.forEach(column => {
|
||||
const option = new Option(column.column_name, column.column_name);
|
||||
if (column.column_name === '{{ $resource_type_mapping->resource_value_column }}') {
|
||||
option.selected = true;
|
||||
}
|
||||
columnSelect.add(option);
|
||||
});
|
||||
|
||||
columnSelect.disabled = false;
|
||||
} catch (error) {
|
||||
handleError('Error loading columns: ' + error.message);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Trigger initial loads if we have existing values
|
||||
if (schemaSelect.value) {
|
||||
schemaSelect.dispatchEvent(new Event('change'));
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize when the document is ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initializeDynamicSelects);
|
||||
} else {
|
||||
initializeDynamicSelects();
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
@endsection
|
||||
@ -0,0 +1,189 @@
|
||||
@extends('admin::layouts.admin')
|
||||
@section('title', 'Resource Type Mappings')
|
||||
|
||||
@section('content')
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-bold">Resource Type Mappings</h1>
|
||||
<x-auth-check :permission="['type' => $thisResourceType, 'value' => $thisResourceValue, 'action' => 'create']">
|
||||
<a href="{{ route('admin.resource-type-mappings.create') }}"
|
||||
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
|
||||
Create New Mapping
|
||||
</a>
|
||||
</x-auth-check>
|
||||
</div>
|
||||
|
||||
<x-alert type="success" :message="session('success')" />
|
||||
<x-alert type="error" :message="session('error')" />
|
||||
|
||||
<!-- Search and Filter Section -->
|
||||
<div class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
|
||||
<x-form
|
||||
:action="route('admin.resource-type-mappings.index')"
|
||||
method="GET"
|
||||
class="flex flex-wrap gap-4 items-end"
|
||||
>
|
||||
<!-- Search Field -->
|
||||
<div class="flex-1 min-w-[200px]">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="search">
|
||||
Search
|
||||
</label>
|
||||
<input type="text"
|
||||
id="search"
|
||||
name="search"
|
||||
value="{{ request('search') }}"
|
||||
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
||||
placeholder="Search by schema, table, column or resource type...">
|
||||
</div>
|
||||
|
||||
<!-- Schema Filter -->
|
||||
<div class="w-48">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="schema">
|
||||
Schema
|
||||
</label>
|
||||
<select id="schema"
|
||||
name="schema"
|
||||
class="shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline">
|
||||
<option value="">All Schemas</option>
|
||||
@foreach($resource_type_mappings->pluck('table_schema')->unique() as $schema)
|
||||
<option value="{{ $schema }}" {{ request('schema') == $schema ? 'selected' : '' }}>
|
||||
{{ $schema }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Sort Field -->
|
||||
<div class="w-48">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="sort">
|
||||
Sort By
|
||||
</label>
|
||||
<select id="sort"
|
||||
name="sort"
|
||||
class="shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline">
|
||||
<option value="table_schema" {{ request('sort', 'table_schema') === 'table_schema' ? 'selected' : '' }}>Schema</option>
|
||||
<option value="table_name" {{ request('sort') === 'table_name' ? 'selected' : '' }}>Table</option>
|
||||
<option value="created_at" {{ request('sort') === 'created_at' ? 'selected' : '' }}>Date Created</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Sort Direction -->
|
||||
<div class="w-48">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="direction">
|
||||
Order
|
||||
</label>
|
||||
<select id="direction"
|
||||
name="direction"
|
||||
class="shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline">
|
||||
<option value="asc" {{ request('direction', 'asc') === 'asc' ? 'selected' : '' }}>Ascending</option>
|
||||
<option value="desc" {{ request('direction') === 'desc' ? 'selected' : '' }}>Descending</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Filter Buttons -->
|
||||
<div class="flex items-center space-x-2">
|
||||
<button type="submit"
|
||||
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">
|
||||
Apply Filters
|
||||
</button>
|
||||
@if(request('search') || request('schema') || request('sort') || request('direction'))
|
||||
<a href="{{ route('admin.resource-type-mappings.index') }}"
|
||||
class="text-gray-600 hover:text-gray-900">
|
||||
Clear Filters
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
</x-form>
|
||||
</div>
|
||||
|
||||
<!-- Results Table -->
|
||||
<div class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
|
||||
@if($resource_type_mappings->isEmpty())
|
||||
<div class="text-center py-8">
|
||||
<p class="text-gray-500 text-lg">No resource type mappings found.</p>
|
||||
@if(request('search') || request('schema') || request('sort') || request('direction'))
|
||||
<p class="text-gray-400 mt-2">Try adjusting your search filters</p>
|
||||
@endif
|
||||
</div>
|
||||
@else
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Resource Type
|
||||
</th>
|
||||
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Schema
|
||||
</th>
|
||||
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Table
|
||||
</th>
|
||||
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Value Column
|
||||
</th>
|
||||
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Created At
|
||||
</th>
|
||||
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
@foreach($resource_type_mappings as $mapping)
|
||||
<tr>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm font-medium text-gray-900">
|
||||
{{ $mapping->resourceType->name }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm text-gray-900">{{ $mapping->table_schema }}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm text-gray-900">{{ $mapping->table_name }}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm text-gray-900">{{ $mapping->resource_value_column }}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm text-gray-500">
|
||||
{{ $mapping->created_at->format('Y-m-d H:i:s') }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<div class="flex items-center space-x-3">
|
||||
<x-auth-check :permission="['type' => $thisResourceType, 'value' => $thisResourceValue, 'action' => 'edit']">
|
||||
<a href="{{ route('admin.resource-type-mappings.edit', $mapping->resource_type_id) }}"
|
||||
class="text-indigo-600 hover:text-indigo-900">
|
||||
Edit
|
||||
</a>
|
||||
</x-auth-check>
|
||||
|
||||
<x-auth-check :permission="['type' => $thisResourceType, 'value' => $thisResourceValue, 'action' => 'delete']">
|
||||
<x-form
|
||||
:action="route('admin.resource-type-mappings.destroy', $mapping->resource_type_id)"
|
||||
method="DELETE"
|
||||
class="inline-block"
|
||||
onsubmit="return confirm('Are you sure you want to delete this mapping? This action cannot be undone.');"
|
||||
>
|
||||
<button type="submit"
|
||||
class="text-red-600 hover:text-red-900">
|
||||
Delete
|
||||
</button>
|
||||
</x-form>
|
||||
</x-auth-check>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
{{ $resource_type_mappings->links() }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endsection
|
||||
@ -0,0 +1,65 @@
|
||||
@extends('admin::layouts.admin')
|
||||
@section('title', 'Create Resource Type')
|
||||
@section('content')
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-bold">Create Resource Type</h1>
|
||||
<a href="{{ route('admin.resource-types.index') }}"
|
||||
class="bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded">
|
||||
Back to List
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@if($errors->any())
|
||||
<x-alert type="error" message="Please fix the following errors:">
|
||||
<ul>
|
||||
@foreach($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</x-alert>
|
||||
@endif
|
||||
|
||||
<div class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
|
||||
<x-form :action="route('admin.resource-types.store')" method="POST">
|
||||
<x-input-field
|
||||
name="name"
|
||||
label="Name"
|
||||
:value="old('name')"
|
||||
placeholder="Enter resource type name"
|
||||
required
|
||||
autofocus
|
||||
>
|
||||
<p class="text-gray-600 text-xs mt-1">The name must be unique and cannot exceed 255 characters.</p>
|
||||
</x-input-field>
|
||||
|
||||
<div class="mb-6">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="description">
|
||||
Description
|
||||
</label>
|
||||
<textarea
|
||||
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline @error('description') border-red-500 @enderror"
|
||||
id="description"
|
||||
name="description"
|
||||
rows="4"
|
||||
placeholder="Enter resource type description">{{ old('description') }}</textarea>
|
||||
@error('description')
|
||||
<p class="text-red-500 text-xs italic mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
<p class="text-gray-600 text-xs mt-1">Provide a clear description of what this resource type represents.</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<a href="{{ route('admin.resource-types.index') }}"
|
||||
class="text-gray-600 hover:text-gray-800">
|
||||
Cancel
|
||||
</a>
|
||||
<button type="submit"
|
||||
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">
|
||||
Create Resource Type
|
||||
</button>
|
||||
</div>
|
||||
</x-form>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@ -0,0 +1,76 @@
|
||||
@extends('admin::layouts.admin')
|
||||
@section('title', 'Edit Resource Type')
|
||||
@section('content')
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-bold">Edit Resource Type</h1>
|
||||
<a href="{{ route('admin.resource-types.index') }}"
|
||||
class="bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded">
|
||||
Back to List
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@if($errors->any())
|
||||
<x-alert type="error" message="Please fix the following errors:">
|
||||
<ul class="mt-2">
|
||||
@foreach($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</x-alert>
|
||||
@endif
|
||||
|
||||
<div class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
|
||||
<x-form :action="route('admin.resource-types.update', $resourceType)" method="PUT">
|
||||
<x-input-field
|
||||
name="name"
|
||||
label="Name"
|
||||
:value="old('name', $resourceType->name)"
|
||||
placeholder="Enter resource type name"
|
||||
required
|
||||
>
|
||||
<p class="text-gray-600 text-xs mt-1">The name must be unique and cannot exceed 255 characters.</p>
|
||||
</x-input-field>
|
||||
|
||||
<div class="mb-6">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="description">
|
||||
Description
|
||||
</label>
|
||||
<textarea
|
||||
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline @error('description') border-red-500 @enderror"
|
||||
id="description"
|
||||
name="description"
|
||||
rows="4"
|
||||
placeholder="Enter resource type description">{{ old('description', $resourceType->description) }}</textarea>
|
||||
@error('description')
|
||||
<p class="text-red-500 text-xs italic mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
<p class="text-gray-600 text-xs mt-1">Provide a clear description of what this resource type represents.</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end">
|
||||
<button type="submit"
|
||||
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">
|
||||
Update Resource Type
|
||||
</button>
|
||||
</div>
|
||||
</x-form>
|
||||
|
||||
<div class="mt-6 pt-6 border-t border-gray-200">
|
||||
<div class="flex justify-between items-center">
|
||||
<h2 class="text-lg font-semibold text-red-600">Danger Zone</h2>
|
||||
<x-form
|
||||
:action="route('admin.resource-types.destroy', $resourceType)"
|
||||
method="DELETE"
|
||||
onsubmit="return confirm('Are you sure you want to delete this resource type? This action cannot be undone.');"
|
||||
>
|
||||
<button type="submit"
|
||||
class="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">
|
||||
Delete Resource Type
|
||||
</button>
|
||||
</x-form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@ -0,0 +1,127 @@
|
||||
@extends('admin::layouts.admin')
|
||||
@section('title', 'Resource Types')
|
||||
@section('content')
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-bold">Resource Types</h1>
|
||||
<x-auth-check :permission="['type' => $thisResourceType, 'value' => $thisResourceValue, 'action' => 'create']">
|
||||
<a href="{{ route('admin.resource-types.create') }}"
|
||||
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
|
||||
Create New Resource Type
|
||||
</a>
|
||||
</x-auth-check>
|
||||
</div>
|
||||
|
||||
<x-alert type="success" :message="session('success')" />
|
||||
<x-alert type="error" :message="session('error')" />
|
||||
|
||||
<!-- Search Section -->
|
||||
<div class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
|
||||
<x-form
|
||||
:action="route('admin.resource-types.index')"
|
||||
method="GET"
|
||||
class="flex gap-4 items-end"
|
||||
>
|
||||
<div class="flex-1">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="search">
|
||||
Search
|
||||
</label>
|
||||
<input type="text"
|
||||
id="search"
|
||||
name="search"
|
||||
value="{{ request('search') }}"
|
||||
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
||||
placeholder="Search resource types...">
|
||||
</div>
|
||||
<div class="w-48">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="sort">
|
||||
Sort By
|
||||
</label>
|
||||
<select id="sort"
|
||||
name="sort"
|
||||
class="shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline">
|
||||
<option value="name" {{ request('sort', 'name') === 'name' ? 'selected' : '' }}>Name</option>
|
||||
<option value="created_at" {{ request('sort') === 'created_at' ? 'selected' : '' }}>Date Created</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit"
|
||||
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">
|
||||
Filter
|
||||
</button>
|
||||
@if(request('search') || request('sort'))
|
||||
<a href="{{ route('admin.resource-types.index') }}"
|
||||
class="ml-2 text-gray-600 hover:text-gray-900">
|
||||
Clear
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
</x-form>
|
||||
</div>
|
||||
|
||||
<div class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
|
||||
@if($resourceTypes->isEmpty())
|
||||
<div class="text-center py-4">
|
||||
<p class="text-gray-500">No resource types found.</p>
|
||||
</div>
|
||||
@else
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Name
|
||||
</th>
|
||||
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Description
|
||||
</th>
|
||||
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Created At
|
||||
</th>
|
||||
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
@foreach($resourceTypes as $resourceType)
|
||||
<tr>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
{{ $resourceType->name }}
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="text-sm text-gray-900">
|
||||
{{ $resourceType->description ?: 'No description' }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
{{ $resourceType->created_at->format('Y-m-d H:i:s') }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
<x-auth-check :permission="['type' => $thisResourceType, 'value' => $thisResourceValue, 'action' => 'edit']">
|
||||
<a href="{{ route('admin.resource-types.edit', $resourceType) }}"
|
||||
class="text-indigo-600 hover:text-indigo-900 mr-3">Edit</a>
|
||||
</x-auth-check>
|
||||
|
||||
<x-auth-check :permission="['type' => $thisResourceType, 'value' => $thisResourceValue, 'action' => 'delete']">
|
||||
<x-form
|
||||
:action="route('admin.resource-types.destroy', $resourceType)"
|
||||
method="DELETE"
|
||||
class="inline-block"
|
||||
onsubmit="return confirm('Are you sure you want to delete this resource type?');"
|
||||
>
|
||||
<button type="submit"
|
||||
class="text-red-600 hover:text-red-900">
|
||||
Delete
|
||||
</button>
|
||||
</x-form>
|
||||
</x-auth-check>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="mt-4">
|
||||
{{ $resourceTypes->links() }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endsection
|
||||
55
app/Components/Admin/resources/views/roles/create.blade.php
Normal file
55
app/Components/Admin/resources/views/roles/create.blade.php
Normal file
@ -0,0 +1,55 @@
|
||||
@extends('admin::layouts.admin')
|
||||
@section('title', 'Create Role')
|
||||
@section('content')
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<x-auth-check :permission="['type' => $thisResourceType, 'value' => $thisResourceValue, 'action' => 'create']">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-bold">Create Role</h1>
|
||||
<a href="{{ route('admin.roles.index') }}"
|
||||
class="bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded">
|
||||
Back to List
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
|
||||
<x-form :action="route('admin.roles.store')" method="POST" class="space-y-6">
|
||||
<x-input-field
|
||||
name="name"
|
||||
label="Name"
|
||||
:value="old('name')"
|
||||
required
|
||||
autofocus
|
||||
placeholder="Enter role name"
|
||||
/>
|
||||
<p class="text-gray-600 text-xs mt-1">Choose a descriptive name for this role (e.g., "Content Editor", "Store Manager")</p>
|
||||
|
||||
<div>
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="description">
|
||||
Description
|
||||
</label>
|
||||
<textarea class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline @error('description') border-red-500 @enderror"
|
||||
id="description"
|
||||
name="description"
|
||||
rows="4"
|
||||
placeholder="Enter role description">{{ old('description') }}</textarea>
|
||||
@error('description')
|
||||
<p class="text-red-500 text-xs italic mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
<p class="text-gray-600 text-xs mt-1">Provide a clear description of what this role represents and its responsibilities</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between pt-4">
|
||||
<a href="{{ route('admin.roles.index') }}"
|
||||
class="text-gray-600 hover:text-gray-800">
|
||||
Cancel
|
||||
</a>
|
||||
<button type="submit"
|
||||
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">
|
||||
Create Role
|
||||
</button>
|
||||
</div>
|
||||
</x-form>
|
||||
</div>
|
||||
</x-auth-check>
|
||||
</div>
|
||||
@endsection
|
||||
70
app/Components/Admin/resources/views/roles/edit.blade.php
Normal file
70
app/Components/Admin/resources/views/roles/edit.blade.php
Normal file
@ -0,0 +1,70 @@
|
||||
@extends('admin::layouts.admin')
|
||||
@section('title', 'Edit Role')
|
||||
@section('content')
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<x-auth-check :permission="['type' => $thisResourceType, 'value' => $thisResourceValue, 'action' => 'edit']">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-bold">Edit Role: {{ $role->name }}</h1>
|
||||
<a href="{{ route('admin.roles.index') }}"
|
||||
class="bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded">
|
||||
Back to List
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
|
||||
<x-form :action="route('admin.roles.update', $role)" method="PUT" class="space-y-6">
|
||||
<x-input-field
|
||||
name="name"
|
||||
label="Name"
|
||||
:value="old('name', $role->name)"
|
||||
required
|
||||
placeholder="Enter role name"
|
||||
/>
|
||||
<p class="text-gray-600 text-xs mt-1">Choose a descriptive name for this role (e.g., "Content Editor", "Store Manager")</p>
|
||||
|
||||
<div>
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="description">
|
||||
Description
|
||||
</label>
|
||||
<textarea class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline @error('description') border-red-500 @enderror"
|
||||
id="description"
|
||||
name="description"
|
||||
rows="4"
|
||||
placeholder="Enter role description">{{ old('description', $role->description) }}</textarea>
|
||||
@error('description')
|
||||
<p class="text-red-500 text-xs italic mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
<p class="text-gray-600 text-xs mt-1">Provide a clear description of what this role represents and its responsibilities</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between pt-4">
|
||||
<a href="{{ route('admin.roles.index') }}"
|
||||
class="text-gray-600 hover:text-gray-800">
|
||||
Cancel
|
||||
</a>
|
||||
<button type="submit"
|
||||
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">
|
||||
Update Role
|
||||
</button>
|
||||
</div>
|
||||
</x-form>
|
||||
</div>
|
||||
|
||||
<x-auth-check :permission="['type' => $thisResourceType, 'value' => $thisResourceValue, 'action' => 'delete']">
|
||||
<div class="bg-white shadow-md rounded px-8 py-6 mt-4">
|
||||
<h2 class="text-xl font-bold text-red-600 mb-4">Danger Zone</h2>
|
||||
<x-form
|
||||
:action="route('admin.roles.destroy', $role)"
|
||||
method="DELETE"
|
||||
onsubmit="return confirm('Are you sure you want to delete this role? This action cannot be undone.');"
|
||||
>
|
||||
<button type="submit"
|
||||
class="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">
|
||||
Delete Role
|
||||
</button>
|
||||
</x-form>
|
||||
</div>
|
||||
</x-auth-check>
|
||||
</x-auth-check>
|
||||
</div>
|
||||
@endsection
|
||||
129
app/Components/Admin/resources/views/roles/index.blade.php
Normal file
129
app/Components/Admin/resources/views/roles/index.blade.php
Normal file
@ -0,0 +1,129 @@
|
||||
@extends('admin::layouts.admin')
|
||||
@section('title', 'Roles')
|
||||
@section('content')
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-bold">Roles</h1>
|
||||
<x-auth-check :permission="['type' => $thisResourceType, 'value' => $thisResourceValue, 'action' => 'create']">
|
||||
<a href="{{ route('admin.roles.create') }}"
|
||||
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
|
||||
Create New Role
|
||||
</a>
|
||||
</x-auth-check>
|
||||
</div>
|
||||
|
||||
<x-alert type="success" :message="session('success')" />
|
||||
<x-alert type="error" :message="session('error')" />
|
||||
|
||||
<!-- Search Section -->
|
||||
<div class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
|
||||
<x-form :action="route('admin.roles.index')" method="GET" class="flex gap-4 items-end">
|
||||
<div class="flex-1">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="search">
|
||||
Search
|
||||
</label>
|
||||
<input type="text"
|
||||
id="search"
|
||||
name="search"
|
||||
value="{{ request('search') }}"
|
||||
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
||||
placeholder="Search roles...">
|
||||
</div>
|
||||
<div class="w-48">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="sort">
|
||||
Sort By
|
||||
</label>
|
||||
<select id="sort"
|
||||
name="sort"
|
||||
class="shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline">
|
||||
<option value="name" {{ request('sort', 'name') === 'name' ? 'selected' : '' }}>Name</option>
|
||||
<option value="created_at" {{ request('sort') === 'created_at' ? 'selected' : '' }}>Date Created</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit"
|
||||
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">
|
||||
Filter
|
||||
</button>
|
||||
@if(request()->hasAny(['search', 'sort']))
|
||||
<a href="{{ route('admin.roles.index') }}"
|
||||
class="ml-2 text-gray-600 hover:text-gray-900">
|
||||
Clear
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
</x-form>
|
||||
</div>
|
||||
|
||||
<div class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
|
||||
@if($roles->isEmpty())
|
||||
<div class="text-center py-4">
|
||||
<p class="text-gray-500">No roles found.</p>
|
||||
</div>
|
||||
@else
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Name
|
||||
</th>
|
||||
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Description
|
||||
</th>
|
||||
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Created At
|
||||
</th>
|
||||
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
@foreach($roles as $role)
|
||||
<tr>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
{{ $role->name }}
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="text-sm text-gray-900">
|
||||
{{ $role->description ?: 'No description' }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
{{ $role->created_at->format('Y-m-d H:i:s') }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
<x-auth-check :permission="['type' => $thisResourceType, 'value' => $thisResourceValue, 'action' => 'edit']">
|
||||
<a href="{{ route('admin.roles.edit', $role) }}"
|
||||
class="text-indigo-600 hover:text-indigo-900 mr-3">Edit</a>
|
||||
|
||||
<a href="{{ route('admin.roles.permissions', $role) }}"
|
||||
class="text-blue-600 hover:text-blue-900 mr-3">
|
||||
Manage Permissions
|
||||
</a>
|
||||
</x-auth-check>
|
||||
|
||||
<x-auth-check :permission="['type' => $thisResourceType, 'value' => $thisResourceValue, 'action' => 'delete']">
|
||||
<x-form
|
||||
:action="route('admin.roles.destroy', $role)"
|
||||
method="DELETE"
|
||||
class="inline-block"
|
||||
onsubmit="return confirm('Are you sure you want to delete this role? This action cannot be undone.');"
|
||||
>
|
||||
<button type="submit"
|
||||
class="text-red-600 hover:text-red-900">
|
||||
Delete
|
||||
</button>
|
||||
</x-form>
|
||||
</x-auth-check>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="mt-4">
|
||||
{{ $roles->links() }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endsection
|
||||
@ -0,0 +1,131 @@
|
||||
@extends('admin::layouts.admin')
|
||||
@section('title', 'Manage Role Permissions')
|
||||
@section('content')
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-bold">
|
||||
Manage Permissions: {{ $role->name }}
|
||||
</h1>
|
||||
<div class="space-x-2">
|
||||
<a href="{{ route('admin.roles.edit', $role) }}"
|
||||
class="bg-indigo-500 hover:bg-indigo-700 text-white font-bold py-2 px-4 rounded">
|
||||
Edit Role
|
||||
</a>
|
||||
<a href="{{ route('admin.roles.index') }}"
|
||||
class="bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded">
|
||||
Back to Roles
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<x-alert type="success" :message="session('success')" />
|
||||
<x-alert type="error" :message="session('error')" />
|
||||
|
||||
<div class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
|
||||
<x-form :action="route('admin.roles.permissions.update', $role)" method="POST">
|
||||
<div class="mb-6">
|
||||
<h2 class="text-lg font-semibold text-gray-700">Role Details</h2>
|
||||
<p class="text-gray-600 mt-1">{{ $role->description ?: 'No description provided' }}</p>
|
||||
|
||||
@if($isSystemRole)
|
||||
<div class="mt-2 p-2 bg-yellow-50 border border-yellow-200 rounded">
|
||||
<p class="text-yellow-700 text-sm">
|
||||
<strong>Warning:</strong> Modifying permissions for this role may affect system-wide access controls.
|
||||
Please proceed with caution.
|
||||
</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="text-lg font-semibold text-gray-700">Permissions</h2>
|
||||
|
||||
<div class="space-x-2">
|
||||
<button type="button"
|
||||
onclick="selectAll()"
|
||||
class="text-sm text-blue-600 hover:text-blue-800">
|
||||
Select All
|
||||
</button>
|
||||
<button type="button"
|
||||
onclick="deselectAll()"
|
||||
class="text-sm text-blue-600 hover:text-blue-800">
|
||||
Deselect All
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($permissions->isEmpty())
|
||||
<p class="text-gray-500 text-center py-4">No permissions available.</p>
|
||||
@else
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
@foreach($permissions as $permission)
|
||||
<div class="relative flex items-start p-2 hover:bg-gray-50 rounded">
|
||||
<div class="flex items-center h-5">
|
||||
<input type="checkbox"
|
||||
id="permission_{{ $permission->id }}"
|
||||
name="permissions[]"
|
||||
value="{{ $permission->id }}"
|
||||
class="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded"
|
||||
{{ in_array($permission->id, $rolePermissionIds) ? 'checked' : '' }}
|
||||
{{ ($criticalPermissions[$permission->id] && !$isSuperAdmin) ? 'disabled' : '' }}>
|
||||
</div>
|
||||
<div class="ml-3 text-sm">
|
||||
<label for="permission_{{ $permission->id }}"
|
||||
class="font-medium text-gray-700">
|
||||
{{ $permission->name }}
|
||||
</label>
|
||||
@if($permission->description)
|
||||
<p class="text-gray-500">{{ $permission->description }}</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<div class="mt-6 p-4 bg-gray-50 rounded">
|
||||
<h3 class="text-sm font-semibold text-gray-700 mb-2">Permission Categories:</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-2 text-sm">
|
||||
<div>
|
||||
<span class="font-medium">View:</span> Read-only access
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-medium">Create:</span> Ability to add new items
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-medium">Edit:</span> Modify existing items
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-medium">Delete:</span> Remove items
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end space-x-4 pt-4 border-t border-gray-200">
|
||||
<button type="reset"
|
||||
class="bg-gray-300 hover:bg-gray-400 text-gray-800 font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">
|
||||
Reset Changes
|
||||
</button>
|
||||
<button type="submit"
|
||||
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">
|
||||
Update Permissions
|
||||
</button>
|
||||
</div>
|
||||
</x-form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function selectAll() {
|
||||
document.querySelectorAll('input[name="permissions[]"]:not([disabled])')
|
||||
.forEach(checkbox => checkbox.checked = true);
|
||||
}
|
||||
|
||||
function deselectAll() {
|
||||
document.querySelectorAll('input[name="permissions[]"]:not([disabled])')
|
||||
.forEach(checkbox => checkbox.checked = false);
|
||||
}
|
||||
</script>
|
||||
@endsection
|
||||
102
app/Components/Admin/resources/views/user-roles/create.blade.php
Normal file
102
app/Components/Admin/resources/views/user-roles/create.blade.php
Normal file
@ -0,0 +1,102 @@
|
||||
@extends('admin::layouts.admin')
|
||||
@section('title', 'Assign User Role')
|
||||
@section('content')
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-bold">Assign User Role</h1>
|
||||
<x-auth-check :permission="['type' => $thisResourceType, 'value' => $thisResourceValue, 'action' => 'view']">
|
||||
<a href="{{ route('admin.user-roles.index') }}"
|
||||
class="bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded">
|
||||
Back to List
|
||||
</a>
|
||||
</x-auth-check>
|
||||
</div>
|
||||
|
||||
@if($errors->any())
|
||||
<x-alert type="error" message="Please fix the following errors:">
|
||||
<ul class="mt-2">
|
||||
@foreach($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</x-alert>
|
||||
@endif
|
||||
|
||||
<div class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
|
||||
<x-auth-check :permission="['type' => $thisResourceType, 'value' => $thisResourceValue, 'action' => 'create']">
|
||||
<x-form :action="route('admin.user-roles.store')" method="POST">
|
||||
<div class="mb-6">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="user_id">
|
||||
User <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<select name="user_id"
|
||||
id="user_id"
|
||||
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline @error('user_id') border-red-500 @enderror"
|
||||
required>
|
||||
<option value="">Select a user</option>
|
||||
@foreach($users as $user)
|
||||
<option value="{{ $user->id }}" {{ old('user_id') == $user->id ? 'selected' : '' }}>
|
||||
{{ $user->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('user_id')
|
||||
<p class="text-red-500 text-xs italic mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
<p class="text-gray-600 text-xs mt-1">Select the user to assign the role to.</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="role_id">
|
||||
Role <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<select name="role_id"
|
||||
id="role_id"
|
||||
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline @error('role_id') border-red-500 @enderror"
|
||||
required>
|
||||
<option value="">Select a role</option>
|
||||
@foreach($roles as $role)
|
||||
<option value="{{ $role->id }}" {{ old('role_id') == $role->id ? 'selected' : '' }}>
|
||||
{{ $role->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('role_id')
|
||||
<p class="text-red-500 text-xs italic mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
<p class="text-gray-600 text-xs mt-1">Select the role to assign to the user.</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="description">
|
||||
Description
|
||||
</label>
|
||||
<textarea
|
||||
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline @error('description') border-red-500 @enderror"
|
||||
id="description"
|
||||
name="description"
|
||||
rows="4"
|
||||
placeholder="Enter description for this role assignment">{{ old('description') }}</textarea>
|
||||
@error('description')
|
||||
<p class="text-red-500 text-xs italic mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
<p class="text-gray-600 text-xs mt-1">Optional: Provide a reason or note for this role assignment.</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<x-auth-check :permission="['type' => $thisResourceType, 'value' => $thisResourceValue, 'action' => 'view']">
|
||||
<a href="{{ route('admin.user-roles.index') }}"
|
||||
class="text-gray-600 hover:text-gray-800">
|
||||
Cancel
|
||||
</a>
|
||||
</x-auth-check>
|
||||
<button type="submit"
|
||||
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">
|
||||
Assign Role
|
||||
</button>
|
||||
</div>
|
||||
</x-form>
|
||||
</x-auth-check>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
117
app/Components/Admin/resources/views/user-roles/edit.blade.php
Normal file
117
app/Components/Admin/resources/views/user-roles/edit.blade.php
Normal file
@ -0,0 +1,117 @@
|
||||
@extends('admin::layouts.admin')
|
||||
@section('title', 'Edit User Role Assignment')
|
||||
@section('content')
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-bold">Edit User Role Assignment</h1>
|
||||
<x-auth-check :permission="['type' => $thisResourceType, 'value' => $thisResourceValue, 'action' => 'view']">
|
||||
<a href="{{ route('admin.user-roles.index') }}"
|
||||
class="bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded">
|
||||
Back to List
|
||||
</a>
|
||||
</x-auth-check>
|
||||
</div>
|
||||
|
||||
@if($errors->any())
|
||||
<x-alert type="error" :message="'Please fix the following errors:'">
|
||||
<ul class="mt-2">
|
||||
@foreach($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</x-alert>
|
||||
@endif
|
||||
|
||||
<div class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
|
||||
<x-auth-check :permission="['type' => $thisResourceType, 'value' => $thisResourceValue, 'action' => 'edit']">
|
||||
<x-form :action="route('admin.user-roles.update', $userRole)" method="PUT">
|
||||
<div class="mb-6">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="user_id">
|
||||
User <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<select name="user_id"
|
||||
id="user_id"
|
||||