diff --git a/app/Http/Controllers/Admin/Developer/RolePermissionController.php b/app/Http/Controllers/Admin/Developer/RolePermissionController.php new file mode 100644 index 0000000..2a0169f --- /dev/null +++ b/app/Http/Controllers/Admin/Developer/RolePermissionController.php @@ -0,0 +1,35 @@ + $this->service->roles(), + 'permissionGroups' => $this->service->groupedPermissions(), + ]); + } + + public function update(UpdateRolePermissionsRequest $request, Role $role): RedirectResponse + { + $this->service->updatePermissions($role, $request->validated('permissions')); + + Inertia::flash('toast', ['type' => 'success', 'message' => 'Permission role berhasil diperbarui.']); + + return to_route('admin.developer.roles.index'); + } +} diff --git a/app/Http/Requests/Admin/Developer/UpdateRolePermissionsRequest.php b/app/Http/Requests/Admin/Developer/UpdateRolePermissionsRequest.php new file mode 100644 index 0000000..9f374d5 --- /dev/null +++ b/app/Http/Requests/Admin/Developer/UpdateRolePermissionsRequest.php @@ -0,0 +1,23 @@ + ['present', 'array'], + 'permissions.*' => ['string', Rule::in(PermissionCatalog::all())], + ]; + } +} diff --git a/app/Services/Admin/Developer/RolePermissionService.php b/app/Services/Admin/Developer/RolePermissionService.php new file mode 100644 index 0000000..008ff87 --- /dev/null +++ b/app/Services/Admin/Developer/RolePermissionService.php @@ -0,0 +1,52 @@ +> + */ + public function groupedPermissions(): array + { + return PermissionCatalog::groups(); + } + + /** + * @return Collection}> + */ + public function roles(): Collection + { + return Role::with('permissions:id,name') + ->orderBy('name') + ->get(['id', 'name']) + ->map(fn (Role $role) => [ + 'id' => $role->id, + 'name' => $role->name, + 'permissions' => $role->permissions->pluck('name')->all(), + ]); + } + + /** + * @param array $permissionNames + */ + public function updatePermissions(Role $role, array $permissionNames): Role + { + $valid = array_values(array_intersect($permissionNames, PermissionCatalog::all())); + + if ($role->name === 'developer') { + $valid = array_values(array_unique([...$valid, 'view-roles', 'update-roles'])); + } + + $role->syncPermissions($valid); + + app(PermissionRegistrar::class)->forgetCachedPermissions(); + + return $role->load('permissions:id,name'); + } +} diff --git a/app/Support/PermissionCatalog.php b/app/Support/PermissionCatalog.php new file mode 100644 index 0000000..66d578c --- /dev/null +++ b/app/Support/PermissionCatalog.php @@ -0,0 +1,75 @@ +> + */ + public static function groups(): array + { + return [ + 'Dashboard' => self::DASHBOARD, + 'Master' => self::MASTER, + 'Kelas' => self::ACADEMIC_CLASSES, + 'Kelola' => self::MANAGE, + 'Keuangan' => self::FINANCES, + 'Layanan' => self::SERVICES, + 'Pengguna' => self::USERS, + 'Pengembang' => self::DEVELOPER, + ]; + } + + /** + * @return array + */ + public static function all(): array + { + return array_merge(...array_values(self::groups())); + } +} diff --git a/database/seeders/RolePermissionSeeder.php b/database/seeders/RolePermissionSeeder.php index 991d897..28eaaf9 100644 --- a/database/seeders/RolePermissionSeeder.php +++ b/database/seeders/RolePermissionSeeder.php @@ -2,6 +2,7 @@ namespace Database\Seeders; +use App\Support\PermissionCatalog; use Illuminate\Database\Seeder; use Spatie\Permission\Models\Permission; use Spatie\Permission\Models\Role; @@ -13,57 +14,14 @@ public function run(): void { app()[PermissionRegistrar::class]->forgetCachedPermissions(); - $master = [ - 'view-academic-terms', 'create-academic-terms', 'update-academic-terms', 'delete-academic-terms', 'update-academic-terms-status', - 'view-departments', 'create-departments', 'update-departments', 'delete-departments', - 'view-courses', 'create-courses', 'update-courses', 'delete-courses', - ]; + $master = PermissionCatalog::MASTER; + $academicClasses = PermissionCatalog::ACADEMIC_CLASSES; + $manage = PermissionCatalog::MANAGE; + $finances = PermissionCatalog::FINANCES; + $services = PermissionCatalog::SERVICES; + $users = PermissionCatalog::USERS; - $academicClasses = [ - 'view-materials', 'create-materials', 'update-materials', 'delete-materials', - 'view-assignments', 'create-assignments', 'update-assignments', 'delete-assignments', - 'view-assignment-submissions', 'create-assignment-submissions', 'update-assignment-submissions', 'delete-assignment-submissions', - 'view-schedules', 'create-schedules', 'update-schedules', 'delete-schedules', - 'view-attendances', 'create-attendances', 'delete-attendances', - ]; - - $manage = [ - 'view-course-classes', 'create-course-classes', 'update-course-classes', 'delete-course-classes', - 'view-course-class-enrollments', 'create-course-class-enrollments', 'delete-course-class-enrollments', - 'view-course-registrations', 'create-course-registrations', 'approve-course-registrations', 'reject-course-registrations', - 'view-announcements', 'create-announcements', 'update-announcements', 'delete-announcements', - ]; - - $finances = [ - 'view-tuition-invoices', 'create-tuition-invoices', 'update-tuition-invoices', 'delete-tuition-invoices', - 'view-tuition-payments', 'create-tuition-payments', 'update-tuition-payments', 'delete-tuition-payments', - ]; - - $services = [ - 'view-letter-requests', 'create-letter-requests', 'update-letter-requests', 'delete-letter-requests', - 'view-academic-advising-logs', 'create-academic-advising-logs', 'update-academic-advising-logs', 'delete-academic-advising-logs', - ]; - - $users = [ - 'view-lecturers', 'create-lecturers', 'update-lecturers', 'delete-lecturers', 'reset-lecturers-password', 'update-lecturers-status', 'export-lecturers', - 'view-students', 'create-students', 'update-students', 'delete-students', 'reset-students-password', 'update-students-academic-status', 'update-students-account-status', 'export-students', - 'view-administrators', 'create-administrators', 'update-administrators', 'delete-administrators', 'reset-administrators-password', 'update-administrators-status', - ]; - - $developer = [ - 'view-logs', - ]; - - $permissionNames = [ - 'view-dashboard', - ...$master, - ...$academicClasses, - ...$manage, - ...$finances, - ...$services, - ...$users, - ...$developer, - ]; + $permissionNames = PermissionCatalog::all(); foreach ($permissionNames as $name) { Permission::firstOrCreate(['name' => $name, 'guard_name' => 'web']); diff --git a/resources/js/components/app-sidebar.tsx b/resources/js/components/app-sidebar.tsx index 34547b6..283b206 100644 --- a/resources/js/components/app-sidebar.tsx +++ b/resources/js/components/app-sidebar.tsx @@ -17,6 +17,7 @@ import { Receipt, School, ScrollText, + ShieldCheck, User, Users, } from 'lucide-react'; @@ -39,6 +40,7 @@ import { index as attendancesRoute } from '@/routes/admin/academic-classes/atten import { index as materialsRoute } from '@/routes/admin/academic-classes/materials'; import { index as schedulesRoute } from '@/routes/admin/academic-classes/schedules'; import { index as logsRoute } from '@/routes/admin/developer/logs'; +import { index as rolePermissionsRoute } from '@/routes/admin/developer/roles'; import { index as feedbackRoute } from '@/routes/admin/feedback'; import { index as tuitionInvoicesRoute } from '@/routes/admin/finances/tuition-invoices'; import { index as announcementsRoute } from '@/routes/admin/manage/announcements'; @@ -63,10 +65,12 @@ function buildNavMain({ isMahasiswa, isDosen, canViewLogs, + canViewRoles, }: { isMahasiswa: boolean; isDosen: boolean; canViewLogs: boolean; + canViewRoles: boolean; }): (NavGroup | NavItem)[] { return [ { @@ -190,16 +194,29 @@ function buildNavMain({ }, ], }, - ...(canViewLogs + ...(canViewLogs || canViewRoles ? [ { label: 'Pengembang', items: [ - { - name: 'Logs', - url: logsRoute.url(), - icon: ScrollText, - }, + ...(canViewLogs + ? [ + { + name: 'Logs', + url: logsRoute.url(), + icon: ScrollText, + }, + ] + : []), + ...(canViewRoles + ? [ + { + name: 'Role & Permission', + url: rolePermissionsRoute.url(), + icon: ShieldCheck, + }, + ] + : []), ], }, ] @@ -227,7 +244,13 @@ export function AppSidebar({ ...props }: React.ComponentProps) { const isMahasiswa = !isStaff && roleNames.includes('mahasiswa'); const isDosen = !isStaff && roleNames.includes('dosen'); const canViewLogs = (auth?.permissions ?? []).includes('view-logs'); - const navMain = buildNavMain({ isMahasiswa, isDosen, canViewLogs }); + const canViewRoles = (auth?.permissions ?? []).includes('view-roles'); + const navMain = buildNavMain({ + isMahasiswa, + isDosen, + canViewLogs, + canViewRoles, + }); return ( diff --git a/resources/js/pages/admin/developer/roles/index.tsx b/resources/js/pages/admin/developer/roles/index.tsx new file mode 100644 index 0000000..3d16c53 --- /dev/null +++ b/resources/js/pages/admin/developer/roles/index.tsx @@ -0,0 +1,186 @@ +import { Head, router } from '@inertiajs/react'; +import { Save } from 'lucide-react'; +import { useState } from 'react'; +import { PageHeader } from '@/components/page-header'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Checkbox } from '@/components/ui/checkbox'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { update } from '@/routes/admin/developer/roles'; +import type { + PermissionGroups, + RoleWithPermissions, +} from '@/types/role-permission'; +import { formatRoleLabel } from '@/types/role-permission'; + +type Props = { + roles: RoleWithPermissions[]; + permissionGroups: PermissionGroups; +}; + +export default function RolePermissionIndex({ + roles, + permissionGroups, +}: Props) { + const [selectedRoleId, setSelectedRoleId] = useState( + roles[0]?.id, + ); + const selectedRole = roles.find((role) => role.id === selectedRoleId); + const [selected, setSelected] = useState( + selectedRole?.permissions ?? [], + ); + const [processing, setProcessing] = useState(false); + + function handleSelectRole(id: string) { + const role = roles.find((r) => r.id === Number(id)); + setSelectedRoleId(role?.id); + setSelected(role?.permissions ?? []); + } + + function toggle(name: string, checked: boolean) { + setSelected((prev) => + checked ? [...prev, name] : prev.filter((p) => p !== name), + ); + } + + function toggleGroup(names: string[], checked: boolean) { + setSelected((prev) => { + const withoutGroup = prev.filter((p) => !names.includes(p)); + + return checked ? [...withoutGroup, ...names] : withoutGroup; + }); + } + + function handleSave() { + if (!selectedRole) { + return; + } + + setProcessing(true); + router.put( + update.url(selectedRole.id), + { permissions: selected }, + { + preserveScroll: true, + onFinish: () => setProcessing(false), + }, + ); + } + + return ( + <> + + +
+ + + Simpan + + ) + } + /> + +
+ + {selectedRole && ( + + {selected.length} permission + + )} +
+ + {selectedRole && ( +
+ {Object.entries(permissionGroups).map( + ([group, names]) => { + const allChecked = names.every((name) => + selected.includes(name), + ); + + return ( + + + + {group} + + + + + {names.map((name) => ( + + ))} + + + ); + }, + )} +
+ )} +
+ + ); +} diff --git a/resources/js/types/role-permission.ts b/resources/js/types/role-permission.ts new file mode 100644 index 0000000..b4f9dfc --- /dev/null +++ b/resources/js/types/role-permission.ts @@ -0,0 +1,20 @@ +export type RoleWithPermissions = { + id: number; + name: string; + permissions: string[]; +}; + +export type PermissionGroups = Record; + +export const RoleLabels: Record = { + mahasiswa: 'Mahasiswa', + dosen: 'Dosen', + 'staff-admin': 'Staff Admin', + 'staff-keuangan': 'Staff Keuangan', + kaprodi: 'Kaprodi', + developer: 'Developer', +}; + +export function formatRoleLabel(name: string): string { + return RoleLabels[name] ?? name; +} diff --git a/routes/admin.php b/routes/admin.php index 28558c2..234d53b 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -6,6 +6,7 @@ use App\Http\Controllers\Admin\AcademicClasses\ScheduleController; use App\Http\Controllers\Admin\AcademicClasses\SubmissionController; use App\Http\Controllers\Admin\Developer\LogController; +use App\Http\Controllers\Admin\Developer\RolePermissionController; use App\Http\Controllers\Admin\FeedbackController; use App\Http\Controllers\Admin\Finances\TuitionInvoiceController; use App\Http\Controllers\Admin\Finances\TuitionPaymentController; @@ -238,8 +239,11 @@ Route::patch('administrators/{user}/user-status', [AdministratorController::class, 'updateUserStatus'])->name('administrators.update_user_status')->middleware('permission:update-administrators-status'); }); - Route::prefix('admin/developer')->name('admin.developer.')->middleware('permission:view-logs')->group(function () { - Route::get('logs', [LogController::class, 'index'])->name('logs.index'); - Route::get('logs/download', [LogController::class, 'download'])->name('logs.download'); + Route::prefix('admin/developer')->name('admin.developer.')->group(function () { + Route::get('logs', [LogController::class, 'index'])->name('logs.index')->middleware('permission:view-logs'); + Route::get('logs/download', [LogController::class, 'download'])->name('logs.download')->middleware('permission:view-logs'); + + Route::get('roles', [RolePermissionController::class, 'index'])->name('roles.index')->middleware('permission:view-roles'); + Route::put('roles/{role}', [RolePermissionController::class, 'update'])->name('roles.update')->middleware('permission:update-roles'); }); });