Files
itflow/admin/starter_content.php
johnnyq 81c80d8965 Canned responses stages 3 and 4: agents keep canned responses of their own in Your account > Canned Responses (on the standard list with Yours and Shared tabs, how often they used each, change, copy, archive and delete their own, Copy to yours from a shared one), offered to them alone and first in the picker as Yours; Save your reply as a canned response at the foot of the picker opens the draft with the ticket's contact, first name, client, ticket number, subject, link and agent swapped back to placeholders (canned_response_unfill, text only, a one-word subject left alone), offered on the ticket's category with a switch to put it back as written, and saves it in JSON without leaving the ticket so the reply stays and the new one goes straight into the picker; Admin > Canned Responses gains an Agents' own tab showing whose each is with Share with everyone, copies made there are shared, and admins can share their own or tick Share when saving a reply; files go with a response (Files tab, up to ten of the ticket attachment kinds, kept in uploads/canned_responses/<id>/, streamed by agent/canned_response_file.php to those who may use it), shown as chips under the picker with an x to leave one off, copied onto the ticket with the reply and sent with its email on a ticket and in Update & Reply; migration 3.2.4 adds canned_response_user_id and canned_response_created_by and the canned_response_files table, existing responses staying shared; the response window moves to includes/canned_response_form.php for the admin and account windows; export and import (functions/canned_response_transfer.php) of one, some or every shared response as a versioned JSON file with categories by name, what it sets up, status by name and files as base64, importing as shared with categories matched or made and leave, add as well or replace (keeping use and archived state), everything made safe and refused files named; an MSP starter pack of 27 canned responses in Starter Content loaded through the same import, with Update for the ones already added, and the Update button now driven by the pack registry; new Canned Responses report under Service Desk by response and by agent, filtered by client and by the agent who used it, admins seeing every agent's own; changelog updated
2026-10-18 15:36:00 -04:00

211 lines
12 KiB
PHP

<?php
require_once "includes/inc_all_admin.php";
define('FROM_STARTER_CONTENT', true);
require_once "post/starter_content_model.php";
require_once "post/demo_data_model.php";
$starter_content_packs = starterContentPacks();
$starter_content_status = starterContentStatus($mysqli);
$demo_data_status = demoDataStatus($mysqli);
$demo_data_loaded = count($demo_data_status['loaded']);
$total_missing = 0;
foreach ($starter_content_status as $pack_status) {
$total_missing = $total_missing + $pack_status['missing'];
}
?>
<div class="card card-dark">
<div class="card-header py-3">
<h3 class="card-title"><i class="fas fa-fw fa-seedling me-2"></i>Starter Content</h3>
<?php if ($total_missing) { ?>
<div class="card-tools">
<form action="post.php" method="POST" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<button type="submit" name="load_starter_content" value="all" class="btn btn-primary btn-sm">
<i class="fas fa-fw fa-download me-2"></i>Add all <?= $total_missing ?> missing
</button>
</form>
</div>
<?php } ?>
</div>
<div class="card-body">
<p>
A library of opinionated defaults for a typical MSP. Add whichever packs are useful - on a
brand new install or on one that has been running for years.
</p>
<p class="text-muted">
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 and canned responses added from an earlier
version can be brought up to this one with Update - that one does replace what they hold.
</p>
<div class="table-responsive">
<table class="table table-striped align-middle">
<thead>
<tr>
<th>Content</th>
<th class="text-center">In library</th>
<th class="text-center">Already here</th>
<th class="text-center">Will be added</th>
<th class="text-end"></th>
</tr>
</thead>
<tbody>
<?php foreach ($starter_content_packs as $pack => $pack_details) {
$pack_status = $starter_content_status[$pack];
?>
<tr>
<td>
<strong><i class="fas fa-fw <?= escapeHtml($pack_details['icon']) ?> me-2"></i><?= escapeHtml($pack_details['label']) ?></strong>
<br><small class="text-muted"><?= escapeHtml($pack_details['description']) ?></small>
<?php
// Loading out of order works, it just produces less - say so up front
// rather than leaving someone with 77 uncategorised products
$requires = $pack_details['requires'] ?? '';
if ($requires && !empty($starter_content_status[$requires]['missing'])) {
?>
<br><small class="text-warning"><i class="fas fa-fw fa-exclamation-triangle me-1"></i>Add <?= escapeHtml($starter_content_packs[$requires]['label']) ?> first, or these come in without them.</small>
<?php } ?>
</td>
<td class="text-center"><?= intval($pack_status['total']) ?></td>
<td class="text-center"><?= intval($pack_status['present']) ?></td>
<td class="text-center">
<?php if ($pack_status['missing']) { ?>
<span class="badge bg-warning text-dark p-2"><?= intval($pack_status['missing']) ?></span>
<?php } else { ?>
<span class="text-success"><i class="fas fa-fw fa-check"></i></span>
<?php } ?>
</td>
<td class="text-end">
<div class="d-inline-flex flex-wrap justify-content-end gap-2">
<?php if ($pack_status['missing']) { ?>
<form action="post.php" method="POST" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<button type="submit" name="load_starter_content" value="<?= escapeHtml($pack) ?>" class="btn btn-primary btn-sm">
<i class="fas fa-fw fa-plus me-2"></i>Add
</button>
</form>
<?php } elseif (empty($pack_details['update']) || !$pack_status['present']) { ?>
<span class="text-muted">Nothing to add</span>
<?php } ?>
<?php if (!empty($pack_details['update']) && $pack_status['present']) { ?>
<!-- The ones already here brought up to this version - asked for, never done by Add -->
<form action="post.php" method="POST" autocomplete="off" onsubmit="return confirm(<?= escapeHtml(json_encode(sprintf($pack_details['update']['confirm'], intval($pack_status['present'])))) ?>);">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<button type="submit" name="<?= escapeHtml($pack_details['update']['button']) ?>" value="1" class="btn btn-outline-secondary btn-sm text-nowrap">
<i class="fas fa-fw fa-sync-alt me-2"></i>Update the <?= intval($pack_status['present']) ?> you have
</button>
</form>
<?php } ?>
</div>
</td>
</tr>
<?php } ?>
</tbody>
</table>
</div>
<p class="text-muted mb-0">
Ticket templates bring their task lists, what they ask for and their request forms with them -
the request forms come in turned off, ready to turn on in each template's Client portal tab. Canned responses
come in shared, offered to every agent on the ticket categories they are for. Products are filed under income
categories and priced as starting points only - hardware and resold licensing come in at
zero because they are quoted per deal.
</p>
</div>
</div>
<div class="card card-dark mt-3">
<div class="card-header py-3">
<h3 class="card-title"><i class="fas fa-fw fa-flask me-2"></i>Demo Data</h3>
</div>
<div class="card-body">
<p>
A fictional book of business for demos, training and screenshots - <?= intval($demo_data_status['total']) ?> clients
with the people, equipment, documentation, credentials, tickets and billing a typical MSP would be carrying for
them. Each one arrives with an onboarding project, an onboarding ticket per asset, response targets, racked
infrastructure, a documented switch port map, IP addresses and a supplier list. Managed, co-managed, break fix and hosting only
clients are all represented, most of them paid up to date and a few of them not.
Your own side of the business is generated too - bank accounts, suppliers, operating costs, recurring expenses,
transfers, catalogue lines for the things you sell, closure days, monthly budgets, and a working diary of team
meetings, maintenance windows and the unbilled running around that never gets logged against a client.
Everything is spread across the <strong>last two years</strong> and dated relative to today, so the dashboard,
ticket queue, aging, profit and loss and every other report have real history behind them rather than one flat month.
</p>
<p class="text-muted">
This is sample data, not configuration. Nothing in it is labelled as demo data - the clients, tags and references
all read as real, which is the point. Removal matches the <?= intval($demo_data_status['total']) ?> client names in
the library, so <strong>renaming a demo client stops it being removable</strong>. Contacts use addresses on the
reserved <strong>.example</strong> domain, which cannot receive mail, and the agreements are created with email
notification switched off. Anything shared - accounts, suppliers, response targets, tax rates, catalogue lines and
calendars - is matched on more than its name, so records this install already had are never touched, and the
company side of the books is always built against accounts this page created rather than yours.
</p>
<?php if ($demo_data_status['other_clients'] && !$demo_data_loaded) { ?>
<div class="alert alert-warning">
<i class="fas fa-fw fa-exclamation-triangle me-2"></i>
This install already has <strong><?= intval($demo_data_status['other_clients']) ?></strong> client(s) of its own.
Demo data is meant for a demo or training install - it will sit alongside your real clients in every list, report and total until it is removed.
</div>
<?php } ?>
<?php if (!$demo_data_loaded) { ?>
<?php if (!empty($starter_content_status['categories']['missing']) || !empty($starter_content_status['tags']['missing'])) { ?>
<p class="text-warning">
<i class="fas fa-fw fa-exclamation-triangle me-1"></i>Add the Categories and Tags packs above first, or the tickets, invoices and tags come in bare.
</p>
<?php } ?>
<form action="post.php" method="POST" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<button type="submit" name="load_demo_data" value="1" class="btn btn-primary">
<i class="fas fa-fw fa-flask me-2"></i>Add demo data
</button>
</form>
<?php } else { ?>
<p>
<strong><?= intval($demo_data_loaded) ?></strong> demo client(s) are here:
<?= escapeHtml(implode(', ', $demo_data_status['loaded'])) ?>
</p>
<div class="d-flex gap-2">
<form action="post.php" method="POST" autocomplete="off" onsubmit="return confirm('Delete the demo clients, along with all of their contacts, assets, documentation, credentials, tickets, quotes, invoices and payments, plus the demo suppliers, accounts, response targets and company expenses? This cannot be undone.');">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<button type="submit" name="remove_demo_data" value="1" class="btn btn-danger">
<i class="fas fa-fw fa-trash me-2"></i>Remove demo data
</button>
</form>
<?php if ($demo_data_loaded < $demo_data_status['total']) { ?>
<form action="post.php" method="POST" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<button type="submit" name="load_demo_data" value="1" class="btn btn-primary">
<i class="fas fa-fw fa-plus me-2"></i>Add the missing <?= intval($demo_data_status['total'] - $demo_data_loaded) ?>
</button>
</form>
<?php } ?>
</div>
<?php } ?>
</div>
</div>
<?php require_once "../includes/footer.php"; ?>