Users - the agents who sign in to ITFlow. Each row is who * they are, their role, whether they can sign in and how (MFA, browsers that * skip the code), the clients they can see and when they last signed in. The * pills split them: everyone, admins, no MFA, disabled - and the archived. * Invited is the people asked to join who aren't in yet: the invites waiting * or run out (functions/user_invite.php), to send again, get the link to * pass on, or cancel. A link made to pass on is shown once, at the top. * * The admin's own row is shown but not changed here: that is their Account * pages, so nobody locks themselves out by mistake. */ // Default Column Sortby Filter $sort = "user_name"; $order = "ASC"; require_once "includes/inc_all_admin.php"; // filter_header.php fills in canned_date=custom when no range was asked for - not a filter here, so not carried into links if (($_GET['canned_date'] ?? '') === 'custom' && empty($_GET['dtf'])) { unset($_GET['canned_date']); } $roles = userRoles(); $role_filter = isset($roles[intval($_GET['role'] ?? 0)]) ? intval($_GET['role']) : 0; $filters = "user_type = 1 AND (user_name LIKE '%$q%' OR user_email LIKE '%$q%' OR role_name LIKE '%$q%')" . ($role_filter ? " AND user_role_id = $role_filter" : ''); $joins = "LEFT JOIN user_roles ON user_role_id = role_id LEFT JOIN user_settings ON user_settings.user_id = users.user_id"; // The views $shows = [ '' => ['label' => 'Users', 'icon' => 'fa-users', 'where' => 'user_archived_at IS NULL', 'title' => 'Everyone not archived'], 'admins' => ['label' => 'Admins', 'icon' => 'fa-user-shield', 'where' => 'user_archived_at IS NULL AND role_is_admin = 1', 'title' => 'Every client and every setting'], 'nomfa' => ['label' => 'No MFA', 'icon' => 'fa-unlock', 'where' => "user_archived_at IS NULL AND user_status = 1 AND (user_token IS NULL OR user_token = '')", 'title' => 'Active, and signing in with a password alone'], 'disabled' => ['label' => 'Disabled', 'icon' => 'fa-ban', 'where' => 'user_archived_at IS NULL AND user_status <> 1', 'title' => 'Cannot sign in'], 'invited' => ['label' => 'Invited', 'icon' => 'fa-paper-plane', 'where' => null, 'title' => 'Asked to join and not in yet - their invite waiting, or run out'], 'archived' => ['label' => 'Archived', 'icon' => 'fa-archive', 'where' => 'user_archived_at IS NOT NULL', 'title' => 'Gone - restore one with a new password'], ]; if (!userInvitesReady()) { unset($shows['invited']); } $show = isset($shows[$_GET['show'] ?? '']) ? ($_GET['show'] ?? '') : ''; // The invites - not users yet, so a list of their own $invite_filters = "invite_accepted_at IS NULL AND invite_cancelled_at IS NULL AND (invite_name LIKE '%$q%' OR invite_email LIKE '%$q%' OR role_name LIKE '%$q%')" . ($role_filter ? " AND invite_role_id = $role_filter" : ''); $invite_joins = "LEFT JOIN user_roles ON role_id = invite_role_id LEFT JOIN users inviter ON inviter.user_id = invite_created_by AND invite_created_by > 0"; // Only these sort - an unknown column would take the page down $last_login_sql = "(SELECT MAX(log_id) FROM logs WHERE log_user_id = users.user_id AND log_type = 'Login' AND log_action = 'Success')"; $sortable_columns = [ 'user_name' => "user_name $order", 'user_email' => "user_email $order", 'role_name' => "role_is_admin DESC, role_name $order, user_name ASC", 'user_status' => "user_status $order, user_name ASC", 'last_login' => "$last_login_sql IS NULL, $last_login_sql $order, user_name ASC", 'user_created_at' => "user_created_at $order, user_name ASC", ]; // ...and the same headers sort the invites: when it runs out for Sign in, when it was sent for Added $invite_sortable_columns = [ 'user_name' => "invite_name $order", 'user_email' => "invite_email $order", 'role_name' => "role_is_admin DESC, role_name $order, invite_name ASC", 'user_status' => "invite_expires_at $order, invite_name ASC", 'user_created_at' => "invite_created_at $order, invite_name ASC", ]; if (!isset(($show === 'invited' ? $invite_sortable_columns : $sortable_columns)[$sort])) { $sort = 'user_name'; } $users = []; $invites = []; if ($show === 'invited') { $sql = mysqli_query($mysqli, "SELECT SQL_CALC_FOUND_ROWS user_invites.*, role_name, role_is_admin, inviter.user_name AS invite_by_name FROM user_invites $invite_joins WHERE $invite_filters ORDER BY {$invite_sortable_columns[$sort]}, invite_id ASC LIMIT $record_from, $record_to"); $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()")); while ($row = mysqli_fetch_assoc($sql)) { $invites[] = $row; } } else { $sql = mysqli_query($mysqli, "SELECT SQL_CALC_FOUND_ROWS users.user_id, user_name, user_email, user_status, user_token, user_avatar, user_created_at, user_archived_at, user_role_id, role_name, role_is_admin, user_config_force_mfa FROM users $joins WHERE $filters AND {$shows[$show]['where']} ORDER BY {$sortable_columns[$sort]}, users.user_id ASC LIMIT $record_from, $record_to"); $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()")); while ($row = mysqli_fetch_assoc($sql)) { $users[intval($row['user_id'])] = $row; } } $logins = userLastLogins(array_keys($users)); $access = userClientAccess(array_keys($users)); $remembered = userRememberCounts(array_keys($users)); // How many in each view, over what the search and role leave $user_views = array_filter($shows, function ($s) { return $s['where'] !== null; }); $view_counts = []; $row = mysqli_fetch_row(mysqli_query($mysqli, "SELECT " . implode(', ', array_map(function ($s) { return "COALESCE(SUM({$s['where']}), 0)"; }, $user_views)) . " FROM users $joins WHERE $filters")); foreach (array_keys($user_views) as $i => $key) { $view_counts[$key] = intval($row[$i]); } if (isset($shows['invited'])) { $view_counts['invited'] = intval(mysqli_fetch_row(mysqli_query($mysqli, "SELECT COUNT(*) FROM user_invites $invite_joins WHERE $invite_filters"))[0]); } // Agent user limit (set in config.php, 0 / unset = unlimited) - an invite waiting holds a seat $agent_user_limit = getAgentUserLimit(); $agent_user_count = getActiveAgentUserCount(); $agent_invites_holding = userInvitesHoldingSeats(); $agent_user_limit_reached = $agent_user_limit > 0 && $agent_user_count + $agent_invites_holding >= $agent_user_limit; // An invite link to pass on - shown this once $invite_link = userInviteTakeLink(); $invite_mail_ready = userInviteMailReady(); $filter_url = function ($changes) { $params = array_merge($_GET, $changes); unset($params['page']); return '?' . http_build_query(array_filter($params, function ($v) { return $v !== '' && $v !== null; })); }; $sort_link = function ($column, $label) use ($url_query_strings_sort, $sort, $disp, $order_icon) { return '' . $label . ($sort === $column ? " $order_icon" : '') . ''; }; $csrf = $_SESSION['csrf_token']; ?>

Users 0) { ?> , of the agent user limit set for this installation"> of seats

The invite link for
Send it to yourself - by chat, text or your own email. With it they choose their own password and join as . It works for days and is only shown now - Get the link again makes a new one.
text-nowrap table-light"> Role removed' : '' . escapeHtml($invite['role_name']); $invite_pill = '' . $invite_label['label'] . ''; ?>
Clients
MFA required
·
Emailed 1 ? " · $invite_sent times" : '' ?> A link to pass on
text-nowrap table-light"> $row) { $is_self = $user_id === intval($session_user_id); $is_archived = !empty($row['user_archived_at']); $is_active = intval($row['user_status']) === 1 && !$is_archived; $is_admin = intval($row['role_is_admin']) === 1; $has_mfa = trim((string) $row['user_token']) !== ''; $display_name = $is_archived ? trim(str_replace(' (archived)', '', $row['user_name'])) : $row['user_name']; $name = escapeHtml($display_name); $quick_url = "modals/user/user.php?id=$user_id"; $user_access = $access[$user_id]; $access_title = $is_admin ? 'Admins see every client' : trim( ($user_access['allow'] ? 'Only: ' . implode(', ', $user_access['allow']) . '. ' : '') . ($user_access['deny'] ? 'Kept out of: ' . implode(', ', $user_access['deny']) . '.' : '') ); $remember_count = $remembered[$user_id] ?? 0; ?>
Clients
You
No role
remembered browser
'users', 'icon' => 'fa-users', 'filtered' => $q !== '' || $role_filter, 'archived' => $show === 'archived', 'views' => ['show'], 'view' => $show === 'nomfa' ? ['title' => 'Everyone active signs in with MFA', 'icon' => 'fa-check'] : ($show === 'invited' ? ['title' => 'No invites waiting', 'text' => 'Everyone invited has joined. Invite someone with New User.', 'icon' => 'fa-paper-plane'] : emptyStatePill($shows, $show)), ]; require_once "../includes/filter_footer.php"; ?>