Categories wins; otherwise the built-in statuses get a sensible * default and anything else is grey. * * Returns ['class' => bootstrap tone or '', 'style' => inline background or ''] */ function assetStatusBadge($status, $category_color = '') { $category_color = trim((string) $category_color); if (preg_match('/^(#[0-9a-f]{3,8}|[a-z]{3,24})$/i', $category_color)) { return ['class' => '', 'style' => "background-color: $category_color"]; } $defaults = [ 'Deployed' => 'success', 'Ready to Deploy' => 'info', 'Out for Repair' => 'warning', 'Lost' => 'danger', 'Stolen' => 'danger', 'Retired' => 'secondary', ]; return ['class' => $defaults[(string) $status] ?? 'secondary', 'style' => '']; } /** Status colours from Admin > Categories, keyed by status name (raw). One query per page. */ function assetStatusColors() { global $mysqli; static $colors = null; if ($colors === null) { $colors = []; $sql = mysqli_query($mysqli, "SELECT category_name, category_color FROM categories WHERE category_type = 'asset_status' AND category_archived_at IS NULL"); while ($row = mysqli_fetch_assoc($sql)) { $colors[$row['category_name']] = (string) $row['category_color']; } } return $colors; } /** The colour a status is drawn in, as a CSS value - the same as its badge. */ function assetStatusColor($status, $category_color = '') { $badge = assetStatusBadge($status, $category_color); return $badge['style'] ? trim(substr($badge['style'], strlen('background-color:'))) : "var(--bs-{$badge['class']})"; } /** The status badge, ready to echo. */ function assetStatusPill($status) { $status = (string) $status; if ($status === '') { return ''; } $badge = assetStatusBadge($status, assetStatusColors()[$status] ?? ''); $class = $badge['class'] ? "text-bg-{$badge['class']}" : 'text-white'; $style = $badge['style'] ? " style=\"{$badge['style']}\"" : ''; return "" . escapeHtml($status) . ""; } /** OS name fragments and their brand icons (Font Awesome 5), first match wins. Also drives the asset form's OS icon. */ function assetOsIconMap() { return [ 'windows' => 'fab fa-windows', 'macos' => 'fab fa-apple', 'mac os' => 'fab fa-apple', 'ios' => 'fab fa-apple', 'ipados' => 'fab fa-apple', 'android' => 'fab fa-android', 'ubuntu' => 'fab fa-ubuntu', 'red hat' => 'fab fa-redhat', 'rhel' => 'fab fa-redhat', 'centos' => 'fab fa-centos', 'fedora' => 'fab fa-fedora', 'suse' => 'fab fa-suse', 'freebsd' => 'fab fa-freebsd', 'pfsense' => 'fab fa-freebsd', 'opnsense' => 'fab fa-freebsd', 'truenas' => 'fab fa-freebsd', 'raspberry' => 'fab fa-raspberry-pi', 'linux' => 'fab fa-linux', 'debian' => 'fab fa-linux', 'mint' => 'fab fa-linux', 'proxmox' => 'fab fa-linux', 'pop!_os' => 'fab fa-linux', 'firmware' => 'fas fa-microchip', 'routeros' => 'fas fa-microchip', ]; } /** A brand icon for the operating system, where one is recognisable (Font Awesome 5 class). */ function assetOsIcon($os) { $os = strtolower((string) $os); foreach (assetOsIconMap() as $needle => $icon) { if (strpos($os, $needle) !== false) { return $icon; } } return 'fas fa-compact-disc'; } /** * Where the warranty stands, or null when there's no date. * * Returns ['tone' => danger|warning|success, 'short' => "Expired" / "23 days left" / "Covered", * 'text' => "Expired 3 months ago" / "Expires in 23 days" / "Until 2028-11-08", * 'days' => days left (negative once expired)] */ function assetWarranty($warranty_expire, $soon_days = 90) { $warranty_expire = trim((string) $warranty_expire); if ($warranty_expire === '' || strpos($warranty_expire, '0000-00-00') === 0) { return null; } $expires = DateTime::createFromFormat('!Y-m-d', substr($warranty_expire, 0, 10)); if (!$expires) { return null; } $today = new DateTime('today'); $days = (int) $today->diff($expires)->format('%r%a'); $date = $expires->format('Y-m-d'); if ($days < 0) { return ['tone' => 'danger', 'short' => 'Expired', 'text' => 'Expired ' . timeAgo($date), 'days' => $days, 'date' => $date]; } if ($days <= $soon_days) { $left = $days === 0 ? 'today' : ($days === 1 ? '1 day left' : "$days days left"); return ['tone' => 'warning', 'short' => $left, 'text' => $days === 0 ? 'Expires today' : "Expires in $days day" . ($days === 1 ? '' : 's'), 'days' => $days, 'date' => $date]; } return ['tone' => 'success', 'short' => 'Covered', 'text' => "Until $date", 'days' => $days, 'date' => $date]; } /** "3 yrs 2 mos" since a date, or '' without one. For purchase / install dates. */ function assetAge($date) { $date = trim((string) $date); if ($date === '' || strpos($date, '0000-00-00') === 0) { return ''; } $from = DateTime::createFromFormat('!Y-m-d', substr($date, 0, 10)); if (!$from) { return ''; } $diff = $from->diff(new DateTime('today')); if ($diff->invert) { return ''; } $parts = []; if ($diff->y) { $parts[] = $diff->y . ' yr' . ($diff->y === 1 ? '' : 's'); } if ($diff->m) { $parts[] = $diff->m . ' mo' . ($diff->m === 1 ? '' : 's'); } if (!$parts) { return $diff->d <= 1 ? 'new' : $diff->d . ' days'; } return implode(' ', $parts); } /** * The asset's links (URI, URI 2, client URI) as [label, url] pairs, skipping * empty ones. Raw - escapeUrl() each url before it goes in an href. */ function assetLinks($uri, $uri_2, $uri_client) { $links = []; foreach ([['Open', $uri], ['Open 2', $uri_2], ['Client link', $uri_client]] as [$label, $url]) { $url = trim((string) $url); if ($url !== '') { $links[] = [$label, $url]; } } return $links; } /** A short name for a link, by what it opens: "Web", "SSH", "RDP"... The full URL goes in its title. */ function assetLinkLabel($url) { $scheme = strtolower((string) parse_url((string) $url, PHP_URL_SCHEME)); $labels = ['http' => 'Web', 'https' => 'Web', 'ssh' => 'SSH', 'telnet' => 'Telnet', 'rdp' => 'RDP', 'vnc' => 'VNC', 'ftp' => 'FTP', 'sftp' => 'SFTP', 'smb' => 'SMB']; return $labels[$scheme] ?? ($scheme !== '' ? strtoupper($scheme) : 'Link'); } /** Link schemes and their icons. Also drives the asset form's link icons. */ function assetLinkIconMap() { return [ 'ssh' => 'fas fa-terminal', 'telnet' => 'fas fa-terminal', 'rdp' => 'fas fa-desktop', 'vnc' => 'fas fa-desktop', 'ftp' => 'fas fa-folder-open', 'sftp' => 'fas fa-folder-open', 'smb' => 'fas fa-folder-open', 'http' => 'fas fa-globe', 'https' => 'fas fa-globe', ]; } /** Icon for a link by its scheme. */ function assetLinkIcon($url) { $scheme = strtolower((string) parse_url((string) $url, PHP_URL_SCHEME)); return assetLinkIconMap()[$scheme] ?? 'fas fa-external-link-alt'; } /** * $id if that location / contact / vendor / network / asset belongs to the * client, else 0. The handlers run their picks through this so a hand-edited * POST can't hang another client's contact or location off an asset. */ function assetClientOwnedId($kind, $id, $client_id) { global $mysqli; $tables = [ 'location' => ['locations', 'location_id', 'location_client_id'], 'contact' => ['contacts', 'contact_id', 'contact_client_id'], 'vendor' => ['vendors', 'vendor_id', 'vendor_client_id'], 'network' => ['networks', 'network_id', 'network_client_id'], 'asset' => ['assets', 'asset_id', 'asset_client_id'], ]; $id = intval($id); $client_id = intval($client_id); if (!$id || !isset($tables[$kind])) { return 0; } [$table, $id_column, $client_column] = $tables[$kind]; $sql = mysqli_query($mysqli, "SELECT 1 FROM $table WHERE $id_column = $id AND $client_column = $client_id LIMIT 1"); return mysqli_num_rows($sql) ? $id : 0; } /* * Quick pickers (js/quick_pick.js) for an asset's status, location and who it * is assigned to - changed where they show on the assets list and the asset * page instead of through Edit. Wrapped in quickPick() (functions/ticket_fields.php) * with the asset's client as the scope, since locations and contacts are per client. */ /** The asset fields a quick picker can change. */ const ASSET_QUICK_FIELDS = ['asset_status', 'asset_location', 'asset_contact']; /** The asset and what its pickers show, by id - the row assetQuickHtml() and assetContactDetails() draw from. */ function assetQuickRow($asset_id) { global $mysqli; return mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT asset_id, asset_name, asset_status, asset_client_id, asset_location_id, location_name, location_archived_at, asset_contact_id, contact_name, contact_photo, contact_archived_at, contact_email, contact_phone, contact_phone_country_code, contact_extension, contact_mobile, contact_mobile_country_code FROM assets LEFT JOIN locations ON location_id = asset_location_id LEFT JOIN contacts ON contact_id = asset_contact_id WHERE asset_id = " . intval($asset_id))) ?: null; } /** * What a picker shows for the value it holds now. $style is 'list' (a pill, * on the assets list and in the asset page header) or 'detail' (a line in the * asset page's Where & who card). $row is raw, as assetQuickRow() returns it. */ function assetQuickHtml($field, $row, $style = 'list') { $client_id = intval($row['asset_client_id'] ?? 0); $pill = 'badge rounded-pill fw-normal d-inline-flex align-items-center gap-1 itflow-asset-pill'; $empty = "$pill itflow-asset-pill-empty"; if ($field === 'asset_status') { return assetStatusPill($row['asset_status'] ?? '') ?: "No status"; } if ($field === 'asset_location') { if (!intval($row['asset_location_id'] ?? 0) || (string) ($row['location_name'] ?? '') === '') { return $style === 'detail' ? 'No location' : "No location"; } $archived = !empty($row['location_archived_at']); $name = '' . escapeHtml($row['location_name']) . ''; return $style === 'detail' ? '$name" : "$name"; } if ($field === 'asset_contact') { if (!intval($row['asset_contact_id'] ?? 0) || (string) ($row['contact_name'] ?? '') === '') { return $style === 'detail' ? 'Nobody' : "Nobody"; } $archived = !empty($row['contact_archived_at']); $avatar = contactAvatar($client_id, $row['asset_contact_id'], $row['contact_name'], $row['contact_photo'] ?? '', 'xs'); $name = '' . escapeHtml($row['contact_name']) . ''; return $style === 'detail' ? "$avatar$name" : "$avatar$name"; } return ''; } /** Under the asset page's Assigned picker: how to reach that person, and a link to them. Redrawn when the contact changes. */ function assetContactDetails($row) { $contact_id = intval($row['asset_contact_id'] ?? 0); if (!$contact_id || (string) ($row['contact_name'] ?? '') === '') { return ''; } $client_id = intval($row['asset_client_id']); $email = escapeHtml($row['contact_email'] ?? ''); $phone = escapeHtml(formatPhoneNumber($row['contact_phone'] ?? '', $row['contact_phone_country_code'] ?? '')); $extension = escapeHtml($row['contact_extension'] ?? ''); $mobile = escapeHtml(formatPhoneNumber($row['contact_mobile'] ?? '', $row['contact_mobile_country_code'] ?? '')); $html = ''; if ($email) { $html .= "
"; } if ($phone) { $html .= "