Starter Content rather than at install time. Every pack is
* idempotent - anything already present by name is skipped - so a pack can be
* loaded on a brand new install or a five year old one and only ever adds what
* is missing. Nothing here updates or deletes an existing row.
*
* The file is deliberately named _model so admin/post.php does not glob it in
* on every admin request. It is required on demand by the page and the handler.
*/
// ------------------------------
// starterContentPacks
// Registry - the key is what the buttons post back, and the only accepted value.
// Order is load order. A pack with a 'requires' still loads on its own, it just
// produces less - products land uncategorised, project templates land with no stages.
// ------------------------------
function starterContentPacks() {
return [
'categories' => [
'label' => 'Categories',
'icon' => 'fa-list-ul',
'description' => 'Expense, income, referral and ticket categories for an MSP. Income categories are also what products are filed under.',
],
'tags' => [
'label' => 'Tags',
'icon' => 'fa-tags',
'description' => 'Client, location, contact, credential and asset tags.',
],
'ticket_templates' => [
'label' => 'Ticket Templates',
'icon' => 'fa-life-ring',
'description' => 'Onboarding, offboarding, deployments, maintenance and incident response, each with its task list.',
],
'project_templates' => [
'label' => 'Project Templates',
'icon' => 'fa-project-diagram',
'description' => 'Common MSP projects, built out of the ticket templates above.',
'requires' => 'ticket_templates',
],
'vendor_templates' => [
'label' => 'Vendor Templates',
'icon' => 'fa-building',
'description' => 'Vendors most clients end up with. Account numbers and support numbers are left blank.',
],
'document_templates' => [
'label' => 'Document Templates',
'icon' => 'fa-file-alt',
'description' => 'Runbooks, build sheets, checklists and plans as fill-in-the-blank skeletons.',
],
'products' => [
'label' => 'Products & Services',
'icon' => 'fa-cubes',
'description' => 'Recurring services, labor rates, project work and hardware lines. Prices are starting points - hardware and resold SKUs come in at zero.',
'requires' => 'categories',
],
];
}
// ------------------------------
// starterInsert
// Builds an "INSERT INTO
SET col = val" from a column => value map.
// Integers pass through unquoted, everything else is escaped and quoted.
// Columns named in $html_columns hold rich text and are escaped without
// strip_tags, matching how the post handlers store TinyMCE content.
// Returns the new row ID.
// ------------------------------
function starterInsert($mysqli, $table, $fields, $html_columns = []) {
$set = [];
foreach ($fields as $column => $value) {
if (is_int($value)) {
$set[] = "$column = $value";
} elseif (in_array($column, $html_columns)) {
$value = mysqli_real_escape_string($mysqli, $value);
$set[] = "$column = '$value'";
} else {
$value = escapeSql($value);
$set[] = "$column = '$value'";
}
}
$set = implode(', ', $set);
mysqli_query($mysqli, "INSERT INTO $table SET $set");
return intval(mysqli_insert_id($mysqli));
}
// ------------------------------
// starterExistingNames
// One query per pack rather than one per row. Keys are lower cased so a pack
// does not re-add a name that differs only in case.
// ------------------------------
function starterExistingNames($mysqli, $table, $name_column, $key_column = null) {
$existing = [];
$columns = $key_column ? "$name_column, $key_column" : $name_column;
$sql = mysqli_query($mysqli, "SELECT $columns FROM $table");
while ($row = mysqli_fetch_assoc($sql)) {
$key = mb_strtolower($row[$name_column]);
if ($key_column) {
$key = $row[$key_column] . '|' . $key;
}
$existing[$key] = true;
}
return $existing;
}
// ------------------------------
// starterCategoryId
// ------------------------------
function starterCategoryId($mysqli, $name, $type) {
$name = escapeSql($name);
$type = escapeSql($type);
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT category_id FROM categories WHERE category_name = '$name' AND category_type = '$type' LIMIT 1"));
return intval($row['category_id'] ?? 0);
}
// ------------------------------
// starterTicketTemplateId
// ------------------------------
function starterTicketTemplateId($mysqli, $name) {
$name = escapeSql($name);
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT ticket_template_id FROM ticket_templates WHERE ticket_template_name = '$name' LIMIT 1"));
return intval($row['ticket_template_id'] ?? 0);
}
// ------------------------------
// starterContentLoad
// Single entry point. $dry_run counts what would be added without touching
// anything, which is what the page uses to show the missing column.
// Returns the number of top level rows added (or that would be added).
// ------------------------------
function starterContentLoad($mysqli, $pack, $dry_run = false) {
switch ($pack) {
case 'categories':
return starterLoadCategories($mysqli, $dry_run);
case 'tags':
return starterLoadTags($mysqli, $dry_run);
case 'ticket_templates':
return starterLoadTicketTemplates($mysqli, $dry_run);
case 'project_templates':
return starterLoadProjectTemplates($mysqli, $dry_run);
case 'vendor_templates':
return starterLoadVendorTemplates($mysqli, $dry_run);
case 'document_templates':
return starterLoadDocumentTemplates($mysqli, $dry_run);
case 'products':
return starterLoadProducts($mysqli, $dry_run);
}
return 0;
}
// ------------------------------
// starterContentStatus
// Per pack totals for the page - how many the library holds, how many are
// already present, and how many loading would add.
// ------------------------------
function starterContentStatus($mysqli) {
$status = [];
foreach (starterContentPacks() as $pack => $details) {
$total = starterContentTotal($pack);
$missing = starterContentLoad($mysqli, $pack, true);
$status[$pack] = [
'total' => $total,
'missing' => $missing,
'present' => $total - $missing,
];
}
return $status;
}
// ------------------------------
// starterContentTotal
// ------------------------------
function starterContentTotal($pack) {
switch ($pack) {
case 'categories':
return count(starterContentCategories());
case 'tags':
return count(starterContentTags());
case 'ticket_templates':
return count(starterContentTicketTemplates());
case 'project_templates':
return count(starterContentProjectTemplates());
case 'vendor_templates':
return count(starterContentVendorTemplates());
case 'document_templates':
return count(starterContentDocumentTemplates());
case 'products':
return count(starterContentProducts());
}
return 0;
}
// ------------------------------
// starterLoadCategories
// ------------------------------
function starterLoadCategories($mysqli, $dry_run = false) {
$existing = starterExistingNames($mysqli, 'categories', 'category_name', 'category_type');
$added = 0;
foreach (starterContentCategories() as $category) {
if (isset($existing[$category['type'] . '|' . mb_strtolower($category['name'])])) {
continue;
}
$added++;
if ($dry_run) {
continue;
}
$fields = [
'category_name' => $category['name'],
'category_type' => $category['type'],
'category_color' => $category['color'],
'category_description' => $category['description'],
];
if ($category['icon']) {
$fields['category_icon'] = $category['icon'];
}
if ($category['order']) {
$fields['category_order'] = $category['order'];
}
starterInsert($mysqli, 'categories', $fields);
}
return $added;
}
// ------------------------------
// starterLoadTags
// ------------------------------
function starterLoadTags($mysqli, $dry_run = false) {
$existing = starterExistingNames($mysqli, 'tags', 'tag_name', 'tag_type');
$added = 0;
foreach (starterContentTags() as $tag) {
if (isset($existing[$tag[0] . '|' . mb_strtolower($tag[1])])) {
continue;
}
$added++;
if ($dry_run) {
continue;
}
starterInsert($mysqli, 'tags', [
'tag_type' => $tag[0],
'tag_name' => $tag[1],
'tag_color' => $tag[2],
'tag_icon' => $tag[3],
]);
}
return $added;
}
// ------------------------------
// starterLoadTicketTemplates
// A template is added whole or not at all - an existing name is left alone
// rather than having its task list topped up.
// ------------------------------
function starterLoadTicketTemplates($mysqli, $dry_run = false) {
$existing = starterExistingNames($mysqli, 'ticket_templates', 'ticket_template_name');
$added = 0;
foreach (starterContentTicketTemplates() as $ticket_template) {
if (isset($existing[mb_strtolower($ticket_template['name'])])) {
continue;
}
$added++;
if ($dry_run) {
continue;
}
$ticket_template_id = starterInsert($mysqli, 'ticket_templates', [
'ticket_template_name' => $ticket_template['name'],
'ticket_template_description' => $ticket_template['description'],
'ticket_template_subject' => $ticket_template['subject'],
'ticket_template_details' => $ticket_template['details'],
], ['ticket_template_details']);
$order = 1;
foreach ($ticket_template['tasks'] as $task) {
starterInsert($mysqli, 'task_templates', [
'task_template_name' => $task[0],
'task_template_order' => $order,
'task_template_completion_estimate' => $task[1],
'task_template_ticket_template_id' => $ticket_template_id,
]);
$order++;
}
}
return $added;
}
// ------------------------------
// starterLoadProjectTemplates
// Links are resolved by ticket template name - one that is not present is
// skipped, so loading this pack before the ticket templates still works and
// simply produces a project template with fewer stages.
// ------------------------------
function starterLoadProjectTemplates($mysqli, $dry_run = false) {
$existing = starterExistingNames($mysqli, 'project_templates', 'project_template_name');
$added = 0;
foreach (starterContentProjectTemplates() as $project_template) {
if (isset($existing[mb_strtolower($project_template['name'])])) {
continue;
}
$added++;
if ($dry_run) {
continue;
}
$project_template_id = starterInsert($mysqli, 'project_templates', [
'project_template_name' => $project_template['name'],
'project_template_description' => $project_template['description'],
]);
$order = 1;
foreach ($project_template['ticket_templates'] as $ticket_template_name) {
$ticket_template_id = starterTicketTemplateId($mysqli, $ticket_template_name);
if ($ticket_template_id) {
starterInsert($mysqli, 'project_template_ticket_templates', [
'project_template_id' => $project_template_id,
'ticket_template_id' => $ticket_template_id,
'ticket_template_order' => $order,
]);
$order++;
}
}
}
return $added;
}
// ------------------------------
// starterLoadVendorTemplates
// ------------------------------
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],
]);
}
return $added;
}
// ------------------------------
// starterLoadDocumentTemplates
// ------------------------------
function starterLoadDocumentTemplates($mysqli, $dry_run = false) {
global $session_user_id;
$existing = starterExistingNames($mysqli, 'document_templates', 'document_template_name');
$added = 0;
foreach (starterContentDocumentTemplates() as $document_template) {
if (isset($existing[mb_strtolower($document_template[0])])) {
continue;
}
$added++;
if ($dry_run) {
continue;
}
starterInsert($mysqli, 'document_templates', [
'document_template_name' => $document_template[0],
'document_template_description' => $document_template[1],
'document_template_content' => $document_template[2],
'document_template_created_by' => intval($session_user_id ?? 0),
], ['document_template_content']);
}
return $added;
}
// ------------------------------
// starterLoadProducts
// Products are filed under income categories. A category that is not present
// leaves the product uncategorised rather than blocking the load, so this pack
// works whether or not the categories pack has been loaded.
// ------------------------------
function starterLoadProducts($mysqli, $dry_run = false) {
global $session_company_currency;
$existing = starterExistingNames($mysqli, 'products', 'product_name');
$added = 0;
$category_ids = [];
foreach (starterContentProducts() as $product) {
if (isset($existing[mb_strtolower($product[0])])) {
continue;
}
$added++;
if ($dry_run) {
continue;
}
if (!isset($category_ids[$product[4]])) {
$category_ids[$product[4]] = starterCategoryId($mysqli, $product[4], 'Income');
}
starterInsert($mysqli, 'products', [
'product_name' => $product[0],
'product_type' => $product[1],
'product_code' => $product[2],
'product_price' => $product[3],
'product_category_id' => $category_ids[$product[4]],
'product_description' => $product[5],
'product_currency_code' => $session_company_currency ?? '',
'product_tax_id' => 0,
]);
}
return $added;
}
// ------------------------------
// starterContentCategories
// Flattened to one row per category so the loader and the counter share a shape.
// ------------------------------
function starterContentCategories() {
$expense_categories = [
['Office Supplies', '#007bff', 'Consumables and general office purchases'],
['Travel', '#6f42c1', 'Airfare, lodging and out of town travel'],
['Advertising', '#fd7e14', 'Marketing, campaigns and sponsorships'],
['Processing Fee', '#6c757d', 'Card and payment gateway processing fees'],
['Shipping and Postage', '#20c997', 'Freight, courier and postage'],
['Software', '#17a2b8', 'Internal software and tooling subscriptions'],
['Bank Fees', '#ffc107', 'Account, wire and merchant service charges'],
['Payroll', '#28a745', 'Wages, payroll taxes and benefits'],
['Professional Services', '#001f3f', 'Legal, accounting and consulting fees'],
['Contractor', '#795548', 'Subcontracted labor and 1099 work'],
['Insurance', '#dc3545', 'General liability, errors and omissions, cyber'],
['Infrastructure', '#3d9970', 'Colocation, racks, power and connectivity'],
['Equipment', '#adb5bd', 'Internal hardware and capital equipment'],
['Education', '#e83e8c', 'Training, certifications and exam fees'],
['Hardware - Cost of Goods', '#343a40', 'Hardware bought for resale to a client'],
['Licensing - Cost of Goods', '#17a2b8', 'Licenses and subscriptions resold to a client'],
['Cloud and Hosting', '#6610f2', 'Cloud compute, storage and hosting spend'],
['Telecom and Internet', '#fd7e14', 'Circuits, mobile plans and phone service'],
['Rent and Utilities', '#795548', 'Office rent, power, water and refuse'],
['Vehicle and Fuel', '#d81b60', 'Fuel, maintenance and vehicle costs'],
['Meals', '#e83e8c', 'Business meals and client entertainment'],
['Tools and Test Equipment', '#6c757d', 'Hand tools, testers and shop equipment'],
['Dues and Subscriptions', '#007bff', 'Memberships, associations and publications'],
['Taxes', '#dc3545', 'Business, sales and franchise taxes'],
['Owner Distribution', '#28a745', 'Owner draw or distribution of profit'],
];
$income_categories = [
['Managed Services', '#28a745', 'Recurring managed service agreements'],
['Consulting', '#007bff', 'Advisory, vCIO and strategy work'],
['Projects', '#6f42c1', 'Fixed scope project work'],
['Hardware Sales', '#adb5bd', 'Resale of hardware and peripherals'],
['Software Sales', '#17a2b8', 'Resale of software and perpetual licenses'],
['Cloud Services', '#6610f2', 'Hosted servers, storage and platforms'],
['Support', '#ffc107', 'Reactive help desk and support labor'],
['Training', '#e83e8c', 'End user and administrator training'],
['Telecom Services', '#fd7e14', 'Voice, SIP and connectivity services'],
['Backup', '#001f3f', 'Backup, replication and disaster recovery'],
['Security', '#dc3545', 'Security tooling, monitoring and response'],
['Licensing', '#20c997', 'Recurring per seat license billing'],
['Monitoring', '#3d9970', 'Remote monitoring and alerting'],
['Labor', '#795548', 'Hourly and after hours labor'],
['Web and Hosting', '#6c757d', 'Websites, domains, certificates and hosting'],
['Onboarding', '#d81b60', 'One off onboarding and setup fees'],
['Reimbursable Expenses', '#343a40', 'Pass through costs rebilled to a client'],
['Late Fees', '#dc3545', 'Interest and late payment charges'],
];
$referral_categories = [
['Friend', '#007bff', 'Personal recommendation'],
['Search', '#fd7e14', 'Found through a search engine'],
['Social Media', '#28a745', 'Came from a social platform'],
['Email', '#ffc107', 'Responded to an email campaign'],
['Partner', '#6f42c1', 'Referred by a channel partner'],
['Event', '#dc3545', 'Met at a trade show or event'],
['Affiliate', '#e83e8c', 'Referred by an affiliate'],
['Client', '#17a2b8', 'Referred by an existing client'],
['Website', '#20c997', 'Enquiry submitted through the website'],
['Networking Group', '#001f3f', 'Referred through a networking group'],
['Chamber of Commerce', '#3d9970', 'Referred through a chamber or association'],
['Vendor', '#adb5bd', 'Referred by a vendor or distributor'],
['Cold Outreach', '#6c757d', 'Result of outbound prospecting'],
['Direct Mail', '#795548', 'Responded to a mailer'],
['Acquisition', '#6610f2', 'Came across with an acquired book of business'],
['Other', '#343a40', 'Source does not fit any standard category'],
];
$ticket_categories = [
['Workstation', '#007bff', 'fa-desktop', 'Desktop and laptop issues'],
['Server', '#001f3f', 'fa-server', 'Physical and virtual server issues'],
['Network', '#6610f2', 'fa-network-wired', 'Switching, routing and cabling'],
['Firewall', '#dc3545', 'fa-shield-alt', 'Firewall, VPN and perimeter security'],
['Wireless', '#17a2b8', 'fa-wifi', 'Access points, coverage and roaming'],
['Printer', '#6c757d', 'fa-print', 'Printers, scanners and copiers'],
['Email', '#fd7e14', 'fa-envelope', 'Mail flow, delivery and client issues'],
['Microsoft 365', '#0078d4', 'fa-cloud', 'Tenant, licensing and cloud app issues'],
['Account and Access', '#ffc107', 'fa-user-lock', 'Passwords, MFA and permissions'],
['Software', '#20c997', 'fa-window-restore', 'Standard application issues'],
['Line of Business Application', '#6f42c1', 'fa-cubes', 'Client specific business applications'],
['Backup and Recovery', '#3d9970', 'fa-database', 'Backup jobs, restores and replication'],
['Security Incident', '#d81b60', 'fa-user-secret', 'Suspected or confirmed compromise'],
['Phishing Report', '#e83e8c', 'fa-fish', 'User reported suspicious email'],
['Phone and VoIP', '#28a745', 'fa-phone-alt', 'Handsets, extensions and call routing'],
['Mobile Device', '#795548', 'fa-mobile-alt', 'Phones, tablets and mobile management'],
['Hardware Failure', '#343a40', 'fa-tools', 'Failed hardware, RMA and replacement'],
['Onboarding', '#28a745', 'fa-user-plus', 'New user and new device setup'],
['Offboarding', '#dc3545', 'fa-user-minus', 'Departing user and device recovery'],
['Procurement', '#adb5bd', 'fa-shopping-cart', 'Quotes, orders and purchasing'],
['Website and DNS', '#17a2b8', 'fa-globe', 'Hosting, domains and certificates'],
['Maintenance', '#6c757d', 'fa-calendar-check', 'Scheduled and preventative maintenance'],
['Monitoring Alert', '#ffc107', 'fa-bell', 'Alerts raised by monitoring tooling'],
['Project Work', '#6f42c1', 'fa-project-diagram', 'Work carried out under a project'],
['Billing', '#001f3f', 'fa-file-invoice-dollar', 'Invoice and account questions'],
['Training', '#e83e8c', 'fa-graduation-cap', 'How to questions and user training'],
['Vendor Coordination', '#795548', 'fa-handshake', 'Work being driven with a third party'],
['Other', '#343a40', 'fa-question-circle', 'Does not fit any standard category'],
];
$categories = [];
$simple_types = [
'Expense' => $expense_categories,
'Income' => $income_categories,
'Referral' => $referral_categories,
];
foreach ($simple_types as $type => $rows) {
foreach ($rows as $row) {
$categories[] = [
'type' => $type,
'name' => $row[0],
'color' => $row[1],
'icon' => '',
'description' => $row[2],
'order' => 0,
];
}
}
$order = 1;
foreach ($ticket_categories as $row) {
$categories[] = [
'type' => 'Ticket',
'name' => $row[0],
'color' => $row[1],
'icon' => $row[2],
'description' => $row[3],
'order' => $order,
];
$order++;
}
return $categories;
}
// ------------------------------
// starterContentTags
// tag_type 1 Client, 2 Location, 3 Contact, 4 Credential, 5 Asset.
// tag_icon is stored without the fa- prefix, the views add it.
// ------------------------------
function starterContentTags() {
$tags = [
// Client
[1, 'Managed', '#28a745', 'handshake'],
[1, 'Co-Managed', '#20c997', 'people-arrows'],
[1, 'Break Fix', '#fd7e14', 'wrench'],
[1, 'Block Hours', '#6f42c1', 'hourglass-half'],
[1, 'Prospect', '#17a2b8', 'binoculars'],
[1, 'Onboarding', '#007bff', 'user-plus'],
[1, 'Offboarding', '#dc3545', 'user-minus'],
[1, 'Key Account', '#ffc107', 'star'],
[1, 'At Risk', '#d81b60', 'exclamation-triangle'],
[1, 'Past Due', '#dc3545', 'file-invoice-dollar'],
[1, 'Multi Site', '#001f3f', 'map-marked-alt'],
[1, 'Non Profit', '#3d9970', 'hand-holding-heart'],
[1, 'After Hours Support', '#343a40', 'moon'],
[1, 'Compliance', '#795548', 'balance-scale'],
[1, 'Cyber Insurance', '#6610f2', 'shield-alt'],
[1, 'Service Hold', '#6c757d', 'pause-circle'],
// Location
[2, 'Head Office', '#007bff', 'building'],
[2, 'Branch', '#17a2b8', 'store'],
[2, 'Warehouse', '#795548', 'warehouse'],
[2, 'Retail', '#e83e8c', 'cash-register'],
[2, 'Home Office', '#20c997', 'house-user'],
[2, 'Data Center', '#001f3f', 'server'],
[2, 'Server Room', '#6f42c1', 'network-wired'],
[2, 'Restricted Access', '#dc3545', 'lock'],
[2, 'Onsite Spares', '#6c757d', 'boxes'],
// Contact
[3, 'Primary', '#007bff', 'user-tie'],
[3, 'Technical', '#17a2b8', 'laptop-code'],
[3, 'Billing', '#28a745', 'file-invoice-dollar'],
[3, 'Authorized Approver', '#6f42c1', 'check-double'],
[3, 'Executive', '#001f3f', 'crown'],
[3, 'Emergency', '#dc3545', 'phone-volume'],
[3, 'After Hours', '#343a40', 'moon'],
[3, 'Onsite Point of Contact', '#20c997', 'map-marker-alt'],
[3, 'Not Authorized', '#6c757d', 'ban'],
[3, 'Departed', '#795548', 'user-slash'],
// Credential
[4, 'Domain Admin', '#dc3545', 'user-shield'],
[4, 'Local Admin', '#fd7e14', 'user-cog'],
[4, 'Service Account', '#6c757d', 'robot'],
[4, 'Break Glass', '#d81b60', 'fire-extinguisher'],
[4, 'Firewall', '#001f3f', 'shield-alt'],
[4, 'Switch', '#6610f2', 'network-wired'],
[4, 'Wireless', '#17a2b8', 'wifi'],
[4, 'Hypervisor', '#343a40', 'server'],
[4, 'Backup Console', '#3d9970', 'database'],
[4, 'Microsoft 365', '#0078d4', 'cloud'],
[4, 'Registrar and DNS', '#20c997', 'globe'],
[4, 'Hosting', '#795548', 'hdd'],
[4, 'Vendor Portal', '#adb5bd', 'external-link-alt'],
[4, 'Shared', '#ffc107', 'users'],
[4, 'Rotate Quarterly', '#6f42c1', 'sync-alt'],
// Asset
[5, 'Workstation', '#007bff', 'desktop'],
[5, 'Laptop', '#17a2b8', 'laptop'],
[5, 'Server', '#001f3f', 'server'],
[5, 'Hypervisor Host', '#343a40', 'layer-group'],
[5, 'Storage', '#795548', 'hdd'],
[5, 'Firewall', '#dc3545', 'shield-alt'],
[5, 'Switch', '#6610f2', 'network-wired'],
[5, 'Access Point', '#20c997', 'wifi'],
[5, 'Printer', '#6c757d', 'print'],
[5, 'UPS', '#ffc107', 'car-battery'],
[5, 'VoIP Handset', '#28a745', 'phone-alt'],
[5, 'Mobile', '#e83e8c', 'mobile-alt'],
[5, 'Business Critical', '#d81b60', 'exclamation-circle'],
[5, 'Monitored', '#3d9970', 'heartbeat'],
[5, 'Backed Up', '#001f3f', 'database'],
[5, 'Endpoint Protection', '#6f42c1', 'user-shield'],
[5, 'Under Warranty', '#28a745', 'certificate'],
[5, 'Warranty Expired', '#fd7e14', 'calendar-times'],
[5, 'End of Life', '#adb5bd', 'skull-crossbones'],
[5, 'Leased', '#6c757d', 'file-contract'],
[5, 'Spare Stock', '#795548', 'boxes'],
[5, 'Patch Excluded', '#ffc107', 'ban'],
];
return $tags;
}
// ------------------------------
// starterContentTicketTemplates
// Details are stored as HTML - the modals edit them through TinyMCE.
// Task estimates are in minutes.
// ------------------------------
function starterContentTicketTemplates() {
$ticket_templates = [
[
'name' => 'New Hire Onboarding',
'description' => 'Standard setup for a new employee joining a client',
'subject' => 'New hire onboarding - [Employee Name]',
'details' => 'New user setup. Confirm the following with the client before starting:
- Full name and preferred display name
- Job title, department and manager
- Start date and working hours
- Who to mirror permissions from
- Hardware and phone requirements
',
'tasks' => [
['Confirm start date and written approval from an authorized contact', 15],
['Create the directory account and set the initial password', 15],
['Assign licenses', 10],
['Create the mailbox, signature and alias', 20],
['Add to security and distribution groups', 15],
['Configure file share and application permissions', 20],
['Image and enroll the workstation', 60],
['Install the standard application stack', 30],
['Install line of business applications', 30],
['Enroll multi-factor authentication', 15],
['Configure the phone extension and voicemail', 15],
['Add to RMM, endpoint protection and backup', 20],
['Hand over credentials to the user securely', 10],
['Create the contact record and document the setup', 15],
],
],
[
'name' => 'Employee Offboarding',
'description' => 'Standard process for a departing employee',
'subject' => 'Employee offboarding - [Employee Name]',
'details' => 'Departing user. Do not start until written authorization has been received from an authorized contact.
Confirm the last working day, who inherits the mailbox and data, and whether the account should be deleted or retained.
',
'tasks' => [
['Obtain written authorization and confirm the effective date', 15],
['Disable the account and reset the password', 10],
['Revoke multi-factor authentication and active sessions', 10],
['Convert the mailbox to shared or set forwarding', 15],
['Remove group, share and application access', 20],
['Reclaim and reassign licenses', 15],
['Collect hardware, keys and access cards', 30],
['Back up and hand over user data', 30],
['Wipe and reimage the workstation', 60],
['Remove the device from RMM, endpoint protection and backup', 15],
['Update documentation and contact records', 15],
['Notify billing of the license change', 10],
],
],
[
'name' => 'Workstation Deployment',
'description' => 'Build and deploy a new desktop or laptop',
'subject' => 'Workstation deployment - [Client] - [User]',
'details' => 'New workstation build. Confirm the order reference, the user it is for, and whether an existing machine is being replaced.
',
'tasks' => [
['Confirm the quote or purchase order', 10],
['Receive, inventory and create the asset record', 20],
['Record the serial number and warranty expiry', 10],
['Image the device and apply all updates', 60],
['Join the domain or cloud directory', 15],
['Install the standard application stack', 30],
['Install line of business applications', 30],
['Map printers and network drives', 15],
['Enroll in RMM, endpoint protection and backup', 20],
['Migrate the user profile and data', 60],
['Verify mail, file sync and printing', 20],
['Deliver, walk the user through it and wipe the old device', 45],
],
],
[
'name' => 'Server Deployment',
'description' => 'Build and commission a new physical or virtual server',
'subject' => 'Server deployment - [Client] - [Server Name]',
'details' => 'New server build. Confirm the role, sizing, network placement and maintenance window before starting.
',
'tasks' => [
['Confirm the specification, role and sizing', 30],
['Rack, cable and label the hardware', 60],
['Configure out of band management', 30],
['Install the operating system or hypervisor and patch fully', 90],
['Configure storage, RAID and volumes', 45],
['Set the static addressing, DNS and time source', 20],
['Install the server role and application software', 60],
['Add to RMM, monitoring and endpoint protection', 20],
['Add to the backup job and run a first full backup', 45],
['Document the build and hand over to the client', 30],
],
],
[
'name' => 'Monthly Maintenance',
'description' => 'Recurring monthly maintenance pass for a managed client',
'subject' => 'Monthly maintenance - [Client] - [Month]',
'details' => 'Scheduled monthly maintenance. Work the checklist and send the client a summary when complete.
',
'tasks' => [
['Review patch compliance across the fleet', 20],
['Approve and deploy pending patches', 30],
['Reboot within the agreed maintenance window', 30],
['Verify services and applications after reboot', 30],
['Check disk space and clear down where needed', 20],
['Review endpoint protection alerts and quarantine', 20],
['Verify backup jobs completed and spot check a restore', 30],
['Check UPS battery health and runtime', 15],
['Review firewall firmware and license expiry', 15],
['Review certificate and domain expiry', 15],
['Update documentation and send the client a summary', 30],
],
],
[
'name' => 'Backup Failure Investigation',
'description' => 'Triage and resolve a failed or missed backup job',
'subject' => 'Backup failure - [Client] - [Job Name]',
'details' => 'Backup job failed or did not report. Establish the last known good restore point first, then work the cause.
',
'tasks' => [
['Identify the last successful restore point', 15],
['Review the job log and error detail', 20],
['Check source, target and network availability', 20],
['Check free space on the backup target', 10],
['Check agent and service health on the protected system', 20],
['Apply the fix and re-run the job', 30],
['Verify the job completes and the restore point is valid', 20],
['Document the cause and update the client', 20],
],
],
[
'name' => 'Backup Restore Test',
'description' => 'Scheduled proof that backups can actually be restored',
'subject' => 'Restore test - [Client] - [Quarter]',
'details' => 'Scheduled restore test. The goal is a documented, dated proof of recoverability - record the actual recovery time achieved.
',
'tasks' => [
['Agree the restore targets and test window with the client', 20],
['Restore a file level sample and verify contents', 30],
['Perform a full system or virtual machine test restore', 90],
['Verify the restored system boots and services start', 45],
['Verify application and database integrity', 45],
['Record the recovery time actually achieved', 15],
['Tear down the test environment', 20],
['Document the result and report to the client', 30],
],
],
[
'name' => 'Phishing Report',
'description' => 'User reported a suspicious email',
'subject' => 'Reported phishing email - [Client] - [User]',
'details' => 'User reported a suspicious message. Treat as credential compromise until proven otherwise - the first question is always whether credentials were entered.
',
'tasks' => [
['Obtain the message with full headers', 15],
['Analyze the sender, links and attachments', 20],
['Establish whether credentials were entered or attachments opened', 15],
['Force a password reset and revoke sessions if in doubt', 20],
['Audit mailbox rules and forwarding', 20],
['Review sign-in logs for anomalous access', 30],
['Block the sender and domain at the mail gateway', 15],
['Purge the message from all affected mailboxes', 20],
['Feed back to the reporting user and management', 15],
['Document the outcome', 15],
],
],
[
'name' => 'Security Incident Response',
'description' => 'Suspected or confirmed compromise',
'subject' => 'Security incident - [Client] - [Summary]',
'details' => 'Suspected or confirmed compromise. Preserve evidence before remediating, and check the client cyber insurance policy for notification requirements and approved responders before acting.
',
'tasks' => [
['Declare the incident and establish scope and impact', 30],
['Isolate affected systems and accounts', 30],
['Preserve logs, images and evidence', 60],
['Notify the client, insurer and any required authority', 30],
['Contain the spread and close the entry point', 120],
['Eradicate persistence and reset all affected credentials', 120],
['Restore from a known good backup and verify', 180],
['Monitor for reinfection', 60],
['Produce the post incident report and remediation plan', 90],
],
],
[
'name' => 'Client Onboarding',
'description' => 'Bring a newly signed client under management',
'subject' => 'Client onboarding - [Client]',
'details' => 'New client onboarding. Nothing goes live until the agreement is countersigned. The output of this ticket is a fully documented client that can be supported by anyone on the team.
',
'tasks' => [
['Confirm the countersigned agreement is on file', 15],
['Hold the kickoff call and agree the escalation path', 60],
['Collect contacts, locations, hours and authorized approvers', 60],
['Discover and document the network', 180],
['Inventory assets and create asset records', 180],
['Deploy RMM and endpoint protection', 120],
['Move all credentials into the vault', 120],
['Configure and verify backups', 120],
['Run a security baseline audit', 120],
['Review DNS, mail records and certificate expiry', 60],
['Set up billing, invoicing and autopay', 45],
['Schedule the review cadence', 20],
['Hold the 30 day review', 60],
],
],
[
'name' => 'Client Offboarding',
'description' => 'Cleanly exit a departing client',
'subject' => 'Client offboarding - [Client]',
'details' => 'Departing client. Agree in writing what is handed over, what is retained and for how long, and settle the final invoice before access is removed.
',
'tasks' => [
['Confirm the notice period and termination date in writing', 20],
['Agree the handover scope with the client or incoming provider', 45],
['Raise and settle the final invoice', 30],
['Export and hand over documentation', 60],
['Hand over credentials securely and confirm receipt', 45],
['Remove RMM, endpoint protection and monitoring agents', 60],
['Release or transfer licenses and subscriptions', 45],
['Agree the backup retention and destruction date', 20],
['Revoke all remaining access and delegated permissions', 45],
['Archive the client record and close out', 30],
],
],
[
'name' => 'Firewall Deployment',
'description' => 'Replace or deploy a perimeter firewall',
'subject' => 'Firewall deployment - [Client] - [Site]',
'details' => 'Firewall deployment or replacement. Capture the existing configuration and public addressing before the change window, and agree a rollback plan.
',
'tasks' => [
['Capture the current configuration, rules and public addressing', 60],
['Confirm the license, subscription and support entitlement', 20],
['Stage and update the firmware on the bench', 45],
['Build the base configuration, interfaces and routing', 90],
['Recreate firewall rules, NAT and port forwards', 90],
['Configure VPN tunnels and remote access', 60],
['Agree the change window and notify the client', 20],
['Cut over and verify connectivity and services', 60],
['Verify VPN, remote access and any published services', 45],
['Document the configuration and store the credentials', 45],
],
],
[
'name' => 'Microsoft 365 Tenant Onboarding',
'description' => 'Bring a client tenant under management and to a security baseline',
'subject' => 'Microsoft 365 tenant onboarding - [Client]',
'details' => 'Tenant onboarding. Establish delegated access, document the licensing position, then bring the tenant to the agreed security baseline.
',
'tasks' => [
['Establish delegated administrative access', 30],
['Document the tenant ID, domains and license position', 45],
['Create the break glass administrator account and vault it', 30],
['Enforce multi-factor authentication for all users', 60],
['Review and harden the conditional access policy', 60],
['Review mail flow, connectors and forwarding rules', 45],
['Verify SPF, DKIM and DMARC records', 45],
['Enable and configure audit logging and alerting', 45],
['Configure tenant backup', 60],
['Document the tenant and report the baseline findings', 60],
],
],
[
'name' => 'Quarterly Business Review',
'description' => 'Prepare and deliver a client review meeting',
'subject' => 'Quarterly business review - [Client] - [Quarter]',
'details' => 'Client review meeting. Pull the numbers before the meeting and lead with outcomes rather than activity - ticket volume, uptime, risks closed and what is coming next.
',
'tasks' => [
['Pull ticket volume, response and resolution figures', 45],
['Review asset age, warranty and end of life exposure', 45],
['Review the security posture and any open risks', 45],
['Review backup and restore test results for the period', 30],
['Review spend against budget and the license position', 30],
['Build the roadmap and budget recommendations', 60],
['Circulate the pack and hold the meeting', 90],
['Log the agreed actions and raise the follow up work', 45],
],
],
];
return $ticket_templates;
}
// ------------------------------
// starterContentProjectTemplates
// Stages are named ticket templates, resolved at load time.
// ------------------------------
function starterContentProjectTemplates() {
$project_templates = [
[
'name' => 'New Client Onboarding',
'description' => 'Take a newly signed client from signature to fully managed and documented.',
'ticket_templates' => ['Client Onboarding', 'Microsoft 365 Tenant Onboarding', 'Backup Restore Test', 'Quarterly Business Review'],
],
[
'name' => 'Client Offboarding',
'description' => 'Cleanly exit a departing client with a documented handover.',
'ticket_templates' => ['Client Offboarding'],
],
[
'name' => 'Server Refresh',
'description' => 'Replace ageing server hardware and migrate roles and data across.',
'ticket_templates' => ['Server Deployment', 'Backup Restore Test'],
],
[
'name' => 'Workstation Refresh',
'description' => 'Phased replacement of end of life workstations across a client fleet.',
'ticket_templates' => ['Workstation Deployment'],
],
[
'name' => 'Network Refresh',
'description' => 'Replace switching, firewall and wireless and re-document the network.',
'ticket_templates' => ['Firewall Deployment'],
],
[
'name' => 'Microsoft 365 Migration',
'description' => 'Migrate mail and files to a client tenant and decommission the legacy platform.',
'ticket_templates' => ['Microsoft 365 Tenant Onboarding'],
],
[
'name' => 'Security Baseline',
'description' => 'Bring a client up to the agreed security baseline - typically driven by a cyber insurance or compliance requirement.',
'ticket_templates' => ['Microsoft 365 Tenant Onboarding', 'Backup Restore Test', 'Firewall Deployment'],
],
[
'name' => 'Backup and Disaster Recovery Implementation',
'description' => 'Design, deploy and prove a backup and recovery solution.',
'ticket_templates' => ['Backup Restore Test'],
],
[
'name' => 'Office Move',
'description' => 'Relocate a client site including circuits, cabling, network and workstations.',
'ticket_templates' => ['Firewall Deployment', 'Workstation Deployment', 'Server Deployment'],
],
];
return $project_templates;
}
// ------------------------------
// 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.
// ------------------------------
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'],
];
return $vendor_templates;
}
// ------------------------------
// starterContentDocumentTemplates
// name, description, content. Square bracket placeholders are filled in when
// the document is created from the template.
// ------------------------------
function starterContentDocumentTemplates() {
$document_templates = [
[
'Network Overview',
'Topology, addressing, VLANs and internet circuits for a site',
'Network Overview
Site: [Site]
Last reviewed: [Date]
Internet Circuits
| Provider | Type | Speed | Public IP | Account | Support |
|---|
| | | | | |
Addressing and VLANs
| VLAN | Name | Subnet | Gateway | DHCP Scope |
|---|
| | | | |
Core Equipment
- Firewall:
- Core switch:
- Access switches:
- Wireless controller and access points:
DNS and DHCP
- Internal DNS servers:
- External forwarders:
- DHCP server and reservations:
Remote Access
- VPN type and endpoint:
- Who has access:
Notes
',
],
[
'Server Build Sheet',
'As-built record for a physical or virtual server',
'Server Build Sheet
Hostname: [Hostname]
Role: [Role]
Built: [Date]
Built by: [Technician]
Hardware or Virtual Specification
| CPU | Memory | Storage | Host or Chassis | Serial or Service Tag |
|---|
| | | | |
Operating System
- Version and edition:
- Licensing:
- Patch level at handover:
Network
- Addressing:
- Out of band management:
Roles and Applications
Backup
- Job name and schedule:
- Retention:
- First successful backup:
Monitoring
- Agents installed:
- Alerts configured:
Dependencies and Restart Order
',
],
[
'Backup and Recovery Runbook',
'What is protected, how often, and how to restore it',
'Backup and Recovery Runbook
Client: [Client]
Last restore test: [Date]
Agreed Objectives
- Recovery point objective:
- Recovery time objective:
Protected Systems
| System | Job | Schedule | Retention | Target | Offsite Copy |
|---|
| | | | | |
Not Protected
Record anything deliberately excluded and who accepted the risk.
Restore Procedure - File Level
Restore Procedure - Full System
Restore Test History
| Date | Scope | Recovery Time Achieved | Result | Tested By |
|---|
| | | | |
',
],
[
'Disaster Recovery Plan',
'What happens when a site or critical system is lost',
'Disaster Recovery Plan
Client: [Client]
Approved by: [Contact]
Last reviewed: [Date]
Scope and Assumptions
Critical Systems in Priority Order
| Priority | System | Business Function | Recovery Target |
|---|
| 1 | | | |
Declaration and Authority
- Who can declare a disaster:
- How the team is mobilised:
Communication Plan
Recovery Procedures
Alternate Working Arrangements
Return to Normal Operations
',
],
[
'Site and Access Details',
'Physical access, keyholders and on-site logistics',
'Site and Access Details
Site: [Site]
Address: [Address]
Access
- Building access method:
- Server room access method:
- Notice required before attending:
- Escort required:
Key Contacts
Hours and Restrictions
- Normal site hours:
- Agreed maintenance window:
- Restricted periods:
Logistics
- Parking and loading:
- Delivery instructions:
- Health and safety requirements:
Do not record alarm codes, door codes or key safe combinations here - store those in the credential vault.
',
],
[
'Microsoft 365 Tenant Details',
'Tenant identifiers, licensing position and security baseline',
'Microsoft 365 Tenant Details
Client: [Client]
Tenant domain: [Tenant]
Tenant ID: [Tenant ID]
Domains
| Domain | Registrar | Verified | Primary |
|---|
| | | |
Licensing
| SKU | Assigned | Purchased | Renewal |
|---|
| | | |
Administrative Access
- Delegated access model:
- Break glass account location:
- Global administrators:
Security Baseline
- Multi-factor authentication enforced:
- Conditional access policies:
- Legacy authentication blocked:
- Audit logging enabled:
Mail Flow
- Inbound and outbound connectors:
- SPF, DKIM and DMARC status:
- Filtering platform:
Tenant Backup
',
],
[
'Line of Business Application Profile',
'Everything needed to support a client business critical application',
'Line of Business Application Profile
Application: [Application]
Business owner: [Contact]
Criticality: [Critical / High / Normal]
Vendor and Support
- Vendor:
- Support portal:
- Support hours and contract level:
- Account or customer number:
Architecture
- Hosting model:
- Servers and services involved:
- Database platform and location:
- Integrations and dependencies:
Access
- How users connect:
- Licensing model and count:
- Who administers it:
Maintenance
- Update process and cadence:
- Known constraints - patching, reboots, versions:
- Backup approach:
Common Issues
',
],
[
'New Hire Onboarding Checklist',
'Client facing form for requesting a new user setup',
'New Hire Onboarding Request
Return this form at least five working days before the start date.
Employee
- Full name:
- Preferred display name:
- Job title:
- Department:
- Manager:
- Start date:
- Working hours and location:
Access
- Mirror access from:
- Shared mailboxes required:
- Distribution groups:
- Applications required:
Equipment
- Laptop or desktop:
- Monitors and peripherals:
- Mobile phone required:
- Desk phone or extension:
Authorization
- Requested by:
- Approved by:
- Date:
',
],
[
'Employee Offboarding Checklist',
'Client facing form authorizing a user removal',
'Employee Offboarding Request
This form is the written authorization to disable access. It must come from an authorized contact.
Employee
- Full name:
- Last working day:
- Effective time for access removal:
Mailbox and Data
- Forward mail to:
- Who takes ownership of files:
- Retention period required:
- Auto reply wording:
Equipment
- Equipment to be returned:
- Collected by:
- Return date:
Authorization
- Requested by:
- Approved by:
- Date:
',
],
[
'Change Record',
'Record of a planned change, its impact and how to roll it back',
'Change Record
Change: [Summary]
Requested by: [Contact]
Scheduled: [Date and Window]
Risk: [Low / Medium / High]
Reason for Change
Systems Affected
Expected Impact
- User impact:
- Expected downtime:
- Who has been notified:
Implementation Steps
Verification
Rollback Plan
Outcome
- Completed by:
- Result:
- Documentation updated:
',
],
[
'Incident Response Plan',
'Agreed process and contacts for a security incident',
'Incident Response Plan
Client: [Client]
Last reviewed: [Date]
Contacts
| Role | Name | Contact | Out of Hours |
|---|
| Client decision maker | | | |
| Technical lead | | | |
| Cyber insurer | | | |
| Legal counsel | | | |
Insurance and Notification Requirements
- Policy number and insurer:
- Notification deadline:
- Approved responders required:
- Regulatory reporting obligations:
Severity Definitions
Response Stages
- Detect and declare
- Contain and isolate
- Preserve evidence
- Notify
- Eradicate
- Recover and verify
- Review
Evidence Handling
',
],
[
'Quarterly Business Review Notes',
'Standing agenda and record of a client review meeting',
'Quarterly Business Review
Client: [Client]
Period: [Quarter]
Attendees: [Attendees]
Date: [Date]
Service Performance
- Tickets raised and closed:
- Response and resolution against target:
- Recurring themes:
Availability and Incidents
Security Posture
- Open risks:
- Risks closed this period:
- Training completion:
Backup and Recovery
- Restore test result:
- Coverage gaps:
Asset and License Position
- End of life exposure:
- Warranty expiry in the next 12 months:
- License changes:
Roadmap and Budget
| Item | Driver | Indicative Cost | Target Quarter |
|---|
| | | |
Agreed Actions
',
],
];
return $document_templates;
}
// ------------------------------
// starterContentProducts
// name, type, code, price, income category, description.
// Hardware and resold SKUs come in at zero - they are quoted per deal or move
// with the vendor price list.
// ------------------------------
function starterContentProducts() {
$products = [
// Recurring managed services
['Managed Workstation', 'service', 'MS-WKS', '65.00', 'Managed Services', 'Per workstation, per month. Monitoring, patching, endpoint protection and unlimited remote support.'],
['Managed Server', 'service', 'MS-SRV', '195.00', 'Managed Services', 'Per server, per month. Monitoring, patching, maintenance and support.'],
['Managed Network Device', 'service', 'MS-NET', '45.00', 'Managed Services', 'Per switch or access point, per month. Monitoring, firmware and configuration management.'],
['Managed Firewall', 'service', 'MS-FW', '65.00', 'Managed Services', 'Per firewall, per month. Monitoring, firmware, rule management and subscription renewal.'],
['Co-Managed IT', 'service', 'MS-COMG', '35.00', 'Managed Services', 'Per seat, per month. Tooling and escalation support alongside an internal IT team.'],
['Help Desk Support', 'service', 'MS-HD', '45.00', 'Support', 'Per user, per month. Unlimited remote help desk during business hours.'],
['Virtual CIO', 'service', 'MS-VCIO', '500.00', 'Consulting', 'Per month. Strategic planning, budgeting and quarterly business reviews.'],
['Remote Monitoring', 'service', 'MS-RMM', '15.00', 'Monitoring', 'Per device, per month. Agent based monitoring and alerting.'],
['Patch Management', 'service', 'MS-PATCH', '8.00', 'Monitoring', 'Per endpoint, per month. Operating system and third party patching.'],
// Security
['Endpoint Protection', 'service', 'SEC-EDR', '7.00', 'Security', 'Per endpoint, per month. Managed endpoint detection and response.'],
['DNS Filtering', 'service', 'SEC-DNS', '3.00', 'Security', 'Per endpoint, per month. Malicious and category based web filtering.'],
['Email Security', 'service', 'SEC-MAIL', '4.00', 'Security', 'Per mailbox, per month. Spam, phishing and malware filtering.'],
['Security Awareness Training', 'service', 'SEC-SAT', '4.00', 'Security', 'Per user, per month. Training campaigns and simulated phishing.'],
['Password Manager', 'service', 'SEC-PWD', '4.00', 'Security', 'Per user, per month. Managed password vault.'],
['Dark Web Monitoring', 'service', 'SEC-DWM', '25.00', 'Security', 'Per client, per month. Credential exposure monitoring for client domains.'],
// Backup
['Managed Backup - Workstation', 'service', 'BU-WKS', '15.00', 'Backup', 'Per workstation, per month. Image based backup with offsite copy.'],
['Managed Backup - Server', 'service', 'BU-SRV', '75.00', 'Backup', 'Per server, per month. Image based backup with offsite copy and restore testing.'],
['Microsoft 365 Backup', 'service', 'BU-M365', '4.00', 'Backup', 'Per user, per month. Mail, calendar, contacts, OneDrive and SharePoint.'],
['Offsite Backup Storage', 'service', 'BU-STOR', '25.00', 'Backup', 'Per terabyte, per month. Offsite retention beyond the included allowance.'],
// Cloud, web and telecom
['Cloud Server Hosting', 'service', 'CLD-SRV', '150.00', 'Cloud Services', 'Per virtual server, per month. Compute, storage and backup included.'],
['Web Hosting', 'service', 'WEB-HOST', '25.00', 'Web and Hosting', 'Per site, per month. Hosting, updates and monitoring.'],
['Email Hosting', 'service', 'WEB-MAIL', '5.00', 'Web and Hosting', 'Per mailbox, per month.'],
['Domain Registration', 'service', 'WEB-DOM', '25.00', 'Web and Hosting', 'Per domain, per year. Registration and DNS management.'],
['SSL Certificate', 'service', 'WEB-SSL', '95.00', 'Web and Hosting', 'Per certificate, per year. Issue, installation and renewal.'],
['VoIP Seat', 'service', 'TEL-SEAT', '25.00', 'Telecom Services', 'Per extension, per month. Hosted voice seat with unlimited domestic calling.'],
['VoIP Direct Dial Number', 'service', 'TEL-DID', '3.00', 'Telecom Services', 'Per number, per month.'],
['SIP Trunk Channel', 'service', 'TEL-SIP', '20.00', 'Telecom Services', 'Per concurrent call path, per month.'],
// Licensing - priced at zero, these move with the vendor price list
['Microsoft 365 Business Basic', 'service', 'LIC-M365BB', '0.00', 'Licensing', 'Per user, per month. Set the price from the current vendor price list.'],
['Microsoft 365 Business Standard', 'service', 'LIC-M365BS', '0.00', 'Licensing', 'Per user, per month. Set the price from the current vendor price list.'],
['Microsoft 365 Business Premium', 'service', 'LIC-M365BP', '0.00', 'Licensing', 'Per user, per month. Set the price from the current vendor price list.'],
['Microsoft 365 Exchange Online Plan 1', 'service', 'LIC-M365EX', '0.00', 'Licensing', 'Per mailbox, per month. Set the price from the current vendor price list.'],
['Third Party Software Subscription', 'service', 'LIC-3P', '0.00', 'Software Sales', 'Generic resold subscription. Set the name and price per client.'],
// Labor
['Remote Support', 'service', 'LAB-REM', '125.00', 'Labor', 'Per hour. Remote support outside of an agreement, billed in 15 minute increments.'],
['Onsite Support', 'service', 'LAB-ONS', '150.00', 'Labor', 'Per hour. Onsite support, one hour minimum.'],
['After Hours Support', 'service', 'LAB-AH', '225.00', 'Labor', 'Per hour. Outside business hours, one and a half times the standard rate.'],
['Weekend and Holiday Support', 'service', 'LAB-WKD', '300.00', 'Labor', 'Per hour. Weekends and public holidays, twice the standard rate.'],
['Project Engineering', 'service', 'LAB-PRJ', '165.00', 'Labor', 'Per hour. Project delivery and implementation work.'],
['Network Engineering', 'service', 'LAB-NET', '175.00', 'Labor', 'Per hour. Network, firewall and server engineering.'],
['Consulting', 'service', 'LAB-CON', '200.00', 'Consulting', 'Per hour. Advisory, design and assessment work.'],
['Data Recovery', 'service', 'LAB-REC', '175.00', 'Labor', 'Per hour. Best effort recovery, no guarantee of success.'],
['End User Training', 'service', 'LAB-TRN', '125.00', 'Training', 'Per hour. Group or one to one training.'],
['Web Development', 'service', 'LAB-WEB', '125.00', 'Web and Hosting', 'Per hour. Website build and maintenance work.'],
['Block Hours - 10 Hours', 'service', 'LAB-BLK10', '1150.00', 'Labor', 'Prepaid block of ten support hours, valid twelve months.'],
['Block Hours - 20 Hours', 'service', 'LAB-BLK20', '2200.00', 'Labor', 'Prepaid block of twenty support hours, valid twelve months.'],
['Trip Charge', 'service', 'LAB-TRIP', '45.00', 'Reimbursable Expenses', 'Per visit. Applied to onsite work outside the included travel radius.'],
['Mileage', 'service', 'LAB-MILE', '0.70', 'Reimbursable Expenses', 'Per mile beyond the included travel radius.'],
// One off project and setup work
['Client Onboarding', 'service', 'ONB-CLIENT', '1500.00', 'Onboarding', 'One off. Discovery, documentation, tooling deployment and security baseline.'],
['Workstation Setup', 'service', 'PRJ-WKS', '150.00', 'Projects', 'Per workstation. Build, data migration and deployment.'],
['Server Deployment', 'service', 'PRJ-SRV', '1200.00', 'Projects', 'Per server. Build, configuration, migration and documentation.'],
['Firewall Deployment', 'service', 'PRJ-FW', '850.00', 'Projects', 'Per firewall. Configuration, cutover and documentation.'],
['Wireless Survey and Deployment', 'service', 'PRJ-WIFI', '950.00', 'Projects', 'Per site. Survey, installation and tuning.'],
['Email Migration', 'service', 'PRJ-MAIL', '45.00', 'Projects', 'Per mailbox. Migration, cutover and client reconfiguration.'],
['Data Migration', 'service', 'PRJ-DATA', '500.00', 'Projects', 'Per migration. File share and data platform moves.'],
['Website Build', 'service', 'PRJ-WEB', '2500.00', 'Web and Hosting', 'Per site. Design, build and launch.'],
['Equipment Disposal', 'service', 'PRJ-DISP', '25.00', 'Projects', 'Per device. Secure data destruction and certified recycling.'],
// Hardware - quoted per deal, seeded at zero
['Desktop', 'product', 'HW-DSK', '0.00', 'Hardware Sales', 'Business class desktop. Priced per quote.'],
['Laptop', 'product', 'HW-LAP', '0.00', 'Hardware Sales', 'Business class laptop. Priced per quote.'],
['Docking Station', 'product', 'HW-DOCK', '0.00', 'Hardware Sales', 'Priced per quote.'],
['Monitor', 'product', 'HW-MON', '0.00', 'Hardware Sales', 'Priced per quote.'],
['Server', 'product', 'HW-SRV', '0.00', 'Hardware Sales', 'Rack or tower server. Priced per quote.'],
['Network Attached Storage', 'product', 'HW-NAS', '0.00', 'Hardware Sales', 'Priced per quote.'],
['Firewall Appliance', 'product', 'HW-FW', '0.00', 'Hardware Sales', 'Priced per quote, excludes subscription.'],
['Switch - 24 Port', 'product', 'HW-SW24', '0.00', 'Hardware Sales', 'Priced per quote.'],
['Switch - 48 Port', 'product', 'HW-SW48', '0.00', 'Hardware Sales', 'Priced per quote.'],
['Wireless Access Point', 'product', 'HW-AP', '0.00', 'Hardware Sales', 'Priced per quote.'],
['Uninterruptible Power Supply', 'product', 'HW-UPS', '0.00', 'Hardware Sales', 'Priced per quote.'],
['Solid State Drive', 'product', 'HW-SSD', '0.00', 'Hardware Sales', 'Priced per quote.'],
['Memory Upgrade', 'product', 'HW-RAM', '0.00', 'Hardware Sales', 'Priced per quote.'],
['Keyboard and Mouse', 'product', 'HW-KBM', '0.00', 'Hardware Sales', 'Priced per quote.'],
['Headset', 'product', 'HW-HS', '0.00', 'Hardware Sales', 'Priced per quote.'],
['Webcam', 'product', 'HW-CAM', '0.00', 'Hardware Sales', 'Priced per quote.'],
['Printer', 'product', 'HW-PRT', '0.00', 'Hardware Sales', 'Priced per quote.'],
['Toner Cartridge', 'product', 'HW-TON', '0.00', 'Hardware Sales', 'Priced per quote.'],
['VoIP Handset', 'product', 'HW-PHONE', '0.00', 'Hardware Sales', 'Priced per quote.'],
['Patch Cable', 'product', 'HW-CBL', '0.00', 'Hardware Sales', 'Priced per quote.'],
['Rack Cabinet', 'product', 'HW-RACK', '0.00', 'Hardware Sales', 'Priced per quote.'],
['Power Distribution Unit', 'product', 'HW-PDU', '0.00', 'Hardware Sales', 'Priced per quote.'],
];
return $products;
}