<?php
/**
 * Coder access — the single DATA-SCOPE (alignment) controller for the Coding vertical.  coding-access-v2
 *
 * Each coder is aligned to a SPECIFIC client » process » facility (provider stays "all").
 *   - Manager / sysadmin : assign a NEW client (» process » facility); bulk re-align many coders at once.
 *   - Supervisor         : add a process » facility WITHIN a client the coder already works.
 *   - Removal            : a supervisor removes a process/facility within their team; only a manager
 *                          (or sysadmin) strips a whole-client alignment.
 * Team-scoped: a supervisor/manager only sees/affects their reporting team; sysadmin sees everyone.
 *
 * Replaces the old 3-page sprawl (the access-control.php "vertical" tab + coding-bulk-access.php).
 * Enforcement is unchanged: coding_user_can_tuple() gates upload; coding_accessible_tree() the upload picker.
 * (2026-06-18 access-control rebuild.)
 */
require_once __DIR__ . '/../includes/config.php';
$user = require_login('internal');                         // route gate: coding.access.manage (+ sysadmin)
$pdo  = db();
$uid  = (int) $user['id'];

$isSysadmin     = user_has_role($uid, 'sysadmin');
$isManagerGrant = $isSysadmin || user_has_role($uid, 'coding_manager');   // may assign a NEW client + bulk

$team   = coding_team_scope($pdo, $uid);                   // null = unrestricted (sysadmin)
$inTeam = fn(int $id) => $team === null || in_array($id, $team, true);

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

/* ─────────────────────────────────────── POST handlers ─────────────────────────────────────── */
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    csrf_check_or_die();
    $act = $_POST['action'] ?? '';

    /* ── Assign ONE coder a client » process » facility ── */
    if ($act === 'add_scope' && $targetId) {
        if (!$inTeam($targetId))                  { flash('error', 'You can only manage access for your own team members.'); redirect('/admin/coding-access.php'); }
        if (user_has_role($targetId, 'sysadmin')) { flash('error', 'SysAdmin already has full access — no alignment needed.'); redirect('/admin/coding-access.php'); }
        $cid = (int) input('client_id');
        $pid = (int) input('process_id')  ?: null;
        $fid = (int) input('facility_id') ?: null;
        $prid = (int) input('provider_id') ?: null;
        if (!$cid) { flash('error', 'Choose at least a client.'); redirect('/admin/coding-access.php?user_id=' . $targetId); }
        if ($prid && !$fid) { flash('error', 'Pick a facility before a provider.'); redirect('/admin/coding-access.php?user_id=' . $targetId); }
        $codV = app_vertical_id_by_code('CODING');   // active CODING id stamped on new rows (NULL stays NULL = legacy coding, never 0)

        // ── TIER RULE ── the MANAGER assigns the client » process; a SUPERVISOR then allocates only the FACILITY
        // (+ provider) WITHIN a client » process the coder already works. So a supervisor must pick a facility, and
        // the coder must already hold a manager grant covering that client » process (an exact-process row, or a
        // whole-client row — i.e. one with no facility pinned). Assigning a client / process stays a manager action.
        if (!$isManagerGrant) {                                  // gate on the ACTOR's tier only — a supervisor is always restricted
            if (!$fid) {
                flash('error', 'As a supervisor you allocate a facility (and optionally a provider). The client & process come from the manager’s assignment — pick a facility under one.');
                redirect('/admin/coding-access.php?user_id=' . $targetId);
            }
            // The covering grant must be a PURE client»process row the manager gave (no facility AND no provider pinned),
            // so a supervisor can only deepen an unpinned manager grant — never one already scoped to a provider/facility.
            $has = $pdo->prepare("SELECT 1 FROM coding_user_scope WHERE user_id=? AND client_id=?
                AND (process_id IS NULL OR process_id <=> ?) AND facility_id IS NULL AND provider_id IS NULL
                AND " . coding_vertical_scope_sql('vertical_id') . " LIMIT 1");
            $has->execute([$targetId, $cid, $pid]);
            if (!$has->fetchColumn()) {
                flash('error', 'A manager must first assign this client » process to the coder; then you can allocate a facility within it.');
                redirect('/admin/coding-access.php?user_id=' . $targetId);
            }
        }

        // Dedupe (NULL-aware across all four levels), within the CODING vertical only.
        $dup = $pdo->prepare("SELECT id FROM coding_user_scope WHERE user_id=? AND " . coding_vertical_scope_sql('vertical_id') . " AND client_id=?
            AND process_id <=> ? AND facility_id <=> ? AND provider_id <=> ? LIMIT 1");
        $dup->execute([$targetId, $cid, $pid, $fid, $prid]);
        if (!$dup->fetchColumn()) {
            $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())")->execute([$targetId, $codV, $cid, $pid, $fid, $prid, $uid]);
            audit_log('coding.scope.grant', 'user', $targetId, null, ['client'=>$cid,'process'=>$pid,'facility'=>$fid,'provider'=>$prid]);
            flash('success', 'Access assigned.');
        } else {
            flash('error', 'That access is already assigned.');
        }
        redirect('/admin/coding-access.php?user_id=' . $targetId);
    }

    /* ── Remove ONE alignment row ── */
    if ($act === 'remove_scope') {
        $id  = (int) input('id');
        $row = $pdo->prepare("SELECT * FROM coding_user_scope WHERE id=?"); $row->execute([$id]); $row = $row->fetch();
        if ($row) {
            if (!$inTeam((int) $row['user_id'])) { flash('error', 'You can only manage access for your own team members.'); redirect('/admin/coding-access.php'); }
            // Tier: only a manager/sysadmin may strip a WHOLE-CLIENT alignment; a supervisor may remove a
            // process/facility row within it.
            $isWholeClient = empty($row['process_id']) && empty($row['facility_id']) && empty($row['provider_id']);
            if ($isWholeClient && !$isManagerGrant) {
                flash('error', 'Only a manager can remove a whole-client alignment. You can remove a process / facility within it.');
                redirect('/admin/coding-access.php?user_id=' . (int) $row['user_id']);
            }
            $pdo->prepare("DELETE FROM coding_user_scope WHERE id=?")->execute([$id]);
            audit_log('coding.scope.revoke', 'user', (int) $row['user_id'], $row, null);
            flash('success', 'Access removed.');
            $targetId = (int) $row['user_id'];
        }
        redirect('/admin/coding-access.php?user_id=' . $targetId);
    }

    /* ── Allocation summary: ASSIGN an unallocated provider to a coder (mirrors add_scope, provider-level) ── */
    /* Rebuild the summary's filter view + anchor so the round-trip lands back where the user was. */
    $allocReturn = function () {
        parse_str((string) input('ret'), $rq);
        $keep = array_intersect_key($rq, array_flip(['al_client','al_process','al_facility','al_provider','al_coder','user_id']));
        return '/admin/coding-access.php' . ($keep ? '?' . http_build_query($keep) : '') . '#allocSummary';
    };
    if ($act === 'al_assign') {
        $coderId = (int) input('coder_id');
        $cid  = (int) input('client_id');
        $pid  = (int) input('process_id')  ?: null;
        $fid  = (int) input('facility_id') ?: null;
        $prid = (int) input('provider_id') ?: null;
        if (!$coderId || !$cid || !$fid || !$prid)   { flash('error', 'Pick a coder to assign this provider to.'); redirect($allocReturn()); }
        if (!$inTeam($coderId))                       { flash('error', 'You can only assign to your own team members.'); redirect($allocReturn()); }
        if (user_has_role($coderId, 'sysadmin'))      { flash('error', 'SysAdmin already has full access — no alignment needed.'); redirect($allocReturn()); }
        // Supervisor tier rule: may only deepen a client » process the coder already holds via a manager grant.
        if (!$isManagerGrant) {
            $has = $pdo->prepare("SELECT 1 FROM coding_user_scope WHERE user_id=? AND client_id=?
                AND (process_id IS NULL OR process_id <=> ?) AND facility_id IS NULL AND provider_id IS NULL
                AND " . coding_vertical_scope_sql('vertical_id') . " LIMIT 1");
            $has->execute([$coderId, $cid, $pid]);
            if (!$has->fetchColumn()) { flash('error', 'A manager must first assign this client » process to the coder; then you can allocate a provider within it.'); redirect($allocReturn()); }
        }
        $codV = app_vertical_id_by_code('CODING');
        $dup = $pdo->prepare("SELECT id FROM coding_user_scope WHERE user_id=? AND " . coding_vertical_scope_sql('vertical_id') . "
            AND client_id=? AND process_id <=> ? AND facility_id <=> ? AND provider_id <=> ? LIMIT 1");
        $dup->execute([$coderId, $cid, $pid, $fid, $prid]);
        if (!$dup->fetchColumn()) {
            $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())")->execute([$coderId, $codV, $cid, $pid, $fid, $prid, $uid]);
            audit_log('coding.scope.grant', 'user', $coderId, null, ['client'=>$cid,'process'=>$pid,'facility'=>$fid,'provider'=>$prid,'via'=>'alloc_summary']);
            flash('success', 'Provider assigned to the coder.');
        } else { flash('error', 'That provider is already assigned to this coder.'); }
        redirect($allocReturn());
    }

    /* ── Allocation summary: DELETE (unlink) a provider from a facility (master data — manager / sysadmin only) ── */
    if ($act === 'al_unlink') {
        if (!$isManagerGrant) { flash('error', 'Only a manager can remove a provider from a facility.'); redirect($allocReturn()); }
        $fid  = (int) input('facility_id');
        $prid = (int) input('provider_id');
        if (!$fid || !$prid) { flash('error', 'Nothing to remove.'); redirect($allocReturn()); }
        try { coding_unlink_provider($pdo, $fid, $prid, $uid); flash('success', 'Provider removed from the facility (past charts are kept).'); }
        catch (Throwable $e) { flash('error', $e->getMessage()); }
        redirect($allocReturn());
    }

    /* ── Bulk re-align: MANY coders × one client » process » facility (manager / sysadmin only) ── */
    if ($act === 'bulk_assign') {
        if (!$isManagerGrant) { flash('error', 'Only a manager can bulk-assign access.'); redirect('/admin/coding-access.php'); }
        $userIds = array_values(array_filter(array_map('intval', (array) ($_POST['user_ids'] ?? []))));
        if ($team !== null) $userIds = array_values(array_filter($userIds, fn($id) => in_array($id, $team, true)));   // never trust the posted list
        $userIds = array_values(array_filter($userIds, fn($id) => !user_has_role($id, 'sysadmin')));                  // scope rows are a no-op for sysadmins
        $sel = [
            'clients'    => (array) ($_POST['client_ids'] ?? []),
            'processes'  => (array) ($_POST['process_ids'] ?? []),
            'facilities' => (array) ($_POST['facility_ids'] ?? []),
            'providers'  => (array) ($_POST['provider_ids'] ?? []),
        ];
        if (!$userIds)                               { flash('error', 'Pick at least one coder from your team.'); redirect('/admin/coding-access.php'); }
        if (!array_filter($sel, fn($a)=>!empty($a))) { flash('error', 'Pick at least one client / process / facility / provider.'); redirect('/admin/coding-access.php'); }
        try {
            if (!empty($_POST['replace'])) {
                // Replace wipes only the targets' CODING-vertical alignment rows (vertical_id = a Coding-code row;
                // legacy NULL = Coding) — NEVER their AR/Billing rows. Master data is shared now, so fence by the
                // ROW's vertical (matched by code, robust to id changes), not by the client's vertical.
                $inU = implode(',', array_fill(0, count($userIds), '?'));
                $st  = $pdo->prepare("DELETE FROM coding_user_scope WHERE user_id IN ($inU) AND " . coding_vertical_scope_sql('vertical_id'));
                $st->execute($userIds);
                if ($st->rowCount()) audit_log('coding.scope.replace', 'coding_user_scope', null, null, ['users'=>count($userIds),'removed'=>$st->rowCount()]);
            }
            $n = coding_bulk_grant_scope($pdo, $userIds, $sel, $uid);   // resolves parents, dedupes, self-audits new rows
            audit_log('coding.scope.bulk_grant', 'coding_user_scope', null, null, ['users'=>count($userIds),'rows'=>$n,'replace'=>!empty($_POST['replace'])]);
            flash($n ? 'success' : 'warning', $n
                ? "Assigned $n access row(s) to " . count($userIds) . ' coder(s)' . (!empty($_POST['replace']) ? ' — prior Coding access replaced.' : '.')
                : 'Nothing new assigned — the selection resolved to access those coders already had.');
        } catch (Throwable $e) {
            flash('error', 'Assign failed: ' . (APP_DEBUG ? $e->getMessage() : 'server error'));
        }
        redirect('/admin/coding-access.php');
    }
}

/* ─────────────────────────────────────────── GET ─────────────────────────────────────────── */
$pageTitle = 'Coder access';
require __DIR__ . '/../includes/header.php';

/* Coders to manage — the actor's reporting team (sysadmin = everyone). Validators included (scope-restricted). */
$teamFilter = $team === null ? '' : (' AND u.id IN (' . implode(',', $team ?: [0]) . ')');
$coders = $pdo->query("SELECT DISTINCT u.id, u.name, u.email
    FROM users u JOIN user_roles ur ON ur.user_id=u.id JOIN roles r ON r.id=ur.role_id
    WHERE u.user_type='internal' AND u.status='active'
      AND r.code IN ('coder','coding_auditor','coding_validator','coding_supervisor','coding_manager')
      $teamFilter ORDER BY u.name")->fetchAll();

/* Per-coder alignment counts for the dropdown hint. */
$counts = [];
if ($coders) {
    $ids = array_map(fn($c) => (int) $c['id'], $coders);
    $in  = implode(',', array_fill(0, count($ids), '?'));
    $cs  = $pdo->prepare("SELECT user_id, COUNT(DISTINCT client_id) ncli, COUNT(*) nrows FROM coding_user_scope WHERE user_id IN ($in) AND " . coding_vertical_scope_sql('vertical_id') . " GROUP BY user_id");   // this page shows CODING alignment only
    $cs->execute($ids);
    foreach ($cs->fetchAll() as $r) $counts[(int) $r['user_id']] = $r;
}

$selected = null;
foreach ($coders as $c) if ((int) $c['id'] === $targetId) $selected = $c;
$selectedUnrestricted = $selected ? coding_scope_unrestricted($targetId) : false;

/* Picker tree: manager/sysadmin → whole Coding tree; supervisor → only the selected coder's clients. */
$assignTree = [];
if ($selected && !$selectedUnrestricted) {
    $assignTree = $isManagerGrant ? coding_full_facility_tree($pdo) : coding_scope_tree_for_user($pdo, $targetId);
}

/* Bulk re-align master-data lists (manager only). */
$bClients = $bProcs = $bFacs = $bProvs = []; $bTree = [];
if ($isManagerGrant) {
    $bClients = $pdo->query("SELECT c.id, c.name FROM clients c JOIN verticals v ON v.id=c.vertical_id
        WHERE c.deleted_at IS NULL AND " . coding_workflow_vertical_in_sql('v.id') . " ORDER BY c.name")->fetchAll();
    $bProcs = $pdo->query("SELECT p.id, p.name, c.name client FROM processes p JOIN clients c ON c.id=p.client_id
        JOIN verticals v ON v.id=c.vertical_id
        WHERE p.deleted_at IS NULL AND c.deleted_at IS NULL AND " . coding_workflow_vertical_in_sql('v.id') . " ORDER BY c.name, p.name")->fetchAll();
    $bFacs = $pdo->query("SELECT f.id, f.name, p.name process, c.name client FROM coding_facilities f
        JOIN processes p ON p.id=f.process_id JOIN clients c ON c.id=p.client_id
        JOIN verticals v ON v.id=c.vertical_id
        WHERE f.status='active' AND " . coding_workflow_vertical_in_sql('v.id') . " ORDER BY c.name, p.name, f.name")->fetchAll();
    $bProvs = $pdo->query("SELECT DISTINCT pr.id, pr.name FROM coding_providers pr
        JOIN coding_facility_providers fp ON fp.provider_id=pr.id
        JOIN coding_facilities f ON f.id=fp.facility_id JOIN processes p ON p.id=f.process_id
        JOIN clients c ON c.id=p.client_id JOIN verticals v ON v.id=c.vertical_id
        WHERE pr.status='active' AND " . coding_workflow_vertical_in_sql('v.id') . " ORDER BY pr.name")->fetchAll();
    // Complete Coding tree (LEFT JOINs → facility-less processes / provider-less facilities still appear)
    // drives the CASCADING bulk pickers: process options follow the chosen client(s), facilities the
    // chosen process(es), providers the chosen facilit(ies).
    foreach ($pdo->query("SELECT c.id cid, c.name cname, p.id pid, p.name pname, f.id fid, f.name fname, pr.id prid, pr.name prname
        FROM clients c
        JOIN verticals v ON v.id=c.vertical_id AND v.deleted_at IS NULL AND " . coding_workflow_vertical_in_sql('v.id') . "
        JOIN processes p ON p.client_id=c.id AND p.deleted_at IS NULL
        LEFT JOIN coding_facilities f ON f.process_id=p.id AND f.status='active'
        LEFT JOIN coding_facility_providers fp ON fp.facility_id=f.id
        LEFT JOIN coding_providers pr ON pr.id=fp.provider_id AND pr.status='active'
        WHERE c.deleted_at IS NULL ORDER BY c.name, p.name, f.name, pr.name") as $r) {
        $cid=(int)$r['cid']; $pid=(int)$r['pid'];
        $bTree[$cid]['name'] = $r['cname'];
        $bTree[$cid]['processes'][$pid]['name'] = $r['pname'];
        if ($r['fid'] !== null) {
            $fid=(int)$r['fid'];
            $bTree[$cid]['processes'][$pid]['facilities'][$fid]['name'] = $r['fname'];
            if ($r['prid'] !== null) $bTree[$cid]['processes'][$pid]['facilities'][$fid]['providers'][(int)$r['prid']] = $r['prname'];
        }
    }
}
$roleWord = $isManagerGrant ? 'manager' : 'supervisor';
?>
<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>

<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-users-group me-2 text-primary"></i>Coder access</h2>
        <div class="text-secondary small">Align each coder to a specific <strong>client » process » facility » provider</strong>.
            <?= $isManagerGrant ? 'You can assign a new client and bulk re-align many coders.' : 'You can add a process / facility within a client the coder already works.' ?></div>
    </div>
    <div class="col-auto d-flex gap-2">
        <?php if ($isManagerGrant): ?><a href="#bulkCard" class="btn btn-outline-primary"><i class="ti ti-stack-2 me-1"></i>Bulk re-align</a><?php endif; ?>
        <?php if ($isSysadmin): ?><a href="<?= e(url('/admin/employee-import.php')) ?>" class="btn btn-outline-secondary"><i class="ti ti-users-plus me-1"></i>Import employees</a><?php endif; ?>
    </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">This page aligns your <strong>coding team</strong> (manager/supervisor tool). To map data access for <strong>any user in any vertical</strong> — the one unified flow — use <a href="<?= e(url('/admin/user-alignment.php')) ?>" class="alert-link">User alignment</a>.</div>
    </div>
    <?php endif; ?>

    <!-- coder picker -->
    <div class="card mb-3"><div class="card-body">
        <form method="GET" class="row g-2 align-items-end">
            <div class="col-md-7"><label class="form-label">Coder <span class="text-secondary small">(<?= count($coders) ?> in <?= $team===null?'all teams':'your team' ?>)</span></label>
                <select name="user_id" id="coderPick" class="form-select" onchange="this.form.submit()">
                    <option value="">— choose a coder —</option>
                    <?php foreach ($coders as $c): $cid=(int)$c['id']; $ct=$counts[$cid]??null; ?>
                        <option value="<?= $cid ?>" <?= $targetId===$cid?'selected':'' ?>><?= e($c['name']) ?> · <?= e($c['email']) ?><?= $ct ? '  ('.(int)$ct['ncli'].' client'.((int)$ct['ncli']===1?'':'s').')' : '  (no access)' ?></option>
                    <?php endforeach; ?>
                </select>
            </div>
        </form>
    </div></div>

    <?php if (!$selected): ?>
        <div class="card mb-3"><div class="card-body text-center text-secondary py-5">
            <i class="ti ti-user-search" style="font-size:2.2rem"></i>
            <div class="mt-2">Choose a coder to view and assign their client / process / facility access.</div>
        </div></div>

    <?php elseif ($selectedUnrestricted): ?>
        <div class="alert alert-info"><i class="ti ti-shield-check me-1"></i><strong><?= e($selected['name']) ?></strong> is a supervisor / manager and already has
            <strong>unrestricted</strong> access to the whole Coding tree — no per-client alignment is needed.</div>

    <?php else:
        $grants = $pdo->prepare("SELECT s.*, c.name cname, p.name pname, f.name fname, pr.name prname, g.name gname
            FROM coding_user_scope s
            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
            LEFT JOIN users g ON g.id=s.granted_by
            WHERE s.user_id=? AND " . coding_vertical_scope_sql('s.vertical_id') . " ORDER BY c.name, p.name, f.name, pr.name");
        $grants->execute([$targetId]); $grants = $grants->fetchAll();
    ?>
        <!-- current alignment -->
        <div class="card mb-3">
            <div class="card-header"><h3 class="card-title"><i class="ti ti-sitemap me-1"></i>Current access for <?= e($selected['name']) ?>
                <span class="badge bg-blue-lt ms-2"><?= count($grants) ?></span></h3></div>
            <div class="table-responsive"><table class="table table-vcenter card-table">
                <thead><tr><th>Client</th><th>Process</th><th>Facility</th><th class="w-1 text-nowrap">Scope</th><th>Assigned by</th><th>When</th><th class="w-1"></th></tr></thead>
                <tbody>
                <?php foreach ($grants as $g):
                    if     (!empty($g['provider_id'])) { $lvl='Provider';     $lc='purple'; }
                    elseif (!empty($g['facility_id'])) { $lvl='Facility';     $lc='teal';   }
                    elseif (!empty($g['process_id']))  { $lvl='Process';      $lc='azure';  }
                    else                               { $lvl='Whole client'; $lc='green';  } ?>
                    <tr>
                        <td><strong><?= e($g['cname']) ?></strong></td>
                        <td><?= $g['pname'] ? e($g['pname']) : '<span class="text-secondary fst-italic">All</span>' ?></td>
                        <td><?= $g['fname'] ? e($g['fname']) : '<span class="text-secondary fst-italic">All</span>' ?>
                            <?= !empty($g['prname']) ? ' <span class="text-secondary small">· provider: '.e($g['prname']).'</span>' : '' ?></td>
                        <td><span class="badge bg-<?= $lc ?>-lt text-nowrap"><?= $lvl ?></span></td>
                        <td class="small text-secondary"><?= e($g['gname'] ?? '—') ?></td>
                        <td class="small text-secondary text-nowrap"><?= e(fmt_date($g['created_at'])) ?></td>
                        <td><form method="POST" onsubmit="return confirm('Remove this access?');"><?= csrf_field() ?>
                            <input type="hidden" name="action" value="remove_scope"><input type="hidden" name="id" value="<?= (int)$g['id'] ?>">
                            <button class="btn btn-sm btn-ghost-danger" title="Remove"><i class="ti ti-trash"></i></button></form></td>
                    </tr>
                <?php endforeach; ?>
                <?php if (!$grants): ?><tr><td colspan="7" class="text-center text-secondary py-4">No access yet — this coder cannot upload or work any inventory until aligned.</td></tr><?php endif; ?>
                </tbody>
            </table></div>
        </div>

        <!-- assign -->
        <div class="card mb-3">
            <div class="card-header"><h3 class="card-title"><i class="ti ti-plus me-1"></i>Assign access</h3></div>
            <div class="card-body">
                <?php if (!$isManagerGrant): ?>
                <div class="alert alert-info py-1 px-2 small mb-2"><i class="ti ti-info-circle me-1"></i>As a <strong>supervisor</strong> you allocate a
                    <strong>facility (and optionally a provider)</strong> within a <strong>client » process a manager already assigned</strong> to <?= e($selected['name']) ?>. Assigning a <strong>client / process</strong> is a manager action.</div>
                <?php endif; ?>
                <?php if (!$assignTree): ?>
                    <div class="text-secondary py-2"><i class="ti ti-alert-triangle me-1"></i>
                        <?= $isManagerGrant ? 'No Coding clients / facilities are available to assign.' : 'This coder has no client » process yet — a manager must assign one before you can allocate a facility within it.' ?></div>
                <?php else: ?>
                <form method="POST" class="row g-2 align-items-end">
                    <?= csrf_field() ?><input type="hidden" name="action" value="add_scope"><input type="hidden" name="user_id" value="<?= $targetId ?>">
                    <div class="col-md-3"><label class="form-label required">Client</label>
                        <select name="client_id" id="sel_client" class="form-select" required><option value="">— client —</option></select></div>
                    <div class="col-md-3"><label class="form-label">Process</label>
                        <select name="process_id" id="sel_process" class="form-select"><option value="">— all —</option></select></div>
                    <div class="col-md-3"><label class="form-label">Facility</label>
                        <select name="facility_id" id="sel_facility" class="form-select"><option value="">— all —</option></select></div>
                    <div class="col-md-2"><label class="form-label">Provider</label>
                        <select name="provider_id" id="sel_provider" class="form-select"><option value="">— all —</option></select></div>
                    <div class="col-md-1"><button class="btn btn-primary w-100"><i class="ti ti-check"></i></button></div>
                    <div class="col-12"><div class="form-hint"><?= $isManagerGrant
                        ? 'Leave Process / Facility / Provider as “all” to grant everything beneath the chosen level.'
                        : 'Pick the client » process the manager assigned, then a Facility (Provider optional). Process can’t be left as “all”.' ?></div></div>
                </form>
                <script>
                (function(){
                    var TREE = <?= json_encode($assignTree, JSON_HEX_TAG|JSON_HEX_AMP|JSON_HEX_QUOT|JSON_HEX_APOS) ?>;
                    var elC=document.getElementById('sel_client'), elP=document.getElementById('sel_process'),
                        elF=document.getElementById('sel_facility'), elPr=document.getElementById('sel_provider');
                    function opt(v,t){ var o=document.createElement('option'); o.value=v; o.textContent=t; return o; }
                    function fill(el, map, allLabel){ el.innerHTML=''; if(allLabel) el.appendChild(opt('',allLabel));
                        Object.keys(map||{}).sort(function(a,b){ return (map[a].name||map[a]).localeCompare(map[b].name||map[b]); })
                            .forEach(function(k){ el.appendChild(opt(k, map[k].name||map[k])); }); }
                    elC.innerHTML='<option value="">— client —</option>';
                    Object.keys(TREE).sort(function(a,b){ return TREE[a].name.localeCompare(TREE[b].name); }).forEach(function(k){ elC.appendChild(opt(k, TREE[k].name)); });
                    function resetBelow(lvl){ if(lvl<=1){ elP.innerHTML='<option value="">— all —</option>'; } if(lvl<=2){ elF.innerHTML='<option value="">— all —</option>'; } if(lvl<=3){ elPr.innerHTML='<option value="">— all —</option>'; } }
                    elC.onchange=function(){ resetBelow(1); var c=TREE[elC.value]; if(c){ fill(elP, c.processes, '— all —'); } };
                    elP.onchange=function(){ resetBelow(2); var c=TREE[elC.value]; if(c && c.processes[elP.value]){ fill(elF, c.processes[elP.value].facilities, '— all —'); } };
                    elF.onchange=function(){ resetBelow(3); var c=TREE[elC.value]; if(c && c.processes[elP.value] && c.processes[elP.value].facilities[elF.value]){ fill(elPr, c.processes[elP.value].facilities[elF.value].providers, '— all —'); } };
                })();
                </script>
                <?php endif; ?>
            </div>
        </div>
    <?php endif; ?>

    <?php if ($isManagerGrant): ?>
    <!-- bulk re-align (manager / sysadmin) -->
    <div class="card" id="bulkCard">
        <div class="card-header"><h3 class="card-title"><i class="ti ti-stack-2 me-1"></i>Bulk re-align <span class="text-secondary fw-normal small ms-1">— assign many coders to one client / process / facility at once</span></h3></div>
        <form method="POST" class="card-body">
            <?= csrf_field() ?><input type="hidden" name="action" value="bulk_assign">
            <div class="mb-3">
                <label class="form-label fw-bold"><span class="badge bg-primary me-1">1</span> Coders <span class="text-secondary small">(pick one or many)</span></label>
                <select name="user_ids[]" id="bUsers" multiple class="form-select" data-placeholder="Search coders…">
                    <?php foreach ($coders as $c): $cid=(int)$c['id']; $ct=$counts[$cid]??null; ?>
                        <option value="<?= $cid ?>"><?= e($c['name']) ?> · <?= e($c['email']) ?><?= $ct ? '  ('.(int)$ct['ncli'].' client'.((int)$ct['ncli']===1?'':'s').')' : '  (no access)' ?></option>
                    <?php endforeach; ?>
                </select>
            </div>
            <label class="form-label fw-bold"><span class="badge bg-primary me-1">2</span> Access to assign <span class="text-secondary small">(any combination — each becomes the deepest scope; parents resolved automatically)</span></label>
            <div class="row g-2 mb-2">
                <div class="col-md-3"><select name="client_ids[]" id="bClients" multiple class="form-select" data-placeholder="Clients (whole client)…">
                    <?php foreach ($bClients as $c): ?><option value="<?= (int)$c['id'] ?>"><?= e($c['name']) ?></option><?php endforeach; ?></select></div>
                <div class="col-md-3"><select name="process_ids[]" id="bProcs" multiple class="form-select" data-placeholder="Processes…">
                    <?php foreach ($bProcs as $p): ?><option value="<?= (int)$p['id'] ?>"><?= e($p['client']) ?> » <?= e($p['name']) ?></option><?php endforeach; ?></select></div>
                <div class="col-md-3"><select name="facility_ids[]" id="bFacs" multiple class="form-select" data-placeholder="Facilities…">
                    <?php foreach ($bFacs as $f): ?><option value="<?= (int)$f['id'] ?>"><?= e($f['client']) ?> » <?= e($f['process']) ?> » <?= e($f['name']) ?></option><?php endforeach; ?></select></div>
                <div class="col-md-3"><select name="provider_ids[]" id="bProvs" multiple class="form-select" data-placeholder="Providers…">
                    <?php foreach ($bProvs as $pr): ?><option value="<?= (int)$pr['id'] ?>"><?= e($pr['name']) ?></option><?php endforeach; ?></select></div>
            </div>
            <div class="d-flex align-items-center flex-wrap gap-3">
                <label class="form-check mb-0"><input type="checkbox" class="form-check-input" name="replace" value="1">
                    <span class="form-check-label">Replace these coders' existing Coding access first</span></label>
                <span id="bPreview" class="text-secondary small"></span>
                <button class="btn btn-primary ms-auto"><i class="ti ti-key me-1"></i>Assign access</button>
            </div>
        </form>
    </div>
    <?php endif; ?>

    <?php
    /* ─────────────────────── Allocation summary (read-only) ─────────────────────────
       Client » Process » Facility » Provider tree showing the coder(s) assigned to each provider via
       coding_user_scope. Assignment is HIERARCHICAL: a client/process/facility-level grant (deeper cols NULL)
       covers every provider under it. Only role='coder' rows count, so a manager/supervisor's broad scope doesn't
       mask true coder coverage. Providers with no coder → "Unallocated" (red). Collapsible + filterable. */
    $alClient=(int)($_GET['al_client']??0); $alProcess=(int)($_GET['al_process']??0);
    $alFacility=(int)($_GET['al_facility']??0); $alProvider=(int)($_GET['al_provider']??0); $alCoder=(int)($_GET['al_coder']??0);
    $alAnyFilter = $alClient||$alProcess||$alFacility||$alProvider||$alCoder;
    $alVin = coding_workflow_vertical_in_sql('v.id');
    // Team scope: a supervisor/manager sees only THEIR reporting team's coders here (sysadmin = null = everyone).
    // Providers assigned only to a coder OUTSIDE the actor's team therefore read as Unallocated in this view.
    $alTeamU = $team === null ? '' : (' AND u.id IN (' . implode(',', array_map('intval', $team ?: [0])) . ')');
    $alClientsOpt=$pdo->query("SELECT c.id,c.name FROM clients c JOIN verticals v ON v.id=c.vertical_id WHERE c.deleted_at IS NULL AND $alVin ORDER BY c.name")->fetchAll();
    $alProcsOpt=$pdo->query("SELECT p.id,p.name,c.id client_id,c.name client FROM processes p JOIN clients c ON c.id=p.client_id JOIN verticals v ON v.id=c.vertical_id WHERE p.deleted_at IS NULL AND c.deleted_at IS NULL AND $alVin ORDER BY c.name,p.name")->fetchAll();
    $alFacsOpt=$pdo->query("SELECT f.id,f.name,p.id process_id,c.id client_id,p.name process,c.name client FROM coding_facilities f JOIN processes p ON p.id=f.process_id JOIN clients c ON c.id=p.client_id JOIN verticals v ON v.id=c.vertical_id WHERE f.status='active' AND $alVin ORDER BY c.name,p.name,f.name")->fetchAll();
    $alProvsOpt=$pdo->query("SELECT DISTINCT pr.id,pr.name FROM coding_providers pr JOIN coding_facility_providers fp ON fp.provider_id=pr.id JOIN coding_facilities f ON f.id=fp.facility_id JOIN processes p ON p.id=f.process_id JOIN clients c ON c.id=p.client_id JOIN verticals v ON v.id=c.vertical_id WHERE pr.status='active' AND $alVin ORDER BY pr.name")->fetchAll();
    $alCodersOpt=$pdo->query("SELECT DISTINCT u.id,u.name FROM users u JOIN user_roles ur ON ur.user_id=u.id JOIN roles r ON r.id=ur.role_id WHERE r.code='coder' AND u.status='active'$alTeamU ORDER BY u.name")->fetchAll();
    /* Cascade maps so the filter dropdowns are DEPENDENT: client→its processes, process→its facilities, facility→its providers. */
    $casProc=[]; foreach($alProcsOpt as $o) $casProc[(int)$o['client_id']][]=['id'=>(int)$o['id'],'name'=>$o['name']];
    $casFac=[];  foreach($alFacsOpt  as $o) $casFac[(int)$o['process_id']][]=['id'=>(int)$o['id'],'name'=>$o['name']];
    $casProv=[];
    foreach($pdo->query("SELECT fp.facility_id fid, pr.id, pr.name FROM coding_facility_providers fp
        JOIN coding_providers pr ON pr.id=fp.provider_id JOIN coding_facilities f ON f.id=fp.facility_id
        JOIN processes p ON p.id=f.process_id JOIN clients c ON c.id=p.client_id JOIN verticals v ON v.id=c.vertical_id
        WHERE pr.status='active' AND f.status='active' AND $alVin ORDER BY pr.name") as $o)
        $casProv[(int)$o['fid']][]=['id'=>(int)$o['id'],'name'=>$o['name']];
    $casJson=json_encode(['proc'=>$casProc,'fac'=>$casFac,'prov'=>$casProv], JSON_HEX_TAG|JSON_HEX_AMP|JSON_HEX_QUOT|JSON_HEX_APOS);
    $alW='c.deleted_at IS NULL';
    if($alClient)$alW.=' AND c.id='.$alClient; if($alProcess)$alW.=' AND p.id='.$alProcess;
    if($alFacility)$alW.=' AND f.id='.$alFacility; if($alProvider)$alW.=' AND pr.id='.$alProvider;
    $alRows=$pdo->query("SELECT c.id cid,c.name cname,p.id pid,p.name pname,f.id fid,f.name fname,pr.id prid,pr.name prname
        FROM clients c JOIN verticals v ON v.id=c.vertical_id AND v.deleted_at IS NULL AND $alVin
        JOIN processes p ON p.client_id=c.id AND p.deleted_at IS NULL
        LEFT JOIN coding_facilities f ON f.process_id=p.id AND f.status='active'
        LEFT JOIN coding_facility_providers fp ON fp.facility_id=f.id
        LEFT JOIN coding_providers pr ON pr.id=fp.provider_id AND pr.status='active'
        WHERE $alW ORDER BY c.name,p.name,f.name,pr.name")->fetchAll();
    $alSql="SELECT DISTINCT s.user_id,u.name uname,s.client_id,s.process_id,s.facility_id,s.provider_id
        FROM coding_user_scope s JOIN users u ON u.id=s.user_id
        JOIN user_roles ur ON ur.user_id=u.id JOIN roles r ON r.id=ur.role_id
        WHERE r.code='coder' AND u.status='active'$alTeamU AND ".coding_vertical_scope_sql('s.vertical_id');
    if($alCoder)$alSql.=' AND s.user_id='.$alCoder;
    $alScopeByClient=[]; foreach($pdo->query($alSql)->fetchAll() as $s) $alScopeByClient[(int)$s['client_id']][]=$s;
    $alCoversFor=function(int $cid,int $pid,int $fid,int $prid) use($alScopeByClient){ $o=[];
        foreach($alScopeByClient[$cid]??[] as $s){
            if($s['process_id']!==null&&(int)$s['process_id']!==$pid)continue;
            if($s['facility_id']!==null&&(int)$s['facility_id']!==$fid)continue;
            if($s['provider_id']!==null&&(int)$s['provider_id']!==$prid)continue;
            $o[(int)$s['user_id']]=$s['uname']; } return $o; };
    /* Flat rows — one per (client » process » facility » provider) with its assigned coder(s). */
    $alFlat=[]; $alUnalloc=0;
    foreach($alRows as $r){
        if($r['fid']===null||$r['prid']===null) continue;   // only rows that reach an actual provider
        $coders=$alCoversFor((int)$r['cid'],(int)$r['pid'],(int)$r['fid'],(int)$r['prid']);
        if($alCoder&&!isset($coders[$alCoder])) continue;   // coder filter → only this coder's providers
        if(empty($coders)) $alUnalloc++;
        $alFlat[]=['cid'=>(int)$r['cid'],'pid'=>(int)$r['pid'],'fid'=>(int)$r['fid'],'prid'=>(int)$r['prid'],'client'=>$r['cname'],'process'=>$r['pname'],'facility'=>$r['fname'],'provider'=>$r['prname'],'coders'=>$coders];
    }
    /* querystring to return to after an assign/unlink so the summary keeps its filters + anchor */
    $alRet=http_build_query(array_filter(['al_client'=>$alClient?:null,'al_process'=>$alProcess?:null,'al_facility'=>$alFacility?:null,'al_provider'=>$alProvider?:null,'al_coder'=>$alCoder?:null,'user_id'=>$targetId?:null]));
    ?>
    <div class="card mt-3" id="allocSummary">
        <div class="card-header">
            <h3 class="card-title mb-0"><i class="ti ti-sitemap me-1 text-primary"></i>Allocation summary <span class="text-secondary fw-normal small ms-1">— which provider is assigned to which coder</span></h3>
            <div class="card-actions text-secondary small"><strong><?= count($alFlat) ?></strong> provider(s)<?php if($alUnalloc): ?> · <strong class="text-red"><?= $alUnalloc ?></strong> unallocated<?php else: ?> · <span class="text-green">all allocated</span><?php endif; ?></div>
        </div>
        <div class="card-body">
            <form method="GET" class="row g-2 mb-3 align-items-end">
                <?php if($targetId):?><input type="hidden" name="user_id" value="<?=$targetId?>"><?php endif;?>
                <div class="col-6 col-md"><label class="form-label small mb-1">Client</label><select name="al_client" class="form-select form-select-sm"><option value="0">All clients</option><?php foreach($alClientsOpt as $o):?><option value="<?=(int)$o['id']?>" <?=$alClient===(int)$o['id']?'selected':''?>><?=e($o['name'])?></option><?php endforeach;?></select></div>
                <div class="col-6 col-md"><label class="form-label small mb-1">Process</label><select name="al_process" class="form-select form-select-sm"><option value="0">All processes</option><?php foreach($alProcsOpt as $o):?><option value="<?=(int)$o['id']?>" <?=$alProcess===(int)$o['id']?'selected':''?>><?=e($o['client'].' » '.$o['name'])?></option><?php endforeach;?></select></div>
                <div class="col-6 col-md"><label class="form-label small mb-1">Facility</label><select name="al_facility" class="form-select form-select-sm"><option value="0">All facilities</option><?php foreach($alFacsOpt as $o):?><option value="<?=(int)$o['id']?>" <?=$alFacility===(int)$o['id']?'selected':''?>><?=e($o['client'].' » '.$o['process'].' » '.$o['name'])?></option><?php endforeach;?></select></div>
                <div class="col-6 col-md"><label class="form-label small mb-1">Provider</label><select name="al_provider" class="form-select form-select-sm"><option value="0">All providers</option><?php foreach($alProvsOpt as $o):?><option value="<?=(int)$o['id']?>" <?=$alProvider===(int)$o['id']?'selected':''?>><?=e($o['name'])?></option><?php endforeach;?></select></div>
                <div class="col-6 col-md"><label class="form-label small mb-1">Coder</label><select name="al_coder" class="form-select form-select-sm"><option value="0">All coders</option><?php foreach($alCodersOpt as $o):?><option value="<?=(int)$o['id']?>" <?=$alCoder===(int)$o['id']?'selected':''?>><?=e($o['name'])?></option><?php endforeach;?></select></div>
                <div class="col-auto"><button class="btn btn-sm btn-primary"><i class="ti ti-filter me-1"></i>Filter</button> <a href="<?=e(url('/admin/coding-access.php'.($targetId?('?user_id='.$targetId):'').'#allocSummary'))?>" class="btn btn-sm btn-ghost-secondary">Clear</a></div>
            </form>
            <div class="small text-secondary mb-2"><i class="ti ti-info-circle me-1"></i>Each provider lists its assigned coder(s); <span class="badge bg-red-lt text-red">Unallocated</span> = no coder. A facility/process/client-level grant covers every provider under it.<?php if($team!==null): ?> <strong>Scope:</strong> your team's coders — a provider assigned only to a coder outside your team reads as Unallocated here.<?php endif; ?></div>
            <script>
            (function(){
                var root=document.getElementById('allocSummary'); if(!root) return;
                var CAS=<?= $casJson ?: '{"proc":{},"fac":{},"prov":{}}' ?>;
                var C=root.querySelector('[name="al_client"]'), P=root.querySelector('[name="al_process"]'),
                    F=root.querySelector('[name="al_facility"]'), PR=root.querySelector('[name="al_provider"]');
                if(!C||!P||!F||!PR) return;
                function dedupe(a){ var s={},o=[]; (a||[]).forEach(function(x){ if(!s[x.id]){s[x.id]=1;o.push(x);} }); return o; }
                function flat(m){ var o=[]; Object.keys(m||{}).forEach(function(k){ (m[k]||[]).forEach(function(x){o.push(x);}); }); return dedupe(o); }
                function fill(sel,items,allLabel,keep){ sel.innerHTML='';
                    var a=document.createElement('option'); a.value='0'; a.textContent=allLabel; sel.appendChild(a);
                    (items||[]).forEach(function(it){ var e=document.createElement('option'); e.value=it.id; e.textContent=it.name; if(String(it.id)===String(keep))e.selected=true; sel.appendChild(e); }); }
                function procOpts(){ var c=C.value; return c!=='0'?(CAS.proc[c]||[]):flat(CAS.proc); }
                function facOpts(){ var p=P.value; if(p!=='0')return CAS.fac[p]||[];
                    var c=C.value; if(c!=='0'){ var o=[]; (CAS.proc[c]||[]).forEach(function(pr){ (CAS.fac[pr.id]||[]).forEach(function(f){o.push(f);}); }); return dedupe(o); }
                    return flat(CAS.fac); }
                function provOpts(){ var f=F.value; if(f!=='0')return CAS.prov[f]||[];
                    var o=[]; facOpts().forEach(function(fa){ (CAS.prov[fa.id]||[]).forEach(function(p){o.push(p);}); }); return dedupe(o); }
                C.addEventListener('change',function(){ fill(P,procOpts(),'All processes','0'); fill(F,facOpts(),'All facilities','0'); fill(PR,provOpts(),'All providers','0'); });
                P.addEventListener('change',function(){ fill(F,facOpts(),'All facilities','0'); fill(PR,provOpts(),'All providers','0'); });
                F.addEventListener('change',function(){ fill(PR,provOpts(),'All providers','0'); });
                /* initial scope, preserving the server-selected values */
                fill(P,procOpts(),'All processes','<?= (int)$alProcess ?>');
                fill(F,facOpts(),'All facilities','<?= (int)$alFacility ?>');
                fill(PR,provOpts(),'All providers','<?= (int)$alProvider ?>');
            })();
            </script>
            <div class="table-responsive">
                <table class="table table-vcenter card-table table-sm">
                    <thead><tr><th>Client</th><th>Process</th><th>Facility</th><th>Provider</th><th>Assigned coder(s)</th><th class="w-1 text-nowrap">Actions</th></tr></thead>
                    <tbody>
                    <?php if(!$alFlat): ?>
                        <tr><td colspan="6" class="text-center text-secondary py-3">No providers match these filters.</td></tr>
                    <?php else: foreach($alFlat as $row): ?>
                        <tr<?= empty($row['coders']) ? ' style="background:rgba(220,53,69,.055)"' : '' ?>>
                            <td class="text-secondary small text-nowrap"><?= e($row['client']) ?></td>
                            <td class="text-secondary small text-nowrap"><?= e($row['process']) ?></td>
                            <td class="small text-nowrap"><?= e($row['facility']) ?></td>
                            <td class="fw-medium"><?= e($row['provider']) ?></td>
                            <td><?php if($row['coders']): foreach($row['coders'] as $nm): ?><span class="badge bg-green-lt text-green me-1"><i class="ti ti-user-check me-1"></i><?= e($nm) ?></span><?php endforeach; else: ?><span class="badge bg-red-lt text-red fw-bold">Unallocated</span><?php endif; ?></td>
                            <td class="text-nowrap">
                                <?php if(empty($row['coders'])): ?>
                                <form method="POST" class="d-inline-flex gap-1 align-items-center">
                                    <?= csrf_field() ?><input type="hidden" name="action" value="al_assign">
                                    <input type="hidden" name="client_id" value="<?= (int)$row['cid'] ?>"><input type="hidden" name="process_id" value="<?= (int)$row['pid'] ?>">
                                    <input type="hidden" name="facility_id" value="<?= (int)$row['fid'] ?>"><input type="hidden" name="provider_id" value="<?= (int)$row['prid'] ?>">
                                    <input type="hidden" name="ret" value="<?= e($alRet) ?>">
                                    <select name="coder_id" class="form-select form-select-sm" style="width:150px" required>
                                        <option value="">Assign coder…</option>
                                        <?php foreach($alCodersOpt as $o): ?><option value="<?= (int)$o['id'] ?>"><?= e($o['name']) ?></option><?php endforeach; ?>
                                    </select>
                                    <button class="btn btn-sm btn-outline-primary" title="Assign this provider to the coder"><i class="ti ti-user-plus"></i></button>
                                </form>
                                <?php endif; ?>
                                <?php if($isManagerGrant): ?>
                                <form method="POST" class="d-inline" onsubmit="return confirm('Remove <?= e(addslashes($row['provider'])) ?> from <?= e(addslashes($row['facility'])) ?>? Past charts are kept; the provider is unlinked (no longer works at this facility).');">
                                    <?= csrf_field() ?><input type="hidden" name="action" value="al_unlink">
                                    <input type="hidden" name="facility_id" value="<?= (int)$row['fid'] ?>"><input type="hidden" name="provider_id" value="<?= (int)$row['prid'] ?>">
                                    <input type="hidden" name="ret" value="<?= e($alRet) ?>">
                                    <button class="btn btn-sm btn-ghost-danger" title="Provider no longer works at this facility — unlink"><i class="ti ti-trash"></i></button>
                                </form>
                                <?php endif; ?>
                            </td>
                        </tr>
                    <?php endforeach; endif; ?>
                    </tbody>
                </table>
            </div>
        </div>
    </div>

</div></div>

<?php if ($isManagerGrant): ?>
<script>
(function(){
    if (typeof TomSelect === 'undefined') return;
    var opts = { plugins:['remove_button','clear_button'], maxOptions:5000, persist:false };
    var u  = new TomSelect('#bUsers',   {...opts, placeholder:'Search coders…'});
    var c  = new TomSelect('#bClients', {...opts, placeholder:'Clients…'});
    var p  = new TomSelect('#bProcs',   {...opts, placeholder:'Processes…'});
    var f  = new TomSelect('#bFacs',    {...opts, placeholder:'Facilities…'});
    var pr = new TomSelect('#bProvs',   {...opts, placeholder:'Providers…'});

    // ── Cascading filter ── a child picker only shows options under the selected parent(s); an empty
    // parent means no filter (show everything below). Rebuilds run silently so they don't recurse.
    var TREE = <?= json_encode($bTree, JSON_HEX_TAG|JSON_HEX_AMP|JSON_HEX_QUOT|JSON_HEX_APOS) ?>;
    function setOpts(ts, list){
        var valid={}; list.forEach(function(o){ valid[o.v]=1; });
        var keep = ts.items.filter(function(v){ return valid[v]; });
        ts.clearOptions(); ts.addOptions(list.map(function(o){ return {value:o.v, text:o.t}; }));
        ts.refreshOptions(false); ts.setValue(keep, true);
    }
    function any(arr, v){ return arr.length === 0 || arr.indexOf(String(v)) !== -1; }
    function buildProcs(){
        var cs=c.items, out=[];
        Object.keys(TREE).forEach(function(ci){ if(!any(cs,ci)) return; var cn=TREE[ci];
            Object.keys(cn.processes||{}).forEach(function(pi){ out.push({v:pi, t:cn.name+' » '+cn.processes[pi].name}); }); });
        setOpts(p, out); buildFacs();
    }
    function buildFacs(){
        var cs=c.items, ps=p.items, out=[];
        Object.keys(TREE).forEach(function(ci){ if(!any(cs,ci)) return; var cn=TREE[ci];
            Object.keys(cn.processes||{}).forEach(function(pi){ if(!any(ps,pi)) return; var pn=cn.processes[pi];
                Object.keys(pn.facilities||{}).forEach(function(fi){ out.push({v:fi, t:cn.name+' » '+pn.name+' » '+pn.facilities[fi].name}); }); }); });
        setOpts(f, out); buildProvs();
    }
    function buildProvs(){
        var cs=c.items, ps=p.items, fs=f.items, seen={}, out=[];
        Object.keys(TREE).forEach(function(ci){ if(!any(cs,ci)) return; var cn=TREE[ci];
            Object.keys(cn.processes||{}).forEach(function(pi){ if(!any(ps,pi)) return; var pn=cn.processes[pi];
                Object.keys(pn.facilities||{}).forEach(function(fi){ if(!any(fs,fi)) return; var fn=pn.facilities[fi];
                    Object.keys(fn.providers||{}).forEach(function(pri){ if(!seen[pri]){ seen[pri]=1; out.push({v:pri, t:fn.providers[pri]}); } }); }); }); });
        setOpts(pr, out);
    }
    c.on('change', buildProcs); p.on('change', buildFacs); f.on('change', buildProvs);
    buildProcs();   // initial fill (nothing selected = everything)

    function preview(){
        var users  = u.items.length;
        var tuples = c.items.length + p.items.length + f.items.length + pr.items.length;
        var el = document.getElementById('bPreview');
        el.innerHTML = (users && tuples)
            ? 'About <strong>'+(users*tuples)+'</strong> row(s): '+users+' coder(s) × '+tuples+' selection(s).'
            : 'Pick coders and at least one client / process / facility / provider.';
    }
    [u,c,p,f,pr].forEach(function(ts){ ts.on('change', preview); });
    preview();
})();
</script>
<?php endif; ?>
<?php require __DIR__ . '/../includes/footer.php'; ?>
