Files
itflow/agent/modals/asset/asset.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

496 lines
29 KiB
PHP

<?php
/*
* Asset quick look - the asset at a glance, opened from a ticket, the client
* overview, a contact, a credential or a document without leaving the page.
*
* A small version of the asset page (agent/asset.php), in the same look: what
* it is in the header, how to get on it and where it is in a few lines with
* no field labels, then whatever is linked to it in tabs with counts. Status,
* location and who it is assigned to change in place (js/quick_pick.js), like
* on the asset page. Purchase details, IPv6, NAT and the like stay on the
* asset page - "Open asset" is one click away.
*/
require_once '../../../includes/modal_header.php';
enforceUserPermission('module_support');
$asset_id = intval($_GET['id'] ?? 0);
$sql = mysqli_query($mysqli, "SELECT asset_archived_at, asset_client_id, asset_contact_id, asset_description, asset_favorite, asset_id,
asset_location_id, asset_make, asset_model, asset_name, asset_notes, asset_os, asset_photo, asset_physical_location,
asset_serial, asset_status, asset_type, asset_uri, asset_uri_2, asset_uri_client, asset_warranty_expire,
contact_archived_at, contact_name, contact_photo,
interface_ip, interface_mac,
location_archived_at, location_name FROM assets
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 asset_id = $asset_id
LIMIT 1
");
$asset_row = mysqli_fetch_assoc($sql);
if (!$asset_row) {
echo json_encode(['error' => 'That asset no longer exists.']);
exit;
}
$client_id = intval($asset_row['asset_client_id']);
enforceClientAccess();
$can_edit = lookupUserPermission('module_support') >= 2;
$can_credentials = lookupUserPermission('module_credential') >= 1;
$asset_name = escapeHtml($asset_row['asset_name']);
$asset_type = escapeHtml($asset_row['asset_type']);
$asset_description = escapeHtml($asset_row['asset_description']);
$asset_make_model = escapeHtml(trim($asset_row['asset_make'] . ' ' . $asset_row['asset_model']));
$asset_os = escapeHtml($asset_row['asset_os']);
$asset_os_icon = assetOsIcon($asset_row['asset_os']);
$asset_serial = escapeHtml($asset_row['asset_serial']);
$asset_ip = escapeHtml($asset_row['interface_ip']);
$asset_mac = escapeHtml($asset_row['interface_mac']);
$asset_links = assetLinks($asset_row['asset_uri'], $asset_row['asset_uri_2'], $asset_row['asset_uri_client']);
$asset_warranty = assetWarranty($asset_row['asset_warranty_expire']);
$asset_photo = escapeHtml($asset_row['asset_photo']);
$asset_physical_location = escapeHtml($asset_row['asset_physical_location']);
$asset_notes = escapeHtml($asset_row['asset_notes']);
$asset_favorite = intval($asset_row['asset_favorite']);
$asset_archived = !empty($asset_row['asset_archived_at']);
$asset_contact_id = intval($asset_row['asset_contact_id']);
$asset_tone = assetTypeTone($asset_row['asset_type']);
$device_icon = getAssetIcon($asset_row['asset_type']);
$asset_tags_display = tagPills('asset', $asset_id, tagsForRecord('asset', $asset_id), "assets.php?client_id=$client_id&");
// The rack it sits in, if any
$asset_rack = rackPositionForAsset($asset_id);
// Interfaces, primary first, and what each one is plugged into
$sql_related_interfaces = mysqli_query($mysqli, "
SELECT ai.interface_id, ai.interface_name, ai.interface_mac, ai.interface_ip, ai.interface_primary,
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 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);
// Credentials - only for agents who may see them
$related_credentials = $can_credentials ? credentialRows("credential_asset_id = $asset_id AND credential_client_id = $client_id") : [];
$credential_count = count($related_credentials);
// Tickets - open ones first; the most recent few here, the rest on the asset page
$quick_ticket_limit = 8;
[$ticket_count, $ticket_open_count] = ticketCounts(ticketAssetSql($asset_id));
$related_tickets = ticketRows(ticketAssetSql($asset_id), $quick_ticket_limit);
// Recurring tickets
$sql_related_recurring_tickets = mysqli_query($mysqli, "SELECT recurring_tickets.recurring_ticket_id, recurring_ticket_subject,
recurring_ticket_frequency, recurring_ticket_next_run, recurring_ticket_paused_at
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);
// Licenses it holds a seat on, with the key it was given
$held_software = softwareHeldBy('asset', $asset_id);
$software_count = count($held_software);
// The services it runs - what goes down with it
$asset_services = servicesFor('asset', $asset_id);
$service_count = count($asset_services);
// Documents
$linked_documents = linkedRecords('asset', $asset_id, 'document');
$document_count = count($linked_documents);
// Files
$linked_files = linkedRecords('asset', $asset_id, 'file');
$file_count = count($linked_files);
// Notes
$sql_related_notes = mysqli_query($mysqli, "SELECT asset_note, asset_note_created_at, 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 = [];
$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[$row['category_name']] = escapeHtml($row['category_icon']);
}
// Only the tabs with something in them - same order as the asset page
$quick_tabs = array_filter([
'interfaces' => ['label' => 'Interfaces', 'count' => $interface_count],
'credentials' => ['label' => 'Credentials', 'count' => $credential_count],
'tickets' => ['label' => 'Tickets', 'count' => $ticket_count, 'badge' => $ticket_open_count],
'recurring' => ['label' => 'Recurring', 'count' => $recurring_ticket_count],
'licenses' => ['label' => 'Licenses', 'count' => $software_count],
'services' => ['label' => 'Services', 'count' => $service_count],
'documents' => ['label' => 'Documents', 'count' => $document_count],
'files' => ['label' => 'Files', 'count' => $file_count],
'notes' => ['label' => 'Notes', 'count' => $note_count],
], function ($tab) {
return $tab['count'] > 0;
});
$first_tab = array_key_first($quick_tabs);
// Its type's details (functions/asset_type.php) - and Assigned only on a type someone is given, or one that has someone
$asset_spec_rows = assetSpecRows($asset_row['asset_type'], assetSpecs($asset_id), $client_id);
$asset_has_contact = assetTypeHasField($asset_row['asset_type'], 'contact') || $asset_row['contact_name'];
$has_left = $asset_ip || $asset_mac || $asset_serial || $asset_warranty || $asset_spec_rows;
$has_right = $can_edit || $asset_row['location_name'] || $asset_row['contact_name'] || $asset_rack;
ob_start();
?>
<!-- What it is - the asset page's header, in the modal's dark bar -->
<div class="modal-header bg-dark" data-bs-theme="dark">
<div class="d-flex align-items-center gap-3 min-w-0">
<?php if ($asset_photo) { ?>
<img class="itflow-asset-icon" src="<?= "../uploads/clients/$client_id/$asset_photo" ?>" alt="">
<?php } else { ?>
<span class="itflow-asset-icon itflow-asset-tone-<?= $asset_tone ?>" aria-hidden="true"><i class="fas fa-fw fa-<?= $device_icon ?>"></i></span>
<?php } ?>
<div class="min-w-0">
<h5 class="modal-title d-flex flex-wrap align-items-center column-gap-2 row-gap-1">
<span class="fw-semibold text-break"><?= $asset_name ?></span>
<?php if ($asset_favorite) { ?><i class="fas fa-star text-warning fs-6" title="Favorite"></i><?php } ?>
<?php if ($asset_archived) { ?><span class="badge rounded-pill text-bg-secondary fw-normal fs-6">Archived</span><?php } ?>
<span class="fs-6">
<?php if ($can_edit) { ?>
<?= quickPick('asset_status', $asset_id, (string) $asset_row['asset_status'], assetQuickHtml('asset_status', $asset_row), 'list', 0, $client_id) ?>
<?php } else { ?>
<?= assetStatusPill($asset_row['asset_status']) ?>
<?php } ?>
</span>
</h5>
<div class="small text-body-secondary d-flex flex-wrap column-gap-3 mt-1">
<span><?= $asset_type ?></span>
<?php if ($asset_make_model) { ?><span><?= $asset_make_model ?></span><?php } ?>
<?php if ($asset_os) { ?><span><i class="<?= $asset_os_icon ?> fa-fw me-1"></i><?= $asset_os ?></span><?php } ?>
</div>
</div>
</div>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<?php if ($asset_description || $asset_tags_display || $asset_links) { ?>
<div class="d-flex flex-wrap align-items-start justify-content-between gap-2 mb-3">
<div class="min-w-0">
<?php if ($asset_description) { ?><div class="text-body-secondary"><?= $asset_description ?></div><?php } ?>
<?php if ($asset_tags_display) { ?><div class="<?= $asset_description ? 'mt-1' : '' ?>"><?= $asset_tags_display ?></div><?php } ?>
</div>
<?php if ($asset_links) { ?>
<div class="d-flex flex-wrap gap-2">
<?php foreach ($asset_links as [$link_label, $link_url]) { ?>
<a class="btn btn-sm btn-outline-primary" 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)) ?><?= $link_label === 'Client link' ? ' (client)' : '' ?>
</a>
<?php } ?>
</div>
<?php } ?>
</div>
<?php } ?>
<!-- At a glance: how to get on it, and where it is - no labels, the icons say what each line is -->
<?php if ($has_left || $has_right) { ?>
<div class="row g-2 itflow-asset-glance">
<?php if ($has_left) { ?>
<div class="col-sm-6">
<?php if ($asset_ip) { ?>
<div class="itflow-glance-item" title="IP address">
<i class="fas fa-fw fa-globe"></i>
<span><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 } ?></span>
</div>
<?php } ?>
<?php if ($asset_mac) { ?>
<div class="itflow-glance-item" title="MAC address">
<i class="fas fa-fw fa-ethernet"></i>
<span><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></span>
</div>
<?php } ?>
<?php if ($asset_serial) { ?>
<div class="itflow-glance-item" title="Serial number">
<i class="fas fa-fw fa-barcode"></i>
<span><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></span>
</div>
<?php } ?>
<?php if ($asset_warranty) { ?>
<div class="itflow-glance-item" title="Warranty <?= escapeHtml($asset_warranty['date']) ?>">
<i class="fas fa-fw fa-shield-alt"></i>
<span class="text-<?= $asset_warranty['tone'] ?>-emphasis">Warranty <?= strtolower(escapeHtml($asset_warranty['text'])) ?></span>
</div>
<?php } ?>
<?php foreach ($asset_spec_rows as $spec_key => $spec_row) { ?>
<div class="itflow-glance-item" title="<?= escapeHtml($spec_row['label']) ?>" data-asset-spec-row="<?= $spec_key ?>">
<i class="fas fa-fw <?= $spec_row['icon'] ?>"></i>
<?php if ($spec_row['flag']) { ?>
<span class="text-<?= $spec_row['flag']['tone'] ?>-emphasis"><?= escapeHtml($spec_row['label']) ?> - <?= strtolower(escapeHtml($spec_row['flag']['text'])) ?></span>
<?php } else { ?>
<span><?php if (!$spec_row['alone']) { ?><span class="small text-body-secondary me-1"><?= escapeHtml($spec_row['label']) ?></span><?php } ?><?= $spec_row['html'] ?></span>
<?php } ?>
</div>
<?php } ?>
</div>
<?php } ?>
<?php if ($has_right) { ?>
<div class="col-sm-6">
<?php if ($can_edit || $asset_row['location_name']) { ?>
<div class="itflow-glance-item" title="Location">
<i class="fas fa-fw fa-map-marker-alt"></i>
<div class="flex-grow-1 min-w-0">
<?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 } else { ?>
<?= assetQuickHtml('asset_location', $asset_row, 'detail') ?>
<?php } ?>
<?php if ($asset_physical_location) { ?><div class="small text-body-secondary"><?= $asset_physical_location ?></div><?php } ?>
</div>
</div>
<?php } ?>
<?php if ($asset_has_contact && ($can_edit || $asset_row['contact_name'])) { ?>
<div class="itflow-glance-item" title="Assigned to">
<i class="fas fa-fw fa-user"></i>
<div class="flex-grow-1 min-w-0">
<?php if ($can_edit) { ?>
<?= quickPick('asset_contact', $asset_id, (string) $asset_contact_id, assetQuickHtml('asset_contact', $asset_row, 'detail'), 'detail', 0, $client_id) ?>
<?php } else { ?>
<?= assetQuickHtml('asset_contact', $asset_row, 'detail') ?>
<?php } ?>
</div>
</div>
<?php } ?>
<?php if ($asset_rack) { ?>
<div class="itflow-glance-item" title="Rack">
<i class="fas fa-fw fa-server"></i>
<span><a href="rack.php?client_id=<?= $client_id ?>&rack_id=<?= intval($asset_rack['rack_id']) ?>"><?= escapeHtml($asset_rack['rack_name']) ?></a> <span class="text-body-secondary">&middot; <?= rackPositionLabel($asset_rack['unit_start_number'], $asset_rack['unit_end_number']) ?>, <?= $asset_rack['unit_face'] === 'rear' ? 'rear' : 'front' ?></span></span>
</div>
<?php } ?>
</div>
<?php } ?>
</div>
<?php } ?>
<?php if ($can_edit || $asset_notes) { ?>
<textarea class="form-control form-control-sm mt-3 itflow-asset-quicknote" rows="2" placeholder="Quick notes - saves when you click away" aria-label="Quick notes"
data-asset-id="<?= $asset_id ?>" data-csrf="<?= escapeHtml($_SESSION['csrf_token'] ?? '') ?>" <?= $can_edit ? '' : 'readonly' ?>><?= $asset_notes ?></textarea>
<?php } ?>
<?php if ($quick_tabs) { ?>
<!-- Everything linked to it -->
<div class="itflow-asset-related itflow-asset-quick-related mt-3" data-asset-quick="<?= $asset_id ?>">
<ul class="nav nav-pills itflow-asset-pills mb-2" role="tablist">
<?php foreach ($quick_tabs as $tab_key => $tab) { ?>
<li class="nav-item" role="presentation">
<button class="nav-link text-nowrap <?= $tab_key === $first_tab ? 'active' : '' ?>" data-bs-toggle="pill" data-bs-target="#asset-quick-<?= $asset_id ?>-<?= $tab_key ?>" data-asset-quick-tab="<?= $tab_key ?>" type="button" role="tab">
<?= $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 class="tab-content">
<?php if ($interface_count) { ?>
<div class="tab-pane fade <?= $first_tab === 'interfaces' ? 'show active' : '' ?>" id="asset-quick-<?= $asset_id ?>-interfaces" role="tabpanel">
<div class="table-responsive">
<table class="table table-sm table-hover align-middle mb-0">
<thead class="text-nowrap"><tr><th>Port</th><th>IP</th><th>MAC</th><th>Connected to</th></tr></thead>
<tbody>
<?php while ($row = mysqli_fetch_assoc($sql_related_interfaces)) {
$interface_ip = escapeHtml($row['interface_ip']);
$interface_mac = escapeHtml($row['interface_mac']);
$connected_asset_id = intval($row['connected_asset_id']); ?>
<tr>
<td class="text-nowrap">
<?= escapeHtml($row['interface_name']) ?>
<?php if (intval($row['interface_primary'])) { ?><span class="badge rounded-pill text-bg-primary fw-normal ms-1">Primary</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 } ?></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_id) { ?>
<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-<?= getAssetIcon($row['connected_asset_type']) ?> me-1"></i><?= escapeHtml($row['connected_asset_name']) ?></a>
<span class="text-body-secondary">&middot; <?= escapeHtml($row['connected_interface_name']) ?></span>
<?php } else { ?>
<span class="text-body-secondary">-</span>
<?php } ?>
</td>
</tr>
<?php } ?>
</tbody>
</table>
</div>
</div>
<?php } ?>
<?php if ($credential_count) { ?>
<div class="tab-pane fade <?= $first_tab === 'credentials' ? 'show active' : '' ?>" id="asset-quick-<?= $asset_id ?>-credentials" role="tabpanel">
<?= credentialListHtml($related_credentials) ?>
</div>
<?php } ?>
<?php if ($ticket_count) { ?>
<div class="tab-pane fade <?= $first_tab === 'tickets' ? 'show active' : '' ?>" id="asset-quick-<?= $asset_id ?>-tickets" role="tabpanel">
<?= ticketTableHtml($related_tickets, ['compact' => true]) ?>
<?php if ($ticket_count > count($related_tickets)) { ?>
<div class="small mt-2"><a href="asset.php?client_id=<?= $client_id ?>&asset_id=<?= $asset_id ?>#tickets">All <?= $ticket_count ?> tickets on the asset page<i class="fas fa-arrow-right ms-1"></i></a></div>
<?php } ?>
</div>
<?php } ?>
<?php if ($recurring_ticket_count) { ?>
<div class="tab-pane fade <?= $first_tab === 'recurring' ? 'show active' : '' ?>" id="asset-quick-<?= $asset_id ?>-recurring" role="tabpanel">
<ul class="list-group list-group-flush">
<?php while ($row = mysqli_fetch_assoc($sql_related_recurring_tickets)) { ?>
<li class="list-group-item px-0 d-flex justify-content-between gap-3">
<span><?= escapeHtml($row['recurring_ticket_subject']) ?></span>
<span class="small text-body-secondary text-nowrap"><?= escapeHtml($row['recurring_ticket_frequency']) ?> <?= recurringTicketNextRunPill($row) ?></span>
</li>
<?php } ?>
</ul>
</div>
<?php } ?>
<?php if ($software_count) { ?>
<div class="tab-pane fade <?= $first_tab === 'licenses' ? 'show active' : '' ?>" id="asset-quick-<?= $asset_id ?>-licenses" role="tabpanel">
<!-- Each opens its quick look - seats, renewal, the key to install -->
<?= softwareHeldList($held_software) ?>
</div>
<?php } ?>
<?php if ($service_count) { ?>
<div class="tab-pane fade <?= $first_tab === 'services' ? 'show active' : '' ?>" id="asset-quick-<?= $asset_id ?>-services" role="tabpanel">
<!-- What goes down with it - most important first -->
<?= serviceRowsHtml($asset_services) ?>
</div>
<?php } ?>
<?php if ($document_count) { ?>
<div class="tab-pane fade <?= $first_tab === 'documents' ? 'show active' : '' ?>" id="asset-quick-<?= $asset_id ?>-documents" role="tabpanel">
<!-- The same rows as a document's Linked card - tile, name, when it changed -->
<ul class="list-unstyled mb-0 itflow-links">
<?php foreach ($linked_documents as $row) { ?>
<li class="itflow-link-row"><?= linkRowHtml('document', $row, $client_id) ?></li>
<?php } ?>
</ul>
</div>
<?php } ?>
<?php if ($file_count) { ?>
<div class="tab-pane fade <?= $first_tab === 'files' ? 'show active' : '' ?>" id="asset-quick-<?= $asset_id ?>-files" role="tabpanel">
<!-- Each opens its quick look - preview, details, what else it is linked to -->
<ul class="list-unstyled mb-0 itflow-links">
<?php foreach ($linked_files as $row) { ?>
<li class="itflow-link-row"><?= linkRowHtml('file', $row, $client_id) ?></li>
<?php } ?>
</ul>
</div>
<?php } ?>
<?php if ($note_count) { ?>
<div class="tab-pane fade <?= $first_tab === 'notes' ? 'show active' : '' ?>" id="asset-quick-<?= $asset_id ?>-notes" role="tabpanel">
<ul class="list-group list-group-flush">
<?php while ($row = mysqli_fetch_assoc($sql_related_notes)) {
$note_icon = $note_type_icons[$row['asset_note_type']] ?? '' ?: 'fa-sticky-note'; ?>
<li class="list-group-item px-0 d-flex gap-3">
<i class="fas fa-fw <?= $note_icon ?> text-secondary mt-1" title="<?= escapeHtml($row['asset_note_type']) ?>"></i>
<div class="min-w-0">
<div><?= nl2br(escapeHtml($row['asset_note'])) ?></div>
<div class="small text-body-secondary"><?= escapeHtml($row['user_name']) ?> &middot; <span title="<?= escapeHtml($row['asset_note_created_at']) ?>"><?= timeAgo($row['asset_note_created_at']) ?></span></div>
</div>
</li>
<?php } ?>
</ul>
</div>
<?php } ?>
</div>
</div>
<?php } ?>
</div>
<div class="modal-footer">
<a href="asset.php?client_id=<?= $client_id ?>&asset_id=<?= $asset_id ?>" class="btn btn-primary text-bold"><i class="fas fa-arrow-right me-2"></i>Open asset</a>
<?php if ($can_edit) { ?>
<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>
<?php } ?>
<button type="button" class="btn btn-light" data-bs-dismiss="modal"><i class="fa fa-times me-2"></i>Close</button>
</div>
<script>
(function () {
// Quick notes save when you click away. Scoped to this modal's own
// textarea - an id here would collide with the asset page's own
// Quick notes, which is still on the page under the modal
document.querySelectorAll('.itflow-asset-quicknote:not([data-wired])').forEach(function (notes) {
notes.dataset.wired = '1';
var saved = notes.value;
notes.addEventListener('blur', function () {
if (notes.readOnly || notes.value === saved) {
return;
}
saved = notes.value;
itflowPost('ajax.php', {
asset_set_notes: 'TRUE',
csrf_token: notes.dataset.csrf,
asset_id: notes.dataset.assetId,
notes: notes.value
});
});
});
// Open on the tab used last time, if this asset has one
var key = 'itflow.assetQuickTab';
document.querySelectorAll('[data-asset-quick]:not([data-wired])').forEach(function (box) {
box.dataset.wired = '1';
var remembered = null;
try {
remembered = localStorage.getItem(key);
} catch (e) {}
var tab = remembered && box.querySelector('[data-asset-quick-tab="' + remembered + '"]');
if (tab && !tab.classList.contains('active')) {
bootstrap.Tab.getOrCreateInstance(tab).show();
}
box.addEventListener('shown.bs.tab', function (e) {
try {
localStorage.setItem(key, e.target.dataset.assetQuickTab);
} catch (err) {}
});
});
})();
</script>
<?php
require_once '../../../includes/modal_footer.php';