diff --git a/CHANGELOG.md b/CHANGELOG.md
index f3b28ecbf..7f063402a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,9 +6,9 @@ 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.3.5) 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.3.6) in the same pass. Nothing else is required.
-This is a big release, with fifty-six database steps in one go, so take a backup before you update.
+This is a big release, with fifty-seven database steps in one go, so take a backup before you update.
The update also clears out data that deletes in older versions left behind, all of it pointing at something that no longer exists:
@@ -51,7 +51,7 @@ Document templates keep their names, summaries, what they say and their images.
License templates keep everything they hold. A license called exactly what one license template is called - as the New License form named it when started from one - counts as made from that template, so a template's use starts out counted; where two templates share a name, neither is given its licenses. Other licenses don't know a template, so they count from the update. No template sets seats, a renewal term, a vendor or a product until you give it one, and none is required of anyone until you set it up on its page. Maintenance > Starter Content has a new License Templates pack to add.
-Vendor templates keep everything they hold, and the vendors made from them stay linked to them. Who made each and last changed it is taken from the audit log, where it has a record of it. A vendor made from one that has since been changed by hand shows as out of step with it, and nothing on it changes until you bring it in line. Vendors made by hand stay unlinked - a template's page lists the ones that look like it, to link. No template is required of anyone until you set it up on its page.
+Vendor templates keep everything they hold, and the vendors made from them stay linked to them. Who made each and last changed it is taken from the audit log, where it has a record of it. A vendor made from one that has since been changed by hand shows as out of step with it, and nothing on it changes until you bring it in line. Vendors made by hand stay unlinked - a template's page lists the ones that look like it, to link. No template is required of anyone until you set it up on its page. Vendors and their templates have a status page and a support portal from this version, empty until you fill them in, and templates keep the account manager's line and email as vendors do. The starter vendor templates you added from Maintenance > Starter Content stay as they are; Update on that page gives them their support site, status page, portal and what support asks for.
The app logs are kept for as many days as the audit log, as before, and each can now be set on its own in Admin > Settings > Logs, which is where the log retention went from Settings > Security. A retention of 0 now keeps a log forever (see Breaking Changes, Logs). Nothing is sent to a syslog server or SIEM until you set one up there.
@@ -592,6 +592,11 @@ If you are coming from 26.08 or earlier, the database update still has to be run
- The list has Required and Out of step tabs, and shows how many of the clients a required one is for have it, and how many of its vendors are out of step.
- A client's vendor says which fields differ from its template, with Update from template, and has Unlink from template in its menu. One made by hand that looks like a template - the same name or support site - offers Link to template.
- A client's Vendors list the vendors it should have and doesn't, each a click from New Vendor started from its template, and its overview counts them as Vendors missing.
+ - A status page and a support portal on vendors and vendor templates - where the vendor posts outages, and where cases are opened. A vendor's page has Status page beside Support site, and its quick look, the vendors list and the tickets it is on link its status page, to see at a glance whether it is them. Vendors made from a template are kept in step with both.
+ - A template keeps the account manager's line and email too, for a rep every client shares. A vendor made from it starts with them, but they are never changed on it afterwards - nor its account number or PIN.
+ - Export and Import on Admin > Vendor Templates, and Export on a template's page and menu - a file to copy templates to another ITFlow, keep or share. Never an account number or PIN, and the account manager only when asked. A template already here by name can be left alone, added as well or replaced - replaced, it keeps its account number, PIN and who should have it, and the vendors made from it that still said what it did change with it.
+ - Maintenance > Starter Content: the starter vendor templates come with their support site, status page and the portal cases are opened in where they have one, and what support asks for - the tenant, the service tag, the serial number. Update brings the ones added before up to date: only those fields change, and the vendors made from them that still said what they did change with them. Phone numbers, hours and SLAs differ by region and contract, so they stay yours.
+ - A license template's file carries its vendor's status page and portal.
- Templates can be copied, archived - not offered on New Vendor or Add to Clients, while the vendors made from them stay - restored and deleted. Deleted, its vendors and the license templates that used it stay, no longer linked to it.
- Make a template from it, in the menu of a client's vendor for an admin, starts a template from that vendor - leaving its account number, PIN and account manager, which are the client's own - and links the vendor to it, so other clients can have the same one.
- A vendor made from a template links to it from its page, for an admin.
@@ -1123,6 +1128,7 @@ If you are coming from 26.08 or earlier, the database update still has to be run
- License templates: the type picker showed a stray "<" at the top.
- Vendor templates: Delete was only offered to the built-in Administrator role, not to other admin roles.
- Vendor templates: Add to clients skipped a client whose vendor from the template had been archived, as if it still had it.
+- Vendors: a support site saved with a scheme other than http or https is no longer made a link.
- App Logs: the category filter did nothing - it read a misspelt field - and the date filter was slow on a large log.
- App logs: an entry over 1,000 characters with an accented letter or a symbol where it was cut could fail to save, stopping what was logging it.
- Admin > Settings > Defaults: Payment Method and Expense Payment Method listed the payment method categories that moved to Admin > Payment Methods in 2.3.1, so a new install had nothing to pick. They list Admin > Payment Methods now.
@@ -1664,6 +1670,11 @@ If you are coming from 26.08 or earlier, the database update still has to be run
- `admin/post/vendor_template.php`: `save_vendor_template_required` (`vendor_template_id`, `required`, `tags[]`), logged as Vendor Template Edit. New `admin/modals/vendor_template/vendor_template_required.php` (`id`). `admin/vendor_template.php` takes `view` (`out` or `account`) and loads `js/bulk_actions.js` (`vendor_ids[]`); `admin/vendor_templates.php` takes `show=required` and `show=out`.
- `agent/post/vendor.php`: `update_vendors_from_template` (`vendor_template_id`, `vendor_ids[]`, `fields[]` - keys of `VENDOR_TEMPLATE_SHARED`), `link_vendors_to_template` (`vendor_template_id`, `vendor_ids[]`, `bring`; not to an archived template) and GET `unlink_vendor_template`, each only for vendors the agent may edit (`vendorsForRequest()`), logged as Vendor Edit on each vendor with its client. `add_vendor_template_to_clients` counts only a current vendor from the template as having it. New `agent/modals/vendor/vendor_template_update.php` (`vendor_id`, or `template_id` and `vendor_ids[]`); `vendor_template_clients.php` takes `client_ids[]` and `missing=1` to start with clients picked.
- `agent/vendors.php` with `client_id` shows `vendorTemplatesMissingFor()` (`.itflow-vendors-missing`), and `clientOverviewAttention()` adds Vendors missing for agents with `module_client`.
+ - Database 3.3.6: `vendors.vendor_status_page` and `vendor_portal`, and `vendor_templates.vendor_template_status_page` and `vendor_template_portal` (after the website), and `vendor_template_contact_phone_country_code`, `vendor_template_contact_phone` and `vendor_template_contact_email` (after the contact name), each checked first.
+ - `vendorTemplateFieldLabels()` and `vendorTemplateColumns()` have `status_page`, `portal`, `contact_phone` (country code and number) and `contact_email`; `VENDOR_TEMPLATE_SHARED` adds `status_page` and `portal`. `vendorTemplateFields()` reads them (a bad `contact_email` is refused), `vendorTemplateRowFields()` is built from the columns and takes a missing row as blank, and `vendorTemplateValueHtml()` links the sites. `vendorTemplateFormFields()` and `vendorFormSet()` (`status_page`, `portal`) carry them to vendors; the website, status page and portal are saved without their scheme or a trailing slash. `vendorWebsiteUrl()` returns '' for a scheme other than http or https.
+ - New `functions/vendor_template_transfer.php`: `VENDOR_TEMPLATE_FILE_FORMAT` (`itflow-vendor-templates`), `VENDOR_TEMPLATE_FILE_VERSION` (1), `VENDOR_TEMPLATE_FILE_FIELDS` and `VENDOR_TEMPLATE_FILE_MANAGER_FIELDS`, `vendorTemplatesExport($ids, $archived, $manager)`, `vendorTemplatesExportFileName()`, `vendorTemplateFileValue()`, `vendorTemplatesImportRead($contents, &$error)` (up to 1000), `vendorTemplatesImport($templates, $existing, $only_existing, $only_fields)` (`added`, `replaced`, `skipped`, `ids`, `followed`), `vendorTemplatesImportLogFollowed()`, `vendorTemplatesImportFollowedCount()` and `vendorTemplatesImportSummary()`. `admin/post/vendor_template.php`: `export_vendor_templates` (`vendor_template_ids[]`, `archived`, `manager`) and GET `export_vendor_template`, and `import_vendor_templates` (`file`, `existing` - `skip`, `copy` or `replace`), logged as Vendor Template Export and Import, and Vendor Edit on each vendor that followed. New `admin/modals/vendor_template/vendor_template_export.php` and `vendor_template_import.php`.
+ - `SOFTWARE_TEMPLATE_FILE_VENDOR_FIELDS` adds `status_page` and `portal`. The starter vendor templates (`starterContentVendorTemplates()`) are in the file's shape and load through the import; `update_starter_vendor_templates` (`starterUpdateVendorTemplates()`) replaces only `description`, `website`, `status_page`, `portal` and `notes`, logged as Starter Content Edit.
+ - API: `vendors/create.php` and `update.php` take `vendor_status_page` and `vendor_portal`. The vendors export has Status Page and Support Portal columns (off by default), and the client export's vendors have them.
- Empty states: every list page sets `$empty_state` before including `includes/filter_footer.php`, and `includes/inc_empty_state.php` draws it with `emptyStateHtml()` (`functions/format.php`) when the list has nothing - `thing`, `icon`, `title`, `text`, `actions` (`label`, `modal` or `href`, `size`, `icon`; the first is the main one), `archived`, `view`, `filtered`, `views`, `match_title`, `match_text` and `clear_url`. `emptyStateFiltered($views)` reads the GET string for a search or filter, leaving out paging, sorting, `client_id`, `archived` and the page's views, and `emptyStateClearUrl($views)` is the page without them. `emptyStatePill($pills, $key, $default, $good)` builds a pill's own state from the page's pill array (`label`, `icon`, `title`, `alarm`), with a tick for `alarm` pills and the keys in `$good`. Files, notifications and racks call `emptyStateHtml()` in their own layout; files and notifications set `$empty_state_skip`. A page that sets nothing still gets one, worded from `$page_title`. The pages' own empty blocks (`.itflow-asset-empty` under a list) are gone. CSS: `.itflow-empty-state` with `-icon` (a grey round tile), `-title`, `-text` and `-actions`.
- Removed unused helpers and files:
- `credentialLink()`, `projectLink()`, `linkCounts()`, `assetContactAvatar()`, `clientOverviewPersonInitials()`, and `getMonthlyTax()` and `getQuarterlyTax()`, which looked a tax up by name
diff --git a/admin/database_updates/3.3.6.php b/admin/database_updates/3.3.6.php
new file mode 100644
index 000000000..31d5c36ba
--- /dev/null
+++ b/admin/database_updates/3.3.6.php
@@ -0,0 +1,31 @@
+ $from_vendor['vendor_sla'],
'email' => $from_vendor['vendor_email'],
'website' => $from_vendor['vendor_website'],
+ 'status_page' => $from_vendor['vendor_status_page'],
+ 'portal' => $from_vendor['vendor_portal'],
'notes' => $from_vendor['vendor_notes'],
];
}
diff --git a/admin/modals/vendor_template/vendor_template_edit.php b/admin/modals/vendor_template/vendor_template_edit.php
index 85f575240..ed01be10c 100644
--- a/admin/modals/vendor_template/vendor_template_edit.php
+++ b/admin/modals/vendor_template/vendor_template_edit.php
@@ -2,7 +2,8 @@
/*
* Edit a vendor template - the fields are vendor_template_form.php, with
- * the fields ticked changed on the vendors made from it too.
+ * the vendors made from it following what changes, where they still said
+ * what it did (vendorTemplateFollow()).
*/
require_once '../../includes/modal_header.php';
diff --git a/admin/modals/vendor_template/vendor_template_export.php b/admin/modals/vendor_template/vendor_template_export.php
new file mode 100644
index 000000000..a5001d584
--- /dev/null
+++ b/admin/modals/vendor_template/vendor_template_export.php
@@ -0,0 +1,61 @@
+
+
+
+
+
+{$counts['replaced']} starter vendor templates up to date" . ($followed ? " - $followed vendor" . ($followed === 1 ? '' : 's') . " made from them changed with them" : ''));
+ } else {
+ flashAlert("None of the starter vendor templates are here to update - add them instead", 'info');
+ }
+
+ redirect();
+
+}
+
if (isset($_POST['update_starter_license_templates'])) {
// The starter license templates already here, brought up to this version
diff --git a/admin/post/starter_content_model.php b/admin/post/starter_content_model.php
index 3cd321b82..780f6ff88 100644
--- a/admin/post/starter_content_model.php
+++ b/admin/post/starter_content_model.php
@@ -59,7 +59,11 @@ function starterContentPacks() {
'vendor_templates' => [
'label' => 'Vendor Templates',
'icon' => 'fa-building',
- 'description' => 'Vendors most clients end up with. Account numbers and support numbers are left blank.',
+ 'description' => "Vendors most clients end up with - each with its support site, status page and the portal cases are opened in, and what their support asks for. Phone numbers, hours and SLAs differ by region and contract, and account numbers and PINs by account, so they are yours to add.",
+ 'update' => [
+ 'button' => 'update_starter_vendor_templates',
+ 'confirm' => 'Bring the %d starter vendor templates you have up to date? What they are, their support site, status page, portal and notes are replaced with this version - changes you made to those are lost, and the vendors made from them that still said what they did change with them. Their support line, hours, SLA, email, account details and who should have them stay as they are.',
+ ],
],
'document_templates' => [
'label' => 'Document Templates',
@@ -390,27 +394,32 @@ function starterLoadProjectTemplates($mysqli, $dry_run = false) {
// ------------------------------
// starterLoadVendorTemplates
+// Through the vendor template import. One whose name a template here already
+// has, archived or not, is left alone.
// ------------------------------
function starterLoadVendorTemplates($mysqli, $dry_run = false) {
- $existing = starterExistingNames($mysqli, 'vendor_templates', 'vendor_template_name');
- $added = 0;
-
- foreach (starterContentVendorTemplates() as $vendor_template) {
- if (isset($existing[mb_strtolower($vendor_template[0])])) {
- continue;
- }
- $added++;
- if ($dry_run) {
- continue;
- }
- starterInsert($mysqli, 'vendor_templates', [
- 'vendor_template_name' => $vendor_template[0],
- 'vendor_template_description' => $vendor_template[1],
- 'vendor_template_website' => $vendor_template[2],
- ]);
+ $templates = vendorTemplatesImportRead(['format' => VENDOR_TEMPLATE_FILE_FORMAT, 'version' => VENDOR_TEMPLATE_FILE_VERSION, 'templates' => starterContentVendorTemplates()]);
+ if ($dry_run) {
+ $existing = starterExistingNames($mysqli, 'vendor_templates', 'vendor_template_name');
+ return count(array_filter($templates, function ($template) use ($existing) {
+ return !isset($existing[mb_strtolower($template['name'])]);
+ }));
}
+ return vendorTemplatesImport($templates, 'skip')['added'];
+}
- return $added;
+// ------------------------------
+// starterUpdateVendorTemplates
+// The starter vendor templates already here, brought up to this library's
+// version - only what this library says: what they are, their support site,
+// status page, portal and notes. Their support line, hours, SLA, email,
+// account details and who should have them stay; the vendors made from them
+// that still said what they did change with them (vendorTemplateFollow()).
+// Returns the import's counts (vendorTemplatesImport()).
+// ------------------------------
+function starterUpdateVendorTemplates() {
+ $templates = vendorTemplatesImportRead(['format' => VENDOR_TEMPLATE_FILE_FORMAT, 'version' => VENDOR_TEMPLATE_FILE_VERSION, 'templates' => starterContentVendorTemplates()]);
+ return vendorTemplatesImport($templates, 'replace', true, ['description', 'website', 'status_page', 'portal', 'notes']);
}
// ------------------------------
@@ -1446,50 +1455,91 @@ function starterContentProjectTemplates() {
// ------------------------------
// starterContentVendorTemplates
-// name, description, website. Phone and account fields differ per region and
-// per account, so they are left for whoever adds the vendor to a client.
+// In the vendor templates file's shape (functions/vendor_template_transfer.php):
+// name, what they are, their support site, status page and the portal cases
+// are opened in where they have one, and what support asks for. Phone
+// numbers, hours and SLAs differ by region and by contract, and account
+// numbers and PINs per account, so they are left for whoever adds the vendor
+// to a client.
// ------------------------------
function starterContentVendorTemplates() {
- $vendor_templates = [
- ['Microsoft', 'Cloud, productivity and operating system vendor', 'https://www.microsoft.com'],
- ['Google', 'Workspace, cloud and domain services', 'https://workspace.google.com'],
- ['Amazon Web Services', 'Cloud infrastructure and hosting', 'https://aws.amazon.com'],
- ['Dell', 'Workstation, laptop and server hardware', 'https://www.dell.com'],
- ['HP', 'Workstation, laptop and printer hardware', 'https://www.hp.com'],
- ['Lenovo', 'Workstation and laptop hardware', 'https://www.lenovo.com'],
- ['Supermicro', 'Server and storage hardware', 'https://www.supermicro.com'],
- ['Apple', 'Workstation, laptop and mobile hardware', 'https://www.apple.com'],
- ['Synology', 'Network attached storage and backup appliances', 'https://www.synology.com'],
- ['QNAP', 'Network attached storage appliances', 'https://www.qnap.com'],
- ['Ubiquiti', 'Networking, wireless and surveillance hardware', 'https://www.ui.com'],
- ['MikroTik', 'Routing and switching hardware', 'https://mikrotik.com'],
- ['Cisco', 'Networking, wireless and collaboration hardware', 'https://www.cisco.com'],
- ['Fortinet', 'Firewall and network security appliances', 'https://www.fortinet.com'],
- ['SonicWall', 'Firewall and network security appliances', 'https://www.sonicwall.com'],
- ['APC by Schneider Electric', 'Uninterruptible power supplies and power distribution', 'https://www.apc.com'],
- ['Brother', 'Printer and scanner hardware', 'https://www.brother.com'],
- ['Ingram Micro', 'Hardware and software distribution', 'https://www.ingrammicro.com'],
- ['TD SYNNEX', 'Hardware and software distribution', 'https://www.tdsynnex.com'],
- ['Veeam', 'Backup, replication and recovery software', 'https://www.veeam.com'],
- ['Acronis', 'Backup and cyber protection software', 'https://www.acronis.com'],
- ['Backblaze', 'Cloud backup and object storage', 'https://www.backblaze.com'],
- ['Wasabi', 'Cloud object storage', 'https://wasabi.com'],
- ['Bitdefender', 'Endpoint protection and security software', 'https://www.bitdefender.com'],
- ['ESET', 'Endpoint protection and security software', 'https://www.eset.com'],
- ['Proofpoint', 'Email security and filtering', 'https://www.proofpoint.com'],
- ['Bitwarden', 'Password management', 'https://bitwarden.com'],
- ['Cloudflare', 'DNS, content delivery and security', 'https://www.cloudflare.com'],
- ['Namecheap', 'Domain registration and hosting', 'https://www.namecheap.com'],
- ['GoDaddy', 'Domain registration and hosting', 'https://www.godaddy.com'],
- ['Adobe', 'Creative and document software', 'https://www.adobe.com'],
- ['Intuit', 'Accounting and payroll software', 'https://www.intuit.com'],
- ['Zoom', 'Video conferencing and collaboration', 'https://zoom.us'],
- ['RingCentral', 'Hosted voice and unified communications', 'https://www.ringcentral.com'],
- ['Stripe', 'Card and online payment processing', 'https://stripe.com'],
- ];
+ $template = function ($name, $description, $website, $status_page = '', $portal = '', $notes = '') {
+ return ['name' => $name, 'description' => $description, 'website' => $website, 'status_page' => $status_page, 'portal' => $portal, 'notes' => $notes];
+ };
- return $vendor_templates;
+ return [
+ $template('Microsoft', 'Cloud, productivity and operating system vendor', 'support.microsoft.com', 'status.cloud.microsoft', 'admin.microsoft.com',
+ "Open a service request from the Microsoft 365 admin center (Support > Help & support), signed in as an admin of the client's tenant - or from Partner Center as their partner.\n\nHave the tenant's domain and the users affected ready. Service health in the admin center shows what Microsoft already knows about; the status page is for when the admin center itself is down."),
+ $template('Google', 'Workspace, cloud and domain services', 'support.google.com/a', 'www.google.com/appsstatus/dashboard', 'admin.google.com',
+ "Contact support from the Admin console (Help > Contact support), signed in as a super admin of the client's Workspace.\n\nHave the primary domain and the customer ID ready - Account > Account settings in the Admin console."),
+ $template('Amazon Web Services', 'Cloud infrastructure and hosting', 'aws.amazon.com/premiumsupport', 'health.aws.amazon.com/health/status', 'console.aws.amazon.com/support',
+ "Open a case from the Support Center in the console of the client's AWS account. How fast they answer depends on its support plan - Basic covers account and billing questions only.\n\nHave the account ID and the region ready."),
+ $template('Dell', 'Workstation, laptop and server hardware', 'www.dell.com/support', '', 'techdirect.dell.com',
+ "Have the service tag ready - it decides the warranty and what support covers. Partners open cases and dispatch parts from TechDirect."),
+ $template('HP', 'Workstation, laptop and printer hardware', 'support.hp.com', '', '',
+ "Have the serial number and product number ready - on the label, or from HP Support Assistant."),
+ $template('Lenovo', 'Workstation and laptop hardware', 'support.lenovo.com', '', '',
+ "Have the machine type and serial number ready - on the label, or from Lenovo Vantage."),
+ $template('Supermicro', 'Server and storage hardware', 'www.supermicro.com', '', '',
+ "Have the system or motherboard serial number ready, and the BMC and BIOS versions."),
+ $template('Apple', 'Workstation, laptop and mobile hardware', 'support.apple.com', 'www.apple.com/support/systemstatus', '',
+ "Have the device's serial number ready. The system status page covers iCloud, the App Store and Apple's other services."),
+ $template('Synology', 'Network attached storage and backup appliances', 'www.synology.com', '', 'account.synology.com',
+ "Open a ticket from the Synology Account the device is registered to. Have the serial number and DSM version ready - they usually ask for the debug logs from Support Center on the device."),
+ $template('QNAP', 'Network attached storage appliances', 'www.qnap.com', '', '',
+ "Have the serial number and QTS or QuTS version ready."),
+ $template('Ubiquiti', 'Networking, wireless and surveillance hardware', 'help.ui.com', 'status.ui.com', '',
+ "Support is by chat and ticket from the help center, signed in with the UI account the devices are on. Have the device's MAC address and firmware version ready."),
+ $template('MikroTik', 'Routing and switching hardware', 'help.mikrotik.com', '', '',
+ "Attach a supout.rif from the device to the ticket - Make Supout.rif in WinBox, or /system sup-output - and say which RouterOS version it runs."),
+ $template('Cisco', 'Networking, wireless and collaboration hardware', 'www.cisco.com', '', '',
+ "A case needs a support contract on the device - have the serial number and contract number ready."),
+ $template('Fortinet', 'Firewall and network security appliances', 'www.fortinet.com', '', 'support.fortinet.com',
+ "Devices are registered, and tickets opened, in FortiCare. Have the serial number ready."),
+ $template('SonicWall', 'Firewall and network security appliances', 'www.sonicwall.com/support', '', 'www.mysonicwall.com',
+ "Appliances are registered, and cases opened, in MySonicWall. Have the serial number ready."),
+ $template('APC by Schneider Electric', 'Uninterruptible power supplies and power distribution', 'www.apc.com', '', '',
+ "Have the model and serial number ready, and when the batteries were last replaced."),
+ $template('Brother', 'Printer and scanner hardware', 'support.brother.com', '', '',
+ "Have the model and serial number ready."),
+ $template('Ingram Micro', 'Hardware and software distribution', 'www.ingrammicro.com', '', '',
+ "Your reseller account - quotes, orders and returns go through it and your account rep."),
+ $template('TD SYNNEX', 'Hardware and software distribution', 'www.tdsynnex.com', '', '',
+ "Your reseller account - quotes, orders and returns go through it and your account rep."),
+ $template('Veeam', 'Backup, replication and recovery software', 'www.veeam.com/support.html', '', 'my.veeam.com',
+ "Open a case in My Veeam with the license or support ID, and attach the logs the console exports for support."),
+ $template('Acronis', 'Backup and cyber protection software', 'www.acronis.com', '', '',
+ "Have the account or tenant name ready, and the agent version."),
+ $template('Backblaze', 'Cloud backup and object storage', 'help.backblaze.com', 'status.backblaze.com', 'secure.backblaze.com',
+ "Have the account's email address ready, and the bucket or computer the case is about."),
+ $template('Wasabi', 'Cloud object storage', 'docs.wasabi.com', '', 'console.wasabisys.com',
+ "Have the account, the bucket and its region ready."),
+ $template('Bitdefender', 'Endpoint protection and security software', 'www.bitdefender.com', '', '',
+ "Have the GravityZone company the client is under ready, and the endpoint's name."),
+ $template('ESET', 'Endpoint protection and security software', 'www.eset.com', '', '',
+ "Have the license or ESET business account ready, and the product version."),
+ $template('Proofpoint', 'Email security and filtering', 'www.proofpoint.com', '', '',
+ "Have the organization or domain ready, and the message ID of the email the case is about."),
+ $template('Bitwarden', 'Password management', 'bitwarden.com/help', 'status.bitwarden.com', 'vault.bitwarden.com',
+ "Have the organization's name and the email address of the account the case is about ready."),
+ $template('Cloudflare', 'DNS, content delivery and security', 'developers.cloudflare.com', 'www.cloudflarestatus.com', 'dash.cloudflare.com',
+ "Cases are opened from the dashboard of the client's Cloudflare account - what support is offered depends on its plan. Have the domain ready."),
+ $template('Namecheap', 'Domain registration and hosting', 'www.namecheap.com/support', 'www.namecheap.com/status-updates', '',
+ "Have the domain and the account's username ready."),
+ $template('GoDaddy', 'Domain registration and hosting', 'www.godaddy.com/help', '', '',
+ "Have the customer number and support PIN ready - support asks for them before it helps."),
+ $template('Adobe', 'Creative and document software', 'helpx.adobe.com', 'status.adobe.com', 'adminconsole.adobe.com',
+ "Admins open cases from the Admin Console (Support). Have the organization and the user's email address ready."),
+ $template('Intuit', 'Accounting and payroll software', 'www.intuit.com', '', '',
+ "Have the product, the company file or account, and the license number for desktop products ready."),
+ $template('Zoom', 'Video conferencing and collaboration', 'support.zoom.com', 'status.zoom.us', '',
+ "Have the account number ready, and the meeting ID the case is about."),
+ $template('RingCentral', 'Hosted voice and unified communications', 'support.ringcentral.com', '', 'service.ringcentral.com',
+ "Have the account's main number ready - admins manage it in the admin portal."),
+ $template('Stripe', 'Card and online payment processing', 'support.stripe.com', 'status.stripe.com', 'dashboard.stripe.com',
+ "Support is from the Dashboard of the client's Stripe account. Have the account ID (acct_...) ready, and the payment or payout the case is about."),
+ ];
}
@@ -1916,7 +1966,7 @@ function starterContentLicenseTemplates() {
$vendors = [];
foreach (starterContentVendorTemplates() as $vendor) {
- $vendors[$vendor[0]] = ['name' => $vendor[0], 'description' => $vendor[1], 'website' => $vendor[2]];
+ $vendors[$vendor['name']] = $vendor;
}
$template = function ($name, $description, $type, $license_type, $term, $vendor, $product, array $notes) use ($vendors) {
return [
diff --git a/admin/post/vendor_template.php b/admin/post/vendor_template.php
index b5910b69a..c4b659e09 100644
--- a/admin/post/vendor_template.php
+++ b/admin/post/vendor_template.php
@@ -3,8 +3,9 @@
/*
* Vendor templates (Admin > Vendor Templates) - making, changing (the
* vendors made from it that still said what it did changing with it),
- * copying, archiving, restoring and deleting them, and saying who should have
- * one. The work is in functions/vendor_template.php; a vendor is made from
+ * copying, archiving, restoring and deleting them, saying who should have
+ * one, and taking them to and from a file (functions/vendor_template_transfer.php).
+ * The work is in functions/vendor_template.php; a vendor is made from
* one by add_vendor, add_vendor_from_template and
* add_vendor_template_to_clients, and brought in line with one, linked to one
* or unlinked by update_vendors_from_template, link_vendors_to_template and
@@ -152,3 +153,68 @@ if (isset($_POST['save_vendor_template_required'])) {
redirect();
}
+
+if (isset($_POST['export_vendor_templates']) || isset($_GET['export_vendor_template'])) {
+
+ // A file of templates to import into another ITFlow - every current one, the ones picked, or one
+ validateCSRFToken();
+
+ if (isset($_GET['export_vendor_template'])) {
+ $row = vendorTemplateForRequest($_GET['export_vendor_template']);
+ $export = vendorTemplatesExport([intval($row['vendor_template_id'])]);
+ } else {
+ $export = vendorTemplatesExport((array) ($_POST['vendor_template_ids'] ?? []), !empty($_POST['archived']), !empty($_POST['manager']));
+ }
+ if (!$export['templates']) {
+ flashAlert('There are no vendor templates to export.', 'error');
+ redirect();
+ }
+
+ $count = count($export['templates']);
+ logAudit("Vendor Template", "Export", "$session_name exported $count vendor template" . ($count === 1 ? ' ' . escapeSql($export['templates'][0]['name']) : 's')
+ . (!empty($_POST['manager']) ? ' with their account managers' : ''));
+
+ header('Content-Type: application/json; charset=utf-8');
+ header('Content-Disposition: attachment; filename="' . vendorTemplatesExportFileName($export) . '"');
+ header('Cache-Control: no-store');
+ echo json_encode($export, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
+ exit;
+
+}
+
+if (isset($_POST['import_vendor_templates'])) {
+
+ // Templates from a file exported by ITFlow
+ validateCSRFToken();
+
+ $file = $_FILES['file'] ?? null;
+ if ($file && in_array($file['error'], [UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_FORM_SIZE], true)) {
+ flashAlert('That file is bigger than this server takes in an upload - export a few at a time.', 'error');
+ redirect('vendor_templates.php');
+ }
+ if (!$file || $file['error'] !== UPLOAD_ERR_OK || !is_uploaded_file($file['tmp_name'])) {
+ flashAlert('Pick the file to import.', 'error');
+ redirect('vendor_templates.php');
+ }
+ if ($file['size'] > 10 * 1024 * 1024) {
+ flashAlert('That file is too big to be vendor templates.', 'error');
+ redirect('vendor_templates.php');
+ }
+ $templates = vendorTemplatesImportRead(file_get_contents($file['tmp_name']), $import_error);
+ if ($templates === null) {
+ flashAlert(escapeHtml($import_error), 'error');
+ redirect('vendor_templates.php');
+ }
+ $existing = in_array($_POST['existing'] ?? '', ['skip', 'copy', 'replace'], true) ? $_POST['existing'] : 'skip';
+ $counts = vendorTemplatesImport($templates, $existing);
+ vendorTemplatesImportLogFollowed($counts);
+ $followed = vendorTemplatesImportFollowedCount($counts);
+
+ logAudit("Vendor Template", "Import", "$session_name imported vendor templates from " . escapeSql(mb_substr((string) $file['name'], 0, 100)) . " - {$counts['added']} added, {$counts['replaced']} replaced, {$counts['skipped']} skipped"
+ . ($followed ? ", $followed vendors changed with them" : ''));
+
+ flashAlert(vendorTemplatesImportSummary($counts), $counts['added'] || $counts['replaced'] ? 'success' : 'info');
+
+ redirect('vendor_templates.php');
+
+}
diff --git a/admin/starter_content.php b/admin/starter_content.php
index a217cd759..dcac32240 100644
--- a/admin/starter_content.php
+++ b/admin/starter_content.php
@@ -43,8 +43,8 @@ foreach ($starter_content_status as $pack_status) {
Adding never overwrites or deletes anything. Anything already present under the same name is
skipped, so a pack can be added again later to pick up entries you removed by mistake, and
adding twice never produces duplicates. Everything added is editable and deletable
- afterwards like any other record. Ticket templates, canned responses, document templates and license templates added
- from an earlier version can be brought up to this one with Update - that one does replace what they hold.
+ afterwards like any other record. Ticket templates, canned responses, vendor templates, document templates and license
+ templates added from an earlier version can be brought up to this one with Update - that one does replace what they hold.
diff --git a/admin/vendor_template.php b/admin/vendor_template.php
index b40340039..b3b40d00e 100644
--- a/admin/vendor_template.php
+++ b/admin/vendor_template.php
@@ -2,9 +2,9 @@
/*
* One vendor template - what a vendor made from it starts with (its name,
- * what they are, the support line, hours, SLA, support email and site, any
- * account number, PIN and account manager every client shares, and its
- * notes), who made it and last changed it, and its use: the clients with a
+ * what they are, the support line, hours, SLA, support email and site, its
+ * status page and support portal, any account number, PIN and account
+ * manager every client shares, and its notes), who made it and last changed it, and its use: the clients with a
* vendor made from it - each with its own account number and PIN, or not yet,
* and in step with it or saying something else (views, and bringing one or
* several in line) - the vendors made by hand that look like it (linking
@@ -89,17 +89,28 @@ $in_line_url = "$update_url?template_id=$vendor_template_id";
// What a vendor from it starts with - the ones it has
$t = $template;
-$website = vendorWebsiteLabel($t['vendor_template_website']);
+$link = function ($site) {
+ $url = vendorWebsiteUrl($site);
+ return $url !== '' ? '' . escapeHtml(vendorWebsiteLabel($site)) . '' : '';
+};
+// The account manager every client shares - their name, with their line and email under it
+$manager = array_filter([
+ escapeHtml(trim((string) $t['vendor_template_contact_name'])),
+ trim((string) $t['vendor_template_contact_phone']) !== '' ? '' . contactPhoneLink($t['vendor_template_contact_phone'], $t['vendor_template_contact_phone_country_code']) : '',
+ trim((string) $t['vendor_template_contact_email']) !== '' ? '' . escapeHtml($t['vendor_template_contact_email']) . '' : '',
+]);
$facts = array_filter([
'What they are' => escapeHtml((string) $t['vendor_template_description']),
'Support line' => vendorTemplatePhoneHtml($t['vendor_template_phone'], $t['vendor_template_phone_country_code'], $t['vendor_template_extension']),
'Support hours' => escapeHtml((string) $t['vendor_template_hours']),
'SLA' => escapeHtml((string) $t['vendor_template_sla']),
'Support email' => trim((string) $t['vendor_template_email']) !== '' ? '' . escapeHtml($t['vendor_template_email']) . '' : '',
- 'Support site' => $website !== '' ? '' . escapeHtml($website) . '' : '',
+ 'Support site' => $link($t['vendor_template_website']),
+ 'Status page' => $link($t['vendor_template_status_page']),
+ 'Support portal' => $link($t['vendor_template_portal']),
'Account number' => escapeHtml(trim((string) $t['vendor_template_account_number'])),
'PIN' => vendorSecretHtml($t['vendor_template_code']),
- 'Account manager' => escapeHtml(trim((string) $t['vendor_template_contact_name'])),
+ 'Account manager' => $manager ? '
No way to reach their support yet - give it their support line, email or site, so every client's vendor has it.
+
+
No way to reach their support yet - give it their support line, email, site or portal, so every client's vendor has it.
diff --git a/admin/vendor_templates.php b/admin/vendor_templates.php
index bc2420a91..029e6346e 100644
--- a/admin/vendor_templates.php
+++ b/admin/vendor_templates.php
@@ -10,7 +10,8 @@
* step with it (the Required and Out of step tabs). Each opens its page
* (vendor_template.php). A template can be added to several clients at once,
* changed, copied as the start of another, archived - not offered for new
- * vendors, while the vendors made from it stay - or deleted. The work is in
+ * vendors, while the vendors made from it stay - or deleted, and they go to
+ * and from a file (Export and Import). The work is in
* functions/vendor_template.php; the handlers are
* admin/post/vendor_template.php.
*/
@@ -60,12 +61,13 @@ if (!isset($sortable_columns[$sort])) {
$phone_search = preg_replace('/[^0-9]/', '', (string) ($_GET['q'] ?? ''));
$search = "(vendor_template_name LIKE '%$q%' OR vendor_template_description LIKE '%$q%' OR vendor_template_website LIKE '%$q%' OR vendor_template_email LIKE '%$q%'
+ OR vendor_template_status_page LIKE '%$q%' OR vendor_template_portal LIKE '%$q%' OR vendor_template_contact_email LIKE '%$q%'
OR vendor_template_hours LIKE '%$q%' OR vendor_template_sla LIKE '%$q%' OR vendor_template_contact_name LIKE '%$q%' OR vendor_template_account_number LIKE '%$q%'
OR vendor_template_notes LIKE '%$q%'" . ($phone_search !== '' ? " OR vendor_template_phone LIKE '%$phone_search%'" : '') . ")";
// Its use is the current vendors made from it for current clients - as vendorTemplateUsage() counts them
$sql = mysqli_query($mysqli, "SELECT SQL_CALC_FOUND_ROWS vendor_templates.vendor_template_id, vendor_template_name, vendor_template_description, vendor_template_phone_country_code,
- vendor_template_phone, vendor_template_extension, vendor_template_email, vendor_template_website, vendor_template_hours, vendor_template_sla,
+ vendor_template_phone, vendor_template_extension, vendor_template_email, vendor_template_website, vendor_template_status_page, vendor_template_hours, vendor_template_sla,
vendor_template_created_at, vendor_template_updated_at, vendor_template_archived_at, vendor_template_required,
COALESCE(vendor_template_updated_at, vendor_template_created_at) AS changed_at, COALESCE(updater.user_name, creator.user_name) AS changed_by,
COALESCE(used.used_count, 0) AS used_count, used.last_used
@@ -112,6 +114,8 @@ $pill_url = function ($show) {
$add_url = 'modals/vendor_template/vendor_template_add.php';
$clients_url = '/agent/modals/vendor/vendor_template_clients.php';
+$import_url = 'modals/vendor_template/vendor_template_import.php';
+$export_url = 'modals/vendor_template/vendor_template_export.php';
$csrf = $_SESSION['csrf_token'];
?>
@@ -128,6 +132,13 @@ $csrf = $_SESSION['csrf_token'];
Add one to clients
+
+ Import
+
+
+ Export
+
+
Starter vendor templates
@@ -141,7 +152,7 @@ $csrf = $_SESSION['csrf_token'];
diff --git a/agent/modals/vendor/vendor_add.php b/agent/modals/vendor/vendor_add.php
index 5fca9b300..6d8206def 100644
--- a/agent/modals/vendor/vendor_add.php
+++ b/agent/modals/vendor/vendor_add.php
@@ -21,7 +21,7 @@ $vendor_form_mode = 'add';
$vendor_id = 0;
$vendor_name = $vendor_description = $vendor_account_number = $vendor_code = '';
$vendor_contact_name = $vendor_contact_phone_country_code = $vendor_contact_phone = $vendor_contact_email = '';
-$vendor_phone_country_code = $vendor_phone = $vendor_extension = $vendor_email = $vendor_website = '';
+$vendor_phone_country_code = $vendor_phone = $vendor_extension = $vendor_email = $vendor_website = $vendor_status_page = $vendor_portal = '';
$vendor_hours = $vendor_sla = $vendor_notes = '';
$vendor_favorite = 0;
$vendor_template_id = 0;
diff --git a/agent/modals/vendor/vendor_edit.php b/agent/modals/vendor/vendor_edit.php
index e2a987ff9..f7d46816a 100644
--- a/agent/modals/vendor/vendor_edit.php
+++ b/agent/modals/vendor/vendor_edit.php
@@ -26,6 +26,8 @@ $vendor_phone = escapeHtml(formatPhoneNumber($vendor['vendor_phone'], $vendor['v
$vendor_extension = escapeHtml($vendor['vendor_extension']);
$vendor_email = escapeHtml($vendor['vendor_email']);
$vendor_website = escapeHtml($vendor['vendor_website']);
+$vendor_status_page = escapeHtml($vendor['vendor_status_page']);
+$vendor_portal = escapeHtml($vendor['vendor_portal']);
$vendor_hours = escapeHtml($vendor['vendor_hours']);
$vendor_sla = escapeHtml($vendor['vendor_sla']);
$vendor_notes = escapeHtml($vendor['vendor_notes']);
diff --git a/agent/modals/vendor/vendor_form.php b/agent/modals/vendor/vendor_form.php
index efb7fdf76..9621fe2dd 100644
--- a/agent/modals/vendor/vendor_form.php
+++ b/agent/modals/vendor/vendor_form.php
@@ -8,7 +8,8 @@
* (functions/vendor.php) reads.
*
* Tabs: Details (name, star, what they are, tags, template), Support (the
- * number to ring, hours, SLA, support email and site), Account (the account
+ * number to ring, hours, SLA, support email and site, the status page and
+ * the portal cases are opened in), Account (the account
* number and PIN they ask for, the account manager) and Notes. A new vendor
* can start from a template, which fills every field in.
*
@@ -185,12 +186,28 @@ $form_id = 'vendorForm' . ($vendor_form_mode === 'edit' ? $vendor_id : 'New');
-
+
-
Link the portal login itself from the vendor's Logins tab.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Where they post outages, and where you sign in to open a case - link the login itself from the vendor's Logins tab.
@@ -302,10 +319,11 @@ $form_id = 'vendorForm' . ($vendor_form_mode === 'edit' ? $vendor_id : 'New');
if (!fields) {
return;
}
- ['name', 'description', 'account_number', 'code', 'contact_name', 'email', 'website', 'hours', 'sla', 'notes', 'extension'].forEach(function (name) {
+ ['name', 'description', 'account_number', 'code', 'contact_name', 'contact_email', 'email', 'website', 'status_page', 'portal', 'hours', 'sla', 'notes', 'extension'].forEach(function (name) {
setField(name, fields[name]);
});
setPhone('phone', fields.phone_country_code, fields.phone);
+ setPhone('contact_phone', fields.contact_phone_country_code, fields.contact_phone);
});
});
diff --git a/agent/post/vendor.php b/agent/post/vendor.php
index e22723721..d7a48e9f4 100644
--- a/agent/post/vendor.php
+++ b/agent/post/vendor.php
@@ -482,7 +482,7 @@ if (isExportRequest('export_vendors')) {
LEFT JOIN clients ON client_id = vendor_client_id
WHERE $archive_query
AND (vendor_name LIKE '%$q%' OR vendor_description LIKE '%$q%' OR vendor_account_number LIKE '%$q%' OR vendor_website LIKE '%$q%'
- OR vendor_contact_name LIKE '%$q%' OR vendor_email LIKE '%$q%' OR vendor_contact_email LIKE '%$q%' OR vendor_phone LIKE '%$q%' OR vendor_notes LIKE '%$q%')
+ OR vendor_status_page LIKE '%$q%' OR vendor_portal LIKE '%$q%' OR vendor_contact_name LIKE '%$q%' OR vendor_email LIKE '%$q%' OR vendor_contact_email LIKE '%$q%' OR vendor_phone LIKE '%$q%' OR vendor_notes LIKE '%$q%')
$client_query
$tag_query
" . clientScopeSql('vendor_client_id') . "
diff --git a/agent/ticket.php b/agent/ticket.php
index aed9e4e0d..b9f9b9006 100644
--- a/agent/ticket.php
+++ b/agent/ticket.php
@@ -1562,6 +1562,11 @@ if (isset($_GET['ticket_id'])) {