diff --git a/admin/database_updates/2.8.2.php b/admin/database_updates/2.8.2.php
new file mode 100644
index 000000000..7f4c02465
--- /dev/null
+++ b/admin/database_updates/2.8.2.php
@@ -0,0 +1,36 @@
+ 0 AND `vendor_id` IS NULL");
+ mysqli_query($mysqli, "UPDATE `software` LEFT JOIN `vendors` ON `vendor_id` = `software_vendor_id` SET `software_vendor_id` = 0 WHERE `software_vendor_id` > 0 AND `vendor_id` IS NULL");
+ mysqli_query($mysqli, "UPDATE `tickets` LEFT JOIN `vendors` ON `vendor_id` = `ticket_vendor_id` SET `ticket_vendor_id` = 0 WHERE `ticket_vendor_id` > 0 AND `vendor_id` IS NULL");
+ foreach (['domain_registrar', 'domain_webhost', 'domain_dnshost', 'domain_mailhost'] as $domain_vendor_column) {
+ mysqli_query($mysqli, "UPDATE `domains` LEFT JOIN `vendors` ON `vendor_id` = `$domain_vendor_column` SET `$domain_vendor_column` = 0 WHERE `$domain_vendor_column` > 0 AND `vendor_id` IS NULL");
+ }
diff --git a/admin/modals/tag/tag_add.php b/admin/modals/tag/tag_add.php
index 4d8913bdb..473f61791 100644
--- a/admin/modals/tag/tag_add.php
+++ b/admin/modals/tag/tag_add.php
@@ -17,6 +17,8 @@ if (isset($_GET['type'])) {
$type_display = "Credential";
} elseif ($type === 5) {
$type_display = "Asset";
+ } elseif ($type === 6) {
+ $type_display = "Vendor";
}
}
ob_start();
diff --git a/admin/modals/tag/tag_edit.php b/admin/modals/tag/tag_edit.php
index 22c74ffac..f7cfc8a5d 100644
--- a/admin/modals/tag/tag_edit.php
+++ b/admin/modals/tag/tag_edit.php
@@ -22,6 +22,8 @@ if ($tag_type == 1) {
$tag_type_display = "Credential";
} elseif ( $tag_type == 5) {
$tag_type_display = "Asset";
+} elseif ($tag_type == 6) {
+ $tag_type_display = "Vendor";
} else {
$tag_type_display = "Unknown";
}
diff --git a/admin/post/demo_data_model.php b/admin/post/demo_data_model.php
index d1361fbe5..dd116be30 100644
--- a/admin/post/demo_data_model.php
+++ b/admin/post/demo_data_model.php
@@ -410,6 +410,57 @@ function demoAttachTags($mysqli, $join_table, $join_column, $tag_column, $row_id
}
}
+// ------------------------------
+// demoVendorTags / demoVendorManager
+// What kind of vendor each demo vendor is, as Tags starter pack vendor tags,
+// and the account manager who looks after the account - a name, a direct
+// line and an email at the vendor's domain, the same every time it is seeded.
+// ------------------------------
+function demoVendorTags($name, $description, $notes = '', $ours = false) {
+ if ($ours) {
+ $map = [
+ 'Distribution' => ['Distributor', 'Hardware'], 'Netgate' => ['Hardware', 'Security'], 'Ubiquiti' => ['Hardware'],
+ 'Synology' => ['Hardware'], 'Proxmox' => ['Software'], 'Nextcloud' => ['Cloud', 'Hosting'], 'Licensing' => ['Cloud', 'Distributor'],
+ 'Colocation' => ['Hosting'], 'Bank' => ['Professional Services'], 'CPA' => ['Professional Services'],
+ 'Insurance' => ['Professional Services'], 'Property' => ['Professional Services'],
+ ];
+ foreach ($map as $match => $tags) {
+ if (stripos($name, $match) !== false) {
+ return $tags;
+ }
+ }
+ return [];
+ }
+ $text = "$name $description";
+ $tags = [];
+ if (preg_match('/internet|fibre|fiber|circuit|broadband|connect|wireless|cable/i', $text)) {
+ $tags[] = 'ISP';
+ }
+ if (preg_match('/phone|telecom|voice/i', $text)) {
+ $tags[] = 'Telecom';
+ }
+ if (!$tags) {
+ $tags[] = 'Line of Business';
+ }
+ if (stripos($notes, 'authorised list') !== false) {
+ $tags[] = 'Authorised Contacts Only';
+ }
+ return $tags;
+}
+
+function demoVendorManager($name, $phone, $domain) {
+ $people = ['Dana Whitlock', 'Marcus Feld', 'Priya Raman', 'Owen Castell', 'Jolene Avery', 'Theo Marsh', 'Rosa Delgado', 'Ian Kowalski', 'Hannah Pryce', 'Victor Lund'];
+ $person = $people[crc32($name) % count($people)];
+ $digits = preg_replace('/[^0-9]/', '', (string) $phone);
+ $direct = $digits !== '' ? substr($digits, 0, -2) . str_pad((string) ((intval(substr($digits, -2)) + 37) % 100), 2, '0', STR_PAD_LEFT) : '';
+ return [
+ 'vendor_contact_name' => $person,
+ 'vendor_contact_phone_country_code' => '1',
+ 'vendor_contact_phone' => $direct,
+ 'vendor_contact_email' => strtolower(str_replace(' ', '.', $person)) . '@' . $domain,
+ ];
+}
+
// ------------------------------
// demoLink
// The relationship joins are all two integer columns. Linking assets to the
@@ -475,10 +526,9 @@ function demoEnsureOrgVendors($mysqli) {
$vendors[$vendor[0]] = intval($row['vendor_id']);
continue;
}
- $vendors[$vendor[0]] = starterInsert($mysqli, 'vendors', [
+ $vendors[$vendor[0]] = starterInsert($mysqli, 'vendors', demoVendorManager($vendor[0], $vendor[2], $vendor[3]) + [
'vendor_name' => $vendor[0],
'vendor_description' => $vendor[1],
- 'vendor_contact_name' => 'Account Manager',
'vendor_phone_country_code' => '1',
'vendor_phone' => $vendor[2],
'vendor_extension' => (string)(200 + (crc32($vendor[0]) % 90)),
@@ -492,6 +542,7 @@ function demoEnsureOrgVendors($mysqli) {
'vendor_client_id' => 0,
'vendor_created_at' => demoMonthDateTime(24, 2, 9, 0),
]);
+ demoAttachTags($mysqli, 'vendor_tags', 'vendor_id', 'tag_id', $vendors[$vendor[0]], 6, demoVendorTags($vendor[0], $vendor[1], '', true));
}
return $vendors;
@@ -1170,10 +1221,9 @@ function demoBuildClient($mysqli, $profile, $index, $context, &$counts) {
// The client's own vendors - who they buy their line of business kit from
$vendor_ids = [];
foreach ($profile['vendors'] as $vendor_index => $vendor) {
- $vendor_ids[] = starterInsert($mysqli, 'vendors', [
+ $vendor_id = starterInsert($mysqli, 'vendors', demoVendorManager($vendor[0], $vendor[2], $vendor[3]) + [
'vendor_name' => $vendor[0],
'vendor_description' => $vendor[1],
- 'vendor_contact_name' => 'Business Support',
'vendor_phone_country_code' => '1',
'vendor_phone' => $vendor[2],
'vendor_email' => 'support@' . $vendor[3],
@@ -1183,9 +1233,12 @@ function demoBuildClient($mysqli, $profile, $index, $context, &$counts) {
'vendor_code' => demoSerial($profile['abbreviation'] . $vendor[0], 6),
'vendor_account_number' => demoSerial($profile['abbreviation'] . 'acct' . $vendor_index, 8),
'vendor_notes' => $vendor[5],
+ 'vendor_favorite' => $vendor_index === 0 ? 1 : 0,
'vendor_client_id' => $client_id,
'vendor_created_at' => demoMonthDateTime($months, 5, 13, 0),
]);
+ demoAttachTags($mysqli, 'vendor_tags', 'vendor_id', 'tag_id', $vendor_id, 6, demoVendorTags($vendor[0], $vendor[1], $vendor[5]));
+ $vendor_ids[] = $vendor_id;
$counts['documentation']++;
}
$primary_vendor_id = $vendor_ids[0] ?? 0;
diff --git a/admin/post/starter_content_model.php b/admin/post/starter_content_model.php
index 96a406f7d..e578e67bb 100644
--- a/admin/post/starter_content_model.php
+++ b/admin/post/starter_content_model.php
@@ -30,7 +30,7 @@ function starterContentPacks() {
'tags' => [
'label' => 'Tags',
'icon' => 'fa-tags',
- 'description' => 'Client, location, contact, credential and asset tags.',
+ 'description' => 'Client, location, contact, credential, asset and vendor tags.',
],
'ticket_templates' => [
'label' => 'Ticket Templates',
@@ -661,6 +661,20 @@ function starterContentTags() {
[5, 'Leased', '#6c757d', 'file-contract'],
[5, 'Spare Stock', '#795548', 'boxes'],
[5, 'Patch Excluded', '#ffc107', 'ban'],
+
+ // Vendor
+ [6, 'ISP', '#007bff', 'wifi'],
+ [6, 'Telecom', '#28a745', 'phone-alt'],
+ [6, 'Line of Business', '#6f42c1', 'cubes'],
+ [6, 'Software', '#20c997', 'window-restore'],
+ [6, 'Hardware', '#001f3f', 'server'],
+ [6, 'Cloud', '#0078d4', 'cloud'],
+ [6, 'Security', '#dc3545', 'shield-alt'],
+ [6, 'Distributor', '#795548', 'truck'],
+ [6, 'Printing', '#6c757d', 'print'],
+ [6, 'Hosting', '#17a2b8', 'globe'],
+ [6, 'Professional Services', '#343a40', 'briefcase'],
+ [6, 'Authorised Contacts Only', '#d81b60', 'user-lock'],
];
return $tags;
diff --git a/admin/post/vendor_template.php b/admin/post/vendor_template.php
index fb4777d7d..edbca17b9 100644
--- a/admin/post/vendor_template.php
+++ b/admin/post/vendor_template.php
@@ -25,7 +25,7 @@ if (isset($_POST['add_vendor_template'])) {
$code = escapeSql($_POST['code']);
$notes = escapeSql($_POST['notes']);
- mysqli_query($mysqli,"INSERT INTO vendor_templates SET vendor_template_name = '$name', vendor_template_description = '$description', vendor_template_contact_name = '$contact_name', vendor_template_phone = '$phone', vendor_template_extension = '$extension', vendor_template_email = '$email', vendor_template_website = '$website', vendor_template_hours = '$hours', vendor_template_sla = '$sla', vendor_template_code = '$code', vendor_template_account_number = '$account_number', vendor_template_notes = '$notes'");
+ mysqli_query($mysqli,"INSERT INTO vendor_templates SET vendor_template_name = '$name', vendor_template_description = '$description', vendor_template_contact_name = '$contact_name', vendor_template_phone_country_code = '$phone_country_code', vendor_template_phone = '$phone', vendor_template_extension = '$extension', vendor_template_email = '$email', vendor_template_website = '$website', vendor_template_hours = '$hours', vendor_template_sla = '$sla', vendor_template_code = '$code', vendor_template_account_number = '$account_number', vendor_template_notes = '$notes'");
$vendor_template_id = mysqli_insert_id($mysqli);
@@ -132,7 +132,10 @@ if (isset($_POST['edit_vendor_template'])) {
// Remove the first comma to prevent MySQL error
$sql = preg_replace('/,/', '', $sql, 1);
- mysqli_query($mysqli,"UPDATE vendors SET $sql WHERE vendor_template_id = $vendor_template_id");
+ // Nothing ticked to push - nothing to update, rather than an empty SET
+ if (trim($sql) !== '') {
+ mysqli_query($mysqli,"UPDATE vendors SET $sql WHERE vendor_template_id = $vendor_template_id");
+ }
}
logAudit("Vendor Template", "Edit", "$session_name edited vendor template $name", 0, $vendor_template_id);
diff --git a/admin/tags.php b/admin/tags.php
index e3a8ce76c..8d2320d86 100644
--- a/admin/tags.php
+++ b/admin/tags.php
@@ -19,6 +19,7 @@ $tag_types = [
3 => ['label' => 'Contact', 'icon' => 'fa-address-book'],
4 => ['label' => 'Credential', 'icon' => 'fa-key'],
5 => ['label' => 'Asset', 'icon' => 'fa-desktop'],
+ 6 => ['label' => 'Vendor', 'icon' => 'fa-building'],
];
// Label for the selected type
diff --git a/admin/vendor_templates.php b/admin/vendor_templates.php
index 0b44072d6..46a905c63 100644
--- a/admin/vendor_templates.php
+++ b/admin/vendor_templates.php
@@ -17,6 +17,13 @@ $sql = mysqli_query(
$num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
+// How many clients have a vendor made from each template
+$template_use = [];
+$sql_template_use = mysqli_query($mysqli, "SELECT vendor_template_id, COUNT(DISTINCT vendor_client_id) FROM vendors WHERE vendor_template_id > 0 AND vendor_client_id > 0 AND vendor_archived_at IS NULL GROUP BY vendor_template_id");
+while ($use_row = mysqli_fetch_row($sql_template_use)) {
+ $template_use[intval($use_row[0])] = intval($use_row[1]);
+}
+
?>
@@ -59,6 +66,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
Contact |
+
Used by |
Action |
@@ -95,7 +103,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
- = $vendor_template_name ?>
+ = vendorTile($row['vendor_template_name'], 'sm') ?>= $vendor_template_name ?>
+ |
+
+
+ = $used_by ?> client= $used_by === 1 ? '' : 's' ?>
+
+ -
+
+ |
|
-
= $vendor_name ?> |
+
= $expense_vendor_id ? vendorLink($expense_vendor_id, $vendor_name, ['page' => true]) : '-' ?> |
= numfmt_format_currency($currency_format, $expense_amount, $expense_currency_code) ?> |
= $account_name ?> |
= $client_name ?> |
diff --git a/agent/includes/client_overview_side_nav.php b/agent/includes/client_overview_side_nav.php
index eb544a2c6..868611b08 100644
--- a/agent/includes/client_overview_side_nav.php
+++ b/agent/includes/client_overview_side_nav.php
@@ -31,6 +31,9 @@ $num_certificates = $row['num'];
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT('software_id') AS num FROM software LEFT JOIN clients ON software_client_id = client_id WHERE software_archived_at IS NULL AND client_archived_at IS NULL " . clientScopeSql('software_client_id') . ""));
$num_software = $row['num'];
+$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT('vendor_id') AS num FROM vendors JOIN clients ON vendor_client_id = client_id WHERE vendor_archived_at IS NULL AND client_archived_at IS NULL " . clientScopeSql('vendor_client_id') . ""));
+$num_vendors = $row['num'];
+
?>
@@ -173,6 +176,19 @@ $num_software = $row['num'];
+
+ ">
+
+
+ Vendors
+ 0) { ?>
+ = $num_vendors ?>
+
+
+
+
+
">
diff --git a/agent/includes/client_side_nav.php b/agent/includes/client_side_nav.php
index 845a55364..eac3e08d8 100644
--- a/agent/includes/client_side_nav.php
+++ b/agent/includes/client_side_nav.php
@@ -84,7 +84,7 @@
- ">
+ ">
Locations
@@ -97,7 +97,7 @@
- ">
+ ">
Vendors
diff --git a/agent/includes/side_nav.php b/agent/includes/side_nav.php
index a03565380..43c6b76af 100644
--- a/agent/includes/side_nav.php
+++ b/agent/includes/side_nav.php
@@ -74,7 +74,7 @@
- ">
+ ">
Vendors
diff --git a/agent/js/tickets_add_modal.js b/agent/js/tickets_add_modal.js
index 9f3625c9f..ea0a1faea 100644
--- a/agent/js/tickets_add_modal.js
+++ b/agent/js/tickets_add_modal.js
@@ -324,7 +324,15 @@
// Populate dropdown
vendors.forEach(vendor => {
- vendorSelectDropdown[vendorSelectDropdown.length] = new Option(vendor.vendor_name, vendor.vendor_id);
+ const option = new Option(vendor.vendor_name, vendor.vendor_id);
+ // The vendor's tile and what they are, as on the vendor pages
+ option.dataset.initials = vendor.vendor_initials || '';
+ option.dataset.tone = vendor.vendor_tone || 'secondary';
+ option.dataset.soft = '1';
+ if (vendor.vendor_description) {
+ option.dataset.description = vendor.vendor_description;
+ }
+ vendorSelectDropdown[vendorSelectDropdown.length] = option;
});
applyPreselection(vendorSelectDropdown);
diff --git a/agent/license.php b/agent/license.php
index f54d07956..979345277 100644
--- a/agent/license.php
+++ b/agent/license.php
@@ -265,11 +265,12 @@ if (!$license || ($client_url && intval($license['software_client_id']) !== intv
$vendor_phone = contactPhoneLink($license['vendor_phone'], $license['vendor_phone_country_code'], $license['vendor_extension']);
$vendor_email = escapeHtml($license['vendor_email']);
$vendor_website = escapeHtml($license['vendor_website']);
- $vendor_account = escapeHtml($license['vendor_account_number']); ?>
+ $vendor_account = escapeHtml($license['vendor_account_number']);
+ $vendor_pin = vendorSecretHtml($license['vendor_code']); ?>
@@ -277,6 +278,9 @@ if (!$license || ($client_url && intval($license['software_client_id']) !== intv
- Account
- = $vendor_account ?>
+
+ - PIN
- = $vendor_pin ?>
+
- Phone
- = $vendor_phone ?>
@@ -285,7 +289,7 @@ if (!$license || ($client_url && intval($license['software_client_id']) !== intv
- = $vendor_email ?>
- - Website
- = $vendor_website ?>
+ - Website
- = escapeHtml(vendorWebsiteLabel($license['vendor_website'])) ?>
- Hours
- = escapeHtml($license['vendor_hours']) ?>
diff --git a/agent/modals/asset/asset_add.php b/agent/modals/asset/asset_add.php
index 76322934d..7cace027e 100644
--- a/agent/modals/asset/asset_add.php
+++ b/agent/modals/asset/asset_add.php
@@ -21,7 +21,9 @@ $asset_ip = $asset_ipv6 = $asset_nat_ip = $asset_mac = $asset_uri = $asset_uri_2
$asset_status = $asset_purchase_reference = $asset_purchase_date = $asset_install_date = $asset_warranty_expire = '';
$asset_physical_location = $asset_notes = $asset_photo = '';
$asset_favorite = 0;
-$asset_vendor_id = $asset_network_id = 0;
+$asset_network_id = 0;
+// Opened from a vendor's page - bought from them
+$asset_vendor_id = $client_id ? assetClientOwnedId('vendor', $_GET['vendor_id'] ?? 0, $client_id) : 0;
// Opened from a location's page - start at that location
$asset_location_id = $client_id ? assetClientOwnedId('location', $_GET['location_id'] ?? 0, $client_id) : 0;
$asset_contact_id = intval($_GET['contact_id'] ?? 0);
diff --git a/agent/modals/asset/asset_form.php b/agent/modals/asset/asset_form.php
index d1c492983..5a5efde36 100644
--- a/agent/modals/asset/asset_form.php
+++ b/agent/modals/asset/asset_form.php
@@ -53,9 +53,9 @@ if ($client_id) {
while ($row = mysqli_fetch_assoc($sql)) {
$network_options[] = $row;
}
- $sql = mysqli_query($mysqli, "SELECT vendor_archived_at, vendor_id, vendor_name FROM vendors
+ $sql = mysqli_query($mysqli, "SELECT vendor_archived_at, vendor_id, vendor_name, vendor_description FROM vendors
WHERE vendor_client_id = $client_id AND (vendor_archived_at IS NULL OR vendor_id = $asset_vendor_id)
- ORDER BY vendor_name ASC");
+ ORDER BY vendor_favorite DESC, vendor_name ASC");
while ($row = mysqli_fetch_assoc($sql)) {
$vendor_options[] = $row;
}
@@ -384,7 +384,7 @@ $asset_form_icons = [
-
+
diff --git a/agent/modals/credential/credential_add.php b/agent/modals/credential/credential_add.php
index b2d0fcf92..8ff05da0e 100644
--- a/agent/modals/credential/credential_add.php
+++ b/agent/modals/credential/credential_add.php
@@ -5,16 +5,20 @@ require_once '../../../includes/modal_header.php';
$client_id = intval($_GET['client_id'] ?? 0);
$contact_id = intval($_GET['contact_id'] ?? 0);
$asset_id = intval($_GET['asset_id'] ?? 0);
+// Opened from a vendor's page - their portal login, linked to them
+$vendor_id = $client_id ? assetClientOwnedId('vendor', $_GET['vendor_id'] ?? 0, $client_id) : 0;
+$vendor_name = $vendor_id ? getFieldById('vendors', $vendor_id, 'vendor_name') : '';
ob_start();
?>
@@ -123,12 +123,12 @@ ob_start();
-
+
@@ -142,12 +142,12 @@ ob_start();
-
+
diff --git a/agent/modals/domain/domain_edit.php b/agent/modals/domain/domain_edit.php
index d9f8780e6..5a6f034ba 100644
--- a/agent/modals/domain/domain_edit.php
+++ b/agent/modals/domain/domain_edit.php
@@ -92,12 +92,12 @@ ob_start();