Initial commit of base infrastructure

This commit is contained in:
trogers1884 2025-02-16 08:56:21 -06:00
commit 747b6b4004
298 changed files with 30815 additions and 0 deletions

18
.editorconfig Normal file
View 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
View 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
View 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
View 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
View 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

View 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);
}
}

View File

@ -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);
}
}

View 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');
});
}
}

View File

@ -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
]);
});
}
}

View File

@ -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');
});
}
}

View 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');
});
}
}

View File

@ -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';
}
}
}

View File

@ -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]);
}
}

View File

@ -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');
});
}
}

View 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');
});
}
}

View 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');
});
}
}

View 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');
});
}
}

View 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');
});
}
}

View 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';
}
}

View 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.',
];
}
}

View 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'
];
}
}

View 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'),
]);
}
}

View 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,
]);
}
}

View 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',
];
}
}

View File

@ -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
]);
}
}
}

View File

@ -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.'
];
}
}

View 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.'
];
}
}

View 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.'
];
}
}

View 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',
];
}
}

View 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'
];
}
}

View 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.',
];
}
}

View 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');
}
}
}

View 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 [];
}
}
}

View 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();
}
}

View 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);
}
}

View 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

View 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>

View File

@ -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

View 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

View 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

View 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

View 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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View 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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View 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

View 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

View 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

View File

@ -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

View 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

View 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

View 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

View 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

View 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

View 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

View File

@ -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

View File

@ -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

View File

@ -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

View 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

View 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',
]);
});
});

View 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);
}
}

View 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');
}
}

View File

@ -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'
);
}
}

View File

@ -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'
);
}
}

View File

@ -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'
);
}
}

View File

@ -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'
);
}
}

View File

@ -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'
);
}
}

View 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;
}
}

View 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);
}
}

View 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',
];

View File

View 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';

View 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');

View 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');
});
});

View 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');
});
});

View 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');
});
});

View 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');
});
});

View 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');
});
});

View File

@ -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;
}

View File

@ -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;
}

View File

@ -0,0 +1,9 @@
<?php
namespace App\Components\DataExtraction\Exceptions;
use Exception;
class ConnectionException extends Exception
{
}

View File

@ -0,0 +1,9 @@
<?php
namespace App\Components\DataExtraction\Exceptions;
use Exception;
class ExtractionException extends Exception
{
}

View File

@ -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
{
//
}
}

View File

@ -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;
}
}

View 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
View 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');
}
}

View 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) {
//
});
}
}

View 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