<?php
/**
 * User alignment (cross-vertical) — SysAdmin + each vertical's MANAGER / SUPERVISOR.
 *
 * This page writes alignment rows for ANY vertical (a target can be aligned to AR / Billing / Coding / any future
 * vertical their role grants). SysAdmin may align anyone, anywhere. A vertical's manager / supervisor may ALSO use
 * it, but is fenced — both in the UI and on every write — to their OWN reporting team and their OWN vertical(s), so
 * a Coding manager can never align Billing / AR users (the cross-vertical escalation this page must prevent).
 *
 * Tier (Option A): a MANAGER assigns the client » process (and may pin facility » provider); a SUPERVISOR only adds
 * facility » provider WITHIN a client » process a manager already assigned. Removal mirrors it — only a manager
 * strips a client / process-level row; a supervisor removes a facility / provider within one.
 *
 * Align a user to specific client » process » facility » provider from the ONE shared master tree.
 * Flow: pick a user → the system derives the vertical(s) they work from their role → if they work more than
 * one vertical, choose which → pick the c»p»f»p. The alignment row is (user, VERTICAL, client, process,
 * facility, provider) so one person can hold different c»p»f»p per vertical. NULL at a level = "all below the
 * set level". Each vertical's work + reports read only that vertical's rows (see coding_user_can_tuple).
 */
require_once __DIR__ . '/../includes/config.php';
$user = require_login('internal');                       // route gate: sysadmin OR a vertical's access-manage / supervise
$pdo  = db();
$actor = (int) $user['id'];

/* ── Actor scope ──────────────────────────────────────────────────────────────────────────────────────
 * SysAdmin aligns ANY user across ALL verticals. A vertical's MANAGER / SUPERVISOR may also use this page,
 * but is fenced to (a) their own reporting team and (b) the vertical(s) they themselves work. Tier (Option A):
 *   - MANAGER    assigns client » process (and may also pin facility » provider).
 *   - SUPERVISOR adds only facility » provider WITHIN a client » process a manager already assigned.
 * The fences below are enforced on EVERY write; the UI just mirrors them. */
$isSysadmin = user_has_role($actor, 'sysadmin');
$team       = coding_team_scope($pdo, $actor);                   // null = unrestricted (sysadmin); else team ids incl self
$inTeam     = fn(int $id) => $team === null || in_array($id, $team, true);
$actorVerticalIds  = $isSysadmin ? array_keys(app_verticals()) : app_user_vertical_ids($actor);
$isVerticalManager = function (int $vid) use ($actor, $isSysadmin): bool {
    if ($isSysadmin) return true;
    // Resolve the vertical to its canonical role group (handles dedup-suffixed codes like 'CODING-2' and empty
    // codes by preferring the NAME). Roles are coding_manager / ar_manager / billing_manager.
    $grp = app_vertical_group_canonical(app_verticals()[$vid] ?? null);
    return $grp !== '' && user_has_role($actor, $grp . '_manager');
};
$codingVid = app_vertical_id_by_code('CODING');
if ($codingVid === null) foreach (app_verticals() as $cvId => $cvV) if (app_vertical_group_canonical($cvV) === 'coding') { $codingVid = (int) $cvId; break; }   // prod active code is 'CODING-2'
// Canonical group of a vertical id (resolves soft-deleted rows from the raw table too). Used to match a client's
// vertical to the chosen one even across dedup'd vertical rows (e.g. a client under the deleted 'CODING' vs active 'CODING-2').
$vidGroup = function (int $id) use ($pdo): string {
    $g = app_vertical_group_canonical(app_verticals()[$id] ?? null);
    if ($g === '' && $id > 0) { $rv = $pdo->prepare("SELECT code, name FROM verticals WHERE id=?"); $rv->execute([$id]); $g = app_vertical_group_canonical($rv->fetch() ?: null); }
    return $g;
};
// Active vertical id per canonical group + a canonicalizer (MIRRORS coding_alignment_tree, which keys clients by the
// ACTIVE id). Scope rows may carry a legacy/dedup'd vertical id (old 'CODING' vs active 'CODING-2') or NULL; without
// canonicalizing them, a supervisor's granted client wouldn't line up with the tree → they couldn't pick a process/
// facility for their team. Applied to the supervisor picker-prune AND the write-time covering-grant check below.
$activeByGroup = [];
foreach (app_verticals() as $vvid => $vv) { $g = app_vertical_group_canonical($vv); if ($g !== '' && !isset($activeByGroup[$g])) $activeByGroup[$g] = (int) $vvid; }
$canonVid = function (?int $id) use ($activeByGroup, $vidGroup, $codingVid): int {
    if ($id === null || $id === 0) return (int) ($activeByGroup['coding'] ?? $codingVid);   // legacy NULL = Coding
    $g = $vidGroup($id);
    return ($g !== '' && isset($activeByGroup[$g])) ? $activeByGroup[$g] : $id;
};
// All raw vertical ids (incl. soft-deleted) that share $vid's canonical group — for the covering-grant SQL check.
$groupVids = function (int $vid) use ($pdo, $vidGroup): array {
    $g = $vidGroup($vid); if ($g === '') return [$vid];
    $out = [];
    foreach ($pdo->query("SELECT id, code, name FROM verticals") as $v) if (app_vertical_group_canonical($v) === $g) $out[] = (int) $v['id'];
    if (!in_array($vid, $out, true)) $out[] = $vid;
    return $out;
};

$selUserId = (int) ($_GET['user_id'] ?? 0);

/* ----------------------------------------------------------- POST handlers */
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    csrf_check_or_die();
    $act = $_POST['action'] ?? '';
    $tid = (int) input('target_user_id');
    $back = '/admin/user-alignment.php' . ($tid ? ('?user_id=' . $tid) : '');

    if ($act === 'add') {
        $vid = (int) input('vertical_id');
        $cid = (int) input('client_id'); $pid = (int) input('process_id');
        // MULTI: one or more facilities × one or more providers (blank = all below the chosen level).
        $fids  = array_values(array_unique(array_filter(array_map('intval', (array) ($_POST['facility_id'] ?? [])))));
        $prids = array_values(array_unique(array_filter(array_map('intval', (array) ($_POST['provider_id'] ?? [])))));
        // target must be a real internal user who actually works the chosen vertical
        $tu = $pdo->prepare("SELECT 1 FROM users WHERE id=? AND user_type='internal' AND status='active'"); $tu->execute([$tid]);
        if (!$tu->fetchColumn())                 { flash('error', 'Pick a valid active internal user.'); redirect('/admin/user-alignment.php'); }
        if (!$isSysadmin && !$inTeam($tid))      { flash('error', 'You can only align users on your own reporting team.'); redirect('/admin/user-alignment.php'); }
        if (user_has_role($tid, 'sysadmin'))     { flash('error', 'SysAdmin already has full access — no alignment needed.'); redirect('/admin/user-alignment.php'); }
        if (!$vid || !app_user_in_vertical($tid, $vid)) { flash('error', 'Pick a vertical the user actually works (from their role).'); redirect($back); }
        if (!$isSysadmin && !in_array($vid, $actorVerticalIds, true)) { flash('error', 'You can only align users within a vertical you work.'); redirect($back); }
        if (!$cid)                               { flash('error', 'Pick at least a client.'); redirect($back); }
        $cvq = $pdo->prepare("SELECT vertical_id FROM clients WHERE id=? AND deleted_at IS NULL");
        $cvq->execute([$cid]); $cVid = $cvq->fetchColumn();
        // Accept the client if its vertical IS the chosen one, or canonicalizes to the same group (dedup'd vertical rows).
        $okVert = $cVid !== false && ((int) $cVid === $vid || ($vidGroup($vid) !== '' && $vidGroup($vid) === $vidGroup((int) $cVid)));
        if (!$okVert)                            { flash('error', 'That client is not in the chosen vertical.'); redirect($back); }
        if ($prids && !$fids) { flash('error', 'Choose at least one facility for those providers.'); redirect($back); }
        if ($fids && !$pid)   { flash('error', 'Choose a process for those facilities.'); redirect($back); }
        if ($pid) { $q = $pdo->prepare("SELECT 1 FROM processes WHERE id=? AND client_id=? AND deleted_at IS NULL"); $q->execute([$pid, $cid]);
            if (!$q->fetchColumn()) { flash('error', 'That process is not under the chosen client.'); redirect($back); } }
        // ── TIER (Option A · cascade mgr→supervisor→coder) ── a MANAGER assigns the client » process; a SUPERVISOR
        // then allocates a facility (+ provider) to a coder WITHIN a client » process a manager assigned to the
        // SUPERVISOR THEMSELVES (the actor) — not to the coder. So the covering grant is checked on $actor.
        if (!$isVerticalManager($vid)) {
            if (!$fids) { flash('error', 'As a supervisor you allocate a facility (and optionally a provider) within a client » process your manager assigned you. Pick at least one facility.'); redirect($back); }
            // Match the covering grant across ALL vertical-id variants of the chosen vertical's canonical group
            // (+ legacy NULL for Coding), so a client grant stored under a deduped/legacy id still counts.
            $gvids  = $groupVids($vid);
            $inSet  = implode(',', array_map('intval', $gvids));
            $nullOk = ($vidGroup($vid) === 'coding') ? ' OR s.vertical_id IS NULL' : '';
            $cov = $pdo->prepare("SELECT 1 FROM coding_user_scope s WHERE s.user_id=? AND (s.vertical_id IN ($inSet)$nullOk) AND s.client_id=?
                AND (s.process_id IS NULL OR s.process_id <=> ?) AND s.facility_id IS NULL AND s.provider_id IS NULL LIMIT 1");
            $cov->execute([$actor, $cid, $pid ?: null]);
            if (!$cov->fetchColumn()) { flash('error', 'Your manager has not assigned you this client » process yet — ask them to assign it, then you can allocate a facility within it for your coders.'); redirect($back); }
        }
        // build valid (facility, provider) pairs (a provider is kept only under a facility it actually sits in)
        $facChk = $pdo->prepare("SELECT 1 FROM coding_facilities WHERE id=? AND process_id=?");
        $prChk  = $pdo->prepare("SELECT 1 FROM coding_facility_providers WHERE facility_id=? AND provider_id=?");
        $pairs = [];
        if (!$fids) { $pairs[] = [null, null]; }                                       // client / process level — all facilities
        else foreach ($fids as $fid) {
            $facChk->execute([$fid, $pid]); if (!$facChk->fetchColumn()) continue;       // skip a facility not under this process
            if (!$prids) { $pairs[] = [$fid, null]; continue; }                          // facility, all providers
            $matched = false;
            foreach ($prids as $prid) { $prChk->execute([$fid, $prid]); if ($prChk->fetchColumn()) { $pairs[] = [$fid, $prid]; $matched = true; } }
            if (!$matched) $pairs[] = [$fid, null];                                      // none of the chosen providers sit here → facility-all
        }
        if (!$pairs) { flash('error', 'No valid facility / provider path to align.'); redirect($back); }
        $dup = $pdo->prepare("SELECT 1 FROM coding_user_scope WHERE user_id=? AND vertical_id=? AND client_id=?
            AND process_id <=> ? AND facility_id <=> ? AND provider_id <=> ? LIMIT 1");
        $ins = $pdo->prepare("INSERT INTO coding_user_scope (user_id,vertical_id,client_id,process_id,facility_id,provider_id,granted_by,created_at)
                              VALUES (?,?,?,?,?,?,?,NOW())");
        $added = 0; $skipped = 0;
        foreach ($pairs as [$fid, $prid]) {
            $dup->execute([$tid, $vid, $cid, $pid ?: null, $fid ?: null, $prid ?: null]);
            if ($dup->fetchColumn()) { $skipped++; continue; }
            $ins->execute([$tid, $vid, $cid, $pid ?: null, $fid ?: null, $prid ?: null, $actor]);
            $added++;
        }
        audit_log('user.align.add', 'coding_user_scope', null, null,
            ['user'=>$tid,'vertical'=>$vid,'client'=>$cid,'process'=>$pid,'facilities'=>$fids,'providers'=>$prids,'added'=>$added]);
        flash($added ? 'success' : 'warning', $added . ' alignment(s) added' . ($skipped ? " · {$skipped} already existed" : '') . '.');
        redirect($back);
    }

    if ($act === 'remove') {
        $sid = (int) input('scope_id');
        $row = $pdo->prepare("SELECT * FROM coding_user_scope WHERE id=?"); $row->execute([$sid]); $row = $row->fetch(PDO::FETCH_ASSOC);
        if ($row) {
            if (!$isSysadmin && !$inTeam((int) $row['user_id'])) { flash('error', 'You can only manage alignments for your own team.'); redirect('/admin/user-alignment.php'); }
            // Cross-vertical fence (mirrors the add handler): a non-sysadmin may only touch rows in a vertical THEY work —
            // otherwise a Coding lead could strip a teammate's Billing/AR facility/provider row (which skips the CP-tier check).
            $rowVid = (int) ($row['vertical_id'] ?: $codingVid);
            if (!$isSysadmin && !in_array($rowVid, $actorVerticalIds, true)) { flash('error', 'You can only manage alignments within a vertical you work.'); redirect('/admin/user-alignment.php?user_id=' . (int) $row['user_id']); }
            // Tier: only a manager may strip a client / process-level grant; a supervisor may remove a facility / provider within it.
            $isCpLevel = empty($row['facility_id']) && empty($row['provider_id']);
            if ($isCpLevel && !$isVerticalManager($rowVid)) {
                flash('error', 'Only a manager can remove a client / process alignment. You can remove a facility / provider within it.');
                redirect('/admin/user-alignment.php?user_id=' . (int) $row['user_id']);
            }
            $pdo->prepare("DELETE FROM coding_user_scope WHERE id=?")->execute([$sid]);
            audit_log('user.align.remove', 'coding_user_scope', $sid, $row, null);
            flash('success', 'Alignment removed.');
            $back = '/admin/user-alignment.php?user_id=' . (int) $row['user_id'];
        }
        redirect($back);
    }
    redirect('/admin/user-alignment.php');
}

/* ----------------------------------------------------------- view data */
$teamSql  = $team === null ? '' : (' AND id IN (' . implode(',', $team ?: [0]) . ')');   // sysadmin: all; else own team only
// Never offer the actor themselves — a manager/supervisor has full access by default and doesn't self-align.
$allUsers = $pdo->query("SELECT id, name, email, reports_to FROM users WHERE user_type='internal' AND status='active' AND id <> $actor$teamSql ORDER BY name")->fetchAll();

/* ── per-user metadata for the roster + filters (batched) ── */
$allVerticals = app_verticals();
$userIds = array_map(fn($u) => (int) $u['id'], $allUsers);
$rolesByUser = []; $alignByUser = []; $vertsByUser = []; $roleOptionSet = [];
if ($userIds) {
    $in = implode(',', $userIds);
    foreach ($pdo->query("SELECT ur.user_id, GROUP_CONCAT(DISTINCT r.name ORDER BY r.name SEPARATOR ', ') roles
                          FROM user_roles ur JOIN roles r ON r.id=ur.role_id WHERE ur.user_id IN ($in) GROUP BY ur.user_id") as $r)
        $rolesByUser[(int) $r['user_id']] = (string) $r['roles'];
    foreach ($pdo->query("SELECT user_id, COUNT(*) n FROM coding_user_scope WHERE user_id IN ($in) GROUP BY user_id") as $r)
        $alignByUser[(int) $r['user_id']] = (int) $r['n'];
    foreach ($allUsers as $u) {
        $id = (int) $u['id'];
        // show only verticals this actor may align in (sysadmin = all the user works)
        $vertsByUser[$id] = $isSysadmin ? app_user_vertical_ids($id)
                          : array_values(array_intersect(app_user_vertical_ids($id), $actorVerticalIds));
        foreach (array_filter(array_map('trim', explode(',', $rolesByUser[$id] ?? ''))) as $rt) $roleOptionSet[$rt] = true;
    }
}
$roleOptions = array_keys($roleOptionSet); sort($roleOptions);
$vertFilterIds = $isSysadmin ? array_keys($allVerticals) : $actorVerticalIds;

/* ── Reporting-tree index (G3): top level = the actor's DIRECT reports (sysadmin: org roots / orphans), each
   expandable to their reports recursively; a "(N)" badge shows each node's direct-report count. ── */
$idset = array_flip($userIds);
$childrenOf = [];                                   // parent_id => [user rows] within the accessible set
foreach ($allUsers as $u) $childrenOf[(int) ($u['reports_to'] ?? 0)][] = $u;
$treeRoots = [];
foreach ($allUsers as $u) {
    $pt = (int) ($u['reports_to'] ?? 0);
    if ($pt === 0 || $pt === $actor || !isset($idset[$pt])) $treeRoots[] = $u;   // parent absent from the accessible set → a top-level node
}
$fSort = in_array($_GET['f_sort'] ?? '', ['name', 'reports'], true) ? $_GET['f_sort'] : 'name';
$sortNodes = function (array $nodes) use ($fSort, $childrenOf) {
    usort($nodes, function ($a, $b) use ($fSort, $childrenOf) {
        if ($fSort === 'reports') { $d = count($childrenOf[(int) $b['id']] ?? []) <=> count($childrenOf[(int) $a['id']] ?? []); if ($d) return $d; }
        return strcasecmp((string) $a['name'], (string) $b['name']);
    });
    return $nodes;
};

/* ── filters ── */
$fVertical = (int) ($_GET['f_vertical'] ?? 0);
if ($fVertical && !in_array($fVertical, $vertFilterIds, true)) $fVertical = 0;
$fRole   = isset($_GET['f_role']) && in_array($_GET['f_role'], $roleOptions, true) ? (string) $_GET['f_role'] : '';
$fStatus = in_array($_GET['f_status'] ?? '', ['aligned', 'unaligned'], true) ? $_GET['f_status'] : 'all';
$fSearch = trim((string) ($_GET['f_q'] ?? ''));

$filteredUsers = array_values(array_filter($allUsers, function ($u) use ($fVertical, $fRole, $fStatus, $fSearch, $vertsByUser, $rolesByUser, $alignByUser) {
    $id = (int) $u['id'];
    if ($fVertical && !in_array($fVertical, $vertsByUser[$id] ?? [], true)) return false;
    if ($fRole !== '') { $t = array_map('trim', explode(',', $rolesByUser[$id] ?? '')); if (!in_array($fRole, $t, true)) return false; }
    $n = (int) ($alignByUser[$id] ?? 0);
    if ($fStatus === 'aligned' && $n === 0)   return false;
    if ($fStatus === 'unaligned' && $n > 0)   return false;
    if ($fSearch !== '' && stripos(($u['name'] ?? '') . ' ' . ($u['email'] ?? ''), $fSearch) === false) return false;
    return true;
}));
$linkFilters = array_filter([
    'f_vertical' => $fVertical ?: '', 'f_role' => $fRole,
    'f_status' => $fStatus === 'all' ? '' : $fStatus, 'f_q' => $fSearch,
    'f_sort' => $fSort === 'name' ? '' : $fSort,
], fn($v) => $v !== '' && $v !== 0);
// When ANY filter/search is active, fall back to a FLAT matching list; otherwise show the expandable reporting tree.
$anyFilter = ($fVertical || $fRole !== '' || $fStatus !== 'all' || $fSearch !== '');

$selUser  = null; $targetVerticalIds = []; $targetVerticalIdsAll = []; $current = []; $selUserRoles = [];
if ($selUserId) {
    foreach ($allUsers as $u) if ((int) $u['id'] === $selUserId) { $selUser = $u; break; }
    if ($selUser) {
        $targetVerticalIdsAll = app_user_vertical_ids($selUserId);
        // a non-sysadmin actor may only align this user within the vertical(s) the actor themselves works
        $targetVerticalIds = $isSysadmin ? $targetVerticalIdsAll : array_values(array_intersect($targetVerticalIdsAll, $actorVerticalIds));
        $rq = $pdo->prepare("SELECT r.name FROM user_roles ur JOIN roles r ON r.id=ur.role_id WHERE ur.user_id=? ORDER BY r.name");
        $rq->execute([$selUserId]); $selUserRoles = $rq->fetchAll(PDO::FETCH_COLUMN);
        $cur = $pdo->prepare("SELECT s.id, s.vertical_id, v.name vname, s.client_id, c.name cname, s.process_id, p.name pname,
                s.facility_id, f.name fname, s.provider_id, pr.name prname
            FROM coding_user_scope s
            LEFT JOIN verticals v ON v.id=s.vertical_id
            JOIN clients c ON c.id=s.client_id
            LEFT JOIN processes p ON p.id=s.process_id
            LEFT JOIN coding_facilities f ON f.id=s.facility_id
            LEFT JOIN coding_providers pr ON pr.id=s.provider_id
            WHERE s.user_id=? ORDER BY v.name, c.name, p.name, f.name, pr.name");
        $cur->execute([$selUserId]); $current = $cur->fetchAll();
    }
}
$verticals = app_verticals();
$tree = coding_alignment_tree($pdo);                     // by vertical → client → … ; includes facility-less clients

// Fence the picker tree to what THIS actor may assign to THIS user: only the actor-allowed verticals, and — for
// verticals where the actor is a SUPERVISOR (not a manager) — only the client » process a manager already gave this
// user. $mgrByVid tells the client side whether to keep "process = all" (manager) or pin a process (supervisor).
$mgrByVid = [];
// Manager flag is keyed by the SELECTABLE verticals (the dropdown), NOT by tree keys — otherwise a manager whose
// vertical has no clients in the tree (e.g. a vertical-id data quirk) would be mis-read as a supervisor.
foreach ($targetVerticalIds as $vid) $mgrByVid[(int) $vid] = $isVerticalManager((int) $vid) ? 1 : 0;
if ($selUser) {
    // Cascade mgr→supervisor→coder: a SUPERVISOR's assignable client»process = what a manager assigned to the
    // SUPERVISOR THEMSELVES (the actor), NOT the target coder. So $allowedCP comes from the ACTOR's own scope rows.
    $actorScopeRows = [];
    if (!$isSysadmin) { $asq = $pdo->prepare("SELECT vertical_id, client_id, process_id, facility_id, provider_id FROM coding_user_scope WHERE user_id=?"); $asq->execute([$actor]); $actorScopeRows = $asq->fetchAll(); }
    $allowedCP = [];   // [vid][cid] => true (whole client) | [pid => true] — from the ACTOR's client»process-level grants
    foreach ($actorScopeRows as $r) {
        if (!empty($r['facility_id']) || !empty($r['provider_id'])) continue;
        $rvid = $canonVid($r['vertical_id'] !== null ? (int) $r['vertical_id'] : null); $rcid = (int) $r['client_id'];   // canonical id → matches the tree keys
        if ($r['process_id'] === null)                       $allowedCP[$rvid][$rcid] = true;
        elseif (($allowedCP[$rvid][$rcid] ?? null) !== true) $allowedCP[$rvid][$rcid][(int) $r['process_id']] = true;
    }
    foreach (array_keys($tree) as $vid) {
        $vid = (int) $vid;
        if (!in_array($vid, $targetVerticalIds, true)) { unset($tree[$vid]); continue; }   // not an actor∩target vertical
        if (!empty($mgrByVid[$vid])) continue;                                             // manager → full subtree
        $av = $allowedCP[$vid] ?? [];                                                      // supervisor → prune to manager grants
        foreach (array_keys($tree[$vid]) as $cid) {
            $cid = (int) $cid;
            if (!isset($av[$cid]))   { unset($tree[$vid][$cid]); continue; }
            if ($av[$cid] === true)  continue;                                             // whole client → keep all processes
            foreach (array_keys($tree[$vid][$cid]['processes'] ?? []) as $pid)
                if (!isset($av[$cid][(int) $pid])) unset($tree[$vid][$cid]['processes'][(int) $pid]);
        }
    }
}

// The ACTOR's OWN covering grants (client»process level = facility+provider NULL) — what their manager assigned
// THEM. A supervisor may allocate a facility within these to their team; shown as a panel so it's visible (and it
// makes an empty client picker self-diagnosing: no covering grant → nothing to allocate within).
$myGrants = [];
if (!$isSysadmin) {
    $mg = $pdo->prepare("SELECT s.vertical_id, s.client_id, c.name cname, s.process_id, p.name pname
        FROM coding_user_scope s JOIN clients c ON c.id=s.client_id LEFT JOIN processes p ON p.id=s.process_id
        WHERE s.user_id=? AND s.facility_id IS NULL AND s.provider_id IS NULL ORDER BY c.name, p.name");
    $mg->execute([$actor]);
    $myGrants = $mg->fetchAll();
}

$pageTitle = 'User alignment';
require __DIR__ . '/../includes/header.php';
?>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/tom-select@2.3.1/dist/css/tom-select.bootstrap5.min.css">
<script src="https://cdn.jsdelivr.net/npm/tom-select@2.3.1/dist/js/tom-select.complete.min.js"></script><!-- loaded BEFORE the cascade script so facility/provider multi-selects init -->
<div class="page-header"><div class="container-xl"><div class="row align-items-center">
    <div class="col"><h2 class="page-title"><i class="ti ti-user-shield me-2 text-primary"></i>User alignment</h2>
        <div class="text-secondary small">Align a user to specific client » process » facility » provider from the shared master tree.
            <?= $isSysadmin
                ? 'SysAdmin — any user, any vertical.'
                : 'Manager assigns client » process; supervisor adds facility » provider within it. Your reporting team &amp; your vertical(s) only.' ?></div></div>
</div></div></div>
<div class="page-body"><div class="container-xl">

    <?php if (!$isSysadmin): ?>
    <div class="alert alert-info d-flex align-items-center py-2"><i class="ti ti-info-circle me-2"></i>
        <div class="small">You're aligning <strong>your reporting team</strong> within your vertical(s). A <strong>manager</strong> assigns the
            <strong>client » process</strong>; a <strong>supervisor</strong> then allocates a <strong>facility</strong> (and optionally a provider) within a client » process the manager already assigned.</div>
    </div>
    <!-- Your OWN covering scope (what your manager assigned YOU). A supervisor can only allocate a facility within these. -->
    <div class="card mb-3"><div class="card-body py-2">
        <div class="d-flex align-items-center mb-1"><i class="ti ti-map-pin me-2 text-azure"></i><strong class="small">Client » process assigned to YOU</strong>
            <span class="text-secondary small ms-2">— a supervisor can allocate a facility within these to their team</span></div>
        <?php if (!$myGrants): ?>
            <div class="text-danger small"><i class="ti ti-alert-triangle me-1"></i>Nothing is assigned to you yet. If you are a <strong>supervisor</strong>, ask your <strong>manager</strong> to assign a client » process to <strong>you</strong> (they pick YOU as the user on this page). A <strong>manager</strong> already has full access to their vertical and does not need this.</div>
        <?php else: ?>
            <div class="d-flex flex-wrap gap-1">
            <?php foreach ($myGrants as $g): ?>
                <span class="badge bg-azure-lt"><?= e($g['cname']) ?><?= $g['pname'] ? ' » ' . e($g['pname']) : ' » (all processes)' ?></span>
            <?php endforeach; ?>
            </div>
        <?php endif; ?>
    </div></div>
    <?php endif; ?>

    <!-- Step 1: filter + pick the user -->
    <div class="card mb-3">
        <div class="card-header"><h3 class="card-title"><i class="ti ti-users me-2"></i>1 · Find a user</h3>
            <div class="card-actions"><span class="badge bg-blue-lt"><?= count($filteredUsers) ?><?php if (count($filteredUsers) !== count($allUsers)): ?> <span class="text-secondary">of <?= count($allUsers) ?></span><?php endif; ?></span></div></div>
        <div class="card-body">
            <!-- one GET form: filters + the user picker (changing any control preserves the rest) -->
            <form method="GET" class="row g-2 align-items-end">
                <?php if (count($vertFilterIds) > 1): ?>
                <div class="col-6 col-md-auto"><label class="form-label small mb-1">Vertical</label>
                    <select name="f_vertical" class="form-select form-select-sm" onchange="this.form.submit()">
                        <option value="0">All verticals</option>
                        <?php foreach ($vertFilterIds as $vid): if (isset($allVerticals[$vid])): ?><option value="<?= (int)$vid ?>" <?= $fVertical===(int)$vid?'selected':'' ?>><?= e($allVerticals[$vid]['name']) ?></option><?php endif; endforeach; ?>
                    </select></div>
                <?php endif; ?>
                <div class="col-6 col-md-auto"><label class="form-label small mb-1">Role</label>
                    <select name="f_role" class="form-select form-select-sm" onchange="this.form.submit()">
                        <option value="">All roles</option>
                        <?php foreach ($roleOptions as $ro): ?><option value="<?= e($ro) ?>" <?= $fRole===$ro?'selected':'' ?>><?= e($ro) ?></option><?php endforeach; ?>
                    </select></div>
                <div class="col-6 col-md-auto"><label class="form-label small mb-1">Alignment</label>
                    <select name="f_status" class="form-select form-select-sm" onchange="this.form.submit()">
                        <option value="all"       <?= $fStatus==='all'?'selected':'' ?>>Any</option>
                        <option value="aligned"   <?= $fStatus==='aligned'?'selected':'' ?>>Aligned</option>
                        <option value="unaligned" <?= $fStatus==='unaligned'?'selected':'' ?>>Not aligned</option>
                    </select></div>
                <div class="col-6 col-md"><label class="form-label small mb-1">Search</label>
                    <input type="search" name="f_q" value="<?= e($fSearch) ?>" class="form-control form-control-sm" placeholder="Name or email…"></div>
                <div class="col-6 col-md-auto"><label class="form-label small mb-1">Sort</label>
                    <select name="f_sort" class="form-select form-select-sm" onchange="this.form.submit()">
                        <option value="name"    <?= $fSort==='name'?'selected':'' ?>>Name (A–Z)</option>
                        <option value="reports" <?= $fSort==='reports'?'selected':'' ?>>Most reports</option>
                    </select></div>
                <div class="col-auto"><button class="btn btn-sm btn-primary"><i class="ti ti-filter me-1"></i>Filter</button></div>
                <div class="col-auto"><a href="<?= e(url('/admin/user-alignment.php')) ?>" class="btn btn-sm btn-link text-secondary">Clear</a></div>
                <div class="col-12"><hr class="my-2"></div>
                <div class="col-md-7"><label class="form-label">Pick a user</label>
                    <select name="user_id" id="selUser" class="form-select" onchange="this.form.submit()">
                        <option value="">— choose a user —</option>
                        <?php foreach ($filteredUsers as $u): ?><option value="<?= (int)$u['id'] ?>" <?= $selUserId===(int)$u['id']?'selected':'' ?>><?= e($u['name']) ?> · <?= e($u['email']) ?></option><?php endforeach; ?>
                    </select></div>
            </form>
        </div>
        <!-- Reporting-team tree: direct reports at top, click ▸ to expand indirect reports. A filter/search shows a flat matching list. -->
        <?php
        // One <td> cell renderer shared by tree + flat rows.
        $uaCells = function (array $u) use ($vertsByUser, $allVerticals, $rolesByUser, $alignByUser, $selUserId, $linkFilters) {
            $id = (int) $u['id']; $vn = array_map(fn($v) => $allVerticals[$v]['name'] ?? '?', $vertsByUser[$id] ?? []); $n = (int) ($alignByUser[$id] ?? 0);
            ob_start(); ?>
            <td class="small"><?php foreach ($vn as $x): ?><span class="badge bg-purple-lt me-1"><?= e($x) ?></span><?php endforeach; ?><?= $vn?'':'<span class="text-secondary">—</span>' ?></td>
            <td class="small text-secondary"><?= e($rolesByUser[$id] ?? '') ?></td>
            <td class="text-end"><span class="badge bg-<?= $n?'green':'secondary' ?>-lt"><?= $n ?></span></td>
            <td><a href="<?= e(url('/admin/user-alignment.php?' . http_build_query(array_merge($linkFilters, ['user_id'=>$id])))) ?>" class="btn btn-sm btn-<?= $selUserId===$id?'primary':'outline-primary' ?>"><?= $selUserId===$id?'Selected':'Select' ?></a></td>
            <?php return ob_get_clean();
        };
        // Recursive tree-node renderer: a row + its (hidden-until-expanded) descendants. $seen guards a reports_to cycle.
        $seen = [];
        $renderNode = function (array $u, int $depth) use (&$renderNode, &$seen, $childrenOf, $sortNodes, $uaCells, $selUserId) {
            $id = (int) $u['id']; if (isset($seen[$id]) || $depth > 20) return; $seen[$id] = true;
            $kids = $sortNodes($childrenOf[$id] ?? []); $nk = count($kids); ?>
            <tr class="ua-node <?= $selUserId===$id?'table-active':'' ?>" data-uid="<?= $id ?>" data-parent="<?= (int)($u['reports_to'] ?? 0) ?>" <?= $depth>0?'style="display:none"':'' ?>>
                <td><div class="d-flex align-items-center" style="padding-left:<?= $depth*22 ?>px">
                    <?php if ($nk): ?><button type="button" class="btn btn-sm btn-ghost-secondary p-0 me-1 ua-caret" data-uid="<?= $id ?>" style="width:20px" title="Show / hide reports"><i class="ti ti-chevron-right"></i></button>
                    <?php else: ?><span class="d-inline-block me-1" style="width:20px"></span><?php endif; ?>
                    <div><div class="fw-medium"><?= e($u['name']) ?><?php if ($nk): ?> <span class="badge bg-blue-lt ms-1" title="Direct reports">(<?= $nk ?>)</span><?php endif; ?></div>
                        <div class="text-secondary small"><?= e($u['email']) ?></div></div>
                </div></td>
                <?= $uaCells($u) ?>
            </tr>
            <?php foreach ($kids as $k) $renderNode($k, $depth + 1);
        };
        ?>
        <div class="table-responsive"><table class="table table-vcenter card-table">
            <thead><tr><th>User</th><th>Vertical(s)</th><th>Role(s)</th><th class="text-end">Alignments</th><th class="w-1"></th></tr></thead>
            <tbody id="uaTree">
            <?php if (!$allUsers): ?><tr><td colspan="5" class="text-center text-secondary py-4">No users on your reporting team yet.</td></tr>
            <?php elseif ($anyFilter): // filter/search active → flat matching list ?>
                <?php if (!$filteredUsers): ?><tr><td colspan="5" class="text-center text-secondary py-4">No users match these filters. <a href="<?= e(url('/admin/user-alignment.php')) ?>">Clear</a>.</td></tr>
                <?php else: foreach ($sortNodes($filteredUsers) as $u): $id = (int) $u['id']; $nk = count($childrenOf[$id] ?? []); ?>
                    <tr class="<?= $selUserId===$id?'table-active':'' ?>">
                        <td><div class="fw-medium"><?= e($u['name']) ?><?php if ($nk): ?> <span class="badge bg-blue-lt ms-1" title="Direct reports">(<?= $nk ?>)</span><?php endif; ?></div><div class="text-secondary small"><?= e($u['email']) ?></div></td>
                        <?= $uaCells($u) ?>
                    </tr>
                <?php endforeach; endif; ?>
            <?php else: // no filter → the expandable reporting tree ?>
                <?php if (!$treeRoots): ?><tr><td colspan="5" class="text-center text-secondary py-4">No direct reports.</td></tr>
                <?php else: foreach ($sortNodes($treeRoots) as $u) $renderNode($u, 0); endif; ?>
            <?php endif; ?>
            </tbody>
        </table></div>
    </div>
    <script>
    (function(){
        var tb=document.getElementById('uaTree'); if(!tb) return;
        function sub(uid, show){
            tb.querySelectorAll('tr.ua-node[data-parent="'+uid+'"]').forEach(function(row){
                row.style.display = show ? '' : 'none';
                if(!show){ var c=row.querySelector('.ua-caret'); if(c){ c.dataset.open=''; var i=c.querySelector('i'); if(i) i.className='ti ti-chevron-right'; } sub(row.dataset.uid, false); }
            });
        }
        tb.addEventListener('click', function(e){
            var btn=e.target.closest('.ua-caret'); if(!btn) return;
            var open=btn.dataset.open==='1'; btn.dataset.open=open?'':'1';
            var i=btn.querySelector('i'); if(i) i.className=open?'ti ti-chevron-right':'ti ti-chevron-down';
            sub(btn.dataset.uid, !open);
        });
    })();
    </script>

    <?php if ($selUser): ?>
        <?php if (!$targetVerticalIds): ?>
            <?php if (!$targetVerticalIdsAll): ?>
                <div class="alert alert-warning"><i class="ti ti-alert-triangle me-1"></i><strong><?= e($selUser['name']) ?></strong> has no vertical-bearing role yet — assign them a role (coding / AR / billing / …) first, then align them here.
                    <div class="small mt-1">Current role(s): <?= $selUserRoles ? e(implode(', ', $selUserRoles)) : '<em>none</em>' ?>.</div></div>
            <?php else: ?>
                <div class="alert alert-warning"><i class="ti ti-alert-triangle me-1"></i><strong><?= e($selUser['name']) ?></strong> works
                    <?= e(implode(', ', array_map(fn($v) => $verticals[$v]['name'] ?? '?', $targetVerticalIdsAll))) ?>, which is outside the vertical(s) you manage —
                    only a manager / supervisor of that vertical (or SysAdmin) can align them.</div>
            <?php endif; ?>
        <?php else: ?>
        <!-- Step 2 + 3: vertical (from their role) + the c»p»f»p -->
        <div class="card mb-3">
            <div class="card-header"><h3 class="card-title">Add alignment for <strong><?= e($selUser['name']) ?></strong></h3></div>
            <div class="card-body">
                <form method="POST" class="row g-2 align-items-end">
                    <?= csrf_field() ?><input type="hidden" name="action" value="add"><input type="hidden" name="target_user_id" value="<?= $selUserId ?>">
                    <div class="col-md-2"><label class="form-label required">2 · Vertical</label>
                        <select name="vertical_id" id="al_vertical" class="form-select" required <?= count($targetVerticalIds)===1?'':'' ?>>
                            <?php if (count($targetVerticalIds) > 1): ?><option value="">— vertical —</option><?php endif; ?>
                            <?php foreach ($targetVerticalIds as $vid): if(isset($verticals[$vid])): ?><option value="<?= (int)$vid ?>"><?= e($verticals[$vid]['name']) ?></option><?php endif; endforeach; ?>
                        </select></div>
                    <div class="col-md-2"><label class="form-label required">Client</label>
                        <select name="client_id" id="al_client" class="form-select" required><option value="">— client —</option></select></div>
                    <div class="col-md-2"><label class="form-label">Process <span class="text-secondary small">(all)</span></label>
                        <select name="process_id" id="al_process" class="form-select"><option value="">— all —</option></select></div>
                    <div class="col-md-2"><label class="form-label">Facility <span class="text-secondary small">(one+ · blank=all)</span></label>
                        <select name="facility_id[]" id="al_facility" class="form-select" multiple></select></div>
                    <div class="col-md-2"><label class="form-label">Provider <span class="text-secondary small">(one+ · blank=all)</span></label>
                        <select name="provider_id[]" id="al_provider" class="form-select" multiple></select></div>
                    <div class="col-md-2"><button class="btn btn-primary w-100"><i class="ti ti-plus me-1"></i>Align</button></div>
                    <div class="col-12">
                        <div class="form-hint" id="al_hint_mgr">Leave Process / Facility / Provider as “all” to grant everything beneath the level you chose.</div>
                        <div class="form-hint text-orange d-none" id="al_hint_sup"><i class="ti ti-info-circle me-1"></i>Supervisor scope — Client » Process are limited to what your manager assigned <strong>you</strong>; pick a <strong>Facility</strong> (Provider optional) within one to give this coder.</div>
                        <div class="form-hint text-danger d-none" id="al_hint_empty"></div>
                    </div>
                </form>
            </div>
        </div>

        <!-- current alignments -->
        <div class="card">
            <div class="card-header"><h3 class="card-title">Current alignments</h3><span class="badge bg-blue-lt ms-2"><?= count($current) ?></span></div>
            <div class="table-responsive"><table class="table table-vcenter card-table">
                <thead><tr><th>Vertical</th><th>Client</th><th>Process</th><th>Facility</th><th>Provider</th><th class="w-1"></th></tr></thead>
                <tbody>
                <?php if (!$current): ?><tr><td colspan="6" class="text-center text-secondary py-4">No alignments yet — add one above.</td></tr>
                <?php else: foreach ($current as $r): ?>
                    <tr>
                        <td><span class="badge bg-purple-lt"><?= e($r['vname'] ?? '—') ?></span></td>
                        <td><strong><?= e($r['cname']) ?></strong></td>
                        <td class="small"><?= e($r['pname'] ?? '— all —') ?></td>
                        <td class="small"><?= e($r['fname'] ?? '— all —') ?></td>
                        <td class="small"><?= e($r['prname'] ?? '— all —') ?></td>
                        <td><form method="POST" class="d-inline" onsubmit="return confirm('Remove this alignment?');">
                            <?= csrf_field() ?><input type="hidden" name="action" value="remove"><input type="hidden" name="scope_id" value="<?= (int)$r['id'] ?>">
                            <button class="btn btn-sm btn-ghost-danger"><i class="ti ti-trash"></i></button></form></td>
                    </tr>
                <?php endforeach; endif; ?>
                </tbody>
            </table></div>
        </div>
        <script>
        (function(){
            // TREE is keyed by VERTICAL id → client id → {name, processes}. The client list follows the chosen vertical,
            // so only that vertical's clients show (no cross-vertical same-name duplicates) and facility-less clients appear.
            var TREE = <?= json_encode($tree, JSON_HEX_TAG|JSON_HEX_AMP|JSON_HEX_QUOT|JSON_HEX_APOS) ?>;
            var elV=document.getElementById('al_vertical'), elC=document.getElementById('al_client'),
                elP=document.getElementById('al_process'), elF=document.getElementById('al_facility'), elPr=document.getElementById('al_provider');
            function opt(v,t){ var o=document.createElement('option'); o.value=v; o.textContent=t; return o; }
            // Facility + Provider are MULTI-select (TomSelect). Init once; fill() drives their options via the TS API.
            var facTS = (typeof TomSelect!=='undefined') ? new TomSelect(elF,{maxOptions:3000,plugins:['remove_button'],placeholder:'— all facilities —'}) : null;
            var prTS  = (typeof TomSelect!=='undefined') ? new TomSelect(elPr,{maxOptions:3000,plugins:['remove_button'],placeholder:'— all providers —'}) : null;
            function fill(el, map, ph){
                if(el.tomselect){ var ts=el.tomselect; ts.clear(true); ts.clearOptions();
                    Object.keys(map||{}).sort(function(a,b){ return String(map[a].name||map[a]).localeCompare(String(map[b].name||map[b])); })
                        .forEach(function(k){ ts.addOption({value:String(k), text:String(map[k].name||map[k])}); });
                    ts.refreshOptions(false); return; }
                el.innerHTML=''; el.appendChild(opt('', ph));
                Object.keys(map||{}).sort(function(a,b){ return String(map[a].name||map[a]).localeCompare(String(map[b].name||map[b])); })
                    .forEach(function(k){ el.appendChild(opt(k, map[k].name||map[k])); }); }
            function vClients(){ return TREE[elV.value] || {}; }
            function selFacIds(){ return elF.tomselect ? elF.tomselect.getValue() : (elF.value?[elF.value]:[]); }
            function facUnionProviders(){ var c=vClients()[elC.value], p=c&&c.processes[elP.value]; if(!p) return {};
                var union={}; (selFacIds()||[]).forEach(function(fid){ var f=p.facilities[fid]; if(f&&f.providers){ Object.keys(f.providers).forEach(function(pk){ union[pk]=f.providers[pk]; }); } }); return union; }
            function rebuildProviders(){ fill(elPr, facUnionProviders(), ''); }
            // ISMGR[vid] = 1 when the actor is a MANAGER of that vertical (full tree); 0 = SUPERVISOR (pruned to manager grants).
            var ISMGR = <?= json_encode((object) $mgrByVid, JSON_HEX_TAG|JSON_HEX_APOS) ?>;
            function isMgr(){ return !!ISMGR[String(elV.value)]; }
            function autoOne(el){ var o=[].slice.call(el.options).filter(function(x){return x.value;}); if(o.length===1){ el.value=o[0].value; return true; } return false; }
            function syncHint(){
                var v = elV.value, mgr = !!(v && isMgr()), empty = !!v && Object.keys(vClients()).length === 0;
                var hm=document.getElementById('al_hint_mgr'), hs=document.getElementById('al_hint_sup'), he=document.getElementById('al_hint_empty');
                if(he){ if(empty){ he.innerHTML = mgr
                        ? '<i class="ti ti-alert-triangle me-1"></i>No clients are mapped to this vertical yet — add one under Clients / master data, then align here.'
                        : '<i class="ti ti-alert-triangle me-1"></i>Your manager has not assigned you any client » process in this vertical yet — once they do, you can allocate a facility within it for your coders.'; }
                    he.classList.toggle('d-none', !empty); }
                if(hm) hm.classList.toggle('d-none', !(mgr && !empty));     // manager hint only when there are clients to pick
                if(hs) hs.classList.toggle('d-none', !(v && !mgr && !empty)); // supervisor hint only when there are clients to pick
            }
            elV.onchange=function(){ fill(elC, vClients(), '— client —'); fill(elP,{},'— all —'); fill(elF,{},''); fill(elPr,{},''); syncHint();
                if(autoOne(elC)) elC.onchange(); };                       // lone client → pre-select (supervisor's pre-set feel)
            elC.onchange=function(){ var c=vClients()[elC.value]; fill(elP, c?c.processes:{}, '— all —'); fill(elF,{},''); fill(elPr,{},'');
                if(!isMgr() && autoOne(elP)) elP.onchange(); };          // supervisor only: lone process → pre-select (never narrows a manager's "all")
            elP.onchange=function(){ var c=vClients()[elC.value], p=c&&c.processes[elP.value]; fill(elF, p?p.facilities:{}, ''); fill(elPr,{},''); };
            if(facTS){ facTS.on('change', rebuildProviders); } else { elF.onchange=rebuildProviders; }
            elV.onchange();   // populate clients for the initial vertical (auto-selected when the user has just one)
        })();
        </script>
        <?php endif; ?>
    <?php endif; ?>
</div></div>
<script>(function(){ if(typeof TomSelect==='undefined') return; var el=document.getElementById('selUser'); if(el&&!el.tomselect) new TomSelect(el,{maxOptions:5000,persist:false,placeholder:'Type a name or email…'}); })();</script>
<?php require __DIR__ . '/../includes/footer.php'; ?>
