orderBy('menu_parent_id', 'asc') ->orderBy('label', 'asc') ->orderBy('id', 'asc') ->get() ->toArray(); $byId = []; foreach ($all as $row) { $row['children'] = []; $byId[$row['id']] = $row; } $tree = []; foreach ($byId as $id => &$node) { $pid = $node['menu_parent_id'] ?: null; if ($pid && isset($byId[$pid])) { $byId[$pid]['children'][] = &$node; } else { $tree[] = &$node; } } unset($node); $this->sortTreeAlpha($tree); $flatAlpha = $all; usort($flatAlpha, function ($a, $b) { $result = strnatcasecmp($this->labelKey($a['label'] ?? ''), $this->labelKey($b['label'] ?? '')); return $result !== 0 ? $result : ((int) ($a['id'] ?? 0) <=> (int) ($b['id'] ?? 0)); }); $roleAssignments = $this->buildRoleAssignments(); $flattened = $this->flattenTreeForResponse($tree, $roleAssignments); $parentOptions = array_map(static function ($row) { return [ 'id' => (int) ($row['id'] ?? 0), 'label' => (string) ($row['label'] ?? ''), ]; }, $flatAlpha); $roles = Role::query()->orderBy('name')->get(['id', 'name'])->toArray(); return [ 'items' => $flattened, 'roles' => $roles, 'parentOptions' => $parentOptions, ]; } public function save(array $payload): array { $id = (int) ($payload['id'] ?? 0); $menuParentId = $payload['menu_parent_id'] ?? $payload['parent_id'] ?? null; $menuParentId = ($menuParentId === '' || $menuParentId === null) ? null : (int) $menuParentId; if ($id && $menuParentId === $id) { $menuParentId = null; } if ($menuParentId !== null && ! NavItem::query()->whereKey($menuParentId)->exists()) { return ['ok' => false, 'message' => 'Selected parent does not exist.']; } $data = [ 'menu_parent_id' => $menuParentId, 'label' => trim((string) ($payload['label'] ?? '')), 'url' => trim((string) ($payload['url'] ?? '')) ?: null, 'icon_class' => trim((string) ($payload['icon_class'] ?? '')) ?: null, 'target' => trim((string) ($payload['target'] ?? '')) ?: null, 'sort_order' => (int) ($payload['sort_order'] ?? 0), 'is_enabled' => ! empty($payload['is_enabled']) ? 1 : 0, ]; $roleIds = array_values(array_unique(array_filter( array_map('intval', (array) ($payload['roles'] ?? [])), fn ($v) => $v > 0 ))); $navItemId = null; DB::transaction(function () use ($id, $data, $roleIds, &$navItemId): void { if ($id > 0) { NavItem::query()->whereKey($id)->update($data); $navItemId = $id; } else { $navItemId = (int) NavItem::query()->create($data)->id; } RoleNavItem::query()->where('nav_item_id', $navItemId)->delete(); foreach ($roleIds as $rid) { RoleNavItem::query()->create([ 'role_id' => $rid, 'nav_item_id' => $navItemId, ]); } $this->normalizeSortOrdersAlphabetically(); }); $this->navbarService->clearCache(); return ['ok' => true, 'id' => $navItemId]; } public function delete(int $id): bool { $deleted = false; DB::transaction(function () use ($id, &$deleted): void { $deleted = (bool) NavItem::query()->whereKey($id)->delete(); if ($deleted) { $this->normalizeSortOrdersAlphabetically(); } }); $this->navbarService->clearCache(); return $deleted; } public function reorder(array $orders): void { DB::transaction(function (): void { $this->normalizeSortOrdersAlphabetically(); }); $this->navbarService->clearCache(); } private function normalizeSortOrdersAlphabetically(): void { $rows = NavItem::query() ->select(['id', 'menu_parent_id', 'label']) ->orderBy('menu_parent_id', 'asc') ->orderBy('label', 'asc') ->orderBy('id', 'asc') ->get() ->groupBy(fn ($row) => $row->menu_parent_id === null ? 'root' : (string) $row->menu_parent_id); foreach ($rows as $siblings) { $ordered = $siblings->values()->all(); usort($ordered, function ($a, $b) { $result = strnatcasecmp($this->labelKey((string) ($a->label ?? '')), $this->labelKey((string) ($b->label ?? ''))); return $result !== 0 ? $result : ((int) $a->id <=> (int) $b->id); }); foreach ($ordered as $index => $item) { NavItem::query() ->whereKey((int) $item->id) ->update(['sort_order' => ($index + 1) * 10]); } } } private function buildRoleAssignments(): array { $rows = RoleNavItem::query() ->select('role_nav_items.nav_item_id', 'roles.id as role_id', 'roles.name as role_name') ->join('roles', 'roles.id', '=', 'role_nav_items.role_id') ->get() ->toArray(); $roleAssignments = []; foreach ($rows as $row) { $navId = (int) ($row['nav_item_id'] ?? 0); if ($navId <= 0) { continue; } $roleId = (int) ($row['role_id'] ?? 0); $roleName = $row['role_name'] ?? ($roleId ? ('#'.$roleId) : null); if ($roleId > 0) { $roleAssignments[$navId]['ids'][] = $roleId; } if ($roleName !== null) { $roleAssignments[$navId]['names'][] = $roleName; } } return $roleAssignments; } private function flattenTreeForResponse(array $tree, array $roleAssignments, int $depth = 0): array { $output = []; foreach ($tree as $node) { $id = (int) ($node['id'] ?? 0); $roles = $roleAssignments[$id] ?? ['ids' => [], 'names' => []]; $output[] = [ 'id' => $id, 'label' => (string) ($node['label'] ?? ''), 'url' => $node['url'] ?? null, 'icon_class' => $node['icon_class'] ?? null, 'target' => $node['target'] ?? null, 'menu_parent_id' => $node['menu_parent_id'] ?? null, 'sort_order' => (int) ($node['sort_order'] ?? 0), 'is_enabled' => (int) ($node['is_enabled'] ?? 0), 'depth' => $depth, 'roles' => [ 'ids' => array_values(array_unique($roles['ids'] ?? [])), 'names' => array_values(array_unique($roles['names'] ?? [])), ], ]; if (! empty($node['children'])) { $output = array_merge($output, $this->flattenTreeForResponse($node['children'], $roleAssignments, $depth + 1)); } } return $output; } private function labelKey(string $s): string { $s = trim(preg_replace('/\s+/', ' ', $s)); $s = preg_replace('/^[^[:alnum:]]+/u', '', $s); $s = preg_replace('/^(?i)(the|an|a)\s+/', '', $s); return mb_strtolower($s, 'UTF-8'); } private function sortTreeAlpha(array &$nodes): void { usort($nodes, function ($a, $b) { $result = strnatcasecmp($this->labelKey($a['label'] ?? ''), $this->labelKey($b['label'] ?? '')); return $result !== 0 ? $result : ((int) ($a['id'] ?? 0) <=> (int) ($b['id'] ?? 0)); }); foreach ($nodes as &$node) { if (! empty($node['children'])) { $this->sortTreeAlpha($node['children']); } } unset($node); } }