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()")); - + + + + + client + + - + + 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(); ?>
+ @@ -104,12 +104,12 @@ 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(); - + @@ -132,12 +132,12 @@ ob_start(); - + diff --git a/agent/modals/expense/expense_add.php b/agent/modals/expense/expense_add.php index a7f2a5486..e0b5def43 100644 --- a/agent/modals/expense/expense_add.php +++ b/agent/modals/expense/expense_add.php @@ -3,6 +3,8 @@ require_once '../../../includes/modal_header.php'; $client_id = intval($_GET['client_id'] ?? 0); +// Opened from one of our vendors' pages - that vendor +$prefill_vendor_id = intval($_GET['vendor_id'] ?? 0); ob_start(); @@ -80,12 +82,12 @@ ob_start(); - + - + '$expense_created_at' OR vendor_archived_at IS NULL) ORDER BY vendor_name ASC"); + $sql_select = mysqli_query($mysqli, "SELECT vendor_id, vendor_name, vendor_description FROM vendors WHERE vendor_client_id = 0 AND (vendor_archived_at > '$expense_created_at' OR vendor_archived_at IS NULL OR vendor_id = $expense_vendor_id) ORDER BY vendor_favorite DESC, vendor_name ASC"); while ($row = mysqli_fetch_assoc($sql_select)) { $vendor_id_select = intval($row['vendor_id']); $vendor_name_select = escapeHtml($row['vendor_name']); ?> - + "> + diff --git a/agent/modals/ticket/ticket_add.php b/agent/modals/ticket/ticket_add.php index b27f68edb..7cd3063fb 100644 --- a/agent/modals/ticket/ticket_add.php +++ b/agent/modals/ticket/ticket_add.php @@ -7,6 +7,7 @@ $contact_id = intval($_GET['contact_id'] ?? 0); $project_id = intval($_GET['project_id'] ?? 0); $asset_id = intval($_GET['asset_id'] ?? 0); $location_id = intval($_GET['location_id'] ?? 0); +$vendor_id = intval($_GET['vendor_id'] ?? 0); // A ticket started from somewhere that knows what it is about (a license's // Renewal Ticket) - plain text in, escaped here @@ -247,7 +248,7 @@ ob_start();
-
diff --git a/agent/modals/ticket/ticket_edit.php b/agent/modals/ticket/ticket_edit.php index d47642e8d..4d41c77d0 100644 --- a/agent/modals/ticket/ticket_edit.php +++ b/agent/modals/ticket/ticket_edit.php @@ -306,12 +306,14 @@ ob_start(); - + No vendor - + 'That vendor no longer exists.']); + exit; +} +$client_id = intval($vendor['vendor_client_id']); if ($client_id) { + enforceUserPermission('module_client'); enforceClientAccess(); } -// Licenses bought through this vendor - soonest renewal first -$vendor_licenses = []; -if (lookupUserPermission('module_support') >= 1) { - $sql_vendor_licenses = mysqli_query($mysqli, "SELECT software_id AS id, software_name AS name, software_type, software_version, software_license_type, +$vendor_name = escapeHtml($vendor['vendor_name']); +$vendor_archived = !empty($vendor['vendor_archived_at']); +$vendor_phone = contactPhoneLink($vendor['vendor_phone'], $vendor['vendor_phone_country_code'], $vendor['vendor_extension']); +$vendor_dial = vendorDialNumber($vendor['vendor_phone'], $vendor['vendor_phone_country_code']); +$vendor_email = escapeHtml($vendor['vendor_email']); +$vendor_website_url = vendorWebsiteUrl($vendor['vendor_website']); +$vendor_account = escapeHtml($vendor['vendor_account_number']); +$vendor_pin = vendorSecretHtml($vendor['vendor_code']); +$vendor_notes = escapeHtml($vendor['vendor_notes']); +$manager_phone = contactPhoneLink($vendor['vendor_contact_phone'], $vendor['vendor_contact_phone_country_code']); +$manager_email = escapeHtml($vendor['vendor_contact_email']); + +$can_edit = vendorCan($client_id, 2) && !$vendor_archived; +$can_support = lookupUserPermission('module_support') >= 1; +$can_support_edit = lookupUserPermission('module_support') >= 2; +$can_credentials = lookupUserPermission('module_credential') >= 1; +$can_financial = lookupUserPermission('module_financial') >= 1; + +$cases = $licenses = $assets = $logins = []; +if ($can_support) { + $sql = mysqli_query($mysqli, "SELECT ticket_id, ticket_prefix, ticket_number, ticket_subject, ticket_vendor_ticket_number, ticket_status_name, ticket_status_color + FROM tickets + LEFT JOIN ticket_statuses ON ticket_status_id = ticket_status + WHERE ticket_client_id = $client_id AND ticket_vendor_id = $vendor_id AND ticket_resolved_at IS NULL AND ticket_closed_at IS NULL + ORDER BY ticket_number DESC"); + while ($row = mysqli_fetch_assoc($sql)) { + $cases[] = $row; + } + $sql = mysqli_query($mysqli, "SELECT software_id AS id, software_name AS name, software_type, software_version, software_license_type, software_seats, software_expire, " . softwareSeatsUsedSql('software.software_id') . " AS seats_used FROM software - WHERE software_vendor_id = $vendor_id AND software_archived_at IS NULL " . clientScopeSql('software_client_id') . " + WHERE software_client_id = $client_id AND software_vendor_id = $vendor_id AND software_archived_at IS NULL ORDER BY software_expire IS NULL, software_expire ASC, software_name ASC"); - while ($vendor_license = mysqli_fetch_assoc($sql_vendor_licenses)) { - $vendor_licenses[] = $vendor_license; + while ($row = mysqli_fetch_assoc($sql)) { + $licenses[] = $row; + } + $sql = mysqli_query($mysqli, "SELECT asset_id AS id, asset_name AS name, asset_type, asset_make, asset_model, asset_warranty_expire + FROM assets + WHERE asset_client_id = $client_id AND asset_vendor_id = $vendor_id AND asset_archived_at IS NULL + ORDER BY asset_type ASC, asset_name ASC"); + while ($row = mysqli_fetch_assoc($sql)) { + $assets[] = $row; } } +if ($can_credentials && $client_id) { + $logins = linkedRecords('vendor', $vendor_id, 'credential'); +} +$spend = !$client_id && $can_financial ? vendorSpend($vendor_id) : null; + +$quick_tabs = array_filter([ + 'cases' => ['label' => 'Open cases', 'count' => count($cases)], + 'licenses' => ['label' => 'Licenses', 'count' => count($licenses)], + 'assets' => ['label' => 'Assets', 'count' => count($assets)], + 'logins' => ['label' => 'Logins', 'count' => count($logins)], +], function ($tab) { + return $tab['count'] > 0; +}); +$first_tab = array_key_first($quick_tabs); +$quick_id = "vendor-quick-$vendor_id"; +$page_url = 'vendor.php?' . ($client_id ? "client_id=$client_id&" : '') . "vendor_id=$vendor_id"; ob_start(); ?> -