'; } $tone = contactAvatarTone($contact_id); return ''; } /** * A contact wherever a page mentions one: their avatar, then their name, * linked to their quick look - or to their page with 'page' => true. * Other options: 'size' of the avatar ('xs' unless given), 'class' on the * link, 'archived' to strike the name through. * Takes values raw or already escaped. */ function contactLink($client_id, $contact_id, $name, $photo = '', $options = []) { $client_id = intval($client_id); $contact_id = intval($contact_id); $name = html_entity_decode((string) $name, ENT_QUOTES, 'UTF-8'); $label = escapeHtml($name !== '' ? $name : 'Unnamed contact'); $archived = !empty($options['archived']); $class = trim('text-truncate ' . ($options['class'] ?? '') . ($archived ? ' text-decoration-line-through text-body-secondary' : '')); $title = $archived ? ' title="Archived contact"' : ''; $link = !empty($options['page']) ? "$label" : "$label"; return '' . contactAvatar($client_id, $contact_id, $name, $photo, $options['size'] ?? 'xs') . $link . ''; } /** The quieter line under a contact in a picker: their title, and Primary or Technical. */ function contactOptionDescription($row) { $role = !empty($row['contact_primary']) ? 'Primary' : (!empty($row['contact_technical']) ? 'Technical' : ''); return implode(' ยท ', array_filter([trim(html_entity_decode((string) ($row['contact_title'] ?? ''), ENT_QUOTES, 'UTF-8')), $role])); } /** For JSON (pickers, search): what contactAvatar() draws - the photo's URL, or their initials and colour. */ function contactAvatarData($client_id, $contact_id, $name, $photo) { $photo = html_entity_decode((string) $photo, ENT_QUOTES, 'UTF-8'); if ($photo !== '') { return ['image' => '/uploads/clients/' . intval($client_id) . '/' . rawurlencode($photo)]; } return ['initials' => personInitials($name) ?: '?', 'tone' => contactAvatarTone($contact_id)]; } /** * How they log in to the client portal, or '' with no access: 'Password' * or 'Microsoft'. */ function contactPortalAccess($contact_user_id, $auth_method) { if (!intval($contact_user_id) || (string) $auth_method === '') { return ''; } return $auth_method === 'azure' ? 'Microsoft' : 'Password'; } /** * Their flags as pills: Primary (or Important when not primary - a primary * contact is always important), Billing, Technical, and Portal when they can * log in to the client portal. */ function contactFlagPills($row, $with_portal = true) { $pills = []; if (!empty($row['contact_primary'])) { $pills[] = ['Primary', 'text-bg-success', 'The client\'s main contact']; } elseif (!empty($row['contact_important'])) { $pills[] = ['Important', 'bg-warning-subtle text-warning-emphasis', 'Pinned to the top of the list']; } if (!empty($row['contact_billing'])) { $pills[] = ['Billing', 'bg-secondary-subtle text-secondary-emphasis', 'Receives invoices']; } if (!empty($row['contact_technical'])) { $pills[] = ['Technical', 'bg-secondary-subtle text-secondary-emphasis', 'Technical contact']; } if ($with_portal) { $portal = contactPortalAccess($row['contact_user_id'] ?? 0, $row['user_auth_method'] ?? ''); if ($portal) { $pills[] = ['Portal', 'bg-info-subtle text-info-emphasis', "Client portal login ($portal)"]; } } $html = ''; foreach ($pills as [$label, $class, $title]) { $html .= '' . $label . ' '; } return rtrim($html); } /** A phone number as a tel: link, with its extension. '' when there is no number. */ function contactPhoneLink($phone, $country_code, $extension = '', $class = '') { $phone = (string) $phone; if ($phone === '') { return ''; } $display = escapeHtml(formatPhoneNumber($phone, $country_code)); $country_digits = preg_replace('/[^0-9]/', '', (string) $country_code); $dial = escapeHtml(($country_digits !== '' ? "+$country_digits" : '') . preg_replace('/[^0-9]/', '', $phone)); $extension = escapeHtml($extension); return '' . $display . '' . ($extension ? ' x' . $extension . '' : ''); } /* * Quick picker (js/quick_pick.js) for a contact's location - the same picker * the assets use, with the contact's client as the scope. */ /** The contact and their location, by id. */ function contactQuickRow($contact_id) { global $mysqli; return mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT contact_id, contact_name, contact_client_id, contact_location_id, location_name, location_archived_at FROM contacts LEFT JOIN locations ON location_id = contact_location_id WHERE contact_id = " . intval($contact_id))) ?: null; } /** What the location picker shows - the asset location pill, fed the contact's row. */ function contactLocationHtml($row, $style = 'list') { return assetQuickHtml('asset_location', [ 'asset_client_id' => $row['contact_client_id'] ?? 0, 'asset_location_id' => $row['contact_location_id'] ?? 0, 'location_name' => $row['location_name'] ?? '', 'location_archived_at' => $row['location_archived_at'] ?? null, ], $style); } /** Move a contact to one of their client's locations (0 for none), as Edit Contact would. Returns [ok, plain-text message]. */ function contactQuickSetLocation($contact_id, $value) { global $mysqli, $session_name; $row = contactQuickRow($contact_id); if (!$row) { return [false, 'That contact no longer exists.']; } $contact_id = intval($row['contact_id']); $client_id = intval($row['contact_client_id']); $location_id = assetClientOwnedId('location', $value, $client_id); if (intval($value) && !$location_id) { return [false, 'That location is not one of this client\'s.']; } if ($location_id === intval($row['contact_location_id'])) { return [true, '']; } $location_name = $location_id ? (string) mysqli_fetch_row(mysqli_query($mysqli, "SELECT location_name FROM locations WHERE location_id = $location_id"))[0] : ''; mysqli_query($mysqli, "UPDATE contacts SET contact_location_id = $location_id WHERE contact_id = $contact_id"); $contact_name = escapeSql($row['contact_name']); logAudit("Contact", "Edit", $location_id ? "$session_name set the location of contact $contact_name to " . escapeSql($location_name) : "$session_name removed the location of contact $contact_name", $client_id, $contact_id); return [true, $location_id ? "Moved to $location_name" : 'Location removed']; } /** What quick_pick.js redraws after a change. */ function contactQuickRecord($contact_id) { $row = contactQuickRow($contact_id); return [ 'current' => (string) intval($row['contact_location_id']), 'html' => ['list' => contactLocationHtml($row, 'list'), 'detail' => contactLocationHtml($row, 'detail')], ]; }