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"
|
||||
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', $userRole->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', $userRole->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', $userRole->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-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 Role Assignment
|
||||
</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-auth-check :permission="['type' => $thisResourceType, 'value' => $thisResourceValue, 'action' => 'delete']">
|
||||
<x-form
|
||||
:action="route('admin.user-roles.destroy', $userRole)"
|
||||
method="DELETE"
|
||||
onsubmit="return confirm('Are you sure you want to remove this role from the user? 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">
|
||||
Remove Role Assignment
|
||||
</button>
|
||||
</x-form>
|
||||
</x-auth-check>
|
||||
</div>
|
||||
</div>
|
||||
</x-auth-check>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
156
app/Components/Admin/resources/views/user-roles/index.blade.php
Normal file
156
app/Components/Admin/resources/views/user-roles/index.blade.php
Normal file
@ -0,0 +1,156 @@
|
||||
@extends('admin::layouts.admin')
|
||||
@section('title', 'User Roles')
|
||||
@section('content')
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-bold">User Role Assignments</h1>
|
||||
<x-auth-check :permission="['type' => $thisResourceType, 'value' => $thisResourceValue, 'action' => 'create']">
|
||||
<a href="{{ route('admin.user-roles.create') }}"
|
||||
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
|
||||
Assign 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.user-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 by user name, role name 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="user_name" {{ request('sort') === 'user_name' ? 'selected' : '' }}>User Name</option>
|
||||
<option value="role_name" {{ request('sort') === 'role_name' ? 'selected' : '' }}>Role Name</option>
|
||||
<option value="created_at" {{ request('sort', 'created_at') === 'created_at' ? 'selected' : '' }}>Date Assigned</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('search') || request('sort') || request('direction'))
|
||||
<a href="{{ route('admin.user-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($userRoles->isEmpty())
|
||||
<div class="text-center py-4">
|
||||
<p class="text-gray-500">No user role assignments 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">
|
||||
Role
|
||||
</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">
|
||||
Date Assigned
|
||||
</th>
|
||||
@if(auth()->user()->checkResourcePermission($thisResourceType, $thisResourceValue, 'edit') ||
|
||||
auth()->user()->checkResourcePermission($thisResourceType, $thisResourceValue, 'delete'))
|
||||
<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($userRoles as $userRole)
|
||||
<tr>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm font-medium text-gray-900">
|
||||
{{ $userRole->user->name }}
|
||||
</div>
|
||||
<div class="text-sm text-gray-500">
|
||||
{{ $userRole->user->email }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
{{ $userRole->role->name }}
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="text-sm text-gray-900">
|
||||
{{ $userRole->description ?: 'No description' }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
{{ $userRole->created_at->format('Y-m-d H:i:s') }}
|
||||
</td>
|
||||
@if(auth()->user()->checkResourcePermission($thisResourceType, $thisResourceValue, 'edit') ||
|
||||
auth()->user()->checkResourcePermission($thisResourceType, $thisResourceValue, 'delete'))
|
||||
<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.user-roles.edit', $userRole) }}"
|
||||
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.user-roles.destroy', $userRole)"
|
||||
method="DELETE"
|
||||
class="inline-block"
|
||||
onsubmit="return confirm('Are you sure you want to remove this role from the user?');"
|
||||
>
|
||||
<button type="submit"
|
||||
class="text-red-600 hover:text-red-900">
|
||||
Remove
|
||||
</button>
|
||||
</x-form>
|
||||
</x-auth-check>
|
||||
</td>
|
||||
@endif
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="mt-4">
|
||||
{{ $userRoles->links() }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endsection
|
90
app/Components/Admin/resources/views/users/create.blade.php
Normal file
90
app/Components/Admin/resources/views/users/create.blade.php
Normal file
@ -0,0 +1,90 @@
|
||||
@extends('admin::layouts.admin')
|
||||
|
||||
@section('title', 'Create User')
|
||||
|
||||
@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 User</h1>
|
||||
<x-auth-check :permission="['type' => $thisResourceType, 'value' => $thisResourceValue, 'action' => 'view']">
|
||||
<a href="{{ route('admin.users.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>
|
||||
|
||||
<div class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
|
||||
<x-form :action="route('admin.users.store')" method="POST">
|
||||
<x-input-field
|
||||
name="name"
|
||||
label="Name"
|
||||
:value="old('name')"
|
||||
required
|
||||
/>
|
||||
|
||||
<x-input-field
|
||||
type="email"
|
||||
name="email"
|
||||
label="Email"
|
||||
:value="old('email')"
|
||||
required
|
||||
/>
|
||||
|
||||
<x-input-field
|
||||
type="password"
|
||||
name="password"
|
||||
label="Password"
|
||||
required
|
||||
/>
|
||||
|
||||
<x-input-field
|
||||
type="password"
|
||||
name="password_confirmation"
|
||||
label="Confirm Password"
|
||||
required
|
||||
/>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox"
|
||||
name="active"
|
||||
value="1"
|
||||
{{ old('active', true) ? 'checked' : '' }}
|
||||
class="form-checkbox">
|
||||
<span class="ml-2">Active</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end gap-4">
|
||||
<x-auth-check :permission="['type' => $thisResourceType, 'value' => $thisResourceValue, 'action' => 'view']">
|
||||
<a href="{{ route('admin.users.index') }}"
|
||||
class="bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">
|
||||
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 User
|
||||
</button>
|
||||
</div>
|
||||
</x-form>
|
||||
</div>
|
||||
</x-auth-check>
|
||||
|
||||
<x-auth-check :permission="['type' => $thisResourceType, 'value' => $thisResourceValue, 'action' => 'create']" else>
|
||||
<div class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
|
||||
<div class="text-center py-4">
|
||||
<p class="text-red-500">You don't have permission to create users.</p>
|
||||
<x-auth-check :permission="['type' => $thisResourceType, 'value' => $thisResourceValue, 'action' => 'view']">
|
||||
<a href="{{ route('admin.users.index') }}"
|
||||
class="mt-4 inline-block bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded">
|
||||
Back to Users List
|
||||
</a>
|
||||
</x-auth-check>
|
||||
</div>
|
||||
</div>
|
||||
</x-auth-check>
|
||||
</div>
|
||||
@endsection
|
91
app/Components/Admin/resources/views/users/edit.blade.php
Normal file
91
app/Components/Admin/resources/views/users/edit.blade.php
Normal file
@ -0,0 +1,91 @@
|
||||
@extends('admin::layouts.admin')
|
||||
|
||||
@section('title', 'Edit User')
|
||||
|
||||
@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 User: {{ $user->name }}</h1>
|
||||
<x-auth-check :permission="['type' => $thisResourceType, 'value' => $thisResourceValue, 'action' => 'view']">
|
||||
<a href="{{ route('admin.users.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>
|
||||
|
||||
<div class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
|
||||
<x-form :action="route('admin.users.update', $user)" method="PUT">
|
||||
<x-input-field
|
||||
name="name"
|
||||
label="Name"
|
||||
:value="old('name', $user->name)"
|
||||
required
|
||||
/>
|
||||
|
||||
<x-input-field
|
||||
type="email"
|
||||
name="email"
|
||||
label="Email"
|
||||
:value="old('email', $user->email)"
|
||||
required
|
||||
/>
|
||||
|
||||
<x-input-field
|
||||
type="password"
|
||||
name="password"
|
||||
label="Password"
|
||||
/>
|
||||
<p class="text-gray-600 text-xs mt-1">Leave blank to keep current password</p>
|
||||
|
||||
<x-input-field
|
||||
type="password"
|
||||
name="password_confirmation"
|
||||
label="Confirm Password"
|
||||
/>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox"
|
||||
name="active"
|
||||
value="1"
|
||||
{{ old('active', $user->active) ? 'checked' : '' }}
|
||||
class="form-checkbox">
|
||||
<span class="ml-2">Active</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end gap-4">
|
||||
<x-auth-check :permission="['type' => $thisResourceType, 'value' => $thisResourceValue, 'action' => 'view']">
|
||||
<a href="{{ route('admin.users.index') }}"
|
||||
class="bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">
|
||||
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">
|
||||
Update User
|
||||
</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.users.destroy', $user)"
|
||||
method="DELETE"
|
||||
onsubmit="return confirm('Are you sure you want to delete this user? 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 User
|
||||
</button>
|
||||
</x-form>
|
||||
</div>
|
||||
</x-auth-check>
|
||||
</x-auth-check>
|
||||
</div>
|
||||
@endsection
|
144
app/Components/Admin/resources/views/users/index.blade.php
Normal file
144
app/Components/Admin/resources/views/users/index.blade.php
Normal file
@ -0,0 +1,144 @@
|
||||
@extends('admin::layouts.admin')
|
||||
@section('title', 'Users')
|
||||
@section('content')
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-bold">Users</h1>
|
||||
<x-auth-check :permission="['type' => $thisResourceType, 'value' => $thisResourceValue, 'action' => 'create']">
|
||||
<a href="{{ route('admin.users.create') }}"
|
||||
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
|
||||
Create New User
|
||||
</a>
|
||||
</x-auth-check>
|
||||
</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-auth-check :permission="['type' => $thisResourceType, 'value' => $thisResourceValue, 'action' => 'view']">
|
||||
<!-- Search and Filter Section -->
|
||||
<div class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
|
||||
<x-form :action="route('admin.users.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 email...">
|
||||
</div>
|
||||
<div class="w-48">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="status">
|
||||
Status
|
||||
</label>
|
||||
<select id="status"
|
||||
name="status"
|
||||
class="shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline">
|
||||
<option value="">All Status</option>
|
||||
<option value="active" {{ request('status') === 'active' ? 'selected' : '' }}>Active</option>
|
||||
<option value="inactive" {{ request('status') === 'inactive' ? 'selected' : '' }}>Inactive</option>
|
||||
</select>
|
||||
</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="email" {{ request('sort') === 'email' ? 'selected' : '' }}>Email</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', 'status', 'sort']))
|
||||
<a href="{{ route('admin.users.index') }}"
|
||||
class="ml-2 text-gray-600 hover:text-gray-900">
|
||||
Clear
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
</x-form>
|
||||
</div>
|
||||
|
||||
<div class="mb-4 text-sm text-gray-600">
|
||||
Total Users: {{ $users->total() }}
|
||||
</div>
|
||||
|
||||
@if($users->isEmpty())
|
||||
<div class="text-center py-4">
|
||||
<p class="text-gray-500">No users 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">
|
||||
Email
|
||||
</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($users as $user)
|
||||
<tr>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
{{ $user->name }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
{{ $user->email }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full
|
||||
{{ $user->active ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800' }}">
|
||||
{{ $user->active ? 'Active' : 'Inactive' }}
|
||||
</span>
|
||||
</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.users.edit', $user) }}"
|
||||
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.users.destroy', $user)"
|
||||
method="DELETE"
|
||||
class="inline-block"
|
||||
onsubmit="return confirm('Are you sure you want to delete this user?');"
|
||||
>
|
||||
<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">
|
||||
{{ $users->links() }}
|
||||
</div>
|
||||
@endif
|
||||
</x-auth-check>
|
||||
</div>
|
||||
@endsection
|
@ -0,0 +1,140 @@
|
||||
@extends('admin::layouts.admin')
|
||||
@section('title', 'Manage User Roles')
|
||||
@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 Roles: {{ $user->name }}</h1>
|
||||
<a href="{{ route('admin.users.index') }}"
|
||||
class="bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded">
|
||||
Back to Users
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@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
|
||||
|
||||
@if(session('error'))
|
||||
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative mb-4">
|
||||
{{ session('error') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Current Role Assignments --}}
|
||||
<div class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
|
||||
<h2 class="text-xl font-semibold mb-4">Current Role Assignments</h2>
|
||||
|
||||
@if($userAccessRoles->isEmpty())
|
||||
<p class="text-gray-500 mb-4">No roles currently assigned.</p>
|
||||
@else
|
||||
<table class="min-w-full mb-4">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-left">Role</th>
|
||||
<th class="text-left">Context</th>
|
||||
<th class="text-left">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($userAccessRoles as $userRole)
|
||||
<tr class="border-t">
|
||||
<td class="py-2">{{ $userRole->role->name }}</td>
|
||||
<td>
|
||||
@if($userRole->store)
|
||||
Store: {{ $userRole->store->name }}
|
||||
@elseif($userRole->group)
|
||||
Group: {{ $userRole->group->name }}
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
<form method="POST"
|
||||
action="{{ route('admin.users.roles.remove', ['user' => $user->id, 'userAccessRole' => $userRole->id]) }}"
|
||||
class="inline"
|
||||
onsubmit="return confirm('Are you sure you want to remove this role?');">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit"
|
||||
class="text-red-600 hover:text-red-900">
|
||||
Remove
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
@endif
|
||||
|
||||
{{-- Assign New Role Form --}}
|
||||
<h2 class="text-xl font-semibold mb-4">Assign New Role</h2>
|
||||
<form method="POST" action="{{ route('admin.users.roles.assign', $user) }}">
|
||||
@csrf
|
||||
|
||||
<div class="grid grid-cols-1 gap-6">
|
||||
{{-- Role Selection --}}
|
||||
<div>
|
||||
<label for="role_id" class="block text-sm font-medium text-gray-700">Role</label>
|
||||
<select name="role_id" id="role_id" required
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50">
|
||||
<option value="">Select a role...</option>
|
||||
@foreach($roles as $role)
|
||||
<option value="{{ $role->id }}">{{ $role->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{{-- Context Selection --}}
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
{{-- Store Selection --}}
|
||||
<div>
|
||||
<label for="store_id" class="block text-sm font-medium text-gray-700">Store (Optional)</label>
|
||||
<select name="store_id" id="store_id"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50">
|
||||
<option value="">Select a store...</option>
|
||||
@foreach($stores as $store)
|
||||
<option value="{{ $store->id }}">{{ $store->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{{-- Group Selection --}}
|
||||
<div>
|
||||
<label for="group_id" class="block text-sm font-medium text-gray-700">Group (Optional)</label>
|
||||
<select name="group_id" id="group_id"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50">
|
||||
<option value="">Select a group...</option>
|
||||
@foreach($groups as $group)
|
||||
<option value="{{ $group->id }}">{{ $group->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</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">
|
||||
Assign Role
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Make store and group selections mutually exclusive
|
||||
document.getElementById('store_id').addEventListener('change', function() {
|
||||
if (this.value) {
|
||||
document.getElementById('group_id').value = '';
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('group_id').addEventListener('change', function() {
|
||||
if (this.value) {
|
||||
document.getElementById('store_id').value = '';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endsection
|
@ -0,0 +1,71 @@
|
||||
@extends('admin::layouts.admin')
|
||||
@section('title', 'Create Web Page')
|
||||
@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 Web Page</h1>
|
||||
<a href="{{ route('admin.web-pages.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())
|
||||
<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">
|
||||
<x-form :action="route('admin.web-pages.store')" method="POST">
|
||||
<div class="mb-6">
|
||||
<x-input-field
|
||||
type="text"
|
||||
name="url"
|
||||
label="URL"
|
||||
:value="old('url')"
|
||||
placeholder="Enter web page URL"
|
||||
required
|
||||
autofocus>
|
||||
<x-slot name="labelSuffix">
|
||||
<span class="text-red-500">*</span>
|
||||
</x-slot>
|
||||
<x-slot name="hint">
|
||||
<p class="text-gray-600 text-xs mt-1">The URL must be unique and cannot exceed 255 characters.</p>
|
||||
</x-slot>
|
||||
</x-input-field>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<x-input-field
|
||||
type="textarea"
|
||||
name="description"
|
||||
label="Description"
|
||||
:value="old('description')"
|
||||
placeholder="Enter web page description"
|
||||
rows="4">
|
||||
<x-slot name="hint">
|
||||
<p class="text-gray-600 text-xs mt-1">Provide a clear description of what this web page represents.</p>
|
||||
</x-slot>
|
||||
</x-input-field>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<a href="{{ route('admin.web-pages.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 Web Page
|
||||
</button>
|
||||
</div>
|
||||
</x-form>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
@ -0,0 +1,85 @@
|
||||
@extends('admin::layouts.admin')
|
||||
@section('title', 'Edit Web Page')
|
||||
@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 Web Page</h1>
|
||||
<a href="{{ route('admin.web-pages.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())
|
||||
<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">
|
||||
<x-form :action="route('admin.web-pages.update', $webPage)" method="PUT">
|
||||
<div class="mb-6">
|
||||
<x-input-field
|
||||
type="text"
|
||||
name="url"
|
||||
label="URL"
|
||||
:value="old('url', $webPage->url)"
|
||||
placeholder="Enter web page URL"
|
||||
required>
|
||||
<x-slot name="labelSuffix">
|
||||
<span class="text-red-500">*</span>
|
||||
</x-slot>
|
||||
<x-slot name="hint">
|
||||
<p class="text-gray-600 text-xs mt-1">The URL must be unique and cannot exceed 255 characters.</p>
|
||||
</x-slot>
|
||||
</x-input-field>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<x-input-field
|
||||
type="textarea"
|
||||
name="description"
|
||||
label="Description"
|
||||
:value="old('description', $webPage->description)"
|
||||
placeholder="Enter web page description"
|
||||
rows="4">
|
||||
<x-slot name="hint">
|
||||
<p class="text-gray-600 text-xs mt-1">Provide a clear description of what this web page represents.</p>
|
||||
</x-slot>
|
||||
</x-input-field>
|
||||
</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 Web Page
|
||||
</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.web-pages.destroy', $webPage)"
|
||||
method="DELETE"
|
||||
onsubmit="return confirm('Are you sure you want to delete this web page? 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 Web Page
|
||||
</button>
|
||||
</x-form>
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-gray-600">
|
||||
Once you delete this web page, there is no going back. Please be certain.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
124
app/Components/Admin/resources/views/web-pages/index.blade.php
Normal file
124
app/Components/Admin/resources/views/web-pages/index.blade.php
Normal file
@ -0,0 +1,124 @@
|
||||
@extends('admin::layouts.admin')
|
||||
@section('title', 'Web Pages')
|
||||
|
||||
@section('content')
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-bold">Web Pages</h1>
|
||||
@if(auth()->user()->checkResourcePermission($thisResourceType, 'admin.web-pages.php','create'))
|
||||
<a href="{{ route('admin.web-pages.create') }}"
|
||||
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
|
||||
Create New Page
|
||||
</a>
|
||||
@endif
|
||||
</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.web-pages.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 web pages...">
|
||||
</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="url" {{ request('sort', 'url') === 'url' ? 'selected' : '' }}>URL</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>
|
||||
<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.web-pages.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($webPages->isEmpty())
|
||||
<div class="text-center py-4">
|
||||
<p class="text-gray-500">No web pages 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">
|
||||
URL
|
||||
</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($webPages as $page)
|
||||
<tr>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
{{ $page->url }}
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="text-sm text-gray-900">
|
||||
{{ $page->description ?: 'No description' }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm">
|
||||
{{ $page->created_at->format('Y-m-d H:i:s') }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
@if(auth()->user()->checkResourcePermission('web_pages', 'admin.web-pages.php','edit'))
|
||||
<a href="{{ route('admin.web-pages.edit', $page) }}"
|
||||
class="text-indigo-600 hover:text-indigo-900 mr-3">Edit</a>
|
||||
@endif
|
||||
|
||||
@if(auth()->user()->checkResourcePermission('web_pages', 'admin.web-pages.php','delete'))
|
||||
<x-form
|
||||
:action="route('admin.web-pages.destroy', $page)"
|
||||
method="DELETE"
|
||||
class="inline-block"
|
||||
onsubmit="return confirm('Are you sure you want to delete this web page? This action cannot be undone.');">
|
||||
<button type="submit"
|
||||
class="text-red-600 hover:text-red-900">
|
||||
Delete
|
||||
</button>
|
||||
</x-form>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="mt-4">
|
||||
{{ $webPages->links() }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endsection
|
193
app/Components/Admin/routes/web.php
Normal file
193
app/Components/Admin/routes/web.php
Normal file
@ -0,0 +1,193 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use App\Components\Admin\Http\Controllers\UsersController;
|
||||
use App\Components\Admin\Http\Controllers\RolesController;
|
||||
use App\Components\Admin\Http\Controllers\PermissionsController;
|
||||
use App\Components\Admin\Http\Controllers\ResourceTypesController;
|
||||
use App\Components\Admin\Http\Controllers\ResourceTypeMappingsController;
|
||||
use App\Components\Admin\Http\Controllers\WebPagesController;
|
||||
use App\Components\Admin\Http\Controllers\MenuTypesController;
|
||||
use App\Components\Admin\Http\Controllers\NavigationItemsController;
|
||||
use App\Components\Admin\Http\Controllers\AdminDashboardController;
|
||||
use App\Components\Admin\Http\Controllers\UserRolesController;
|
||||
use App\Components\Admin\Http\Controllers\ResourceAssociationsController;
|
||||
use App\Components\Admin\Http\Controllers\MigrationsController;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Admin Web Routes
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
Route::prefix('admin')->name('admin.')->middleware(['web', 'auth'])->group(function () {
|
||||
|
||||
Route::middleware(['resource.access:web_pages,admin.roles.php'])
|
||||
->group(function () {
|
||||
// Resource routes with updated naming convention
|
||||
Route::resource('roles', RolesController::class)
|
||||
->except(['show'])
|
||||
->names([
|
||||
'index' => 'roles.index',
|
||||
'create' => 'roles.create',
|
||||
'store' => 'roles.store',
|
||||
'edit' => 'roles.edit',
|
||||
'update' => 'roles.update',
|
||||
'destroy' => 'roles.destroy',
|
||||
]);
|
||||
|
||||
// Permission management routes
|
||||
Route::get('/roles/{role}/permissions', [RolesController::class, 'managePermissions'])
|
||||
->name('roles.permissions');
|
||||
|
||||
Route::post('/roles/{role}/permissions', [RolesController::class, 'updatePermissions'])
|
||||
->name('roles.permissions.update');
|
||||
});
|
||||
|
||||
Route::middleware('resource.access:web_pages,admin.users.php')
|
||||
->group(function () {
|
||||
Route::resource('users', UsersController::class)
|
||||
->except(['show']) // We don't need a show route for users
|
||||
->names([
|
||||
'index' => 'users.index',
|
||||
'create' => 'users.create',
|
||||
'store' => 'users.store',
|
||||
'edit' => 'users.edit',
|
||||
'update' => 'users.update',
|
||||
'destroy' => 'users.destroy',
|
||||
]);
|
||||
});
|
||||
|
||||
// Resource Types management
|
||||
Route::middleware('resource.access:web_pages,admin.resource-types.php')->group(function () {
|
||||
Route::resource('resource-types', ResourceTypesController::class);
|
||||
});
|
||||
|
||||
// Resource Type Mappings Routes
|
||||
Route::middleware('resource.access:web_pages,admin.resource-type-mappings.php')
|
||||
->group(function () {
|
||||
// AJAX routes must come BEFORE resource routes to prevent conflicts
|
||||
Route::prefix('resource-type-mappings')
|
||||
->name('resource-type-mappings.')
|
||||
->middleware(['resource.access:web_pages,admin.resource-type-mappings.php'])
|
||||
->group(function () {
|
||||
Route::get('/ajax/tables', [ResourceTypeMappingsController::class, 'getTables'])
|
||||
->name('tables');
|
||||
Route::get('/ajax/columns', [ResourceTypeMappingsController::class, 'getColumns'])
|
||||
->name('columns');
|
||||
});
|
||||
|
||||
// Main resource routes
|
||||
Route::resource('resource-type-mappings', ResourceTypeMappingsController::class)
|
||||
->parameters([
|
||||
'resource-type-mappings' => 'resource_type_mapping'
|
||||
])
|
||||
->except(['show']) // We don't need a show route
|
||||
->names([
|
||||
'index' => 'resource-type-mappings.index',
|
||||
'create' => 'resource-type-mappings.create',
|
||||
'store' => 'resource-type-mappings.store',
|
||||
'edit' => 'resource-type-mappings.edit',
|
||||
'update' => 'resource-type-mappings.update',
|
||||
'destroy' => 'resource-type-mappings.destroy',
|
||||
]);
|
||||
});
|
||||
|
||||
Route::middleware('resource.access:web_pages,admin.menu-types.php')
|
||||
->group(function () {
|
||||
Route::resource('menu-types', MenuTypesController::class)
|
||||
->except(['show']) // Since we don't have a show route
|
||||
->names([
|
||||
'index' => 'menu-types.index',
|
||||
'create' => 'menu-types.create',
|
||||
'store' => 'menu-types.store',
|
||||
'edit' => 'menu-types.edit',
|
||||
'update' => 'menu-types.update',
|
||||
'destroy' => 'menu-types.destroy',
|
||||
]);
|
||||
});
|
||||
|
||||
Route::middleware('resource.access:web_pages,admin.navigation-items.php')
|
||||
->group(function () {
|
||||
Route::resource('navigation-items', NavigationItemsController::class)
|
||||
->except(['show']) // We don't have a show route
|
||||
->names([
|
||||
'index' => 'navigation-items.index',
|
||||
'create' => 'navigation-items.create',
|
||||
'store' => 'navigation-items.store',
|
||||
'edit' => 'navigation-items.edit',
|
||||
'update' => 'navigation-items.update',
|
||||
'destroy' => 'navigation-items.destroy',
|
||||
]);
|
||||
});
|
||||
|
||||
// Route::resource('permissions', PermissionsController::class);
|
||||
Route::get('/', [AdminDashboardController::class, 'index'])->name('admin.dashboard');
|
||||
|
||||
Route::get('/database-io', [AdminDashboardController::class, 'getDatabaseIO'])
|
||||
->name('database.io');
|
||||
|
||||
Route::middleware('resource.access:web_pages,admin.user-roles.php')
|
||||
->group(function () {
|
||||
Route::resource('user-roles', UserRolesController::class)
|
||||
->parameters([
|
||||
'user-roles' => 'userRole'
|
||||
])
|
||||
->except(['show']) // We don't have a show route
|
||||
->names([
|
||||
'index' => 'user-roles.index',
|
||||
'create' => 'user-roles.create',
|
||||
'store' => 'user-roles.store',
|
||||
'edit' => 'user-roles.edit',
|
||||
'update' => 'user-roles.update',
|
||||
'destroy' => 'user-roles.destroy',
|
||||
]);
|
||||
});
|
||||
|
||||
Route::middleware('resource.access:web_pages,admin.web-pages.php')->group(function () {
|
||||
Route::resource('web-pages', WebPagesController::class);
|
||||
});
|
||||
|
||||
Route::middleware('resource.access:web_pages,admin.permissions.php')->group(function () {
|
||||
Route::resource('permissions', PermissionsController::class);
|
||||
});
|
||||
|
||||
|
||||
// Resource Associations Routes
|
||||
Route::middleware('resource.access:web_pages,admin.resource-associations.php')
|
||||
->group(function () {
|
||||
// AJAX routes must come before resource routes to prevent conflicts
|
||||
Route::prefix('resource-associations')->name('resource-associations.')->group(function () {
|
||||
Route::get('/roles', [ResourceAssociationsController::class, 'getRoles'])
|
||||
->name('roles');
|
||||
Route::get('/resources', [ResourceAssociationsController::class, 'getResources'])
|
||||
->name('resources');
|
||||
});
|
||||
|
||||
// Main resource routes
|
||||
Route::resource('resource-associations', ResourceAssociationsController::class)
|
||||
->parameters([
|
||||
'resource-associations' => 'resource_association'
|
||||
])
|
||||
->except(['show']) // We don't need a show route
|
||||
->names([
|
||||
'index' => 'resource-associations.index',
|
||||
'create' => 'resource-associations.create',
|
||||
'store' => 'resource-associations.store',
|
||||
'edit' => 'resource-associations.edit',
|
||||
'update' => 'resource-associations.update',
|
||||
'destroy' => 'resource-associations.destroy',
|
||||
]);
|
||||
});
|
||||
|
||||
Route::middleware('resource.access:web_pages,admin.migrations.php')
|
||||
->group(function () {
|
||||
Route::resource('migrations', MigrationsController::class)
|
||||
->only(['index', 'show'])
|
||||
->names([
|
||||
'index' => 'migrations.index',
|
||||
'show' => 'migrations.show',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
141
app/Components/Api/Http/Controllers/BaseApiController.php
Normal file
141
app/Components/Api/Http/Controllers/BaseApiController.php
Normal file
@ -0,0 +1,141 @@
|
||||
<?php
|
||||
namespace App\Components\Api\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class BaseApiController extends Controller
|
||||
{
|
||||
/**
|
||||
* Default success status code
|
||||
*/
|
||||
protected int $statusCode = 200;
|
||||
|
||||
/**
|
||||
* API Version
|
||||
*/
|
||||
protected string $apiVersion;
|
||||
|
||||
/**
|
||||
* Request ID
|
||||
*/
|
||||
protected string $requestId;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
// Generate a unique request ID if not already set
|
||||
$this->requestId = request()->header('X-Request-ID') ?? (string) Str::uuid();
|
||||
|
||||
// Get API version from route parameter or default to 'v1'
|
||||
$this->apiVersion = request()->route('version') ?? 'v1';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get response metadata.
|
||||
*
|
||||
* @param array $additionalMeta
|
||||
* @return array
|
||||
*/
|
||||
protected function getMetadata(array $additionalMeta = []): array
|
||||
{
|
||||
return array_merge([
|
||||
'timestamp' => now()->toIso8601String(),
|
||||
'request_id' => $this->requestId,
|
||||
'api_version' => $this->apiVersion,
|
||||
], $additionalMeta);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a success response.
|
||||
*
|
||||
* @param mixed $data
|
||||
* @param string|null $message
|
||||
* @param array $meta
|
||||
* @return JsonResponse
|
||||
*/
|
||||
protected function respondSuccess(mixed $data = null, ?string $message = null, array $meta = []): JsonResponse
|
||||
{
|
||||
$response = [
|
||||
'success' => true,
|
||||
'data' => $data,
|
||||
'meta' => $this->getMetadata($meta)
|
||||
];
|
||||
|
||||
if ($message) {
|
||||
$response['message'] = $message;
|
||||
}
|
||||
|
||||
return response()->json($response, $this->statusCode)
|
||||
->header('X-Request-ID', $this->requestId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an error response.
|
||||
*
|
||||
* @param string $message
|
||||
* @param mixed $errors
|
||||
* @param int $statusCode
|
||||
* @return JsonResponse
|
||||
*/
|
||||
protected function respondError(string $message, mixed $errors = null, int $statusCode = 400): JsonResponse
|
||||
{
|
||||
$response = [
|
||||
'success' => false,
|
||||
'message' => $message,
|
||||
'meta' => $this->getMetadata()
|
||||
];
|
||||
|
||||
if ($errors !== null) {
|
||||
$response['errors'] = $errors;
|
||||
}
|
||||
|
||||
return response()->json($response, $statusCode)
|
||||
->header('X-Request-ID', $this->requestId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a not found response.
|
||||
*
|
||||
* @param string $message
|
||||
* @return JsonResponse
|
||||
*/
|
||||
protected function respondNotFound(string $message = 'Resource not found'): JsonResponse
|
||||
{
|
||||
return $this->respondError($message, null, 404);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an unauthorized response.
|
||||
*
|
||||
* @param string $message
|
||||
* @return JsonResponse
|
||||
*/
|
||||
protected function respondUnauthorized(string $message = 'Unauthorized'): JsonResponse
|
||||
{
|
||||
return $this->respondError($message, null, 401);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a forbidden response.
|
||||
*
|
||||
* @param string $message
|
||||
* @return JsonResponse
|
||||
*/
|
||||
protected function respondForbidden(string $message = 'Forbidden'): JsonResponse
|
||||
{
|
||||
return $this->respondError($message, null, 403);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a validation error response.
|
||||
*
|
||||
* @param mixed $errors
|
||||
* @param string $message
|
||||
* @return JsonResponse
|
||||
*/
|
||||
protected function respondValidationError(mixed $errors, string $message = 'Validation failed'): JsonResponse
|
||||
{
|
||||
return $this->respondError($message, $errors, 422);
|
||||
}
|
||||
}
|
34
app/Components/Api/Http/Controllers/v1/AuthController.php
Normal file
34
app/Components/Api/Http/Controllers/v1/AuthController.php
Normal file
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
namespace App\Components\Api\Http\Controllers\v1;
|
||||
|
||||
use App\Components\Api\Http\Controllers\BaseApiController;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class AuthController extends BaseApiController
|
||||
{
|
||||
public function login(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'email' => 'required|email',
|
||||
'password' => 'required',
|
||||
'device_name' => 'required',
|
||||
]);
|
||||
|
||||
$user = User::where('email', $request->email)->first();
|
||||
|
||||
if (! $user || ! Hash::check($request->password, $user->password)) {
|
||||
throw ValidationException::withMessages([
|
||||
'email' => ['The provided credentials are incorrect.'],
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->respondSuccess([
|
||||
'token' => $user->createToken($request->device_name)->plainTextToken,
|
||||
'user' => $user
|
||||
], 'Login successful');
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
namespace App\Components\Api\Http\Controllers\v1\Cafe;
|
||||
|
||||
use App\Components\Api\Http\Controllers\BaseApiController;
|
||||
|
||||
class MenuController extends BaseApiController
|
||||
{
|
||||
public function hello()
|
||||
{
|
||||
return $this->respondSuccess(
|
||||
['message' => 'Hello from Cafe Menu System'],
|
||||
'Cafe module active'
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
namespace App\Components\Api\Http\Controllers\v1\Loyalty;
|
||||
|
||||
use App\Components\Api\Http\Controllers\BaseApiController;
|
||||
|
||||
class LoyaltyController extends BaseApiController
|
||||
{
|
||||
public function hello()
|
||||
{
|
||||
return $this->respondSuccess(
|
||||
['message' => 'Hello from Loyalty System'],
|
||||
'Loyalty module active'
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
namespace App\Components\Api\Http\Controllers\v1\Management;
|
||||
|
||||
use App\Components\Api\Http\Controllers\BaseApiController;
|
||||
|
||||
class ReportingController extends BaseApiController
|
||||
{
|
||||
public function hello()
|
||||
{
|
||||
return $this->respondSuccess(
|
||||
['message' => 'Hello from Management Reporting'],
|
||||
'Reporting module active'
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
namespace App\Components\Api\Http\Controllers\v1\Sbux;
|
||||
|
||||
use App\Components\Api\Http\Controllers\BaseApiController;
|
||||
|
||||
class SbuxController extends BaseApiController
|
||||
{
|
||||
public function hello()
|
||||
{
|
||||
return $this->respondSuccess(
|
||||
['message' => 'Hello from Sbux System'],
|
||||
'Sbux module active'
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
namespace App\Components\Api\Http\Controllers\v1\Trading;
|
||||
|
||||
use App\Components\Api\Http\Controllers\BaseApiController;
|
||||
|
||||
class DeskController extends BaseApiController
|
||||
{
|
||||
public function hello()
|
||||
{
|
||||
return $this->respondSuccess(
|
||||
['message' => 'Hello from Trading Desk'],
|
||||
'Trading module active'
|
||||
);
|
||||
}
|
||||
}
|
83
app/Components/Api/Http/Middleware/ApiVersioning.php
Normal file
83
app/Components/Api/Http/Middleware/ApiVersioning.php
Normal file
@ -0,0 +1,83 @@
|
||||
<?php
|
||||
namespace App\Components\Api\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class ApiVersioning
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param Closure $next
|
||||
* @param string|null $version
|
||||
* @return Response
|
||||
*/
|
||||
public function handle(Request $request, Closure $next, ?string $version = null): Response
|
||||
{
|
||||
// Get supported versions from config
|
||||
$supportedVersions = config('api.versions', ['v1']);
|
||||
$defaultVersion = config('api.default_version', 'v1');
|
||||
|
||||
// Check version from different sources in order of priority:
|
||||
// 1. URL segment (from route parameter)
|
||||
// 2. Accept header with version (Accept: application/vnd.api.{version}+json)
|
||||
// 3. X-API-Version header
|
||||
// 4. Default version
|
||||
|
||||
$requestedVersion = $version ?? // From route parameter
|
||||
$this->getVersionFromAcceptHeader($request) ??
|
||||
$request->header('X-API-Version') ??
|
||||
$defaultVersion;
|
||||
|
||||
// Clean up version string (remove 'v' prefix if present)
|
||||
$requestedVersion = ltrim(strtolower($requestedVersion), 'v');
|
||||
$requestedVersion = 'v' . $requestedVersion;
|
||||
|
||||
// Check if requested version is supported
|
||||
if (!in_array($requestedVersion, $supportedVersions)) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Unsupported API version',
|
||||
'meta' => [
|
||||
'supported_versions' => $supportedVersions,
|
||||
'current_version' => $requestedVersion,
|
||||
'timestamp' => now()->toIso8601String(),
|
||||
]
|
||||
], 400);
|
||||
}
|
||||
|
||||
// Add version to request for use in controllers
|
||||
$request->merge(['api_version' => $requestedVersion]);
|
||||
|
||||
// Add version to route parameters
|
||||
$request->route()->forgetParameter('version');
|
||||
$request->route()->setParameter('version', $requestedVersion);
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract version from Accept header.
|
||||
*
|
||||
* @param Request $request
|
||||
* @return string|null
|
||||
*/
|
||||
protected function getVersionFromAcceptHeader(Request $request): ?string
|
||||
{
|
||||
$accept = $request->header('Accept');
|
||||
|
||||
if (!$accept) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Match version in Accept header (application/vnd.api.v1+json)
|
||||
if (preg_match('/application\/vnd\.api\.v(\d+)\+json/', $accept, $matches)) {
|
||||
return 'v' . $matches[1];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
83
app/Components/Api/Providers/ApiServiceProvider.php
Normal file
83
app/Components/Api/Providers/ApiServiceProvider.php
Normal file
@ -0,0 +1,83 @@
|
||||
<?php
|
||||
// app/Components/Api/Providers/ApiServiceProvider.php
|
||||
|
||||
namespace App\Components\Api\Providers;
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class ApiServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register any API services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
// Register config file if we create one later
|
||||
$this->mergeConfigFrom(
|
||||
__DIR__ . '/../config/api.php', 'api'
|
||||
);
|
||||
|
||||
// Register our API routes
|
||||
$this->registerRoutes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap any API services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
// Load routes
|
||||
if ($this->app->routesAreCached()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Register middleware
|
||||
$this->registerMiddleware();
|
||||
|
||||
// Load views if we add any later
|
||||
$this->loadViewsFrom(__DIR__ . '/../resources/views', 'api');
|
||||
|
||||
// Load translations if we add any later
|
||||
$this->loadTranslationsFrom(__DIR__ . '/../resources/lang', 'api');
|
||||
|
||||
// Publish configuration if we add it
|
||||
$this->publishes([
|
||||
__DIR__ . '/../config/api.php' => config_path('api.php'),
|
||||
], 'api-config');
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the API routes.
|
||||
*/
|
||||
protected function registerRoutes(): void
|
||||
{
|
||||
Route::group([
|
||||
'prefix' => 'api',
|
||||
'middleware' => ['api'],
|
||||
// 'namespace' => 'App\Components\Api\Http\Controllers',
|
||||
], function () {
|
||||
$this->loadRoutesFrom(__DIR__ . '/../routes/api.php');
|
||||
});
|
||||
|
||||
// Version-specific routes
|
||||
Route::group([
|
||||
'prefix' => 'api/v1',
|
||||
'middleware' => ['api', 'api.version:v1'],
|
||||
// 'namespace' => 'App\Components\Api\Http\Controllers\v1',
|
||||
], function () {
|
||||
$this->loadRoutesFrom(__DIR__ . '/../routes/v1/api.php');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Register API middleware.
|
||||
*/
|
||||
protected function registerMiddleware(): void
|
||||
{
|
||||
$router = $this->app['router'];
|
||||
|
||||
// Add our custom middleware
|
||||
$router->aliasMiddleware('api.version', \App\Components\Api\Http\Middleware\ApiVersioning::class);
|
||||
}
|
||||
}
|
35
app/Components/Api/config/api.php
Normal file
35
app/Components/Api/config/api.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| API Versions
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| List of supported API versions
|
||||
|
|
||||
*/
|
||||
'versions' => [
|
||||
'v1',
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default API Version
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The default API version to use when not specified
|
||||
|
|
||||
*/
|
||||
'default_version' => 'v1',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Request ID Header
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The header key to use for the request ID
|
||||
|
|
||||
*/
|
||||
'request_id_header' => 'X-Request-ID',
|
||||
];
|
0
app/Components/Api/routes/api.php
Normal file
0
app/Components/Api/routes/api.php
Normal file
9
app/Components/Api/routes/v1/api.php
Normal file
9
app/Components/Api/routes/v1/api.php
Normal file
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
require __DIR__ . '/auth.php';
|
||||
require __DIR__ . '/cafe.php';
|
||||
require __DIR__ . '/loyalty.php';
|
||||
require __DIR__ . '/management.php';
|
||||
require __DIR__ . '/sbux.php';
|
||||
require __DIR__ . '/trading.php';
|
5
app/Components/Api/routes/v1/auth.php
Normal file
5
app/Components/Api/routes/v1/auth.php
Normal file
@ -0,0 +1,5 @@
|
||||
<?php
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use App\Components\Api\Http\Controllers\v1\AuthController;
|
||||
|
||||
Route::post('/login', [AuthController::class, 'login'])->name('api.login');
|
9
app/Components/Api/routes/v1/cafe.php
Normal file
9
app/Components/Api/routes/v1/cafe.php
Normal file
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use App\Components\Api\Http\Controllers\v1\Cafe\MenuController;
|
||||
|
||||
Route::middleware(['auth:sanctum'])->group(function () {
|
||||
Route::prefix('cafe')->group(function () {
|
||||
Route::get('/hello', [MenuController::class, 'hello'])->name('cafe.hello');
|
||||
});
|
||||
});
|
9
app/Components/Api/routes/v1/loyalty.php
Normal file
9
app/Components/Api/routes/v1/loyalty.php
Normal file
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use App\Components\Api\Http\Controllers\v1\Loyalty\LoyaltyController;
|
||||
|
||||
Route::middleware(['auth:sanctum'])->group(function () {
|
||||
Route::prefix('loyalty')->group(function () {
|
||||
Route::get('/hello', [LoyaltyController::class, 'hello'])->name('loyalty.hello');
|
||||
});
|
||||
});
|
9
app/Components/Api/routes/v1/management.php
Normal file
9
app/Components/Api/routes/v1/management.php
Normal file
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use App\Components\Api\Http\Controllers\v1\Management\ReportingController;
|
||||
|
||||
Route::middleware(['auth:sanctum'])->group(function () {
|
||||
Route::prefix('management')->group(function () {
|
||||
Route::get('/hello', [ReportingController::class, 'hello'])->name('management.hello');
|
||||
});
|
||||
});
|
9
app/Components/Api/routes/v1/sbux.php
Normal file
9
app/Components/Api/routes/v1/sbux.php
Normal file
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use App\Components\Api\Http\Controllers\v1\Sbux\SbuxController;
|
||||
|
||||
Route::middleware(['auth:sanctum'])->group(function () {
|
||||
Route::prefix('sbux')->group(function () {
|
||||
Route::get('/hello', [SbuxController::class, 'hello'])->name('sbux.hello');
|
||||
});
|
||||
});
|
9
app/Components/Api/routes/v1/trading.php
Normal file
9
app/Components/Api/routes/v1/trading.php
Normal file
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use App\Components\Api\Http\Controllers\v1\Trading\DeskController;
|
||||
|
||||
Route::middleware(['auth:sanctum'])->group(function () {
|
||||
Route::prefix('trading')->group(function () {
|
||||
Route::get('/hello', [DeskController::class, 'hello'])->name('desk.hello');
|
||||
});
|
||||
});
|
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Components\DataExtraction\Contracts;
|
||||
|
||||
interface DataSourceInterface
|
||||
{
|
||||
/**
|
||||
* Establish connection to the data source
|
||||
*/
|
||||
public function connect(): bool;
|
||||
|
||||
/**
|
||||
* Disconnect from the data source
|
||||
*/
|
||||
public function disconnect(): void;
|
||||
|
||||
/**
|
||||
* Check if currently connected
|
||||
*/
|
||||
public function isConnected(): bool;
|
||||
|
||||
/**
|
||||
* Get the type of source (e.g., 'csv', 'api')
|
||||
*/
|
||||
public function getSourceType(): string;
|
||||
|
||||
/**
|
||||
* Get unique identifier for this source
|
||||
*/
|
||||
public function getSourceIdentifier(): string;
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Components\DataExtraction\Contracts;
|
||||
|
||||
interface ExtractorInterface
|
||||
{
|
||||
/**
|
||||
* Extract data from the given source
|
||||
*/
|
||||
public function extract(DataSourceInterface $source): array;
|
||||
|
||||
/**
|
||||
* Check if this extractor supports the given source
|
||||
*/
|
||||
public function supports(DataSourceInterface $source): bool;
|
||||
|
||||
/**
|
||||
* Get the data from the last extraction
|
||||
*/
|
||||
public function getLastExtraction(): ?array;
|
||||
|
||||
/**
|
||||
* Get the name of this extractor
|
||||
*/
|
||||
public function getExtractorName(): string;
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\Components\DataExtraction\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class ConnectionException extends Exception
|
||||
{
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\Components\DataExtraction\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class ExtractionException extends Exception
|
||||
{
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Components\DataExtraction\Providers;
|
||||
|
||||
use App\Components\DataExtraction\Contracts\ExtractorInterface;
|
||||
use App\Components\DataExtraction\Services\Extractors\CsvExtractor;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class DataExtractionServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register any application services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
// Register our CSV extractor as the default implementation
|
||||
$this->app->bind(ExtractorInterface::class, CsvExtractor::class);
|
||||
|
||||
// Register our CSV extractor specifically
|
||||
$this->app->bind('extractor.csv', function ($app) {
|
||||
return new CsvExtractor();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace App\Components\DataExtraction\Services\Extractors;
|
||||
|
||||
use App\Components\DataExtraction\Contracts\DataSourceInterface;
|
||||
use App\Components\DataExtraction\Exceptions\ConnectionException;
|
||||
use SplFileInfo;
|
||||
|
||||
class CsvDataSource implements DataSourceInterface
|
||||
{
|
||||
private ?SplFileInfo $file = null;
|
||||
private bool $isConnected = false;
|
||||
|
||||
/**
|
||||
* @param string $filePath Path to the CSV file
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly string $filePath,
|
||||
private readonly string $identifier
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Verify file exists and is readable
|
||||
* @throws ConnectionException
|
||||
*/
|
||||
public function connect(): bool
|
||||
{
|
||||
try {
|
||||
$this->file = new SplFileInfo($this->filePath);
|
||||
|
||||
if (!$this->file->isReadable() || $this->file->getExtension() !== 'csv') {
|
||||
throw new ConnectionException("File is not readable or is not a CSV file");
|
||||
}
|
||||
|
||||
$this->isConnected = true;
|
||||
return true;
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->isConnected = false;
|
||||
throw new ConnectionException("Failed to connect to CSV source: {$e->getMessage()}");
|
||||
}
|
||||
}
|
||||
|
||||
public function disconnect(): void
|
||||
{
|
||||
$this->file = null;
|
||||
$this->isConnected = false;
|
||||
}
|
||||
|
||||
public function isConnected(): bool
|
||||
{
|
||||
return $this->isConnected;
|
||||
}
|
||||
|
||||
public function getSourceType(): string
|
||||
{
|
||||
return 'csv';
|
||||
}
|
||||
|
||||
public function getSourceIdentifier(): string
|
||||
{
|
||||
return $this->identifier;
|
||||
}
|
||||
|
||||
public function getFile(): ?SplFileInfo
|
||||
{
|
||||
return $this->file;
|
||||
}
|
||||
}
|
@ -0,0 +1,165 @@
|
||||
<?php
|
||||
|
||||
namespace App\Components\DataExtraction\Services\Extractors;
|
||||
|
||||
use App\Components\DataExtraction\Contracts\DataSourceInterface;
|
||||
use App\Components\DataExtraction\Contracts\ExtractorInterface;
|
||||
use App\Components\DataExtraction\Exceptions\ExtractionException;
|
||||
|
||||
class CsvExtractor implements ExtractorInterface
|
||||
{
|
||||
private ?array $lastExtraction = null;
|
||||
private array $extractionErrors = [];
|
||||
private ?array $headers = null;
|
||||
|
||||
/**
|
||||
* Extract data from a CSV source
|
||||
*
|
||||
* @throws ExtractionException
|
||||
*/
|
||||
public function extract(DataSourceInterface $source): array
|
||||
{
|
||||
$this->resetState();
|
||||
|
||||
if (!$this->supports($source)) {
|
||||
throw new ExtractionException("Unsupported data source type");
|
||||
}
|
||||
|
||||
if (!$source->isConnected()) {
|
||||
$source->connect();
|
||||
}
|
||||
|
||||
/** @var CsvDataSource $source */
|
||||
$file = $source->getFile();
|
||||
|
||||
try {
|
||||
$handle = fopen($file->getRealPath(), 'r');
|
||||
if ($handle === false) {
|
||||
throw new ExtractionException("Could not open file for reading");
|
||||
}
|
||||
|
||||
// Read and validate headers
|
||||
$this->headers = $this->readHeaders($handle);
|
||||
if (empty($this->headers)) {
|
||||
throw new ExtractionException("No headers found in CSV file");
|
||||
}
|
||||
|
||||
$data = $this->processRows($handle);
|
||||
fclose($handle);
|
||||
$this->lastExtraction = $data;
|
||||
return $data;
|
||||
} catch (\Exception $e) {
|
||||
if (isset($handle) && is_resource($handle)) {
|
||||
fclose($handle);
|
||||
}
|
||||
throw new ExtractionException("Failed to extract CSV data: {$e->getMessage()}");
|
||||
}
|
||||
}
|
||||
|
||||
private function readHeaders($handle): array
|
||||
{
|
||||
$headers = fgetcsv($handle);
|
||||
if (!$headers) {
|
||||
return [];
|
||||
}
|
||||
// Clean up headers (trim whitespace, remove empty columns)
|
||||
return array_map(
|
||||
fn($header) => trim($header),
|
||||
array_filter($headers, fn($header) => !empty(trim($header)))
|
||||
);
|
||||
}
|
||||
|
||||
// if ($headers === false) {
|
||||
// throw new ExtractionException("Could not read CSV headers");
|
||||
// }
|
||||
//
|
||||
// $data = [];
|
||||
// while (($row = fgetcsv($handle)) !== false) {
|
||||
// // Combine headers with row data
|
||||
// $data[] = array_combine($headers, $row);
|
||||
// }
|
||||
//
|
||||
// fclose($handle);
|
||||
//
|
||||
// $this->lastExtraction = $data;
|
||||
// return $data;
|
||||
//
|
||||
// } catch (\Exception $e) {
|
||||
// throw new ExtractionException("Failed to extract CSV data: {$e->getMessage()}");
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
private function processRows($handle): array
|
||||
{
|
||||
$data = [];
|
||||
$rowNumber = 1;
|
||||
|
||||
while (($row = fgetcsv($handle)) !== false) {
|
||||
$rowNumber++;
|
||||
// Handle row having different number of columns than headers
|
||||
if(count($row) !== count($this->headers)) {
|
||||
$this->addError(
|
||||
$rowNumber,
|
||||
"Row has " . count($row) . "columns, expected " . count($this->headers)
|
||||
);
|
||||
// Pad or truncate row to match header count
|
||||
if (count($row) < count($this->headers)) {
|
||||
$row = array_pad($row, count($this->headers), null);
|
||||
} else {
|
||||
$row = array_slice($row, 0, count($this->headers));
|
||||
}
|
||||
}
|
||||
// Clean row data
|
||||
$row = array_map(fn($value) => $this->cleanValue($value), $row);
|
||||
// Combine with headers
|
||||
$rowData = array_combine($this->headers, $row);
|
||||
$data[] = $rowData;
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function cleanValue(?string $value): ?string
|
||||
{
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
$value = trim($value);
|
||||
return $value === '' ? null : $value;
|
||||
}
|
||||
|
||||
private function addError(int $row, string $message): void
|
||||
{
|
||||
$this->extractionErrors[] = [
|
||||
'row' => $row,
|
||||
'message' => $message,
|
||||
'timestamp' => now()
|
||||
];
|
||||
}
|
||||
|
||||
private function resetState(): void
|
||||
{
|
||||
$this->extractionErrors = [];
|
||||
$this->headers = null;
|
||||
}
|
||||
|
||||
public function getExtractionErrors(): array
|
||||
{
|
||||
return $this->extractionErrors;
|
||||
}
|
||||
|
||||
public function supports(DataSourceInterface $source): bool
|
||||
{
|
||||
return $source instanceof CsvDataSource && $source->getSourceType() === 'csv';
|
||||
}
|
||||
|
||||
public function getLastExtraction(): ?array
|
||||
{
|
||||
return $this->lastExtraction;
|
||||
}
|
||||
|
||||
public function getExtractorName(): string
|
||||
{
|
||||
return 'csv_extractor';
|
||||
}
|
||||
}
|
27
app/Console/Kernel.php
Normal file
27
app/Console/Kernel.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console;
|
||||
|
||||
use Illuminate\Console\Scheduling\Schedule;
|
||||
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
|
||||
|
||||
class Kernel extends ConsoleKernel
|
||||
{
|
||||
/**
|
||||
* Define the application's command schedule.
|
||||
*/
|
||||
protected function schedule(Schedule $schedule): void
|
||||
{
|
||||
// $schedule->command('inspire')->hourly();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the commands for the application.
|
||||
*/
|
||||
protected function commands(): void
|
||||
{
|
||||
$this->load(__DIR__.'/Commands');
|
||||
|
||||
require base_path('routes/console.php');
|
||||
}
|
||||
}
|
30
app/Exceptions/Handler.php
Normal file
30
app/Exceptions/Handler.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
||||
use Throwable;
|
||||
|
||||
class Handler extends ExceptionHandler
|
||||
{
|
||||
/**
|
||||
* The list of the inputs that are never flashed to the session on validation exceptions.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $dontFlash = [
|
||||
'current_password',
|
||||
'password',
|
||||
'password_confirmation',
|
||||
];
|
||||
|
||||
/**
|
||||
* Register the exception handling callbacks for the application.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
$this->reportable(function (Throwable $e) {
|
||||
//
|
||||
});
|
||||
}
|
||||
}
|
75
app/Helpers/IconHelper.php
Normal file
75
app/Helpers/IconHelper.php
Normal file
@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace App\Helpers;
|
||||
|
||||
class IconHelper
|
||||
{
|
||||
/**
|
||||
* Common icons mapped to their Font Awesome equivalents
|
||||
*/
|
||||
private static $commonIcons = [
|
||||
'dashboard' => 'gauge',
|
||||
'users' => 'users',
|
||||
'settings' => 'cog',
|
||||
'reports' => 'chart-bar',
|
||||
'menu' => 'bars',
|
||||
'resources' => 'folder',
|
||||
'navigation' => 'compass',
|
||||
'roles' => 'user-shield',
|
||||
'permissions' => 'key',
|
||||
'stores' => 'store',
|
||||
'pages' => 'file',
|
||||
'types' => 'tags',
|
||||
'list' => 'list',
|
||||
'home' => 'home',
|
||||
'database' => 'database',
|
||||
'server' => 'server',
|
||||
'business' => 'globe',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the full icon class string
|
||||
*/
|
||||
public static function getIconClasses(?string $icon = null): string
|
||||
{
|
||||
if (empty($icon)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Check if it's a common icon name
|
||||
if (isset(self::$commonIcons[$icon])) {
|
||||
return 'fas fa-' . self::$commonIcons[$icon];
|
||||
}
|
||||
|
||||
// If it's a direct FA icon name, use it
|
||||
return 'fas fa-' . $icon;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of common icons for the form selection
|
||||
*/
|
||||
public static function getCommonIcons(): array
|
||||
{
|
||||
$icons = [];
|
||||
foreach (self::$commonIcons as $name => $icon) {
|
||||
$icons[$name] = [
|
||||
'name' => ucfirst($name),
|
||||
'value' => $name,
|
||||
'classes' => self::getIconClasses($name)
|
||||
];
|
||||
}
|
||||
return $icons;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an icon name is valid
|
||||
*/
|
||||
public static function isValidIcon(?string $icon): bool
|
||||
{
|
||||
if (empty($icon)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return isset(self::$commonIcons[$icon]);
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user