with('tenant') ->when( $actor && ! $actor->isMasterAdmin(), fn ($q) => $q->where('tenant_id', $tenantId) ) ->when( ! empty($filters['search']), fn ($q) => $q->where(function ($x) use ($filters) { $x->where('name', 'ILIKE', "%{$filters['search']}%") ->orWhere('description', 'ILIKE', "%{$filters['search']}%"); }) ) ->when( array_key_exists('is_active', $filters) && $filters['is_active'] !== null && $filters['is_active'] !== '', fn ($q) => $q->where('is_active', filter_var($filters['is_active'], FILTER_VALIDATE_BOOLEAN)) ) ->orderBy( $filters['sort_by'] ?? 'id', $filters['sort_direction'] ?? 'desc' ) ->paginate($filters['per_page'] ?? 10); } public function create(array $data, User $actor, ?int $tenantId): MenuGroup { if (! array_key_exists('is_active', $data) || $data['is_active'] === null) { $data['is_active'] = true; } $scopeTenantId = $actor->isMasterAdmin() ? ($data['tenant_id'] ?? null) : $tenantId; if (! $actor->isMasterAdmin() && ! $scopeTenantId) { throw ValidationException::withMessages([ 'tenant_id' => 'Tenant aktif wajib tersedia untuk membuat Menu Group.', ]); } $data['tenant_id'] = $scopeTenantId; $data['is_system'] = $actor->isMasterAdmin() && ! $scopeTenantId; return MenuGroup::create($data)->load('tenant'); } public function update( MenuGroup $menuGroup, array $data, User $actor, ?int $tenantId ): MenuGroup { if (array_key_exists('is_active', $data) && $data['is_active'] === null) { unset($data['is_active']); } if ($actor->isMasterAdmin()) { if (array_key_exists('tenant_id', $data)) { $data['is_system'] = ! $data['tenant_id']; } } else { unset($data['tenant_id'], $data['is_system']); if ((int) $menuGroup->tenant_id !== (int) $tenantId) { abort(403); } } $menuGroup->update($data); return $menuGroup->fresh('tenant'); } public function delete(MenuGroup $menuGroup): bool { $hasMenuMappings = $menuGroup->menus()->exists(); $hasUserMappings = $menuGroup->userMenuGroups()->exists(); if ($hasMenuMappings || $hasUserMappings) { throw ValidationException::withMessages([ 'menu_group' => 'Menu Group tidak dapat dihapus karena masih memiliki relasi menu/user.', ]); } return $menuGroup->delete(); } public function listMenus(MenuGroup $menuGroup): array { $rows = DB::table('menu_lists as m') ->leftJoin('menu_group_menus as mgm', function ($join) use ($menuGroup) { $join->on('mgm.menu_id', '=', 'm.id') ->where('mgm.menu_group_id', '=', $menuGroup->id); }) ->where('m.is_active', true) ->select( 'm.id', 'm.parent_id', 'm.name', 'm.slug', 'm.url', 'm.icon', 'm.component', 'm.sort_order', DB::raw('COALESCE(mgm.can_view, false) as can_view'), DB::raw('COALESCE(mgm.can_create, false) as can_create'), DB::raw('COALESCE(mgm.can_update, false) as can_update'), DB::raw('COALESCE(mgm.can_delete, false) as can_delete'), DB::raw('COALESCE(mgm.can_approve, false) as can_approve'), DB::raw('COALESCE(mgm.can_export, false) as can_export') ) ->orderBy('m.sort_order') ->orderBy('m.id') ->get(); return $rows->map(function ($row) { return [ 'menu_id' => (int) $row->id, 'parent_id' => $row->parent_id ? (int) $row->parent_id : null, 'name' => $row->name, 'slug' => $row->slug, 'url' => $row->url, 'icon' => $row->icon, 'component' => $row->component, 'sort_order' => (int) $row->sort_order, 'permissions' => [ 'can_view' => (bool) $row->can_view, 'can_create' => (bool) $row->can_create, 'can_update' => (bool) $row->can_update, 'can_delete' => (bool) $row->can_delete, 'can_approve' => (bool) $row->can_approve, 'can_export' => (bool) $row->can_export, ], 'available_permissions' => [ 'can_view' => true, 'can_create' => true, 'can_update' => true, 'can_delete' => true, 'can_approve' => true, 'can_export' => true, ], ]; })->all(); } public function syncMenus(MenuGroup $menuGroup, array $menus, ?User $actor = null): void { if ($actor && ! $actor->isMasterAdmin()) { $allowed = $this->getAllowedMenuPermissionsForUser( $actor, request()->attributes->get('tenant_id') ); foreach ($menus as $menu) { $actorPermissions = $allowed[(int) $menu['menu_id']] ?? null; if (! $actorPermissions) { throw ValidationException::withMessages([ 'menus' => 'Terdapat menu di luar hak akses user login.', ]); } foreach (['view', 'create', 'update', 'delete', 'approve', 'export'] as $permission) { if (($menu["can_$permission"] ?? false) && ! $actorPermissions["can_$permission"]) { throw ValidationException::withMessages([ 'menus' => "Permission can_$permission tidak boleh melebihi akses Anda.", ]); } } } } $syncData = []; foreach ($menus as $menu) { $syncData[$menu['menu_id']] = [ 'can_view' => (bool) ($menu['can_view'] ?? false), 'can_create' => (bool) ($menu['can_create'] ?? false), 'can_update' => (bool) ($menu['can_update'] ?? false), 'can_delete' => (bool) ($menu['can_delete'] ?? false), 'can_approve' => (bool) ($menu['can_approve'] ?? false), 'can_export' => (bool) ($menu['can_export'] ?? false), 'updated_at' => now(), 'created_at' => now(), ]; } $menuGroup->menus()->sync($syncData); } public function listAvailableMenusForGroup(MenuGroup $menuGroup, User $actor): array { if ($actor->isMasterAdmin()) { return $this->listMenus($menuGroup); } $allowedPermissions = $this->getAllowedMenuPermissionsForUser( $actor, request()->attributes->get('tenant_id') ); $allowedMenuIds = array_keys($allowedPermissions); $rows = DB::table('menu_lists as m') ->leftJoin('menu_group_menus as mgm', function ($join) use ($menuGroup) { $join->on('mgm.menu_id', '=', 'm.id') ->where('mgm.menu_group_id', '=', $menuGroup->id); }) ->where('m.is_active', true) ->whereIn('m.id', $allowedMenuIds) ->select( 'm.id', 'm.parent_id', 'm.name', 'm.sort_order', DB::raw('COALESCE(mgm.can_view, false) as can_view'), DB::raw('COALESCE(mgm.can_create, false) as can_create'), DB::raw('COALESCE(mgm.can_update, false) as can_update'), DB::raw('COALESCE(mgm.can_delete, false) as can_delete'), DB::raw('COALESCE(mgm.can_approve, false) as can_approve'), DB::raw('COALESCE(mgm.can_export, false) as can_export') ) ->orderBy('m.sort_order') ->orderBy('m.id') ->get(); return $rows->map(function ($row) use ($allowedPermissions) { $available = $allowedPermissions[(int) $row->id]; return [ 'menu_id' => (int) $row->id, 'parent_id' => $row->parent_id ? (int) $row->parent_id : null, 'name' => $row->name, 'sort_order' => (int) $row->sort_order, 'permissions' => [ 'can_view' => (bool) $row->can_view, 'can_create' => (bool) $row->can_create, 'can_update' => (bool) $row->can_update, 'can_delete' => (bool) $row->can_delete, 'can_approve' => (bool) $row->can_approve, 'can_export' => (bool) $row->can_export, ], 'available_permissions' => $available, ]; })->all(); } public function getAllowedMenuIdsForUser(User $user, ?int $tenantId = null): array { if ($user->isMasterAdmin()) { return DB::table('menu_lists') ->where('is_active', true) ->pluck('id') ->map(fn ($id) => (int) $id) ->all(); } return DB::table('menu_group_menus as mgm') ->join('user_menu_groups as umg', 'umg.menu_group_id', '=', 'mgm.menu_group_id') ->join('menu_lists as m', 'm.id', '=', 'mgm.menu_id') ->where('umg.user_id', $user->id) ->where('umg.tenant_id', $tenantId) ->where('m.is_active', true) ->where(function ($q) { $q->where('mgm.can_view', true) ->orWhere('mgm.can_create', true) ->orWhere('mgm.can_update', true) ->orWhere('mgm.can_delete', true) ->orWhere('mgm.can_approve', true) ->orWhere('mgm.can_export', true); }) ->distinct() ->pluck('mgm.menu_id') ->map(fn ($id) => (int) $id) ->all(); } public function getAllowedMenuPermissionsForUser(User $user, ?int $tenantId): array { return DB::table('menu_group_menus as mgm') ->join('user_menu_groups as umg', 'umg.menu_group_id', '=', 'mgm.menu_group_id') ->join('menu_groups as mg', 'mg.id', '=', 'mgm.menu_group_id') ->join('menu_lists as m', 'm.id', '=', 'mgm.menu_id') ->where('umg.user_id', $user->id) ->where('umg.tenant_id', $tenantId) ->where('mg.is_active', true) ->where('m.is_active', true) ->groupBy('mgm.menu_id') ->select( 'mgm.menu_id', DB::raw('MAX(CASE WHEN mgm.can_view THEN 1 ELSE 0 END) as can_view'), DB::raw('MAX(CASE WHEN mgm.can_create THEN 1 ELSE 0 END) as can_create'), DB::raw('MAX(CASE WHEN mgm.can_update THEN 1 ELSE 0 END) as can_update'), DB::raw('MAX(CASE WHEN mgm.can_delete THEN 1 ELSE 0 END) as can_delete'), DB::raw('MAX(CASE WHEN mgm.can_approve THEN 1 ELSE 0 END) as can_approve'), DB::raw('MAX(CASE WHEN mgm.can_export THEN 1 ELSE 0 END) as can_export') ) ->get() ->mapWithKeys(fn ($row) => [(int) $row->menu_id => [ 'can_view' => (bool) $row->can_view, 'can_create' => (bool) $row->can_create, 'can_update' => (bool) $row->can_update, 'can_delete' => (bool) $row->can_delete, 'can_approve' => (bool) $row->can_approve, 'can_export' => (bool) $row->can_export, ]]) ->all(); } public function listUserAssignments(User $user, ?int $tenantId = null): array { return DB::table('user_menu_groups as umg') ->join('menu_groups as mg', 'mg.id', '=', 'umg.menu_group_id') ->leftJoin('tenants as t', 't.id', '=', 'umg.tenant_id') ->where('umg.user_id', $user->id) ->when($tenantId, fn ($q) => $q->where('umg.tenant_id', $tenantId)) ->select( 'umg.menu_group_id', 'mg.name as menu_group_name', 'mg.is_active as menu_group_is_active', 'umg.tenant_id', 't.tenant_name as tenant_name', 'umg.created_at', 'umg.updated_at' ) ->orderBy('umg.menu_group_id') ->orderBy('umg.tenant_id') ->get() ->map(function ($row) { return [ 'menu_group_id' => (int) $row->menu_group_id, 'menu_group_name' => $row->menu_group_name, 'menu_group_is_active' => (bool) $row->menu_group_is_active, 'tenant_id' => $row->tenant_id ? (int) $row->tenant_id : null, 'tenant_name' => $row->tenant_name, 'created_at' => $row->created_at, 'updated_at' => $row->updated_at, ]; }) ->all(); } public function syncUserAssignments(User $user, array $assignments, ?int $tenantId = null): void { $rows = []; foreach ($assignments as $assignment) { if ($tenantId && (int) ($assignment['tenant_id'] ?? 0) !== $tenantId) { throw ValidationException::withMessages([ 'assignments' => 'Assignment hanya boleh dibuat pada tenant aktif.', ]); } $group = MenuGroup::find($assignment['menu_group_id']); if ($tenantId && $group && (int) $group->tenant_id !== $tenantId) { throw ValidationException::withMessages([ 'assignments' => 'Menu group bukan milik tenant aktif.', ]); } $rows[] = [ 'user_id' => $user->id, 'menu_group_id' => (int) $assignment['menu_group_id'], 'tenant_id' => $assignment['tenant_id'] ?? null, 'created_at' => now(), 'updated_at' => now(), ]; } DB::transaction(function () use ($user, $rows) { DB::table('user_menu_groups') ->where('user_id', $user->id) ->when($tenantId, fn ($q) => $q->where('tenant_id', $tenantId)) ->delete(); if (! empty($rows)) { DB::table('user_menu_groups')->insert($rows); } }); } }