Files
itflow/agent/assets.php
johnnyq 9ba21cf352 Assets: add NAS, UPS, PDU, Modem/ONT and NVR/DVR types from a new registry in functions/asset_type.php that sets each type's icon, tone, group, fields and details; the asset form shows only the fields a type uses (fields that already hold a value stay shown) and adds a Details section per type (CPU/RAM/storage, VM host/vCPUs, switch ports/SFP/PoE/management, NAS drives/capacity/RAID, UPS capacity/output/runtime/outlets/battery dates, PDU kind/outlets/input, modem circuit ID/ISP account, printer, phone, mobile, display, camera and NVR details) stored in a new asset_specs table (database 3.4.9); new or changed port and SFP counts add the missing interfaces, continuing the asset's numbering, skipping Wi-Fi, VLAN and management ports and never deleting any; the asset page and quick look show the details, a server or NAS lists the VMs that run on it, and Assigned and Warranty show only on types that use them; the list gets Servers & NAS and Power tabs, an optional Details column, a UPS battery badge, an Assigned picker only on assigned types, and New buttons named for what they add; nightly UPS Battery Due notifications at 45, 7 and 1 days as a Support notification; copy leaves out per-unit details, Transfer to Client carries details and interface types, export adds per-detail columns, import reads detail columns after the standard twelve (VM hosts by name, resolved after every row is in) and reports values it could not keep, and the API reads and writes asset_specs; racks, client portal, client export, overview, document templates and AI lists group the new types; fix asset import carrying physical location and notes into the next row, saving 0000-00-00 for a missing purchase date and warning on an unknown contact or location, save API and import types in the list's spelling, and keep interface types on transfer; changelog updated
2026-10-27 15:59:03 -04:00

784 lines
48 KiB
PHP

<?php
/*
* Assets - one client's assets, or every asset the agent can see from All
* Client Docs. Each row reads left to right the way a tech scans for a box:
* what it is, how to reach it (IP, links), what it is physically (make,
* model, serial, warranty), where it is (location, rack) and its status.
* IPs, MACs and serials copy with one click.
*/
// Default Column Sortby Filter
$sort = "asset_name";
$order = "ASC";
// 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&";
// Override Filter Header Archived
if (isset($_GET['archived']) && $_GET['archived'] == 1) {
$archived = 1;
$archive_query = "asset_archived_at IS NOT NULL";
} else {
$archived = 0;
$archive_query = "asset_archived_at IS NULL";
}
} else {
require_once "includes/inc_client_overview_all.php";
$client_query = '';
$client_url = '';
// Override Filter Header Archived
if (isset($_GET['archived']) && $_GET['archived'] == 1) {
$archived = 1;
$archive_query = "(client_archived_at IS NOT NULL OR asset_archived_at IS NOT NULL)";
} else {
$archived = 0;
$archive_query = "(client_archived_at IS NULL AND asset_archived_at IS NULL)";
}
}
// Perms
enforceUserPermission('module_support');
$can_edit = lookupUserPermission('module_support') >= 2;
$can_delete = lookupUserPermission('module_support') >= 3;
// Asset type tabs (functions/asset_type.php)
$asset_type_tabs = assetTypeTabs();
$type_filter = (isset($_GET['type']) && isset($asset_type_tabs[$_GET['type']])) ? $_GET['type'] : '';
$_GET['type'] = $type_filter;
$type_query = $type_filter ? $asset_type_tabs[$type_filter]['query'] : "1 = 1";
$client = '';
if (!$client_url) {
// Client Filter
if (!empty($_GET['client'])) {
$client = intval($_GET['client']);
$client_query = "AND asset_client_id = $client";
}
}
// Location Filter
if ($client_url && !empty($_GET['location'])) {
$location_filter = intval($_GET['location']);
$location_query = "AND asset_location_id = $location_filter";
} else {
// Default - any
$location_query = '';
$location_filter = 0;
}
// Status Filter - the asset statuses (Admin > Categories), held by name
$status_filter = trim(strip_tags((string) ($_GET['status'] ?? '')));
$status_query = $status_filter !== '' ? "AND asset_status = '" . escapeSql($status_filter) . "'" : '';
// Tags Filter
if (isset($_GET['tags']) && is_array($_GET['tags']) && !empty($_GET['tags'])) {
$tag_filter = implode(",", array_map('intval', $_GET['tags']));
$tag_query = "AND asset_id IN (SELECT asset_tag_asset_id FROM asset_tags WHERE asset_tag_tag_id IN ($tag_filter))";
} else {
$tag_filter = 0;
$tag_query = '';
}
// Expiring In Filter
if (!empty($_GET['expire_days'])) {
if ($_GET['expire_days'] == "expired") {
$expire_days = "expired";
$expire_query = "AND (asset_warranty_expire IS NOT NULL AND asset_warranty_expire != '0000-00-00' AND asset_warranty_expire < CURDATE())";
} else {
$expire_days = intval($_GET['expire_days']);
$expire_query = "AND (asset_warranty_expire IS NOT NULL AND asset_warranty_expire != '0000-00-00' AND asset_warranty_expire BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL $expire_days DAY))";
}
} else {
// Default - any
$expire_days = '';
$expire_query = '';
}
// Extra columns, picked from the filter bar
$extra_column_options = [
'Mac_Address' => 'MAC address',
'Purchase_Date' => 'Purchase date',
'Install_Date' => 'Install date',
'Warranty_Expire' => 'Warranty',
'Details' => 'Details',
];
$extra_columns = array_values(array_intersect(array_keys($extra_column_options), (array) ($_GET['show_column'] ?? [])));
$show_mac = in_array('Mac_Address', $extra_columns, true);
$show_purchase = in_array('Purchase_Date', $extra_columns, true);
$show_install = in_array('Install_Date', $extra_columns, true);
$show_warranty = in_array('Warranty_Expire', $extra_columns, true);
$show_details = in_array('Details', $extra_columns, true);
// Assigned To only means something for workstations and the like - printers are shared
$show_contact = !in_array($type_filter, ['network', 'server', 'virtual', 'power', 'printer', 'other'], true);
$show_model = $type_filter !== 'virtual';
// 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 = [
'asset_name', 'asset_type', 'asset_make', 'interface_ip', 'interface_mac', 'asset_purchase_date',
'asset_install_date', 'asset_warranty_expire', 'contact_name', 'location_name', 'asset_status',
'client_name', 'asset_created_at'
];
if (!in_array($sort, $sortable_columns, true)) {
$sort = 'asset_name';
}
// Addresses sort by value - .9 before .10 - and this works for IPv4 and IPv6 alike
$order_by = $sort === 'interface_ip' ? "INET6_ATON(interface_ip) $order, interface_ip $order" : "$sort $order";
// Tab counts - within the client, status, tag and archive scope, before the search narrows it
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "
SELECT
COUNT(*) AS all_count" . implode('', array_map(function ($tab_key, $tab) {
return ",\n SUM({$tab['query']}) AS {$tab_key}_count";
}, array_keys($asset_type_tabs), $asset_type_tabs)) . "
FROM assets
LEFT JOIN clients ON client_id = asset_client_id
WHERE $archive_query
$status_query
$tag_query
" . clientScopeSql('asset_client_id') . "
$client_query
"));
$tab_counts = ['' => intval($row['all_count'])];
foreach ($asset_type_tabs as $tab_key => $tab) {
$tab_counts[$tab_key] = intval($row[$tab_key . '_count']);
}
$sql = mysqli_query(
$mysqli,
"SELECT SQL_CALC_FOUND_ROWS asset_archived_at, asset_client_id, asset_contact_id, asset_description, asset_favorite,
asset_id, asset_install_date, asset_location_id, asset_make, asset_model, asset_name, asset_os, asset_physical_location,
asset_purchase_date, asset_serial, asset_status, asset_type, asset_uri, asset_uri_2,
asset_uri_client, asset_warranty_expire, client_id, client_name,
contact_archived_at, contact_name, contact_photo, interface_ip, interface_ipv6, interface_mac,
location_archived_at, location_name FROM assets
LEFT JOIN clients ON asset_client_id = client_id
LEFT JOIN contacts ON asset_contact_id = contact_id
LEFT JOIN locations ON asset_location_id = location_id
LEFT JOIN asset_interfaces ON interface_asset_id = asset_id AND interface_primary = 1
WHERE $archive_query
$tag_query
AND (asset_name LIKE '%$q%' OR asset_description LIKE '%$q%' OR asset_type LIKE '%$q%' OR interface_ip LIKE '%$q%' OR interface_ipv6 LIKE '%$q%' OR interface_mac LIKE '%$q%' OR asset_make LIKE '%$q%' OR asset_model LIKE '%$q%' OR asset_serial LIKE '%$q%' OR asset_os LIKE '%$q%' OR contact_name LIKE '%$q%' OR location_name LIKE '%$q%' OR client_name LIKE '%$q%'
OR asset_id IN (SELECT asset_tag_asset_id FROM asset_tags JOIN tags ON tag_id = asset_tag_tag_id WHERE tag_name LIKE '%$q%'))
AND ($type_query)
" . clientScopeSql('asset_client_id') . "
$status_query
$location_query
$expire_query
$client_query
GROUP BY asset_id
ORDER BY $order_by LIMIT $record_from, $record_to"
);
$num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
$assets = [];
while ($row = mysqli_fetch_assoc($sql)) {
$assets[intval($row['asset_id'])] = $row;
}
// Each one's details - its type's (functions/asset_type.php): a UPS battery due is flagged like a warranty, and Details lists them
$asset_list_specs = assetSpecsFor(array_keys($assets));
// Tags and rack positions for the whole page in two queries, not two per row
$assets_tags = tagsForRecords('asset', array_keys($assets));
$assets_racked = [];
if ($assets) {
$sql_racked = mysqli_query($mysqli, "SELECT unit_asset_id, unit_start_number, unit_end_number, rack_id, rack_name FROM rack_units
JOIN racks ON rack_id = unit_rack_id
WHERE unit_asset_id IN (" . implode(',', array_keys($assets)) . ")");
while ($row = mysqli_fetch_assoc($sql_racked)) {
$assets_racked[intval($row['unit_asset_id'])] = $row;
}
}
$search_noun = $type_filter ? $asset_type_tabs[$type_filter]['label'] : 'assets';
?>
<div class="card">
<div class="card-header bg-dark py-2">
<h3 class="card-title mt-2"><i class="fas fa-fw fa-desktop me-2"></i>Assets</h3>
<div class="card-tools">
<?php if ($can_edit) { ?>
<div class="btn-group">
<button type="button" class="btn btn-primary ajax-modal" data-modal-size="lg" data-modal-url="modals/asset/asset_add.php?<?= $client_url ?>&type=<?= $type_filter ?>">
<i class="fas fa-plus me-2"></i>New <?= $type_filter ? $asset_type_tabs[$type_filter]['new'] : 'Asset' ?>
</button>
<button type="button" class="btn btn-primary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown"></button>
<div class="dropdown-menu">
<?php if ($client_url) { ?>
<a class="dropdown-item text-dark ajax-modal" href="#"
data-modal-url="modals/asset/asset_import.php?<?= $client_url ?>">
<i class="fa fa-fw fa-upload me-2"></i>Import
</a>
<?php } ?>
<?php if ($num_rows[0] > 0) { ?>
<?php if ($client_url) { ?>
<div class="dropdown-divider"></div>
<?php } ?>
<a class="dropdown-item text-dark ajax-modal" href="#"
data-modal-url="<?= buildExportModalUrl('modals/asset/asset_export.php', ['client_id', 'type', 'client', 'location', 'status', 'tags', 'expire_days', 'archived', 'q']) ?>">
<i class="fa fa-fw fa-download me-2"></i>Export
</a>
<?php } ?>
</div>
</div>
<?php } ?>
</div>
</div>
<?php if ($tab_counts['']) { ?>
<!-- Kind of asset -->
<div class="card-header py-2 itflow-asset-tabs">
<nav class="nav nav-pills flex-nowrap overflow-auto">
<a class="nav-link text-nowrap <?= $type_filter === '' ? 'active' : '' ?>" href="?<?= $url_query_strings_sort ?>&type=">
All <span class="badge rounded-pill ms-1"><?= $tab_counts[''] ?></span>
</a>
<?php foreach ($asset_type_tabs as $tab_key => $tab) {
if (!$tab_counts[$tab_key] && $type_filter !== $tab_key) {
continue;
} ?>
<a class="nav-link text-nowrap <?= $type_filter === $tab_key ? 'active' : '' ?>" href="?<?= $url_query_strings_sort ?>&type=<?= $tab_key ?>">
<i class="fas fa-fw <?= $tab['icon'] ?> me-1"></i><?= $tab['label'] ?> <span class="badge rounded-pill ms-1"><?= $tab_counts[$tab_key] ?></span>
</a>
<?php } ?>
</nav>
</div>
<?php } ?>
<div class="card-header py-3">
<form autocomplete="off">
<?php if ($client_url) { ?>
<input type="hidden" name="client_id" value="<?= $client_id ?>">
<?php } ?>
<input type="hidden" name="type" value="<?= $type_filter ?>">
<input type="hidden" name="archived" value="<?= $archived ?>">
<div class="row g-2 align-items-center">
<div class="col-xl-3 col-lg-4 col-md-6">
<div class="input-group">
<input type="search" class="form-control" name="q" value="<?php if (isset($q)) { echo stripslashes(escapeHtml($q)); } ?>" placeholder="Search <?= strtolower($search_noun) ?>, IPs, serials...">
<button class="btn btn-dark" title="Search"><i class="fa fa-search"></i></button>
</div>
</div>
<?php if ($client_url) { ?>
<div class="col-xl col-lg-4 col-md-6">
<select class="form-select select2" name="location" onchange="this.form.submit()">
<option value="">- All Locations -</option>
<?php
$sql_locations_filter = mysqli_query($mysqli, "
SELECT DISTINCT location_id, location_name
FROM locations
WHERE location_client_id = $client_id
AND ( EXISTS (SELECT 1 FROM assets WHERE asset_location_id = location_id AND $archive_query) OR location_id = $location_filter)
ORDER BY location_name ASC
");
while ($row = mysqli_fetch_assoc($sql_locations_filter)) {
$location_id = intval($row['location_id']);
$location_name = escapeHtml($row['location_name']);
?>
<option <?php if ($location_filter == $location_id) { echo "selected"; } ?> value="<?= $location_id ?>"><?= $location_name ?></option>
<?php } ?>
</select>
</div>
<?php } else { ?>
<div class="col-xl col-lg-4 col-md-6">
<select class="form-select select2" name="client" onchange="this.form.submit()">
<option value="" <?php if ($client == "") { echo "selected"; } ?>>- All Clients -</option>
<?php
$sql_clients_filter = mysqli_query($mysqli, "
SELECT DISTINCT client_id, client_name, client_abbreviation
FROM clients
JOIN assets ON asset_client_id = client_id
WHERE $archive_query
" . clientScopeSql('clients.client_id') . "
ORDER BY client_name ASC
");
while ($row = mysqli_fetch_assoc($sql_clients_filter)) {
$client_id_filter = intval($row['client_id']);
$client_name_filter = escapeHtml($row['client_name']);
?>
<option <?php if ($client == $client_id_filter) { echo "selected"; } ?> value="<?= $client_id_filter ?>"<?= selectOptionClient($row['client_id'], $row['client_name'], $row['client_abbreviation'] ?? '') ?>><?= $client_name_filter ?></option>
<?php } ?>
</select>
</div>
<?php } ?>
<div class="col-xl col-lg-4 col-md-6">
<select class="form-select select2" name="status" onchange="this.form.submit()">
<option value="">- Status -</option>
<?= categoryFilterOptionsHtml('asset_status', $status_filter, "SELECT DISTINCT asset_status FROM assets
LEFT JOIN clients ON client_id = asset_client_id
WHERE $archive_query AND asset_status IS NOT NULL AND asset_status <> ''
" . clientScopeSql('asset_client_id') . " $client_query") ?>
</select>
</div>
<div class="col-xl col-lg-3 col-md-6">
<select onchange="this.form.submit()" class="form-select select2" name="tags[]" data-placeholder="- Tags -" multiple>
<?php
// Tags in use on assets in this scope, plus any already picked
$sql_tags_filter = mysqli_query($mysqli, "
SELECT tag_id, tag_name, tag_color, tag_icon
FROM tags
WHERE tag_type = 5 AND (tag_archived_at IS NULL OR tag_id IN ($tag_filter))
AND (
EXISTS (SELECT 1 FROM asset_tags JOIN assets ON asset_id = asset_tag_asset_id
WHERE asset_tag_tag_id = tag_id " . clientScopeSql('asset_client_id') . " $client_query)
OR tag_id IN ($tag_filter)
)
ORDER BY tag_name ASC
");
while ($row = mysqli_fetch_assoc($sql_tags_filter)) {
$tag_id = intval($row['tag_id']);
$tag_name = escapeHtml($row['tag_name']); ?>
<option value="<?= $tag_id ?>" <?php if (isset($_GET['tags']) && in_array($tag_id, $_GET['tags'])) { echo 'selected'; } ?><?= tagOptionLook($row['tag_color'] ?? '', $row['tag_icon'] ?? '') ?>><?= $tag_name ?></option>
<?php } ?>
</select>
</div>
<div class="col-xl col-lg-3 col-md-6">
<select class="form-select select2" name="expire_days" onchange="this.form.submit()">
<option value="" <?php if ($expire_days == "") { echo "selected"; } ?>>- Warranty -</option>
<option value="expired" <?php if ($expire_days === "expired") { echo "selected"; } ?>>Expired</option>
<?php foreach ([7, 30, 45, 60, 90] as $expire_option) { ?>
<option value="<?= $expire_option ?>" <?php if ($expire_days === $expire_option) { echo "selected"; } ?>>Expiring in <?= $expire_option ?> days</option>
<?php } ?>
</select>
</div>
<div class="col-xl col-lg-3 col-md-6">
<select onchange="this.form.submit()" class="form-select select2" name="show_column[]" data-placeholder="- More columns -" multiple>
<?php foreach ($extra_column_options as $column_key => $column_label) { ?>
<option value="<?= $column_key ?>" <?php if (in_array($column_key, $extra_columns, true)) { echo 'selected'; } ?>><?= $column_label ?></option>
<?php } ?>
</select>
</div>
<div class="col-xl-auto col-lg-3 col-md-12">
<div class="d-flex justify-content-end gap-2">
<a href="?<?= $client_url ?>archived=<?= $archived ? 0 : 1 ?>"
class="btn <?= $archived ? 'btn-dark' : 'btn-outline-dark' ?>" title="<?= $archived ? 'Back to active assets' : 'Show archived assets' ?>">
<i class="fa fa-fw fa-archive"></i><span class="d-lg-none ms-2">Archived</span>
</a>
<?php if ($can_edit) { // every bulk action changes them ?>
<div class="dropdown" id="bulkActionButton" hidden>
<button class="btn btn-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"></span>)
</button>
<div class="dropdown-menu dropdown-menu-end">
<button class="dropdown-item"
type="submit" form="bulkActions" name="bulk_favorite_assets">
<i class="fas fa-fw fa-star text-warning me-2"></i>Favorite
</button>
<button class="dropdown-item"
type="submit" form="bulkActions" name="bulk_unfavorite_assets">
<i class="far fa-fw fa-star me-2"></i>Unfavorite
</button>
<div class="dropdown-divider"></div>
<?php if ($client_url) { ?>
<a class="dropdown-item ajax-modal" href="#"
data-modal-url="modals/asset/asset_bulk_assign_contact.php?<?= $client_url ?>"
data-bulk="true">
<i class="fas fa-fw fa-user me-2"></i>Assign Contact
</a>
<a class="dropdown-item ajax-modal" href="#"
data-modal-url="modals/asset/asset_bulk_assign_location.php?<?= $client_url ?>"
data-bulk="true">
<i class="fas fa-fw fa-map-marker-alt me-2"></i>Assign Location
</a>
<?php } ?>
<a class="dropdown-item ajax-modal" href="#"
data-modal-url="modals/asset/asset_bulk_assign_physical_location.php"
data-bulk="true">
<i class="fas fa-fw fa-door-closed me-2"></i>Set Physical Location
</a>
<a class="dropdown-item ajax-modal" href="#"
data-modal-url="modals/asset/asset_bulk_edit_status.php"
data-bulk="true">
<i class="fas fa-fw fa-info-circle me-2"></i>Set Status
</a>
<a class="dropdown-item" href="#" data-tag-picker data-tag-entity="asset" data-tag-bulk data-csrf="<?= $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-tags me-2"></i>Tags
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item ajax-modal" href="#"
data-modal-url="modals/asset/asset_bulk_add_ticket.php"
data-modal-size="lg"
data-bulk="true">
<i class="fas fa-fw fa-life-ring me-2"></i>Create Tickets
</a>
<a class="dropdown-item ajax-modal" href="#"
data-modal-url="modals/asset/asset_bulk_transfer_client.php?<?= $client_url ?>"
data-bulk="true">
<i class="fas fa-fw fa-arrow-right me-2"></i>Transfer to Client
</a>
<?php if ($archived) { ?>
<div class="dropdown-divider"></div>
<button class="dropdown-item text-info"
type="submit" form="bulkActions" name="bulk_restore_assets">
<i class="fas fa-fw fa-redo me-2"></i>Restore
</button>
<?php if ($can_delete) { ?>
<button class="dropdown-item text-danger text-bold"
type="submit" form="bulkActions" name="bulk_delete_assets">
<i class="fas fa-fw fa-trash me-2"></i>Delete
</button>
<?php } ?>
<?php } else { ?>
<div class="dropdown-divider"></div>
<button class="dropdown-item text-danger confirm-link"
type="submit" form="bulkActions" name="bulk_archive_assets">
<i class="fas fa-fw fa-archive me-2"></i>Archive
</button>
<?php } ?>
</div>
</div>
<?php } ?>
</div>
</div>
</div>
</form>
</div>
<form id="bulkActions" action="post.php" method="post" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0 itflow-asset-table">
<thead class="<?php if (!$num_rows[0]) { echo "d-none"; } ?> text-nowrap table-light">
<tr>
<?php if ($can_edit) { ?>
<th class="checkbox-column border-end">
<div class="form-check">
<input class="form-check-input" id="selectAllCheckbox" type="checkbox" onclick="checkAll(this)" title="Select all on this page">
</div>
</th>
<?php } ?>
<th>
<a class="text-reset text-decoration-none" href="?<?= $url_query_strings_sort ?>&sort=asset_name&order=<?= $disp ?>">
Name <?php if ($sort == 'asset_name') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-reset text-decoration-none" href="?<?= $url_query_strings_sort ?>&sort=asset_type&order=<?= $disp ?>">
Type / OS <?php if ($sort == 'asset_type') { echo $order_icon; } ?>
</a>
</th>
<?php if ($show_model) { ?>
<th>
<a class="text-reset text-decoration-none" href="?<?= $url_query_strings_sort ?>&sort=asset_make&order=<?= $disp ?>">
Model / Serial <?php if ($sort == 'asset_make') { echo $order_icon; } ?>
</a>
</th>
<?php } ?>
<th>
<a class="text-reset text-decoration-none" href="?<?= $url_query_strings_sort ?>&sort=interface_ip&order=<?= $disp ?>">
IP <?php if ($sort == 'interface_ip') { echo $order_icon; } ?>
</a>
</th>
<?php if ($show_mac) { ?>
<th>
<a class="text-reset text-decoration-none" href="?<?= $url_query_strings_sort ?>&sort=interface_mac&order=<?= $disp ?>">
MAC <?php if ($sort == 'interface_mac') { echo $order_icon; } ?>
</a>
</th>
<?php } ?>
<?php if ($show_purchase) { ?>
<th>
<a class="text-reset text-decoration-none" href="?<?= $url_query_strings_sort ?>&sort=asset_purchase_date&order=<?= $disp ?>">
Purchased <?php if ($sort == 'asset_purchase_date') { echo $order_icon; } ?>
</a>
</th>
<?php } ?>
<?php if ($show_details) { ?>
<th>Details</th>
<?php } ?>
<?php if ($show_install) { ?>
<th>
<a class="text-reset text-decoration-none" href="?<?= $url_query_strings_sort ?>&sort=asset_install_date&order=<?= $disp ?>">
Installed <?php if ($sort == 'asset_install_date') { echo $order_icon; } ?>
</a>
</th>
<?php } ?>
<?php if ($show_warranty) { ?>
<th>
<a class="text-reset text-decoration-none" href="?<?= $url_query_strings_sort ?>&sort=asset_warranty_expire&order=<?= $disp ?>">
Warranty <?php if ($sort == 'asset_warranty_expire') { echo $order_icon; } ?>
</a>
</th>
<?php } ?>
<?php if ($show_contact) { ?>
<th>
<a class="text-reset text-decoration-none" href="?<?= $url_query_strings_sort ?>&sort=contact_name&order=<?= $disp ?>">
Assigned To <?php if ($sort == 'contact_name') { echo $order_icon; } ?>
</a>
</th>
<?php } ?>
<th>
<a class="text-reset text-decoration-none" href="?<?= $url_query_strings_sort ?>&sort=location_name&order=<?= $disp ?>">
Location <?php if ($sort == 'location_name') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-reset text-decoration-none" href="?<?= $url_query_strings_sort ?>&sort=asset_status&order=<?= $disp ?>">
Status <?php if ($sort == 'asset_status') { echo $order_icon; } ?>
</a>
</th>
<?php if (!$client_url) { ?>
<th>
<a class="text-reset text-decoration-none" href="?<?= $url_query_strings_sort ?>&sort=client_name&order=<?= $disp ?>">
Client <?php if ($sort == 'client_name') { echo $order_icon; } ?>
</a>
</th>
<?php } ?>
<th></th>
</tr>
</thead>
<tbody>
<?php
foreach ($assets as $asset_id => $row) {
$client_id = intval($row['client_id']);
$client_name = escapeHtml($row['client_name']);
$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_ip = escapeHtml($row['interface_ip']);
$asset_ipv6 = escapeHtml($row['interface_ipv6']);
$asset_mac = escapeHtml($row['interface_mac']);
$asset_links = assetLinks($row['asset_uri'], $row['asset_uri_2'], $row['asset_uri_client']);
$asset_status_pill = assetStatusPill($row['asset_status']);
$asset_purchase_date = escapeHtml($row['asset_purchase_date']);
$asset_install_date = escapeHtml($row['asset_install_date']);
$asset_warranty = assetWarranty($row['asset_warranty_expire']);
$asset_physical_location = escapeHtml($row['asset_physical_location']);
$asset_favorite = intval($row['asset_favorite']);
$asset_archived_at = escapeHtml($row['asset_archived_at']);
$asset_contact_id = intval($row['asset_contact_id']);
$asset_tone = assetTypeTone($row['asset_type']);
$device_icon = getAssetIcon($row['asset_type']);
$asset_spec_rows = assetSpecRows($row['asset_type'], $asset_list_specs[$asset_id] ?? [], $client_id);
$asset_battery = $asset_spec_rows['battery_replace']['flag'] ?? null;
$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']);
$asset_rack = $assets_racked[$asset_id] ?? null;
$asset_tags = $assets_tags[$asset_id] ?? [];
?>
<tr>
<?php if ($can_edit) { ?>
<td class="checkbox-column bg-light border-end">
<div class="form-check">
<input class="form-check-input bulk-select" type="checkbox" name="asset_ids[]" value="<?= $asset_id ?>" aria-label="Select <?= $asset_name ?>">
</div>
</td>
<?php } ?>
<!-- The whole cell opens the asset (stretched-link); the tag pills sit above it -->
<td class="position-relative itflow-asset-name-cell">
<div class="d-flex align-items-center gap-3">
<span class="itflow-asset-icon itflow-asset-tone-<?= $asset_tone ?>" aria-hidden="true"><i class="fas fa-fw fa-<?= $device_icon ?>"></i></span>
<div class="min-w-0">
<a class="fw-semibold text-body stretched-link" href="asset.php?client_id=<?= $client_id ?>&asset_id=<?= $asset_id ?>"><?= $asset_name ?></a>
<?php if ($asset_favorite) { ?><i class="fas fa-star text-warning ms-1" title="Favorite"></i><?php } ?>
<?php if ($asset_archived_at) { ?><span class="badge rounded-pill text-bg-secondary fw-normal ms-1">Archived</span><?php } ?>
<?php if ($asset_description) { ?>
<div class="small text-body-secondary text-truncate itflow-asset-desc" title="<?= $asset_description ?>"><?= $asset_description ?></div>
<?php } ?>
<?php if ($asset_tags || lookupUserPermission('module_client') >= 2) { ?>
<div class="mt-1 itflow-above-stretch"><?= tagPills('asset', $asset_id, $asset_tags, "assets.php?$client_url") ?></div>
<?php } ?>
</div>
</div>
</td>
<td>
<div><?= $asset_type ?></div>
<?php if ($asset_os) { ?>
<div class="small text-body-secondary text-nowrap"><i class="<?= $asset_os_icon ?> fa-fw me-1"></i><?= $asset_os ?></div>
<?php } ?>
</td>
<?php if ($show_model) { ?>
<td class="itflow-asset-model-cell">
<div><?= $asset_make_model ?: '<span class="text-body-secondary">-</span>' ?></div>
<?php if ($asset_serial) { ?>
<div class="small text-body-secondary text-nowrap">
<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>
</div>
<?php } ?>
<?php if ($asset_warranty && $asset_warranty['tone'] !== 'success' && !$show_warranty) { ?>
<span class="badge rounded-pill bg-<?= $asset_warranty['tone'] ?>-subtle text-<?= $asset_warranty['tone'] ?>-emphasis border border-<?= $asset_warranty['tone'] ?>-subtle fw-normal mt-1" title="Warranty <?= escapeHtml($asset_warranty['date']) ?>">
<i class="fas fa-shield-alt me-1"></i>Warranty <?= strtolower($asset_warranty['short']) ?>
</span>
<?php } ?>
<?php if ($asset_battery && $asset_battery['tone'] !== 'success') { ?>
<span class="badge rounded-pill bg-<?= $asset_battery['tone'] ?>-subtle text-<?= $asset_battery['tone'] ?>-emphasis border border-<?= $asset_battery['tone'] ?>-subtle fw-normal mt-1" title="Replace battery by <?= escapeHtml($asset_battery['date']) ?>" data-asset-battery>
<i class="fas fa-battery-quarter me-1"></i>Battery <?= strtolower(escapeHtml($asset_battery['short'])) ?>
</span>
<?php } ?>
</td>
<?php } ?>
<td class="text-nowrap">
<?php if ($asset_ip) { ?>
<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 } ?>
<?php } else { ?>
<span class="text-body-secondary">-</span>
<?php } ?>
<?php if ($asset_ipv6) { ?>
<div class="small text-body-secondary font-monospace text-truncate" style="max-width: 14rem" title="<?= $asset_ipv6 ?>"><?= $asset_ipv6 ?></div>
<?php } ?>
<?php if ($asset_links) { ?>
<div class="mt-1 d-flex gap-1">
<?php foreach ($asset_links as [$link_label, $link_url]) { ?>
<a class="btn btn-xs btn-outline-secondary" href="<?= escapeUrl($link_url) ?>" target="_blank" rel="noopener" title="<?= $link_label ?>: <?= escapeHtml($link_url) ?>">
<i class="<?= assetLinkIcon($link_url) ?> me-1"></i><?= escapeHtml(assetLinkLabel($link_url)) ?>
</a>
<?php } ?>
</div>
<?php } ?>
</td>
<?php if ($show_mac) { ?>
<td class="text-nowrap">
<?php if ($asset_mac) { ?>
<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>
<?php } else { ?>
<span class="text-body-secondary">-</span>
<?php } ?>
</td>
<?php } ?>
<?php if ($show_purchase) { ?>
<td class="text-nowrap">
<?= $asset_purchase_date ?: '<span class="text-body-secondary">-</span>' ?>
<?php if ($asset_purchase_date) { ?><div class="small text-body-secondary"><?= assetAge($row['asset_purchase_date']) ?> old</div><?php } ?>
</td>
<?php } ?>
<?php if ($show_details) { ?>
<td class="small" data-asset-details>
<?php if ($asset_spec_rows) { ?>
<?php foreach ($asset_spec_rows as $spec_row) { ?>
<div class="text-nowrap"><?= $spec_row['flag'] ? '<span class="text-body-secondary">' . escapeHtml($spec_row['label']) . '</span> <span class="text-' . $spec_row['flag']['tone'] . '-emphasis">' . escapeHtml($spec_row['flag']['date']) . '</span>' : $spec_row['brief'] ?></div>
<?php } ?>
<?php } else { ?>
<span class="text-body-secondary">-</span>
<?php } ?>
</td>
<?php } ?>
<?php if ($show_install) { ?>
<td class="text-nowrap"><?= $asset_install_date ?: '<span class="text-body-secondary">-</span>' ?></td>
<?php } ?>
<?php if ($show_warranty) { ?>
<td class="text-nowrap">
<?php if ($asset_warranty) { ?>
<?= escapeHtml($asset_warranty['date']) ?>
<div class="small text-<?= $asset_warranty['tone'] ?>-emphasis"><?= $asset_warranty['tone'] === 'success' ? 'Covered for ' . escapeHtml(str_replace('in ', '', timeAgo($asset_warranty['date']))) : escapeHtml($asset_warranty['text']) ?></div>
<?php } else { ?>
<span class="text-body-secondary">-</span>
<?php } ?>
</td>
<?php } ?>
<?php if ($show_contact) { ?>
<td>
<?php if ($can_edit && ($asset_contact_id || assetTypeHasField($row['asset_type'], 'contact'))) { // a type that is assigned to someone (functions/asset_type.php) ?>
<?= quickPick('asset_contact', $asset_id, (string) $asset_contact_id, assetQuickHtml('asset_contact', $row), 'list', 0, $client_id) ?>
<?php } elseif ($contact_name) { ?>
<?= contactLink($client_id, $asset_contact_id, $row['contact_name'], $row['contact_photo'], ['archived' => $contact_archived]) ?>
<?php } else { ?>
<span class="text-body-secondary">-</span>
<?php } ?>
</td>
<?php } ?>
<td>
<?php if ($can_edit) { ?>
<?= quickPick('asset_location', $asset_id, (string) intval($row['asset_location_id']), assetQuickHtml('asset_location', $row), 'list', 0, $client_id) ?>
<?php } elseif ($location_name) { ?>
<div class="<?= $location_archived ? 'text-danger text-decoration-line-through' : '' ?>" <?= $location_archived ? 'title="Archived location"' : '' ?>><?= $location_name ?></div>
<?php } elseif (!$asset_physical_location && !$asset_rack) { ?>
<span class="text-body-secondary">-</span>
<?php } ?>
<?php if ($asset_physical_location) { ?>
<div class="small text-body-secondary <?= $can_edit ? 'mt-1' : '' ?>"><?= $asset_physical_location ?></div>
<?php } ?>
<?php if ($asset_rack) { ?>
<a class="small text-nowrap d-inline-block <?= $can_edit && !$asset_physical_location ? 'mt-1' : '' ?>" href="rack.php?client_id=<?= $client_id ?>&rack_id=<?= intval($asset_rack['rack_id']) ?>" title="Racked">
<i class="fas fa-server me-1"></i><?= escapeHtml($asset_rack['rack_name']) ?> &middot; <?= rackPositionLabel($asset_rack['unit_start_number'], $asset_rack['unit_end_number']) ?>
</a>
<?php } ?>
</td>
<td>
<?php if ($can_edit) { ?>
<?= quickPick('asset_status', $asset_id, (string) $row['asset_status'], assetQuickHtml('asset_status', $row), 'list', 0, $client_id) ?>
<?php } else { ?>
<?= $asset_status_pill ?: '<span class="text-body-secondary">-</span>' ?>
<?php } ?>
</td>
<?php if (!$client_url) { ?>
<td><a href="assets.php?client_id=<?= $client_id ?>"><?= $client_name ?></a></td>
<?php } ?>
<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/asset/asset_edit.php?id=<?= $asset_id ?>">
<i class="fas fa-fw fa-edit text-secondary me-2"></i>Edit
</a>
<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 } ?>
</td>
</tr>
<?php } ?>
</tbody>
</table>
</div>
</form>
<?php
$empty_state = [
'thing' => $type_filter ? strtolower($asset_type_tabs[$type_filter]['label']) : 'assets',
'icon' => $type_filter ? $asset_type_tabs[$type_filter]['icon'] : 'fa-desktop',
'views' => ['type'],
'text' => $type_filter ? '' : 'Their computers, servers, network gear and phones - what each is, where it is and who uses it.',
'actions' => $can_edit ? array_merge(
[['label' => 'New ' . ($type_filter ? rtrim($asset_type_tabs[$type_filter]['label'], 's') : 'Asset'), 'modal' => "modals/asset/asset_add.php?$client_url&type=$type_filter", 'size' => 'lg']],
$client_url ? [['label' => 'Import', 'modal' => "modals/asset/asset_import.php?$client_url", 'icon' => 'fa-upload']] : []
) : [],
];
?>
<?php require_once "../includes/filter_footer.php"; ?>
</div>
<script src="../js/bulk_actions.js"></script>
<?php
require_once "../includes/footer.php";