Files
2026-05-16 13:44:12 -04:00

304 lines
9.6 KiB
PHP
Executable File

<?php
namespace App\Controllers\View;
use App\Controllers\BaseController;
use App\Models\NavItemModel;
use App\Models\RoleNavItemModel;
use App\Services\NavbarService;
class NavBuilderController extends BaseController
{
protected NavItemModel $items;
protected RoleNavItemModel $maps;
protected NavbarService $service;
public function __construct()
{
$this->items = new NavItemModel();
$this->maps = new RoleNavItemModel();
$this->service = new NavbarService();
}
protected function ensureAdmin(): void
{
$sessionRole = session()->get('role'); // could be a string or array in your app
$roleNames = is_array($sessionRole) ? $sessionRole : [$sessionRole];
$roleNames = array_values(array_filter(array_map('strval', $roleNames)));
if (empty($roleNames)) {
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
}
$db = \Config\Database::connect();
// Map role names -> ids
$roleIdRows = $db->table('roles')->select('id')->whereIn('name', $roleNames)->get()->getResultArray();
$roleIds = array_map('intval', array_column($roleIdRows, 'id'));
if (empty($roleIds)) {
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
}
// Is this route allowed for any of the user's roles?
$allowed = $db->table('role_nav_items AS rni')
->select('1')
->join('nav_items AS ni', 'ni.id = rni.nav_item_id')
->where('ni.url', 'nav-builder') // IMPORTANT: your current route path
->whereIn('rni.role_id', $roleIds)
->get(1)->getFirstRow();
if (!$allowed) {
// You can show a nicer "Access Denied" view if you prefer
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
}
}
public function index(){
$this->ensureAdmin();
helper(['url', 'form']);
return view('nav_builder/index');
}
/** Case-insensitive, natural sort; ignores leading punctuation & articles (“a/an/the”) */
private function labelKey(string $s): string
{
$s = trim(preg_replace('/\s+/', ' ', $s));
$s = preg_replace('/^[^[:alnum:]]+/u', '', $s); // strip leading punctuation
$s = preg_replace('/^(?i)(the|an|a)\s+/', '', $s); // drop leading articles
return mb_strtolower($s, 'UTF-8');
}
private function sortTreeAlpha(array &$nodes): void
{
usort($nodes, fn($a,$b) => strnatcasecmp(
$this->labelKey($a['label'] ?? ''), $this->labelKey($b['label'] ?? '')
));
foreach ($nodes as &$n) {
if (!empty($n['children'])) {
$this->sortTreeAlpha($n['children']);
}
}
unset($n);
}
public function save()
{
$this->ensureAdmin();
$id = (int) $this->request->getPost('id');
// Accept either "menu_parent_id" or "parent_id" from the form
$menuParentRaw = $this->request->getPost('menu_parent_id');
if ($menuParentRaw === null) {
$menuParentRaw = $this->request->getPost('parent_id');
}
$menuParentId = ($menuParentRaw === '' || $menuParentRaw === null) ? null : (int) $menuParentRaw;
if ($id && $menuParentId === $id) {
// item cannot be its own parent
$menuParentId = null;
}
// Validate parent exists (optional but helpful)
if ($menuParentId !== null) {
$parent = $this->items->select('id')->where('id', $menuParentId)->first();
if (!$parent) {
return redirect()->back()->with('error', 'Selected parent does not exist.')->withInput();
}
}
$data = [
'menu_parent_id' => $menuParentId,
'label' => trim((string) $this->request->getPost('label')),
'url' => trim((string) $this->request->getPost('url')) ?: null,
'icon_class' => trim((string) $this->request->getPost('icon_class')) ?: null,
'target' => trim((string) $this->request->getPost('target')) ?: null,
'sort_order' => (int) $this->request->getPost('sort_order'),
'is_enabled' => (int) ($this->request->getPost('is_enabled') ? 1 : 0),
];
// Save (force insert to return ID)
if ($id) {
$this->items->update($id, $data);
} else {
$id = (int) $this->items->insert($data, true); // ensure insertID is returned
}
// Roles
$roleIds = array_values(array_unique(array_filter(
array_map('intval', (array) ($this->request->getPost('roles') ?? [])),
fn ($v) => $v > 0
)));
$this->maps->where('nav_item_id', $id)->delete();
foreach ($roleIds as $rid) {
$this->maps->insert(['role_id' => $rid, 'nav_item_id' => $id]);
}
$this->service->clearCache();
return redirect()->back()->with('success', 'Menu saved.');
}
public function delete($id)
{
$this->ensureAdmin();
$id = (int) $id;
$this->items->delete($id); // children handled by FK (SET NULL on parent)
$this->service->clearCache();
return redirect()->back()->with('success', 'Menu item deleted.');
}
public function reorder()
{
$this->ensureAdmin();
$orders = $this->request->getPost('orders') ?? [];
foreach ($orders as $id => $order) {
$this->items->update((int) $id, ['sort_order' => (int) $order]);
}
$this->service->clearCache();
return $this->response->setJSON(['ok' => true]);
}
protected function distinctRoles(): array
{
$db = \Config\Database::connect();
return $db->table('roles')
->select('id, name')
->orderBy('name')
->get()
->getResultArray();
}
public function data()
{
$this->ensureAdmin();
return $this->response->setJSON($this->buildNavPayload());
}
private function buildNavPayload(): array
{
$all = $this->items
->orderBy('menu_parent_id', 'ASC')
->orderBy('label', 'ASC')
->orderBy('id', 'ASC')
->findAll();
$byId = [];
foreach ($all as $a) {
$a['children'] = [];
$byId[$a['id']] = $a;
}
$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, fn($a, $b) => strnatcasecmp(
$this->labelKey($a['label'] ?? ''),
$this->labelKey($b['label'] ?? '')
));
$roleAssignments = [];
$roleRows = $this->maps
->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', 'left')
->findAll();
foreach ($roleRows 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;
}
}
$flattened = $this->flattenTreeForResponse($tree, $roleAssignments);
$parentOptions = array_map(static function ($row) {
return [
'id' => (int) ($row['id'] ?? 0),
'label' => (string) ($row['label'] ?? ''),
];
}, $flatAlpha);
$roles = array_map(static function ($role) {
return [
'id' => (int) ($role['id'] ?? 0),
'name' => (string) ($role['name'] ?? ''),
];
}, $this->distinctRoles());
return [
'items' => $flattened,
'roles' => $roles,
'parentOptions' => $parentOptions,
];
}
private function flattenTreeForResponse(array $nodes, array $roleAssignments, ?string $parentLabel = null, int $depth = 0, array &$rows = []): array
{
foreach ($nodes as $node) {
$raw = $node;
unset($raw['children']);
$navId = (int) ($raw['id'] ?? 0);
$roles = $roleAssignments[$navId] ?? ['ids' => [], 'names' => []];
$rows[] = [
'id' => $navId,
'label' => (string) ($raw['label'] ?? ''),
'url' => $raw['url'] ?? null,
'parent_label' => $parentLabel ?? '—',
'parent_id' => isset($raw['menu_parent_id']) && (int) $raw['menu_parent_id'] !== 0
? (int) $raw['menu_parent_id']
: null,
'order' => (int) ($raw['sort_order'] ?? 0),
'enabled' => (int) ($raw['is_enabled'] ?? 0) === 1,
'target' => $raw['target'] ?? null,
'depth' => $depth,
'roles' => [
'ids' => array_values(array_unique($roles['ids'] ?? [])),
'names' => array_values(array_unique($roles['names'] ?? [])),
],
'raw' => $raw,
];
if (!empty($node['children'])) {
$this->flattenTreeForResponse(
$node['children'],
$roleAssignments,
(string) ($raw['label'] ?? ''),
$depth + 1,
$rows
);
}
}
return $rows;
}
}