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

728 lines
41 KiB
PHP

<?php
/*
* Asset form fields - shared by asset_add.php, asset_edit.php and
* asset_copy.php, which set $asset_form_mode ('add' | 'edit' | 'copy'),
* $client_id (0 = pick the client here) and the $asset_* values (escaped,
* empty for a new asset) before including this. Posts the field set
* agent/post/asset_model.php reads.
*
* Tabs like the other add / edit modals: Details, Assignment, Network,
* Purchase, then Login on a new asset or History on an existing one, and
* Notes. Every field is always in the form, whatever the asset type: fields
* that came and went with the type used to drop out of the POST entirely,
* and the handler then saved them as empty - editing an access point wiped
* its firmware version. So a field its type doesn't have (ASSET_TYPES in
* functions/asset_type.php) is only hidden, and one with a value is shown
* all the same; a tab left with nothing in it goes too. Details - the type's
* own, from ASSET_SPECS - are under Hardware, as spec[key]; the caller sets
* $asset_specs (raw) for an asset that has them.
*
* Should not be accessed directly.
*/
isset($mysqli) || die("Direct file access is not allowed");
// Status - from Admin > Categories, keeping one the asset has that is archived or isn't a status any more
$status_colors = [];
foreach (categoryPicker('asset_status', [html_entity_decode($asset_status)]) as $status_name => $status_row) {
$status_colors[$status_name] = assetStatusColor($status_name, $status_row['category_color'] ?? '');
}
// The client's own lists - only once the client is known
$location_options = $contact_options = $network_options = $vendor_options = [];
if ($client_id) {
$sql = mysqli_query($mysqli, "SELECT location_archived_at, location_id, location_name FROM locations
WHERE location_client_id = $client_id AND (location_archived_at IS NULL OR location_id = $asset_location_id)
ORDER BY location_name ASC");
while ($row = mysqli_fetch_assoc($sql)) {
$location_options[] = $row;
}
$sql = mysqli_query($mysqli, "SELECT contact_archived_at, contact_id, contact_name, contact_client_id, contact_photo, contact_title FROM contacts
WHERE contact_client_id = $client_id AND (contact_archived_at IS NULL OR contact_id = $asset_contact_id)
ORDER BY contact_name ASC");
while ($row = mysqli_fetch_assoc($sql)) {
$contact_options[] = $row;
}
$sql = mysqli_query($mysqli, "SELECT network, network_archived_at, network_id, network_name FROM networks
WHERE network_client_id = $client_id AND (network_archived_at IS NULL OR network_id = $asset_network_id)
ORDER BY network_name ASC");
while ($row = mysqli_fetch_assoc($sql)) {
$network_options[] = $row;
}
$sql = mysqli_query($mysqli, "SELECT vendor_archived_at, vendor_id, vendor_name, vendor_description FROM vendors
WHERE vendor_client_id = $client_id AND (vendor_archived_at IS NULL OR vendor_id = $asset_vendor_id)
ORDER BY vendor_favorite DESC, vendor_name ASC");
while ($row = mysqli_fetch_assoc($sql)) {
$vendor_options[] = $row;
}
} else {
$client_options = [];
$sql = mysqli_query($mysqli, "SELECT client_id, client_name, client_abbreviation FROM clients WHERE client_archived_at IS NULL " . clientScopeSql('clients.client_id') . " ORDER BY client_name ASC");
while ($row = mysqli_fetch_assoc($sql)) {
$client_options[] = $row;
}
}
// OS suggestions - what's already in use, most common first
$os_suggestions = [];
$sql = mysqli_query($mysqli, "SELECT asset_os FROM assets WHERE asset_archived_at IS NULL AND asset_os IS NOT NULL AND asset_os != '' GROUP BY asset_os ORDER BY COUNT(*) DESC LIMIT 200");
while ($row = mysqli_fetch_assoc($sql)) {
$os_suggestions[] = ['label' => $row['asset_os'], 'value' => $row['asset_os']];
}
$asset_ip_is_dhcp = $asset_ip === 'DHCP';
// The type's details (functions/asset_type.php) - every one is in the form, its type's shown
$asset_specs = $asset_specs ?? [];
$asset_type_raw = html_entity_decode($asset_type, ENT_QUOTES, 'UTF-8');
$asset_type_known = assetTypeName($asset_type_raw);
// What each type shows, for the script - fields, what the OS field is called, details
$asset_form_types = [];
foreach (ASSET_TYPES as $asset_form_type => $asset_form_info) {
$asset_form_types[$asset_form_type] = ['fields' => $asset_form_info['fields'], 'os' => assetTypeOsLabel($asset_form_type), 'specs' => $asset_form_info['specs'], 'example' => $asset_form_info['example']];
}
// Ports it has now, for the counts' hints - copper and SFP cages
$asset_port_counts = ['copper' => 0, 'sfp' => 0];
if ($asset_form_mode === 'edit' && $asset_id) {
$sql = mysqli_query($mysqli, "SELECT interface_name, interface_type, interface_primary FROM asset_interfaces WHERE interface_asset_id = " . intval($asset_id) . " AND interface_archived_at IS NULL");
while ($row = mysqli_fetch_assoc($sql)) {
$port_kind = interfacePortKind($row['interface_type'], $row['interface_name']);
if ($port_kind !== '') {
$asset_port_counts[$port_kind]++;
if ($row['interface_primary']) {
$asset_port_counts['primary'] = $port_kind;
}
}
}
}
$asset_host_options = $client_id ? assetSpecAssetOptions('vm_host', $client_id, $asset_id ?? 0, intval($asset_specs['vm_host'] ?? 0)) : [];
// Leading icons that follow what is picked or typed: the status in its
// colour, the OS brand and the kind of link - the same as the assets list
// shows them (functions/asset.php). Type needs none, its picker already shows
// the type's icon. These are the starting ones; the script at the end keeps
// them in step.
$asset_form_icons = [
'status' => escapeHtml($status_colors[html_entity_decode($asset_status)] ?? 'var(--bs-secondary-color)'),
'os' => assetOsIcon(html_entity_decode($asset_os)),
'uri' => $asset_uri !== '' ? assetLinkIcon(html_entity_decode($asset_uri)) : 'fas fa-link',
'uri_2' => $asset_uri_2 !== '' ? assetLinkIcon(html_entity_decode($asset_uri_2)) : 'fas fa-link',
];
?>
<!-- Tabs, like the other add / edit modals - Login on a new asset, History on an existing one -->
<ul class="nav nav-pills nav-fill flex-nowrap overflow-auto mb-3 itflow-asset-form-tabs" role="tablist">
<li class="nav-item">
<a class="nav-link active" data-bs-toggle="pill" href="#assetFormDetails"><i class="fas fa-fw fa-desktop me-2 d-none d-lg-inline"></i>Details</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="pill" href="#assetFormAssignment"><i class="fas fa-fw fa-map-marker-alt me-2 d-none d-lg-inline"></i>Assignment</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="pill" href="#assetFormNetwork"><i class="fas fa-fw fa-network-wired me-2 d-none d-lg-inline"></i>Network</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="pill" href="#assetFormPurchase"><i class="fas fa-fw fa-receipt me-2 d-none d-lg-inline"></i>Purchase</a>
</li>
<?php if ($asset_form_mode !== 'edit') { ?>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="pill" href="#assetFormLogin"><i class="fas fa-fw fa-key me-2 d-none d-lg-inline"></i>Login</a>
</li>
<?php } ?>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="pill" href="#assetFormNotes"><i class="fas fa-fw fa-sticky-note me-2 d-none d-lg-inline"></i>Notes</a>
</li>
<?php if ($asset_form_mode === 'edit') { ?>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="pill" href="#assetFormHistory"><i class="fas fa-fw fa-history me-2 d-none d-lg-inline"></i>History</a>
</li>
<?php } ?>
</ul>
<div class="tab-content">
<!-- What it is -->
<div class="tab-pane fade show active" id="assetFormDetails">
<div class="row g-3">
<?php if (!$client_id) { ?>
<div class="col-12">
<label class="form-label">Client <strong class="text-danger">*</strong></label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-user"></i></span>
<select class="form-select select2" name="client_id" required>
<option value="">- Select Client -</option>
<?php foreach ($client_options as $client_option) { ?>
<option value="<?= intval($client_option['client_id']) ?>"<?= selectOptionClient($client_option['client_id'], $client_option['client_name'], $client_option['client_abbreviation'] ?? '') ?>><?= escapeHtml($client_option['client_name']) ?></option>
<?php } ?>
</select>
</div>
<div class="form-text">Pick the asset's location, contact, network and vendor once it's saved.</div>
</div>
<?php } ?>
<div class="col-md-4">
<label class="form-label">Type <strong class="text-danger">*</strong></label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-layer-group fa-fw"></i></span>
<select class="form-select select2" name="type" required>
<option value="">- Select Type -</option>
<?php foreach ($asset_types_array as $asset_type_select => $asset_icon_select) { ?>
<option <?php if ($asset_type_select === $asset_type_raw) { echo "selected"; } ?><?= selectOptionData(['icon' => "fas $asset_icon_select"]) ?>><?= $asset_type_select ?></option>
<?php } ?>
<?php if ($asset_type_raw !== '' && $asset_type_known === '') { ?>
<option selected<?= selectOptionData(['icon' => 'fas fa-tag']) ?>><?= escapeHtml($asset_type_raw) ?></option>
<?php } ?>
</select>
</div>
</div>
<div class="col-md-4">
<label class="form-label">Name <strong class="text-danger">*</strong></label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-id-badge fa-fw"></i></span>
<input type="text" class="form-control" name="name" placeholder="SRV-DC01" maxlength="200" value="<?= $asset_name ?>" required <?= $asset_form_mode === 'edit' ? '' : 'autofocus' ?>>
<div class="input-group-text">
<label class="star-toggle mb-0" title="Favorite - pinned to the client overview">
<input class="form-check-input" type="checkbox" name="favorite" value="1" <?= $asset_favorite ? 'checked' : '' ?>><i class="far fa-star"></i>
</label>
</div>
</div>
</div>
<div class="col-md-4">
<label class="form-label">Status</label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-circle fa-fw" data-asset-form-icon="status" style="color: <?= $asset_form_icons['status'] ?>"></i></span>
<select class="form-select select2" name="status">
<option value="">- Status -</option>
<?= categoryOptionsHtml('asset_status', html_entity_decode($asset_status)) ?>
</select>
<?= categoryAddButtonHtml('asset_status', 'btn btn-secondary') ?>
</div>
</div>
<div class="col-md-7">
<label class="form-label">Description</label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-align-left"></i></span>
<input type="text" class="form-control" name="description" placeholder="Domain controller for HQ" maxlength="255" value="<?= $asset_description ?>">
</div>
</div>
<div class="col-md-5">
<label class="form-label">Tags</label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-tags"></i></span>
<select class="form-select select2" name="tags[]" data-placeholder="Add some tags" multiple<?= !empty($session_is_admin) ? ' data-tags="true"' : '' ?>>
<?= tagOptionsHtml('asset', $asset_tag_id_array) ?>
</select>
</div>
</div>
</div>
<div class="itflow-form-section mt-4">Hardware</div>
<div class="row g-3">
<div class="col-md-3">
<label class="form-label">Make</label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-industry fa-fw"></i></span>
<input type="text" class="form-control" name="make" placeholder="Dell" maxlength="200" value="<?= $asset_make ?>">
</div>
</div>
<div class="col-md-4">
<label class="form-label">Model</label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-box fa-fw"></i></span>
<input type="text" class="form-control" name="model" placeholder="PowerEdge R650" maxlength="200" value="<?= $asset_model ?>">
</div>
</div>
<div class="col-md-5" data-asset-field="serial">
<label class="form-label">Serial number</label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-barcode"></i></span>
<input type="text" class="form-control font-monospace" name="serial" placeholder="ABC1234XYZ" maxlength="200" value="<?= $asset_serial ?>">
</div>
</div>
<div class="col-12" data-asset-field="os">
<label class="form-label" for="assetFormOs" data-asset-os-label><?= $asset_type_raw !== '' ? assetTypeOsLabel($asset_type_raw) : 'Operating system / firmware' ?></label>
<div class="input-group">
<span class="input-group-text"><i class="<?= $asset_form_icons['os'] ?> fa-fw" data-asset-form-icon="os"></i></span>
<input type="text" class="form-control" name="os" id="assetFormOs" placeholder="Windows 11 Pro 24H2, Ubuntu 24.04, UniFi firmware 7.0" maxlength="200" value="<?= $asset_os ?>">
</div>
</div>
</div>
<?php // Its type's details - each type's are shown by the script below, the rest hidden ?>
<div class="itflow-form-section mt-4" data-asset-specs-heading>Details</div>
<div class="row g-3" data-asset-specs>
<?php foreach (ASSET_SPECS as $spec_key => $spec) {
$spec_value = (string) ($asset_specs[$spec_key] ?? '');
$spec_id = 'assetFormSpec_' . $spec_key;
$spec_shown = in_array($spec_key, assetTypeInfo($asset_type_raw)['specs'], true); ?>
<div class="<?= in_array($spec['input'], ['text', 'asset'], true) && !isset($spec['suggest']) ? 'col-md-6' : 'col-md-4 col-sm-6' ?>" data-asset-spec="<?= $spec_key ?>"<?= $spec_shown ? '' : ' hidden' ?>>
<label class="form-label" for="<?= $spec_id ?>"><?= escapeHtml($spec['label']) ?></label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-fw <?= $spec['icon'] ?>"></i></span>
<?php if ($spec['input'] === 'select') { ?>
<select class="form-select" name="spec[<?= $spec_key ?>]" id="<?= $spec_id ?>">
<option value="">-</option>
<?php foreach ($spec['options'] as $spec_option) { ?>
<option <?= strcasecmp($spec_option, $spec_value) === 0 ? 'selected' : '' ?>><?= escapeHtml($spec_option) ?></option>
<?php } ?>
<?php if ($spec_value !== '' && !in_array(strtolower($spec_value), array_map('strtolower', $spec['options']), true)) { ?>
<option selected><?= escapeHtml($spec_value) ?></option>
<?php } ?>
</select>
<?php } elseif ($spec['input'] === 'asset') { ?>
<?php if ($client_id) { ?>
<select class="form-select select2" name="spec[<?= $spec_key ?>]" id="<?= $spec_id ?>">
<option value="">- None -</option>
<?php foreach ($asset_host_options as $host_id => $host) { ?>
<option value="<?= $host_id ?>" <?= intval($spec_value) === $host_id ? 'selected' : '' ?><?= selectOptionData(['icon' => 'fas ' . assetTypeInfo($host['asset_type'])['icon'], 'description' => $host['asset_type']]) ?>><?= escapeHtml($host['asset_name']) ?></option>
<?php } ?>
</select>
<?php } else { ?>
<input type="text" class="form-control" id="<?= $spec_id ?>" value="" placeholder="Pick it once the asset is saved" disabled>
<?php } ?>
<?php } else { ?>
<input type="<?= $spec['input'] === 'date' ? 'date' : ($spec['input'] === 'number' ? 'number' : 'text') ?>"
class="form-control<?= !empty($spec['mono']) ? ' font-monospace' : '' ?>" name="spec[<?= $spec_key ?>]" id="<?= $spec_id ?>"
value="<?= escapeHtml($spec_value) ?>"
<?php if ($spec['input'] === 'number') { ?>min="<?= $spec['min'] ?? 0 ?>" max="<?= $spec['max'] ?? '' ?>" step="<?= $spec['step'] ?? 'any' ?>" inputmode="decimal"<?php } ?>
<?php if ($spec['input'] === 'date') { ?>max="2999-12-31"<?php } ?>
<?php if ($spec['input'] === 'text') { ?>maxlength="255"<?php } ?>
<?= isset($spec['placeholder']) ? 'placeholder="' . escapeHtml($spec['placeholder']) . '"' : '' ?>
<?= isset($spec['suggest']) ? 'list="' . $spec_id . 'List"' : '' ?>>
<?php if (isset($spec['unit'])) { ?>
<span class="input-group-text"><?= escapeHtml($spec['unit']) ?></span>
<?php } ?>
<?php } ?>
</div>
<?php if (isset($spec['suggest'])) { ?>
<datalist id="<?= $spec_id ?>List">
<?php foreach ($spec['suggest'] as $suggestion) { ?>
<option value="<?= escapeHtml($suggestion) ?>"></option>
<?php } ?>
</datalist>
<?php } ?>
<?php if (isset($spec['ports'])) {
$port_kind = $spec['ports']; ?>
<div class="form-text" data-asset-ports-hint>
<?php if ($asset_form_mode === 'edit') { ?>
Has <?= $asset_port_counts[$port_kind] ?> now<?= ($asset_port_counts['primary'] ?? '') === $port_kind ? ', its primary included' : '' ?>. A new count adds the ones missing to its interfaces - none are taken away.
<?php } else { ?>
Added to its interfaces when it's saved.
<?php } ?>
</div>
<?php } elseif (!empty($spec['flag'])) { ?>
<div class="form-text">Flagged on its page as the date nears, like a warranty.</div>
<?php } ?>
</div>
<?php } ?>
</div>
<?php if ($asset_form_mode === 'edit') { ?>
<p class="text-body-secondary small text-end mt-3 mb-0">Asset ID <span class="font-monospace"><?= $asset_id ?></span></p>
<?php } ?>
</div>
<!-- Where it is and who has it -->
<div class="tab-pane fade" id="assetFormAssignment">
<?php if ($client_id) { ?>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">Location</label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-map-marker-alt fa-fw"></i></span>
<select class="form-select select2" name="location">
<option value="">- Select Location -</option>
<?php foreach ($location_options as $location_option) {
$location_option_id = intval($location_option['location_id']); ?>
<option value="<?= $location_option_id ?>" <?php if ($asset_location_id === $location_option_id) { echo "selected"; } ?>><?= escapeHtml($location_option['location_name']) ?><?= $location_option['location_archived_at'] ? ' (archived)' : '' ?></option>
<?php } ?>
</select>
</div>
</div>
<div class="col-md-6">
<label class="form-label">Where in the building</label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-door-open fa-fw"></i></span>
<input type="text" class="form-control" name="physical_location" placeholder="Floor 2, comms closet B" maxlength="200" value="<?= $asset_physical_location ?>">
</div>
</div>
<div class="col-12" data-asset-field="contact">
<label class="form-label">Assigned to</label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-user fa-fw"></i></span>
<select class="form-select select2" name="contact">
<option value="">- Nobody -</option>
<?php foreach ($contact_options as $contact_option) {
$contact_option_id = intval($contact_option['contact_id']); ?>
<option value="<?= $contact_option_id ?>" <?php if ($asset_contact_id === $contact_option_id) { echo "selected"; } ?><?= selectOptionContact($contact_option['contact_client_id'] ?? 0, $contact_option['contact_name'], $contact_option['contact_photo'] ?? '', $contact_option['contact_title'] ?? '', $contact_option['contact_id'] ?? 0) ?>><?= escapeHtml($contact_option['contact_name']) ?><?= $contact_option['contact_archived_at'] ? ' (archived)' : '' ?></option>
<?php } ?>
</select>
</div>
</div>
</div>
<?php } else { ?>
<div class="row g-3">
<div class="col-12">
<label class="form-label">Where in the building</label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-door-open fa-fw"></i></span>
<input type="text" class="form-control" name="physical_location" placeholder="Floor 2, comms closet B" maxlength="200" value="<?= $asset_physical_location ?>">
</div>
<div class="form-text">The location and who it's assigned to come from the client - pick them once it's saved.</div>
</div>
</div>
<?php } ?>
</div>
<!-- How to reach it -->
<div class="tab-pane fade" id="assetFormNetwork">
<p class="text-body-secondary small">The primary interface. Add more ports from the asset's Interfaces tab.</p>
<div class="row g-3">
<?php if ($client_id) { ?>
<div class="col-md-6" data-asset-field="network">
<label class="form-label">Network</label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-network-wired"></i></span>
<select class="form-select select2" name="network">
<option value="">- Select Network -</option>
<?php foreach ($network_options as $network_option) {
$network_option_id = intval($network_option['network_id']); ?>
<option value="<?= $network_option_id ?>" <?php if ($asset_network_id === $network_option_id) { echo "selected"; } ?><?= selectOptionData(['description' => $network_option['network']]) ?>><?= escapeHtml($network_option['network_name']) ?><?= $network_option['network_archived_at'] ? ' (archived)' : '' ?></option>
<?php } ?>
</select>
</div>
</div>
<?php } ?>
<div class="col-md-6" data-asset-field="network">
<label class="form-label">IPv4 address</label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-globe fa-fw"></i></span>
<input type="text" class="form-control font-monospace" name="ip" id="assetFormIp" placeholder="192.168.1.10" maxlength="200" value="<?= $asset_ip_is_dhcp ? '' : $asset_ip ?>" data-inputmask="'alias': 'ip'" data-mask <?= $asset_ip_is_dhcp ? 'disabled' : '' ?>>
<div class="input-group-text">
<div class="form-check mb-0">
<input class="form-check-input" type="checkbox" name="dhcp" value="1" id="assetFormDhcp" <?= $asset_ip_is_dhcp ? 'checked' : '' ?>>
<label class="form-check-label small" for="assetFormDhcp">DHCP</label>
</div>
</div>
</div>
</div>
<div class="col-md-6" data-asset-field="network">
<label class="form-label">MAC address</label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-ethernet"></i></span>
<input type="text" class="form-control font-monospace" name="mac" placeholder="00:1A:2B:3C:4D:5E" maxlength="200" value="<?= $asset_mac ?>" data-inputmask="'alias': 'mac'" data-mask>
</div>
</div>
<div class="col-md-6" data-asset-field="nat">
<label class="form-label">NAT address</label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-exchange-alt"></i></span>
<input type="text" class="form-control font-monospace" name="nat_ip" placeholder="203.0.113.10" maxlength="200" value="<?= $asset_nat_ip ?>" data-inputmask="'alias': 'ip'" data-mask>
</div>
</div>
<div class="col-12" data-asset-field="network">
<label class="form-label">IPv6 address</label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-globe fa-fw"></i></span>
<input type="text" class="form-control font-monospace" name="ipv6" placeholder="2001:db8::1" maxlength="200" value="<?= $asset_ipv6 ?>">
</div>
</div>
</div>
<div data-asset-field="links">
<div class="itflow-form-section mt-4">Links</div>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">Link</label>
<div class="input-group">
<span class="input-group-text"><i class="<?= $asset_form_icons['uri'] ?> fa-fw" data-asset-form-icon="link"></i></span>
<input type="text" class="form-control" name="uri" placeholder="https://, ssh://, rdp://" maxlength="500" value="<?= $asset_uri ?>">
</div>
</div>
<div class="col-md-6">
<label class="form-label">Second link</label>
<div class="input-group">
<span class="input-group-text"><i class="<?= $asset_form_icons['uri_2'] ?> fa-fw" data-asset-form-icon="link"></i></span>
<input type="text" class="form-control" name="uri_2" placeholder="https://, ssh://, rdp://" maxlength="500" value="<?= $asset_uri_2 ?>">
</div>
</div>
<div class="col-12">
<label class="form-label">Client link</label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-share-square fa-fw"></i></span>
<input type="text" class="form-control" name="uri_client" placeholder="https://" maxlength="500" value="<?= $asset_uri_client ?>">
</div>
<div class="form-text">Shown to the client in their portal.</div>
</div>
</div>
</div>
</div>
<!-- Where it came from -->
<div class="tab-pane fade" id="assetFormPurchase">
<div class="row g-3">
<?php if ($client_id) { ?>
<div class="col-md-6" data-asset-field="purchase">
<label class="form-label">Vendor</label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-building"></i></span>
<select class="form-select select2" name="vendor">
<option value="">- Select Vendor -</option>
<?php foreach ($vendor_options as $vendor_option) {
$vendor_option_id = intval($vendor_option['vendor_id']); ?>
<option value="<?= $vendor_option_id ?>" <?php if ($asset_vendor_id === $vendor_option_id) { echo "selected"; } ?><?= selectOptionVendor($vendor_option['vendor_name'], $vendor_option['vendor_description']) ?>><?= escapeHtml($vendor_option['vendor_name']) ?><?= $vendor_option['vendor_archived_at'] ? ' (archived)' : '' ?></option>
<?php } ?>
</select>
</div>
</div>
<?php } ?>
<div class="col-md-6" data-asset-field="purchase">
<label class="form-label">Order / invoice reference</label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-receipt"></i></span>
<input type="text" class="form-control" name="purchase_reference" placeholder="INV-1045, PO-7782" maxlength="200" value="<?= $asset_purchase_reference ?>">
</div>
</div>
<div class="col-md-4" data-asset-field="purchase">
<label class="form-label">Purchased</label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-shopping-cart fa-fw"></i></span>
<input type="date" class="form-control" name="purchase_date" max="2999-12-31" value="<?= $asset_purchase_date ?>">
</div>
</div>
<div class="col-md-4">
<label class="form-label">Installed</label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-tools fa-fw"></i></span>
<input type="date" class="form-control" name="install_date" max="2999-12-31" value="<?= $asset_install_date ?>">
</div>
</div>
<div class="col-md-4" data-asset-field="warranty">
<label class="form-label">Warranty expires</label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-shield-alt fa-fw"></i></span>
<input type="date" class="form-control" name="warranty_expire" max="2999-12-31" value="<?= $asset_warranty_expire ?>">
</div>
</div>
</div>
</div>
<?php if ($asset_form_mode !== 'edit') { ?>
<!-- A login to keep with it -->
<div class="tab-pane fade" id="assetFormLogin">
<p class="text-body-secondary small">Saved as a credential on the asset. Leave it empty to skip.</p>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">Username</label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-user"></i></span>
<input type="text" class="form-control" name="username" placeholder="admin" maxlength="350" autocomplete="off">
</div>
</div>
<div class="col-md-6">
<label class="form-label">Password</label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-lock"></i></span>
<input type="text" class="form-control" name="password" placeholder="Leave empty to skip" maxlength="350" autocomplete="off">
</div>
</div>
</div>
</div>
<?php } ?>
<!-- Notes and a photo -->
<div class="tab-pane fade" id="assetFormNotes">
<div class="row g-3">
<div class="col-12">
<label class="form-label">Notes</label>
<textarea class="form-control" rows="7" placeholder="Configuration details, quirks, anything worth knowing" name="notes"><?= $asset_notes ?></textarea>
</div>
<div class="col-md-6">
<label class="form-label">Photo</label>
<?php if ($asset_photo) { ?>
<div class="mb-2"><img class="img-thumbnail" style="max-height: 7rem" alt="Current photo" src="<?= "../uploads/clients/$client_id/$asset_photo" ?>"></div>
<?php } ?>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-camera fa-fw"></i></span>
<input type="file" class="form-control" name="file" accept="image/jpeg,image/png,image/gif,image/webp">
</div>
<?php if ($asset_photo) { ?>
<div class="form-check mt-2">
<input class="form-check-input" type="checkbox" name="remove_photo" value="1" id="assetFormRemovePhoto">
<label class="form-check-label" for="assetFormRemovePhoto">Remove this photo</label>
</div>
<?php } ?>
</div>
</div>
</div>
<?php if ($asset_form_mode === 'edit') { ?>
<div class="tab-pane fade" id="assetFormHistory">
<?php if (mysqli_num_rows($sql_asset_history)) { ?>
<ul class="list-group list-group-flush">
<?php while ($row = mysqli_fetch_assoc($sql_asset_history)) { ?>
<li class="list-group-item px-0">
<div class="d-flex justify-content-between gap-2">
<span><?= escapeHtml($row['asset_history_description']) ?></span>
<?php if ($row['asset_history_status']) { ?>
<?= assetStatusPill($row['asset_history_status']) ?>
<?php } ?>
</div>
<div class="small text-body-secondary" title="<?= escapeHtml($row['asset_history_created_at']) ?>"><?= timeAgo($row['asset_history_created_at']) ?> &middot; <?= escapeHtml($row['asset_history_created_at']) ?></div>
</li>
<?php } ?>
</ul>
<?php } else { ?>
<p class="text-body-secondary text-center my-4">No history recorded yet.</p>
<?php } ?>
</div>
<?php } ?>
</div>
<script>
itflowReady(function () {
var form = document.getElementById('assetFormOs').closest('form');
// OS / firmware suggestions from what's already in use
var osInput = form.querySelector('#assetFormOs');
itflowAutocomplete(osInput, {
minLength: 1,
source: <?= json_encode($os_suggestions, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) ?>,
onSelect: function (item) {
osInput.value = item.label;
}
});
// A required field on a tab that isn't showing: show that tab and point
// at the field - otherwise the browser just quietly refuses to send
var reported = false;
form.addEventListener('invalid', function (e) {
var pane = e.target.closest('.tab-pane');
var tab = pane && form.querySelector('[data-bs-toggle="pill"][href="#' + pane.id + '"]');
if (!tab || pane.classList.contains('active') || reported) {
return;
}
reported = true;
tab.addEventListener('shown.bs.tab', function () {
reported = false;
e.target.reportValidity();
}, { once: true });
bootstrap.Tab.getOrCreateInstance(tab).show();
}, true);
// Leading icons that follow the field: the status colour, the OS
// brand, the kind of link
var icons = {
status: <?= json_encode($status_colors, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) ?>,
os: <?= json_encode(assetOsIconMap(), JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) ?>,
link: <?= json_encode(assetLinkIconMap(), JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) ?>
};
function setIcon(icon, classes) {
icon.className = classes + ' fa-fw';
}
function follow(field, kind, update) {
var group = field.closest('.input-group');
var icon = group && group.querySelector('[data-asset-form-icon="' + kind + '"]');
if (!icon) {
return;
}
['input', 'change'].forEach(function (type) {
field.addEventListener(type, function () {
update(icon, field.value);
});
});
}
follow(form.querySelector('[name="status"]'), 'status', function (icon, value) {
icon.style.color = icons.status[value] || 'var(--bs-secondary-color)';
});
follow(osInput, 'os', function (icon, value) {
var os = value.toLowerCase();
var match = Object.keys(icons.os).find(function (needle) {
return os.indexOf(needle) !== -1;
});
setIcon(icon, match ? icons.os[match] : 'fas fa-compact-disc');
});
form.querySelectorAll('[name="uri"], [name="uri_2"]').forEach(function (field) {
follow(field, 'link', function (icon, value) {
var scheme = (value.match(/^([a-z][a-z0-9+.-]*):/i) || [])[1];
setIcon(icon, value.trim() === '' ? 'fas fa-link' : (icons.link[(scheme || '').toLowerCase()] || 'fas fa-external-link-alt'));
});
});
// What the type shows: its fields - and any other with something in it,
// so nothing is hidden with a value - its details, and a tab only while
// something on it is showing. Hidden fields are still sent as they are.
var types = <?= json_encode($asset_form_types, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) ?>;
var typeSelect = form.querySelector('[name="type"]');
var osLabel = form.querySelector('[data-asset-os-label]');
function hasValue(box) {
return Array.prototype.some.call(box.querySelectorAll('input, select, textarea'), function (field) {
if (field.type === 'checkbox') {
return field.checked && field.name === 'dhcp';
}
return field.type !== 'hidden' && field.value.trim() !== '';
});
}
function applyType() {
var type = types[typeSelect.value] || types.Other;
form.querySelectorAll('[data-asset-field]').forEach(function (box) {
box.hidden = type.fields.indexOf(box.dataset.assetField) === -1 && !hasValue(box);
});
var anySpec = false;
form.querySelectorAll('[data-asset-spec]').forEach(function (box) {
box.hidden = type.specs.indexOf(box.dataset.assetSpec) === -1;
anySpec = anySpec || !box.hidden;
});
var heading = form.querySelector('[data-asset-specs-heading]');
heading.hidden = !anySpec;
heading.textContent = (types[typeSelect.value] ? typeSelect.value + ' ' : '') + 'details';
if (osLabel) {
osLabel.textContent = typeSelect.value ? type.os : 'Operating system / firmware';
}
// Its details in its own order, and suggestions that fit it
type.specs.forEach(function (key, at) {
var box = form.querySelector('[data-asset-spec="' + key + '"]');
if (box) {
box.style.order = at;
}
});
if (types[typeSelect.value]) {
['name', 'description', 'make', 'model', 'os'].forEach(function (field) {
var input = form.querySelector('[name="' + field + '"]');
if (input && type.example[field] !== undefined) {
input.placeholder = type.example[field];
}
});
}
// A tab with nothing left on it goes - back to Details if it was open
form.querySelectorAll('.itflow-asset-form-tabs [data-bs-toggle="pill"]').forEach(function (tab) {
var pane = form.querySelector(tab.getAttribute('href'));
var fields = pane ? pane.querySelectorAll('[data-asset-field]') : [];
var always = pane && Array.prototype.some.call(pane.querySelectorAll('.form-label'), function (label) {
return !label.closest('[data-asset-field]');
});
if (!fields.length) {
return;
}
var empty = Array.prototype.every.call(fields, function (box) { return box.hidden; });
tab.closest('.nav-item').hidden = empty && !always;
if (empty && !always && tab.classList.contains('active')) {
bootstrap.Tab.getOrCreateInstance(form.querySelector('[href="#assetFormDetails"]')).show();
}
});
}
typeSelect.addEventListener('change', applyType);
applyType();
// DHCP means no fixed address to type
var dhcp = form.querySelector('#assetFormDhcp');
var ip = form.querySelector('#assetFormIp');
dhcp.addEventListener('change', function () {
ip.disabled = dhcp.checked;
if (!dhcp.checked) {
ip.focus();
}
});
});
</script>