Files
itflow/functions/document_template.php
johnnyq 9fdc2f7032 Document templates stage 1: Admin > Document Templates on the standard list with full search across name, summary and what it says, Active and Archived tabs with counts, sortable Used (documents made from it and for how many clients), Last used and Changed (when and by whom), paging, and a row menu with Edit, New document for a client, Copy, Archive/Restore and Delete; the template page rebuilt on the ticket template page's layout showing what it says made safe with its bracketed words marked and listed as left to fill in, its use and latest documents with their client, who made and last changed it and its images, with New document for a client opening a window that makes one for the client picked and opens it; one shared New/Edit window with icons in front of each field, a summary, and Draft it with AI only when an AI model is set up for documentation; Copy takes copies of its images, Delete takes its image folder and unlinks its documents, and archived templates aren't offered for new documents; migration 3.2.5 adds documents.document_template_id so documents remember their template and its use is counted from the update, and the document page says which template it came from; new functions/document_template.php shared by the admin pages and add_document_from_template, which now takes the template's name and summary when none are given and checks the client exists; the Document from template window shows each template's summary and how often it has been used and can open with one picked, and its Add one in Admin link checks for an admin rather than role id 3; changelog updated
2026-10-18 15:37:03 -04:00

202 lines
11 KiB
PHP

<?php
/*
* ITFlow - document templates (Admin > Document Templates): documents ready
* to start a client's from - a runbook, a build sheet, a plan - with their
* images, kept in uploads/document_templates/<id>/. A document made from one
* gets a copy of what it says and its images, and remembers which template it
* came from (documents.document_template_id, from database 3.2.5 on), so each
* template's use is counted. Words in square brackets - [Client], [Date] - are
* left for whoever writes the document to fill in.
*
* Shared by admin/document_templates.php, admin/document_template.php,
* admin/modals/document_template/*, admin/post/document_template.php and the
* New document from a template window and its handler on the agent side
* (agent/modals/document/document_add_from_template.php, agent/post/document.php).
*/
/** Where a template's images are kept, from the app's root - its folder under uploads/document_templates/. */
function documentTemplateDir($document_template_id) {
return dirname(__DIR__) . '/uploads/document_templates/' . intval($document_template_id);
}
/** One template, archived or not, with who made it and last changed it - or null. */
function documentTemplateRow($document_template_id) {
global $mysqli;
$document_template_id = intval($document_template_id);
if ($document_template_id <= 0) {
return null;
}
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT document_template_id, document_template_name, document_template_description, document_template_content,
document_template_created_at, document_template_updated_at, document_template_archived_at, document_template_created_by, document_template_updated_by,
creator.user_name AS created_by_name, updater.user_name AS updated_by_name
FROM document_templates
LEFT JOIN users creator ON creator.user_id = document_template_created_by AND document_template_created_by > 0
LEFT JOIN users updater ON updater.user_id = document_template_updated_by AND document_template_updated_by > 0
WHERE document_template_id = $document_template_id"));
return $row ?: null;
}
/*
* How the templates have been used - template id => count (documents made from
* it that are not archived), clients (how many clients have one) and last (when
* the latest was made). Ones not used aren't in it.
*/
function documentTemplateUsage(array $document_template_ids) {
global $mysqli;
$ids = array_values(array_filter(array_map('intval', $document_template_ids)));
$usage = [];
if (!$ids) {
return $usage;
}
$sql = mysqli_query($mysqli, "SELECT document_template_id, COUNT(*) AS n, COUNT(DISTINCT document_client_id) AS clients, MAX(document_created_at) AS last
FROM documents WHERE document_template_id IN (" . implode(',', $ids) . ") AND document_archived_at IS NULL GROUP BY document_template_id");
while ($row = mysqli_fetch_assoc($sql)) {
$usage[intval($row['document_template_id'])] = ['count' => intval($row['n']), 'clients' => intval($row['clients']), 'last' => $row['last']];
}
return $usage;
}
/*
* The template window's fields, checked - or null with $error saying why.
* $post: name, description (one line on what it is for) and content (the
* editor's HTML, kept as written - it is made safe where it is shown).
*/
function documentTemplateFields(array $post, &$error = '') {
$error = '';
$name = trim(preg_replace('/\s+/u', ' ', strip_tags((string) ($post['name'] ?? ''))));
if ($name === '') {
$error = 'A document template needs a name';
return null;
}
if (mb_strlen($name) > 200) {
$error = 'Keep the name to 200 characters';
return null;
}
$description = trim(preg_replace('/\s+/u', ' ', strip_tags((string) ($post['description'] ?? ''))));
if (mb_strlen($description) > 500) {
$error = 'Keep the summary to 500 characters';
return null;
}
return ['name' => $name, 'description' => $description, 'content' => (string) ($post['content'] ?? '')];
}
/*
* Save a template's fields (documentTemplateFields()) - a new one with
* $document_template_id 0. Pasted-in images are kept in its folder, and on a
* change the ones it no longer shows are taken away. Its id.
*/
function documentTemplateSave($document_template_id, array $f) {
global $mysqli, $session_user_id;
$document_template_id = intval($document_template_id);
$user_id = intval($session_user_id ?? 0);
$set = "document_template_name = '" . escapeSql($f['name']) . "', document_template_description = '" . escapeSql($f['description']) . "'";
$new = !$document_template_id;
if ($new) {
// Made first, so its images have a folder named for it
mysqli_query($mysqli, "INSERT INTO document_templates SET $set, document_template_content = '', document_template_created_by = $user_id");
$document_template_id = intval(mysqli_insert_id($mysqli));
}
$content = saveBase64Images($f['content'], dirname(__DIR__) . '/uploads/document_templates/', 'uploads/document_templates/', $document_template_id);
cleanupUnusedImages($content, documentTemplateDir($document_template_id), '/uploads/document_templates/' . $document_template_id);
// A new one hasn't been changed yet - its content going in isn't a change
mysqli_query($mysqli, "UPDATE document_templates SET $set, document_template_content = '" . mysqli_real_escape_string($mysqli, $content) . "',
" . ($new ? 'document_template_updated_at = NULL' : "document_template_updated_by = $user_id") . " WHERE document_template_id = $document_template_id");
return $document_template_id;
}
/*
* A template's HTML with its images pointed at another folder of copies - for a
* copy of the template, or a document made from it. $to_web is the new
* folder's web path ('/uploads/documents/12/').
*/
function documentTemplateCopyImages($document_template_id, $to_dir, $to_web, $html) {
$document_template_id = intval($document_template_id);
copyDirectory(documentTemplateDir($document_template_id), $to_dir);
return str_replace("/uploads/document_templates/$document_template_id/", $to_web, (string) $html);
}
/* A new template starting from this one - "Copy of", with copies of its images. Its id, or 0. */
function documentTemplateCopy($document_template_id) {
global $mysqli, $session_user_id;
$row = documentTemplateRow($document_template_id);
if (!$row) {
return 0;
}
$user_id = intval($session_user_id ?? 0);
mysqli_query($mysqli, "INSERT INTO document_templates SET document_template_name = '" . escapeSql(mb_substr('Copy of ' . $row['document_template_name'], 0, 200)) . "',
document_template_description = '" . escapeSql((string) $row['document_template_description']) . "', document_template_content = '', document_template_created_by = $user_id");
$new_id = intval(mysqli_insert_id($mysqli));
$content = documentTemplateCopyImages($row['document_template_id'], documentTemplateDir($new_id), "/uploads/document_templates/$new_id/", $row['document_template_content']);
mysqli_query($mysqli, "UPDATE document_templates SET document_template_content = '" . mysqli_real_escape_string($mysqli, $content) . "', document_template_updated_at = NULL WHERE document_template_id = $new_id");
return $new_id;
}
/* Delete a template and its folder of images. The documents made from it stay, no longer linked to it. */
function documentTemplateDelete($document_template_id) {
global $mysqli;
$document_template_id = intval($document_template_id);
mysqli_query($mysqli, "DELETE FROM document_templates WHERE document_template_id = $document_template_id");
mysqli_query($mysqli, "UPDATE documents SET document_template_id = 0 WHERE document_template_id = $document_template_id");
removeDirectory(documentTemplateDir($document_template_id));
}
/*
* Make a client's document from a template - what it says and its images,
* copied, in $folder_id, called $name (the template's when '') - remembering
* the template. The new document's id, or 0 when the template is gone or
* archived.
*/
function documentTemplateMakeDocument($document_template_id, $client_id, $name = '', $description = '', $folder_id = 0) {
global $mysqli, $session_user_id;
$row = documentTemplateRow($document_template_id);
if (!$row || $row['document_template_archived_at']) {
return 0;
}
$template_id = intval($row['document_template_id']);
$client_id = intval($client_id);
$name = trim((string) $name) !== '' ? mb_substr(trim((string) $name), 0, 200) : (string) $row['document_template_name'];
$description = trim((string) $description) !== '' ? trim((string) $description) : (string) $row['document_template_description'];
mysqli_query($mysqli, "INSERT INTO documents SET document_name = '" . escapeSql($name) . "', document_description = '" . escapeSql($description) . "',
document_content = '', document_content_raw = '', document_folder_id = " . intval($folder_id) . ", document_created_by = " . intval($session_user_id ?? 0) . ",
document_client_id = $client_id, document_template_id = $template_id");
$document_id = intval(mysqli_insert_id($mysqli));
// The template's images come along, and its HTML is pointed at the copies
$html = documentTemplateCopyImages($template_id, dirname(__DIR__) . "/uploads/documents/$document_id", "/uploads/documents/$document_id/", $row['document_template_content']);
mysqli_query($mysqli, "UPDATE documents SET document_content = '" . mysqli_real_escape_string($mysqli, $html) . "',
document_content_raw = '" . escapeSql(documentContentRaw($name, $html)) . "' WHERE document_id = $document_id");
return $document_id;
}
/* What a template says, as a line of plain text - for the lists. */
function documentTemplatePreview($html, $length = 160) {
$text = trim(preg_replace('/\s+/u', ' ', html_entity_decode(strip_tags(str_replace('<', ' <', (string) $html)), ENT_QUOTES, 'UTF-8')));
return mb_strimwidth($text, 0, $length, '...');
}
/* The words in square brackets in a template's text - [Client], [Date of visit] - each once, as written first. */
function documentTemplateBracketsIn($html) {
$text = html_entity_decode(strip_tags(str_replace('<', ' <', (string) $html)), ENT_QUOTES, 'UTF-8');
preg_match_all('/\[([A-Za-z][A-Za-z0-9 _\-\/\x{00A0}]{0,40})\]/u', $text, $matches);
$found = [];
foreach ($matches[1] as $word) {
$word = trim(preg_replace('/[\s\x{00A0}]+/u', ' ', $word));
if ($word !== '' && !isset($found[mb_strtolower($word)])) {
$found[mb_strtolower($word)] = $word;
}
}
return array_values($found);
}
/* A template's HTML (already made safe) with its [words] marked, in its text only - for its page. */
function documentTemplateMarkBrackets($html) {
$pieces = preg_split('/(<[^>]*>)/', (string) $html, -1, PREG_SPLIT_DELIM_CAPTURE);
foreach ($pieces as $i => $piece) {
if ($i % 2 === 0 && $piece !== '') {
$pieces[$i] = preg_replace('/\[([A-Za-z][A-Za-z0-9 _\-\/]{0,40})\]/', '<span class="badge rounded-pill text-bg-light border font-monospace fw-normal">[$1]</span>', $piece);
}
}
return implode('', $pieces);
}