Files
ADconsle/app/Http/Controllers/Admin/OrgChartController.php
T
kzbkowskiandClaude Sonnet 5 1126d96271
CI / PHP 8.2 (push) Failing after 2m46s
CI / PHP 8.3 (push) Failing after 59s
CI / PHP 8.4 (push) Failing after 1m7s
ADconsole v1.0.0
First public release.

- docs: document the audit log, user offboarding, CSV export and the
  "require a reason for destructive actions" setting across README, docs/
  and CLAUDE/AGENTS.md; note retention for panel_audit
- docs: fill the publishing placeholders (repo URL, security contact,
  issue-template links) and add a semantic-versioning policy
- move agent/contributor context into CLAUDE/ (AGENTS.md + a README);
  root CLAUDE.md is now a thin @-import pointer
- add a favicon (shield-lock, matching the login screen) to the login
  page and the app layout
- CHANGELOG: promote [Unreleased] to [1.0.0]; keep the earlier internal
  build as [0.1.0]

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UfuvfwexztdS7LDpGuKsuK
2026-09-08 23:14:39 +02:00

153 lines
5.3 KiB
PHP

<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Ldap\User;
use App\Services\LdapAttributeHelper;
use Illuminate\Support\Facades\Cache;
use Illuminate\View\View;
class OrgChartController extends Controller
{
/**
* Graficzna struktura organizacyjna z relacji manager -> directReports.
*
* Do wykresu trafiają WYŁĄCZNIE aktywne konta, które faktycznie są w
* hierarchii: mają przypisanego przełożonego LUB są czyimś przełożonym.
* Konta wyłączone oraz konta bez żadnej relacji są pomijane całkowicie.
*
* Cały zbiór pobieramy jednym zapytaniem (paged control) i budujemy drzewo
* w PHP. Wynik w cache pliku na 10 minut ("Przelicz" czyści cache).
*/
public function index(): View
{
$data = Cache::store('file')->remember('org_chart:v3', 600, fn () => $this->build());
return view('admin.org.index', [
'flat' => $data['flat'],
'stats' => $data['stats'],
'refreshed_at' => Cache::store('file')->get('org_chart:v3_ts', now()->format('Y-m-d H:i')),
]);
}
public function refresh()
{
Cache::store('file')->forget('org_chart:v3');
Cache::store('file')->forget('org_chart:v3_ts');
return redirect()->route('org.index');
}
/**
* Buduje PŁASKĄ listę węzłów (id / parentId) dla biblioteki d3-org-chart.
* Jeden syntetyczny korzeń "__root__" spina osoby bez przełożonego.
*
* @return array{flat: list<array<string,mixed>>, stats: array<string,int>}
*/
protected function build(): array
{
$users = User::query()
->in(config('ldap.defaults.user_ou'))->recursive()
->select(['cn', 'samaccountname', 'title', 'department', 'manager', 'directReports', 'userAccountControl', 'distinguishedName'])
->orderBy('cn')
->paginate(500);
// Węzły kwalifikujące się do wykresu: aktywne + faktycznie w hierarchii.
$byDn = []; // dn(lower) => sam
$nodes = []; // sam => dane węzła
foreach ($users as $u) {
$uac = (int) ($u->getFirstAttribute('useraccountcontrol') ?? 0);
if ($uac & LdapAttributeHelper::UAC_ACCOUNTDISABLE) {
continue; // tylko aktywni
}
$managerDn = trim((string) $u->getFirstAttribute('manager'));
$reports = array_filter(array_map('trim', $u->getAttribute('directreports') ?? []));
if ($managerDn === '' && count($reports) === 0) {
continue; // poza hierarchią - nie pokazujemy wcale
}
$sam = $u->getFirstAttribute('samaccountname');
$byDn[strtolower(trim((string) $u->getDn()))] = $sam;
$nodes[$sam] = [
'sam' => $sam,
'cn' => $u->getFirstAttribute('cn') ?: $sam,
'title' => $u->getFirstAttribute('title'),
'department' => $u->getFirstAttribute('department'),
'manager_dn' => $managerDn !== '' ? strtolower($managerDn) : null,
];
}
// parentId = sam przełożonego, jeśli jest w zbiorze; inaczej korzeń.
$parentOf = [];
foreach ($nodes as $sam => $n) {
$pdn = $n['manager_dn'];
$parentOf[$sam] = ($pdn && isset($byDn[$pdn]) && $byDn[$pdn] !== $sam)
? $byDn[$pdn]
: '__root__';
}
// Rozbijanie cykli manager -> ... -> ten sam (d3-org-chart tego nie zniesie).
foreach (array_keys($parentOf) as $sam) {
$seen = [$sam => true];
$cur = $parentOf[$sam];
$guard = 0;
while ($cur !== '__root__' && $guard++ < 100) {
if (isset($seen[$cur])) {
$parentOf[$sam] = '__root__';
break;
}
$seen[$cur] = true;
$cur = $parentOf[$cur] ?? '__root__';
}
}
$childCount = array_count_values(array_values($parentOf)); // parent sam => liczba dzieci
$flat = [[
'id' => '__root__',
'parentId' => '',
'name' => config('ldap.defaults.domain_label'),
'title' => __('org.top_level'),
'department' => null,
'sam' => null,
'root' => true,
]];
$cmp = fn ($a, $b) => strcasecmp((string) $a['name'], (string) $b['name']);
$people = [];
foreach ($nodes as $sam => $n) {
$people[] = [
'id' => $sam,
'parentId' => $parentOf[$sam],
'name' => $n['cn'],
'title' => $n['title'],
'department' => $n['department'],
'sam' => $sam,
];
}
usort($people, $cmp);
$flat = array_merge($flat, $people);
$managers = 0;
foreach ($childCount as $parentSam => $count) {
if ($parentSam !== '__root__' && $count > 0) {
$managers++;
}
}
Cache::store('file')->put('org_chart:v3_ts', now()->format('Y-m-d H:i'), 600);
return [
'flat' => $flat,
'stats' => [
'people' => count($nodes),
'managers' => $managers,
'top' => $childCount['__root__'] ?? 0,
],
];
}
}