feat: add role management functionality with create, edit, and delete capabilities
- Implemented RoleEdit component for editing roles with permissions. - Created RoleIndex component for listing roles with delete confirmation. - Added routes for role management in web.php with appropriate permissions. - Developed RoleTest to cover authentication, authorization, and data integrity for role management.
This commit is contained in:
parent
67e5a6271f
commit
e9237f857e
67
app/Http/Controllers/Admin/RoleController.php
Normal file
67
app/Http/Controllers/Admin/RoleController.php
Normal file
@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\RoleRequest;
|
||||
use App\Services\Admin\Settings\RoleService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
class RoleController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private RoleService $service
|
||||
) {}
|
||||
|
||||
public function index(): Response
|
||||
{
|
||||
return Inertia::render('admin/roles/index', [
|
||||
'roles' => $this->service->getAll(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(): Response
|
||||
{
|
||||
return Inertia::render('admin/roles/create', [
|
||||
'permissions' => $this->service->getPermissionsByModule(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(RoleRequest $request): RedirectResponse
|
||||
{
|
||||
return $this->handleAction(
|
||||
fn() => $this->service->create($request->validated()),
|
||||
'Role berhasil ditambahkan.',
|
||||
'admin.settings.roles.index'
|
||||
);
|
||||
}
|
||||
|
||||
public function edit(Role $role): Response
|
||||
{
|
||||
return Inertia::render('admin/roles/edit', [
|
||||
'role' => $this->service->getById($role->id),
|
||||
'permissions' => $this->service->getPermissionsByModule(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(RoleRequest $request, Role $role): RedirectResponse
|
||||
{
|
||||
return $this->handleAction(
|
||||
fn() => $this->service->update($role, $request->validated()),
|
||||
'Role berhasil diperbarui.',
|
||||
'admin.settings.roles.index'
|
||||
);
|
||||
}
|
||||
|
||||
public function destroy(Role $role): RedirectResponse
|
||||
{
|
||||
$this->service->delete($role);
|
||||
|
||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Role berhasil dihapus.']);
|
||||
|
||||
return to_route('admin.settings.roles.index');
|
||||
}
|
||||
}
|
||||
38
app/Http/Requests/Admin/RoleRequest.php
Normal file
38
app/Http/Requests/Admin/RoleRequest.php
Normal file
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class RoleRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$roleId = $this->route('role')?->id;
|
||||
|
||||
return [
|
||||
'name' => [
|
||||
'required',
|
||||
'string',
|
||||
'max:100',
|
||||
Rule::unique('roles', 'name')->ignore($roleId, 'id'),
|
||||
],
|
||||
'permissions' => ['present', 'array'],
|
||||
'permissions.*' => ['string', 'exists:permissions,name'],
|
||||
];
|
||||
}
|
||||
|
||||
public function attributes(): array
|
||||
{
|
||||
return [
|
||||
'name' => 'Nama Role',
|
||||
'permissions' => 'Permission',
|
||||
];
|
||||
}
|
||||
}
|
||||
54
app/Services/Admin/Settings/RoleService.php
Normal file
54
app/Services/Admin/Settings/RoleService.php
Normal file
@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Admin\Settings;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
class RoleService
|
||||
{
|
||||
public function getAll(): Collection
|
||||
{
|
||||
return Role::withCount('permissions')->get();
|
||||
}
|
||||
|
||||
public function getById(int $id): Role
|
||||
{
|
||||
return Role::with('permissions')->findOrFail($id);
|
||||
}
|
||||
|
||||
public function create(array $data): Role
|
||||
{
|
||||
return DB::transaction(function () use ($data) {
|
||||
$role = Role::create(['name' => $data['name']]);
|
||||
$role->syncPermissions($data['permissions']);
|
||||
|
||||
return $role;
|
||||
});
|
||||
}
|
||||
|
||||
public function update(Role $role, array $data): Role
|
||||
{
|
||||
DB::transaction(function () use ($role, $data) {
|
||||
$role->update(['name' => $data['name']]);
|
||||
$role->syncPermissions($data['permissions']);
|
||||
});
|
||||
|
||||
return $role->fresh('permissions');
|
||||
}
|
||||
|
||||
public function delete(Role $role): bool
|
||||
{
|
||||
return $role->delete();
|
||||
}
|
||||
|
||||
public function getPermissionsByModule(): array
|
||||
{
|
||||
return Permission::all()
|
||||
->groupBy(fn ($p) => explode('.', $p->name)[0])
|
||||
->map(fn ($group) => $group->pluck('name')->map(fn ($name) => explode('.', $name, 2)[1])->values()->toArray())
|
||||
->toArray();
|
||||
}
|
||||
}
|
||||
@ -7,12 +7,15 @@
|
||||
use Illuminate\Foundation\Configuration\Middleware;
|
||||
use Illuminate\Http\Middleware\AddLinkHeadersForPreloadedAssets;
|
||||
use Illuminate\Http\Request;
|
||||
use Spatie\Permission\Middleware\RoleMiddleware;
|
||||
use Spatie\Permission\Middleware\PermissionMiddleware;
|
||||
use Spatie\Permission\Middleware\RoleOrPermissionMiddleware;
|
||||
|
||||
return Application::configure(basePath: dirname(__DIR__))
|
||||
->withRouting(
|
||||
web: __DIR__.'/../routes/web.php',
|
||||
api: __DIR__.'/../routes/api.php',
|
||||
commands: __DIR__.'/../routes/console.php',
|
||||
web: __DIR__ . '/../routes/web.php',
|
||||
api: __DIR__ . '/../routes/api.php',
|
||||
commands: __DIR__ . '/../routes/console.php',
|
||||
health: '/up',
|
||||
)
|
||||
->withMiddleware(function (Middleware $middleware): void {
|
||||
@ -23,9 +26,15 @@
|
||||
HandleInertiaRequests::class,
|
||||
AddLinkHeadersForPreloadedAssets::class,
|
||||
]);
|
||||
|
||||
$middleware->alias([
|
||||
'role' => RoleMiddleware::class,
|
||||
'permission' => PermissionMiddleware::class,
|
||||
'role_or_permission' => RoleOrPermissionMiddleware::class,
|
||||
]);
|
||||
})
|
||||
->withExceptions(function (Exceptions $exceptions): void {
|
||||
$exceptions->shouldRenderJsonWhen(
|
||||
fn (Request $request) => $request->is('api/*') || $request->expectsJson(),
|
||||
fn(Request $request) => $request->is('api/*') || $request->expectsJson(),
|
||||
);
|
||||
})->create();
|
||||
|
||||
219
config/permission.php
Normal file
219
config/permission.php
Normal file
@ -0,0 +1,219 @@
|
||||
<?php
|
||||
|
||||
use Spatie\Permission\DefaultTeamResolver;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
return [
|
||||
|
||||
'models' => [
|
||||
|
||||
/*
|
||||
* When using the "HasPermissions" trait from this package, we need to know which
|
||||
* Eloquent model should be used to retrieve your permissions. Of course, it
|
||||
* is often just the "Permission" model but you may use whatever you like.
|
||||
*
|
||||
* The model you want to use as a Permission model needs to implement the
|
||||
* `Spatie\Permission\Contracts\Permission` contract.
|
||||
*/
|
||||
|
||||
'permission' => Permission::class,
|
||||
|
||||
/*
|
||||
* When using the "HasRoles" trait from this package, we need to know which
|
||||
* Eloquent model should be used to retrieve your roles. Of course, it
|
||||
* is often just the "Role" model but you may use whatever you like.
|
||||
*
|
||||
* The model you want to use as a Role model needs to implement the
|
||||
* `Spatie\Permission\Contracts\Role` contract.
|
||||
*/
|
||||
|
||||
'role' => Role::class,
|
||||
|
||||
/*
|
||||
* When using the "Teams" feature from this package, we need to know which
|
||||
* Eloquent model should be used to retrieve your teams. Of course, it
|
||||
* is often just the "Team" model but you may use whatever you like.
|
||||
*/
|
||||
'team' => null,
|
||||
|
||||
/*
|
||||
* When using the "HasModels" trait and passing raw IDs to syncModels,
|
||||
* attachModels, or detachModels, this model class will be used to
|
||||
* resolve those IDs. If null, defaults to the guard's model.
|
||||
*/
|
||||
'default_model' => null,
|
||||
],
|
||||
|
||||
'table_names' => [
|
||||
|
||||
/*
|
||||
* When using the "HasRoles" trait from this package, we need to know which
|
||||
* table should be used to retrieve your roles. We have chosen a basic
|
||||
* default value but you may easily change it to any table you like.
|
||||
*/
|
||||
|
||||
'roles' => 'roles',
|
||||
|
||||
/*
|
||||
* When using the "HasPermissions" trait from this package, we need to know which
|
||||
* table should be used to retrieve your permissions. We have chosen a basic
|
||||
* default value but you may easily change it to any table you like.
|
||||
*/
|
||||
|
||||
'permissions' => 'permissions',
|
||||
|
||||
/*
|
||||
* When using the "HasPermissions" trait from this package, we need to know which
|
||||
* table should be used to retrieve your models permissions. We have chosen a
|
||||
* basic default value but you may easily change it to any table you like.
|
||||
*/
|
||||
|
||||
'model_has_permissions' => 'model_has_permissions',
|
||||
|
||||
/*
|
||||
* When using the "HasRoles" trait from this package, we need to know which
|
||||
* table should be used to retrieve your models roles. We have chosen a
|
||||
* basic default value but you may easily change it to any table you like.
|
||||
*/
|
||||
|
||||
'model_has_roles' => 'model_has_roles',
|
||||
|
||||
/*
|
||||
* When using the "HasRoles" trait from this package, we need to know which
|
||||
* table should be used to retrieve your roles permissions. We have chosen a
|
||||
* basic default value but you may easily change it to any table you like.
|
||||
*/
|
||||
|
||||
'role_has_permissions' => 'role_has_permissions',
|
||||
],
|
||||
|
||||
'column_names' => [
|
||||
/*
|
||||
* Change this if you want to name the related pivots other than defaults
|
||||
*/
|
||||
'role_pivot_key' => null, // default 'role_id',
|
||||
'permission_pivot_key' => null, // default 'permission_id',
|
||||
|
||||
/*
|
||||
* Change this if you want to name the related model primary key other than
|
||||
* `model_id`.
|
||||
*
|
||||
* For example, this would be nice if your primary keys are all UUIDs. In
|
||||
* that case, name this `model_uuid`.
|
||||
*/
|
||||
|
||||
'model_morph_key' => 'model_id',
|
||||
|
||||
/*
|
||||
* Change this if you want to use the teams feature and your related model's
|
||||
* foreign key is other than `team_id`.
|
||||
*/
|
||||
|
||||
'team_foreign_key' => 'team_id',
|
||||
],
|
||||
|
||||
/*
|
||||
* When set to true, the method for checking permissions will be registered on the gate.
|
||||
* Set this to false if you want to implement custom logic for checking permissions.
|
||||
*/
|
||||
|
||||
'register_permission_check_method' => true,
|
||||
|
||||
/*
|
||||
* When set to true, Laravel\Octane\Events\OperationTerminated event listener will be registered
|
||||
* this will refresh permissions on every TickTerminated, TaskTerminated and RequestTerminated
|
||||
* NOTE: This should not be needed in most cases, but an Octane/Vapor combination benefited from it.
|
||||
*/
|
||||
'register_octane_reset_listener' => false,
|
||||
|
||||
/*
|
||||
* Events will fire when a role or permission is assigned/unassigned:
|
||||
* \Spatie\Permission\Events\RoleAttachedEvent
|
||||
* \Spatie\Permission\Events\RoleDetachedEvent
|
||||
* \Spatie\Permission\Events\PermissionAttachedEvent
|
||||
* \Spatie\Permission\Events\PermissionDetachedEvent
|
||||
*
|
||||
* To enable, set to true, and then create listeners to watch these events.
|
||||
*/
|
||||
'events_enabled' => false,
|
||||
|
||||
/*
|
||||
* Teams Feature.
|
||||
* When set to true the package implements teams using the 'team_foreign_key'.
|
||||
* If you want the migrations to register the 'team_foreign_key', you must
|
||||
* set this to true before doing the migration.
|
||||
* If you already did the migration then you must make a new migration to also
|
||||
* add 'team_foreign_key' to 'roles', 'model_has_roles', and 'model_has_permissions'
|
||||
* (view the latest version of this package's migration file)
|
||||
*/
|
||||
|
||||
'teams' => false,
|
||||
|
||||
/*
|
||||
* The class to use to resolve the permissions team id
|
||||
*/
|
||||
'team_resolver' => DefaultTeamResolver::class,
|
||||
|
||||
/*
|
||||
* Passport Client Credentials Grant
|
||||
* When set to true the package will use Passports Client to check permissions
|
||||
*/
|
||||
|
||||
'use_passport_client_credentials' => false,
|
||||
|
||||
/*
|
||||
* When set to true, the required permission names are added to exception messages.
|
||||
* This could be considered an information leak in some contexts, so the default
|
||||
* setting is false here for optimum safety.
|
||||
*/
|
||||
|
||||
'display_permission_in_exception' => false,
|
||||
|
||||
/*
|
||||
* When set to true, the required role names are added to exception messages.
|
||||
* This could be considered an information leak in some contexts, so the default
|
||||
* setting is false here for optimum safety.
|
||||
*/
|
||||
|
||||
'display_role_in_exception' => false,
|
||||
|
||||
/*
|
||||
* By default wildcard permission lookups are disabled.
|
||||
* See documentation to understand supported syntax.
|
||||
*/
|
||||
|
||||
'enable_wildcard_permission' => false,
|
||||
|
||||
/*
|
||||
* The class to use for interpreting wildcard permissions.
|
||||
* If you need to modify delimiters, override the class and specify its name here.
|
||||
*/
|
||||
// 'wildcard_permission' => Spatie\Permission\WildcardPermission::class,
|
||||
|
||||
/* Cache-specific settings */
|
||||
|
||||
'cache' => [
|
||||
|
||||
/*
|
||||
* By default all permissions are cached for 24 hours to speed up performance.
|
||||
* When permissions or roles are updated the cache is flushed automatically.
|
||||
*/
|
||||
|
||||
'expiration_time' => DateInterval::createFromDateString('24 hours'),
|
||||
|
||||
/*
|
||||
* The cache key used to store all permissions.
|
||||
*/
|
||||
|
||||
'key' => 'spatie.permission.cache',
|
||||
|
||||
/*
|
||||
* You may optionally indicate a specific cache driver to use for permission and
|
||||
* role caching using any of the `store` drivers listed in the cache.php config
|
||||
* file. Using 'default' here means to use the `default` set in cache.php.
|
||||
*/
|
||||
|
||||
'store' => 'default',
|
||||
],
|
||||
];
|
||||
@ -15,6 +15,7 @@ class DatabaseSeeder extends Seeder
|
||||
public function run(): void
|
||||
{
|
||||
$this->call([
|
||||
RolePermissionSeeder::class,
|
||||
UserSeeder::class,
|
||||
CategorySeeder::class,
|
||||
SupplierSeeder::class,
|
||||
|
||||
117
database/seeders/RolePermissionSeeder.php
Normal file
117
database/seeders/RolePermissionSeeder.php
Normal file
@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
class RolePermissionSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions();
|
||||
|
||||
$permissions = [
|
||||
'user' => ['view', 'create', 'update', 'delete', 'toggle-active', 'reset-password'],
|
||||
'category' => ['view', 'create', 'update', 'delete'],
|
||||
'supplier' => ['view', 'create', 'update', 'delete'],
|
||||
'customer' => ['view', 'create', 'update', 'delete'],
|
||||
'cash-account' => ['view', 'deposit', 'withdrawal', 'update', 'delete'],
|
||||
'expense' => ['view', 'create', 'update', 'delete'],
|
||||
'employee-advance' => ['view', 'create', 'update', 'delete', 'approve', 'pay'],
|
||||
'payroll-period' => ['view', 'show', 'current', 'close', 'reopen'],
|
||||
'payroll' => ['pay', 'cancel'],
|
||||
'payroll-adjustment' => ['create', 'delete'],
|
||||
'leave-request' => ['view', 'create', 'update', 'delete', 'approve', 'reject'],
|
||||
'attendance' => ['view', 'check-in', 'check-out', 'by-date'],
|
||||
'settings' => ['view', 'update-system', 'update-homepage', 'update-social-media', 'update-marketplace', 'update-hr'],
|
||||
];
|
||||
|
||||
foreach ($permissions as $module => $actions) {
|
||||
foreach ($actions as $action) {
|
||||
Permission::firstOrCreate(['name' => "{$module}.{$action}"]);
|
||||
}
|
||||
}
|
||||
|
||||
$allPermissions = Permission::all()->pluck('name')->toArray();
|
||||
|
||||
$rolePermissions = [
|
||||
'Developer' => $allPermissions,
|
||||
|
||||
'Owner' => array_filter($allPermissions, fn($p) => !str_starts_with($p, 'settings.update-')),
|
||||
|
||||
'Direktur' => array_filter($allPermissions, function ($p) {
|
||||
return str_ends_with($p, '.view')
|
||||
|| str_starts_with($p, 'payroll-period.')
|
||||
|| $p === 'payroll.pay'
|
||||
|| $p === 'leave-request.approve'
|
||||
|| $p === 'leave-request.reject'
|
||||
|| $p === 'attendance.by-date';
|
||||
}),
|
||||
|
||||
'Admin Bahan Baku' => array_filter($allPermissions, function ($p) {
|
||||
return str_starts_with($p, 'supplier.')
|
||||
|| $p === 'category.view'
|
||||
|| $p === 'customer.view'
|
||||
|| $p === 'cash-account.view';
|
||||
}),
|
||||
|
||||
'Admin Toko' => array_filter($allPermissions, function ($p) {
|
||||
return str_starts_with($p, 'category.')
|
||||
|| str_starts_with($p, 'customer.')
|
||||
|| str_starts_with($p, 'expense.')
|
||||
|| $p === 'cash-account.view'
|
||||
|| $p === 'leave-request.view'
|
||||
|| $p === 'leave-request.create'
|
||||
|| str_starts_with($p, 'attendance.');
|
||||
}),
|
||||
|
||||
'Marketing Offline' => array_filter($allPermissions, function ($p) {
|
||||
return str_starts_with($p, 'customer.')
|
||||
|| $p === 'category.view'
|
||||
|| $p === 'supplier.view'
|
||||
|| $p === 'leave-request.view'
|
||||
|| $p === 'leave-request.create'
|
||||
|| $p === 'attendance.check-in'
|
||||
|| $p === 'attendance.check-out';
|
||||
}),
|
||||
|
||||
'Marketing Online' => array_filter($allPermissions, function ($p) {
|
||||
return str_starts_with($p, 'customer.')
|
||||
|| $p === 'category.view'
|
||||
|| $p === 'supplier.view'
|
||||
|| $p === 'leave-request.view'
|
||||
|| $p === 'leave-request.create'
|
||||
|| $p === 'attendance.check-in'
|
||||
|| $p === 'attendance.check-out';
|
||||
}),
|
||||
|
||||
'Kasir' => array_filter($allPermissions, function ($p) {
|
||||
return str_starts_with($p, 'cash-account.')
|
||||
|| $p === 'expense.view'
|
||||
|| $p === 'leave-request.view'
|
||||
|| $p === 'leave-request.create'
|
||||
|| $p === 'attendance.check-in'
|
||||
|| $p === 'attendance.check-out';
|
||||
}),
|
||||
|
||||
'Stok Opname' => array_filter($allPermissions, function ($p) {
|
||||
return str_starts_with($p, 'supplier.')
|
||||
|| $p === 'category.view'
|
||||
|| $p === 'cash-account.view'
|
||||
|| $p === 'leave-request.view'
|
||||
|| $p === 'leave-request.create'
|
||||
|| $p === 'attendance.check-in'
|
||||
|| $p === 'attendance.check-out';
|
||||
}),
|
||||
|
||||
'Non Operator' => array_filter($allPermissions, fn($p) => str_ends_with($p, '.view')),
|
||||
];
|
||||
|
||||
foreach ($rolePermissions as $roleName => $perms) {
|
||||
$role = Role::firstOrCreate(['name' => $roleName]);
|
||||
$role->syncPermissions($perms);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -26,6 +26,8 @@ public function run(): void
|
||||
'gender' => Gender::MALE->value,
|
||||
]);
|
||||
|
||||
$developer->assignRole('Developer');
|
||||
|
||||
$owner = User::factory()->create([
|
||||
'email' => 'owner@gmail.com',
|
||||
'username' => 'owner',
|
||||
@ -37,6 +39,8 @@ public function run(): void
|
||||
'full_name' => 'Owner',
|
||||
'gender' => Gender::MALE->value,
|
||||
]);
|
||||
|
||||
$owner->assignRole('Owner');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,6 +21,7 @@ import { index as leaveRequestsIndex } from '@/routes/admin/hr/leave-requests';
|
||||
import { index as categoriesIndex } from '@/routes/admin/master/categories';
|
||||
import { index as customersIndex } from '@/routes/admin/master/customers';
|
||||
import { index as suppliersIndex } from '@/routes/admin/master/suppliers';
|
||||
import { index as rolesIndex } from '@/routes/admin/settings/roles';
|
||||
import { Link } from '@inertiajs/react';
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
import {
|
||||
@ -38,6 +39,7 @@ import {
|
||||
RefreshCw,
|
||||
Scissors,
|
||||
Settings,
|
||||
Shield,
|
||||
ShoppingCart,
|
||||
Tags,
|
||||
Truck,
|
||||
@ -90,6 +92,7 @@ const hrItems: NavMenuItem[] = [
|
||||
|
||||
const sistemItems: NavMenuItem[] = [
|
||||
{ title: 'Pengaturan', href: '/admin/settings', icon: Settings },
|
||||
{ title: 'Role & Permission', href: rolesIndex.url(), icon: Shield },
|
||||
{ title: 'Log Aktivitas', href: '#', icon: Activity },
|
||||
];
|
||||
|
||||
|
||||
124
resources/js/pages/admin/roles/columns.tsx
Normal file
124
resources/js/pages/admin/roles/columns.tsx
Normal file
@ -0,0 +1,124 @@
|
||||
import type { ColumnDef } from '@tanstack/react-table';
|
||||
import { ArrowUpDown, Pencil, Trash2 } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip';
|
||||
|
||||
export type Role = {
|
||||
id: number;
|
||||
name: string;
|
||||
permissions_count: number;
|
||||
};
|
||||
|
||||
type CreateColumnsParams = {
|
||||
handleEdit: (role: Role) => void;
|
||||
handleDeleteClick: (role: Role) => void;
|
||||
};
|
||||
|
||||
export function createRoleColumns(
|
||||
params: CreateColumnsParams,
|
||||
): ColumnDef<Role>[] {
|
||||
const { handleEdit, handleDeleteClick } = params;
|
||||
|
||||
return [
|
||||
{
|
||||
id: 'no',
|
||||
header: () => <span className="block text-center">No</span>,
|
||||
cell: ({ row }) => (
|
||||
<span className="block text-center">
|
||||
{row.index + 1}
|
||||
</span>
|
||||
),
|
||||
meta: {
|
||||
className: 'w-[50px] text-center',
|
||||
headerClassName: 'w-[50px] text-center',
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'name',
|
||||
header: ({ column }) => (
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="-ml-3 h-8"
|
||||
onClick={() =>
|
||||
column.toggleSorting(
|
||||
column.getIsSorted() === 'asc',
|
||||
)
|
||||
}
|
||||
>
|
||||
<span>Nama Role</span>
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<span className="font-medium">
|
||||
{row.getValue('name') as string}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'permissions_count',
|
||||
header: () => <span className="block text-center">Jumlah Permission</span>,
|
||||
meta: {
|
||||
className: 'w-[180px] text-center',
|
||||
headerClassName: 'w-[180px] text-center',
|
||||
},
|
||||
cell: ({ row }) => (
|
||||
<span className="inline-flex items-center rounded-md bg-muted px-2 py-1 text-xs font-medium">
|
||||
{row.getValue('permissions_count') as number} permission
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: () => <span className="block text-center">Aksi</span>,
|
||||
meta: {
|
||||
className: 'w-[100px] text-center',
|
||||
headerClassName: 'w-[100px] text-center',
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const role = row.original;
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleEdit(role)}
|
||||
>
|
||||
<Pencil className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">
|
||||
Edit
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleDeleteClick(role)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 text-destructive" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">
|
||||
Hapus
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
164
resources/js/pages/admin/roles/create.tsx
Normal file
164
resources/js/pages/admin/roles/create.tsx
Normal file
@ -0,0 +1,164 @@
|
||||
import InputError from '@/components/input-error';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { index as rolesIndex, store } from '@/routes/admin/settings/roles';
|
||||
import { Form, Head } from '@inertiajs/react';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
|
||||
type PermissionsByModule = Record<string, string[]>;
|
||||
|
||||
type Props = {
|
||||
permissions: PermissionsByModule;
|
||||
};
|
||||
|
||||
const moduleLabels: Record<string, string> = {
|
||||
'user': 'Pengguna',
|
||||
'category': 'Kategori',
|
||||
'supplier': 'Supplier',
|
||||
'customer': 'Customer',
|
||||
'cash-account': 'Kas Toko',
|
||||
'expense': 'Pengeluaran',
|
||||
'employee-advance': 'Kasbon',
|
||||
'payroll-period': 'Periode Gaji',
|
||||
'payroll': 'Gaji',
|
||||
'payroll-adjustment': 'Adjustment Gaji',
|
||||
'leave-request': 'Cuti',
|
||||
'attendance': 'Absensi',
|
||||
'settings': 'Pengaturan',
|
||||
};
|
||||
|
||||
const actionLabels: Record<string, string> = {
|
||||
'view': 'Lihat',
|
||||
'create': 'Tambah',
|
||||
'update': 'Edit',
|
||||
'delete': 'Hapus',
|
||||
'toggle-active': 'Aktif/Nonaktif',
|
||||
'reset-password': 'Reset Kata Sandi',
|
||||
'deposit': 'Setor',
|
||||
'withdrawal': 'Tarik',
|
||||
'approve': 'Setujui',
|
||||
'pay': 'Bayar',
|
||||
'reject': 'Tolak',
|
||||
'current': 'Periode Saat Ini',
|
||||
'close': 'Tutup',
|
||||
'reopen': 'Buka Kembali',
|
||||
'cancel': 'Batalkan',
|
||||
'check-in': 'Check In',
|
||||
'check-out': 'Check Out',
|
||||
'by-date': 'Lihat Per Tanggal',
|
||||
'show': 'Detail',
|
||||
'update-system': 'Update Sistem',
|
||||
'update-homepage': 'Update Homepage',
|
||||
'update-social-media': 'Update Media Sosial',
|
||||
'update-marketplace': 'Update Marketplace',
|
||||
'update-hr': 'Update HR',
|
||||
};
|
||||
|
||||
export default function RoleCreate({ permissions }: Props) {
|
||||
return (
|
||||
<>
|
||||
<Head title="Tambah Role" />
|
||||
|
||||
<div className="flex h-full flex-1 flex-col gap-6 overflow-x-auto p-4 md:p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold tracking-tight">
|
||||
Tambah Role
|
||||
</h2>
|
||||
</div>
|
||||
<Button asChild variant="outline">
|
||||
<a href={rolesIndex.url()}>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
Kembali
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Form action={store()}>
|
||||
{({ errors, processing }) => (
|
||||
<>
|
||||
<div className="grid gap-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Informasi Role</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="name">
|
||||
Nama Role <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="name"
|
||||
name="name"
|
||||
placeholder="Masukkan nama role"
|
||||
/>
|
||||
<InputError message={errors.name} />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Permission</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-6">
|
||||
{Object.entries(permissions).map(([module, actions]) => (
|
||||
<div key={module} className="grid gap-3">
|
||||
<Label className="text-sm font-semibold">
|
||||
{moduleLabels[module] ?? module}
|
||||
</Label>
|
||||
<div className="grid grid-cols-2 gap-2 md:grid-cols-3 lg:grid-cols-4">
|
||||
{actions.map((action) => (
|
||||
<label
|
||||
key={`${module}.${action}`}
|
||||
className="flex items-center gap-2 rounded-md border p-2 text-sm hover:bg-muted cursor-pointer"
|
||||
>
|
||||
<Checkbox
|
||||
name="permissions[]"
|
||||
value={`${module}.${action}`}
|
||||
/>
|
||||
<span className="text-xs">
|
||||
{actionLabels[action] ?? action}
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<InputError message={errors.permissions} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 mt-6">
|
||||
<Button type="submit" disabled={processing}>
|
||||
{processing ? 'Menyimpan...' : 'Simpan'}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
RoleCreate.layout = {
|
||||
breadcrumbs: [
|
||||
{
|
||||
title: 'Pengaturan',
|
||||
href: '/admin/settings',
|
||||
},
|
||||
{
|
||||
title: 'Role & Permission',
|
||||
href: rolesIndex.url(),
|
||||
},
|
||||
{
|
||||
title: 'Tambah',
|
||||
href: '#',
|
||||
},
|
||||
],
|
||||
};
|
||||
178
resources/js/pages/admin/roles/edit.tsx
Normal file
178
resources/js/pages/admin/roles/edit.tsx
Normal file
@ -0,0 +1,178 @@
|
||||
import InputError from '@/components/input-error';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { index as rolesIndex, update } from '@/routes/admin/settings/roles';
|
||||
import { Form, Head } from '@inertiajs/react';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
|
||||
type PermissionsByModule = Record<string, string[]>;
|
||||
|
||||
type RoleData = {
|
||||
id: number;
|
||||
name: string;
|
||||
permissions: {
|
||||
id: number;
|
||||
name: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
type Props = {
|
||||
role: RoleData;
|
||||
permissions: PermissionsByModule;
|
||||
};
|
||||
|
||||
const moduleLabels: Record<string, string> = {
|
||||
'user': 'Pengguna',
|
||||
'category': 'Kategori',
|
||||
'supplier': 'Supplier',
|
||||
'customer': 'Customer',
|
||||
'cash-account': 'Kas Toko',
|
||||
'expense': 'Pengeluaran',
|
||||
'employee-advance': 'Kasbon',
|
||||
'payroll-period': 'Periode Gaji',
|
||||
'payroll': 'Gaji',
|
||||
'payroll-adjustment': 'Adjustment Gaji',
|
||||
'leave-request': 'Cuti',
|
||||
'attendance': 'Absensi',
|
||||
'settings': 'Pengaturan',
|
||||
};
|
||||
|
||||
const actionLabels: Record<string, string> = {
|
||||
'view': 'Lihat',
|
||||
'create': 'Tambah',
|
||||
'update': 'Edit',
|
||||
'delete': 'Hapus',
|
||||
'toggle-active': 'Aktif/Nonaktif',
|
||||
'reset-password': 'Reset Kata Sandi',
|
||||
'deposit': 'Setor',
|
||||
'withdrawal': 'Tarik',
|
||||
'approve': 'Setujui',
|
||||
'pay': 'Bayar',
|
||||
'reject': 'Tolak',
|
||||
'current': 'Periode Saat Ini',
|
||||
'close': 'Tutup',
|
||||
'reopen': 'Buka Kembali',
|
||||
'cancel': 'Batalkan',
|
||||
'check-in': 'Check In',
|
||||
'check-out': 'Check Out',
|
||||
'by-date': 'Lihat Per Tanggal',
|
||||
'show': 'Detail',
|
||||
'update-system': 'Update Sistem',
|
||||
'update-homepage': 'Update Homepage',
|
||||
'update-social-media': 'Update Media Sosial',
|
||||
'update-marketplace': 'Update Marketplace',
|
||||
'update-hr': 'Update HR',
|
||||
};
|
||||
|
||||
export default function RoleEdit({ role, permissions }: Props) {
|
||||
const assignedPermissions = role.permissions.map((p) => p.name);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head title="Edit Role" />
|
||||
|
||||
<div className="flex h-full flex-1 flex-col gap-6 overflow-x-auto p-4 md:p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold tracking-tight">
|
||||
Edit Role
|
||||
</h2>
|
||||
</div>
|
||||
<Button asChild variant="outline">
|
||||
<a href={rolesIndex.url()}>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
Kembali
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Form action={update(role.id)}>
|
||||
{({ errors, processing }) => (
|
||||
<>
|
||||
<div className="grid gap-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Informasi Role</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="name">
|
||||
Nama Role <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="name"
|
||||
name="name"
|
||||
placeholder="Masukkan nama role"
|
||||
defaultValue={role.name}
|
||||
/>
|
||||
<InputError message={errors.name} />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Permission</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-6">
|
||||
{Object.entries(permissions).map(([module, actions]) => (
|
||||
<div key={module} className="grid gap-3">
|
||||
<Label className="text-sm font-semibold">
|
||||
{moduleLabels[module] ?? module}
|
||||
</Label>
|
||||
<div className="grid grid-cols-2 gap-2 md:grid-cols-3 lg:grid-cols-4">
|
||||
{actions.map((action) => (
|
||||
<label
|
||||
key={`${module}.${action}`}
|
||||
className="flex items-center gap-2 rounded-md border p-2 text-sm hover:bg-muted cursor-pointer"
|
||||
>
|
||||
<Checkbox
|
||||
name="permissions[]"
|
||||
value={`${module}.${action}`}
|
||||
defaultChecked={assignedPermissions.includes(`${module}.${action}`)}
|
||||
/>
|
||||
<span className="text-xs">
|
||||
{actionLabels[action] ?? action}
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<InputError message={errors.permissions} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 mt-6">
|
||||
<Button type="submit" disabled={processing}>
|
||||
{processing ? 'Menyimpan...' : 'Simpan'}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
RoleEdit.layout = {
|
||||
breadcrumbs: [
|
||||
{
|
||||
title: 'Pengaturan',
|
||||
href: '/admin/settings',
|
||||
},
|
||||
{
|
||||
title: 'Role & Permission',
|
||||
href: rolesIndex.url(),
|
||||
},
|
||||
{
|
||||
title: 'Edit',
|
||||
href: '#',
|
||||
},
|
||||
],
|
||||
};
|
||||
89
resources/js/pages/admin/roles/index.tsx
Normal file
89
resources/js/pages/admin/roles/index.tsx
Normal file
@ -0,0 +1,89 @@
|
||||
import { ConfirmDialog } from '@/components/confirm-dialog';
|
||||
import { DataTable } from '@/components/data-table';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { index as rolesIndex, create as roleCreate, edit as roleEdit, destroy as roleDestroy } from '@/routes/admin/settings/roles';
|
||||
import { Head, router } from '@inertiajs/react';
|
||||
import { Plus } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { createRoleColumns, type Role } from './columns';
|
||||
|
||||
type Props = {
|
||||
roles: Role[];
|
||||
};
|
||||
|
||||
export default function RoleIndex({ roles }: Props) {
|
||||
const [deleting, setDeleting] = useState<Role | null>(null);
|
||||
|
||||
function handleDelete() {
|
||||
if (!deleting) {
|
||||
return;
|
||||
}
|
||||
|
||||
router.delete(roleDestroy.url(deleting.id), {
|
||||
onSuccess: () => setDeleting(null),
|
||||
});
|
||||
}
|
||||
|
||||
const columns = createRoleColumns({
|
||||
handleEdit: (role) => {
|
||||
window.location.href = roleEdit.url(role.id);
|
||||
},
|
||||
handleDeleteClick: (role) => setDeleting(role),
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head title="Role & Permission" />
|
||||
|
||||
<div className="flex h-full flex-1 flex-col gap-6 overflow-x-auto p-4 md:p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold tracking-tight">
|
||||
Role & Permission
|
||||
</h2>
|
||||
</div>
|
||||
<Button asChild>
|
||||
<a href={roleCreate.url()}>
|
||||
<Plus className="h-4 w-4" />
|
||||
Tambah
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={roles}
|
||||
searchKey="name"
|
||||
searchPlaceholder="Cari role..."
|
||||
emptyText="Belum ada data role."
|
||||
/>
|
||||
|
||||
<ConfirmDialog
|
||||
open={deleting !== null}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setDeleting(null);
|
||||
}
|
||||
}}
|
||||
title="Hapus Role"
|
||||
description={`Apakah Anda yakin ingin menghapus role "${deleting?.name}"? Semua user dengan role ini akan kehilangan permission terkait.`}
|
||||
confirmLabel="Hapus"
|
||||
onConfirm={handleDelete}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
RoleIndex.layout = {
|
||||
breadcrumbs: [
|
||||
{
|
||||
title: 'Pengaturan',
|
||||
href: '/admin/settings',
|
||||
},
|
||||
{
|
||||
title: 'Role & Permission',
|
||||
href: rolesIndex.url(),
|
||||
},
|
||||
],
|
||||
};
|
||||
@ -14,6 +14,7 @@
|
||||
use App\Http\Controllers\Admin\HR\AttendanceController;
|
||||
use App\Http\Controllers\Admin\HR\EmployeeController;
|
||||
use App\Http\Controllers\Admin\HR\LeaveRequestController;
|
||||
use App\Http\Controllers\Admin\RoleController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::get('/', fn () => inertia('welcome', [
|
||||
@ -29,58 +30,67 @@
|
||||
Route::inertia('dashboard', 'dashboard')->name('dashboard');
|
||||
|
||||
Route::prefix('admin/master')->name('admin.master.')->group(function () {
|
||||
Route::resource('categories', CategoryController::class)->except(['show', 'create', 'edit']);
|
||||
Route::resource('suppliers', SupplierController::class)->except(['show', 'create', 'edit']);
|
||||
Route::resource('customers', CustomerController::class)->except(['show', 'create', 'edit']);
|
||||
Route::resource('categories', CategoryController::class)->except(['show', 'create', 'edit'])->middleware('permission:category.view|category.create|category.update|category.delete');
|
||||
Route::resource('suppliers', SupplierController::class)->except(['show', 'create', 'edit'])->middleware('permission:supplier.view|supplier.create|supplier.update|supplier.delete');
|
||||
Route::resource('customers', CustomerController::class)->except(['show', 'create', 'edit'])->middleware('permission:customer.view|customer.create|customer.update|customer.delete');
|
||||
});
|
||||
|
||||
Route::prefix('admin/finance')->name('admin.finance.')->group(function () {
|
||||
Route::get('cash-accounts', [CashAccountController::class, 'index'])->name('cash-accounts.index');
|
||||
Route::post('cash-accounts/deposit', [CashAccountController::class, 'deposit'])->name('cash-accounts.deposit');
|
||||
Route::post('cash-accounts/withdrawal', [CashAccountController::class, 'withdrawal'])->name('cash-accounts.withdrawal');
|
||||
Route::put('cash-accounts/transactions/{transaction}', [CashAccountController::class, 'update'])->name('cash-accounts.transactions.update');
|
||||
Route::delete('cash-accounts/transactions/{transaction}', [CashAccountController::class, 'destroy'])->name('cash-accounts.transactions.destroy');
|
||||
Route::get('cash-accounts', [CashAccountController::class, 'index'])->name('cash-accounts.index')->middleware('permission:cash-account.view');
|
||||
Route::post('cash-accounts/deposit', [CashAccountController::class, 'deposit'])->name('cash-accounts.deposit')->middleware('permission:cash-account.deposit');
|
||||
Route::post('cash-accounts/withdrawal', [CashAccountController::class, 'withdrawal'])->name('cash-accounts.withdrawal')->middleware('permission:cash-account.withdrawal');
|
||||
Route::put('cash-accounts/transactions/{transaction}', [CashAccountController::class, 'update'])->name('cash-accounts.transactions.update')->middleware('permission:cash-account.update');
|
||||
Route::delete('cash-accounts/transactions/{transaction}', [CashAccountController::class, 'destroy'])->name('cash-accounts.transactions.destroy')->middleware('permission:cash-account.delete');
|
||||
|
||||
Route::resource('expenses', ExpenseController::class)->except(['show', 'create', 'edit']);
|
||||
Route::resource('expenses', ExpenseController::class)->except(['show', 'create', 'edit'])->middleware('permission:expense.view|expense.create|expense.update|expense.delete');
|
||||
|
||||
Route::resource('employee-advances', EmployeeAdvanceController::class)->except(['show', 'create', 'edit']);
|
||||
Route::post('employee-advances/{employeeAdvance}/approve', [EmployeeAdvanceController::class, 'approve'])->name('employee-advances.approve');
|
||||
Route::post('employee-advances/{employeeAdvance}/pay', [EmployeeAdvanceController::class, 'pay'])->name('employee-advances.pay');
|
||||
Route::resource('employee-advances', EmployeeAdvanceController::class)->except(['show', 'create', 'edit'])->middleware('permission:employee-advance.view|employee-advance.create|employee-advance.update|employee-advance.delete');
|
||||
Route::post('employee-advances/{employeeAdvance}/approve', [EmployeeAdvanceController::class, 'approve'])->name('employee-advances.approve')->middleware('permission:employee-advance.approve');
|
||||
Route::post('employee-advances/{employeeAdvance}/pay', [EmployeeAdvanceController::class, 'pay'])->name('employee-advances.pay')->middleware('permission:employee-advance.pay');
|
||||
|
||||
Route::get('payroll-periods/current', [PayrollPeriodController::class, 'current'])->name('payroll-periods.current');
|
||||
Route::resource('payroll-periods', PayrollPeriodController::class)->only(['index', 'show']);
|
||||
Route::post('payroll-periods/{payrollPeriod}/close', [PayrollPeriodController::class, 'close'])->name('payroll-periods.close');
|
||||
Route::post('payroll-periods/{payrollPeriod}/reopen', [PayrollPeriodController::class, 'reopen'])->name('payroll-periods.reopen');
|
||||
Route::get('payroll-periods/current', [PayrollPeriodController::class, 'current'])->name('payroll-periods.current')->middleware('permission:payroll-period.current');
|
||||
Route::resource('payroll-periods', PayrollPeriodController::class)->only(['index', 'show'])->middleware('permission:payroll-period.view');
|
||||
Route::post('payroll-periods/{payrollPeriod}/close', [PayrollPeriodController::class, 'close'])->name('payroll-periods.close')->middleware('permission:payroll-period.close');
|
||||
Route::post('payroll-periods/{payrollPeriod}/reopen', [PayrollPeriodController::class, 'reopen'])->name('payroll-periods.reopen')->middleware('permission:payroll-period.reopen');
|
||||
|
||||
Route::post('payrolls/{payroll}/pay', [PayrollController::class, 'pay'])->name('payrolls.pay');
|
||||
Route::post('payrolls/{payroll}/cancel', [PayrollController::class, 'cancel'])->name('payrolls.cancel');
|
||||
Route::post('payrolls/{payroll}/pay', [PayrollController::class, 'pay'])->name('payrolls.pay')->middleware('permission:payroll.pay');
|
||||
Route::post('payrolls/{payroll}/cancel', [PayrollController::class, 'cancel'])->name('payrolls.cancel')->middleware('permission:payroll.cancel');
|
||||
|
||||
Route::post('payrolls/{payroll}/adjustments', [PayrollAdjustmentController::class, 'store'])->name('payrolls.adjustments.store');
|
||||
Route::delete('payroll-adjustments/{payrollAdjustment}', [PayrollAdjustmentController::class, 'destroy'])->name('payroll-adjustments.destroy');
|
||||
Route::post('payrolls/{payroll}/adjustments', [PayrollAdjustmentController::class, 'store'])->name('payrolls.adjustments.store')->middleware('permission:payroll-adjustment.create');
|
||||
Route::delete('payroll-adjustments/{payrollAdjustment}', [PayrollAdjustmentController::class, 'destroy'])->name('payroll-adjustments.destroy')->middleware('permission:payroll-adjustment.delete');
|
||||
});
|
||||
|
||||
Route::prefix('admin/settings')->name('admin.settings.')->group(function () {
|
||||
Route::get('/', [AdminSettingsController::class, 'index'])->name('index');
|
||||
Route::put('system', [AdminSettingsController::class, 'updateSystem'])->name('update-system');
|
||||
Route::put('homepage', [AdminSettingsController::class, 'updateHomepage'])->name('update-homepage');
|
||||
Route::put('social-media', [AdminSettingsController::class, 'updateSocialMedia'])->name('update-social-media');
|
||||
Route::put('marketplace', [AdminSettingsController::class, 'updateMarketplace'])->name('update-marketplace');
|
||||
Route::put('hr', [AdminSettingsController::class, 'updateHR'])->name('update-hr');
|
||||
Route::get('/', [AdminSettingsController::class, 'index'])->name('index')->middleware('permission:settings.view');
|
||||
Route::put('system', [AdminSettingsController::class, 'updateSystem'])->name('update-system')->middleware('permission:settings.update-system');
|
||||
Route::put('homepage', [AdminSettingsController::class, 'updateHomepage'])->name('update-homepage')->middleware('permission:settings.update-homepage');
|
||||
Route::put('social-media', [AdminSettingsController::class, 'updateSocialMedia'])->name('update-social-media')->middleware('permission:settings.update-social-media');
|
||||
Route::put('marketplace', [AdminSettingsController::class, 'updateMarketplace'])->name('update-marketplace')->middleware('permission:settings.update-marketplace');
|
||||
Route::put('hr', [AdminSettingsController::class, 'updateHR'])->name('update-hr')->middleware('permission:settings.update-hr');
|
||||
});
|
||||
|
||||
Route::prefix('admin/settings')->name('admin.settings.')->group(function () {
|
||||
Route::get('roles', [RoleController::class, 'index'])->name('roles.index')->middleware('permission:user.view');
|
||||
Route::get('roles/create', [RoleController::class, 'create'])->name('roles.create')->middleware('permission:user.create');
|
||||
Route::post('roles', [RoleController::class, 'store'])->name('roles.store')->middleware('permission:user.create');
|
||||
Route::get('roles/{role}/edit', [RoleController::class, 'edit'])->name('roles.edit')->middleware('permission:user.update');
|
||||
Route::put('roles/{role}', [RoleController::class, 'update'])->name('roles.update')->middleware('permission:user.update');
|
||||
Route::delete('roles/{role}', [RoleController::class, 'destroy'])->name('roles.destroy')->middleware('permission:user.delete');
|
||||
});
|
||||
|
||||
Route::prefix('admin/hr')->name('admin.hr.')->group(function () {
|
||||
Route::resource('employees', EmployeeController::class)->except(['show'])->parameters(['employees' => 'user']);
|
||||
Route::post('employees/{user}/toggle-active', [EmployeeController::class, 'toggleActive'])->name('employees.toggle-active');
|
||||
Route::post('employees/{user}/reset-password', [EmployeeController::class, 'resetPassword'])->name('employees.reset-password');
|
||||
Route::resource('employees', EmployeeController::class)->except(['show'])->parameters(['employees' => 'user'])->middleware('permission:user.view|user.create|user.update|user.delete');
|
||||
Route::post('employees/{user}/toggle-active', [EmployeeController::class, 'toggleActive'])->name('employees.toggle-active')->middleware('permission:user.toggle-active');
|
||||
Route::post('employees/{user}/reset-password', [EmployeeController::class, 'resetPassword'])->name('employees.reset-password')->middleware('permission:user.reset-password');
|
||||
|
||||
Route::resource('leave-requests', LeaveRequestController::class)->except(['show', 'create', 'edit']);
|
||||
Route::post('leave-requests/{leaveRequest}/approve', [LeaveRequestController::class, 'approve'])->name('leave-requests.approve');
|
||||
Route::post('leave-requests/{leaveRequest}/reject', [LeaveRequestController::class, 'reject'])->name('leave-requests.reject');
|
||||
Route::resource('leave-requests', LeaveRequestController::class)->except(['show', 'create', 'edit'])->middleware('permission:leave-request.view|leave-request.create|leave-request.update|leave-request.delete');
|
||||
Route::post('leave-requests/{leaveRequest}/approve', [LeaveRequestController::class, 'approve'])->name('leave-requests.approve')->middleware('permission:leave-request.approve');
|
||||
Route::post('leave-requests/{leaveRequest}/reject', [LeaveRequestController::class, 'reject'])->name('leave-requests.reject')->middleware('permission:leave-request.reject');
|
||||
|
||||
Route::get('attendances', [AttendanceController::class, 'index'])->name('attendances.index');
|
||||
Route::post('attendances', [AttendanceController::class, 'store'])->name('attendances.store');
|
||||
Route::put('attendances/{attendance}', [AttendanceController::class, 'update'])->name('attendances.update');
|
||||
Route::get('attendances/by-date', [AttendanceController::class, 'byDate'])->name('attendances.by-date');
|
||||
Route::get('attendances', [AttendanceController::class, 'index'])->name('attendances.index')->middleware('permission:attendance.view');
|
||||
Route::post('attendances', [AttendanceController::class, 'store'])->name('attendances.store')->middleware('permission:attendance.check-in');
|
||||
Route::put('attendances/{attendance}', [AttendanceController::class, 'update'])->name('attendances.update')->middleware('permission:attendance.check-out');
|
||||
Route::get('attendances/by-date', [AttendanceController::class, 'byDate'])->name('attendances.by-date')->middleware('permission:attendance.by-date');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
768
tests/Feature/Admin/Settings/RoleTest.php
Normal file
768
tests/Feature/Admin/Settings/RoleTest.php
Normal file
@ -0,0 +1,768 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Inertia\Testing\AssertableInertia as Assert;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions();
|
||||
|
||||
$modules = [
|
||||
'user' => ['view', 'create', 'update', 'delete', 'toggle-active', 'reset-password'],
|
||||
'category' => ['view', 'create', 'update', 'delete'],
|
||||
'supplier' => ['view', 'create', 'update', 'delete'],
|
||||
'customer' => ['view', 'create', 'update', 'delete'],
|
||||
'cash-account' => ['view', 'deposit', 'withdrawal', 'update', 'delete'],
|
||||
'expense' => ['view', 'create', 'update', 'delete'],
|
||||
'employee-advance' => ['view', 'create', 'update', 'delete', 'approve', 'pay'],
|
||||
'payroll-period' => ['view', 'show', 'current', 'close', 'reopen'],
|
||||
'payroll' => ['pay', 'cancel'],
|
||||
'payroll-adjustment' => ['create', 'delete'],
|
||||
'leave-request' => ['view', 'create', 'update', 'delete', 'approve', 'reject'],
|
||||
'attendance' => ['view', 'check-in', 'check-out', 'by-date'],
|
||||
'settings' => ['view', 'update-system', 'update-homepage', 'update-social-media', 'update-marketplace', 'update-hr'],
|
||||
];
|
||||
|
||||
foreach ($modules as $module => $actions) {
|
||||
foreach ($actions as $action) {
|
||||
Permission::create(['name' => "{$module}.{$action}"]);
|
||||
}
|
||||
}
|
||||
|
||||
$allPermissions = Permission::all()->pluck('name')->toArray();
|
||||
|
||||
$developer = Role::create(['name' => 'Developer']);
|
||||
$developer->syncPermissions($allPermissions);
|
||||
});
|
||||
|
||||
function createUserWithRole(string $roleName = 'Developer'): User
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$user->assignRole($roleName);
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| AUTHENTICATION
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
test('guests are redirected to the login page', function () {
|
||||
$response = $this->get(route('admin.settings.roles.index'));
|
||||
$response->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('authenticated users without permission cannot visit the role index page', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$response = $this->get(route('admin.settings.roles.index'));
|
||||
$response->assertForbidden();
|
||||
});
|
||||
|
||||
test('authenticated users with permission can visit the role index page', function () {
|
||||
$this->actingAs(createUserWithRole());
|
||||
|
||||
$response = $this->get(route('admin.settings.roles.index'));
|
||||
$response->assertOk();
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| INDEX PAGE
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
test('role index page displays roles', function () {
|
||||
$this->actingAs(createUserWithRole());
|
||||
|
||||
Role::create(['name' => 'Manager']);
|
||||
Role::create(['name' => 'Staff']);
|
||||
|
||||
$response = $this->get(route('admin.settings.roles.index'));
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn (Assert $page) => $page
|
||||
->component('admin/roles/index')
|
||||
->has('roles', 3)
|
||||
);
|
||||
});
|
||||
|
||||
test('index page works with zero additional roles', function () {
|
||||
$this->actingAs(createUserWithRole());
|
||||
|
||||
$response = $this->get(route('admin.settings.roles.index'));
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn (Assert $page) => $page
|
||||
->component('admin/roles/index')
|
||||
->has('roles', 1)
|
||||
);
|
||||
});
|
||||
|
||||
test('index page displays correct role count after delete', function () {
|
||||
$this->actingAs(createUserWithRole());
|
||||
|
||||
$role = Role::create(['name' => 'To Delete']);
|
||||
|
||||
$this->delete(route('admin.settings.roles.destroy', $role));
|
||||
|
||||
$response = $this->get(route('admin.settings.roles.index'));
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn (Assert $page) => $page
|
||||
->component('admin/roles/index')
|
||||
->has('roles', 1)
|
||||
);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| CREATE / STORE
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
test('authenticated users without permission cannot visit the create page', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$response = $this->get(route('admin.settings.roles.create'));
|
||||
$response->assertForbidden();
|
||||
});
|
||||
|
||||
test('authenticated users with permission can visit the create page', function () {
|
||||
$this->actingAs(createUserWithRole());
|
||||
|
||||
$response = $this->get(route('admin.settings.roles.create'));
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn (Assert $page) => $page
|
||||
->component('admin/roles/create')
|
||||
->has('permissions')
|
||||
);
|
||||
});
|
||||
|
||||
test('role can be created', function () {
|
||||
$this->actingAs(createUserWithRole());
|
||||
|
||||
$response = $this->post(route('admin.settings.roles.store'), [
|
||||
'name' => 'Manager',
|
||||
'permissions' => ['user.view', 'user.create'],
|
||||
]);
|
||||
|
||||
$response
|
||||
->assertSessionHasNoErrors()
|
||||
->assertRedirect(route('admin.settings.roles.index'));
|
||||
|
||||
$this->assertDatabaseHas('roles', ['name' => 'Manager']);
|
||||
});
|
||||
|
||||
test('role name is required', function () {
|
||||
$this->actingAs(createUserWithRole());
|
||||
|
||||
$response = $this->post(route('admin.settings.roles.store'), [
|
||||
'name' => '',
|
||||
'permissions' => ['user.view'],
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('name');
|
||||
});
|
||||
|
||||
test('role name must be string', function () {
|
||||
$this->actingAs(createUserWithRole());
|
||||
|
||||
$response = $this->post(route('admin.settings.roles.store'), [
|
||||
'name' => 12345,
|
||||
'permissions' => ['user.view'],
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('name');
|
||||
});
|
||||
|
||||
test('role name must not exceed 100 characters', function () {
|
||||
$this->actingAs(createUserWithRole());
|
||||
|
||||
$response = $this->post(route('admin.settings.roles.store'), [
|
||||
'name' => str_repeat('a', 101),
|
||||
'permissions' => ['user.view'],
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('name');
|
||||
});
|
||||
|
||||
test('role name exactly 100 characters passes validation', function () {
|
||||
$this->actingAs(createUserWithRole());
|
||||
|
||||
$response = $this->post(route('admin.settings.roles.store'), [
|
||||
'name' => str_repeat('a', 100),
|
||||
'permissions' => ['user.view'],
|
||||
]);
|
||||
|
||||
$response->assertSessionHasNoErrors();
|
||||
});
|
||||
|
||||
test('role name must be unique', function () {
|
||||
$this->actingAs(createUserWithRole());
|
||||
|
||||
Role::create(['name' => 'Existing Role']);
|
||||
|
||||
$response = $this->post(route('admin.settings.roles.store'), [
|
||||
'name' => 'Existing Role',
|
||||
'permissions' => ['user.view'],
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('name');
|
||||
});
|
||||
|
||||
test('role name uniqueness is case sensitive', function () {
|
||||
$this->actingAs(createUserWithRole());
|
||||
|
||||
Role::create(['name' => 'Manager']);
|
||||
|
||||
$response = $this->post(route('admin.settings.roles.store'), [
|
||||
'name' => 'Manager',
|
||||
'permissions' => ['user.view'],
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('name');
|
||||
});
|
||||
|
||||
test('role name with special characters is accepted', function () {
|
||||
$this->actingAs(createUserWithRole());
|
||||
|
||||
$response = $this->post(route('admin.settings.roles.store'), [
|
||||
'name' => 'Admin & Staff!',
|
||||
'permissions' => ['user.view'],
|
||||
]);
|
||||
|
||||
$response->assertSessionHasNoErrors();
|
||||
$this->assertDatabaseHas('roles', ['name' => 'Admin & Staff!']);
|
||||
});
|
||||
|
||||
test('permissions are required', function () {
|
||||
$this->actingAs(createUserWithRole());
|
||||
|
||||
$response = $this->post(route('admin.settings.roles.store'), [
|
||||
'name' => 'New Role',
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('permissions');
|
||||
});
|
||||
|
||||
test('permissions must be an array', function () {
|
||||
$this->actingAs(createUserWithRole());
|
||||
|
||||
$response = $this->post(route('admin.settings.roles.store'), [
|
||||
'name' => 'New Role',
|
||||
'permissions' => 'user.view',
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('permissions');
|
||||
});
|
||||
|
||||
test('permissions must exist in database', function () {
|
||||
$this->actingAs(createUserWithRole());
|
||||
|
||||
$response = $this->post(route('admin.settings.roles.store'), [
|
||||
'name' => 'New Role',
|
||||
'permissions' => ['nonexistent.permission'],
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('permissions.0');
|
||||
});
|
||||
|
||||
test('role can be created with multiple permissions', function () {
|
||||
$this->actingAs(createUserWithRole());
|
||||
|
||||
$response = $this->post(route('admin.settings.roles.store'), [
|
||||
'name' => 'Multi Permission Role',
|
||||
'permissions' => ['user.view', 'user.create', 'category.view'],
|
||||
]);
|
||||
|
||||
$response->assertSessionHasNoErrors();
|
||||
|
||||
$role = Role::where('name', 'Multi Permission Role')->first();
|
||||
expect($role->permissions->pluck('name')->sort()->values()->toArray())->toEqual(['category.view', 'user.create', 'user.view']);
|
||||
});
|
||||
|
||||
test('role can be created with empty permissions array', function () {
|
||||
$this->actingAs(createUserWithRole());
|
||||
|
||||
$response = $this->post(route('admin.settings.roles.store'), [
|
||||
'name' => 'No Permission Role',
|
||||
'permissions' => [],
|
||||
]);
|
||||
|
||||
$response->assertSessionHasNoErrors();
|
||||
|
||||
$role = Role::where('name', 'No Permission Role')->first();
|
||||
expect($role->permissions->count())->toBe(0);
|
||||
});
|
||||
|
||||
test('store flashes success toast via Inertia', function () {
|
||||
$this->actingAs(createUserWithRole());
|
||||
|
||||
$response = $this->post(route('admin.settings.roles.store'), [
|
||||
'name' => 'Toast Role',
|
||||
'permissions' => ['user.view'],
|
||||
]);
|
||||
|
||||
$response->assertRedirect();
|
||||
});
|
||||
|
||||
test('store creates new role in database', function () {
|
||||
$this->actingAs(createUserWithRole());
|
||||
|
||||
$this->post(route('admin.settings.roles.store'), [
|
||||
'name' => 'Database Check',
|
||||
'permissions' => ['user.view'],
|
||||
]);
|
||||
|
||||
$this->assertDatabaseCount('roles', 2);
|
||||
$this->assertDatabaseHas('roles', ['name' => 'Database Check']);
|
||||
});
|
||||
|
||||
test('multiple roles can be created sequentially', function () {
|
||||
$this->actingAs(createUserWithRole());
|
||||
|
||||
$this->post(route('admin.settings.roles.store'), ['name' => 'Role 1', 'permissions' => ['user.view']]);
|
||||
$this->post(route('admin.settings.roles.store'), ['name' => 'Role 2', 'permissions' => ['user.view']]);
|
||||
$this->post(route('admin.settings.roles.store'), ['name' => 'Role 3', 'permissions' => ['user.view']]);
|
||||
|
||||
$this->assertDatabaseCount('roles', 4);
|
||||
$this->assertDatabaseHas('roles', ['name' => 'Role 1']);
|
||||
$this->assertDatabaseHas('roles', ['name' => 'Role 2']);
|
||||
$this->assertDatabaseHas('roles', ['name' => 'Role 3']);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| EDIT / UPDATE
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
test('authenticated users without permission cannot visit the edit page', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$role = Role::create(['name' => 'To Edit']);
|
||||
|
||||
$response = $this->get(route('admin.settings.roles.edit', $role));
|
||||
$response->assertForbidden();
|
||||
});
|
||||
|
||||
test('authenticated users with permission can visit the edit page', function () {
|
||||
$this->actingAs(createUserWithRole());
|
||||
|
||||
$role = Role::create(['name' => 'To Edit']);
|
||||
|
||||
$response = $this->get(route('admin.settings.roles.edit', $role));
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn (Assert $page) => $page
|
||||
->component('admin/roles/edit')
|
||||
->has('role')
|
||||
->has('permissions')
|
||||
);
|
||||
});
|
||||
|
||||
test('role can be updated', function () {
|
||||
$this->actingAs(createUserWithRole());
|
||||
|
||||
$role = Role::create(['name' => 'Old Name']);
|
||||
$role->syncPermissions(['user.view']);
|
||||
|
||||
$response = $this->put(route('admin.settings.roles.update', $role), [
|
||||
'name' => 'New Name',
|
||||
'permissions' => ['user.view', 'user.create'],
|
||||
]);
|
||||
|
||||
$response
|
||||
->assertSessionHasNoErrors()
|
||||
->assertRedirect(route('admin.settings.roles.index'));
|
||||
|
||||
$role->refresh();
|
||||
expect($role->name)->toBe('New Name');
|
||||
expect($role->permissions->pluck('name')->sort()->values()->toArray())->toEqual(['user.create', 'user.view']);
|
||||
});
|
||||
|
||||
test('role name can be updated to itself', function () {
|
||||
$this->actingAs(createUserWithRole());
|
||||
|
||||
$role = Role::create(['name' => 'Same Name']);
|
||||
|
||||
$response = $this->put(route('admin.settings.roles.update', $role), [
|
||||
'name' => 'Same Name',
|
||||
'permissions' => ['user.view'],
|
||||
]);
|
||||
|
||||
$response->assertSessionHasNoErrors();
|
||||
});
|
||||
|
||||
test('role update name must be unique excluding itself', function () {
|
||||
$this->actingAs(createUserWithRole());
|
||||
|
||||
$role = Role::create(['name' => 'First']);
|
||||
Role::create(['name' => 'Second']);
|
||||
|
||||
$response = $this->put(route('admin.settings.roles.update', $role), [
|
||||
'name' => 'Second',
|
||||
'permissions' => ['user.view'],
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('name');
|
||||
});
|
||||
|
||||
test('role update name is required', function () {
|
||||
$this->actingAs(createUserWithRole());
|
||||
|
||||
$role = Role::create(['name' => 'Existing']);
|
||||
|
||||
$response = $this->put(route('admin.settings.roles.update', $role), [
|
||||
'name' => '',
|
||||
'permissions' => ['user.view'],
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('name');
|
||||
});
|
||||
|
||||
test('role update name must not exceed 100 characters', function () {
|
||||
$this->actingAs(createUserWithRole());
|
||||
|
||||
$role = Role::create(['name' => 'Existing']);
|
||||
|
||||
$response = $this->put(route('admin.settings.roles.update', $role), [
|
||||
'name' => str_repeat('a', 101),
|
||||
'permissions' => ['user.view'],
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('name');
|
||||
});
|
||||
|
||||
test('role permissions can be updated', function () {
|
||||
$this->actingAs(createUserWithRole());
|
||||
|
||||
$role = Role::create(['name' => 'Update Perm']);
|
||||
$role->syncPermissions(['user.view']);
|
||||
|
||||
$this->put(route('admin.settings.roles.update', $role), [
|
||||
'name' => 'Update Perm',
|
||||
'permissions' => ['category.view', 'category.create'],
|
||||
]);
|
||||
|
||||
$role->refresh();
|
||||
expect($role->permissions->pluck('name')->sort()->values()->toArray())->toEqual(['category.create', 'category.view']);
|
||||
});
|
||||
|
||||
test('role can be updated multiple times', function () {
|
||||
$this->actingAs(createUserWithRole());
|
||||
|
||||
$role = Role::create(['name' => 'Version 1']);
|
||||
|
||||
$this->put(route('admin.settings.roles.update', $role), ['name' => 'Version 2', 'permissions' => ['user.view']]);
|
||||
$role->refresh();
|
||||
expect($role->name)->toBe('Version 2');
|
||||
|
||||
$this->put(route('admin.settings.roles.update', $role), ['name' => 'Version 3', 'permissions' => ['user.view']]);
|
||||
$role->refresh();
|
||||
expect($role->name)->toBe('Version 3');
|
||||
});
|
||||
|
||||
test('update flashes success toast via Inertia', function () {
|
||||
$this->actingAs(createUserWithRole());
|
||||
|
||||
$role = Role::create(['name' => 'Toast']);
|
||||
|
||||
$response = $this->put(route('admin.settings.roles.update', $role), [
|
||||
'name' => 'Updated Toast',
|
||||
'permissions' => ['user.view'],
|
||||
]);
|
||||
|
||||
$response->assertRedirect();
|
||||
});
|
||||
|
||||
test('updating non-existent role returns 404', function () {
|
||||
$this->actingAs(createUserWithRole());
|
||||
|
||||
$response = $this->put(route('admin.settings.roles.update', 999999), [
|
||||
'name' => 'Ghost Role',
|
||||
'permissions' => ['user.view'],
|
||||
]);
|
||||
|
||||
$response->assertStatus(404);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| DELETE / DESTROY
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
test('authenticated users without permission cannot delete roles', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$role = Role::create(['name' => 'To Delete']);
|
||||
|
||||
$response = $this->delete(route('admin.settings.roles.destroy', $role));
|
||||
$response->assertForbidden();
|
||||
|
||||
$this->assertDatabaseHas('roles', ['id' => $role->id]);
|
||||
});
|
||||
|
||||
test('role can be deleted', function () {
|
||||
$this->actingAs(createUserWithRole());
|
||||
|
||||
$role = Role::create(['name' => 'To Delete']);
|
||||
|
||||
$response = $this->delete(route('admin.settings.roles.destroy', $role));
|
||||
|
||||
$response
|
||||
->assertSessionHasNoErrors()
|
||||
->assertRedirect(route('admin.settings.roles.index'));
|
||||
|
||||
$this->assertDatabaseMissing('roles', ['id' => $role->id]);
|
||||
});
|
||||
|
||||
test('role permissions are removed when role is deleted', function () {
|
||||
$this->actingAs(createUserWithRole());
|
||||
|
||||
$role = Role::create(['name' => 'With Perms']);
|
||||
$role->syncPermissions(['user.view', 'user.create']);
|
||||
|
||||
$this->delete(route('admin.settings.roles.destroy', $role));
|
||||
|
||||
$this->assertDatabaseMissing('roles', ['id' => $role->id]);
|
||||
$this->assertDatabaseMissing('role_has_permissions', ['role_id' => $role->id]);
|
||||
});
|
||||
|
||||
test('delete flashes success toast via Inertia', function () {
|
||||
$this->actingAs(createUserWithRole());
|
||||
|
||||
$role = Role::create(['name' => 'Toast Delete']);
|
||||
|
||||
$response = $this->delete(route('admin.settings.roles.destroy', $role));
|
||||
|
||||
$response->assertRedirect();
|
||||
});
|
||||
|
||||
test('deleting non-existent role returns 404', function () {
|
||||
$this->actingAs(createUserWithRole());
|
||||
|
||||
$response = $this->delete(route('admin.settings.roles.destroy', 999999));
|
||||
|
||||
$response->assertStatus(404);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| AUTHORIZATION - GUEST CANNOT PERFORM ACTIONS
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
test('guest cannot create role', function () {
|
||||
$response = $this->post(route('admin.settings.roles.store'), [
|
||||
'name' => 'Unauthorized',
|
||||
'permissions' => ['user.view'],
|
||||
]);
|
||||
|
||||
$response->assertRedirect(route('login'));
|
||||
$this->assertDatabaseMissing('roles', ['name' => 'Unauthorized']);
|
||||
});
|
||||
|
||||
test('guest cannot update role', function () {
|
||||
$role = Role::create(['name' => 'Existing']);
|
||||
|
||||
$response = $this->put(route('admin.settings.roles.update', $role), [
|
||||
'name' => 'Unauthorized Update',
|
||||
'permissions' => ['user.view'],
|
||||
]);
|
||||
|
||||
$response->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('guest cannot delete role', function () {
|
||||
$role = Role::create(['name' => 'Existing']);
|
||||
|
||||
$response = $this->delete(route('admin.settings.roles.destroy', $role));
|
||||
|
||||
$response->assertRedirect(route('login'));
|
||||
$this->assertDatabaseHas('roles', ['id' => $role->id]);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| AUTHORIZATION - USER WITHOUT ROLE CANNOT PERFORM ACTIONS
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
test('user without developer role cannot create role', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$response = $this->post(route('admin.settings.roles.store'), [
|
||||
'name' => 'Unauthorized',
|
||||
'permissions' => ['user.view'],
|
||||
]);
|
||||
|
||||
$response->assertForbidden();
|
||||
$this->assertDatabaseMissing('roles', ['name' => 'Unauthorized']);
|
||||
});
|
||||
|
||||
test('user without developer role cannot update role', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$role = Role::create(['name' => 'Existing']);
|
||||
|
||||
$response = $this->put(route('admin.settings.roles.update', $role), [
|
||||
'name' => 'Unauthorized Update',
|
||||
'permissions' => ['user.view'],
|
||||
]);
|
||||
|
||||
$response->assertForbidden();
|
||||
});
|
||||
|
||||
test('user without developer role cannot delete role', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$role = Role::create(['name' => 'Existing']);
|
||||
|
||||
$response = $this->delete(route('admin.settings.roles.destroy', $role));
|
||||
|
||||
$response->assertForbidden();
|
||||
$this->assertDatabaseHas('roles', ['id' => $role->id]);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| DATA INTEGRITY
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
test('created role has correct timestamps', function () {
|
||||
$this->actingAs(createUserWithRole());
|
||||
|
||||
$this->post(route('admin.settings.roles.store'), [
|
||||
'name' => 'Timestamp Role',
|
||||
'permissions' => ['user.view'],
|
||||
]);
|
||||
|
||||
$role = Role::where('name', 'Timestamp Role')->first();
|
||||
expect($role->created_at)->not->toBeNull();
|
||||
expect($role->updated_at)->not->toBeNull();
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| PERMISSION ASSIGNMENT
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
test('role permissions are correctly synced', function () {
|
||||
$this->actingAs(createUserWithRole());
|
||||
|
||||
$role = Role::create(['name' => 'Sync Test']);
|
||||
$role->syncPermissions(['user.view', 'user.create', 'category.view']);
|
||||
|
||||
$role->refresh();
|
||||
expect($role->permissions->pluck('name')->sort()->values()->toArray())->toEqual(['category.view', 'user.create', 'user.view']);
|
||||
});
|
||||
|
||||
test('role permissions can be replaced', function () {
|
||||
$this->actingAs(createUserWithRole());
|
||||
|
||||
$role = Role::create(['name' => 'Replace Test']);
|
||||
$role->syncPermissions(['user.view']);
|
||||
|
||||
$role->syncPermissions(['category.view', 'category.create']);
|
||||
$role->refresh();
|
||||
|
||||
expect($role->permissions->pluck('name')->sort()->values()->toArray())->toEqual(['category.create', 'category.view']);
|
||||
});
|
||||
|
||||
test('role can have all permissions', function () {
|
||||
$this->actingAs(createUserWithRole());
|
||||
|
||||
$allPermissions = Permission::pluck('name')->toArray();
|
||||
|
||||
$role = Role::create(['name' => 'Super Admin']);
|
||||
$role->syncPermissions($allPermissions);
|
||||
|
||||
$role->refresh();
|
||||
expect($role->permissions->count())->toBe(count($allPermissions));
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| REALISTIC USER SCENARIOS
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
test('user creates role then immediately edits it', function () {
|
||||
$this->actingAs(createUserWithRole());
|
||||
|
||||
$this->post(route('admin.settings.roles.store'), [
|
||||
'name' => 'Draft Role',
|
||||
'permissions' => ['user.view'],
|
||||
]);
|
||||
|
||||
$role = Role::where('name', 'Draft Role')->first();
|
||||
|
||||
$this->put(route('admin.settings.roles.update', $role), [
|
||||
'name' => 'Final Role',
|
||||
'permissions' => ['user.view', 'user.create'],
|
||||
]);
|
||||
|
||||
$role->refresh();
|
||||
expect($role->name)->toBe('Final Role');
|
||||
expect($role->permissions->pluck('name')->sort()->values()->toArray())->toEqual(['user.create', 'user.view']);
|
||||
});
|
||||
|
||||
test('user creates multiple roles and deletes one', function () {
|
||||
$this->actingAs(createUserWithRole());
|
||||
|
||||
$this->post(route('admin.settings.roles.store'), ['name' => 'Keep Role', 'permissions' => ['user.view']]);
|
||||
$this->post(route('admin.settings.roles.store'), ['name' => 'Delete Role', 'permissions' => ['user.view']]);
|
||||
$this->post(route('admin.settings.roles.store'), ['name' => 'Also Keep Role', 'permissions' => ['user.view']]);
|
||||
|
||||
$toDelete = Role::where('name', 'Delete Role')->first();
|
||||
$this->delete(route('admin.settings.roles.destroy', $toDelete));
|
||||
|
||||
$response = $this->get(route('admin.settings.roles.index'));
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn (Assert $page) => $page
|
||||
->component('admin/roles/index')
|
||||
->has('roles', 3)
|
||||
);
|
||||
});
|
||||
|
||||
test('user tries to create role without submitting any data', function () {
|
||||
$this->actingAs(createUserWithRole());
|
||||
|
||||
$response = $this->post(route('admin.settings.roles.store'), []);
|
||||
|
||||
$response->assertSessionHasErrors(['name', 'permissions']);
|
||||
});
|
||||
|
||||
test('user tries to update role without submitting any data', function () {
|
||||
$this->actingAs(createUserWithRole());
|
||||
|
||||
$role = Role::create(['name' => 'Existing']);
|
||||
|
||||
$response = $this->put(route('admin.settings.roles.update', $role), []);
|
||||
|
||||
$response->assertSessionHasErrors(['name', 'permissions']);
|
||||
});
|
||||
|
||||
test('user rapidly submits same role creation twice', function () {
|
||||
$this->actingAs(createUserWithRole());
|
||||
|
||||
$this->post(route('admin.settings.roles.store'), ['name' => 'Rapid Submit', 'permissions' => ['user.view']]);
|
||||
$response = $this->post(route('admin.settings.roles.store'), ['name' => 'Rapid Submit', 'permissions' => ['user.view']]);
|
||||
|
||||
$response->assertSessionHasErrors('name');
|
||||
$this->assertDatabaseCount('roles', 2);
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user