From 9ba21cf3527fd3cec712adc673dc9507739cc740 Mon Sep 17 00:00:00 2001 From: johnnyq Date: Tue, 27 Oct 2026 15:59:03 -0400 Subject: [PATCH] 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 --- CHANGELOG.md | 24 +- admin/database_updates/3.4.9.php | 28 + agent/asset.php | 61 +- agent/assets.php | 65 +- agent/modals/asset/asset.php | 18 +- agent/modals/asset/asset_add.php | 3 +- agent/modals/asset/asset_copy.php | 7 +- agent/modals/asset/asset_edit.php | 3 + agent/modals/asset/asset_export.php | 9 +- agent/modals/asset/asset_form.php | 238 +++++-- agent/modals/asset/asset_import.php | 1 + .../asset/asset_interface_multiple_add.php | 30 +- agent/modals/rack/rack_device_add.php | 4 +- agent/modals/rack/rack_device_form.php | 4 +- agent/post/asset.php | 110 +++- agent/rack.php | 4 +- api/v1/assets/asset_model.php | 2 +- api/v1/assets/create.php | 8 + api/v1/assets/read.php | 1 + api/v1/assets/update.php | 9 + api/v1/read_output.php | 5 + client/functions.php | 10 +- cron/nightly_tasks.php | 15 + css/itflow_custom.css | 9 + db.sql | 19 + functions.php | 1 + functions/ai_document.php | 4 +- functions/app.php | 33 +- functions/asset.php | 19 +- functions/asset_interface.php | 127 +++- functions/asset_type.php | 585 ++++++++++++++++++ functions/client_export.php | 10 +- functions/client_overview.php | 8 +- functions/document_template.php | 6 +- functions/export.php | 7 + functions/notifications.php | 3 +- functions/products.php | 5 +- functions/rack.php | 4 + includes/load_global_settings.php | 18 +- 39 files changed, 1277 insertions(+), 240 deletions(-) create mode 100644 admin/database_updates/3.4.9.php create mode 100644 functions/asset_type.php diff --git a/CHANGELOG.md b/CHANGELOG.md index d886bc3de..2949fdecd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ This file documents all notable changes made to ITFlow. ### Upgrading to 26.11 -Update from Maintenance > Update — Queue Update hands the job to cron, which updates the files and applies this release's database changes (2.7.9 to 3.4.8) in the same pass. Nothing else is required. +Update from Maintenance > Update — Queue Update hands the job to cron, which updates the files and applies this release's database changes (2.7.9 to 3.4.9) in the same pass. Nothing else is required. This is a big release, with nearly seventy database steps in one go, so take a backup before you update. @@ -75,6 +75,8 @@ The app logs are kept for as many days as the audit log, as before, and each can AI providers keep their address, key and models. Each is given a kind from where it points - OpenAI, Anthropic, Azure OpenAI, Google Gemini, Mistral, Groq, OpenRouter, Ollama or LM Studio - or Other (OpenAI-compatible), and passwords and keys are taken out of what is sent to each from now on, which can be turned off for a provider in its Edit window. Every role keeps using AI, through a new Use AI permission in Admin > Roles that can now be taken away from a role. A model's prompt is now its instructions, sent after what each feature asks for (see Breaking Changes, AI). AI Usage starts empty. AI triage of new tickets starts off - turn it on in Admin > Settings > Tickets. +Assets keep their type and everything they hold. NAS, UPS, PDU, Modem/ONT and NVR/DVR are new types to choose from, and no asset is changed to one by the update - a NAS or UPS entered before as a server or Other stays that until you change its type. No asset has details (ports, RAM, battery dates and the like) until you add them, and the update adds no interfaces (see New Features, Asset types and details). + If you are coming from 26.08 or earlier, the database update still has to be run from the command line one time, exactly as described under Upgrading to 26.09 below. ### Breaking Changes and Notes @@ -1208,6 +1210,17 @@ If you are coming from 26.08 or earlier, the database update still has to be run - Each item shows as it will look: a tag as its pill, a category as its chip in its colour and icon, a product with its price and how it is billed - with what each is for. Categories are grouped by type, tags by what they tag and products by income category. - Preview, on ticket, project, document, vendor and license templates, canned responses and products, opens the whole of it in a window: a ticket template's subject, category, priority, tags, request form, what it says, what it asks for and its tasks; a canned response's reply type, ticket categories, status and what it says; a document template's review period, fill-ins and content; a vendor's support site, status page, portal and what support asks for; a license's type, renewal, vendor and product; a product's price, billing, code and category; a project template's tickets in order. The words filled in as each is used are marked. - On Maintenance > Starter Content, a pack's Add brings only what is ticked - and waits while nothing is - and Will be added says how many. Add all still adds every pack whole. +- **Asset types and details** (new) + - Five more types: NAS, UPS, PDU, Modem/ONT and NVR/DVR, each with its own icon and colour. The asset list's tabs follow them: NAS units sit with the servers in Servers & NAS, UPSs and PDUs in a new Power tab, modems with the network kit and NVRs under Other. + - Each type asks for what it has and nothing else. A virtual machine has no serial number, purchase or warranty, a mobile phone or tablet no network, and a display no OS. Kit that runs firmware - network gear, UPSs, printers, desk phones, cameras - calls the OS field Firmware, and only computers, phones, tablets and displays are assigned to someone. A field a type doesn't ask for still shows while it holds something, so nothing is lost when a type changes. The examples in each field follow the type picked. + - Details: the form has a Details section with what only that kind of thing has - CPU, RAM and storage for a laptop, desktop or server; host, vCPUs, RAM and storage for a virtual machine; ports, SFP ports, PoE budget and management for a switch; ports and SFP ports for a firewall; the Wi-Fi standard for an access point; drive bays, drives, raw and usable capacity, RAID and ports for a NAS; capacity (VA), output, runtime, outlets and when the battery went in and is to be replaced by for a UPS; kind, outlets and input for a PDU; circuit ID, ISP account and ports for a modem; channels, storage and ports for an NVR; colour and supplies for a printer; the extension for a desk phone; number, carrier and IMEI for a mobile phone or tablet; size and resolution for a display, and resolution for a camera. A value a detail can't take - a word for a number, a date that doesn't exist - is left out, and the message after saving says which and why. + - Ports: saving a switch with 24 ports and 4 SFP ports adds the interfaces it doesn't have yet - 02 to 24 after its primary 01, then SFP 1 to 4 - as Ethernet and SFP, carrying on from the ports already there. A new count adds the ones missing and a lower one takes none away; an edit that leaves the counts as they were adds nothing back. Wi-Fi, VLAN and other virtual interfaces don't count as ports, and neither does a management port such as mgmt0 or iDRAC. The form says how many the asset has now. + - The asset page's Hardware card lists the details, and the quick look the main ones. A server or NAS lists the virtual machines that run on it, and a VM links its host - one of the same client's servers or NAS units. + - UPS batteries: the replace-by date shows how long is left on the asset page, a badge in the asset list as it nears or passes, and agents are notified in ITFlow 45, 7 and 1 days before, as for warranties. + - The asset list has a Details column to show, with each asset's main details. Assigned To is offered only on assets that are assigned to someone, and New on each tab says what it adds - New Network Device, New UPS. + - Copy brings the details along, but not those of the one unit - IMEI, phone number, extension, circuit ID and ISP account. Transfer to Client takes the details and the interfaces' types to the new client, all but a VM's host. + - Export can add any detail as a column of its own, and its type filter has the list's tabs. Import reads details from columns after the usual twelve, headed as the export heads them (Ports, RAM (GB), Host and so on), keeps those the row's type has - a host by its name, even one further down the file - adds the ports they count, and says which values it couldn't keep. The API reads and writes them as `asset_specs`. + - Racks offer NAS units, UPSs, PDUs, NVRs and modems with the servers and network kit when racking an asset, and list them under Not racked yet - a NAS or UPS at 2U, a PDU or modem at half depth to start with. In the client portal's Your IT, UPSs and PDUs are under Power and NAS units with the servers, and the client export and overview group them the same way. ### Bug Fixes @@ -1421,6 +1434,9 @@ If you are coming from 26.08 or earlier, the database update still has to be run - Assets: unlinking a contact from an asset with a page opened earlier took off whichever contact the asset had been given since. - Archiving, restoring or deleting an asset, contact, note, invoice, quote, payment or recurring invoice, or saving one, that someone else had just deleted showed an error page or warnings and wrote history for a record that was gone. It now says the record no longer exists. - Settings: the ticket prefix's letters-and-hyphens check didn't run in the browser - its pattern is refused by current browsers. ITFlow still checked it on saving. +- Assets: importing assets gave a row with no physical location or notes those of the row before it, saved rows without a purchase date with 0000-00-00 instead of none, and raised warnings for an Assigned To or Location that isn't one of the client's. A file with any column past the twelve was refused. +- Assets: a type sent through the API or an import in other capitals, such as server, was saved that way, so the asset didn't get its type's icon or fields. It is saved as the list spells it now. +- Assets: Transfer to Client dropped the type of every interface it copied, so SFP cages came across as untyped. ### Developer Updates @@ -2030,6 +2046,12 @@ If you are coming from 26.08 or earlier, the database update still has to be run - Security sweep 2: new in `functions/export.php` - `exportClientName($client_id)` and `exportRecordName($table, $id, $name_column, $client_column)` (the name for an export's filter line only when the agent can reach its client; a record of no client is named) and `guardExportHasRows($num_rows, $what)` ("No $what match - nothing to export." and back, called before the `$num_rows > 0` block of every export but projects, services and the two logs, which already did). `download_assets_csv_template` needs `module_support` and names the file after the client only when `hasClientAccess()`. The flash messages of `delete_ticket_additional_asset`, `bulk_add_ticket_project` and `bulk_edit_asset_interface_network` name the asset, project or network only once it was on the ticket or taken; the unlink handlers in `agent/post/asset.php` and `contact.php` name the other record only when `assetClientOwnedId()` owns it; `bulk_move_files` refuses a folder whose client the agent can't reach before naming it. - Security sweep 2: the guest quote's Accept and Decline post `quote_id`, `url_key` and `accept_quote` or `decline_quote` to `guest/guest_post.php`, which compares the key with `hash_equals()`; the GET form of `accept_quote` / `decline_quote` no longer answers. `contractSignatureByKey()`, `portalInviteByLink()` and `client/join.php` take a key or token that isn't a string as ''. `$_SERVER['HTTP_USER_AGENT'] ?? ''` in `api/v1/validate_api_key.php`, `includes/load_user_session.php`, `client/login_microsoft.php`, `guest/includes/inc_all_guest.php` and `isMobile()`. - Security sweep 2: the handlers that fetch their record and go on when it is gone return "That ... no longer exists." - `archive_asset`, `restore_asset` (both failed on `asset_history`'s foreign key), `delete_asset`, `edit_asset`, the asset and contact note handlers, `add_asset_note`, `add_contact_note`, `anonymize_contact`, `archive_contact`, `restore_contact`, `delete_contact`, `unlink_credential_from_contact`, `unlink_credential_from_asset`, `edit_invoice`, `add_invoice_copy`, `mark_invoice_sent`, `mark_invoice_non-billable`, `cancel_invoice`, `delete_invoice`, `delete_payment`, `refund_payment_stripe`, `add_quote_copy`, `edit_quote`, `mark_quote_sent`, `delete_quote`, `accept_quote`, `decline_quote`, `mark_quote_invoiced`, `add_invoice_recurring`, `edit_recurring_invoice`, `set_recurring_payment`, `delete_recurring_invoice` and `recurring_invoice_email_notify`. `restore_contact_note` read its id from `unarchive_contact_note`. The ticket prefix's `pattern` is `[A-Za-z\-]+` - an unescaped `-` in a class is an error under the `v` flag browsers now compile `pattern` with. +- Asset types: new `functions/asset_type.php`, loaded by `functions.php`. `ASSET_TYPES` (name => `icon`, `tone`, `group`, `os` - `os` or `firmware` - `fields` from `ASSET_FIELDS` (`contact`, `os`, `network`, `nat`, `links`, `serial`, `purchase`, `warranty`), `specs` and `example`, the form's placeholders), `ASSET_SPECS` (key => `label`, `input` - text, number, select, date or asset - `icon`, and as they apply `unit`, `min`, `max`, `step`, `options`, `suggest`, `asset_types`, `placeholder`, `ports` (`copper` or `sfp`), `flag`, `mono`, `unit_only` and `alone` - a value that says what it is, shown without its label in a short line), `ASSET_RACK_TYPES` and `ASSET_TYPE_GROUPS`. `assetTypeInfo()` (case-blind, Other's entry for a type it doesn't know), `assetTypeName()`, `assetTypeIcons()` (what `$asset_types_array` and `productAssetTypes()` return now), `assetTypesInGroup(...$groups)`, `assetTypeGroupSql($group, $column)`, `assetTypeTabs($column)` (`workstation`, `server`, `virtual`, `network`, `power`, `printer` and `other`, each with `label`, `new`, `groups`, `icon` and `query` - the list's and the export's `type`), `assetTypeHasField()`, `assetTypeOsLabel()` and `assetTypeSpecs()`. `assetTypeTone()`, `getAssetIcon()`, `rackItemStyle()`, `portalAssetGroups()` (with `power`), the client overview's counts, the client export's families and the document template and AI asset lists read the registry. +- Asset details: database 3.4.9 adds `asset_specs` - `asset_spec_asset_id` (ON DELETE CASCADE), `asset_spec_key` varchar(50) and `asset_spec_value` varchar(255), unique on the asset and key, keyed on the key and value. `assetSpecsReady()` (false until the update has run: nothing is read, kept or flagged), `assetSpecsFor($asset_ids)`, `assetSpecs($asset_id)`, `assetSpecsFromInput($type, $input, $client_id, $asset_id, &$problems)` (each checked by its input; an asset pick by id or name, the same client's, of its `asset_types`, not itself), `assetSpecsSave($asset_id, $type, $specs)` (keys not given or not the type's are deleted), `assetSpecsFromPost($asset_id, $type, $input, $client_id)` (returns the message's ", with ... added" and the problems; ports are made when a count is new or changed), `assetPortsFromSpecs()`, `assetPortsMadeText()`, `assetSpecText()`, `assetSpecRows($type, $specs, $client_id)` (key => `label`, `icon`, `text`, `html`, `flag` from `assetWarranty()`, `alone` and `brief` - the short line the list and the quick look use), `assetSpecAssetOptions()`, `assetGuests($asset_id, $client_id)`, `assetSpecsExportAttach($rows)` (`spec_`, a host by name) and `assetSpecsApiAttach($rows)`. +- Asset handlers: `add_asset` and `edit_asset` take `spec[key]`; `edit_asset` without `spec` keeps the asset's details, less those its type doesn't take. `import_assets_csv` takes twelve columns or more, matching each after the twelfth to a detail by its label, label and unit, or key, and keeps them once every row is in. `bulk_transfer_client_asset` copies `interface_type` and the details but `vm_host`. `export_assets` takes `spec_` in `columns[]` and the `assetTypeTabs()` keys in `type`. `agent/assets.php` takes `type=power` and `show_column[]=Details`. +- Interfaces: `INTERFACE_SFP_TYPES`, `INTERFACE_NOT_PORT_PATTERN`, `INTERFACE_MANAGEMENT_PATTERN`, `interfaceIsSfp()`, `interfacePortKind($type, $name)` (`copper`, `sfp`, or '' for a radio, VLAN, coax or console, or a management port such as mgmt0), `interfaceSeriesNext($asset_id, $asset_type, $sfp)` ([prefix, start, pad] of the next run, which New interfaces now starts from) and `interfacePortsEnsure($asset_id, $asset_type, $copper, $sfp)` (makes the missing ones, counting live ports, never taking an archived one's name). `INTERFACE_PORT_MAP_TYPES` adds Modem/ONT, NAS and NVR/DVR. +- API: `assets/create` and `assets/update` take `asset_specs` ({key: value}; on update laid over what the asset has, '' clearing one, and a new `asset_type` keeping only its own) and answer `asset_specs_not_kept` (an array of strings) when some weren't kept; `asset_type` is saved as the list spells it. `assets/read` gives every asset `asset_specs` ({} for none), through `$api_asset_specs` in `api/v1/read_output.php`. +- Nightly: "UPS Battery Due" notifications from a UPS's `battery_replace`, on the warranty alert days (1, 7, 45), linking to the asset - a Support type in `notificationTypeModules()`, drawn with `car-battery`. CSS: `.itflow-asset-tone-storage` and `.itflow-asset-tone-power`, light and dark, and `.itflow-port-break` before a port map's SFP cages. ### Library Updates diff --git a/admin/database_updates/3.4.9.php b/admin/database_updates/3.4.9.php new file mode 100644 index 000000000..4c6b39a53 --- /dev/null +++ b/admin/database_updates/3.4.9.php @@ -0,0 +1,28 @@ += 1; $asset_invoice_id = intval($row['asset_invoice_id']); @@ -425,17 +432,38 @@ if (mysqli_num_rows($sql) == 0) {
-
OS
+
+ + $spec_row) { ?> +
+
+ + +
+ + + +
+ + +
Runs
+
+ $guest_name) { ?> +
+ +
+ + +
Warranty
+
+ + +
+ + Not recorded + +
-
Warranty
-
- - -
- - Not recorded - -
Purchased
old
@@ -489,6 +517,7 @@ if (mysqli_num_rows($sql) == 0) {
Rack
+
Assigned
@@ -501,6 +530,7 @@ if (mysqli_num_rows($sql) == 0) {
+ @@ -604,7 +634,16 @@ if (mysqli_num_rows($sql) == 0) {
- $row) { + interfaceIsSfp($b['interface_type']); + }); + foreach ($interfaces as $interface_id => $row) { + if (!$port_map_sfp && interfaceIsSfp($row['interface_type'])) { + $port_map_sfp = true; + echo ''; + } $link = $interface_links[$interface_id] ?? null; $states = array_filter([$link ? 'is-connected' : '', trim((string) $row['interface_ip']) !== '' ? 'is-addressed' : '', intval($row['interface_primary']) ? 'is-primary' : '']); $tip = implode("\n", array_filter([ diff --git a/agent/assets.php b/agent/assets.php index 7fdfaafd7..868f03ed1 100644 --- a/agent/assets.php +++ b/agent/assets.php @@ -45,15 +45,8 @@ enforceUserPermission('module_support'); $can_edit = lookupUserPermission('module_support') >= 2; $can_delete = lookupUserPermission('module_support') >= 3; -//Asset Type from GET -$asset_type_tabs = [ - 'workstation' => ['label' => 'Workstations', 'icon' => 'fa-desktop', 'query' => "asset_type IN ('Desktop', 'Laptop')"], - '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')"], - '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')"], -]; +// Asset type tabs (functions/asset_type.php) +$asset_type_tabs = assetTypeTabs(); $type_filter = (isset($_GET['type']) && isset($asset_type_tabs[$_GET['type']])) ? $_GET['type'] : ''; $_GET['type'] = $type_filter; $type_query = $type_filter ? $asset_type_tabs[$type_filter]['query'] : "1 = 1"; @@ -111,15 +104,17 @@ $extra_column_options = [ 'Purchase_Date' => 'Purchase date', 'Install_Date' => 'Install date', 'Warranty_Expire' => 'Warranty', + 'Details' => 'Details', ]; $extra_columns = array_values(array_intersect(array_keys($extra_column_options), (array) ($_GET['show_column'] ?? []))); $show_mac = in_array('Mac_Address', $extra_columns, true); $show_purchase = in_array('Purchase_Date', $extra_columns, true); $show_install = in_array('Install_Date', $extra_columns, true); $show_warranty = in_array('Warranty_Expire', $extra_columns, true); +$show_details = in_array('Details', $extra_columns, true); // Assigned To only means something for workstations and the like - printers are shared -$show_contact = !in_array($type_filter, ['network', 'server', 'printer', 'other'], true); +$show_contact = !in_array($type_filter, ['network', 'server', 'virtual', 'power', 'printer', 'other'], true); $show_model = $type_filter !== 'virtual'; // Only these sort - filter_header.php will hand through any lowercase word, and @@ -138,13 +133,9 @@ $order_by = $sort === 'interface_ip' ? "INET6_ATON(interface_ip) $order, interfa // Tab counts - within the client, status, tag and archive scope, before the search narrows it $row = mysqli_fetch_assoc(mysqli_query($mysqli, " SELECT - COUNT(*) AS all_count, - SUM(asset_type IN ('Laptop', 'Desktop')) AS workstation_count, - 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 = 'Printer') AS printer_count, - SUM(asset_type NOT IN ('Laptop', 'Desktop', 'Server', 'Virtual Machine', 'Firewall/Router', 'Switch', 'Access Point', 'Printer')) AS other_count + COUNT(*) AS all_count" . implode('', array_map(function ($tab_key, $tab) { + return ",\n SUM({$tab['query']}) AS {$tab_key}_count"; + }, array_keys($asset_type_tabs), $asset_type_tabs)) . " FROM assets LEFT JOIN clients ON client_id = asset_client_id WHERE $archive_query @@ -153,15 +144,10 @@ $row = mysqli_fetch_assoc(mysqli_query($mysqli, " " . clientScopeSql('asset_client_id') . " $client_query ")); -$tab_counts = [ - '' => intval($row['all_count']), - 'workstation' => intval($row['workstation_count']), - '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']), -]; +$tab_counts = ['' => intval($row['all_count'])]; +foreach ($asset_type_tabs as $tab_key => $tab) { + $tab_counts[$tab_key] = intval($row[$tab_key . '_count']); +} $sql = mysqli_query( $mysqli, @@ -195,6 +181,8 @@ $assets = []; while ($row = mysqli_fetch_assoc($sql)) { $assets[intval($row['asset_id'])] = $row; } +// Each one's details - its type's (functions/asset_type.php): a UPS battery due is flagged like a warranty, and Details lists them +$asset_list_specs = assetSpecsFor(array_keys($assets)); // Tags and rack positions for the whole page in two queries, not two per row $assets_tags = tagsForRecords('asset', array_keys($assets)); @@ -219,7 +207,7 @@ $search_noun = $type_filter ? $asset_type_tabs[$type_filter]['label'] : 'assets'
+ $spec_row) { ?> +
+ + + - + + + +
+
@@ -262,7 +276,7 @@ ob_start();
- +
diff --git a/agent/modals/asset/asset_add.php b/agent/modals/asset/asset_add.php index 7cace027e..d7364020c 100644 --- a/agent/modals/asset/asset_add.php +++ b/agent/modals/asset/asset_add.php @@ -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', 'printer' => 'Printer']; +$type_presets = ['workstation' => 'Desktop', 'server' => 'Server', 'virtual' => 'Virtual Machine', 'network' => 'Switch', 'power' => 'UPS', 'printer' => 'Printer']; $type_param = strtolower((string) ($_GET['type'] ?? '')); $asset_form_mode = 'add'; @@ -28,6 +28,7 @@ $asset_vendor_id = $client_id ? assetClientOwnedId('vendor', $_GET['vendor_id'] $asset_location_id = $client_id ? assetClientOwnedId('location', $_GET['location_id'] ?? 0, $client_id) : 0; $asset_contact_id = intval($_GET['contact_id'] ?? 0); $asset_tag_id_array = []; +$asset_specs = []; ob_start(); diff --git a/agent/modals/asset/asset_copy.php b/agent/modals/asset/asset_copy.php index 08fc45513..d24bf2579 100644 --- a/agent/modals/asset/asset_copy.php +++ b/agent/modals/asset/asset_copy.php @@ -53,6 +53,11 @@ $asset_favorite = 0; $device_icon = getAssetIcon($row['asset_type']); +// Its details come along, bar the ones that belong to that one unit - an IMEI, a circuit +$asset_specs = array_filter(assetSpecs($source_asset_id), function ($key) { + return empty(ASSET_SPECS[$key]['unit_only']); +}, ARRAY_FILTER_USE_KEY); + // Tags come along $asset_tag_id_array = array(); $sql_asset_tags = mysqli_query($mysqli, "SELECT asset_tag_tag_id FROM asset_tags WHERE asset_tag_asset_id = $source_asset_id"); @@ -73,7 +78,7 @@ ob_start();
diff --git a/agent/modals/asset/asset_form.php b/agent/modals/asset/asset_form.php index 18c1cb2f4..6888aaa85 100644 --- a/agent/modals/asset/asset_form.php +++ b/agent/modals/asset/asset_form.php @@ -11,7 +11,11 @@ * 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. + * 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. */ @@ -68,6 +72,31 @@ while ($row = mysqli_fetch_assoc($sql)) { $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 @@ -138,7 +167,10 @@ $asset_form_icons = [
@@ -200,15 +232,15 @@ $asset_form_icons = [ -
+
-
- +
+
@@ -216,6 +248,75 @@ $asset_form_icons = [
+ +
Details
+
+ $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); ?> +
> + +
+ + + + + + + + + + + min="" max="" step="" inputmode="decimal" + max="2999-12-31" + maxlength="255" + + > + + + + +
+ + + + + + + + +
+ + Has now. A new count adds the ones missing to its interfaces - none are taken away. + + Added to its interfaces when it's saved. + +
+ +
Flagged on its page as the date nears, like a warranty.
+ +
+ +
+

Asset ID

@@ -245,7 +346,7 @@ $asset_form_icons = [
-
+
@@ -278,7 +379,7 @@ $asset_form_icons = [

The primary interface. Add more ports from the asset's Interfaces tab.

-
+
@@ -292,7 +393,7 @@ $asset_form_icons = [
-
+
@@ -305,21 +406,21 @@ $asset_form_icons = [
-
+
-
+
-
+
@@ -328,29 +429,31 @@ $asset_form_icons = [
-
Links
-
-
- -
- - +
+
Links
+
+
+ +
+ + +
-
-
- -
- - +
+ +
+ + +
-
-
- -
- - +
+ +
+ + +
+
Shown to the client in their portal.
-
Shown to the client in their portal.
@@ -359,7 +462,7 @@ $asset_form_icons = [
-
+
@@ -373,14 +476,14 @@ $asset_form_icons = [
-
+
-
+
@@ -394,7 +497,7 @@ $asset_form_icons = [
-
+
@@ -546,6 +649,71 @@ $asset_form_icons = [ }); }); + // 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 = ; + 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'); diff --git a/agent/modals/asset/asset_import.php b/agent/modals/asset/asset_import.php index 643695478..a5f2752bd 100644 --- a/agent/modals/asset/asset_import.php +++ b/agent/modals/asset/asset_import.php @@ -18,6 +18,7 @@ ob_start();