<?php
/**
 * User management - internal + client users in one screen, separated by tab.
 * Roles assignable per-user. Locked/inactive users can be unlocked.
 */
require_once __DIR__ . '/../includes/config.php';
$me = require_login('internal');

$pdo    = db();
$action = $_GET['action'] ?? $_POST['action'] ?? 'list';
$id     = (int) ($_GET['id'] ?? $_POST['id'] ?? 0);
// Merged user list: ONE list of all logins, with a Type filter (All default · Internal · Client). 'all' = no
// type filter. The two former tabs are now just filter values on the same list.
$tab    = in_array($_GET['tab'] ?? '', ['internal', 'client', 'all'], true) ? (string) $_GET['tab'] : 'all';

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    csrf_check_or_die();

    // Authoritative, type-aware manage gate for the per-user state actions (unlock / force_pw_change / deactivate).
    // The page route only requires a VIEW perm, so without this a view-only user could deactivate/reset any account
    // (incl. SysAdmin). Gate on the TARGET's STORED type: internal targets need user.internal.manage, client targets
    // need user.client.manage (SysAdmin always).
    $requireManageTarget = function (int $targetId) use ($pdo, $me) {
        $t = $pdo->prepare('SELECT user_type FROM users WHERE id=?'); $t->execute([$targetId]);
        $tt = $t->fetchColumn();
        if ($tt === false) { flash('error', 'User not found.'); redirect('/admin/users.php'); }
        $actorIsSysadmin = user_has_role((int) $me['id'], 'sysadmin');
        // A SysAdmin account may be managed ONLY by another SysAdmin — stops a non-sysadmin holder of
        // user.internal.manage (e.g. a Manager, now granted that perm) from resetting / deactivating /
        // unlocking a SysAdmin to seize the account.
        if (!$actorIsSysadmin && user_has_role($targetId, 'sysadmin')) {
            flash('error', 'Only a SysAdmin can manage a SysAdmin account.'); redirect('/admin/users.php');
        }
        $ok = $actorIsSysadmin || ($tt === 'internal' ? can('user.internal.manage') : can('user.client.manage'));
        if (!$ok) { flash('error', 'You do not have permission to manage this user.'); redirect('/admin/users.php'); }
    };

    if ($action === 'apply_extra_roles') {
        // Idempotent — adds Manager / Supervisor / User roles if they don't already exist
        if (!user_has_role((int) $me['id'], 'sysadmin')) {
            flash('error', 'Only sysadmin can add system roles.');
            redirect('/admin/users.php');
        }
        $extras = [
            ['manager',    'Manager',    'Manages users + uploads + downloads on assigned reports', 'internal',
                ['client.view','process.view','process.manage','user.client.view','user.client.manage','user.internal.view','user.internal.manage',
                 'report.definition.view','report.view','report.view_detail','report.upload','report.replace_all','report.download',
                 'report.access.view','audit.view']],
            ['supervisor', 'Supervisor', 'Reviews uploads and approves data revisions',             'internal',
                ['client.view','process.view','user.internal.view','user.client.view',
                 'report.definition.view','report.view','report.view_detail',
                 'report.upload','report.download','report.restore','audit.view']],
            ['user',       'User',       'Standard data-entry user — view + upload + append',      'internal',
                ['report.view','report.view_detail','report.upload','report.download']],
        ];
        $added = 0;
        foreach ($extras as [$code,$name,$desc,$scope,$perms]) {
            $pdo->prepare('INSERT INTO roles (code,name,description,is_system,scope,created_at,updated_at)
                           VALUES (?,?,?,1,?,NOW(),NOW())
                           ON DUPLICATE KEY UPDATE name=VALUES(name), description=VALUES(description), updated_at=NOW()')
                ->execute([$code, $name, $desc, $scope]);
            $rid = (int) $pdo->query("SELECT id FROM roles WHERE code = '$code'")->fetchColumn();
            foreach ($perms as $p) {
                $pid = (int) $pdo->query("SELECT id FROM permissions WHERE code = '$p'")->fetchColumn();
                if ($pid) $pdo->prepare('INSERT IGNORE INTO role_permissions (role_id,permission_id) VALUES (?,?)')->execute([$rid,$pid]);
            }
            $added++;
        }
        audit_log('role.bulk.add', 'role', null, null, ['count'=>$added]);
        flash('success', "Added/refreshed $added roles: Manager, Supervisor, User.");
        redirect('/admin/users.php');
    }

    if ($action === 'save') {
        // Authoritative target type: on EDIT use the STORED type (NEVER trust the POSTed type — the form only
        // disables the selector, which a forged POST ignores); on CREATE the POSTed type is what's being made.
        // This stops a client-manager from POSTing user_type=client against an internal user's id to hijack it.
        $existingType = null;
        if ($id) {
            $et = $pdo->prepare('SELECT user_type FROM users WHERE id=?'); $et->execute([$id]);
            $existingType = $et->fetchColumn();
            if ($existingType === false) { flash('error', 'User not found.'); redirect('/admin/users.php'); }
            // Editing an existing SysAdmin is SysAdmin-only — a non-sysadmin holder of user.internal.manage
            // (e.g. a Manager) must never be able to change a SysAdmin's email/password and hijack the account.
            if (!user_has_role((int) $me['id'], 'sysadmin') && user_has_role($id, 'sysadmin')) {
                flash('error', 'Only a SysAdmin can edit a SysAdmin account.'); redirect('/admin/users.php');
            }
        }
        // MERGED creation: there is no separate "user type" picker — the chosen ROLES decide it. Any client-scope
        // role makes a client-portal login; otherwise it's internal staff. (EDIT keeps the STORED type — a role
        // change never flips an existing login between internal and client.)
        $postedRoleIds = array_map('intval', (array) ($_POST['roles'] ?? []));
        $scopeMap = $pdo->query("SELECT id, scope FROM roles")->fetchAll(PDO::FETCH_KEY_PAIR);
        $selScopes = [];
        foreach ($postedRoleIds as $prid) if (isset($scopeMap[$prid])) $selScopes[] = $scopeMap[$prid];
        $pickedClientRole   = in_array('client', $selScopes, true);
        $pickedInternalRole = in_array('internal', $selScopes, true);
        $userType   = $id ? (string) $existingType : ($pickedClientRole ? 'client' : 'internal');
        $clientId   = $userType === 'client' ? ((int) input('client_id') ?: null) : null;
        $isInternal = $userType === 'internal';

        // Type-aware gate on the AUTHORITATIVE type: managing INTERNAL staff needs user.internal.manage; CLIENT
        // users need user.client.manage (SysAdmin always). A client-manager therefore can't edit internal staff.
        $canManageUsers = user_has_role((int) $me['id'], 'sysadmin')
            || ($isInternal ? can('user.internal.manage') : can('user.client.manage'));
        if (!$canManageUsers) { flash('error', 'You do not have permission to create or edit ' . $userType . ' users.'); redirect('/admin/users.php'); }

        // Employee fields (internal staff only) — merged in from the old employee-directory page.
        $empCode   = $isInternal ? trim((string) input('employee_code')) : '';
        $vertical  = $isInternal ? trim((string) input('vertical'))      : '';
        $reportsTo = $isInternal ? ((int) input('reports_to') ?: null)   : null;

        $data = [
            'name'      => trim((string) input('name')),
            'email'     => strtolower(trim((string) input('email'))),
            'user_type' => $userType,
            'client_id' => $clientId,
            'status'    => in_array(input('status'), ['active','inactive'], true) ? input('status') : 'active',
            'two_factor_enabled' => input('two_factor_enabled') ? 1 : 0,
        ];
        $newPassword = (string) input('password');
        $roleIds = $postedRoleIds;

        $errors = [];
        if ($data['name'] === '') $errors[] = 'Name is required.';
        if (!filter_var($data['email'], FILTER_VALIDATE_EMAIL)) $errors[] = 'Email is invalid.';
        if ($userType === 'client' && !$clientId) $errors[] = 'Client is required for a client login.';
        // A login is internal OR client — never both. (Only relevant on create; edit keeps the stored type.)
        if (!$id && $pickedClientRole && $pickedInternalRole) $errors[] = 'Pick internal roles OR client roles, not both — a login is one or the other.';
        if (!$errors) {
            $dup = $pdo->prepare('SELECT id FROM users WHERE email=? AND id<>?');
            $dup->execute([$data['email'], $id]);
            if ($dup->fetch()) $errors[] = 'Another user already has that email.';
        }
        // Employee ID must stay globally unique (the workbook importer matches existing users on it).
        if ($isInternal && $empCode !== '') {
            $dq = $pdo->prepare('SELECT name FROM users WHERE employee_code=? AND id<>? LIMIT 1');
            $dq->execute([$empCode, $id]);
            if ($dn = $dq->fetchColumn()) $errors[] = 'Employee ID "' . $empCode . '" is already used by ' . $dn . '.';
        }
        // Supervisor (reports_to): active internal, not self, not one of this user's own reports (cycle guard).
        if ($isInternal && $reportsTo) {
            if ($reportsTo === $id) {
                $errors[] = 'A user cannot report to themselves.';
            } else {
                $sq = $pdo->prepare("SELECT 1 FROM users WHERE id=? AND user_type='internal' AND status<>'inactive'");
                $sq->execute([$reportsTo]);
                if (!$sq->fetchColumn()) $errors[] = 'Chosen supervisor not found or inactive.';
                elseif ($id && in_array($reportsTo, coding_managed_user_ids($pdo, $id), true))
                    $errors[] = 'That would create a reporting loop — the chosen supervisor already reports (directly or indirectly) to this user.';
            }
        }
        if (!$id && $newPassword === '') $errors[] = 'Password is required for new users.';
        if ($newPassword !== '') {
            $errs = password_policy_errors($newPassword);
            $errors = array_merge($errors, $errs);
        }

        if ($errors) {
            foreach ($errors as $e) flash('error', $e);
            // Preserve what was typed/picked across the PRG redirect — the merged form DERIVES the login type from
            // the chosen roles, so losing the role selection would silently flip the intended type on resubmit.
            $_SESSION['users_old'] = [
                'user_type' => $userType, 'client_id' => $clientId,
                'name' => $data['name'], 'email' => $data['email'], 'status' => $data['status'],
                'two_factor_enabled' => $data['two_factor_enabled'],
                'employee_code' => $empCode, 'vertical' => $vertical, 'reports_to' => $reportsTo,
                'roles' => $roleIds,
            ];
            redirect('/admin/users.php?action=' . ($id ? 'edit&id=' . $id : 'new'));
        }

        // Normalised employee values written below (NULL when blank / not an internal user) — also fed to the audit.
        $empVal  = $isInternal && $empCode  !== '' ? $empCode  : null;
        $vertVal = $isInternal && $vertical !== '' ? $vertical : null;
        $repVal  = $isInternal ? $reportsTo : null;
        $data['employee_code'] = $empVal; $data['vertical'] = $vertVal; $data['reports_to'] = $repVal;

        if ($id) {
            $before = $pdo->query("SELECT id,name,email,user_type,client_id,status,employee_code,vertical,reports_to FROM users WHERE id=$id")->fetch();
            $sets = 'name=?, email=?, user_type=?, client_id=?, status=?, two_factor_enabled=?, employee_code=?, vertical=?, reports_to=?, updated_at=NOW()';
            $params = [$data['name'], $data['email'], $data['user_type'], $data['client_id'],
                       $data['status'], $data['two_factor_enabled'], $empVal, $vertVal, $repVal];
            if ($newPassword !== '') {
                $sets .= ', password=?, password_changed_at=NOW(), must_change_password=1';
                $params[] = password_hash($newPassword, PASSWORD_BCRYPT, ['cost' => 12]);
            }
            // Unlock if requested
            if ($data['status'] === 'active') {
                $sets .= ', login_attempts=0, locked_until=NULL';
            }
            $params[] = $id;
            $pdo->prepare("UPDATE users SET $sets WHERE id=?")->execute($params);
            audit_log('user.update', 'user', $id, $before, $data);
        } else {
            $pdo->prepare(
                'INSERT INTO users (name, email, password, user_type, client_id, status,
                                   two_factor_enabled, employee_code, vertical, reports_to,
                                   must_change_password, password_changed_at,
                                   created_at, updated_at)
                 VALUES (?,?,?,?,?,?,?,?,?,?,1,NOW(),NOW(),NOW())'
            )->execute([
                $data['name'], $data['email'],
                password_hash($newPassword, PASSWORD_BCRYPT, ['cost' => 12]),
                $data['user_type'], $data['client_id'], $data['status'], $data['two_factor_enabled'],
                $empVal, $vertVal, $repVal,
            ]);
            $id = (int) $pdo->lastInsertId();
            audit_log('user.create', 'user', $id, null, $data);
            // A NEW client-portal user starts able to VIEW all of their client's active reports/dashboards,
            // so their portal isn't empty on first login. (View only — no upload/delete.) Idempotent; a
            // SysAdmin can fine-tune later on Report Access, or re-sync via "Grant all reports" on this page.
            if ($userType === 'client' && $clientId) {
                $pdo->prepare(
                    'INSERT IGNORE INTO report_access
                        (report_id, user_id, can_view, can_upload, can_download, can_delete, can_replace, granted_by, granted_at, created_at, updated_at)
                     SELECT r.id, ?, 1, 0, 0, 0, 0, ?, NOW(), NOW(), NOW()
                     FROM reports r JOIN processes p ON p.id = r.process_id
                     WHERE p.client_id = ? AND r.deleted_at IS NULL AND r.status = "active"'
                )->execute([$id, (int) $me['id'], $clientId]);
            }
        }

        // Refresh roles — sanitize server-side first. The form's "disabled" cross-scope checkboxes are cosmetic
        // (a forged POST ignores them), so enforce here: keep only roles whose scope matches the user type, and
        // never let a non-SysAdmin grant the SysAdmin role (privilege-escalation guard).
        if ($roleIds) {
            $actorIsSysadmin = user_has_role((int) $me['id'], 'sysadmin');
            $roleMeta = $pdo->query('SELECT id,code,scope FROM roles')->fetchAll(PDO::FETCH_UNIQUE | PDO::FETCH_ASSOC);
            // The permission codes each role grants — used for the subset escalation guard below.
            $rolePermCodes = [];
            foreach ($pdo->query('SELECT rp.role_id, pm.code FROM role_permissions rp JOIN permissions pm ON pm.id=rp.permission_id') as $rpc)
                $rolePermCodes[(int) $rpc['role_id']][] = $rpc['code'];
            // Explicit, audited refusal (not a silent drop) if a non-SysAdmin tried to assign SysAdmin via a
            // forged POST — the form disables that checkbox, and the filter below removes it regardless.
            if (!$actorIsSysadmin) {
                foreach ($roleIds as $rid) {
                    if (($roleMeta[$rid]['code'] ?? '') === 'sysadmin') {
                        flash('error', 'Only a SysAdmin can create or grant the SysAdmin role — that selection was ignored.');
                        audit_log('user.roles.sysadmin_grant_blocked', 'user', $id, null, ['actor' => (int) $me['id']]);
                        break;
                    }
                }
            }
            $roleIds = array_values(array_filter($roleIds, function ($rid) use ($roleMeta, $userType, $actorIsSysadmin, $rolePermCodes) {
                $r = $roleMeta[$rid] ?? null;
                if (!$r) return false;
                if ($userType === 'client'   && $r['scope'] === 'internal') return false;   // internal roles never on a client user
                if ($userType === 'internal' && $r['scope'] === 'client')   return false;   // client roles never on internal staff
                if ($r['code'] === 'sysadmin' && !$actorIsSysadmin) return false;            // only a SysAdmin may grant SysAdmin
                // PRIVILEGE-ESCALATION GUARD: a non-SysAdmin (e.g. a Manager now holding user.internal.manage) may
                // only assign a role whose capabilities they THEMSELVES hold — so they can never grant, to anyone or
                // to themselves, a permission they lack (e.g. coding.supervise → org-wide PHI). Lateral assignment of
                // peer roles still works; SysAdmins are exempt. can() reflects the actor's effective perms (role + overrides).
                if (!$actorIsSysadmin) {
                    foreach ($rolePermCodes[$rid] ?? [] as $code) { if (!can($code)) return false; }
                }
                return true;
            }));
        }
        $pdo->prepare('DELETE FROM user_roles WHERE user_id=?')->execute([$id]);
        if ($roleIds) {
            $stmt = $pdo->prepare('INSERT IGNORE INTO user_roles (user_id, role_id) VALUES (?,?)');
            foreach ($roleIds as $rid) {
                $stmt->execute([$id, $rid]);
            }
        }
        audit_log('user.roles.set', 'user', $id, null, ['roles' => $roleIds]);

        flash('success', 'User saved.');
        redirect('/admin/users.php?tab=' . $userType);
    }

    if ($action === 'unlock' && $id) {
        $requireManageTarget($id);
        $pdo->prepare("UPDATE users SET status='active', login_attempts=0, locked_until=NULL WHERE id=?")
            ->execute([$id]);
        audit_log('user.unlock', 'user', $id);
        flash('success', 'User unlocked.');
        redirect(back_url('/admin/users.php'));
    }

    if ($action === 'force_pw_change' && $id) {
        $requireManageTarget($id);
        $pdo->prepare("UPDATE users SET must_change_password=1 WHERE id=?")->execute([$id]);
        audit_log('user.force_pw_change', 'user', $id);
        flash('success', 'User will be required to change password on next login.');
        redirect(back_url('/admin/users.php'));
    }

    if ($action === 'deactivate' && $id) {
        if ($id === (int) $me['id']) { flash('error', 'Cannot deactivate your own account.'); redirect('/admin/users.php'); }
        $requireManageTarget($id);
        // Don't let the last active SysAdmin be deactivated (would lock everyone out of super-admin).
        if (user_has_role($id, 'sysadmin')) {
            $other = $pdo->query("SELECT COUNT(*) 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='sysadmin' AND u.status='active' AND u.id<>" . (int)$id)->fetchColumn();
            if ((int) $other === 0) { flash('error', 'Cannot deactivate the last active SysAdmin.'); redirect('/admin/users.php'); }
        }
        $pdo->prepare("UPDATE users SET status='inactive' WHERE id=?")->execute([$id]);
        audit_log('user.deactivate', 'user', $id);
        flash('success', 'User deactivated.');
        redirect(back_url('/admin/users.php'));
    }

    if ($action === 'grant_client_reports' && $id) {
        // One-click: (re)grant View on ALL of a CLIENT user's client reports — fills the portal in one go.
        // Gated to SysAdmin or report-access granters; only valid for a client-type target.
        if (!user_has_role((int) $me['id'], 'sysadmin') && !can('report.access.grant')) {
            flash('error', 'You are not allowed to grant report access.'); redirect('/admin/users.php?tab=client');
        }
        $t = $pdo->prepare("SELECT user_type, client_id FROM users WHERE id=?"); $t->execute([$id]); $t = $t->fetch();
        if (!$t || $t['user_type'] !== 'client' || !$t['client_id']) {
            flash('error', 'Grant-all reports only applies to a client user tied to a client.'); redirect('/admin/users.php?tab=client');
        }
        $st = $pdo->prepare(
            'INSERT IGNORE INTO report_access
                (report_id, user_id, can_view, can_upload, can_download, can_delete, can_replace, granted_by, granted_at, created_at, updated_at)
             SELECT r.id, ?, 1, 0, 0, 0, 0, ?, NOW(), NOW(), NOW()
             FROM reports r JOIN processes p ON p.id = r.process_id
             WHERE p.client_id = ? AND r.deleted_at IS NULL AND r.status = "active"'
        );
        $st->execute([$id, (int) $me['id'], (int) $t['client_id']]);
        audit_log('report.access.grant_client_all', 'user', $id, null, ['client_id' => (int) $t['client_id'], 'granted' => $st->rowCount()]);
        flash('success', $st->rowCount() . ' report(s) granted (View) to this client user. Already-granted reports were left as-is.');
        redirect(back_url('/admin/users.php?tab=client'));
    }

    if ($action === 'delete_user' && $id) {
        // HARD-delete a login — SysAdmin ONLY. Guards: not self, not the last SysAdmin, and BLOCKED while the user has
        // any coding_inventory involvement (assigned / coded / audited) or a billing allocation — a hard delete would
        // orphan that history, so deactivate or reassign first. On a clean delete, user_permissions + coding_user_scope
        // CASCADE and assigned_to / added_by SET NULL via FKs; we also prune the no-FK user-owned rows.
        if (!user_has_role((int) $me['id'], 'sysadmin')) { flash('error', 'Only a SysAdmin can delete users.'); redirect(back_url('/admin/users.php')); }
        if ($id === (int) $me['id']) { flash('error', 'You cannot delete your own account.'); redirect(back_url('/admin/users.php')); }
        $requireManageTarget($id);   // user exists (a SysAdmin actor clears the SysAdmin-target guard)
        if (user_has_role($id, 'sysadmin')) {
            $other = (int) $pdo->query("SELECT COUNT(*) 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='sysadmin' AND u.id<>" . (int) $id)->fetchColumn();
            if ($other === 0) { flash('error', 'Cannot delete the last SysAdmin account.'); redirect(back_url('/admin/users.php')); }
        }
        $work = (int) $pdo->query("SELECT COUNT(*) FROM coding_inventory WHERE coder_id=" . (int) $id . " OR auditor_id=" . (int) $id . " OR assigned_to=" . (int) $id)->fetchColumn();
        try { $work += (int) $pdo->query("SELECT COUNT(*) FROM billing_allocation WHERE allocated_biller=" . (int) $id)->fetchColumn(); } catch (Throwable $e) {}
        if ($work > 0) {
            flash('error', 'This user has ' . number_format($work) . ' chart / billing record(s) — deletion is blocked to protect that history. Deactivate the user, or reassign their work first.');
            redirect(back_url('/admin/users.php'));
        }
        $snap = $pdo->query("SELECT id,name,email,user_type FROM users WHERE id=" . (int) $id)->fetch() ?: null;
        $pdo->beginTransaction();
        try {
            foreach (['user_roles', 'report_access', 'notifications'] as $t) {
                try { $pdo->prepare("DELETE FROM `$t` WHERE user_id=?")->execute([$id]); } catch (Throwable $e) { /* table may not exist */ }
            }
            // reports_to is a self-reference with NO FK — null it on any subordinates so they don't dangle at a dead id.
            try { $pdo->prepare("UPDATE users SET reports_to=NULL WHERE reports_to=?")->execute([$id]); } catch (Throwable $e) {}
            $pdo->prepare("DELETE FROM users WHERE id=?")->execute([$id]);
            $pdo->commit();
        } catch (Throwable $e) {
            $pdo->rollBack();
            flash('error', 'Delete failed: ' . (defined('APP_DEBUG') && APP_DEBUG ? $e->getMessage() : 'the user is still referenced elsewhere — deactivate instead.'));
            redirect(back_url('/admin/users.php'));
        }
        audit_log('user.delete', 'user', $id, $snap, null);
        flash('success', 'User deleted permanently.');
        redirect(back_url('/admin/users.php'));
    }
}

$pageTitle = 'Users';
require __DIR__ . '/../includes/header.php';

$clients = $pdo->query('SELECT id,name,code FROM clients WHERE deleted_at IS NULL ORDER BY name')->fetchAll();
$roles = $pdo->query('SELECT id,name,code,scope FROM roles ORDER BY scope, name')->fetchAll();
$actorIsSysadmin = user_has_role((int) $me['id'], 'sysadmin');   // only a SysAdmin may grant the SysAdmin role

if ($action === 'new' || $action === 'edit') {
    $row = ['id'=>0,'name'=>'','email'=>'','user_type'=>'internal','client_id'=>null,
            'status'=>'active','two_factor_enabled'=>0,'employee_code'=>'','vertical'=>'','reports_to'=>null];
    $userRoleIds = [];
    if ($action === 'edit' && $id) {
        $stmt = $pdo->prepare('SELECT id,name,email,user_type,client_id,status,two_factor_enabled,must_change_password,employee_code,vertical,reports_to FROM users WHERE id=?');
        $stmt->execute([$id]);
        $row = $stmt->fetch() ?: $row;
        if (!$row['id']) { flash('error','User not found.'); redirect('/admin/users.php'); }
        $rs = $pdo->prepare('SELECT role_id FROM user_roles WHERE user_id=?');
        $rs->execute([$id]);
        $userRoleIds = array_map('intval', $rs->fetchAll(PDO::FETCH_COLUMN));
    }
    // Rehydrate after a validation error (PRG) so the chosen roles / client / fields aren't lost — critical for the
    // merged form where the role selection IS what decides the login type.
    if (!empty($_SESSION['users_old'])) {
        $old = $_SESSION['users_old']; unset($_SESSION['users_old']);
        foreach (['name','email','status','employee_code','vertical','user_type'] as $k) if (isset($old[$k])) $row[$k] = $old[$k];
        if (array_key_exists('client_id', $old))  $row['client_id'] = $old['client_id'];
        if (array_key_exists('reports_to', $old)) $row['reports_to'] = $old['reports_to'];
        if (isset($old['two_factor_enabled']))    $row['two_factor_enabled'] = $old['two_factor_enabled'];
        if (isset($old['roles']))                 $userRoleIds = array_map('intval', (array) $old['roles']);
    }
    // Employee-field option lists (internal staff): assignable verticals + supervisor candidates (active internal, never self).
    $existingVerts = $pdo->query("SELECT DISTINCT vertical FROM users WHERE user_type='internal' AND vertical IS NOT NULL AND vertical<>'' ORDER BY vertical")->fetchAll(PDO::FETCH_COLUMN);
    $vertOpts = array_values(array_unique(array_merge(['Coding','AR','Billing'], $existingVerts)));
    $supSt = $pdo->prepare("SELECT id,name,email FROM users WHERE user_type='internal' AND status='active' AND id<>? ORDER BY name");
    $supSt->execute([(int) $row['id']]);
    $supCands = $supSt->fetchAll();
?>
<div class="page-header"><div class="container-xl">
    <h2 class="page-title"><?= $action === 'edit' ? 'Edit user' : 'New user' ?></h2>
</div></div>
<div class="page-body"><div class="container-xl">
<form method="POST" action="<?= e(url('/admin/users.php?action=save')) ?>">
    <?= csrf_field() ?>
    <input type="hidden" name="id" value="<?= (int) $row['id'] ?>">

    <div class="card mb-3"><div class="card-body row g-3">
        <?php if ($action === 'edit'): ?>
        <div class="col-md-3">
            <label class="form-label">Login type</label>
            <div><span class="badge bg-<?= $row['user_type']==='client'?'azure':'purple' ?>-lt"><?= $row['user_type']==='client'?'Client portal':'Internal' ?></span></div>
            <div class="form-hint">Set by the role at creation; not switchable.</div>
        </div>
        <?php else: ?>
        <div class="col-12">
            <div class="alert alert-info py-2 mb-0"><i class="ti ti-info-circle me-1"></i>The <strong>role(s)</strong> you pick below decide the login type — a <strong>client</strong> role makes a client-portal login (choose its client), any other role makes internal staff.</div>
        </div>
        <?php endif; ?>
        <div class="col-md-9" id="client-row" style="<?= $row['user_type']==='client'?'':'display:none' ?>">
            <label class="form-label">Client <span class="text-danger">*</span></label>
            <select name="client_id" class="form-select">
                <option value="">-- choose --</option>
                <?php foreach ($clients as $c): ?>
                    <option value="<?= (int) $c['id'] ?>" <?= $row['client_id']==$c['id']?'selected':'' ?>>
                        <?= e($c['name']) ?> (<?= e($c['code']) ?>)
                    </option>
                <?php endforeach; ?>
            </select>
        </div>
        <div class="col-md-6">
            <label class="form-label required">Full name</label>
            <input type="text" name="name" class="form-control" required maxlength="150" value="<?= e($row['name']) ?>">
        </div>
        <div class="col-md-6">
            <label class="form-label required">Email</label>
            <input type="email" name="email" class="form-control" required maxlength="191" value="<?= e($row['email']) ?>">
        </div>
        <div class="col-md-4">
            <label class="form-label"><?= $action==='edit' ? 'Reset password (optional)' : 'Password' ?></label>
            <input type="text" name="password" class="form-control" autocomplete="new-password" <?= $action==='edit'?'':'required' ?>
                   placeholder="Min <?= (int)PASSWORD_MIN_LENGTH ?>; mixed/digit/symbol">
        </div>
        <div class="col-md-4">
            <label class="form-label">Status</label>
            <select name="status" class="form-select">
                <option value="active"   <?= $row['status']==='active'  ?'selected':'' ?>>Active</option>
                <option value="inactive" <?= $row['status']==='inactive'?'selected':'' ?>>Inactive</option>
            </select>
            <div class="form-hint">Only Active users can sign in. Inactive users see "Your account is inactive."</div>
        </div>
        <div class="col-md-4 align-self-end">
            <label class="form-check">
                <input type="checkbox" name="two_factor_enabled" value="1" <?= !empty($row['two_factor_enabled'])?'checked':'' ?> class="form-check-input">
                <span class="form-check-label">Enable 2FA (email OTP)</span>
            </label>
        </div>
    </div></div>

    <!-- Employee details (internal staff only) — merged in from the former Employee Directory page. -->
    <div class="card mb-3" id="emp-details" style="<?= $row['user_type']==='internal'?'':'display:none' ?>">
        <div class="card-header"><h3 class="card-title"><i class="ti ti-id-badge-2 me-1"></i>Employee details <span class="text-secondary fw-normal small ms-1">(internal staff)</span></h3></div>
        <div class="card-body row g-3">
            <div class="col-md-4">
                <label class="form-label">Employee ID</label>
                <input type="text" name="employee_code" class="form-control" maxlength="20" value="<?= e($row['employee_code'] ?? '') ?>" placeholder="e.g. EMP001">
                <div class="form-hint">Unique — the employee importer matches existing staff on it.</div>
            </div>
            <div class="col-md-4">
                <label class="form-label">Vertical</label>
                <select name="vertical" class="form-select">
                    <option value="">— none —</option>
                    <?php foreach ($vertOpts as $v): ?><option value="<?= e($v) ?>" <?= (string)($row['vertical'] ?? '')===$v?'selected':'' ?>><?= e($v) ?></option><?php endforeach; ?>
                </select>
            </div>
            <div class="col-md-4">
                <label class="form-label">Reports to (supervisor)</label>
                <select name="reports_to" id="reports_to" class="form-select">
                    <option value="0">— none (org root) —</option>
                    <?php foreach ($supCands as $c): ?><option value="<?= (int)$c['id'] ?>" <?= (int)($row['reports_to'] ?? 0)===(int)$c['id']?'selected':'' ?>><?= e($c['name']) ?> · <?= e($c['email']) ?></option><?php endforeach; ?>
                </select>
                <div class="form-hint">A user can't report to itself or to one of its own reports.</div>
            </div>
        </div>
    </div>

    <div class="card mb-3"><div class="card-header"><h3 class="card-title">Roles</h3></div>
        <div class="card-body row">
            <?php foreach ($roles as $r):
                // Only a SysAdmin may grant the SysAdmin role — lock that checkbox for everyone else
                // (the server enforces this too; this just makes it visibly impossible).
                $sysadminLocked = ($r['code'] === 'sysadmin' && !$actorIsSysadmin);
                // EDIT locks the login type → disable cross-scope roles. CREATE lets you pick any role (the role
                // decides the type; the JS + server enforce internal-XOR-client so you can't mix scopes).
                $disabled = $sysadminLocked
                         || ($action === 'edit' && $row['user_type'] === 'client'   && $r['scope'] === 'internal')
                         || ($action === 'edit' && $row['user_type'] === 'internal' && $r['scope'] === 'client');
                $checked = in_array((int) $r['id'], $userRoleIds, true) && !$sysadminLocked; ?>
                <div class="col-md-6 col-lg-4 mb-2">
                    <label class="form-check" <?= $sysadminLocked ? 'title="Only a SysAdmin can create or grant the SysAdmin role"' : '' ?>>
                        <input type="checkbox" name="roles[]" value="<?= (int) $r['id'] ?>" data-scope="<?= e($r['scope']) ?>"
                               <?= $checked ? 'checked' : '' ?>
                               <?= $disabled ? 'disabled' : '' ?> class="form-check-input role-cb">
                        <span class="form-check-label">
                            <strong><?= e($r['name']) ?></strong>
                            <span class="badge bg-<?= $r['scope']==='client'?'azure':($r['scope']==='internal'?'purple':'secondary') ?>-lt ms-1"><?= e($r['scope']) ?></span>
                            <?php if ($sysadminLocked): ?><span class="badge bg-red-lt ms-1"><i class="ti ti-lock me-1"></i>SysAdmin only</span><?php endif; ?>
                        </span>
                    </label>
                </div>
            <?php endforeach; ?>
        </div>
    </div>

    <div class="form-footer d-flex gap-2">
        <a href="<?= e(url('/admin/users.php?tab='.$row['user_type'])) ?>" class="btn btn-link">Cancel</a>
        <button type="submit" class="btn btn-primary"><i class="ti ti-check me-1"></i>Save</button>
    </div>
</form>
</div></div>
<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>
<script>
(function(){
    // The role decides the login type: a CLIENT-scope role → show the Client picker, hide employee details;
    // any other → internal staff (employee details, no client). On CREATE, ticking one scope unticks the other
    // (a login is internal XOR client). On EDIT the type is locked, so we only sync the visible sections.
    var cbs = Array.prototype.slice.call(document.querySelectorAll('.role-cb'));
    var clientRow = document.getElementById('client-row');
    var emp = document.getElementById('emp-details');
    var isEdit = <?= $action === 'edit' ? 'true' : 'false' ?>;
    function anyClient(){ return cbs.some(function(c){ return c.checked && c.getAttribute('data-scope') === 'client'; }); }
    function sync(){ var cl = anyClient();
        if (clientRow) clientRow.style.display = cl ? '' : 'none';
        if (emp) emp.style.display = cl ? 'none' : '';
    }
    if (!isEdit) cbs.forEach(function(c){ c.addEventListener('change', function(){
        if (c.checked) {
            var mine = c.getAttribute('data-scope');
            var other = mine === 'client' ? 'internal' : (mine === 'internal' ? 'client' : null);
            if (other) cbs.forEach(function(o){ if (o !== c && o.getAttribute('data-scope') === other) o.checked = false; });
        }
        sync();
    }); });
    sync();
})();
if (typeof TomSelect !== 'undefined' && document.getElementById('reports_to')) {
    new TomSelect('#reports_to', { maxOptions: 5000, persist: false });
}
</script>
<?php
} else {
    // ---- Filters (all optional, combinable; applied per active tab) ----
    $q       = trim((string) input('q'));
    $fRole   = (int) input('role');
    $fStatus = (string) input('status'); if (!in_array($fStatus, ['active','inactive','locked'], true)) $fStatus = '';
    $fVert   = trim((string) input('vert'));
    $fSup    = trim((string) input('sup'));      // '' = any · 'none' = no supervisor · <id> = reports to that person
    $fClient = (int) input('client');

    $params = [];
    $where  = 'WHERE 1=1';
    if ($tab !== 'all')  { $where .= ' AND u.user_type = :user_type'; $params[':user_type'] = $tab; }
    // distinct placeholders per occurrence — native (non-emulated) PDO prepares reject a reused named param (HY093).
    if ($q !== '')       { $where .= ' AND (u.name LIKE :qn OR u.email LIKE :qe)'; $params[':qn'] = "%$q%"; $params[':qe'] = "%$q%"; }
    // role: EXISTS (not a JOIN filter) so the Roles column still lists ALL of a user's roles, not only the matched one
    if ($fRole)          { $where .= ' AND EXISTS (SELECT 1 FROM user_roles urf WHERE urf.user_id=u.id AND urf.role_id=:role)'; $params[':role'] = $fRole; }
    if ($fStatus !== '') { $where .= ' AND u.status=:status'; $params[':status'] = $fStatus; }
    if ($tab !== 'client') {   // internal-only narrowing (also available on the merged 'all' list)
        if ($fVert !== '')       { $where .= ' AND u.vertical=:vert'; $params[':vert'] = $fVert; }
        if ($fSup === 'none')    { $where .= ' AND (u.reports_to IS NULL OR u.reports_to=0)'; }
        elseif ((int) $fSup > 0) { $where .= ' AND u.reports_to=:sup'; $params[':sup'] = (int) $fSup; }
    }
    if ($tab !== 'internal' && $fClient) { $where .= ' AND u.client_id=:client'; $params[':client'] = $fClient; }
    $hasFilters = $q !== '' || $fRole || $fStatus !== '' || $fVert !== '' || $fSup !== '' || (bool) $fClient;

    $stmt = $pdo->prepare("
        SELECT u.id, u.name, u.email, u.user_type, u.status, u.last_login_at,
               u.locked_until, u.must_change_password,
               u.employee_code, u.vertical, u.reports_to,
               c.name AS client_name, sup.name AS supervisor,
               GROUP_CONCAT(r.name ORDER BY r.name SEPARATOR ', ') AS roles
        FROM users u
        LEFT JOIN clients c ON c.id = u.client_id
        LEFT JOIN users sup ON sup.id = u.reports_to
        LEFT JOIN user_roles ur ON ur.user_id = u.id
        LEFT JOIN roles r ON r.id = ur.role_id
        $where
        GROUP BY u.id
        ORDER BY u.name
        LIMIT 200
    ");
    $stmt->execute($params);
    $rows = $stmt->fetchAll();

    // ---- Filter option lists ---- (the role dropdown shows all roles on the merged list; scope-limited otherwise)
    $roleOpts = $tab === 'all' ? $roles
        : array_values(array_filter($roles, fn($r) => $tab === 'client' ? $r['scope'] !== 'internal' : $r['scope'] !== 'client'));
    $vertFilterOpts = $supFilterOpts = [];
    if ($tab !== 'client') {
        $vertFilterOpts = $pdo->query("SELECT DISTINCT vertical FROM users WHERE user_type='internal' AND vertical IS NOT NULL AND vertical<>'' ORDER BY vertical")->fetchAll(PDO::FETCH_COLUMN);
        $supFilterOpts  = $pdo->query("SELECT DISTINCT sup.id, sup.name FROM users u JOIN users sup ON sup.id=u.reports_to WHERE u.user_type='internal' ORDER BY sup.name")->fetchAll();
    }

    // Reporting tree (internal staff) — shown on the Internal filter only (org chart is internal-specific).
    $treeRows = $roots = $byParent = [];
    if ($tab === 'internal') {
        $treeRows = $pdo->query("SELECT u.id,u.name,u.employee_code,u.reports_to,
                COALESCE(GROUP_CONCAT(DISTINCT r.name ORDER BY r.name SEPARATOR ', '),'') roles
            FROM users u LEFT JOIN user_roles ur ON ur.user_id=u.id LEFT JOIN roles r ON r.id=ur.role_id
            WHERE u.user_type='internal' GROUP BY u.id ORDER BY u.name LIMIT 5000")->fetchAll();
        $visible = [];
        foreach ($treeRows as $s) { $byParent[(int)($s['reports_to'] ?? 0)][] = $s; $visible[(int)$s['id']] = true; }
        foreach ($treeRows as $s) { $pr = (int)($s['reports_to'] ?? 0); if (!$pr || empty($visible[$pr])) $roots[] = $s; }
    }
    $renderNode = function($s, $depth) use (&$renderNode, $byParent) {
        $kids = $byParent[(int)$s['id']] ?? [];
        echo '<li><span class="text-secondary small">#' . e($s['employee_code'] ?: '—') . '</span> <strong>' . e($s['name']) . '</strong>'
            . ($s['roles'] ? ' <span class="text-secondary small">· ' . e($s['roles']) . '</span>' : '')
            . ' <a class="ms-1 small" href="' . e(url('/admin/users.php?action=edit&id=' . (int)$s['id'])) . '">edit</a>';
        if ($kids && $depth < 8) { echo '<ul>'; foreach ($kids as $k) $renderNode($k, $depth + 1); echo '</ul>'; }
        echo '</li>';
    };
?>
<style>
.workflow-steps { display:flex; gap:8px; align-items:stretch; margin: 8px 0 18px; }
.workflow-step {
    flex:1; background:#fff; border:1px solid var(--m-border); border-radius:8px;
    padding: 12px 14px; position:relative; transition: all .15s;
}
.workflow-step.is-current { border-color: var(--tblr-primary); box-shadow: 0 2px 6px rgba(32,107,196,.15); }
.workflow-step .step-num {
    width:26px; height:26px; border-radius:50%; background:#e9ecef; color:#6c7a89;
    display:inline-flex; align-items:center; justify-content:center; font-weight:700;
    font-size:.85rem; margin-right:8px;
}
.workflow-step.is-current .step-num { background: var(--tblr-primary); color:#fff; }
.workflow-step .step-title { font-weight:600; font-size:.85rem; color:#1a2332; }
.workflow-step .step-desc  { font-size:.72rem; color:#6c7a89; margin-top:2px; }
.workflow-step .step-link  { display:inline-block; margin-top:6px; font-size:.78rem; }
.workflow-arrow { color:#ced4da; align-self:center; font-size:1.2rem; }
.emp-tree, .emp-tree ul{ list-style:none; margin:0; padding-left:1.1rem; }
.emp-tree li{ padding:2px 0; border-left:1px solid var(--tblr-border-color); padding-left:.6rem; }
</style>

<div class="page-header"><div class="container-xl">
    <div class="row align-items-center">
        <div class="col">
            <h2 class="page-title">User &amp; Access Management</h2>
            <div class="text-secondary small mt-1">
                Create users, assign roles, grant per-report permissions. <a href="#" data-bs-toggle="modal" data-bs-target="#roleHelpModal">What does each role do?</a>
            </div>
        </div>
        <div class="col-auto d-flex gap-2">
            <?php if (user_has_role((int)$me['id'],'sysadmin')): ?>
            <a href="<?= e(url('/admin/roles.php')) ?>" class="btn btn-outline-secondary" title="Edit the default features each role grants — applies to everyone with that role">
                <i class="ti ti-shield-cog me-1"></i>Roles &amp; features
            </a>
            <a href="<?= e(url('/admin/access-control.php')) ?>" class="btn btn-outline-secondary" title="Grant or deny capabilities (e.g. let a user create internal users) per user">
                <i class="ti ti-lock-cog me-1"></i>Permissions
            </a>
            <?php endif; ?>
            <?php if ($tab!=='client' && user_has_role((int)$me['id'],'sysadmin')): ?>
            <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; ?>
            <a href="<?= e(url('/admin/users.php?action=new')) ?>" class="btn btn-primary">
                <i class="ti ti-user-plus me-1"></i>New user
            </a>
        </div>
    </div>

    <!-- 3-step workflow strip -->
    <div class="workflow-steps">
        <div class="workflow-step is-current">
            <span class="step-num">1</span>
            <span class="step-title">Create user</span>
            <div class="step-desc">Create the person — the role you pick decides internal staff vs client portal.</div>
            <a href="?action=new" class="step-link">+ New user</a>
        </div>
        <span class="workflow-arrow"><i class="ti ti-chevron-right"></i></span>
        <div class="workflow-step">
            <span class="step-num">2</span>
            <span class="step-title">Assign role</span>
            <div class="step-desc">Pick a role (SysAdmin / Manager / Supervisor / User / Client Admin / Client Viewer / etc.) when creating.</div>
        </div>
        <span class="workflow-arrow"><i class="ti ti-chevron-right"></i></span>
        <div class="workflow-step">
            <span class="step-num">3</span>
            <span class="step-title">Grant report access</span>
            <div class="step-desc">Per-report fine-grained permissions: View / Upload / Download / Delete / Replace.</div>
            <a href="<?= e(url('/admin/access.php')) ?>" class="step-link">Open Report Access &rarr;</a>
        </div>
    </div>
</div></div>

<!-- Hidden help modal -->
<div class="modal modal-blur fade" id="roleHelpModal" tabindex="-1">
  <div class="modal-dialog modal-lg modal-dialog-centered"><div class="modal-content">
    <div class="modal-header"><h5 class="modal-title">Roles &mdash; what each one means</h5>
      <button type="button" class="btn-close" data-bs-dismiss="modal"></button></div>
    <div class="modal-body">
      <table class="table table-sm">
        <thead><tr><th>Role</th><th>For</th><th>Default permissions</th></tr></thead>
        <tbody>
          <tr><td><span class="badge bg-red-lt">SysAdmin</span></td><td>Platform owner</td><td><strong>Everything</strong> &mdash; bypasses all checks.</td></tr>
          <tr><td><span class="badge bg-blue-lt">Developer</span></td><td>Engineer</td><td>Build report definitions, see all reports, no user mgmt.</td></tr>
          <tr><td><span class="badge bg-purple-lt">Manager</span></td><td>Team lead</td><td>Create internal &amp; client users, upload + replace data, download reports.</td></tr>
          <tr><td><span class="badge bg-cyan-lt">Supervisor</span></td><td>QA / reviewer</td><td>View + upload + download + restore snapshots (no replace).</td></tr>
          <tr><td><span class="badge bg-green-lt">User</span></td><td>Data-entry staff</td><td>View reports + upload (append) + download.</td></tr>
          <tr><td><span class="badge bg-yellow-lt">Auditor</span></td><td>Compliance</td><td>Read-only across all reports + full audit log.</td></tr>
          <tr><td><span class="badge bg-orange-lt">Report Controller</span></td><td>Access admin</td><td>Manage report-level permissions only.</td></tr>
          <tr><td><span class="badge bg-pink-lt">Client Admin</span></td><td>Customer admin</td><td>Manage own client portal users, view + download own reports.</td></tr>
          <tr><td><span class="badge bg-azure-lt">Client Viewer</span></td><td>Customer staff</td><td>View dashboards + download. No upload, no admin.</td></tr>
        </tbody>
      </table>
      <p class="text-secondary small mb-0">A user can have multiple roles &mdash; permissions are the union. <strong>Per-report access</strong> on the Report Access page overrides role defaults for specific reports.</p>
    </div>
  </div></div>
</div>

<?php /* The legacy "Install Manager/Supervisor/User roles" banner was removed (2026-06-15): those
       generic roles were consolidated into the AR roles (Account Receivable / AR Supervisor / AR
       Manager) per the role-dedupe, so we no longer prompt to re-create the duplicates. */ ?>
<div class="page-body"><div class="container-xl">
    <ul class="nav nav-tabs mb-2">
        <li class="nav-item"><a class="nav-link <?= $tab==='all'?'active':'' ?>" href="<?= e(url('/admin/users.php?tab=all')) ?>"><i class="ti ti-list me-1"></i>All users</a></li>
        <li class="nav-item"><a class="nav-link <?= $tab==='internal'?'active':'' ?>" href="<?= e(url('/admin/users.php?tab=internal')) ?>"><i class="ti ti-shield-check me-1"></i>Internal</a></li>
        <li class="nav-item"><a class="nav-link <?= $tab==='client'  ?'active':'' ?>" href="<?= e(url('/admin/users.php?tab=client')) ?>"><i class="ti ti-users me-1"></i>Client portal</a></li>
    </ul>
    <div class="card">
        <div class="card-header">
            <form method="GET" id="userFilters" class="row g-2 w-100 align-items-end">
                <input type="hidden" name="tab" value="<?= e($tab) ?>">
                <div class="col-12 col-md">
                    <label class="form-label small mb-1">Search</label>
                    <input type="text" name="q" class="form-control" placeholder="Name or email" value="<?= e($q) ?>">
                </div>
                <div class="col-6 col-md-2">
                    <label class="form-label small mb-1">Role</label>
                    <select name="role" id="f_role" class="form-select">
                        <option value="">All roles</option>
                        <?php foreach ($roleOpts as $ro): ?><option value="<?= (int)$ro['id'] ?>" <?= $fRole===(int)$ro['id']?'selected':'' ?>><?= e($ro['name']) ?></option><?php endforeach; ?>
                    </select>
                </div>
                <?php if ($tab!=='client'): ?>
                <div class="col-6 col-md-2">
                    <label class="form-label small mb-1">Vertical</label>
                    <select name="vert" class="form-select">
                        <option value="">All verticals</option>
                        <?php foreach ($vertFilterOpts as $v): ?><option value="<?= e($v) ?>" <?= $fVert===(string)$v?'selected':'' ?>><?= e($v) ?></option><?php endforeach; ?>
                    </select>
                </div>
                <div class="col-6 col-md-2">
                    <label class="form-label small mb-1">Reports to</label>
                    <select name="sup" id="f_sup" class="form-select">
                        <option value="">Any supervisor</option>
                        <option value="none" <?= $fSup==='none'?'selected':'' ?>>— No supervisor —</option>
                        <?php foreach ($supFilterOpts as $s): ?><option value="<?= (int)$s['id'] ?>" <?= $fSup===(string)$s['id']?'selected':'' ?>><?= e($s['name']) ?></option><?php endforeach; ?>
                    </select>
                </div>
                <?php endif; ?>
                <?php if ($tab!=='internal'): ?>
                <div class="col-6 col-md-3">
                    <label class="form-label small mb-1">Client</label>
                    <select name="client" id="f_client" class="form-select">
                        <option value="">All clients</option>
                        <?php foreach ($clients as $c): ?><option value="<?= (int)$c['id'] ?>" <?= $fClient===(int)$c['id']?'selected':'' ?>><?= e($c['name']) ?></option><?php endforeach; ?>
                    </select>
                </div>
                <?php endif; ?>
                <div class="col-6 col-md-2">
                    <label class="form-label small mb-1">Status</label>
                    <select name="status" class="form-select">
                        <option value="">All statuses</option>
                        <option value="active"   <?= $fStatus==='active'?'selected':'' ?>>Active</option>
                        <option value="inactive" <?= $fStatus==='inactive'?'selected':'' ?>>Inactive</option>
                        <option value="locked"   <?= $fStatus==='locked'?'selected':'' ?>>Locked</option>
                    </select>
                </div>
                <div class="col-auto">
                    <label class="form-label small mb-1 d-block">&nbsp;</label>
                    <button class="btn btn-primary"><i class="ti ti-filter me-1"></i>Filter</button>
                    <?php if ($hasFilters): ?><a href="<?= e(url('/admin/users.php?tab='.$tab)) ?>" class="btn btn-link px-2" title="Clear all filters">Clear</a><?php endif; ?>
                </div>
            </form>
        </div>
        <div class="px-3 py-2 text-secondary small border-bottom d-flex align-items-center">
            <i class="ti ti-users me-1"></i><?= count($rows) ?> <?= e($tab==='all'?'':$tab.' ') ?>user(s)<?= count($rows) >= 200 ? ' · first 200 shown — refine filters to narrow' : '' ?>
            <?php if ($hasFilters): ?><span class="ms-2 badge bg-azure-lt">filtered</span><?php endif; ?>
        </div>
        <div class="table-responsive">
            <table class="table table-vcenter card-table">
                <thead><tr>
                    <th>Name</th><th>Email</th>
                    <?php if ($tab==='all'): ?><th>Type</th><th>Client / vertical</th>
                    <?php elseif ($tab==='client'): ?><th>Client</th>
                    <?php else: ?><th>Emp ID</th><th>Vertical</th><th>Reports to</th><?php endif; ?>
                    <th>Roles</th><th>Status</th><th>Last login</th><th class="w-1"></th>
                </tr></thead>
                <tbody>
                <?php if (!$rows): ?>
                    <tr><td colspan="<?= $tab==='all'?8:($tab==='client'?7:9) ?>" class="text-center text-secondary py-4">No users.</td></tr>
                <?php else: foreach ($rows as $r): ?>
                    <tr>
                        <td><strong><?= e($r['name']) ?></strong>
                            <?php if ($r['must_change_password']): ?>
                                <span class="badge bg-yellow-lt ms-1" title="Must change password on next login">pw reset</span>
                            <?php endif; ?>
                        </td>
                        <td><?= e($r['email']) ?></td>
                        <?php if ($tab==='all'): ?>
                            <td><span class="badge bg-<?= $r['user_type']==='client'?'azure':'purple' ?>-lt"><?= $r['user_type']==='client'?'Client':'Internal' ?></span></td>
                            <td class="small"><?= $r['user_type']==='client' ? e($r['client_name'] ?? '—') : e($r['vertical'] ?: '—') ?></td>
                        <?php elseif ($tab==='client'): ?><td><?= e($r['client_name'] ?? '--') ?></td>
                        <?php else: ?>
                            <td class="small text-secondary"><?= e($r['employee_code'] ?: '—') ?></td>
                            <td class="small"><?= e($r['vertical'] ?: '—') ?></td>
                            <td class="small"><?= $r['supervisor'] ? e($r['supervisor']) : '<span class="text-secondary fst-italic">—</span>' ?></td>
                        <?php endif; ?>
                        <td><?= e($r['roles'] ?? '') ?></td>
                        <td>
                            <?php if ($r['status']==='active'): ?>
                                <span class="badge bg-success-lt">active</span>
                            <?php elseif ($r['status']==='locked'): ?>
                                <span class="badge bg-danger-lt">locked</span>
                            <?php else: ?>
                                <span class="badge bg-secondary-lt"><?= e($r['status']) ?></span>
                            <?php endif; ?>
                        </td>
                        <td class="text-secondary small"><?= e(fmt_dt($r['last_login_at'], 'never')) ?></td>
                        <td>
                            <div class="btn-list flex-nowrap">
                                <a href="<?= e(url('/admin/users.php?action=edit&id='.$r['id'])) ?>" class="btn btn-sm" title="Edit"><i class="ti ti-edit"></i></a>
                                <?php if (user_has_role((int) $me['id'], 'sysadmin')): ?>
                                    <a href="<?= e(url('/admin/access-control.php?tab=user&u='.$r['id'])) ?>" class="btn btn-sm btn-ghost-secondary" title="Permissions — grant/deny what this user can do (e.g. create internal users)"><i class="ti ti-lock-cog"></i></a>
                                <?php endif; ?>
                                <?php if (($r['user_type'] ?? '')==='client' && (user_has_role((int) $me['id'], 'sysadmin') || can('report.access.grant'))): ?>
                                    <form method="POST" action="<?= e(url('/admin/users.php?action=grant_client_reports&id='.$r['id'])) ?>" style="display:inline"
                                          onsubmit="return confirm('Grant this client user View access to ALL of their client\'s reports?');">
                                        <?= csrf_field() ?>
                                        <button class="btn btn-sm btn-ghost-success" title="Grant all this client's reports (View)"><i class="ti ti-report-analytics"></i></button>
                                    </form>
                                <?php endif; ?>
                                <?php if (user_has_role((int) $me['id'], 'sysadmin') && (int) $r['id'] !== (int) $me['id']): ?>
                                    <form method="POST" action="<?= e(url('/admin/impersonate.php')) ?>" style="display:inline"
                                          onsubmit="return confirm('View the platform as <?= e($r['name']) ?>? You can stop anytime.');">
                                        <?= csrf_field() ?><input type="hidden" name="id" value="<?= $r['id'] ?>">
                                        <button class="btn btn-sm btn-ghost-info" title="View as this user"><i class="ti ti-eye"></i></button>
                                    </form>
                                <?php endif; ?>
                                <?php if ($r['status']==='locked'): ?>
                                    <form method="POST" action="<?= e(url('/admin/users.php?action=unlock&id='.$r['id'])) ?>"><?= csrf_field() ?>
                                        <button class="btn btn-sm btn-ghost-success" title="Unlock"><i class="ti ti-lock-open"></i></button>
                                    </form>
                                <?php endif; ?>
                                <form method="POST" action="<?= e(url('/admin/users.php?action=force_pw_change&id='.$r['id'])) ?>"
                                      onsubmit="return confirm('Force this user to change password on next login?')"><?= csrf_field() ?>
                                    <button class="btn btn-sm btn-ghost-warning" title="Force pw change"><i class="ti ti-key"></i></button>
                                </form>
                                <?php if ((int) $r['id'] !== (int) $me['id']): ?>
                                    <form method="POST" action="<?= e(url('/admin/users.php?action=deactivate&id='.$r['id'])) ?>"
                                          onsubmit="return confirm('Deactivate this user?')"><?= csrf_field() ?>
                                        <button class="btn btn-sm btn-ghost-danger" title="Deactivate"><i class="ti ti-user-off"></i></button>
                                    </form>
                                <?php endif; ?>
                                <?php if (is_sysadmin() && (int) $r['id'] !== (int) $me['id']): ?>
                                    <form method="POST" action="<?= e(url('/admin/users.php?action=delete_user&id='.$r['id'])) ?>"
                                          onsubmit="return confirm('Permanently DELETE this user? This cannot be undone. (Blocked if they have coded / audited / billing work — deactivate instead.)')"><?= csrf_field() ?>
                                        <button class="btn btn-sm btn-ghost-danger" title="Delete user permanently (SysAdmin)"><i class="ti ti-trash"></i></button>
                                    </form>
                                <?php endif; ?>
                            </div>
                        </td>
                    </tr>
                <?php endforeach; endif; ?>
                </tbody>
            </table>
        </div>
    </div>

    <?php if ($tab === 'internal'): ?>
    <div class="card mt-3">
        <div class="card-header"><h3 class="card-title mb-0"><i class="ti ti-sitemap me-1"></i>Reporting hierarchy</h3>
            <div class="card-actions text-secondary small">Set each person's supervisor on their Edit page.</div></div>
        <div class="card-body" style="max-height:60vh; overflow:auto">
            <?php if (!$roots): ?><div class="text-secondary small">No reporting lines yet — set a supervisor on a user's Edit page.</div>
            <?php else: ?><ul class="emp-tree"><?php foreach ($roots as $r) $renderNode($r, 0); ?></ul>
            <div class="text-secondary small mt-2">Roots = staff with no supervisor.</div><?php endif; ?>
        </div>
    </div>
    <?php endif; ?>
</div></div>
<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>
<script>
(function(){
  var form=document.getElementById('userFilters'); if(!form) return;
  // searchable dropdowns for the longer lists (role / supervisor / client) — falls back to native select if the CDN is blocked
  ['f_role','f_sup','f_client'].forEach(function(id){ var el=document.getElementById(id);
    if(el && typeof TomSelect!=='undefined' && !el.tomselect) new TomSelect(el,{maxOptions:5000, persist:false, allowEmptyOption:true, sortField:{field:'text',direction:'asc'}}); });
  // auto-apply when any filter changes (the ready guard skips TomSelect's init-time change so the page doesn't submit on load)
  var ready=false; setTimeout(function(){ ready=true; }, 300);
  [].forEach.call(form.querySelectorAll('select'), function(s){ s.addEventListener('change', function(){ if(ready) form.submit(); }); });
})();
</script>
<?php
}
require __DIR__ . '/../includes/footer.php';
