= 2; $can_delete = lookupUserPermission('module_client') >= 3; $can_credentials = lookupUserPermission('module_credential') >= 1; $can_support = lookupUserPermission('module_support') >= 1; // The roles an MSP reaches for $contact_role_tabs = [ 'important' => ['label' => 'Important', 'icon' => 'fa-star', 'query' => "contact_important = 1"], 'billing' => ['label' => 'Billing', 'icon' => 'fa-file-invoice-dollar', 'query' => "contact_billing = 1"], 'technical' => ['label' => 'Technical', 'icon' => 'fa-tools', 'query' => "contact_technical = 1"], 'portal' => ['label' => 'Portal', 'icon' => 'fa-user-circle', 'query' => "contact_user_id > 0 AND COALESCE(user_auth_method, '') != ''"], ]; // Invites to the portal waiting to be accepted - their own tab, with its own list $invites_on = portalInvitesReady() && intval($config_client_portal_enable) === 1; $invites_view = $invites_on && ($_GET['role'] ?? '') === 'invites'; $role_filter = (isset($_GET['role']) && isset($contact_role_tabs[$_GET['role']])) ? $_GET['role'] : ''; $_GET['role'] = $role_filter; $role_query = $role_filter ? $contact_role_tabs[$role_filter]['query'] : "1 = 1"; // Tags Filter if (isset($_GET['tags']) && is_array($_GET['tags']) && !empty($_GET['tags'])) { $tag_filter = implode(",", array_map('intval', $_GET['tags'])); $tag_query = "AND contacts.contact_id IN (SELECT contact_tags.contact_id FROM contact_tags WHERE contact_tags.tag_id IN ($tag_filter))"; } else { $tag_filter = 0; $tag_query = ''; } $client = ''; if (!$client_url) { // Client Filter if (!empty($_GET['client'])) { $client = intval($_GET['client']); $client_query = "AND contact_client_id = $client"; } } if ($client_url && !empty($_GET['location'])) { // Location Filter $location_filter = intval($_GET['location']); $location_query = "AND contact_location_id = $location_filter"; } else { // Default - any $location_query = ''; $location_filter = 0; } // Only these sort - filter_header.php will hand through any lowercase word, and // an unknown column takes the page down with a mysqli exception $sortable_columns = ['contact_name', 'contact_title', 'contact_department', 'contact_email', 'location_name', 'client_name', 'contact_created_at']; if ($invites_view) { // Invites - newest first unless asked if (!in_array($sort, ['invite_email', 'invite_created_at', 'client_name'], true)) { $sort = 'invite_created_at'; $order = isset($_GET['order']) ? $order : 'DESC'; } } elseif (!in_array($sort, $sortable_columns, true)) { $sort = 'contact_name'; } // The invites tab's count - waiting, in date or run out - in this client or filter, for clients still here $invites_scope_sql = clientScopeSql('invite_client_id') . ($client_url ? " AND invite_client_id = $client_id" : ($client ? " AND invite_client_id = " . intval($client) : '')); $invites_count = $invites_on ? intval(mysqli_fetch_row(mysqli_query($mysqli, "SELECT COUNT(*) FROM portal_invites JOIN clients ON client_id = invite_client_id WHERE invite_kind = 'invite' AND invite_accepted_at IS NULL AND invite_cancelled_at IS NULL AND client_archived_at IS NULL $invites_scope_sql"))[0]) : 0; $show_invites_tab = $invites_on && ($client_url || $invites_count || $invites_view); // Tab counts - within the client, location, tag and archive scope, before the search narrows it $row = mysqli_fetch_assoc(mysqli_query($mysqli, " SELECT COUNT(*) AS all_count, SUM(contact_important = 1) AS important_count, SUM(contact_billing = 1) AS billing_count, SUM(contact_technical = 1) AS technical_count, SUM(contact_user_id > 0 AND COALESCE(user_auth_method, '') != '') AS portal_count FROM contacts LEFT JOIN clients ON client_id = contact_client_id LEFT JOIN users ON user_id = contact_user_id WHERE $archive_query $tag_query " . clientScopeSql('contact_client_id') . " $client_query $location_query ")); $tab_counts = ['' => intval($row['all_count'])]; foreach (array_keys($contact_role_tabs) as $tab_key) { $tab_counts[$tab_key] = intval($row[$tab_key . '_count']); } $invites = []; if ($invites_view) { $sql = mysqli_query($mysqli, "SELECT SQL_CALC_FOUND_ROWS invite_id, invite_email, invite_name, invite_billing, invite_technical, invite_expires_at, invite_sent_at, invite_sent_count, invite_created_at, invite_created_by, invite_created_by_contact_id, invite_kind, invite_accepted_at, invite_cancelled_at, invite_client_id, client_name, agent.user_name AS invite_by_agent, by_contact.contact_name AS invite_by_contact FROM portal_invites JOIN clients ON client_id = invite_client_id LEFT JOIN users agent ON agent.user_id = invite_created_by AND invite_created_by > 0 LEFT JOIN contacts by_contact ON by_contact.contact_id = invite_created_by_contact_id AND invite_created_by_contact_id > 0 WHERE invite_kind = 'invite' AND invite_accepted_at IS NULL AND invite_cancelled_at IS NULL AND client_archived_at IS NULL AND (invite_email LIKE '%$q%' OR invite_name LIKE '%$q%' OR client_name LIKE '%$q%') $invites_scope_sql ORDER BY $sort $order, invite_id DESC LIMIT $record_from, $record_to"); while ($row = mysqli_fetch_assoc($sql)) { $invites[] = $row; } } else { $sql = mysqli_query($mysqli, "SELECT SQL_CALC_FOUND_ROWS contacts.contact_id, contact_archived_at, contact_billing, contact_client_id, contact_department, contact_email, contact_extension, contact_important, contact_location_id, contact_mobile, contact_mobile_country_code, contact_name, contact_phone, contact_phone_country_code, contact_photo, contact_primary, contact_technical, contact_title, contact_user_id, client_id, client_name, location_archived_at, location_name, user_auth_method FROM contacts LEFT JOIN clients ON client_id = contact_client_id LEFT JOIN locations ON location_id = contact_location_id LEFT JOIN users ON user_id = contact_user_id WHERE $archive_query $tag_query AND ($role_query) AND (contact_name LIKE '%$q%' OR contact_title LIKE '%$q%' OR location_name LIKE '%$q%' OR contact_email LIKE '%$q%' OR contact_department LIKE '%$q%' OR contact_phone LIKE '%$phone_query%' OR contact_extension LIKE '%$q%' OR contact_mobile LIKE '%$phone_query%' OR client_name LIKE '%$q%' OR contacts.contact_id IN (SELECT contact_tags.contact_id FROM contact_tags JOIN tags ON tags.tag_id = contact_tags.tag_id WHERE tag_name LIKE '%$q%')) " . clientScopeSql('contact_client_id') . " $client_query $location_query ORDER BY contact_primary DESC, contact_important DESC, $sort $order LIMIT $record_from, $record_to" ); } $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()")); $contacts = []; while (!$invites_view && ($row = mysqli_fetch_assoc($sql))) { $contacts[intval($row['contact_id'])] = $row; } // Whether people at this client can sign up themselves, shown on its Invites tab $signup = null; if ($invites_view && $client_url) { $signup = [ 'on' => intval(mysqli_fetch_row(mysqli_query($mysqli, "SELECT client_portal_signup FROM clients WHERE client_id = $client_id"))[0] ?? 0) === 1, 'domains' => portalSignupDomains($client_id), 'everyone' => portalSignupOn(), ]; } // Tags and what is linked to each contact, for the whole page in a handful of // queries rather than five per row $contacts_tags = tagsForRecords('contact', array_keys($contacts)); $linked = ['assets' => [], 'credentials' => [], 'licenses' => [], 'tickets' => [], 'open_tickets' => []]; if ($contacts) { $ids = implode(',', array_keys($contacts)); $sql_linked = mysqli_query($mysqli, "SELECT asset_contact_id AS id, COUNT(*) AS n FROM assets WHERE asset_contact_id IN ($ids) AND asset_archived_at IS NULL GROUP BY asset_contact_id"); while ($row = mysqli_fetch_assoc($sql_linked)) { $linked['assets'][intval($row['id'])] = intval($row['n']); } if ($can_credentials) { $sql_linked = mysqli_query($mysqli, "SELECT credential_contact_id AS id, COUNT(*) AS n FROM credentials WHERE credential_contact_id IN ($ids) AND credential_archived_at IS NULL GROUP BY credential_contact_id"); while ($row = mysqli_fetch_assoc($sql_linked)) { $linked['credentials'][intval($row['id'])] = intval($row['n']); } } $sql_linked = mysqli_query($mysqli, "SELECT software_contacts.contact_id AS id, COUNT(*) AS n FROM software_contacts JOIN software ON software.software_id = software_contacts.software_id WHERE software_contacts.contact_id IN ($ids) AND software_archived_at IS NULL GROUP BY software_contacts.contact_id"); while ($row = mysqli_fetch_assoc($sql_linked)) { $linked['licenses'][intval($row['id'])] = intval($row['n']); } if ($can_support) { $sql_linked = mysqli_query($mysqli, "SELECT ticket_contact_id AS id, COUNT(*) AS n, SUM(ticket_resolved_at IS NULL AND ticket_closed_at IS NULL) AS open_n FROM tickets WHERE ticket_contact_id IN ($ids) AND ticket_archived_at IS NULL GROUP BY ticket_contact_id"); while ($row = mysqli_fetch_assoc($sql_linked)) { $linked['tickets'][intval($row['id'])] = intval($row['n']); $linked['open_tickets'][intval($row['id'])] = intval($row['open_n']); } } } $search_noun = $role_filter ? strtolower($contact_role_tabs[$role_filter]['label']) . ' contacts' : 'contacts'; $search_placeholder = $invites_view ? 'Search invites by email, name or client' : "Search $search_noun, phones, emails..."; ?>

Contacts

Signing up People here can sign up themselves with an address at one of the client's Domains - it has none yet. Anyone with an address at can make an account from the sign-in page, and joins as a plain contact. You're told when they do. On for , but signing up is off for every client in Admin > Settings > Client portal' : 'Admin > Settings > Client portal' ?>. Let anyone with an address at make their own account, instead of being invited.
text-nowrap table-light">
Invited Sees Sent Link Client Actions
Sent
Their own tickets' ?>
1 ? ' · sent ' . intval($row['invite_sent_count']) . ' times' : '' ?>
text-nowrap table-light"> $row) { $client_id = intval($row['client_id']); $client_name = escapeHtml($row['client_name']); $contact_name = escapeHtml($row['contact_name']); $contact_title = escapeHtml($row['contact_title']); $contact_department = escapeHtml($row['contact_department']); $contact_email = escapeHtml($row['contact_email']); $contact_phone_link = contactPhoneLink($row['contact_phone'], $row['contact_phone_country_code'], $row['contact_extension'], 'text-reset'); $contact_mobile_link = contactPhoneLink($row['contact_mobile'], $row['contact_mobile_country_code'], '', 'text-reset'); $contact_primary = intval($row['contact_primary']); $contact_archived_at = $row['contact_archived_at']; $contact_role_line = implode(' · ', array_filter([$contact_title, $contact_department])); $contact_tags = $contacts_tags[$contact_id] ?? []; $contact_url = "contact.php?client_id=$client_id&contact_id=$contact_id"; $assets_n = $linked['assets'][$contact_id] ?? 0; $credentials_n = $linked['credentials'][$contact_id] ?? 0; $licenses_n = $linked['licenses'][$contact_id] ?? 0; $tickets_n = $linked['tickets'][$contact_id] ?? 0; $open_tickets_n = $linked['open_tickets'][$contact_id] ?? 0; ?>
Name Reach Location Linked Client Actions
Archived
-
-
"> -
'invites', 'icon' => 'fa-paper-plane', 'views' => ['role'], 'title' => 'No invites waiting', 'text' => 'Invite people to the client portal by email - they put in their own name, phone and password when they accept, and are added to the contacts then.', 'actions' => $can_edit && $client_url ? [['label' => 'Invite people', 'modal' => "modals/contact/contact_invite.php?$client_url", 'icon' => 'fa-user-plus']] : [], ] : [ 'thing' => 'contacts', 'icon' => 'fa-address-book', 'views' => ['role'], 'view' => emptyStatePill($contact_role_tabs, $role_filter), 'text' => 'The people at the client - who to call, and who gets the invoices.', 'actions' => $can_edit ? array_merge( [['label' => 'New Contact', 'modal' => "modals/contact/contact_add.php?$client_url", 'size' => 'lg']], $client_url ? [['label' => 'Import', 'modal' => "modals/contact/contact_import.php?$client_url", 'icon' => 'fa-upload']] : [] ) : [], ]; ?>