mirror of
https://github.com/itflow-org/itflow
synced 2026-11-11 07:01:16 +00:00
Assets: change status, location and assigned contact in place on the assets list, the asset page and the quick look, using the same quick pickers as tickets (location and contact only offer the asset's own client's); Add/Edit/Copy form split into tabs (Details, Assignment, Network, Purchase, Login or History, Notes) that jumps to a required field left empty on another tab; Printers tab on the assets list with New Printer and a Printers export filter; asset quick look rebuilt as a compact asset page: header with type icon and status, IP/MAC/serial with copy, warranty, location, contact and rack on a few lines, connect buttons, counted tabs showing only what has something in them and reopening on the last one used, open tickets first with a link to the rest, Open asset in the footer. Fixes bulk assign contact/location changing assets of a different client, quick look notes saving the underlying page's asset notes over the connected asset's, and a query per license on the quick look. No DB change
This commit is contained in:
@@ -181,8 +181,8 @@ if (isset($_POST['tag_picker_set']) || isset($_POST['tag_picker_create'])) {
|
||||
|
||||
/*
|
||||
* Quick pickers (js/quick_pick.js) - a ticket's status, priority, assignee
|
||||
* and billable, a project's manager and a recurring ticket's assignee,
|
||||
* changed in place instead of through a modal. GET lists the choices; POST makes the change through the same
|
||||
* and billable, a project's manager, a recurring ticket's assignee and an
|
||||
* asset's status, location and contact, changed in place instead of through a modal. GET lists the choices; POST makes the change through the same
|
||||
* functions the post.php handlers use (functions/ticket_fields.php) and sends
|
||||
* back the redrawn values.
|
||||
*/
|
||||
@@ -210,6 +210,14 @@ if (isset($_GET['quick_pick_options'])) {
|
||||
$options = quickPickAgents();
|
||||
} elseif ($field === 'project_manager') {
|
||||
$options = quickPickAgents('No manager');
|
||||
} elseif (in_array($field, ASSET_QUICK_FIELDS, true)) {
|
||||
// Locations and contacts are the asset's client's own - functions/asset.php
|
||||
$options = assetQuickOptions($field, intval($_GET['scope'] ?? 0));
|
||||
if ($options === null) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['error' => 'You cannot see that client.']);
|
||||
exit();
|
||||
}
|
||||
} else {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Unknown field']);
|
||||
@@ -230,6 +238,40 @@ if (isset($_POST['quick_pick_set'])) {
|
||||
$value = (string) ($_POST['value'] ?? '');
|
||||
$ticket_ids = array_values(array_unique(array_filter(array_map('intval', (array) ($_POST['ids'] ?? [])))));
|
||||
|
||||
// An asset's status, location or contact - assets of clients this agent
|
||||
// can reach, with the same permission as Edit Asset (functions/asset.php)
|
||||
if (in_array($field, ASSET_QUICK_FIELDS, true)) {
|
||||
$asset_ids = $ticket_ids;
|
||||
$allowed = lookupUserPermission('module_support') >= 2 && $asset_ids;
|
||||
foreach ($allowed ? $asset_ids : [] as $id) {
|
||||
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT asset_client_id FROM assets WHERE asset_id = $id"));
|
||||
if (!$row || !hasClientAccess($row['asset_client_id'])) {
|
||||
$allowed = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$allowed) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['error' => 'You cannot change that on these assets.']);
|
||||
exit();
|
||||
}
|
||||
|
||||
$message = '';
|
||||
$response = ['records' => []];
|
||||
foreach ($asset_ids as $id) {
|
||||
[$ok, $message] = assetQuickSet($id, $field, $value);
|
||||
if (!$ok) {
|
||||
http_response_code(422);
|
||||
echo json_encode(['error' => $message]);
|
||||
exit();
|
||||
}
|
||||
$response['records'][$id] = assetQuickRecord($id, $field);
|
||||
}
|
||||
$response['message'] = count($asset_ids) === 1 ? $message : 'Updated ' . count($asset_ids) . ' assets';
|
||||
echo json_encode($response);
|
||||
exit();
|
||||
}
|
||||
|
||||
// A project's manager or a recurring ticket's assignee - open projects
|
||||
// and recurring tickets of clients this agent can reach, like their edit modals
|
||||
if ($field === 'project_manager' || $field === 'recurring_assignee') {
|
||||
|
||||
@@ -29,13 +29,13 @@ enforceUserPermission('module_support');
|
||||
|
||||
$asset_id = intval($_GET['asset_id'] ?? 0);
|
||||
|
||||
$sql = mysqli_query($mysqli, "SELECT asset_archived_at, asset_contact_id, asset_created_at, asset_description, asset_favorite, asset_id,
|
||||
$sql = mysqli_query($mysqli, "SELECT asset_archived_at, asset_client_id, asset_contact_id, asset_created_at, asset_description, asset_favorite, asset_id,
|
||||
asset_install_date, asset_location_id, asset_make, asset_model, asset_name, asset_notes,
|
||||
asset_os, asset_photo, asset_physical_location, asset_purchase_date,
|
||||
asset_purchase_reference, asset_serial, asset_status, asset_type, asset_uri, asset_uri_2,
|
||||
asset_uri_client, asset_vendor_id, asset_warranty_expire, client_id, client_name,
|
||||
contact_archived_at, contact_email, contact_extension, contact_mobile,
|
||||
contact_mobile_country_code, contact_name, contact_phone, contact_phone_country_code,
|
||||
contact_mobile_country_code, contact_name, contact_phone, contact_phone_country_code, contact_photo,
|
||||
interface_ip, interface_ipv6, interface_mac, interface_nat_ip, interface_network_id,
|
||||
location_archived_at, location_name, network_name, vendor_name FROM assets
|
||||
LEFT JOIN clients ON client_id = asset_client_id
|
||||
@@ -57,6 +57,7 @@ if (mysqli_num_rows($sql) == 0) {
|
||||
} else {
|
||||
|
||||
$row = mysqli_fetch_assoc($sql);
|
||||
$asset_row = $row; // raw, for the quick pickers - $row gets reused below
|
||||
$client_id = intval($row['client_id']);
|
||||
$client_name = escapeHtml($row['client_name']);
|
||||
$asset_id = intval($row['asset_id']);
|
||||
@@ -95,10 +96,6 @@ if (mysqli_num_rows($sql) == 0) {
|
||||
$vendor_name = escapeHtml($row['vendor_name']);
|
||||
|
||||
$contact_name = escapeHtml($row['contact_name']);
|
||||
$contact_email = escapeHtml($row['contact_email']);
|
||||
$contact_phone = escapeHtml(formatPhoneNumber($row['contact_phone'], $row['contact_phone_country_code']));
|
||||
$contact_extension = escapeHtml($row['contact_extension']);
|
||||
$contact_mobile = escapeHtml(formatPhoneNumber($row['contact_mobile'], $row['contact_mobile_country_code']));
|
||||
$contact_archived = !empty($row['contact_archived_at']);
|
||||
$location_name = escapeHtml($row['location_name']);
|
||||
$location_archived = !empty($row['location_archived_at']);
|
||||
@@ -312,7 +309,11 @@ if (mysqli_num_rows($sql) == 0) {
|
||||
<span><?= $asset_name ?></span>
|
||||
<?php if ($asset_favorite) { ?><i class="fas fa-star text-warning fs-6" title="Favorite"></i><?php } ?>
|
||||
<?php if ($asset_archived_at) { ?><span class="badge rounded-pill text-bg-secondary fw-normal fs-6">Archived</span><?php } ?>
|
||||
<span class="fs-6"><?= $asset_status_pill ?></span>
|
||||
<?php if ($can_edit) { ?>
|
||||
<span class="fs-6"><?= quickPick('asset_status', $asset_id, (string) $asset_row['asset_status'], assetQuickHtml('asset_status', $asset_row), 'list', 0, $client_id) ?></span>
|
||||
<?php } else { ?>
|
||||
<span class="fs-6"><?= $asset_status_pill ?></span>
|
||||
<?php } ?>
|
||||
</h1>
|
||||
<div class="text-body-secondary small d-flex flex-wrap column-gap-3 row-gap-1">
|
||||
<span><i class="fas fa-fw fa-<?= $device_icon ?> me-1"></i><?= $asset_type ?></span>
|
||||
@@ -511,7 +512,9 @@ if (mysqli_num_rows($sql) == 0) {
|
||||
<dl class="itflow-asset-dl mb-0">
|
||||
<dt>Location</dt>
|
||||
<dd>
|
||||
<?php if ($location_name) { ?>
|
||||
<?php if ($can_edit) { ?>
|
||||
<?= quickPick('asset_location', $asset_id, (string) intval($asset_row['asset_location_id']), assetQuickHtml('asset_location', $asset_row, 'detail'), 'detail', 0, $client_id) ?>
|
||||
<?php } elseif ($location_name) { ?>
|
||||
<span class="<?= $location_archived ? 'text-danger text-decoration-line-through' : '' ?>" <?= $location_archived ? 'title="Archived location"' : '' ?>><?= $location_name ?></span>
|
||||
<?php } else { ?>
|
||||
<span class="text-body-secondary">-</span>
|
||||
@@ -526,20 +529,15 @@ if (mysqli_num_rows($sql) == 0) {
|
||||
<?php } ?>
|
||||
<dt>Assigned</dt>
|
||||
<dd>
|
||||
<?php if ($contact_name) { ?>
|
||||
<?php if ($can_edit) { ?>
|
||||
<?= quickPick('asset_contact', $asset_id, (string) $asset_contact_id, assetQuickHtml('asset_contact', $asset_row, 'detail'), 'detail', 0, $client_id) ?>
|
||||
<?php } elseif ($contact_name) { ?>
|
||||
<a class="<?= $contact_archived ? 'text-decoration-line-through text-body-secondary' : '' ?>" href="contact.php?client_id=<?= $client_id ?>&contact_id=<?= $asset_contact_id ?>" <?= $contact_archived ? 'title="Archived contact"' : '' ?>><?= $contact_name ?></a>
|
||||
<?php if ($contact_email) { ?>
|
||||
<div class="small text-truncate itflow-copy-always"><a href="mailto:<?= $contact_email ?>"><?= $contact_email ?></a><button type="button" class="itflow-copy clipboardjs" title="Copy email" data-clipboard-text="<?= $contact_email ?>"><i class="far fa-copy"></i></button></div>
|
||||
<?php } ?>
|
||||
<?php if ($contact_phone) { ?>
|
||||
<div class="small"><i class="fas fa-fw fa-phone text-secondary me-1"></i><?= $contact_phone ?><?= $contact_extension ? " x$contact_extension" : '' ?></div>
|
||||
<?php } ?>
|
||||
<?php if ($contact_mobile) { ?>
|
||||
<div class="small"><i class="fas fa-fw fa-mobile-alt text-secondary me-1"></i><?= $contact_mobile ?></div>
|
||||
<?php } ?>
|
||||
<?php } else { ?>
|
||||
<span class="text-body-secondary">Nobody</span>
|
||||
<?php } ?>
|
||||
<!-- How to reach them - redrawn when the contact changes -->
|
||||
<div class="itflow-copy-always" data-quick-related="asset-contact-<?= $asset_id ?>"><?= assetContactDetails($asset_row) ?></div>
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
@@ -51,7 +51,8 @@ $asset_type_tabs = [
|
||||
'server' => ['label' => 'Servers', 'icon' => 'fa-server', 'query' => "asset_type = 'Server'"],
|
||||
'virtual' => ['label' => 'Virtual', 'icon' => 'fa-cloud', 'query' => "asset_type = 'Virtual Machine'"],
|
||||
'network' => ['label' => 'Network', 'icon' => 'fa-network-wired', 'query' => "asset_type IN ('Firewall/Router', 'Switch', 'Access Point')"],
|
||||
'other' => ['label' => 'Other', 'icon' => 'fa-tag', 'query' => "asset_type NOT IN ('Desktop', 'Laptop', 'Server', 'Virtual Machine', 'Firewall/Router', 'Switch', 'Access Point')"],
|
||||
'printer' => ['label' => 'Printers', 'icon' => 'fa-print', 'query' => "asset_type = 'Printer'"],
|
||||
'other' => ['label' => 'Other', 'icon' => 'fa-tag', 'query' => "asset_type NOT IN ('Desktop', 'Laptop', 'Server', 'Virtual Machine', 'Firewall/Router', 'Switch', 'Access Point', 'Printer')"],
|
||||
];
|
||||
$type_filter = (isset($_GET['type']) && isset($asset_type_tabs[$_GET['type']])) ? $_GET['type'] : '';
|
||||
$_GET['type'] = $type_filter;
|
||||
@@ -113,8 +114,8 @@ $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);
|
||||
|
||||
// Assigned To only means something for workstations and the like
|
||||
$show_contact = !in_array($type_filter, ['network', 'server', 'other'], true);
|
||||
// Assigned To only means something for workstations and the like - printers are shared
|
||||
$show_contact = !in_array($type_filter, ['network', 'server', 'printer', 'other'], true);
|
||||
$show_model = $type_filter !== 'virtual';
|
||||
|
||||
// Only these sort - filter_header.php will hand through any lowercase word, and
|
||||
@@ -138,7 +139,8 @@ $row = mysqli_fetch_assoc(mysqli_query($mysqli, "
|
||||
SUM(asset_type = 'Server') AS server_count,
|
||||
SUM(asset_type = 'Virtual Machine') AS virtual_count,
|
||||
SUM(asset_type IN ('Firewall/Router', 'Switch', 'Access Point')) AS network_count,
|
||||
SUM(asset_type NOT IN ('Laptop', 'Desktop', 'Server', 'Virtual Machine', 'Firewall/Router', 'Switch', 'Access Point')) AS other_count
|
||||
SUM(asset_type = 'Printer') AS printer_count,
|
||||
SUM(asset_type NOT IN ('Laptop', 'Desktop', 'Server', 'Virtual Machine', 'Firewall/Router', 'Switch', 'Access Point', 'Printer')) AS other_count
|
||||
FROM assets
|
||||
LEFT JOIN clients ON client_id = asset_client_id
|
||||
WHERE $archive_query
|
||||
@@ -152,16 +154,17 @@ $tab_counts = [
|
||||
'server' => intval($row['server_count']),
|
||||
'virtual' => intval($row['virtual_count']),
|
||||
'network' => intval($row['network_count']),
|
||||
'printer' => intval($row['printer_count']),
|
||||
'other' => intval($row['other_count']),
|
||||
];
|
||||
|
||||
$sql = mysqli_query(
|
||||
$mysqli,
|
||||
"SELECT SQL_CALC_FOUND_ROWS asset_archived_at, asset_contact_id, asset_description, asset_favorite,
|
||||
asset_id, asset_install_date, asset_make, asset_model, asset_name, asset_os, asset_physical_location,
|
||||
"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, interface_ip, interface_ipv6, interface_mac,
|
||||
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
|
||||
@@ -653,7 +656,9 @@ $search_noun = $type_filter ? $asset_type_tabs[$type_filter]['label'] : 'assets'
|
||||
<?php } ?>
|
||||
<?php if ($show_contact) { ?>
|
||||
<td>
|
||||
<?php if ($contact_name) { ?>
|
||||
<?php if ($can_edit) { ?>
|
||||
<?= quickPick('asset_contact', $asset_id, (string) $asset_contact_id, assetQuickHtml('asset_contact', $row), 'list', 0, $client_id) ?>
|
||||
<?php } elseif ($contact_name) { ?>
|
||||
<a class="ajax-modal <?= $contact_archived ? 'text-decoration-line-through text-body-secondary' : '' ?>" href="#" data-modal-url="modals/contact/contact.php?id=<?= $asset_contact_id ?>" data-modal-size="lg" <?= $contact_archived ? 'title="Archived contact"' : '' ?>><?= $contact_name ?></a>
|
||||
<?php } else { ?>
|
||||
<span class="text-body-secondary">-</span>
|
||||
@@ -661,21 +666,29 @@ $search_noun = $type_filter ? $asset_type_tabs[$type_filter]['label'] : 'assets'
|
||||
</td>
|
||||
<?php } ?>
|
||||
<td>
|
||||
<?php if ($location_name) { ?>
|
||||
<?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"><?= $asset_physical_location ?></div>
|
||||
<div class="small text-body-secondary <?= $can_edit ? 'mt-1' : '' ?>"><?= $asset_physical_location ?></div>
|
||||
<?php } ?>
|
||||
<?php if ($asset_rack) { ?>
|
||||
<a class="small text-nowrap" href="rack.php?client_id=<?= $client_id ?>&rack_id=<?= intval($asset_rack['rack_id']) ?>" title="Racked">
|
||||
<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']) ?> · <?= rackPositionLabel($asset_rack['unit_start_number'], $asset_rack['unit_end_number']) ?>
|
||||
</a>
|
||||
<?php } ?>
|
||||
</td>
|
||||
<td><?= $asset_status_pill ?: '<span class="text-body-secondary">-</span>' ?></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 } ?>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -10,7 +10,7 @@ if ($client_id) {
|
||||
}
|
||||
|
||||
// The assets list passes the tab it was on - start on that kind of asset
|
||||
$type_presets = ['workstation' => 'Desktop', 'server' => 'Server', 'virtual' => 'Virtual Machine', 'network' => 'Switch'];
|
||||
$type_presets = ['workstation' => 'Desktop', 'server' => 'Server', 'virtual' => 'Virtual Machine', 'network' => 'Switch', 'printer' => 'Printer'];
|
||||
$type_param = strtolower((string) ($_GET['type'] ?? ''));
|
||||
|
||||
$asset_form_mode = 'add';
|
||||
|
||||
@@ -69,6 +69,7 @@ ob_start();
|
||||
<option <?php if ($type_filter === 'server') { echo "selected"; } ?> value="server">Servers</option>
|
||||
<option <?php if ($type_filter === 'virtual') { echo "selected"; } ?> value="virtual">Virtual Machines</option>
|
||||
<option <?php if ($type_filter === 'network') { echo "selected"; } ?> value="network">Network Devices</option>
|
||||
<option <?php if ($type_filter === 'printer') { echo "selected"; } ?> value="printer">Printers</option>
|
||||
<option <?php if ($type_filter === 'other') { echo "selected"; } ?> value="other">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -6,10 +6,12 @@
|
||||
* empty for a new asset) before including this. Posts the field set
|
||||
* agent/post/asset_model.php reads.
|
||||
*
|
||||
* One page of sections instead of six tabs. Every field is always in the
|
||||
* form: fields that came and went with the asset 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.
|
||||
* 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.
|
||||
*
|
||||
* Should not be accessed directly.
|
||||
*/
|
||||
@@ -76,22 +78,40 @@ $asset_ip_is_dhcp = $asset_ip === 'DHCP';
|
||||
|
||||
?>
|
||||
|
||||
<?php if ($asset_form_mode === 'edit') { ?>
|
||||
<ul class="nav nav-pills mb-3">
|
||||
<!-- 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 active" data-bs-toggle="pill" href="#assetFormDetails">Details</a>
|
||||
<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">History</a>
|
||||
<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>
|
||||
</ul>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane fade show active" id="assetFormDetails">
|
||||
|
||||
<div class="itflow-form-section">Basics</div>
|
||||
<div class="row g-3 mb-4">
|
||||
<!-- 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>
|
||||
@@ -101,7 +121,7 @@ $asset_ip_is_dhcp = $asset_ip === 'DHCP';
|
||||
<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 class="form-text">Pick the asset's location, contact, network and vendor from its page once it's saved.</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">
|
||||
@@ -153,8 +173,8 @@ $asset_ip_is_dhcp = $asset_ip === 'DHCP';
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="itflow-form-section">Hardware</div>
|
||||
<div class="row g-3 mb-4">
|
||||
<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>
|
||||
<input type="text" class="form-control" name="make" placeholder="Dell" maxlength="200" value="<?= $asset_make ?>">
|
||||
@@ -176,8 +196,55 @@ $asset_ip_is_dhcp = $asset_ip === 'DHCP';
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="itflow-form-section">Network <span class="fw-normal text-body-secondary text-lowercase">· the primary interface</span></div>
|
||||
<div class="row g-3 mb-4">
|
||||
<?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>
|
||||
<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 class="col-md-6">
|
||||
<label class="form-label">Where in the building</label>
|
||||
<input type="text" class="form-control" name="physical_location" placeholder="Floor 2, comms closet B" maxlength="200" value="<?= $asset_physical_location ?>">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Assigned to</label>
|
||||
<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'] ?? '') ?>><?= escapeHtml($contact_option['contact_name']) ?><?= $contact_option['contact_archived_at'] ? ' (archived)' : '' ?></option>
|
||||
<?php } ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<?php } else { ?>
|
||||
<div class="row g-3">
|
||||
<div class="col-12">
|
||||
<label class="form-label">Where in the building</label>
|
||||
<input type="text" class="form-control" name="physical_location" placeholder="Floor 2, comms closet B" maxlength="200" value="<?= $asset_physical_location ?>">
|
||||
<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">
|
||||
<label class="form-label">Network</label>
|
||||
@@ -214,6 +281,10 @@ $asset_ip_is_dhcp = $asset_ip === 'DHCP';
|
||||
<label class="form-label">IPv6 address</label>
|
||||
<input type="text" class="form-control font-monospace" name="ipv6" placeholder="2001:db8::1" maxlength="200" value="<?= $asset_ipv6 ?>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<input type="text" class="form-control" name="uri" placeholder="https://, ssh://, rdp://" maxlength="500" value="<?= $asset_uri ?>">
|
||||
@@ -228,47 +299,11 @@ $asset_ip_is_dhcp = $asset_ip === 'DHCP';
|
||||
<div class="form-text">Shown to the client in their portal.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($client_id) { ?>
|
||||
<div class="itflow-form-section">Where & who</div>
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Location</label>
|
||||
<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 class="col-md-6">
|
||||
<label class="form-label">Where in the building</label>
|
||||
<input type="text" class="form-control" name="physical_location" placeholder="Floor 2, comms closet B" maxlength="200" value="<?= $asset_physical_location ?>">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Assigned to</label>
|
||||
<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'] ?? '') ?>><?= escapeHtml($contact_option['contact_name']) ?><?= $contact_option['contact_archived_at'] ? ' (archived)' : '' ?></option>
|
||||
<?php } ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<?php } else { ?>
|
||||
<div class="itflow-form-section">Where</div>
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-12">
|
||||
<label class="form-label">Where in the building</label>
|
||||
<input type="text" class="form-control" name="physical_location" placeholder="Floor 2, comms closet B" maxlength="200" value="<?= $asset_physical_location ?>">
|
||||
</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
<div class="itflow-form-section">Purchase & warranty</div>
|
||||
<div class="row g-3 mb-4">
|
||||
<!-- 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">
|
||||
<label class="form-label">Vendor</label>
|
||||
@@ -298,10 +333,13 @@ $asset_ip_is_dhcp = $asset_ip === 'DHCP';
|
||||
<input type="date" class="form-control" name="warranty_expire" max="2999-12-31" value="<?= $asset_warranty_expire ?>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($asset_form_mode !== 'edit') { ?>
|
||||
<div class="itflow-form-section">Login <span class="fw-normal text-body-secondary text-lowercase">· saved as a credential on the asset</span></div>
|
||||
<div class="row g-3 mb-4">
|
||||
<?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>
|
||||
<input type="text" class="form-control" name="username" placeholder="admin" maxlength="350" autocomplete="off">
|
||||
@@ -311,11 +349,17 @@ $asset_ip_is_dhcp = $asset_ip === 'DHCP';
|
||||
<input type="text" class="form-control" name="password" placeholder="Leave empty to skip" maxlength="350" autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
<div class="itflow-form-section">Photo & notes</div>
|
||||
<!-- Notes and a photo -->
|
||||
<div class="tab-pane fade" id="assetFormNotes">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-5">
|
||||
<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>
|
||||
@@ -328,16 +372,7 @@ $asset_ip_is_dhcp = $asset_ip === 'DHCP';
|
||||
</div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<div class="col-md-7">
|
||||
<label class="form-label">Notes</label>
|
||||
<textarea class="form-control" rows="4" placeholder="Configuration details, quirks, anything worth knowing" name="notes"><?= $asset_notes ?></textarea>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<?php if ($asset_form_mode === 'edit') { ?>
|
||||
@@ -377,6 +412,23 @@ $asset_ip_is_dhcp = $asset_ip === 'DHCP';
|
||||
}
|
||||
});
|
||||
|
||||
// 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);
|
||||
|
||||
// DHCP means no fixed address to type
|
||||
var dhcp = form.querySelector('#assetFormDhcp');
|
||||
var ip = form.querySelector('#assetFormIp');
|
||||
|
||||
@@ -392,14 +392,15 @@ if (isset($_POST['bulk_assign_asset_location'])) {
|
||||
$row = mysqli_fetch_assoc($sql);
|
||||
$location_name = escapeSql($row['location_name']);
|
||||
$client_id = intval($row['location_client_id']);
|
||||
$location_client_id = $client_id;
|
||||
|
||||
enforceClientAccess();
|
||||
|
||||
// Assign Location to Selected Assets
|
||||
if (isset($_POST['asset_ids'])) {
|
||||
|
||||
// Get Selected Asset Count
|
||||
$asset_count = count($_POST['asset_ids']);
|
||||
// Only the assets of the location's client are moved
|
||||
$asset_count = 0;
|
||||
|
||||
foreach($_POST['asset_ids'] as $asset_id) {
|
||||
$asset_id = intval($asset_id);
|
||||
@@ -407,18 +408,24 @@ if (isset($_POST['bulk_assign_asset_location'])) {
|
||||
// Get Asset Details for Logging
|
||||
$sql = mysqli_query($mysqli,"SELECT asset_name, asset_client_id FROM assets WHERE asset_id = $asset_id");
|
||||
$row = mysqli_fetch_assoc($sql);
|
||||
$asset_name = escapeSql($row['asset_name']);
|
||||
$client_id = intval($row['asset_client_id']);
|
||||
$asset_name = escapeSql($row['asset_name'] ?? '');
|
||||
$client_id = intval($row['asset_client_id'] ?? 0);
|
||||
|
||||
if (!$row || $client_id !== $location_client_id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
enforceClientAccess();
|
||||
|
||||
$asset_count++;
|
||||
|
||||
mysqli_query($mysqli,"UPDATE assets SET asset_location_id = $location_id WHERE asset_id = $asset_id");
|
||||
|
||||
logAudit("Asset", "Edit", "$session_name assigned asset $asset_name to location $location_name", $client_id, $asset_id);
|
||||
|
||||
} // End Assign Location Loop
|
||||
|
||||
logAudit("Asset", "Bulk Edit", "$session_name assigned $asset_count assets to location $location_name", $client_id);
|
||||
logAudit("Asset", "Bulk Edit", "$session_name assigned $asset_count assets to location $location_name", $location_client_id);
|
||||
|
||||
flashAlert("You assigned <strong>$asset_count</strong> assets to location <strong>$location_name</strong>");
|
||||
}
|
||||
@@ -578,16 +585,22 @@ if (isset($_POST['bulk_assign_asset_contact'])) {
|
||||
// Assign Contact to Selected Assets
|
||||
if (isset($_POST['asset_ids'])) {
|
||||
|
||||
// Get Selected Contacts Count
|
||||
$asset_count = count($_POST['asset_ids']);
|
||||
// Only the assets of the contact's client are assigned
|
||||
$asset_count = 0;
|
||||
|
||||
foreach($_POST['asset_ids'] as $asset_id) {
|
||||
$asset_id = intval($asset_id);
|
||||
|
||||
// Get Asset Details for Logging
|
||||
$sql = mysqli_query($mysqli,"SELECT asset_name FROM assets WHERE asset_id = $asset_id");
|
||||
$sql = mysqli_query($mysqli,"SELECT asset_name, asset_client_id FROM assets WHERE asset_id = $asset_id");
|
||||
$row = mysqli_fetch_assoc($sql);
|
||||
$asset_name = escapeSql($row['asset_name']);
|
||||
$asset_name = escapeSql($row['asset_name'] ?? '');
|
||||
|
||||
if (!$row || intval($row['asset_client_id']) !== $client_id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$asset_count++;
|
||||
|
||||
mysqli_query($mysqli,"UPDATE assets SET asset_contact_id = $contact_id WHERE asset_id = $asset_id");
|
||||
|
||||
@@ -1344,8 +1357,10 @@ if (isExportRequest('export_assets')) {
|
||||
$type_query = "asset_type = 'Virtual Machine'";
|
||||
} elseif ($type == 'network') {
|
||||
$type_query = "asset_type = 'Firewall/Router' OR asset_type = 'Switch' OR asset_type = 'Access Point'";
|
||||
} elseif ($type == 'printer') {
|
||||
$type_query = "asset_type = 'Printer'";
|
||||
} elseif ($type == 'other') {
|
||||
$type_query = "asset_type NOT LIKE 'laptop' AND asset_type NOT LIKE 'desktop' AND asset_type NOT LIKE 'server' AND asset_type NOT LIKE 'virtual machine' AND asset_type NOT LIKE 'firewall/router' AND asset_type NOT LIKE 'switch' AND asset_type NOT LIKE 'access point'";
|
||||
$type_query = "asset_type NOT LIKE 'laptop' AND asset_type NOT LIKE 'desktop' AND asset_type NOT LIKE 'server' AND asset_type NOT LIKE 'virtual machine' AND asset_type NOT LIKE 'firewall/router' AND asset_type NOT LIKE 'switch' AND asset_type NOT LIKE 'access point' AND asset_type NOT LIKE 'printer'";
|
||||
} else {
|
||||
// Default - any
|
||||
$type = '';
|
||||
|
||||
@@ -3745,3 +3745,169 @@ tr:hover .itflow-copy,
|
||||
text-transform: uppercase;
|
||||
color: var(--bs-secondary-color);
|
||||
}
|
||||
|
||||
/* Asset quick pickers - status, location and who it is assigned to, as pills
|
||||
on the assets list and in the asset page header (functions/asset.php
|
||||
assetQuickHtml), as lines in the asset page's Where & who card */
|
||||
.itflow-asset-pill {
|
||||
max-width: 15rem;
|
||||
padding: .35em .7em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.itflow-asset-pill > .text-truncate {
|
||||
min-width: 0;
|
||||
}
|
||||
.itflow-asset-pill-person {
|
||||
padding: .2rem .6rem .2rem .2rem;
|
||||
}
|
||||
/* Nothing set yet - a dashed slot, so it reads as something to fill in */
|
||||
.itflow-asset-pill-empty {
|
||||
border: 1px dashed var(--bs-border-color);
|
||||
color: var(--bs-secondary-color);
|
||||
background: transparent;
|
||||
}
|
||||
.itflow-asset-avatar {
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
font-size: .6rem;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
background-color: var(--bs-secondary);
|
||||
}
|
||||
/* In the Where & who card, and the quick look's glance lines, the picker is
|
||||
the whole line: it lights up on hover with a chevron, like the ticket
|
||||
page's properties. The negative margin keeps its text in line with the
|
||||
plain values above and below. */
|
||||
.itflow-asset-dl .itflow-quick-pick-detail,
|
||||
.itflow-asset-glance .itflow-quick-pick-detail {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: .5rem;
|
||||
width: calc(100% + 1rem);
|
||||
min-width: 0;
|
||||
margin: -.3rem -.5rem;
|
||||
padding: .3rem .5rem;
|
||||
text-align: left;
|
||||
}
|
||||
.itflow-asset-dl .itflow-quick-pick-detail:hover,
|
||||
.itflow-asset-glance .itflow-quick-pick-detail:hover,
|
||||
.itflow-asset-dl .itflow-quick-pick-detail:focus-visible,
|
||||
.itflow-asset-glance .itflow-quick-pick-detail:focus-visible,
|
||||
.itflow-asset-dl .itflow-quick-pick-detail[aria-expanded="true"],
|
||||
.itflow-asset-glance .itflow-quick-pick-detail[aria-expanded="true"] {
|
||||
background-color: var(--bs-tertiary-bg);
|
||||
}
|
||||
.itflow-asset-dl .itflow-quick-pick-detail + :not(:empty),
|
||||
.itflow-asset-glance .itflow-quick-pick-detail + :not(:empty) {
|
||||
margin-top: .35rem;
|
||||
}
|
||||
.itflow-asset-dl .ticket-prop-edit-icon,
|
||||
.itflow-asset-glance .ticket-prop-edit-icon {
|
||||
flex: 0 0 auto;
|
||||
font-size: .7rem;
|
||||
color: var(--bs-secondary-color);
|
||||
opacity: 0;
|
||||
transition: opacity .15s;
|
||||
}
|
||||
.itflow-asset-dl .itflow-quick-pick-detail:hover .ticket-prop-edit-icon,
|
||||
.itflow-asset-glance .itflow-quick-pick-detail:hover .ticket-prop-edit-icon,
|
||||
.itflow-asset-dl .itflow-quick-pick-detail:focus-visible .ticket-prop-edit-icon,
|
||||
.itflow-asset-glance .itflow-quick-pick-detail:focus-visible .ticket-prop-edit-icon,
|
||||
.itflow-asset-dl .itflow-quick-pick-detail[aria-expanded="true"] .ticket-prop-edit-icon,
|
||||
.itflow-asset-glance .itflow-quick-pick-detail[aria-expanded="true"] .ticket-prop-edit-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
@media (hover: none) {
|
||||
.itflow-asset-dl .ticket-prop-edit-icon,
|
||||
.itflow-asset-glance .ticket-prop-edit-icon {
|
||||
opacity: .6;
|
||||
}
|
||||
}
|
||||
.itflow-asset-dl .itflow-quick-pick-detail.is-saved,
|
||||
.itflow-asset-glance .itflow-quick-pick-detail.is-saved {
|
||||
animation: itflowAssetQuickSaved .9s ease-out;
|
||||
}
|
||||
@keyframes itflowAssetQuickSaved {
|
||||
from { background-color: var(--bs-success-bg-subtle); }
|
||||
to { background-color: transparent; }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.itflow-asset-dl .itflow-quick-pick-detail.is-saved,
|
||||
.itflow-asset-glance .itflow-quick-pick-detail.is-saved {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Asset add / edit tabs - one line, scrolling sideways on a phone */
|
||||
.itflow-asset-form-tabs {
|
||||
gap: .25rem;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
.itflow-asset-form-tabs .nav-link {
|
||||
padding-inline: .75rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Asset quick look - agent/modals/asset/asset.php. The glance lines under the
|
||||
header: an icon in place of a label, then the value */
|
||||
.itflow-asset-glance {
|
||||
font-size: .875rem;
|
||||
}
|
||||
.itflow-glance-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: .6rem;
|
||||
min-width: 0;
|
||||
padding: .3rem 0;
|
||||
}
|
||||
.itflow-glance-item > .fa-fw:first-child {
|
||||
flex: 0 0 auto;
|
||||
margin-top: .2rem;
|
||||
color: var(--bs-secondary-color);
|
||||
}
|
||||
.itflow-glance-item .font-monospace {
|
||||
font-size: .82rem;
|
||||
}
|
||||
.itflow-asset-quicknote {
|
||||
min-height: 2.6rem;
|
||||
max-height: 12rem;
|
||||
field-sizing: content;
|
||||
border-color: transparent;
|
||||
background-color: var(--bs-tertiary-bg);
|
||||
resize: vertical;
|
||||
}
|
||||
.itflow-asset-quicknote:focus {
|
||||
background-color: var(--bs-body-bg);
|
||||
}
|
||||
.itflow-asset-quick-related {
|
||||
padding-top: .75rem;
|
||||
border-top: 1px solid var(--bs-border-color-translucent);
|
||||
}
|
||||
.itflow-asset-quick-related .table,
|
||||
.itflow-asset-quick-related .list-group {
|
||||
font-size: .875rem;
|
||||
}
|
||||
.itflow-asset-quick-related .table > :not(caption) > * > :first-child {
|
||||
padding-left: 0;
|
||||
}
|
||||
.itflow-asset-quick-related .table > :not(caption) > * > :last-child {
|
||||
padding-right: 0;
|
||||
}
|
||||
.itflow-asset-quick-related .table td:first-child {
|
||||
min-width: 0;
|
||||
}
|
||||
.itflow-asset-quick-related .itflow-asset-pills {
|
||||
flex-wrap: nowrap;
|
||||
gap: .15rem;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
.itflow-asset-quick-related .itflow-asset-pills .nav-link {
|
||||
padding: .25rem .55rem;
|
||||
}
|
||||
|
||||
@@ -239,3 +239,246 @@ function assetClientOwnedId($kind, $id, $client_id) {
|
||||
$sql = mysqli_query($mysqli, "SELECT 1 FROM $table WHERE $id_column = $id AND $client_column = $client_id LIMIT 1");
|
||||
return mysqli_num_rows($sql) ? $id : 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Quick pickers (js/quick_pick.js) for an asset's status, location and who it
|
||||
* is assigned to - changed where they show on the assets list and the asset
|
||||
* page instead of through Edit. Wrapped in quickPick() (functions/ticket_fields.php)
|
||||
* with the asset's client as the scope, since locations and contacts are per client.
|
||||
*/
|
||||
|
||||
/** The asset fields a quick picker can change. */
|
||||
const ASSET_QUICK_FIELDS = ['asset_status', 'asset_location', 'asset_contact'];
|
||||
|
||||
/** The asset and what its pickers show, by id - the row assetQuickHtml() and assetContactDetails() draw from. */
|
||||
function assetQuickRow($asset_id) {
|
||||
global $mysqli;
|
||||
return mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT asset_id, asset_name, asset_status, asset_client_id,
|
||||
asset_location_id, location_name, location_archived_at,
|
||||
asset_contact_id, contact_name, contact_photo, contact_archived_at, contact_email,
|
||||
contact_phone, contact_phone_country_code, contact_extension, contact_mobile, contact_mobile_country_code
|
||||
FROM assets
|
||||
LEFT JOIN locations ON location_id = asset_location_id
|
||||
LEFT JOIN contacts ON contact_id = asset_contact_id
|
||||
WHERE asset_id = " . intval($asset_id))) ?: null;
|
||||
}
|
||||
|
||||
/** A contact's picture, or their initials in a circle. 20px, for pills and the picker button. */
|
||||
function assetContactAvatar($client_id, $contact_name, $contact_photo) {
|
||||
$contact_photo = (string) $contact_photo;
|
||||
if ($contact_photo !== '') {
|
||||
return '<img src="/uploads/clients/' . intval($client_id) . '/' . escapeHtml(rawurlencode($contact_photo)) . '" alt="" width="20" height="20" class="rounded-circle object-fit-cover flex-shrink-0">';
|
||||
}
|
||||
return '<span class="itflow-asset-avatar" aria-hidden="true">' . escapeHtml(personInitials($contact_name)) . '</span>';
|
||||
}
|
||||
|
||||
/**
|
||||
* What a picker shows for the value it holds now. $style is 'list' (a pill,
|
||||
* on the assets list and in the asset page header) or 'detail' (a line in the
|
||||
* asset page's Where & who card). $row is raw, as assetQuickRow() returns it.
|
||||
*/
|
||||
function assetQuickHtml($field, $row, $style = 'list') {
|
||||
$client_id = intval($row['asset_client_id'] ?? 0);
|
||||
$pill = 'badge rounded-pill fw-normal d-inline-flex align-items-center gap-1 itflow-asset-pill';
|
||||
$empty = "$pill itflow-asset-pill-empty";
|
||||
|
||||
if ($field === 'asset_status') {
|
||||
return assetStatusPill($row['asset_status'] ?? '') ?: "<span class=\"$empty\">No status</span>";
|
||||
}
|
||||
|
||||
if ($field === 'asset_location') {
|
||||
if (!intval($row['asset_location_id'] ?? 0) || (string) ($row['location_name'] ?? '') === '') {
|
||||
return $style === 'detail'
|
||||
? '<span class="text-body-secondary">No location</span>'
|
||||
: "<span class=\"$empty\"><i class=\"fas fa-map-marker-alt\"></i>No location</span>";
|
||||
}
|
||||
$archived = !empty($row['location_archived_at']);
|
||||
$name = '<span class="text-truncate' . ($archived ? ' text-decoration-line-through' : '') . '"' . ($archived ? ' title="Archived location"' : '') . '>' . escapeHtml($row['location_name']) . '</span>';
|
||||
return $style === 'detail'
|
||||
? '<span class="d-inline-flex align-items-center gap-2 min-w-0' . ($archived ? ' text-danger' : '') . "\">$name</span>"
|
||||
: "<span class=\"$pill " . ($archived ? 'text-danger-emphasis bg-danger-subtle' : 'text-body-emphasis bg-body-secondary') . "\"><i class=\"fas fa-map-marker-alt text-body-secondary\"></i>$name</span>";
|
||||
}
|
||||
|
||||
if ($field === 'asset_contact') {
|
||||
if (!intval($row['asset_contact_id'] ?? 0) || (string) ($row['contact_name'] ?? '') === '') {
|
||||
return $style === 'detail'
|
||||
? '<span class="text-body-secondary">Nobody</span>'
|
||||
: "<span class=\"$empty\"><i class=\"fas fa-user-slash\"></i>Nobody</span>";
|
||||
}
|
||||
$archived = !empty($row['contact_archived_at']);
|
||||
$avatar = assetContactAvatar($client_id, $row['contact_name'], $row['contact_photo'] ?? '');
|
||||
$name = '<span class="text-truncate' . ($archived ? ' text-decoration-line-through' : '') . '"' . ($archived ? ' title="Archived contact"' : '') . '>' . escapeHtml($row['contact_name']) . '</span>';
|
||||
return $style === 'detail'
|
||||
? "<span class=\"d-inline-flex align-items-center gap-2 min-w-0\">$avatar$name</span>"
|
||||
: "<span class=\"$pill itflow-asset-pill-person " . ($archived ? 'text-body-secondary' : 'text-body-emphasis') . " bg-body-secondary\">$avatar$name</span>";
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/** Under the asset page's Assigned picker: how to reach that person, and a link to them. Redrawn when the contact changes. */
|
||||
function assetContactDetails($row) {
|
||||
$contact_id = intval($row['asset_contact_id'] ?? 0);
|
||||
if (!$contact_id || (string) ($row['contact_name'] ?? '') === '') {
|
||||
return '';
|
||||
}
|
||||
$client_id = intval($row['asset_client_id']);
|
||||
$email = escapeHtml($row['contact_email'] ?? '');
|
||||
$phone = escapeHtml(formatPhoneNumber($row['contact_phone'] ?? '', $row['contact_phone_country_code'] ?? ''));
|
||||
$extension = escapeHtml($row['contact_extension'] ?? '');
|
||||
$mobile = escapeHtml(formatPhoneNumber($row['contact_mobile'] ?? '', $row['contact_mobile_country_code'] ?? ''));
|
||||
|
||||
$html = '';
|
||||
if ($email) {
|
||||
$html .= "<div class=\"small text-truncate\"><a href=\"mailto:$email\">$email</a><button type=\"button\" class=\"itflow-copy clipboardjs\" title=\"Copy email\" data-clipboard-text=\"$email\"><i class=\"far fa-copy\"></i></button></div>";
|
||||
}
|
||||
if ($phone) {
|
||||
$html .= "<div class=\"small\"><i class=\"fas fa-fw fa-phone text-secondary me-1\"></i>$phone" . ($extension ? " x$extension" : '') . '</div>';
|
||||
}
|
||||
if ($mobile) {
|
||||
$html .= "<div class=\"small\"><i class=\"fas fa-fw fa-mobile-alt text-secondary me-1\"></i>$mobile</div>";
|
||||
}
|
||||
$html .= "<div class=\"small\"><a href=\"contact.php?client_id=$client_id&contact_id=$contact_id\">Open contact<i class=\"fas fa-arrow-right ms-1\"></i></a></div>";
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* The choices a picker offers, in js/quick_pick.js's option shape. Locations
|
||||
* and contacts are the client's own (null if the agent can't reach that client).
|
||||
*/
|
||||
function assetQuickOptions($field, $client_id) {
|
||||
global $mysqli;
|
||||
$client_id = intval($client_id);
|
||||
|
||||
if ($field === 'asset_status') {
|
||||
$options = [['value' => '', 'label' => 'No status', 'icon' => 'fas fa-minus', 'none' => true]];
|
||||
$sql = mysqli_query($mysqli, "SELECT category_name, category_color FROM categories
|
||||
WHERE category_type = 'asset_status' AND category_archived_at IS NULL
|
||||
ORDER BY category_order ASC, category_name ASC");
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
$badge = assetStatusBadge($row['category_name'], $row['category_color']);
|
||||
$color = $badge['style'] ? trim(substr($badge['style'], strlen('background-color:'))) : "var(--bs-{$badge['class']})";
|
||||
$options[] = ['value' => $row['category_name'], 'label' => $row['category_name'], 'color' => $color];
|
||||
}
|
||||
return $options;
|
||||
}
|
||||
|
||||
if (!$client_id || !hasClientAccess($client_id)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($field === 'asset_location') {
|
||||
$options = [['value' => '0', 'label' => 'No location', 'icon' => 'fas fa-ban', 'none' => true]];
|
||||
$sql = mysqli_query($mysqli, "SELECT location_id, location_name, location_primary, location_city, location_state FROM locations
|
||||
WHERE location_client_id = $client_id AND location_archived_at IS NULL
|
||||
ORDER BY location_primary DESC, location_name ASC");
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
$place = implode(', ', array_filter([trim((string) $row['location_city']), trim((string) $row['location_state'])]));
|
||||
$description = implode(' · ', array_filter([intval($row['location_primary']) ? 'Primary' : '', $place]));
|
||||
$options[] = ['value' => (string) intval($row['location_id']), 'label' => $row['location_name'], 'icon' => 'fas fa-map-marker-alt', 'description' => $description];
|
||||
}
|
||||
return $options;
|
||||
}
|
||||
|
||||
if ($field === 'asset_contact') {
|
||||
$options = [['value' => '0', 'label' => 'Nobody', 'icon' => 'fas fa-user-slash', 'none' => true]];
|
||||
$sql = mysqli_query($mysqli, "SELECT contact_id, contact_name, contact_title, contact_photo FROM contacts
|
||||
WHERE contact_client_id = $client_id AND contact_archived_at IS NULL
|
||||
ORDER BY contact_name ASC");
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
$option = ['value' => (string) intval($row['contact_id']), 'label' => $row['contact_name'], 'description' => (string) $row['contact_title']];
|
||||
if ($row['contact_photo']) {
|
||||
$option['image'] = "/uploads/clients/$client_id/" . rawurlencode($row['contact_photo']);
|
||||
} else {
|
||||
$option['initials'] = personInitials($row['contact_name']);
|
||||
}
|
||||
$options[] = $option;
|
||||
}
|
||||
return $options;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change one field on one asset, as Edit Asset would - audit log and asset
|
||||
* history included. The caller checks the agent may edit assets of its
|
||||
* client. Returns [ok, plain-text message].
|
||||
*/
|
||||
function assetQuickSet($asset_id, $field, $value) {
|
||||
global $mysqli, $session_name;
|
||||
|
||||
$asset_id = intval($asset_id);
|
||||
$row = assetQuickRow($asset_id);
|
||||
if (!$row) {
|
||||
return [false, 'That asset no longer exists.'];
|
||||
}
|
||||
$client_id = intval($row['asset_client_id']);
|
||||
$asset_name = escapeSql($row['asset_name']);
|
||||
$value = trim((string) $value);
|
||||
|
||||
if ($field === 'asset_status') {
|
||||
if ($value !== '' && !isset(assetStatusColors()[$value])) {
|
||||
return [false, 'Pick one of the asset statuses.'];
|
||||
}
|
||||
if ($value === (string) $row['asset_status']) {
|
||||
return [true, ''];
|
||||
}
|
||||
$status = escapeSql($value);
|
||||
mysqli_query($mysqli, "UPDATE assets SET asset_status = '$status' WHERE asset_id = $asset_id");
|
||||
$what = $value === '' ? "cleared the status on $asset_name" : "set status to $status on $asset_name";
|
||||
$message = $value === '' ? 'Status cleared' : "Status set to $value";
|
||||
|
||||
} elseif ($field === 'asset_location') {
|
||||
$location_id = assetClientOwnedId('location', $value, $client_id);
|
||||
if (intval($value) && !$location_id) {
|
||||
return [false, 'That location is not one of this client\'s.'];
|
||||
}
|
||||
if ($location_id === intval($row['asset_location_id'])) {
|
||||
return [true, ''];
|
||||
}
|
||||
$location_name = $location_id ? (string) mysqli_fetch_row(mysqli_query($mysqli, "SELECT location_name FROM locations WHERE location_id = $location_id"))[0] : '';
|
||||
$status = escapeSql($row['asset_status']);
|
||||
mysqli_query($mysqli, "UPDATE assets SET asset_location_id = $location_id WHERE asset_id = $asset_id");
|
||||
$what = $location_id ? "assigned asset $asset_name to location " . escapeSql($location_name) : "removed asset $asset_name from its location";
|
||||
$message = $location_id ? "Moved to $location_name" : 'Location removed';
|
||||
|
||||
} elseif ($field === 'asset_contact') {
|
||||
$contact_id = assetClientOwnedId('contact', $value, $client_id);
|
||||
if (intval($value) && !$contact_id) {
|
||||
return [false, 'That contact is not one of this client\'s.'];
|
||||
}
|
||||
if ($contact_id === intval($row['asset_contact_id'])) {
|
||||
return [true, ''];
|
||||
}
|
||||
$contact_name = $contact_id ? (string) mysqli_fetch_row(mysqli_query($mysqli, "SELECT contact_name FROM contacts WHERE contact_id = $contact_id"))[0] : '';
|
||||
$status = escapeSql($row['asset_status']);
|
||||
mysqli_query($mysqli, "UPDATE assets SET asset_contact_id = $contact_id WHERE asset_id = $asset_id");
|
||||
$what = $contact_id ? "assigned asset $asset_name to contact " . escapeSql($contact_name) : "unassigned asset $asset_name";
|
||||
$message = $contact_id ? "Assigned to $contact_name" : 'Unassigned';
|
||||
|
||||
} else {
|
||||
return [false, 'Unknown field.'];
|
||||
}
|
||||
|
||||
logAudit("Asset", "Edit", "$session_name $what", $client_id, $asset_id);
|
||||
mysqli_query($mysqli, "INSERT INTO asset_history SET asset_history_status = '$status', asset_history_description = '$session_name $what', asset_history_asset_id = $asset_id");
|
||||
|
||||
return [true, $message];
|
||||
}
|
||||
|
||||
/** What quick_pick.js redraws after a change: the value in both looks, plus the contact details on the asset page. */
|
||||
function assetQuickRecord($asset_id, $field) {
|
||||
$row = assetQuickRow($asset_id);
|
||||
$current = [
|
||||
'asset_status' => (string) $row['asset_status'],
|
||||
'asset_location' => (string) intval($row['asset_location_id']),
|
||||
'asset_contact' => (string) intval($row['asset_contact_id']),
|
||||
][$field];
|
||||
$record = ['current' => $current, 'html' => ['list' => assetQuickHtml($field, $row, 'list'), 'detail' => assetQuickHtml($field, $row, 'detail')]];
|
||||
if ($field === 'asset_contact') {
|
||||
$record['related'] = ['asset-contact-' . intval($asset_id) => assetContactDetails($row)];
|
||||
}
|
||||
return $record;
|
||||
}
|
||||
|
||||
@@ -268,18 +268,21 @@ function ticketStatusHtml($status_name, $status_color, $style = 'list') {
|
||||
|
||||
/*
|
||||
* The button that opens a quick picker (js/quick_pick.js) for one record's
|
||||
* field, wrapped round how the value looks now. $id is the ticket, project
|
||||
* or recurring ticket, depending on the field.
|
||||
* field, wrapped round how the value looks now. $id is the ticket, project,
|
||||
* recurring ticket or asset, depending on the field. $scope is the client
|
||||
* whose choices it offers, for fields where those are per client (an asset's
|
||||
* location and contact).
|
||||
*/
|
||||
function quickPick($field, $id, $current, $inner, $style = 'list', $status_id = 0) {
|
||||
function quickPick($field, $id, $current, $inner, $style = 'list', $status_id = 0, $scope = 0) {
|
||||
$labels = [
|
||||
'priority' => 'Change priority', 'assignee' => 'Change who it is assigned to', 'billable' => 'Change billing', 'status' => 'Change status',
|
||||
'project_manager' => 'Change the project manager', 'recurring_assignee' => 'Change who it is assigned to'
|
||||
'project_manager' => 'Change the project manager', 'recurring_assignee' => 'Change who it is assigned to',
|
||||
'asset_status' => 'Change status', 'asset_location' => 'Change location', 'asset_contact' => 'Change who it is assigned to'
|
||||
];
|
||||
// On the ticket page it looks like the other editable properties there (agent/css/ticket.css)
|
||||
$class = 'itflow-quick-pick itflow-quick-pick-' . $style . ($style === 'detail' ? ' ticket-prop-edit' : '');
|
||||
return '<button type="button" class="' . $class . '" data-quick-pick="' . $field . '" data-quick-id="' . intval($id) . '"'
|
||||
. ' data-quick-current="' . escapeHtml($current) . '" data-quick-style="' . $style . '"' . ($status_id ? ' data-status-id="' . intval($status_id) . '"' : '') . ' data-csrf="' . escapeHtml($_SESSION['csrf_token'] ?? '') . '"'
|
||||
. ' data-quick-current="' . escapeHtml($current) . '" data-quick-style="' . $style . '"' . ($status_id ? ' data-status-id="' . intval($status_id) . '"' : '') . ($scope ? ' data-quick-scope="' . intval($scope) . '"' : '') . ' data-csrf="' . escapeHtml($_SESSION['csrf_token'] ?? '') . '"'
|
||||
. ' title="' . $labels[$field] . '" aria-haspopup="listbox" aria-expanded="false">'
|
||||
. '<span class="itflow-quick-pick-value">' . $inner . '</span>'
|
||||
. ($style === 'detail' ? '<i class="fas fa-chevron-down ticket-prop-edit-icon" aria-hidden="true"></i>' : '')
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
/*
|
||||
* Quick pickers - a ticket's status, priority, assignee and billable, a
|
||||
* project's manager and a recurring ticket's assignee, changed where you see
|
||||
* them (ticket list and page, projects and project page, recurring tickets)
|
||||
* project's manager, a recurring ticket's assignee and an asset's status,
|
||||
* location and contact, changed where you see them (ticket list and page,
|
||||
* projects and project page, recurring tickets, assets list and asset page)
|
||||
* instead of in a modal.
|
||||
*
|
||||
* Click the value and a small menu opens under it (a sheet from the bottom
|
||||
@@ -13,6 +14,12 @@
|
||||
* ticket's status redraws too, wherever it is marked with
|
||||
* data-ticket-status-for="<ticket id>".
|
||||
*
|
||||
* Where the choices depend on the client (an asset's location and contact)
|
||||
* the button carries data-quick-scope="<client id>", and the choices are
|
||||
* fetched and kept per client. Anything else on the page that shows the
|
||||
* value comes back as "related" HTML for [data-quick-related="<key>"] - the
|
||||
* asset page's contact details under its Assigned picker.
|
||||
*
|
||||
* Markup comes from quickPick() in functions/ticket_fields.php. Styled
|
||||
* with the select picker's panel classes, like the tag picker.
|
||||
*/
|
||||
@@ -25,8 +32,12 @@
|
||||
|
||||
var sheetMedia = window.matchMedia('(max-width: 575.98px)');
|
||||
var finePointer = window.matchMedia('(pointer: fine)');
|
||||
var titles = { priority: 'Priority', assignee: 'Assign to', billable: 'Billing', status: 'Status', project_manager: 'Project manager', recurring_assignee: 'Assign to' };
|
||||
var agentFields = ['assignee', 'project_manager', 'recurring_assignee'];
|
||||
var titles = {
|
||||
priority: 'Priority', assignee: 'Assign to', billable: 'Billing', status: 'Status', project_manager: 'Project manager', recurring_assignee: 'Assign to',
|
||||
asset_status: 'Status', asset_location: 'Location', asset_contact: 'Assign to'
|
||||
};
|
||||
// People and places get a wider menu
|
||||
var wideFields = ['assignee', 'project_manager', 'recurring_assignee', 'asset_location', 'asset_contact'];
|
||||
var cache = {};
|
||||
var open = null;
|
||||
|
||||
@@ -66,19 +77,29 @@
|
||||
return node;
|
||||
}
|
||||
|
||||
function options(field, done, failed) {
|
||||
if (cache[field]) {
|
||||
done(cache[field]);
|
||||
function options(field, scope, done, failed) {
|
||||
var key = field + '|' + scope;
|
||||
if (cache[key]) {
|
||||
done(cache[key]);
|
||||
return;
|
||||
}
|
||||
itflowGet('ajax.php', { quick_pick_options: 1, field: field }, function (text) {
|
||||
var query = { quick_pick_options: 1, field: field };
|
||||
if (scope) {
|
||||
query.scope = scope;
|
||||
}
|
||||
itflowGet('ajax.php', query, function (text) {
|
||||
var list;
|
||||
try {
|
||||
cache[field] = JSON.parse(text).options;
|
||||
list = JSON.parse(text).options;
|
||||
} catch (e) {
|
||||
list = null;
|
||||
}
|
||||
if (!Array.isArray(list)) {
|
||||
failed();
|
||||
return;
|
||||
}
|
||||
done(cache[field]);
|
||||
cache[key] = list;
|
||||
done(list);
|
||||
}, failed);
|
||||
}
|
||||
|
||||
@@ -87,6 +108,7 @@
|
||||
this.field = button.dataset.quickPick;
|
||||
this.ids = [button.dataset.quickId];
|
||||
this.current = button.dataset.quickCurrent;
|
||||
this.scope = button.dataset.quickScope || '';
|
||||
this.csrf = button.dataset.csrf || '';
|
||||
this.query = '';
|
||||
this.rows = [];
|
||||
@@ -170,7 +192,7 @@
|
||||
this.button.setAttribute('aria-expanded', 'true');
|
||||
this.place();
|
||||
|
||||
options(this.field, function (list) {
|
||||
options(this.field, this.scope, function (list) {
|
||||
if (open !== self) {
|
||||
return;
|
||||
}
|
||||
@@ -217,7 +239,7 @@
|
||||
var rect = this.button.getBoundingClientRect();
|
||||
var viewWidth = document.documentElement.clientWidth;
|
||||
var viewHeight = window.innerHeight;
|
||||
var width = Math.min(agentFields.indexOf(this.field) !== -1 ? 264 : 224, viewWidth - 16);
|
||||
var width = Math.min(wideFields.indexOf(this.field) !== -1 ? 264 : 224, viewWidth - 16);
|
||||
var panel = this.panel;
|
||||
panel.style.width = width + 'px';
|
||||
panel.style.left = Math.min(Math.max(8, rect.left), viewWidth - width - 8) + 'px';
|
||||
@@ -381,6 +403,11 @@
|
||||
spot.classList.remove('is-saved');
|
||||
}, 900);
|
||||
});
|
||||
Object.keys(record.related || {}).forEach(function (key) {
|
||||
document.querySelectorAll('[data-quick-related="' + key + '"]').forEach(function (spot) {
|
||||
spot.innerHTML = record.related[key];
|
||||
});
|
||||
});
|
||||
if (record.status_id === undefined) {
|
||||
return; // not a ticket
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user