diff --git a/CHANGELOG.md b/CHANGELOG.md index f0e80b8c8..034956f42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,9 +6,9 @@ This file documents all notable changes made to ITFlow. ### Upgrading to 26.11 -Update from Maintenance > Update — Queue Update hands the job to cron, which updates the files and applies this release's database changes (2.7.9 to 3.2.5) in the same pass. Nothing else is required. +Update from Maintenance > Update — Queue Update hands the job to cron, which updates the files and applies this release's database changes (2.7.9 to 3.2.6) in the same pass. Nothing else is required. -This is a big release, with forty-six database steps in one go, so take a backup before you update. +This is a big release, with forty-seven database steps in one go, so take a backup before you update. The update also clears out data that deletes in older versions left behind, all of it pointing at something that no longer exists: @@ -47,7 +47,7 @@ Ticket templates keep their subject, details and tasks, and set nothing else on Canned responses keep their names, what they say and their ticket category. One whose category had been deleted is offered on every ticket, as it already was. How often each is used is counted from the update. None sets up anything on the reply until you give it a reply type, status or time worked on its Sets up tab. Every canned response already there is shared - offered to everyone, as before - and none has files. -Document templates keep their names, summaries, what they say and their images. Documents made from a template before the update don't know which one, so a template's use is counted from the update. +Document templates keep their names, summaries, what they say and their images. Words in square brackets already in them are now filled in when a document is made: [Client], [Site], [Contact], [Date] and the others listed under New Features (Document templates) from the client, and the rest are asked for as plain, optional text until you set them up on the template's page - one left blank stays as it is, as before. Documents made from a template before the update don't know which one, so a template's use is counted from the update. If you are coming from 26.08 or earlier, the database update still has to be run from the command line one time, exactly as described under Upgrading to 26.09 below. @@ -527,9 +527,13 @@ If you are coming from 26.08 or earlier, the database update still has to be run - The New Canned Response button's menu has Import, Export, the report and the starter ones. - **Document templates** (rebuilt) - Admin > Document Templates lists what each is for, how many documents have been made from it and for how many clients, when it was last used and when it last changed, and by whom - searched by name, summary or what it says, sorted from the column headings, a page at a time, with Active and Archived tabs. - - A template's page shows what it says, with the words in square brackets left to fill in marked and listed, who made it and last changed it, its images, and the latest documents made from it with their client. New document for a client, from there or the list, makes one for the client you pick and opens it. + - A template's page shows what it says, with its words in square brackets marked - filled in from the client, a table of records, or asked for - who made it and last changed it, its images, and the latest documents made from it with their client. New document for a client, from there or the list, makes one for the client you pick and opens it. + - Filled in from the client: [Client], [Website], [Site], [Address], [Site Phone], [Contact], [First Name], [Contact Email], [Contact Phone], [Agent], [Date], [Month], [Quarter] and [Year] in a template's name, summary and what it says are filled in when a document is made from it. The site and contact are the client's primary ones, unless you pick another - or none, to leave them to fill in - when you make it. One with nothing to put in (a client with no website) stays as it is. + - Tables of records: [Contacts], [Locations], [Assets], [Servers], [Workstations], [Network Equipment], [Printers], [Networks], [Domains], [Certificates], [Licenses], [Vendors] and [Services] put in the client's records as they are when the document is made. On a line of its own, it becomes a table - [Networks] gives each network's VLAN, network, gateway, DHCP range and site, [Servers] each server's operating system, IP address and site, [Domains] each domain's registrar and expiry - with an empty row to fill in when the client has none. In a sentence, it becomes their names. The template's page lists the ones it uses and what each holds, and the template window has a button for each placeholder and table to put it in. + - Fill-ins: any other word in square brackets in a template's name, summary or what it says - [Firewall Model], [Review Date] - is asked for when a document is made, and the answer goes in wherever it is. On the template's page each fill-in can be made a date, a choice from a list or yes or no, made required, given a line of help, renamed (it is renamed in the template too) and dragged into the order it is asked in. One left blank stays for whoever writes the document. + - New document for a client and Document from template show what the template fills in from the client, the site and contact to use, and its fill-ins, as soon as the template and client are picked. A copied template keeps its fill-ins. - Templates can be copied (with their images), archived - not offered for new documents, while the documents made from them stay - restored and deleted. A document made from one remembers it, and its page says which template it came from. - - The template window has its icons, a summary, and Draft it with AI when an AI model is set up for documentation. + - The template window has its icons, a summary, and Draft it with AI when an AI model is set up for documentation. A draft uses the placeholders and tables of records, and puts what else is to be filled in in square brackets. - Document from template in a client's Files shows each template's summary and how often it has been used. A document made with no name or summary given takes the template's. - **Assets** - The asset list has: @@ -1509,6 +1513,10 @@ If you are coming from 26.08 or earlier, the database update still has to be run - New `functions/document_template.php`: `documentTemplateDir()`, `documentTemplateRow($id)` (with `created_by_name` and `updated_by_name`), `documentTemplateUsage($ids)` (`count`, `clients`, `last` - documents not archived), `documentTemplateFields($post, &$error)` (`name`, `description`, `content`), `documentTemplateSave($id, $fields)` (images through `saveBase64Images()`, the unused ones through `cleanupUnusedImages()`; a new one's first save leaves `document_template_updated_at` empty), `documentTemplateCopyImages()`, `documentTemplateCopy($id)`, `documentTemplateDelete($id)` (clears the template from its documents and takes its folder away), `documentTemplateMakeDocument($id, $client_id, $name, $description, $folder_id)` (the template's name and summary when none given), `documentTemplatePreview()`, `documentTemplateBracketsIn()` and `documentTemplateMarkBrackets()`. - Admin: `admin/document_templates.php` takes `q`, `sort` - `document_template_name`, `used_count`, `last_used` or `changed_at` - `order`, `page` and `show=archived`. `admin/document_template.php` is rebuilt on the ticket template page's layout. `admin/modals/document_template/document_template_form.php` is shared by `document_template_add.php` and `document_template_edit.php` (`id`); new `document_template_use.php` (`id`) posts `add_document_from_template` to `agent/post.php`. `admin/post/document_template.php`: `add_document_template` (redirects to its page), `edit_document_template`, GET `copy_document_template`, `archive_document_template`, `restore_document_template` and `delete_document_template` (`from_page`). Audit entries are Document Template Create, Edit, Archive, Restore and Delete. - `add_document_from_template` makes the document with `documentTemplateMakeDocument()`, checks the client exists and redirects to `/agent/document.php`. `agent/modals/document/document_add_from_template.php` takes `document_template_id` to pick one, and shows each one's use. + - Database 3.2.6: new `document_template_fill_ins` (`document_fill_in_id`, `document_fill_in_document_template_id` - deleted with its template - `document_fill_in_name`, unique per template, `document_fill_in_type` - `text`, `date`, `list` or `yesno` - `document_fill_in_options`, one per line, `document_fill_in_required`, `document_fill_in_help` and `document_fill_in_order`). + - `functions/document_template.php`: `DOCUMENT_TEMPLATE_PLACEHOLDERS`, `DOCUMENT_TEMPLATE_SITE_PLACEHOLDERS`, `DOCUMENT_TEMPLATE_CONTACT_PLACEHOLDERS` and `DOCUMENT_TEMPLATE_TABLES` (name => what it is); `documentFillInName()` ('' for a placeholder's or table's name), `documentTemplateWordKind($word)` (`placeholder`, `table` or `fill_in`), `documentTemplateBracketsInAll($template)` and `documentTemplateWordsUsed($template)` (its name, summary and content), `documentTemplateFillInRowsFor($ids)`, `documentTemplateFillIns($template, $rows)` and `documentTemplateFillInsFor($id)` (the ones set up, then the rest in the text as optional text, each with `in_text`) - in the shape `ticketFillInAnswers()` and `ticketFillInFieldHtml()` take - `documentTemplateRenameFillIn()`, `documentTemplateClientLocations()` and `documentTemplateClientContacts()` (primary first), `documentTemplateValues($client_id, $location_id, $contact_id, $agent_name)` (-1 for none, 0 or another client's for the primary), `documentTemplateTableData($table, $client_id)` (`head`, `rows`, `names`), `documentTemplateTableHtml()` and `documentTemplateFill($html, $client_id, $values)`. `documentTemplateMakeDocument()` takes `$location_id`, `$contact_id` and `$answers` (from `ticketFillInAnswers()`) after `$folder_id`, and fills the name, summary and content. `documentTemplateCopy()` copies the fill-ins, and `documentTemplateMarkBrackets()` marks each kind in its own color. + - Admin: new `admin/modals/document_template/document_template_fill_in.php` (`document_template_id`, `name`). `admin/post/document_template.php`: `save_document_template_fill_in` (`original_name` to change or rename one), GET `delete_document_template_fill_in` (reset while still in the text, otherwise removed) and POST `reorder_document_template_fill_ins` (`names[]`, answers JSON). Audit entries are Document Template Edit. + - `agent/ajax.php?document_template_asks=ID&client_id=ID` returns `{html}` - what the template fills in, the `location_id` and `contact_id` pickers when it uses them, and its fill-ins as `fill_ins[Name]` - or '' without Support at edit level, access to the client, or a live template. `add_document_from_template` checks the answers with `ticketFillInAnswers()` and takes `location_id` and `contact_id`. New `agent/js/document_template_make.js` draws it in a form with `data-doc-template-make`, from its `data-doc-template-select` and `data-doc-template-client` fields into `data-doc-template-asks`; new `js/document_template_form.js` puts a `data-document-template-word` button's word in the editor (on a line of its own with `data-document-template-table`). `ai_create_document_template` tells the model the placeholders and tables. - Empty states: every list page sets `$empty_state` before including `includes/filter_footer.php`, and `includes/inc_empty_state.php` draws it with `emptyStateHtml()` (`functions/format.php`) when the list has nothing - `thing`, `icon`, `title`, `text`, `actions` (`label`, `modal` or `href`, `size`, `icon`; the first is the main one), `archived`, `view`, `filtered`, `views`, `match_title`, `match_text` and `clear_url`. `emptyStateFiltered($views)` reads the GET string for a search or filter, leaving out paging, sorting, `client_id`, `archived` and the page's views, and `emptyStateClearUrl($views)` is the page without them. `emptyStatePill($pills, $key, $default, $good)` builds a pill's own state from the page's pill array (`label`, `icon`, `title`, `alarm`), with a tick for `alarm` pills and the keys in `$good`. Files, notifications and racks call `emptyStateHtml()` in their own layout; files and notifications set `$empty_state_skip`. A page that sets nothing still gets one, worded from `$page_title`. The pages' own empty blocks (`.itflow-asset-empty` under a list) are gone. CSS: `.itflow-empty-state` with `-icon` (a grey round tile), `-title`, `-text` and `-actions`. - Removed unused helpers and files: - `credentialLink()`, `projectLink()`, `linkCounts()`, `assetContactAvatar()`, `clientOverviewPersonInitials()`, and `getMonthlyTax()` and `getQuarterlyTax()`, which looked a tax up by name diff --git a/admin/database_updates/3.2.6.php b/admin/database_updates/3.2.6.php new file mode 100644 index 000000000..fabafa2ae --- /dev/null +++ b/admin/database_updates/3.2.6.php @@ -0,0 +1,24 @@ + 0, 'clients' => 0, 'last' => null]; $content = trim((string) $template['document_template_content']) !== '' ? documentTemplateMarkBrackets(documentHtmlPurify($template['document_template_content'])) : ''; -$brackets = documentTemplateBracketsIn($template['document_template_content']); +$used = documentTemplateWordsUsed($template); +$fill_ins = documentTemplateFillIns($template, documentTemplateFillInRowsFor([$document_template_id])[$document_template_id]); +$fill_in_url = "modals/document_template/document_template_fill_in.php?document_template_id=$document_template_id"; $images = is_dir(documentTemplateDir($document_template_id)) ? count(glob(documentTemplateDir($document_template_id) . '/*')) : 0; $csrf = $_SESSION['csrf_token']; @@ -64,7 +68,7 @@ $use_url = "modals/document_template/document_template_use.php?id=$document_temp
Asked for when a document is made from it - each answer goes in wherever its [Name] is, and one left blank stays for whoever writes it. Drag to change the order they are asked in.
+[" . escapeHtml($name) . "] in what it says where the answer goes" : ''));
+
+ redirect();
+
+}
+
+if (isset($_GET['delete_document_template_fill_in'])) {
+
+ // A fill-in's setup removed - gone, or, while its [Name] is still in the text, asked as plain optional text
+ validateCSRFToken();
+
+ $fill_in_id = intval($_GET['delete_document_template_fill_in']);
+ $fill_in = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT document_fill_in_name, document_fill_in_document_template_id FROM document_template_fill_ins WHERE document_fill_in_id = $fill_in_id"));
+ $row = documentTemplateForRequest($fill_in['document_fill_in_document_template_id'] ?? 0);
+ $document_template_id = intval($row['document_template_id']);
+ $name = (string) $fill_in['document_fill_in_name'];
+
+ mysqli_query($mysqli, "DELETE FROM document_template_fill_ins WHERE document_fill_in_id = $fill_in_id");
+
+ $still_asked = false;
+ foreach (documentTemplateFillInsFor($document_template_id) as $other) {
+ $still_asked = $still_asked || strtolower($other['name']) === strtolower($name);
+ }
+
+ logAudit("Document Template", "Edit", "$session_name removed fill-in " . escapeSql($name) . " from document template " . escapeSql($row['document_template_name']), 0, $document_template_id);
+
+ flashAlert($still_asked
+ ? "Fill-in " . escapeHtml($name) . " reset - [" . escapeHtml($name) . "] is still in what it says, so it is asked for as plain, optional text"
+ : "Fill-in " . escapeHtml($name) . " removed", 'error');
+
+ redirect();
+
+}
+
+if (isset($_POST['reorder_document_template_fill_ins'])) {
+
+ // Dragged into order on the template's page - answers with JSON, as the page saves it in the background.
+ // Every one dragged keeps its place from now on, so each gets a row.
+ header('Content-Type: application/json');
+ if (!hash_equals((string) ($_SESSION['csrf_token'] ?? ''), (string) ($_POST['csrf_token'] ?? ''))) {
+ echo json_encode(['ok' => false, 'error' => 'The page is out of date - reload it']);
+ exit;
+ }
+ $row = documentTemplateRow($_POST['document_template_id'] ?? 0);
+ if (!$row) {
+ echo json_encode(['ok' => false, 'error' => 'That document template no longer exists']);
+ exit;
+ }
+ $document_template_id = intval($row['document_template_id']);
+ $fill_ins = [];
+ foreach (documentTemplateFillInsFor($document_template_id) as $fill_in) {
+ $fill_ins[strtolower($fill_in['name'])] = $fill_in;
+ }
+ $order = 0;
+ foreach ((array) ($_POST['names'] ?? []) as $name) {
+ $fill_in = $fill_ins[strtolower((string) $name)] ?? null;
+ if (!$fill_in) {
+ continue;
+ }
+ $order++;
+ if ($fill_in['id']) {
+ mysqli_query($mysqli, "UPDATE document_template_fill_ins SET document_fill_in_order = $order WHERE document_fill_in_id = " . intval($fill_in['id']));
+ } else {
+ mysqli_query($mysqli, "INSERT INTO document_template_fill_ins SET document_fill_in_document_template_id = $document_template_id, document_fill_in_name = '" . escapeSql($fill_in['name']) . "', document_fill_in_type = 'text', document_fill_in_order = $order");
+ }
+ }
+ logAudit("Document Template", "Edit", "$session_name reordered the fill-ins on document template " . escapeSql($row['document_template_name']), 0, $document_template_id);
+
+ echo json_encode(['ok' => true]);
+ exit;
+
+}
diff --git a/admin/post/ticket_template.php b/admin/post/ticket_template.php
index 53caf0626..539d64a30 100644
--- a/admin/post/ticket_template.php
+++ b/admin/post/ticket_template.php
@@ -250,7 +250,7 @@ if (isset($_POST['save_ticket_template_fill_in'])) {
logAudit("Ticket Template", "Edit", "$session_name " . ($original === '' ? 'added' : 'changed') . " fill-in " . escapeSql($name) . " on ticket template $template_name", 0, $ticket_template_id);
- flashAlert("Fill-in " . escapeHtml($name) . " saved" . ($original === '' ? " - put [" . escapeHtml($name) . "] in the text where the answer goes" : ''));
+ flashAlert("Fill-in " . escapeHtml($name) . " saved" . ($original === '' ? " - put [" . escapeHtml($name) . "] in the text where the answer goes" : ''));
redirect();
@@ -278,7 +278,7 @@ if (isset($_GET['delete_ticket_template_fill_in'])) {
logAudit("Ticket Template", "Edit", "$session_name removed fill-in " . escapeSql($name) . " from ticket template " . escapeSql($row['ticket_template_name']), 0, $ticket_template_id);
flashAlert($still_asked
- ? "Fill-in " . escapeHtml($name) . " reset - [" . escapeHtml($name) . "] is still in the text, so it is asked for as plain, optional text"
+ ? "Fill-in " . escapeHtml($name) . " reset - [" . escapeHtml($name) . "] is still in the text, so it is asked for as plain, optional text"
: "Fill-in " . escapeHtml($name) . " removed", 'error');
redirect();
diff --git a/agent/ajax.php b/agent/ajax.php
index 49d04df70..96680d372 100644
--- a/agent/ajax.php
+++ b/agent/ajax.php
@@ -1025,6 +1025,72 @@ if (isset($_GET['global_search'])) {
* than rendered into every ticket page, because the bodies are full HTML replies and an
* install can have a lot of them.
*/
+/*
+ * What making a document from a template asks for, for a client - the site
+ * and contact its placeholders come from (when it uses them; the client's
+ * primary ones picked), and its fill-ins - as the window's fields (HTML), with
+ * a line on what is filled in from the client. agent/js/document_template_make.js
+ * draws it in the Document from template and New document for a client windows.
+ */
+if (isset($_GET['document_template_asks'])) {
+ header('Content-Type: application/json');
+
+ $asks_template = documentTemplateRow($_GET['document_template_asks']);
+ $asks_client_id = intval($_GET['client_id'] ?? 0);
+ if (intval(lookupUserPermission('module_support')) < 2 || !$asks_template || $asks_template['document_template_archived_at'] || !$asks_client_id || !hasClientAccess($asks_client_id)) {
+ echo json_encode(['html' => '']);
+ exit;
+ }
+ $asks_used = documentTemplateWordsUsed($asks_template);
+ $asks_fill_ins = documentTemplateFillIns($asks_template, documentTemplateFillInRowsFor([$asks_template['document_template_id']])[$asks_template['document_template_id']]);
+ $asks_site = array_intersect(array_keys($asks_used['placeholder']), DOCUMENT_TEMPLATE_SITE_PLACEHOLDERS);
+ $asks_contact = array_intersect(array_keys($asks_used['placeholder']), DOCUMENT_TEMPLATE_CONTACT_PLACEHOLDERS);
+
+ $html = '';
+ if ($asks_used['placeholder'] || $asks_used['table']) {
+ $filled = array_map(function ($word) {
+ return '[' . $word . ']';
+ }, array_values($asks_used['placeholder'] + $asks_used['table']));
+ $html .= '[Networks]
) and as a list of names in a sentence - use them instead of drawing those tables yourself: " . implode(', ', array_map(function ($word) { + return "[$word]"; + }, array_keys(DOCUMENT_TEMPLATE_TABLES))) . "." + . " For anything else someone fills in, use a short name of its own in square brackets, like [Firewall Model] or [Backup Schedule] - letters, digits and spaces, starting with a letter."; $user_message = "Create an HTML formatted IT documentation template based on the following request:\n\n\"$prompt\"\n\nThe template should be structured, professional, and useful for IT staff. Include relevant sections, instructions, prerequisites, and best practices."; $result = callAiApi($model, [ diff --git a/agent/files.php b/agent/files.php index f40b33b36..3b595b0d5 100644 --- a/agent/files.php +++ b/agent/files.php @@ -324,7 +324,7 @@ $preview = []; // the grid's previewer payload New document - + Document from template diff --git a/agent/js/document_template_make.js b/agent/js/document_template_make.js new file mode 100644 index 000000000..b1f1116cd --- /dev/null +++ b/agent/js/document_template_make.js @@ -0,0 +1,68 @@ +/* + * Making a document from a template - the Document from template window + * (agent/modals/document/document_add_from_template.php) and New document for + * a client (admin/modals/document_template/document_template_use.php). When + * the template or the client is picked, what it asks for - the site and + * contact its placeholders come from, and its fill-ins - is drawn in the + * form's [data-doc-template-asks] box (agent/ajax.php document_template_asks). + * + * The windows are ajax modals that run this each time they open: each form is + * drawn as it opens, and the change listener is bound once. + */ +(function () { + + function draw(form) { + const box = form.querySelector('[data-doc-template-asks]'); + const template = form.querySelector('[data-doc-template-select]'); + const client = form.querySelector('[data-doc-template-client]'); + if (!box || !template || !client) { + return; + } + const templateId = parseInt(template.value, 10) || 0; + const clientId = parseInt(client.value, 10) || 0; + const key = templateId + ':' + clientId; + if (box.dataset.drawn === key) { + return; + } + box.dataset.drawn = key; + if (!templateId || !clientId) { + box.innerHTML = ''; + box.hidden = true; + return; + } + fetch('/agent/ajax.php?document_template_asks=' + templateId + '&client_id=' + clientId, { credentials: 'same-origin' }) + .then(function (response) { + if (!response.ok) { + throw new Error('HTTP ' + response.status); + } + return response.json(); + }) + .then(function (data) { + // Picked again meanwhile - the later answer draws it + if (box.dataset.drawn !== key) { + return; + } + box.innerHTML = data.html || ''; + box.hidden = !data.html; + }) + .catch(function () { + box.innerHTML = ''; + box.hidden = true; + }); + } + + document.querySelectorAll('form[data-doc-template-make]').forEach(draw); + + if (window.itflowDocumentTemplateMakeBound) { + return; + } + window.itflowDocumentTemplateMakeBound = true; + + document.addEventListener('change', function (e) { + const field = e.target.closest ? e.target.closest('[data-doc-template-select], [data-doc-template-client]') : null; + const form = field ? field.closest('form[data-doc-template-make]') : null; + if (form) { + draw(form); + } + }); +})(); diff --git a/agent/modals/document/document_add_from_template.php b/agent/modals/document/document_add_from_template.php index 92fafdb96..cfa20d99d 100644 --- a/agent/modals/document/document_add_from_template.php +++ b/agent/modals/document/document_add_from_template.php @@ -4,7 +4,9 @@ * New document from one of the admin's document templates - its content and * its images, copied, in the folder it was opened from (add_document_from_template, * functions/document_template.php). Each template is offered with its summary - * and how often it has been used; ?document_template_id picks one. + * and how often it has been used; ?document_template_id picks one. What the + * one picked asks for - the site and contact its placeholders come from, and + * its fill-ins - is drawn under it (agent/js/document_template_make.js). */ require_once '../../../includes/modal_header.php'; @@ -33,9 +35,9 @@ ob_start();