Show each starter content item as it will look under Pick which - a tag as its pill, a category as its chip, a product with its price and billing, with what each is for - and Preview the whole of a ticket, project, document, vendor or license template, canned response or product in a window, the words filled in as it is used marked; Maintenance > Starter Content gets Pick which too, its Add bringing only what is ticked and Will be added following the ticks, sharing admin/includes/starter_content_pick.php, admin/starter_content_preview.php and js/starter_content.js with Get started

This commit is contained in:
johnnyq
2026-10-25 15:04:11 -04:00
parent 6d29583cef
commit 52df79f8b0
11 changed files with 648 additions and 123 deletions

View File

@@ -188,6 +188,7 @@ define('FROM_GETTING_STARTED', true);
<script src="/js/app.js"></script>
<script src="/js/settings.js"></script>
<script src="/js/getting_started.js"></script>
<script src="/js/starter_content.js"></script>
</body>

View File

@@ -3,17 +3,17 @@
/*
* Get started - Starter content: the packs Maintenance > Starter Content has,
* each with how much it would add, ticked to add (getting_started_content).
* Pick which, under each, lists what it would add - every item ticked, so the
* whole pack goes in unless some are unticked (picked[pack] says the list was
* there, items[pack][] what is ticked in it; js/getting_started.js keeps the
* pack's tick and count in step). Only what isn't here already is listed or
* added.
* Pick which, under each, lists what it would add as it will look, with
* Preview - every item ticked, so the whole pack goes in unless some are
* unticked (includes/starter_content_pick.php, js/starter_content.js). Only
* what isn't here already is listed or added.
*/
defined('FROM_GETTING_STARTED') || die("Direct file access is not allowed");
defined('FROM_STARTER_CONTENT') || define('FROM_STARTER_CONTENT', true);
require_once dirname(__DIR__) . '/post/starter_content_model.php';
require_once dirname(__DIR__) . '/includes/starter_content_pick.php';
$gs_packs = starterContentPacks();
@@ -26,11 +26,10 @@ $gs_packs = starterContentPacks();
$gs_items = starterContentItems($mysqli, $gs_key);
$gs_total = starterContentTotal($gs_key);
$gs_missing = count($gs_items);
$gs_all_here = $gs_missing === 0;
$gs_count = $gs_all_here ? '' : ($gs_missing < $gs_total ? "$gs_missing more to add" : "$gs_missing to add"); ?>
<div class="list-group-item<?= $gs_all_here ? ' is-here' : '' ?>" data-gs-pack="<?= $gs_key ?>">
$gs_all_here = $gs_missing === 0; ?>
<div class="list-group-item<?= $gs_all_here ? ' is-here' : '' ?>" data-gs-pack="<?= $gs_key ?>" data-starter-pick>
<div class="d-flex gap-3 align-items-start">
<input class="form-check-input flex-shrink-0 mt-1" type="checkbox" name="packs[]" value="<?= $gs_key ?>" id="gsPack_<?= $gs_key ?>"<?= $gs_all_here ? ' disabled' : ' checked' ?> data-gs-pack-check>
<input class="form-check-input flex-shrink-0 mt-1" type="checkbox" name="packs[]" value="<?= $gs_key ?>" id="gsPack_<?= $gs_key ?>"<?= $gs_all_here ? ' disabled' : ' checked' ?> data-starter-pack-check>
<span class="itflow-gs-pack-icon" aria-hidden="true"><i class="fas fa-fw <?= escapeHtml($gs_pack['icon']) ?>"></i></span>
<div class="min-w-0 flex-grow-1">
<label class="d-flex flex-wrap align-items-baseline gap-2 mb-0" for="gsPack_<?= $gs_key ?>">
@@ -38,36 +37,11 @@ $gs_packs = starterContentPacks();
<?php if ($gs_all_here) { ?>
<span class="small text-body-secondary"><i class="fas fa-check text-success me-1"></i>All <?= $gs_total ?> here</span>
<?php } else { ?>
<span class="small text-body-secondary" data-gs-pack-count data-gs-of="<?= $gs_missing ?>" data-gs-more="<?= $gs_missing < $gs_total ? 1 : 0 ?>"><?= $gs_count ?></span>
<span class="small text-body-secondary" data-starter-count data-starter-more="<?= $gs_missing < $gs_total ? 1 : 0 ?>"><?= $gs_missing < $gs_total ? "$gs_missing more to add" : "$gs_missing to add" ?></span>
<?php } ?>
</label>
<div class="small text-body-secondary itflow-gs-pack-about" title="<?= escapeHtml($gs_pack['description']) ?>"><?= escapeHtml($gs_pack['description']) ?></div>
<?php if (!$gs_all_here) { ?>
<details class="itflow-gs-pick mt-1" data-gs-pick>
<summary class="small">Pick which</summary>
<input type="hidden" name="picked[<?= $gs_key ?>]" value="1">
<div class="itflow-gs-pick-tools small mt-2">
<button type="button" class="btn btn-link btn-sm p-0 align-baseline" data-gs-pick-all>All</button>
<span class="text-body-secondary" aria-hidden="true">·</span>
<button type="button" class="btn btn-link btn-sm p-0 align-baseline" data-gs-pick-none>None</button>
</div>
<div class="itflow-gs-pick-list mt-2">
<div class="itflow-gs-pick-cols">
<?php $gs_group = null;
foreach ($gs_items as $gs_n => $gs_item) {
if ($gs_item['group'] !== '' && $gs_item['group'] !== $gs_group) {
$gs_group = $gs_item['group']; ?>
<div class="itflow-gs-pick-group"><?= escapeHtml($gs_group) ?></div>
<?php } ?>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="items[<?= $gs_key ?>][]" value="<?= escapeHtml($gs_item['key']) ?>" id="gsItem_<?= $gs_key ?>_<?= $gs_n ?>" checked data-gs-item>
<label class="form-check-label" for="gsItem_<?= $gs_key ?>_<?= $gs_n ?>"><?= escapeHtml($gs_item['name']) ?></label>
</div>
<?php } ?>
</div>
</div>
</details>
<?php } ?>
<?= starterPickHtml($gs_key, $gs_items) ?>
</div>
</div>
</div>
@@ -77,3 +51,4 @@ $gs_packs = starterContentPacks();
<button type="submit" name="getting_started_content" value="1" class="btn btn-primary" data-gs-save>Add the ones ticked<i class="fas fa-arrow-right ms-2"></i></button>
</form>
<?= starterPreviewModalHtml() ?>

View File

@@ -0,0 +1,281 @@
<?php
/*
* Pick which - choosing a starter content pack's items one by one, as
* Maintenance > Starter Content (admin/starter_content.php) and the Starter
* content step of Get started (admin/getting_started/content.php) both offer
* it. Each item is shown as it will look - a tag as its pill, a category as
* its chip, a product with its price - with what it is for, and a template or
* canned response has Preview, the whole of it in a window
* (admin/starter_content_preview.php answers it). js/starter_content.js keeps
* the ticks and counts in step and opens the previews.
*
* The form sends picked[pack] (the list was there) and items[pack][] (what is
* ticked, by starterItemKey()); the item lists come from
* starterContentItems() in post/starter_content_model.php, which has to be
* loaded first.
*/
defined('FROM_STARTER_CONTENT') || die("Direct file access is not allowed");
/* What an item is for, in a line - '' when the library says nothing. */
function starterItemAbout($pack, $row) {
switch ($pack) {
case 'categories':
return (string) ($row['description'] ?? '');
case 'tags':
return '';
case 'products':
return (string) ($row[5] ?? '');
}
return (string) ($row['description'] ?? '');
}
/*
* An item as it will look - a tag as its pill, a category as its chip, a
* product with its price and how it is billed, anything else its name.
*/
function starterItemLook($pack, $row) {
switch ($pack) {
case 'tags':
return tagPill(['tag_name' => $row[1], 'tag_color' => $row[2], 'tag_icon' => $row[3]]);
case 'categories':
return categoryChip(['category_name' => $row['name'], 'category_type' => $row['type'], 'category_color' => $row['color'], 'category_icon' => $row['icon']]);
case 'products':
[$frequency, $seat] = array_pad(explode(':', (string) ($row[6] ?? '')), 2, '');
return '<span class="fw-semibold">' . escapeHtml($row[0]) . '</span> <span class="text-body-secondary text-nowrap">'
. escapeHtml(billingMoney(floatval($row[3])) . ' · ' . productBilledText($frequency, $seat)) . '</span>';
}
return '<span class="fw-semibold">' . escapeHtml(starterItemName($pack, $row)) . '</span>';
}
/* The packs whose items have more to them than a line - shown whole by Preview. */
function starterPreviewPacks() {
return ['ticket_templates', 'project_templates', 'canned_responses', 'vendor_templates', 'document_templates', 'license_templates', 'products'];
}
/* [Words in brackets] in a template, marked as the bits filled in as it is used. */
function starterPreviewFills($html) {
return preg_replace('/\[([A-Za-z][A-Za-z0-9 ,.\'\/&;-]{0,40})\]/', '<span class="itflow-starter-fill">[$1]</span>', (string) $html);
}
/* A fact of a preview - its label and the value (html), or nothing when there isn't one. */
function starterPreviewFact($label, $html) {
return trim((string) $html) === '' ? '' : '<dt>' . escapeHtml($label) . '</dt><dd>' . $html . '</dd>';
}
/* What a template asks for, as a list - each with its kind, choices and whether it has to be answered. */
function starterPreviewFillIns(array $fill_ins) {
if (!$fill_ins) {
return '';
}
$kinds = ['text' => 'text', 'textarea' => 'longer text', 'date' => 'a date', 'number' => 'a number', 'yesno' => 'yes or no', 'list' => 'one of', 'email' => 'an email address'];
$html = '<h3 class="itflow-starter-preview-head">What it asks for</h3><ul class="itflow-starter-preview-list">';
foreach ($fill_ins as $fill_in) {
$type = (string) ($fill_in['type'] ?? 'text');
$options = (array) ($fill_in['options'] ?? []);
$kind = $kinds[$type] ?? $type;
if ($type === 'list' && $options) {
$kind .= ' ' . implode(', ', array_map('strval', $options));
}
$html .= '<li><span class="fw-semibold">' . escapeHtml((string) $fill_in['name']) . '</span> <span class="text-body-secondary">- ' . escapeHtml($kind)
. (!empty($fill_in['required']) ? ', needed' : '') . '</span>'
. (!empty($fill_in['help']) ? '<div class="small text-body-secondary">' . escapeHtml((string) $fill_in['help']) . '</div>' : '') . '</li>';
}
return $html . '</ul>';
}
/* A web address as a link out - its host and path as given, https:// in front. */
function starterPreviewLink($address) {
$address = trim((string) $address);
if ($address === '') {
return '';
}
$url = preg_match('#^https?://#i', $address) ? $address : "https://$address";
return '<a href="' . escapeHtml($url) . '" target="_blank" rel="noopener noreferrer">' . escapeHtml($address) . '<i class="fas fa-external-link-alt small ms-1"></i></a>';
}
/*
* An item whole, for Preview - what it says and sets up, as it will come in.
* '' for a pack whose items are all there is to them (starterItemLook()).
*/
function starterItemPreview($pack, $row) {
$about = starterItemAbout($pack, $row);
$html = $about !== '' && $pack !== 'products' ? '<p class="text-body-secondary">' . escapeHtml($about) . '</p>' : '';
switch ($pack) {
case 'ticket_templates':
$category = !empty($row['category']) ? categoryChip(['category_name' => $row['category']['name'], 'category_type' => 'Ticket', 'category_color' => $row['category']['color'] ?? '', 'category_icon' => $row['category']['icon'] ?? '']) : '';
$tags = implode(' ', array_map(function ($tag) {
return tagPill(['tag_name' => $tag['name'], 'tag_color' => $tag['color'] ?? '', 'tag_icon' => $tag['icon'] ?? '']);
}, (array) ($row['tags'] ?? [])));
$due = intval($row['due_hours'] ?? 0);
$portal = !empty($row['portal']) ? '<span class="fw-semibold">' . escapeHtml((string) $row['portal']['title']) . '</span> - ' . escapeHtml((string) ($row['portal']['description'] ?? ''))
. '<div class="small text-body-secondary">A request form in the client portal - it comes in turned off.</div>' : '';
$html .= '<dl class="itflow-starter-preview-facts">'
. starterPreviewFact('Subject', starterPreviewFills(escapeHtml((string) $row['subject'])))
. starterPreviewFact('Category', $category)
. starterPreviewFact('Priority', escapeHtml((string) ($row['priority'] ?? '')))
. starterPreviewFact('Due', $due ? ($due % 24 === 0 ? ($due / 24) . ' day' . ($due === 24 ? '' : 's') : "$due hour" . ($due === 1 ? '' : 's')) . ' after it is raised' : '')
. starterPreviewFact('Tags', $tags)
. starterPreviewFact('Clients ask with', $portal)
. '</dl>';
$html .= '<h3 class="itflow-starter-preview-head">What it says</h3><div class="itflow-starter-preview-doc">' . starterPreviewFills($row['details'] ?? '') . '</div>';
$html .= starterPreviewFillIns((array) ($row['fill_ins'] ?? []));
if (!empty($row['tasks'])) {
$minutes = array_sum(array_map(function ($task) {
return intval($task['estimate'] ?? 0);
}, $row['tasks']));
$html .= '<h3 class="itflow-starter-preview-head">Tasks <span class="fw-normal text-body-secondary">- ' . count($row['tasks']) . ', about ' . escapeHtml(starterMinutesText($minutes)) . '</span></h3><ol class="itflow-starter-preview-list">';
foreach ($row['tasks'] as $task) {
$when = !empty($task['when']) ? ' <span class="badge rounded-pill bg-body-tertiary text-body border fw-normal">when ' . escapeHtml($task['when']['fill_in'] . ' is ' . $task['when']['value']) . '</span>' : '';
$html .= '<li>' . starterPreviewFills(escapeHtml((string) $task['name'])) . $when
. (!empty($task['estimate']) ? ' <span class="text-body-secondary small">' . intval($task['estimate']) . ' min</span>' : '') . '</li>';
}
$html .= '</ol>';
}
return $html;
case 'canned_responses':
$types = ['internal' => 'An internal note', 'public' => 'A public note', 'email' => 'An email reply'];
$categories = implode(' ', array_map(function ($category) {
return categoryChip(['category_name' => $category['name'], 'category_type' => 'Ticket', 'category_color' => $category['color'] ?? '', 'category_icon' => $category['icon'] ?? '']);
}, (array) ($row['categories'] ?? [])));
$html .= '<dl class="itflow-starter-preview-facts">'
. starterPreviewFact('Goes in as', escapeHtml($types[$row['reply_type'] ?? ''] ?? ucfirst((string) ($row['reply_type'] ?? ''))))
. starterPreviewFact('Offered on', $categories !== '' ? $categories : '<span class="text-body-secondary">Every ticket</span>')
. starterPreviewFact('Sets the status to', escapeHtml((string) ($row['status'] ?? '')))
. starterPreviewFact('Adds time worked', !empty($row['minutes']) ? intval($row['minutes']) . ' minutes' : '')
. '</dl>';
return $html . '<h3 class="itflow-starter-preview-head">What it says</h3><div class="itflow-starter-preview-doc">' . starterPreviewFills($row['body'] ?? '') . '</div>';
case 'project_templates':
$html .= '<h3 class="itflow-starter-preview-head">Its tickets, in order</h3><ol class="itflow-starter-preview-list">';
foreach ((array) ($row['ticket_templates'] ?? []) as $ticket_template) {
$html .= '<li>' . escapeHtml((string) $ticket_template) . '</li>';
}
return $html . '</ol><p class="small text-body-secondary mb-0">Each from the ticket template of that name - one that isn\'t here is left out.</p>';
case 'vendor_templates':
$html .= '<dl class="itflow-starter-preview-facts">'
. starterPreviewFact('Support site', starterPreviewLink($row['website'] ?? ''))
. starterPreviewFact('Status page', starterPreviewLink($row['status_page'] ?? ''))
. starterPreviewFact('Cases opened at', starterPreviewLink($row['portal'] ?? ''))
. '</dl>';
return $html . (!empty($row['notes']) ? '<h3 class="itflow-starter-preview-head">What support asks for</h3><div class="itflow-starter-preview-notes">' . nl2br(escapeHtml((string) $row['notes'])) . '</div>' : '');
case 'document_templates':
$review = intval($row['review_months'] ?? 0);
$html .= '<dl class="itflow-starter-preview-facts">'
. starterPreviewFact('Reviewed', $review ? 'Every ' . ($review === 1 ? 'month' : "$review months") : '')
. '</dl>';
$html .= starterPreviewFillIns((array) ($row['fill_ins'] ?? []));
return $html . '<h3 class="itflow-starter-preview-head">What it says</h3><div class="itflow-starter-preview-doc">' . starterPreviewFills($row['content'] ?? '') . '</div>';
case 'license_templates':
$html .= '<dl class="itflow-starter-preview-facts">'
. starterPreviewFact('Type', escapeHtml((string) ($row['type'] ?? '')))
. starterPreviewFact('Licensed per', escapeHtml((string) ($row['license_type'] ?? '')))
. starterPreviewFact('Renews every', escapeHtml(SOFTWARE_RENEWAL_TERMS[(string) ($row['term'] ?? '')] ?? ''))
. starterPreviewFact('Vendor', escapeHtml((string) ($row['vendor']['name'] ?? '')))
. starterPreviewFact('Sold as', escapeHtml((string) ($row['product'] ?? '')))
. '</dl>';
return $html . (!empty($row['notes']) ? '<h3 class="itflow-starter-preview-head">What to know</h3><div class="itflow-starter-preview-notes">' . nl2br(escapeHtml((string) $row['notes'])) . '</div>' : '');
case 'products':
[$frequency, $seat] = array_pad(explode(':', (string) ($row[6] ?? '')), 2, '');
$types = productTypes();
return '<p class="text-body-secondary">' . escapeHtml((string) $row[5]) . '</p><dl class="itflow-starter-preview-facts">'
. starterPreviewFact('Type', escapeHtml($types[$row[1]]['label'] ?? ucfirst((string) $row[1])))
. starterPreviewFact('Code', escapeHtml((string) $row[2]))
. starterPreviewFact('Price', escapeHtml(billingMoney(floatval($row[3]))) . (floatval($row[3]) == 0 ? ' <span class="text-body-secondary">- priced on each quote</span>' : ''))
. starterPreviewFact('Billed', escapeHtml(productBilledText($frequency, $seat)))
. starterPreviewFact('Income category', escapeHtml((string) $row[4]))
. starterPreviewFact('Becomes', !empty($row[7]) ? escapeHtml('A client asset (' . $row[7] . ') when sold' . (!empty($row[8]) ? ', with ' . intval($row[8]) . ' months of warranty' : '')) : '')
. '</dl><p class="small text-body-secondary mb-0">Prices are starting points - change them once it is here.</p>';
}
return '';
}
/* Minutes as words without the business-hours qualifier - "3 hours 15 minutes". */
function starterMinutesText($minutes) {
$minutes = intval($minutes);
if ($minutes < 60) {
return $minutes . ' minute' . ($minutes === 1 ? '' : 's');
}
$hours = intdiv($minutes, 60);
$rest = $minutes % 60;
return $hours . ' hour' . ($hours === 1 ? '' : 's') . ($rest ? " $rest minutes" : '');
}
/*
* A pack's Pick which - its items not here yet, each ticked, shown as they
* will look with what they are for, and Preview where there is more to see.
* $form is the id of the form the ticks go with, when they sit outside it (a
* table row's Add). $items from starterContentItems().
*/
function starterPickHtml($pack, array $items, $form = '') {
if (!$items) {
return '';
}
$form_attr = $form !== '' ? ' form="' . escapeHtml($form) . '"' : '';
$previews = in_array($pack, starterPreviewPacks(), true);
$kind = $pack === 'tags' ? ' is-pills' : '';
ob_start(); ?>
<details class="itflow-starter-pick mt-1" data-starter-pick-list>
<summary class="small">Pick which</summary>
<input type="hidden" name="picked[<?= escapeHtml($pack) ?>]" value="1"<?= $form_attr ?>>
<div class="itflow-starter-pick-tools small mt-2">
<button type="button" class="btn btn-link btn-sm p-0 align-baseline" data-starter-all>All</button>
<span class="text-body-secondary" aria-hidden="true">·</span>
<button type="button" class="btn btn-link btn-sm p-0 align-baseline" data-starter-none>None</button>
</div>
<div class="itflow-starter-pick-box mt-2">
<div class="itflow-starter-pick-items<?= $kind ?>">
<?php $group = null;
foreach ($items as $n => $item) {
if ($item['group'] !== '' && $item['group'] !== $group) {
$group = $item['group']; ?>
<div class="itflow-starter-pick-group"><?= escapeHtml($group) ?></div>
<?php }
$id = 'starterItem_' . $pack . '_' . $n;
$about = $pack === 'products' ? '' : starterItemAbout($pack, $item['row']); ?>
<div class="itflow-starter-item" data-starter-item-row>
<input class="form-check-input flex-shrink-0" type="checkbox" name="items[<?= escapeHtml($pack) ?>][]" value="<?= escapeHtml($item['key']) ?>" id="<?= escapeHtml($id) ?>" checked data-starter-item<?= $form_attr ?>>
<label class="itflow-starter-item-label min-w-0" for="<?= escapeHtml($id) ?>">
<?= starterItemLook($pack, $item['row']) ?>
<?php if ($about !== '') { ?>
<span class="d-block small text-body-secondary itflow-starter-item-about"><?= escapeHtml($about) ?></span>
<?php } ?>
</label>
<?php if ($previews) { ?>
<button type="button" class="btn btn-link btn-sm text-decoration-none flex-shrink-0 ms-auto py-0" data-starter-preview="<?= escapeHtml($pack) ?>" data-starter-key="<?= escapeHtml($item['key']) ?>"
title="See all of <?= escapeHtml($item['name']) ?>"><i class="far fa-eye me-1"></i>Preview</button>
<?php } ?>
</div>
<?php } ?>
</div>
</div>
</details>
<?php return ob_get_clean();
}
/* The window Preview opens - once on a page. */
function starterPreviewModalHtml() {
ob_start(); ?>
<div class="modal fade" id="starterPreview" tabindex="-1" aria-labelledby="starterPreviewTitle" aria-hidden="true" data-starter-preview-url="/admin/starter_content_preview.php">
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title h5" id="starterPreviewTitle">Preview</h2>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body itflow-starter-preview" data-starter-preview-body></div>
<div class="modal-footer">
<span class="small text-body-secondary me-auto">As it comes in - yours to change once it is here.</span>
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<?php return ob_get_clean();
}

View File

@@ -22,12 +22,23 @@ if (isset($_POST['load_starter_content'])) {
redirect();
}
// One pack's Add takes the items ticked under its Pick which
// (includes/starter_content_pick.php) - the whole pack when the list wasn't sent
$only = null;
if ($requested_pack !== 'all' && isset($_POST['picked'][$requested_pack])) {
$only = array_map('strval', array_filter((array) ($_POST['items'][$requested_pack] ?? []), 'is_string'));
if (!$only) {
flashAlert("Nothing is ticked under Pick which - tick what to add", 'info');
redirect();
}
}
// Registry order is load order - categories before products, ticket
// templates before the project templates that link to them
$items_added = 0;
$summary = [];
foreach ($selected_packs as $pack) {
$pack_added = starterContentLoad($mysqli, $pack);
$pack_added = starterContentLoad($mysqli, $pack, false, $only);
if ($pack_added) {
$items_added = $items_added + $pack_added;
$summary[] = "$pack_added " . $starter_content_packs[$pack]['label'];

View File

@@ -276,9 +276,10 @@ function starterOnly($pack, array $rows, $only) {
// ------------------------------
// starterContentItems
// What loading a pack would add - its items not here yet, each as key, name
// and group - for picking which. Here is as each loader judges it: the same
// name (and type, for a category or tag), whatever its case.
// What loading a pack would add - its items not here yet, each as key, name,
// group and the library's row - for picking which. Here is as each loader
// judges it: the same name (and type, for a category or tag), whatever its
// case.
// ------------------------------
function starterContentItems($mysqli, $pack) {
switch ($pack) {
@@ -316,7 +317,7 @@ function starterContentItems($mysqli, $pack) {
$items = [];
foreach (starterContentRows($pack) as $row) {
if (!$here($row)) {
$items[] = ['key' => starterItemKey($pack, $row), 'name' => starterItemName($pack, $row), 'group' => starterItemGroup($pack, $row)];
$items[] = ['key' => starterItemKey($pack, $row), 'name' => starterItemName($pack, $row), 'group' => starterItemGroup($pack, $row), 'row' => $row];
}
}
return $items;

View File

@@ -5,9 +5,19 @@ 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";
require_once "includes/starter_content_pick.php";
$starter_content_packs = starterContentPacks();
$starter_content_status = starterContentStatus($mysqli);
// Each pack's items not here yet - for Pick which, and how many Add brings
$starter_content_items = [];
$starter_content_status = [];
foreach ($starter_content_packs as $pack => $pack_details) {
$starter_content_items[$pack] = starterContentItems($mysqli, $pack);
$total = starterContentTotal($pack);
$missing = count($starter_content_items[$pack]);
$starter_content_status[$pack] = ['total' => $total, 'missing' => $missing, 'present' => $total - $missing];
}
$demo_data_status = demoDataStatus($mysqli);
$demo_data_loaded = count($demo_data_status['loaded']);
@@ -42,7 +52,8 @@ foreach ($starter_content_status as $pack_status) {
<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
adding twice never produces duplicates. Pick which, under a pack, lists what it would add - as it will
look, with a preview of each template - to add only some of it. Everything added is editable and deletable
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.
</p>
@@ -62,8 +73,8 @@ foreach ($starter_content_status as $pack_status) {
<?php foreach ($starter_content_packs as $pack => $pack_details) {
$pack_status = $starter_content_status[$pack];
?>
<tr>
<td>
<tr data-starter-pick>
<td class="itflow-starter-pack-cell">
<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
@@ -74,12 +85,13 @@ foreach ($starter_content_status as $pack_status) {
?>
<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 } ?>
<?= starterPickHtml($pack, $starter_content_items[$pack], "starterAdd_$pack") ?>
</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>
<span class="badge bg-warning text-dark p-2" data-starter-count data-starter-format="short"><?= intval($pack_status['missing']) ?></span>
<?php } else { ?>
<span class="text-success"><i class="fas fa-fw fa-check"></i></span>
<?php } ?>
@@ -87,9 +99,9 @@ foreach ($starter_content_status as $pack_status) {
<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">
<form action="post.php" method="POST" autocomplete="off" id="starterAdd_<?= escapeHtml($pack) ?>">
<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">
<button type="submit" name="load_starter_content" value="<?= escapeHtml($pack) ?>" class="btn btn-primary btn-sm text-nowrap" data-starter-add>
<i class="fas fa-fw fa-plus me-2"></i>Add
</button>
</form>
@@ -210,4 +222,7 @@ foreach ($starter_content_status as $pack_status) {
</div>
</div>
<?= starterPreviewModalHtml() ?>
<script src="/js/starter_content.js"></script>
<?php require_once "../includes/footer.php"; ?>

View File

@@ -0,0 +1,40 @@
<?php
/*
* A starter content item whole, for Preview under Pick which
* (admin/includes/starter_content_pick.php) - ?pack=&item= (its
* starterItemKey()), answered in JSON as its name and the preview's html.
* Admins only, as the two pages that ask are.
*/
require_once $_SERVER['DOCUMENT_ROOT'] . '/config.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/functions.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/includes/check_login.php';
enforceAdminPermission();
define('FROM_STARTER_CONTENT', true);
require_once __DIR__ . '/post/starter_content_model.php';
require_once __DIR__ . '/includes/starter_content_pick.php';
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store');
$pack = (string) ($_GET['pack'] ?? '');
$key = (string) ($_GET['item'] ?? '');
$row = null;
if (isset(starterContentPacks()[$pack]) && in_array($pack, starterPreviewPacks(), true)) {
foreach (starterContentRows($pack) as $candidate) {
if (starterItemKey($pack, $candidate) === $key) {
$row = $candidate;
break;
}
}
}
if (!$row) {
http_response_code(404);
echo json_encode(['error' => 'There is no such starter content.']);
exit;
}
echo json_encode(['title' => starterItemName($pack, $row), 'pack' => starterContentPacks()[$pack]['label'], 'html' => starterItemPreview($pack, $row)]);