Files
alrahma_sunday_school_api/app/Services/Navigation/NavBuilderService.php
T
root e0dfc3ec82
API CI/CD / Validate (composer + pint) (push) Successful in 3m15s
API CI/CD / Test (PHPUnit) (push) Failing after 5m4s
API CI/CD / Build frontend assets (push) Successful in 1m3s
API CI/CD / Security audit (push) Failing after 49s
API CI/CD / Deploy to shared hosting (PHP) (push) Has been skipped
Fixed many feature failures around preferences, route coverage, administrator enrollment, assignment section names, attendance tracking controller access, finance PDF generation, and finance notification logging.
2026-07-07 21:26:47 -04:00

270 lines
8.7 KiB
PHP

<?php
namespace App\Services\Navigation;
use App\Models\NavItem;
use App\Models\Role;
use App\Models\RoleNavItem;
use Illuminate\Support\Facades\DB;
class NavBuilderService
{
public function __construct(private NavbarService $navbarService) {}
public function getAdminPayload(): array
{
$all = NavItem::query()
->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 () use ($orders): void {
foreach ($orders as $key => $item) {
if (is_array($item)) {
$id = (int) ($item['id'] ?? 0);
$sortOrder = (int) ($item['sort_order'] ?? 0);
} else {
$id = (int) $key;
$sortOrder = (int) $item;
}
if ($id > 0) {
NavItem::query()->whereKey($id)->update(['sort_order' => $sortOrder]);
}
}
});
$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);
}
}