Files
itflow/agent/asset.php
johnnyq 9e55d52432 Vendors overhaul for daily MSP work: new vendor page built around calling them, with Call support (dials the extension), Email, Support site, a star, and a New menu (case/ticket with them, license from them, asset bought from them, portal login, document, upload, or expense and recurring expense for our own vendors), Support card (phone, hours, SLA, email, site), Account card (account number, PIN shown as dots with show and copy, account manager name, direct line and email), blur-to-save notes, an Also used by list of the agent's other clients with the same vendor by name or template, and tabs for Cases (open first, with their ticket number), Licenses (soonest renewal first), Assets (serial, order ref, warranty status), Domains (registrar, web host, DNS, mail), Services, Logins, Documents, Files and History; our own vendors show spend this year, last year and in all, recent expenses and recurring expenses; new vendor quick look; rebuilt vendors list with starred first, initials tiles coloured by vendor name, support line, hours and SLA, account number and PIN, chips for open cases, licenses, assets, domains, services and logins, an Ours / Clients' switch with an all-clients view in All Client Docs, wider search (account manager, emails, tags), tag filter and bulk Tags/Archive/Restore/Delete; shared tabbed vendor form (Details, Support, Account, Notes) with star, tags and start from template that fills every field; Add a Template to Clients to roll one vendor out to many clients, skipping those that have it; Admin Vendor Templates shows how many clients use each template and has Add to clients; vendor tags (tag type 6) with vendor tags in the Tags starter pack; ticket vendor block shows their ticket number, support line, hours, SLA, account number and PIN with copy buttons; vendor tiles in every vendor picker and quick look links across tickets, licenses, assets, domains, services and expense lists; search opens the vendor page; CSV export adds tags, open cases, account manager and PIN; logins, documents and files can be linked to a vendor and ones made from a vendor's page are linked to it; demo vendors get tags, a star and an account manager; database update 2.8.2 adds vendor_tags and vendor_contact_phone_country_code, vendor_contact_phone, vendor_contact_email (db.sql updated) and clears references to vendors that no longer exist; ticket vendor now validated against the ticket's client and removing a vendor from a ticket is logged as such; deleting a vendor from a template no longer unlinks every other vendor from that template; deleting a vendor clears it from assets, licenses, tickets and domains, and vendors with expenses, recurring expenses or payment provider fees are archived rather than deleted; vendor websites without a scheme open correctly; domain form vendor names escaped; new vendor templates keep the phone country code and pushing a template update with nothing ticked no longer errors; vendors API accepts the account manager fields; new functions/vendor.php (VENDOR_USES, vendorCan/vendorEnforce with client vendors on module_client and ours on module_financial, tile, link, website, PIN, form, tags, counts, delete check, reference clearing, also used by, spend, history), credential|vendor and file|vendor link relations, tag module_ours for our own vendors, handlers add_vendor_template_to_clients, favorite_vendor, unfavorite_vendor and ajax vendor_set_notes, vendor_model.php removed
2026-10-14 22:49:05 -04:00

1274 lines
88 KiB
PHP

<?php
/*
* Asset - one asset.
*
* Built around what a tech needs from the box: what it is and its status
* (header), how to get on it (IP, MAC, links - each one copies or opens in a
* click), what it physically is and whether it is still under warranty, where
* it is and who has it. Everything linked to it - interfaces, credentials,
* tickets, licenses, documents, files, services, notes - sits in tabs with
* counts, so nothing is hidden just because it is empty and the page does not
* run to ten screens. The open tab follows the URL hash (#tickets) and is
* remembered between visits.
*/
// If client_id is in URI then show client Side Bar and client header
if (isset($_GET['client_id'])) {
require_once "includes/inc_all_client.php";
$client_query = "AND asset_client_id = $client_id";
$client_url = "client_id=$client_id&";
} else {
require_once "includes/inc_client_overview_all.php";
$client_query = '';
$client_url = '';
}
// Perms
enforceUserPermission('module_support');
$asset_id = intval($_GET['asset_id'] ?? 0);
$sql = mysqli_query($mysqli, "SELECT asset_archived_at, asset_client_id, asset_contact_id, asset_created_at, asset_description, asset_favorite, asset_id,
asset_install_date, asset_location_id, asset_make, asset_model, asset_name, asset_notes,
asset_os, asset_photo, asset_physical_location, asset_purchase_date,
asset_purchase_reference, asset_serial, asset_status, asset_type, asset_uri, asset_uri_2,
asset_uri_client, asset_vendor_id, asset_warranty_expire, client_id, client_name,
contact_archived_at, contact_email, contact_extension, contact_mobile,
contact_mobile_country_code, contact_name, contact_phone, contact_phone_country_code, contact_photo,
interface_ip, interface_ipv6, interface_mac, interface_nat_ip, interface_network_id,
location_archived_at, location_name, network_name, vendor_name FROM assets
LEFT JOIN clients ON client_id = asset_client_id
LEFT JOIN contacts ON asset_contact_id = contact_id
LEFT JOIN locations ON asset_location_id = location_id
LEFT JOIN vendors ON asset_vendor_id = vendor_id
LEFT JOIN asset_interfaces ON interface_asset_id = asset_id AND interface_primary = 1
LEFT JOIN networks ON network_id = interface_network_id
WHERE asset_id = $asset_id
" . clientScopeSql('asset_client_id') . "
$client_query
LIMIT 1
");
if (mysqli_num_rows($sql) == 0) {
echo "<center><h1 class='text-secondary mt-5'>Nothing to see here</h1><a class='btn btn-lg btn-secondary mt-3' href='javascript:history.back()'><i class='fa fa-fw fa-arrow-left'></i> Go Back</a></center>";
} else {
$row = mysqli_fetch_assoc($sql);
$asset_row = $row; // raw, for the quick pickers - $row gets reused below
$client_id = intval($row['client_id']);
$client_name = escapeHtml($row['client_name']);
$asset_id = intval($row['asset_id']);
$asset_type = escapeHtml($row['asset_type']);
$asset_name = escapeHtml($row['asset_name']);
$asset_description = escapeHtml($row['asset_description']);
$asset_make_model = escapeHtml(trim($row['asset_make'] . ' ' . $row['asset_model']));
$asset_serial = escapeHtml($row['asset_serial']);
$asset_os = escapeHtml($row['asset_os']);
$asset_os_icon = assetOsIcon($row['asset_os']);
$asset_links = assetLinks($row['asset_uri'], $row['asset_uri_2'], $row['asset_uri_client']);
$asset_status_pill = assetStatusPill($row['asset_status']);
$asset_purchase_reference = escapeHtml($row['asset_purchase_reference']);
$asset_purchase_date = escapeHtml($row['asset_purchase_date']);
$asset_purchase_age = assetAge($row['asset_purchase_date']);
$asset_install_date = escapeHtml($row['asset_install_date']);
$asset_install_age = assetAge($row['asset_install_date']);
$asset_warranty = assetWarranty($row['asset_warranty_expire']);
$asset_photo = escapeHtml($row['asset_photo']);
$asset_physical_location = escapeHtml($row['asset_physical_location']);
$asset_notes = escapeHtml($row['asset_notes']);
$asset_favorite = intval($row['asset_favorite']);
$asset_created_at = escapeHtml($row['asset_created_at']);
$asset_archived_at = escapeHtml($row['asset_archived_at']);
$asset_vendor_id = intval($row['asset_vendor_id']);
$asset_contact_id = intval($row['asset_contact_id']);
$asset_tone = assetTypeTone($row['asset_type']);
$device_icon = getAssetIcon($row['asset_type']);
$asset_ip = escapeHtml($row['interface_ip']);
$asset_ipv6 = escapeHtml($row['interface_ipv6']);
$asset_nat_ip = escapeHtml($row['interface_nat_ip']);
$asset_mac = escapeHtml($row['interface_mac']);
$asset_network_id = intval($row['interface_network_id']);
$network_name = escapeHtml($row['network_name']);
$vendor_name = escapeHtml($row['vendor_name']);
$contact_name = escapeHtml($row['contact_name']);
$contact_archived = !empty($row['contact_archived_at']);
$location_name = escapeHtml($row['location_name']);
$location_archived = !empty($row['location_archived_at']);
$can_edit = lookupUserPermission('module_support') >= 2;
$can_delete = lookupUserPermission('module_support') >= 3;
$can_credentials = lookupUserPermission('module_credential') >= 1;
// The rack it sits in, if any
$asset_rack = rackPositionForAsset($asset_id);
if ($asset_rack) {
$asset_rack_id = intval($asset_rack['rack_id']);
$asset_rack_name = escapeHtml($asset_rack['rack_name']);
$asset_rack_position = rackPositionLabel($asset_rack['unit_start_number'], $asset_rack['unit_end_number']) . ', ' . ($asset_rack['unit_face'] === 'rear' ? 'rear' : 'front');
}
// Override Tab Title // No Sanitizing needed as this var will opnly be used in the tab title
$page_title = $row['asset_name'];
// Tags - functions/tags.php
$asset_tags_display = tagPills('asset', $asset_id, tagsForRecord('asset', $asset_id), "assets.php?client_id=$client_id&");
// Tickets - linked as the ticket's asset or as one of its additional assets
$sql_related_tickets = mysqli_query($mysqli, "
SELECT tickets.ticket_id, ticket_prefix, ticket_number, ticket_subject, ticket_priority, ticket_created_at,
ticket_updated_at, ticket_resolved_at, ticket_closed_at, ticket_assigned_to,
user_name, user_avatar, ticket_status_id, ticket_status_name, ticket_status_color
FROM tickets
LEFT JOIN users ON ticket_assigned_to = user_id
LEFT JOIN ticket_statuses ON ticket_status_id = ticket_status
LEFT JOIN ticket_assets ON tickets.ticket_id = ticket_assets.ticket_id
WHERE ticket_asset_id = $asset_id OR ticket_assets.asset_id = $asset_id
GROUP BY tickets.ticket_id
ORDER BY (ticket_resolved_at IS NULL AND ticket_closed_at IS NULL) DESC, ticket_number DESC
");
$ticket_count = mysqli_num_rows($sql_related_tickets);
$ticket_open_count = 0;
$related_tickets = [];
while ($row = mysqli_fetch_assoc($sql_related_tickets)) {
$related_tickets[] = $row;
if (empty($row['ticket_resolved_at']) && empty($row['ticket_closed_at'])) {
$ticket_open_count++;
}
}
// Recurring tickets
$sql_related_recurring_tickets = mysqli_query($mysqli, "SELECT recurring_tickets.recurring_ticket_id, recurring_ticket_subject,
recurring_ticket_priority, recurring_ticket_frequency, recurring_ticket_next_run
FROM recurring_tickets
LEFT JOIN recurring_ticket_assets ON recurring_tickets.recurring_ticket_id = recurring_ticket_assets.recurring_ticket_id
WHERE recurring_ticket_asset_id = $asset_id OR recurring_ticket_assets.asset_id = $asset_id
GROUP BY recurring_tickets.recurring_ticket_id
ORDER BY recurring_ticket_next_run ASC"
);
$recurring_ticket_count = mysqli_num_rows($sql_related_recurring_tickets);
// Network interfaces, and what each one is plugged into
$sql_related_interfaces = mysqli_query($mysqli, "
SELECT
ai.interface_id,
ai.interface_name,
ai.interface_description,
ai.interface_type,
ai.interface_mac,
ai.interface_ip,
ai.interface_nat_ip,
ai.interface_ipv6,
ai.interface_primary,
n.network_name,
n.network_id,
connected_interfaces.interface_name AS connected_interface_name,
connected_assets.asset_name AS connected_asset_name,
connected_assets.asset_id AS connected_asset_id,
connected_assets.asset_type AS connected_asset_type
FROM asset_interfaces AS ai
LEFT JOIN networks AS n
ON n.network_id = ai.interface_network_id
LEFT JOIN asset_interface_links AS ail
ON (ail.interface_a_id = ai.interface_id OR ail.interface_b_id = ai.interface_id)
LEFT JOIN asset_interfaces AS connected_interfaces
ON (
(ail.interface_a_id = ai.interface_id AND ail.interface_b_id = connected_interfaces.interface_id)
OR
(ail.interface_b_id = ai.interface_id AND ail.interface_a_id = connected_interfaces.interface_id)
)
LEFT JOIN assets AS connected_assets
ON connected_assets.asset_id = connected_interfaces.interface_asset_id
WHERE
ai.interface_asset_id = $asset_id
AND ai.interface_archived_at IS NULL
ORDER BY ai.interface_primary DESC, ai.interface_name ASC
");
$interface_count = mysqli_num_rows($sql_related_interfaces);
// Files
$sql_related_files = mysqli_query($mysqli, "SELECT file_created_at, file_description, file_ext, files.file_id, file_name FROM asset_files
LEFT JOIN files ON asset_files.file_id = files.file_id
WHERE asset_files.asset_id = $asset_id
AND file_archived_at IS NULL
ORDER BY file_name ASC"
);
$files_count = mysqli_num_rows($sql_related_files);
// Documents
$sql_related_documents = mysqli_query($mysqli, "SELECT document_created_at, document_description, documents.document_id, document_name,
document_updated_at, user_name FROM asset_documents, documents
LEFT JOIN users ON document_created_by = user_id
WHERE asset_documents.asset_id = $asset_id
AND asset_documents.document_id = documents.document_id
AND document_archived_at IS NULL
ORDER BY document_name ASC"
);
$document_count = mysqli_num_rows($sql_related_documents);
// Credentials
$credential_count = 0;
if ($can_credentials) {
$sql_related_credentials = mysqli_query($mysqli, "
SELECT credential_id, credential_name, credential_description, credential_uri,
credential_username, credential_otp_secret
FROM credentials
WHERE credential_asset_id = $asset_id
AND credential_archived_at IS NULL
ORDER BY credential_name ASC
");
$credential_count = mysqli_num_rows($sql_related_credentials);
}
// Licenses it holds a seat on, with each one's seats in use and the key it was given
$held_software = softwareHeldBy('asset', $asset_id);
$software_count = count($held_software);
// Linked services
$sql_linked_services = mysqli_query($mysqli, "SELECT service_category, service_description, service_assets.service_id, service_importance,
service_name FROM service_assets, services
WHERE service_assets.asset_id = $asset_id
AND service_assets.service_id = services.service_id
ORDER BY service_name ASC"
);
$service_count = mysqli_num_rows($sql_linked_services);
// Notes - 1 to many relationship
$sql_related_notes = mysqli_query($mysqli, "SELECT asset_note, asset_note_created_at, asset_note_id, asset_note_type, user_name FROM asset_notes
LEFT JOIN users ON asset_note_created_by = user_id
WHERE asset_note_asset_id = $asset_id
AND asset_note_archived_at IS NULL
ORDER BY asset_note_created_at DESC"
);
$note_count = mysqli_num_rows($sql_related_notes);
// Note type icons, read from the categories list so the seeded icons
// stay the single source of truth
$note_type_icons = array();
$sql_note_type_icons = mysqli_query($mysqli, "SELECT category_name, category_icon FROM categories WHERE category_type = 'asset_note_type'");
while ($row = mysqli_fetch_assoc($sql_note_type_icons)) {
$note_type_icons[escapeHtml($row['category_name'])] = escapeHtml($row['category_icon']);
}
// The tabs, in the order a tech reaches for them
$asset_tabs = [
'interfaces' => ['label' => 'Interfaces', 'icon' => 'fa-ethernet', 'count' => $interface_count],
'credentials' => ['label' => 'Credentials', 'icon' => 'fa-key', 'count' => $credential_count],
'tickets' => ['label' => 'Tickets', 'icon' => 'fa-life-ring', 'count' => $ticket_count, 'badge' => $ticket_open_count],
'recurring' => ['label' => 'Recurring', 'icon' => 'fa-redo-alt', 'count' => $recurring_ticket_count],
'licenses' => ['label' => 'Licenses', 'icon' => 'fa-cube', 'count' => $software_count],
'documents' => ['label' => 'Documents', 'icon' => 'fa-file-alt', 'count' => $document_count],
'files' => ['label' => 'Files', 'icon' => 'fa-paperclip', 'count' => $files_count],
'services' => ['label' => 'Services', 'icon' => 'fa-stream', 'count' => $service_count],
'notes' => ['label' => 'Notes', 'icon' => 'fa-sticky-note', 'count' => $note_count],
];
if (!$can_credentials) {
unset($asset_tabs['credentials']);
}
?>
<ol class="breadcrumb d-print-none">
<li class="breadcrumb-item">
<a href="client_overview.php?client_id=<?= $client_id ?>"><?= $client_name ?></a>
</li>
<li class="breadcrumb-item">
<a href="assets.php?client_id=<?= $client_id ?>">Assets</a>
</li>
<li class="breadcrumb-item active"><?= $asset_name ?></li>
</ol>
<!-- What it is -->
<div class="card mb-3">
<div class="card-body d-flex flex-wrap align-items-center gap-3">
<?php if ($asset_photo) { ?>
<a href="<?= "../uploads/clients/$client_id/$asset_photo" ?>" target="_blank" rel="noopener" title="Open photo">
<img class="itflow-asset-icon itflow-asset-icon-lg" src="<?= "../uploads/clients/$client_id/$asset_photo" ?>" alt="Photo of <?= $asset_name ?>">
</a>
<?php } else { ?>
<span class="itflow-asset-icon itflow-asset-icon-lg itflow-asset-tone-<?= $asset_tone ?>" aria-hidden="true"><i class="fas fa-fw fa-<?= $device_icon ?>"></i></span>
<?php } ?>
<div class="flex-grow-1 min-w-0">
<h1 class="h4 fw-semibold mb-1 d-flex flex-wrap align-items-center gap-2">
<span><?= $asset_name ?></span>
<?php if ($asset_favorite) { ?><i class="fas fa-star text-warning fs-6" title="Favorite"></i><?php } ?>
<?php if ($asset_archived_at) { ?><span class="badge rounded-pill text-bg-secondary fw-normal fs-6">Archived</span><?php } ?>
<?php if ($can_edit) { ?>
<span class="fs-6"><?= quickPick('asset_status', $asset_id, (string) $asset_row['asset_status'], assetQuickHtml('asset_status', $asset_row), 'list', 0, $client_id) ?></span>
<?php } else { ?>
<span class="fs-6"><?= $asset_status_pill ?></span>
<?php } ?>
</h1>
<div class="text-body-secondary small d-flex flex-wrap column-gap-3 row-gap-1">
<span><i class="fas fa-fw fa-<?= $device_icon ?> me-1"></i><?= $asset_type ?></span>
<?php if ($asset_make_model) { ?>
<span><i class="fas fa-fw fa-cube me-1"></i><?= $asset_make_model ?></span>
<?php } ?>
<?php if ($asset_os) { ?>
<span><i class="<?= $asset_os_icon ?> fa-fw me-1"></i><?= $asset_os ?></span>
<?php } ?>
<?php if ($asset_description) { ?>
<span><?= $asset_description ?></span>
<?php } ?>
</div>
<?php if ($asset_tags_display) { ?>
<div class="mt-2"><?= $asset_tags_display ?></div>
<?php } ?>
</div>
<div class="d-flex flex-wrap gap-2 d-print-none">
<?php foreach ($asset_links as [$link_label, $link_url]) { ?>
<a class="btn btn-outline-primary" href="<?= escapeUrl($link_url) ?>" target="_blank" rel="noopener" title="<?= $link_label ?>: <?= escapeHtml($link_url) ?>">
<i class="<?= assetLinkIcon($link_url) ?> me-2"></i><?= escapeHtml(assetLinkLabel($link_url)) ?><?= $link_label === 'Client link' ? ' (client)' : '' ?>
</a>
<?php } ?>
<?php if ($can_edit) { ?>
<div class="dropdown">
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown"><i class="fas fa-plus me-2"></i>New</button>
<div class="dropdown-menu dropdown-menu-end">
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/ticket/ticket_add.php?<?= $client_url ?>&asset_id=<?= $asset_id ?>" data-modal-size="lg">
<i class="fa fa-fw fa-life-ring text-secondary me-2"></i>Ticket
</a>
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/recurring_ticket/recurring_ticket_add.php?<?= $client_url ?>&asset_id=<?= $asset_id ?>" data-modal-size="lg">
<i class="fa fa-fw fa-redo-alt text-secondary me-2"></i>Recurring Ticket
</a>
<?php if ($can_credentials) { ?>
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/credential/credential_add.php?<?= $client_url ?>asset_id=<?= $asset_id ?>">
<i class="fa fa-fw fa-key text-secondary me-2"></i>Credential
</a>
<?php } ?>
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/asset/asset_interface_add.php?&asset_id=<?= $asset_id ?>">
<i class="fa fa-fw fa-ethernet text-secondary me-2"></i>Interface
</a>
<a class="dropdown-item ajax-modal" href="#" data-modal-size="lg" data-modal-url="modals/software/software_add.php?client_id=<?= $client_id ?>&asset_id=<?= $asset_id ?>">
<i class="fa fa-fw fa-cube text-secondary me-2"></i>License
</a>
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/document/document_add.php?<?= $client_url ?>&asset_id=<?= $asset_id ?>" data-modal-size="xl">
<i class="fa fa-fw fa-file-alt text-secondary me-2"></i>Document
</a>
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/file/file_upload.php?<?= $client_url ?>&asset_id=<?= $asset_id ?>">
<i class="fa fa-fw fa-upload text-secondary me-2"></i>Upload File(s)
</a>
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/asset/asset_note_add.php?id=<?= $asset_id ?>">
<i class="fas fa-fw fa-sticky-note text-secondary me-2"></i>Note
</a>
</div>
</div>
<div class="dropdown">
<button type="button" class="btn btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown"><i class="fas fa-link me-2"></i>Link</button>
<div class="dropdown-menu dropdown-menu-end">
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/link/link.php?from=asset&id=<?= $asset_id ?>&to=software">
<i class="fa fa-fw fa-cube text-secondary me-2"></i>License
</a>
<?php if ($can_credentials) { ?>
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/asset/asset_link_credential.php?id=<?= $asset_id ?>">
<i class="fa fa-fw fa-key text-secondary me-2"></i>Credential
</a>
<?php } ?>
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/asset/asset_link_service.php?id=<?= $asset_id ?>">
<i class="fa fa-fw fa-stream text-secondary me-2"></i>Service
</a>
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/asset/asset_link_document.php?id=<?= $asset_id ?>">
<i class="fa fa-fw fa-file-alt text-secondary me-2"></i>Document
</a>
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/asset/asset_link_file.php?id=<?= $asset_id ?>">
<i class="fa fa-fw fa-paperclip text-secondary me-2"></i>File
</a>
</div>
</div>
<button type="button" class="btn btn-outline-secondary ajax-modal" data-modal-size="lg" data-modal-url="modals/asset/asset_edit.php?id=<?= $asset_id ?>">
<i class="fas fa-edit me-2"></i>Edit
</button>
<div class="dropdown dropstart">
<button class="btn btn-outline-secondary" title="More actions" type="button" data-bs-toggle="dropdown">
<i class="fas fa-fw fa-ellipsis-v"></i>
</button>
<div class="dropdown-menu">
<a class="dropdown-item ajax-modal" href="#" data-modal-size="lg" data-modal-url="modals/asset/asset_copy.php?id=<?= $asset_id ?>">
<i class="fas fa-fw fa-copy text-secondary me-2"></i>Copy
</a>
<div class="dropdown-divider"></div>
<?php if ($asset_archived_at) { ?>
<a class="dropdown-item" href="post.php?restore_asset=<?= $asset_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-redo text-secondary me-2"></i>Restore
</a>
<?php if ($can_delete) { ?>
<a class="dropdown-item text-danger text-bold confirm-link" href="post.php?delete_asset=<?= $asset_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-trash me-2"></i>Delete
</a>
<?php } ?>
<?php } else { ?>
<a class="dropdown-item text-danger confirm-link" href="post.php?archive_asset=<?= $asset_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-archive me-2"></i>Archive
</a>
<?php } ?>
</div>
</div>
<?php } ?>
</div>
</div>
</div>
<div class="row g-3">
<!-- The facts: how to reach it, what it is, where it is -->
<div class="col-xl-4 col-lg-5">
<?php if ($asset_ip || $asset_ipv6 || $asset_nat_ip || $asset_mac || $network_name) { ?>
<div class="card mb-3">
<div class="card-header">
<h3 class="card-title mb-0"><i class="fas fa-fw fa-network-wired text-secondary me-2"></i>Network</h3>
</div>
<div class="card-body">
<dl class="itflow-asset-dl mb-0">
<?php if ($asset_ip) { ?>
<dt>IP</dt>
<dd><span class="font-monospace"><?= $asset_ip ?></span><?php if ($asset_ip !== 'DHCP') { ?><button type="button" class="itflow-copy clipboardjs" title="Copy IP" data-clipboard-text="<?= $asset_ip ?>"><i class="far fa-copy"></i></button><?php } ?></dd>
<?php } ?>
<?php if ($asset_nat_ip) { ?>
<dt>NAT</dt>
<dd><span class="font-monospace"><?= $asset_nat_ip ?></span><button type="button" class="itflow-copy clipboardjs" title="Copy NAT IP" data-clipboard-text="<?= $asset_nat_ip ?>"><i class="far fa-copy"></i></button></dd>
<?php } ?>
<?php if ($asset_ipv6) { ?>
<dt>IPv6</dt>
<dd><span class="font-monospace"><?= $asset_ipv6 ?></span><button type="button" class="itflow-copy clipboardjs" title="Copy IPv6" data-clipboard-text="<?= $asset_ipv6 ?>"><i class="far fa-copy"></i></button></dd>
<?php } ?>
<?php if ($asset_mac) { ?>
<dt>MAC</dt>
<dd><span class="font-monospace"><?= $asset_mac ?></span><button type="button" class="itflow-copy clipboardjs" title="Copy MAC" data-clipboard-text="<?= $asset_mac ?>"><i class="far fa-copy"></i></button></dd>
<?php } ?>
<?php if ($network_name) { ?>
<dt>Network</dt>
<dd><a href="network.php?client_id=<?= $client_id ?>&network_id=<?= $asset_network_id ?>"><?= $network_name ?></a></dd>
<?php } ?>
</dl>
</div>
</div>
<?php } ?>
<div class="card mb-3">
<div class="card-header">
<h3 class="card-title mb-0"><i class="fas fa-fw fa-microchip text-secondary me-2"></i>Hardware</h3>
</div>
<div class="card-body">
<dl class="itflow-asset-dl mb-0">
<?php if ($asset_make_model) { ?>
<dt>Model</dt><dd><?= $asset_make_model ?></dd>
<?php } ?>
<?php if ($asset_serial) { ?>
<dt>Serial</dt>
<dd><span class="font-monospace"><?= $asset_serial ?></span><button type="button" class="itflow-copy clipboardjs" title="Copy serial" data-clipboard-text="<?= $asset_serial ?>"><i class="far fa-copy"></i></button></dd>
<?php } ?>
<?php if ($asset_os) { ?>
<dt>OS</dt><dd><i class="<?= $asset_os_icon ?> fa-fw text-secondary me-1"></i><?= $asset_os ?></dd>
<?php } ?>
<dt>Warranty</dt>
<dd>
<?php if ($asset_warranty) { ?>
<span class="badge rounded-pill bg-<?= $asset_warranty['tone'] ?>-subtle text-<?= $asset_warranty['tone'] ?>-emphasis border border-<?= $asset_warranty['tone'] ?>-subtle fw-normal"><?= escapeHtml($asset_warranty['short']) ?></span>
<div class="small text-body-secondary"><?= escapeHtml($asset_warranty['text']) ?><?= $asset_warranty['tone'] !== 'success' ? ' (' . escapeHtml($asset_warranty['date']) . ')' : '' ?></div>
<?php } else { ?>
<span class="text-body-secondary">Not recorded</span>
<?php } ?>
</dd>
<?php if ($asset_purchase_date) { ?>
<dt>Purchased</dt>
<dd><?= $asset_purchase_date ?><?php if ($asset_purchase_age) { ?><div class="small text-body-secondary"><?= $asset_purchase_age ?> old</div><?php } ?></dd>
<?php } ?>
<?php if ($asset_install_date) { ?>
<dt>Installed</dt>
<dd><?= $asset_install_date ?><?php if ($asset_install_age) { ?><div class="small text-body-secondary"><?= $asset_install_age ?> in service</div><?php } ?></dd>
<?php } ?>
<?php if ($vendor_name) { ?>
<dt>Vendor</dt>
<dd><?= vendorLink($asset_vendor_id, $vendor_name) ?></dd>
<?php } ?>
<?php if ($asset_purchase_reference) { ?>
<dt>Order ref</dt><dd><?= $asset_purchase_reference ?></dd>
<?php } ?>
<dt>Added</dt><dd title="<?= $asset_created_at ?>"><?= timeAgo($asset_created_at) ?></dd>
</dl>
</div>
</div>
<div class="card mb-3">
<div class="card-header">
<h3 class="card-title mb-0"><i class="fas fa-fw fa-map-marker-alt text-secondary me-2"></i>Where &amp; who</h3>
</div>
<div class="card-body">
<dl class="itflow-asset-dl mb-0">
<dt>Location</dt>
<dd>
<?php if ($can_edit) { ?>
<?= quickPick('asset_location', $asset_id, (string) intval($asset_row['asset_location_id']), assetQuickHtml('asset_location', $asset_row, 'detail'), 'detail', 0, $client_id) ?>
<?php } elseif ($location_name) { ?>
<span class="<?= $location_archived ? 'text-danger text-decoration-line-through' : '' ?>" <?= $location_archived ? 'title="Archived location"' : '' ?>><?= $location_name ?></span>
<?php } else { ?>
<span class="text-body-secondary">-</span>
<?php } ?>
<?php if ($asset_physical_location) { ?>
<div class="small text-body-secondary"><?= $asset_physical_location ?></div>
<?php } ?>
</dd>
<?php if ($asset_rack) { ?>
<dt>Rack</dt>
<dd><a href="rack.php?client_id=<?= $client_id ?>&rack_id=<?= $asset_rack_id ?>"><?= $asset_rack_name ?></a><div class="small text-body-secondary"><?= $asset_rack_position ?></div></dd>
<?php } ?>
<dt>Assigned</dt>
<dd>
<?php if ($can_edit) { ?>
<?= quickPick('asset_contact', $asset_id, (string) $asset_contact_id, assetQuickHtml('asset_contact', $asset_row, 'detail'), 'detail', 0, $client_id) ?>
<?php } elseif ($contact_name) { ?>
<?= contactLink($client_id, $asset_contact_id, $asset_row['contact_name'], $asset_row['contact_photo'], ['page' => true, 'archived' => $contact_archived]) ?>
<?php } else { ?>
<span class="text-body-secondary">Nobody</span>
<?php } ?>
<!-- How to reach them - redrawn when the contact changes -->
<div data-quick-related="asset-contact-<?= $asset_id ?>"><?= assetContactDetails($asset_row) ?></div>
</dd>
</dl>
</div>
</div>
<div class="card mb-3">
<div class="card-header">
<h3 class="card-title mb-0"><i class="fas fa-fw fa-pen text-secondary me-2"></i>Quick notes</h3>
</div>
<textarea class="form-control border-0 rounded-0 rounded-bottom" rows="5" id="assetNotes" placeholder="Jot something down - saves when you click away" onblur="updateAssetNotes(<?= $asset_id ?>)" <?= $can_edit ? '' : 'readonly' ?>><?= $asset_notes ?></textarea>
</div>
</div>
<!-- Everything linked to it -->
<div class="col-xl-8 col-lg-7">
<div class="card mb-3 itflow-asset-related">
<div class="card-header py-2">
<ul class="nav nav-pills itflow-asset-pills" role="tablist">
<?php foreach ($asset_tabs as $tab_key => $tab) { ?>
<li class="nav-item" role="presentation">
<button class="nav-link text-nowrap <?= $tab['count'] ? '' : 'is-empty' ?>" id="asset-tab-<?= $tab_key ?>" data-bs-toggle="pill" data-bs-target="#asset-pane-<?= $tab_key ?>" data-asset-tab="<?= $tab_key ?>" type="button" role="tab">
<i class="fas fa-fw <?= $tab['icon'] ?> me-1 d-none d-xxl-inline"></i><?= $tab['label'] ?>
<span class="badge rounded-pill ms-1"><?= $tab['count'] ?></span>
<?php if (!empty($tab['badge'])) { ?>
<span class="badge rounded-pill text-bg-danger" title="Open"><?= intval($tab['badge']) ?> open</span>
<?php } ?>
</button>
</li>
<?php } ?>
</ul>
</div>
<div class="tab-content">
<!-- Interfaces -->
<div class="tab-pane fade" id="asset-pane-interfaces" role="tabpanel">
<div class="itflow-asset-pane-bar">
<span class="text-body-secondary small">Ports and addresses. The primary interface is the one shown on the left.</span>
<?php if ($can_edit) { ?>
<div class="d-flex gap-2">
<div class="dropdown" id="bulkActionButton" hidden>
<button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown">
<i class="fas fa-fw fa-layer-group me-2"></i>Bulk Action (<span id="selectedCount">0</span>)
</button>
<div class="dropdown-menu dropdown-menu-end">
<a class="dropdown-item ajax-modal" href="#"
data-modal-url="modals/asset/asset_interface_bulk_edit_network.php?client_id=<?= $client_id ?>"
data-bulk="true">
<i class="fas fa-fw fa-network-wired me-2"></i>Assign Network
</a>
<a class="dropdown-item ajax-modal" href="#"
data-modal-url="modals/asset/asset_interface_bulk_edit_type.php?client_id=<?= $client_id ?>"
data-bulk="true">
<i class="fas fa-fw fa-ethernet me-2"></i>Set Type
</a>
<button class="dropdown-item" type="submit" form="bulkActions" name="bulk_edit_asset_interface_ip_dhcp">
<i class="fas fa-fw fa-list-ul me-2"></i>Set to DHCP
</button>
<?php if ($can_delete) { ?>
<div class="dropdown-divider"></div>
<button class="dropdown-item text-danger text-bold confirm-link" type="submit" form="bulkActions" name="bulk_delete_asset_interfaces">
<i class="fas fa-fw fa-trash me-2"></i>Delete
</button>
<?php } ?>
</div>
</div>
<div class="btn-group">
<button type="button" class="btn btn-sm btn-primary ajax-modal" data-modal-url="modals/asset/asset_interface_add.php?&asset_id=<?= $asset_id ?>">
<i class="fas fa-plus me-2"></i>New Interface
</button>
<button type="button" class="btn btn-sm btn-primary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown"></button>
<div class="dropdown-menu dropdown-menu-end">
<a class="dropdown-item" href="#" data-bs-toggle="modal" data-bs-target="#addMultipleAssetInterfacesModal">
<i class="fa fa-fw fa-check-double me-2"></i>Add Multiple
</a>
<a class="dropdown-item" href="#" data-bs-toggle="modal" data-bs-target="#importAssetInterfaceModal">
<i class="fa fa-fw fa-upload me-2"></i>Import
</a>
<a class="dropdown-item" href="#" data-bs-toggle="modal" data-bs-target="#exportAssetInterfaceModal">
<i class="fa fa-fw fa-download me-2"></i>Export
</a>
</div>
</div>
</div>
<?php } ?>
</div>
<form id="bulkActions" action="post.php" method="post">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="text-nowrap table-light <?php if ($interface_count == 0) { echo "d-none"; } ?>">
<tr>
<th class="checkbox-column border-end">
<div class="form-check">
<input class="form-check-input" id="selectAllCheckbox" type="checkbox" onclick="checkAll(this)" onkeydown="checkAll(this)" title="Select all">
</div>
</th>
<th>Name / Port</th>
<th>Type</th>
<th>Network</th>
<th>IP</th>
<th>MAC</th>
<th>Connected To</th>
<th></th>
</tr>
</thead>
<tbody>
<?php while ($row = mysqli_fetch_assoc($sql_related_interfaces)) {
$interface_id = intval($row['interface_id']);
$interface_name = escapeHtml($row['interface_name']);
$interface_description = escapeHtml($row['interface_description']);
$interface_type = escapeHtml($row['interface_type']);
$interface_mac = escapeHtml($row['interface_mac']);
$interface_ip = escapeHtml($row['interface_ip']);
$interface_nat_ip = escapeHtml($row['interface_nat_ip']);
$interface_ipv6 = escapeHtml($row['interface_ipv6']);
$interface_primary = intval($row['interface_primary']);
$interface_network_id = intval($row['network_id']);
$interface_network_name = escapeHtml($row['network_name']);
$connected_asset_id = intval($row['connected_asset_id']);
$connected_asset_name = escapeHtml($row['connected_asset_name']);
$connected_asset_icon = getAssetIcon($row['connected_asset_type']);
$connected_interface_name = escapeHtml($row['connected_interface_name']);
?>
<tr>
<td class="checkbox-column bg-light border-end">
<div class="form-check">
<input class="form-check-input bulk-select" type="checkbox" name="interface_ids[]" value="<?= $interface_id ?>" aria-label="Select <?= $interface_name ?>">
</div>
</td>
<td>
<a class="text-body fw-semibold ajax-modal" href="#" data-modal-url="modals/asset/asset_interface_edit.php?id=<?= $interface_id ?>"><?= $interface_name ?></a>
<?php if ($interface_primary) { ?><span class="badge rounded-pill text-bg-primary fw-normal ms-1">Primary</span><?php } ?>
<?php if ($interface_description) { ?><div class="small text-body-secondary"><?= $interface_description ?></div><?php } ?>
</td>
<td><?= $interface_type ?: '<span class="text-body-secondary">-</span>' ?></td>
<td>
<?php if ($interface_network_name) { ?>
<a href="network.php?client_id=<?= $client_id ?>&network_id=<?= $interface_network_id ?>"><?= $interface_network_name ?></a>
<?php } else { ?>
<span class="text-body-secondary">-</span>
<?php } ?>
</td>
<td class="text-nowrap">
<?php if ($interface_ip) { ?>
<span class="font-monospace"><?= $interface_ip ?></span><?php if ($interface_ip !== 'DHCP') { ?><button type="button" class="itflow-copy clipboardjs" title="Copy IP" data-clipboard-text="<?= $interface_ip ?>"><i class="far fa-copy"></i></button><?php } ?>
<?php } else { ?>
<span class="text-body-secondary">-</span>
<?php } ?>
<?php if ($interface_nat_ip) { ?><div class="small text-body-secondary font-monospace">NAT <?= $interface_nat_ip ?></div><?php } ?>
<?php if ($interface_ipv6) { ?><div class="small text-body-secondary font-monospace"><?= $interface_ipv6 ?></div><?php } ?>
</td>
<td class="text-nowrap">
<?php if ($interface_mac) { ?>
<span class="font-monospace"><?= $interface_mac ?></span><button type="button" class="itflow-copy clipboardjs" title="Copy MAC" data-clipboard-text="<?= $interface_mac ?>"><i class="far fa-copy"></i></button>
<?php } else { ?>
<span class="text-body-secondary">-</span>
<?php } ?>
</td>
<td>
<?php if ($connected_asset_name) { ?>
<a class="ajax-modal" href="#" data-modal-size="lg" data-modal-url="modals/asset/asset.php?id=<?= $connected_asset_id ?>">
<i class="fas fa-fw fa-<?= $connected_asset_icon ?> me-1"></i><?= $connected_asset_name ?></a>
<span class="text-body-secondary">&middot; <?= $connected_interface_name ?></span>
<?php } else { ?>
<span class="text-body-secondary">-</span>
<?php } ?>
</td>
<td class="text-end">
<?php if ($can_edit) { ?>
<div class="dropdown dropstart">
<button class="btn btn-sm btn-light" title="More actions" type="button" data-bs-toggle="dropdown">
<i class="fas fa-fw fa-ellipsis-v"></i>
</button>
<div class="dropdown-menu">
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/asset/asset_interface_edit.php?id=<?= $interface_id ?>">
<i class="fas fa-fw fa-edit text-secondary me-2"></i>Edit
</a>
<?php if ($can_delete && !$interface_primary) { ?>
<div class="dropdown-divider"></div>
<a class="dropdown-item text-danger text-bold confirm-link" href="post.php?delete_asset_interface=<?= $interface_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-trash me-2"></i>Delete
</a>
<?php } ?>
</div>
</div>
<?php } ?>
</td>
</tr>
<?php } ?>
</tbody>
</table>
</div>
</form>
<?php if (!$interface_count) { ?>
<div class="itflow-asset-empty">No interfaces yet.</div>
<?php } ?>
</div>
<?php if ($can_credentials) { ?>
<!-- Credentials -->
<div class="tab-pane fade" id="asset-pane-credentials" role="tabpanel">
<div class="itflow-asset-pane-bar">
<span class="text-body-secondary small">Logins for this asset. Passwords and codes are fetched only when you ask for them.</span>
<?php if ($can_edit) { ?>
<div class="d-flex gap-2">
<button type="button" class="btn btn-sm btn-outline-secondary ajax-modal" data-modal-url="modals/asset/asset_link_credential.php?id=<?= $asset_id ?>">
<i class="fas fa-link me-2"></i>Link
</button>
<button type="button" class="btn btn-sm btn-primary ajax-modal" data-modal-url="modals/credential/credential_add.php?<?= $client_url ?>asset_id=<?= $asset_id ?>">
<i class="fas fa-plus me-2"></i>New Credential
</button>
</div>
<?php } ?>
</div>
<?php if ($credential_count) { ?>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="text-nowrap table-light">
<tr>
<th>Name</th>
<th>Username</th>
<th>Password</th>
<th>OTP</th>
<th>URI</th>
<th></th>
</tr>
</thead>
<tbody>
<?php while ($row = mysqli_fetch_assoc($sql_related_credentials)) {
$credential_id = intval($row['credential_id']);
$credential_name = escapeHtml($row['credential_name']);
$credential_description = escapeHtml($row['credential_description']);
$credential_uri = escapeHtml($row['credential_uri']);
$credential_username = escapeHtml(decryptCredentialEntry($row['credential_username']));
$credential_has_otp = !empty($row['credential_otp_secret']);
?>
<tr>
<td>
<a class="text-body fw-semibold ajax-modal" href="#" data-modal-url="modals/credential/credential_edit.php?id=<?= $credential_id ?>"><?= $credential_name ?></a>
<?php if ($credential_description) { ?><div class="small text-body-secondary"><?= $credential_description ?></div><?php } ?>
</td>
<td class="text-nowrap">
<?php if ($credential_username) { ?>
<?= $credential_username ?><button type="button" class="itflow-copy clipboardjs" title="Copy username" data-clipboard-text="<?= $credential_username ?>"><i class="far fa-copy"></i></button>
<?php } else { ?>
<span class="text-body-secondary">-</span>
<?php } ?>
</td>
<td class="text-nowrap">
<button class="btn btn-sm btn-light" aria-label="Show password" title="Show password" type="button" onclick="showPasswordViaCredentialID(this, <?= $credential_id ?>)"><i class="fas fa-ellipsis-h"></i><i class="fas fa-ellipsis-h"></i></button>
<button type="button" class="itflow-copy" title="Copy password" onclick="copyPasswordViaCredentialID(this, <?= $credential_id ?>)"><i class="far fa-copy"></i></button>
</td>
<td class="text-nowrap">
<?php if ($credential_has_otp) { ?>
<span onmouseenter="showOTPViaCredentialID(<?= $credential_id ?>)"><i class="far fa-clock text-secondary me-1"></i><span id="otp_<?= $credential_id ?>"><i>Hover...</i></span></span>
<?php } else { ?>
<span class="text-body-secondary">-</span>
<?php } ?>
</td>
<td class="text-nowrap">
<?php if ($credential_uri) { ?>
<a href="<?= $credential_uri ?>" target="_blank" rel="noopener" title="<?= $credential_uri ?>"><?= truncate($credential_uri, 26) ?></a><button type="button" class="itflow-copy clipboardjs" title="Copy URI" data-clipboard-text="<?= $credential_uri ?>"><i class="far fa-copy"></i></button>
<?php } else { ?>
<span class="text-body-secondary">-</span>
<?php } ?>
</td>
<td class="text-end">
<div class="dropdown dropstart">
<button class="btn btn-sm btn-light" title="More actions" type="button" data-bs-toggle="dropdown">
<i class="fas fa-fw fa-ellipsis-v"></i>
</button>
<div class="dropdown-menu">
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/credential/credential_edit.php?id=<?= $credential_id ?>">
<i class="fas fa-fw fa-edit text-secondary me-2"></i>Edit
</a>
<a class="dropdown-item" href="#" data-bs-toggle="modal" data-bs-target="#shareModal" onclick="populateShareModal(<?= "$client_id, 'Credential', $credential_id" ?>)">
<i class="fas fa-fw fa-share-alt text-secondary me-2"></i>Share
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="post.php?unlink_credential_from_asset&asset_id=<?= $asset_id ?>&credential_id=<?= $credential_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-unlink text-secondary me-2"></i>Unlink
</a>
<?php if (lookupUserPermission('module_credential') >= 3) { ?>
<a class="dropdown-item text-danger text-bold confirm-link" href="post.php?delete_credential=<?= $credential_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-trash me-2"></i>Delete
</a>
<?php } ?>
</div>
</div>
</td>
</tr>
<?php } ?>
</tbody>
</table>
</div>
<?php } else { ?>
<div class="itflow-asset-empty">No credentials for this asset.</div>
<?php } ?>
</div>
<?php } ?>
<!-- Tickets -->
<div class="tab-pane fade" id="asset-pane-tickets" role="tabpanel">
<div class="itflow-asset-pane-bar">
<span class="text-body-secondary small">Open tickets first. Includes tickets where this is one of several assets.</span>
<?php if ($can_edit) { ?>
<button type="button" class="btn btn-sm btn-primary ajax-modal" data-modal-size="lg" data-modal-url="modals/ticket/ticket_add.php?<?= $client_url ?>&asset_id=<?= $asset_id ?>">
<i class="fas fa-plus me-2"></i>New Ticket
</button>
<?php } ?>
</div>
<?php if ($related_tickets) { ?>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="text-nowrap table-light">
<tr>
<th>Ticket</th>
<th>Subject</th>
<th>Priority</th>
<th>Status</th>
<th>Assigned</th>
<th>Updated</th>
</tr>
</thead>
<tbody>
<?php foreach ($related_tickets as $row) {
$ticket_id = intval($row['ticket_id']);
$ticket_prefix = escapeHtml($row['ticket_prefix']);
$ticket_number = intval($row['ticket_number']);
$ticket_subject = escapeHtml($row['ticket_subject']);
$ticket_priority = escapeHtml($row['ticket_priority']);
$ticket_status_id = intval($row['ticket_status_id']);
$ticket_status_name = escapeHtml($row['ticket_status_name']);
$ticket_status_color = preg_match('/^#[0-9a-f]{3,8}$/i', (string) $row['ticket_status_color']) ? $row['ticket_status_color'] : '#6c757d';
$ticket_created_at = escapeHtml($row['ticket_created_at']);
$ticket_updated_at = escapeHtml($row['ticket_updated_at'] ?: $row['ticket_created_at']);
$ticket_is_open = empty($row['ticket_resolved_at']) && empty($row['ticket_closed_at']);
$priority_classes = ['Urgent' => 'text-bg-dark', 'High' => 'text-bg-danger', 'Medium' => 'text-bg-warning', 'Low' => 'text-bg-info'];
$ticket_assigned_to_display = formatAssignee($row['ticket_assigned_to'], $row['user_name'], $row['user_avatar'], !in_array($ticket_status_id, [4, 5]));
?>
<tr class="<?= $ticket_is_open ? '' : 'text-body-secondary' ?>">
<td class="text-nowrap"><a class="font-monospace" href="ticket.php?client_id=<?= $client_id ?>&ticket_id=<?= $ticket_id ?>"><?= "$ticket_prefix$ticket_number" ?></a></td>
<td><a class="<?= $ticket_is_open ? 'text-body fw-semibold' : 'text-body-secondary' ?>" href="ticket.php?client_id=<?= $client_id ?>&ticket_id=<?= $ticket_id ?>"><?= $ticket_subject ?></a></td>
<td><?php if (isset($priority_classes[$row['ticket_priority']])) { ?><span class="badge rounded-pill fw-normal <?= $priority_classes[$row['ticket_priority']] ?>"><?= $ticket_priority ?></span><?php } else { ?>-<?php } ?></td>
<td><span class="badge rounded-pill fw-normal text-white" style="background-color: <?= $ticket_status_color ?>"><?= $ticket_status_name ?></span></td>
<td><?= $ticket_assigned_to_display ?></td>
<td class="text-nowrap small" title="Created <?= $ticket_created_at ?>"><?= timeAgo($ticket_updated_at) ?></td>
</tr>
<?php } ?>
</tbody>
</table>
</div>
<?php } else { ?>
<div class="itflow-asset-empty">No tickets have mentioned this asset.</div>
<?php } ?>
</div>
<!-- Recurring tickets -->
<div class="tab-pane fade" id="asset-pane-recurring" role="tabpanel">
<div class="itflow-asset-pane-bar">
<span class="text-body-secondary small">Scheduled work - patching, checks, maintenance visits.</span>
<?php if ($can_edit) { ?>
<button type="button" class="btn btn-sm btn-primary ajax-modal" data-modal-size="lg" data-modal-url="modals/recurring_ticket/recurring_ticket_add.php?<?= $client_url ?>&asset_id=<?= $asset_id ?>">
<i class="fas fa-plus me-2"></i>New Recurring Ticket
</button>
<?php } ?>
</div>
<?php if ($recurring_ticket_count) { ?>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="text-nowrap table-light">
<tr>
<th>Subject</th>
<th>Priority</th>
<th>Frequency</th>
<th>Next Run</th>
<th></th>
</tr>
</thead>
<tbody>
<?php while ($row = mysqli_fetch_assoc($sql_related_recurring_tickets)) {
$recurring_ticket_id = intval($row['recurring_ticket_id']);
$recurring_ticket_subject = escapeHtml($row['recurring_ticket_subject']);
$recurring_ticket_priority = escapeHtml($row['recurring_ticket_priority']);
$recurring_ticket_frequency = escapeHtml($row['recurring_ticket_frequency']);
$recurring_ticket_next_run = escapeHtml($row['recurring_ticket_next_run']);
?>
<tr>
<td>
<a class="text-body fw-semibold ajax-modal" href="#" data-modal-size="lg" data-modal-url="modals/recurring_ticket/recurring_ticket_edit.php?id=<?= $recurring_ticket_id ?>"><?= $recurring_ticket_subject ?></a>
</td>
<td><?= $recurring_ticket_priority ?></td>
<td><?= $recurring_ticket_frequency ?></td>
<td class="text-nowrap"><?= $recurring_ticket_next_run ?> <span class="small text-body-secondary">(<?= timeAgo($recurring_ticket_next_run) ?>)</span></td>
<td class="text-end">
<?php if ($can_edit) { ?>
<div class="dropdown dropstart">
<button class="btn btn-sm btn-light" title="More actions" type="button" data-bs-toggle="dropdown">
<i class="fas fa-fw fa-ellipsis-v"></i>
</button>
<div class="dropdown-menu">
<a class="dropdown-item ajax-modal" href="#" data-modal-size="lg" data-modal-url="modals/recurring_ticket/recurring_ticket_edit.php?id=<?= $recurring_ticket_id ?>">
<i class="fas fa-fw fa-edit text-secondary me-2"></i>Edit
</a>
<a class="dropdown-item" href="post.php?force_recurring_ticket=<?= $recurring_ticket_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
<i class="fa fa-fw fa-paper-plane text-secondary me-2"></i>Force Reoccur
</a>
<?php if ($can_delete) { ?>
<div class="dropdown-divider"></div>
<a class="dropdown-item text-danger text-bold confirm-link" href="post.php?delete_recurring_ticket=<?= $recurring_ticket_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-trash me-2"></i>Delete
</a>
<?php } ?>
</div>
</div>
<?php } ?>
</td>
</tr>
<?php } ?>
</tbody>
</table>
</div>
<?php } else { ?>
<div class="itflow-asset-empty">Nothing scheduled for this asset.</div>
<?php } ?>
</div>
<!-- Licenses -->
<div class="tab-pane fade" id="asset-pane-licenses" role="tabpanel">
<div class="itflow-asset-pane-bar">
<span class="text-body-secondary small">Licenses this asset holds a seat on, and the key to install.</span>
<?php if ($can_edit) { ?>
<div class="d-flex gap-2">
<button type="button" class="btn btn-sm btn-outline-secondary ajax-modal" data-modal-url="modals/link/link.php?from=asset&id=<?= $asset_id ?>&to=software">
<i class="fas fa-link me-2"></i>Link
</button>
<button type="button" class="btn btn-sm btn-primary ajax-modal" data-modal-size="lg" data-modal-url="modals/software/software_add.php?client_id=<?= $client_id ?>&asset_id=<?= $asset_id ?>">
<i class="fas fa-plus me-2"></i>New License
</button>
</div>
<?php } ?>
</div>
<?php if ($software_count) { ?>
<?= softwareHeldTable($held_software, 'asset', $asset_id, $can_edit) ?>
<?php } else { ?>
<div class="itflow-asset-empty">No licenses linked.</div>
<?php } ?>
</div>
<!-- Documents -->
<div class="tab-pane fade" id="asset-pane-documents" role="tabpanel">
<div class="itflow-asset-pane-bar">
<span class="text-body-secondary small">Runbooks, configs and how-tos for this asset.</span>
<?php if ($can_edit) { ?>
<div class="d-flex gap-2">
<button type="button" class="btn btn-sm btn-outline-secondary ajax-modal" data-modal-url="modals/asset/asset_link_document.php?id=<?= $asset_id ?>">
<i class="fas fa-link me-2"></i>Link
</button>
<button type="button" class="btn btn-sm btn-primary ajax-modal" data-modal-size="xl" data-modal-url="modals/document/document_add.php?<?= $client_url ?>&asset_id=<?= $asset_id ?>">
<i class="fas fa-plus me-2"></i>New Document
</button>
</div>
<?php } ?>
</div>
<?php if ($document_count) { ?>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="text-nowrap table-light">
<tr>
<th>Document</th>
<th>By</th>
<th>Updated</th>
<th></th>
</tr>
</thead>
<tbody>
<?php while ($row = mysqli_fetch_assoc($sql_related_documents)) {
$document_id = intval($row['document_id']);
$document_name = escapeHtml($row['document_name']);
$document_description = escapeHtml($row['document_description']);
$document_created_by = escapeHtml($row['user_name']);
$document_updated_at = escapeHtml($row['document_updated_at'] ?: $row['document_created_at']);
?>
<tr>
<td>
<div class="d-flex align-items-center gap-2">
<?= documentTile('sm') ?>
<div class="min-w-0">
<a class="fw-semibold" href="document.php?client_id=<?= $client_id ?>&document_id=<?= $document_id ?>"><?= $document_name ?></a>
<?php if ($document_description) { ?><div class="small text-body-secondary"><?= $document_description ?></div><?php } ?>
</div>
</div>
</td>
<td><?= $document_created_by ?></td>
<td class="text-nowrap small" title="<?= $document_updated_at ?>"><?= timeAgo($document_updated_at) ?></td>
<td class="text-end text-nowrap">
<a href="#" class="btn btn-sm btn-light ajax-modal" title="View document" data-modal-size="lg" data-modal-url="modals/document/document_view.php?id=<?= $document_id ?>"><i class="fas fa-fw fa-eye"></i></a>
<?php if ($can_edit) { ?>
<a href="post.php?unlink_asset_from_document&asset_id=<?= $asset_id ?>&document_id=<?= $document_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>" class="btn btn-sm btn-light" title="Unlink"><i class="fas fa-fw fa-unlink"></i></a>
<?php } ?>
</td>
</tr>
<?php } ?>
</tbody>
</table>
</div>
<?php } else { ?>
<div class="itflow-asset-empty">No documents linked.</div>
<?php } ?>
</div>
<!-- Files -->
<div class="tab-pane fade" id="asset-pane-files" role="tabpanel">
<div class="itflow-asset-pane-bar">
<span class="text-body-secondary small">Configs, photos, invoices, exports.</span>
<?php if ($can_edit) { ?>
<div class="d-flex gap-2">
<button type="button" class="btn btn-sm btn-outline-secondary ajax-modal" data-modal-url="modals/asset/asset_link_file.php?id=<?= $asset_id ?>">
<i class="fas fa-link me-2"></i>Link
</button>
<button type="button" class="btn btn-sm btn-primary ajax-modal" data-modal-url="modals/file/file_upload.php?<?= $client_url ?>&asset_id=<?= $asset_id ?>">
<i class="fas fa-upload me-2"></i>Upload
</button>
</div>
<?php } ?>
</div>
<?php if ($files_count) {
?>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="text-nowrap table-light">
<tr>
<th>Name</th>
<th>Uploaded</th>
<th></th>
</tr>
</thead>
<tbody>
<?php while ($row = mysqli_fetch_assoc($sql_related_files)) {
$file_id = intval($row['file_id']);
$file_name = escapeHtml($row['file_name']);
$file_description = escapeHtml($row['file_description']);
$file_created_at = escapeHtml($row['file_created_at']);
?>
<tr>
<td>
<div class="d-flex align-items-center gap-2">
<?= fileTypeTile($row['file_ext'], 'sm') ?>
<div class="min-w-0">
<a class="fw-semibold ajax-modal" href="#" data-modal-size="lg" data-modal-url="modals/file/file.php?id=<?= $file_id ?>"><?= $file_name ?></a>
<?php if ($file_description) { ?><div class="small text-body-secondary"><?= $file_description ?></div><?php } ?>
</div>
</div>
</td>
<td class="text-nowrap small" title="<?= $file_created_at ?>"><?= timeAgo($file_created_at) ?></td>
<td class="text-end">
<?php if ($can_edit) { ?>
<a href="post.php?unlink_asset_from_file&asset_id=<?= $asset_id ?>&file_id=<?= $file_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>" class="btn btn-sm btn-light" title="Unlink"><i class="fas fa-fw fa-unlink"></i></a>
<?php } ?>
</td>
</tr>
<?php } ?>
</tbody>
</table>
</div>
<?php } else { ?>
<div class="itflow-asset-empty">No files linked.</div>
<?php } ?>
</div>
<!-- Services -->
<div class="tab-pane fade" id="asset-pane-services" role="tabpanel">
<div class="itflow-asset-pane-bar">
<span class="text-body-secondary small">Services that depend on this asset.</span>
<?php if ($can_edit) { ?>
<button type="button" class="btn btn-sm btn-outline-secondary ajax-modal" data-modal-url="modals/asset/asset_link_service.php?id=<?= $asset_id ?>">
<i class="fas fa-link me-2"></i>Link Service
</button>
<?php } ?>
</div>
<?php if ($service_count) { ?>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="text-nowrap table-light">
<tr>
<th>Service</th>
<th>Category</th>
<th>Importance</th>
<th></th>
</tr>
</thead>
<tbody>
<?php while ($row = mysqli_fetch_assoc($sql_linked_services)) {
$service_id = intval($row['service_id']);
$service_name = escapeHtml($row['service_name']);
$service_description = escapeHtml($row['service_description']);
$service_category = escapeHtml($row['service_category']);
$service_importance = escapeHtml($row['service_importance']);
?>
<tr>
<td>
<a class="fw-semibold ajax-modal" href="#" data-modal-size="lg" data-modal-url="modals/service/service.php?id=<?= $service_id ?>"><?= $service_name ?></a>
<?php if ($service_description) { ?><div class="small text-body-secondary"><?= $service_description ?></div><?php } ?>
</td>
<td><?= $service_category ?></td>
<td><?= $service_importance ?></td>
<td class="text-end">
<?php if ($can_edit) { ?>
<a href="post.php?unlink_service_from_asset&asset_id=<?= $asset_id ?>&service_id=<?= $service_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>" class="btn btn-sm btn-light" title="Unlink"><i class="fas fa-fw fa-unlink"></i></a>
<?php } ?>
</td>
</tr>
<?php } ?>
</tbody>
</table>
</div>
<?php } else { ?>
<div class="itflow-asset-empty">Not part of any service.</div>
<?php } ?>
</div>
<!-- Notes -->
<div class="tab-pane fade" id="asset-pane-notes" role="tabpanel">
<div class="itflow-asset-pane-bar">
<span class="text-body-secondary small">A dated log - visits, changes, findings.</span>
<?php if ($can_edit) { ?>
<button type="button" class="btn btn-sm btn-primary ajax-modal" data-modal-url="modals/asset/asset_note_add.php?id=<?= $asset_id ?>">
<i class="fas fa-plus me-2"></i>New Note
</button>
<?php } ?>
</div>
<?php if ($note_count) { ?>
<div class="list-group list-group-flush">
<?php while ($row = mysqli_fetch_assoc($sql_related_notes)) {
$asset_note_id = intval($row['asset_note_id']);
$asset_note_type = escapeHtml($row['asset_note_type']);
$asset_note = nl2br(escapeHtml($row['asset_note']));
$note_by = escapeHtml($row['user_name']);
$asset_note_created_at = escapeHtml($row['asset_note_created_at']);
$note_type_icon = !empty($note_type_icons[$asset_note_type]) ? $note_type_icons[$asset_note_type] : 'fa-sticky-note';
?>
<div class="list-group-item d-flex gap-3 py-3">
<span class="itflow-asset-icon itflow-asset-icon-sm itflow-asset-tone-other" title="<?= $asset_note_type ?>"><i class="fas fa-fw <?= $note_type_icon ?>"></i></span>
<div class="flex-grow-1 min-w-0">
<div class="small text-body-secondary mb-1">
<span class="fw-semibold text-body"><?= $asset_note_type ?></span>
&middot; <?= $note_by ?> &middot; <span title="<?= $asset_note_created_at ?>"><?= timeAgo($asset_note_created_at) ?></span>
</div>
<div><?= $asset_note ?></div>
</div>
<?php if ($can_edit) { ?>
<div class="dropdown dropstart">
<button class="btn btn-sm btn-light" title="More actions" type="button" data-bs-toggle="dropdown">
<i class="fas fa-fw fa-ellipsis-v"></i>
</button>
<div class="dropdown-menu">
<a class="dropdown-item text-danger" href="post.php?archive_asset_note=<?= $asset_note_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-archive me-2"></i>Archive
</a>
<?php if ($can_delete) { ?>
<a class="dropdown-item text-danger text-bold confirm-link" href="post.php?delete_asset_note=<?= $asset_note_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-trash me-2"></i>Delete
</a>
<?php } ?>
</div>
</div>
<?php } ?>
</div>
<?php } ?>
</div>
<?php } else { ?>
<div class="itflow-asset-empty">No notes yet.</div>
<?php } ?>
</div>
</div>
</div>
</div>
</div>
<?php
require_once "modals/share_modal.php";
require_once "modals/asset/asset_interface_multiple_add.php";
require_once "modals/asset/asset_interface_import.php";
require_once "modals/asset/asset_interface_export.php";
?>
<script>
function updateAssetNotes(asset_id) {
itflowPost("ajax.php", {
asset_set_notes: 'TRUE',
csrf_token: '<?= $_SESSION['csrf_token'] ?>',
asset_id: asset_id,
notes: document.getElementById("assetNotes").value
});
}
/*
* Which tab is open: the URL hash wins (#tickets - links from elsewhere
* use it), then the tab last used on any asset, then Interfaces.
* Choosing a tab updates the hash without adding a history entry, so a
* refresh or the redirect back from a save lands on the same tab.
* Bootstrap loads in the footer, so this waits for it.
*/
document.addEventListener('DOMContentLoaded', function () {
var key = 'itflow.assetTab';
var tabs = document.querySelectorAll('[data-asset-tab]');
if (!tabs.length) {
return;
}
function byName(name) {
return document.querySelector('[data-asset-tab="' + name + '"]');
}
var wanted = location.hash.replace('#', '');
var remembered = '';
try { remembered = localStorage.getItem(key) || ''; } catch (e) {}
var start = byName(wanted) || byName(remembered) || tabs[0];
bootstrap.Tab.getOrCreateInstance(start).show();
tabs.forEach(function (tab) {
tab.addEventListener('shown.bs.tab', function () {
var name = tab.getAttribute('data-asset-tab');
try { localStorage.setItem(key, name); } catch (e) {}
history.replaceState(null, '', location.pathname + location.search + '#' + name);
});
});
});
</script>
<!-- Fetch TOTP codes and passwords only when asked for -->
<script src="js/credential_show_otp_via_id.js"></script>
<script src="js/credential_show_password_via_id.js"></script>
<script src="../js/bulk_actions.js"></script>
<?php
}
require_once "../includes/footer.php";