mirror of
https://github.com/itflow-org/itflow
synced 2026-11-13 16:07:23 +00:00
Ticket templates stage 1: templates now set up the whole ticket - category, priority, billable, agent, due time and tags (migration 3.1.9 adds them, the ticket_template_tags table and tickets.ticket_template_id) - and fill [Client], [Contact], [Asset], [Site], [Agent], [Project], [Ticket], [Date], [Month], [Quarter] and [Year] in the subject, details and tasks when a ticket is raised, leaving other bracketed text for whoever picks it up; tickets remember the template they came from and each template shows how many tickets it raised, how many are open and when it was last used; the template picker fills New Ticket, the New Tickets windows on the assets and clients lists and recurring tickets, and project templates use each template's setup and agent; Admin > Ticket Templates rebuilt with search, Current/Archived, Copy, Archive/Restore and New ticket from it, and each template gets a page showing the ticket it starts, its tasks in drag-to-sort order with time estimates, what it sets up, its placeholders and the tickets, recurring tickets and project templates that use it; template handling moved into functions/ticket_template.php with ticketTemplateFinish, tagKeepIds and tagIdsFromValues split out of tagSetForRecord, and template task editing moved to admin/post; fixes editing a template task moving it to the top of the list, client bulk tickets skipping the template's tasks and time estimates, and the client bulk window not validating the agent, priority and category, and adds an option there to raise each client's ticket for its primary contact; changelog updated
This commit is contained in:
26
CHANGELOG.md
26
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.1.8) 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.1.9) in the same pass. Nothing else is required.
|
||||
|
||||
This is a big release, with thirty-eight database steps in one go, so take a backup before you update.
|
||||
This is a big release, with thirty-nine 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:
|
||||
|
||||
@@ -43,6 +43,8 @@ The built-in Accountant role gets full access to Financial, so it can delete an
|
||||
|
||||
What the API makes from now on - tickets, replies, clients and the rest - is stamped in ITFlow's timezone, as the pages stamp what they make. What it made before keeps the server's time, which on a server set to UTC is hours off (see Bug Fixes).
|
||||
|
||||
Ticket templates keep their subject, details and tasks, and set nothing else on a ticket until you give them a category, priority, agent, due time or tags. Tickets raised before the update don't know which template they came from, so a template's use is counted from the update on.
|
||||
|
||||
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.
|
||||
|
||||
### Breaking Changes and Notes
|
||||
@@ -164,6 +166,9 @@ If you are coming from 26.08 or earlier, the database update still has to be run
|
||||
- Tickets: an agent is now emailed when a ticket is assigned to them from New Ticket or Edit, as they already were from the assignee pick and Bulk Action. It used to be an app notice only. Assigning a ticket to yourself sends nothing.
|
||||
- Client portal: the email inviting a contact to the portal no longer has their password in it. Tick Email them an invite to the portal - on New Contact as well as Edit - and they get a link to set their own password, good for 7 days, or with a Microsoft login a link to sign in. It used to email the password the agent typed, in plain text. A password you type is still set, for reading out over the phone; the link lets them choose another.
|
||||
- Email on the contacts and clients lists comes from one of your mailboxes in Admin > Mail, picked by name, and goes out in the email look. Archived contacts and contacts without an email address are left out, and each address gets it once. With no roles ticked on the clients list it goes to every contact that isn't archived. Nothing is sent while outgoing mail isn't set up.
|
||||
- Ticket templates: [Client], [Contact], [Asset], [Site], [Agent], [Project], [Ticket], [Date], [Month], [Quarter] and [Year] in a template's subject, details and tasks are filled in when a ticket is raised from it - and in a recurring ticket's each time it raises one, template or not. A recurring ticket with [Month] in its subject used to raise it with [Month] in it. Anything else in square brackets stays as it is.
|
||||
- Ticket templates: a project template's tickets are set up as their ticket templates say. They were always Medium, uncategorised and billed as the default.
|
||||
- New Tickets on the assets list: with [Asset] in the subject, the asset's name isn't put in front of it as well. New Tickets on the assets and clients lists now email the agent they go to, once for all of them, as New Ticket does.
|
||||
- Emails to agents keep their subjects, so mail rules still match. The ones that changed: a client's reply from the portal is "ITFlow - Ticket updated - [TCK-12] Subject", with the dash the emailed reply always had; one recurring ticket assigned from Bulk Action is "ITFlow - Recurring ticket Subject assigned to you", as from the assignee pick; and a ticket raised on a schedule reaches the new-ticket address as "ITFlow - New Recurring Ticket - Client: Subject".
|
||||
- Client portal: primary and technical contacts' Tickets page now opens on everyone's tickets at their company, with Mine to see only their own. The separate All tickets page is gone, and links to it land on Tickets. A ticket shows the client where it stands - Received, In progress, Waiting on you, On hold, Resolved or Closed - rather than your ticket statuses.
|
||||
- Client portal: invoices and quotes now open in the portal, where they used to open their guest link in a new tab. The links in your emails work as before. A quote answered in the portal records the contact's name and any note they left. Pay invoices pays the invoices ticked as one card payment, and the Pay drop-down on each invoice is gone (see Client portal).
|
||||
@@ -479,10 +484,17 @@ If you are coming from 26.08 or earlier, the database update still has to be run
|
||||
- The project page shows warnings when something needs a look, and stat cards for tickets, tasks, time and due date. Tickets, Tasks and Activity are on tabs, alongside the scope, the details and the team with their hours.
|
||||
- Projects now have a start date, budget hours and a client contact.
|
||||
- A template can assign its tickets to the manager. You can also add a template's tickets to an existing project.
|
||||
- Template ticket subjects can use [Client], [Project], [Month], [Quarter] and [Year].
|
||||
- Their tickets are set up as their ticket templates say, with [Client], [Project], [Month] and the like filled in (see Ticket templates).
|
||||
- You can add tickets by picking them or by number, and completed projects can be reopened.
|
||||
- Project templates in Admin keep their ticket templates in order, and can be duplicated, archived and restored.
|
||||
- Projects show on the ticket page, the client overview, the dashboard, the calendar and in search.
|
||||
- **Ticket templates** (rebuilt)
|
||||
- A template sets up the whole ticket, not just its subject, details and tasks: its category, priority, billing, the agent it goes to, when it is due (so many hours or days after it is raised) and its tags. Each is left alone unless the template sets it.
|
||||
- Picking one on New Ticket fills in every field it sets, so you see the ticket before it is made and can change anything. The picker says what each template sets up and how many tasks it has.
|
||||
- The New Tickets windows on the assets and clients lists now offer templates too, as recurring tickets do - and the clients list can raise each ticket for the client's primary contact.
|
||||
- [Client], [Contact], [Asset], [Site], [Agent], [Project], [Ticket], [Date], [Month], [Quarter] and [Year] are filled in when the ticket is raised - in its subject, details and tasks. Anything else in square brackets, like [Employee Name], stays for whoever picks it up, and the template's page lists which are which.
|
||||
- A ticket keeps which template it came from, and its history says so. Admin > Ticket Templates shows what each sets up, its tasks and their time, how many tickets it has raised and the recurring tickets and project templates that raise it. A template's page shows the ticket it starts with its placeholders marked, its tasks (added with their time, dragged into order) and its latest tickets, and New ticket from it opens New Ticket with it filled in.
|
||||
- Templates can be copied, archived - not offered for new tickets, while the recurring tickets and project templates that use them keep them - restored and deleted.
|
||||
- **Assets**
|
||||
- The asset list has:
|
||||
- type icons, colored status pills and OS icons
|
||||
@@ -972,6 +984,8 @@ If you are coming from 26.08 or earlier, the database update still has to be run
|
||||
- Tickets: the email telling someone their email had become a ticket showed an apostrophe in its subject, or in their name, with a backslash in front of it.
|
||||
- Tickets: SLA alert emails were queued when outgoing mail was not set up. For a ticket raised or answered by email, the new ticket and client replied emails showed an apostrophe in the subject, the client's name or the reply with a backslash in front of it.
|
||||
- Admin > Mail: the test email was queued when outgoing mail was not set up, or when what was typed was not an email address, and greeted everyone as "Chap".
|
||||
- Ticket templates: New Tickets on the clients list gave only the first client's ticket the template's tasks, dropped their times, and couldn't be given a template at all from the window. It took any priority, category or agent posted to it, and its "primary contact" never did anything.
|
||||
- Ticket templates: changing one of a template's tasks moved it to the top of the list.
|
||||
- Email on the contacts list queued an email to a contact with no email address, which then failed in the mail queue. Email on the clients list included archived contacts. Both took any From address posted to them.
|
||||
- Emails: a saved card's confirmation, a new agent's sign in details and the email about a change to an agent's account were only sent when an SMTP host was filled in, so they were skipped when mail goes through Microsoft 365 or Google with no host. The saved card's email began "Were writing".
|
||||
- Contacts: an agent without Support saw a contact's Assets, Documents, Files and Services tabs, which led to pages they were refused. A document's or file's linked assets, licenses, services, domains and certificates were listed to them too.
|
||||
@@ -1414,6 +1428,12 @@ If you are coming from 26.08 or earlier, the database update still has to be run
|
||||
- Pulled out of the handlers so the previews can call them: `ticketMailCreated()` (from `agent/post/ticket.php`, now with `$how` and `$to`, which the email parser uses), `ticketMailReply($ticket_id, $reply_html, $by, $resolved, $attachments)` (the reply handler, bulk reply and the API), `ticketMailWatchersAdded()`, `ticketMailTaskApproval($approval_id, $by)`, `ticketMailClosedReply($ticket_id, $from_email)` and `ticketSlaAlert($ticket_id, $track, $breached, $due, $address)` in `functions/ticket.php` - `sendSlaAlert()` in `cron/ticket_sla.php` is gone - and `billingMailCardSaved()` in `functions/billing_email.php`.
|
||||
- `functions/portal.php`: `portalMailReady()`, `portalMailInvite($user_id, $by)` (used by `add_contact`, `edit_contact` - which take `send_email` - and the portal's `portalContactInvite()`), `portalMailPasswordReset($user_id)` and `portalMailAccountChange($email, $name, $change)` (`password`, `password_link`, `mfa_on`, `mfa_off`, `mfa_off_by_us`). `portalAccountEmail()` sends through `emailQueue()`.
|
||||
- `functions/share.php`: `shareMailHtml()` and `shareQueueMail()` are removed. `shareEmailLink()` returns a bool, and new `shareEmailCode($row, $code)` and `shareMailFrom()`. `shareMailReady()` goes by `emailReady()`. `calendarSendInvites()` sends through `emailQueue()`, and `calendarCanEmail()` is `emailReady()`.
|
||||
- Ticket templates:
|
||||
- Database 3.1.9: `ticket_templates` gains `ticket_template_category_id`, `ticket_template_priority` (NULL leaves it), `ticket_template_billable` (NULL leaves it), `ticket_template_assigned_to` and `ticket_template_due_hours`; new `ticket_template_tags` (ticket tags, cascading like `ticket_tags`); `tickets.ticket_template_id` with a key.
|
||||
- New `functions/ticket_template.php`: `ticketTemplateRow()`, `ticketTemplateTasks()`, `ticketTemplateTagIds()`, `ticketTemplateFields($post, &$error)`, `ticketTemplateSave($id, $fields)`, `ticketTemplateSetTags()`, `ticketTemplateCopy()`, `ticketTemplateUsage($ids)`, `ticketTemplateDefaultsHtml()`, `ticketTemplatePickerData()`, `ticketTemplateDueText()`, `ticketTemplateDueAt()`, `ticketTemplateMinutesText()` and `ticketTemplatePlaceholdersIn()`; placeholders in `TICKET_PLACEHOLDERS`, `ticketPlaceholderValues($ticket_id)`, `ticketPlaceholderFill($text, $values, $html)` and `ticketPlaceholdersFill($ticket_id)`; and `ticketTemplateFinish($ticket_id, $template_id, $defaults, $tasks)`, which every path that raises from a template calls - `add_ticket`, `bulk_add_asset_ticket`, `bulk_add_client_ticket` (rewritten, with `use_primary_contact` now a switch in its window) and `projectApplyTemplate()`, which now takes each ticket template's settings. `recurringTicketRaise()` records the recurring ticket's template and calls `ticketPlaceholdersFill()`. `projectTemplateSubject()` is gone.
|
||||
- `agent/includes/inc_ticket_template_select.php` writes each template's settings as `data-template-*` on its option and takes `$ticket_template_field`, `$ticket_template_apply_on_load` and `$ticket_template_hint`; `agent/js/ticket_tasks_modal.js` writes them into the form's fields marked `data-template-field` (`subject`, `details`, `priority`, `category`, `assigned`, `due`, `tags`, `billable`) and the task rows. `ticket_add.php` takes `ticket_template_id`, and `agent/tickets.php?new_from_template=` opens it.
|
||||
- Admin: `admin/modals/ticket_template/ticket_template_form.php` is shared by the add and edit windows, which are ajax windows now; `copy_ticket_template`, `archive_ticket_template` and `restore_ticket_template` are new. `admin/post/ticket_template.php` no longer requires `agent/post/ticket.php` and `agent/post/task.php`, and `edit_ticket_template_task` moved there from `agent/post/task.php`.
|
||||
- `functions/tags.php`: `tagIdsFromValues($entity, $values)` and `tagKeepIds($entity, $ids, $had)` are split out of `tagSetForRecord()`, and `tagFormFieldHtml()` takes the select's extra attributes.
|
||||
- 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
|
||||
|
||||
30
admin/database_updates/3.1.9.php
Normal file
30
admin/database_updates/3.1.9.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 3.1.9 (from 3.1.8)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
// A ticket template sets up the whole ticket: its category, priority,
|
||||
// billing, who it goes to and when it is due, and its tags. NULL priority
|
||||
// or billable, 0 category, agent or due: the template leaves that alone.
|
||||
mysqli_query($mysqli, "ALTER TABLE `ticket_templates`
|
||||
ADD `ticket_template_category_id` int(11) NOT NULL DEFAULT 0 AFTER `ticket_template_details`,
|
||||
ADD `ticket_template_priority` varchar(20) DEFAULT NULL AFTER `ticket_template_category_id`,
|
||||
ADD `ticket_template_billable` tinyint(1) DEFAULT NULL AFTER `ticket_template_priority`,
|
||||
ADD `ticket_template_assigned_to` int(11) NOT NULL DEFAULT 0 AFTER `ticket_template_billable`,
|
||||
ADD `ticket_template_due_hours` int(11) NOT NULL DEFAULT 0 AFTER `ticket_template_assigned_to`");
|
||||
|
||||
mysqli_query($mysqli, "CREATE TABLE `ticket_template_tags` (
|
||||
`ticket_template_id` int(11) NOT NULL,
|
||||
`tag_id` int(11) NOT NULL,
|
||||
PRIMARY KEY (`ticket_template_id`,`tag_id`),
|
||||
KEY `tag_id` (`tag_id`),
|
||||
CONSTRAINT `ticket_template_tags_ibfk_1` FOREIGN KEY (`ticket_template_id`) REFERENCES `ticket_templates` (`ticket_template_id`) ON DELETE CASCADE,
|
||||
CONSTRAINT `ticket_template_tags_ibfk_2` FOREIGN KEY (`tag_id`) REFERENCES `tags` (`tag_id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci");
|
||||
|
||||
// Which template a ticket was raised from - for the template's use, from now on
|
||||
mysqli_query($mysqli, "ALTER TABLE `tickets` ADD `ticket_template_id` int(11) NOT NULL DEFAULT 0 AFTER `ticket_recurring_ticket_id`, ADD KEY `ticket_template_id` (`ticket_template_id`)");
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
require_once '../../includes/modal_header.php';
|
||||
|
||||
$template = [];
|
||||
$template_tag_ids = [];
|
||||
|
||||
ob_start();
|
||||
|
||||
?>
|
||||
@@ -13,53 +16,7 @@ ob_start();
|
||||
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
||||
|
||||
<div class="modal-body">
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Template Name <strong class="text-danger">*</strong></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-life-ring"></i></span>
|
||||
<input type="text" class="form-control" name="name" placeholder="Template name" maxlength="200" required autofocus>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Subject</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-angle-right"></i></span>
|
||||
<input type="text" class="form-control" name="subject" placeholder="Subject" maxlength="500">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<textarea class="form-control tinymceTicket" name="details"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Description</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-angle-right"></i></span>
|
||||
<input type="text" class="form-control" name="description" placeholder="Short description">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Add it to a Project Template?</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-project-diagram"></i></span>
|
||||
<select class="form-select select2" name="project_template">
|
||||
<option value="0">- No -</option>
|
||||
<?php
|
||||
|
||||
$sql_project_templates = mysqli_query($mysqli, "SELECT project_template_id, project_template_name FROM project_templates WHERE project_template_archived_at IS NULL ORDER BY project_template_name ASC");
|
||||
while ($row = mysqli_fetch_assoc($sql_project_templates)) {
|
||||
$project_template_id_select = intval($row['project_template_id']);
|
||||
$project_template_name_select = escapeHtml($row['project_template_name']); ?>
|
||||
<option value="<?= $project_template_id_select ?>"><?= $project_template_name_select ?></option>
|
||||
|
||||
<?php } ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<?php require __DIR__ . '/ticket_template_form.php'; ?>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" name="add_ticket_template" class="btn btn-primary text-bold"><i class="fa fa-check me-2"></i>Create Template</button>
|
||||
|
||||
@@ -1,51 +1,39 @@
|
||||
<div class="modal" id="editTicketTemplateModal" tabindex="-1">
|
||||
<?php
|
||||
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-dark">
|
||||
<h5 class="modal-title"><i class="fa fa-fw fa-life-ring me-2"></i>Editing Ticket Template: <?= $ticket_template_name ?></h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<form action="post.php" method="post" autocomplete="off">
|
||||
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
||||
<input type="hidden" name="ticket_template_id" value="<?= $ticket_template_id ?>">
|
||||
/*
|
||||
* Edit a ticket template - the fields are ticket_template_form.php. Its tasks
|
||||
* are changed on its page (admin/ticket_template.php).
|
||||
*/
|
||||
|
||||
<div class="modal-body">
|
||||
require_once '../../includes/modal_header.php';
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Template Name <strong class="text-danger">*</strong></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-life-ring"></i></span>
|
||||
<input type="text" class="form-control" name="name" maxlength="200" value="<?= $ticket_template_name ?>" placeholder="Template name" required autofocus>
|
||||
</div>
|
||||
</div>
|
||||
$template = ticketTemplateRow($_GET['id'] ?? 0);
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Subject</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-angle-right"></i></span>
|
||||
<input type="text" class="form-control" name="subject" maxlength="500" value="<?= $ticket_template_subject ?>" placeholder="Subject">
|
||||
</div>
|
||||
</div>
|
||||
ob_start();
|
||||
|
||||
<div class="mb-3">
|
||||
<textarea class="form-control tinymceTicket" name="details"><?= $ticket_template_details ?></textarea>
|
||||
</div>
|
||||
if (!$template) { ?>
|
||||
<div class="modal-body text-center text-secondary py-5">That ticket template is gone - it may have been deleted.</div>
|
||||
<?php } else {
|
||||
$ticket_template_id = intval($template['ticket_template_id']);
|
||||
$template_tag_ids = ticketTemplateTagIds($ticket_template_id); ?>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Description</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-angle-right"></i></span>
|
||||
<input type="text" class="form-control" name="description" value="<?= $ticket_template_description ?>" placeholder="Short description">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" name="edit_ticket_template" class="btn btn-primary text-bold"><i class="fa fa-check me-2"></i>Save</button>
|
||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal"><i class="fa fa-times me-2"></i>Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-header bg-dark">
|
||||
<h5 class="modal-title text-truncate"><i class="fa fa-fw fa-life-ring me-2"></i>Ticket template: <strong><?= escapeHtml($template['ticket_template_name']) ?></strong></h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<form action="post.php" method="post" autocomplete="off">
|
||||
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
||||
<input type="hidden" name="ticket_template_id" value="<?= $ticket_template_id ?>">
|
||||
|
||||
<div class="modal-body">
|
||||
<?php require __DIR__ . '/ticket_template_form.php'; ?>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" name="edit_ticket_template" class="btn btn-primary text-bold"><i class="fa fa-check me-2"></i>Save</button>
|
||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal"><i class="fa fa-times me-2"></i>Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<?php }
|
||||
|
||||
require_once '../../../includes/modal_footer.php';
|
||||
|
||||
164
admin/modals/ticket_template/ticket_template_form.php
Normal file
164
admin/modals/ticket_template/ticket_template_form.php
Normal file
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
/*
|
||||
* A ticket template's fields - shared by ticket_template_add.php and
|
||||
* ticket_template_edit.php, which set $template (ticketTemplateRow(), or []
|
||||
* for a new one) and $template_tag_ids before including this. Posts what
|
||||
* ticketTemplateFields() reads (functions/ticket_template.php).
|
||||
*
|
||||
* Two tabs: Ticket (its name, what it is for, the subject and details it
|
||||
* starts with) and Sets up (category, priority, billing, agent, due time and
|
||||
* tags - each left alone unless set).
|
||||
*
|
||||
* Should not be accessed directly.
|
||||
*/
|
||||
|
||||
isset($mysqli) || die("Direct file access is not allowed");
|
||||
|
||||
$t = $template;
|
||||
$due_hours = intval($t['ticket_template_due_hours'] ?? 0);
|
||||
$due_in_days = $due_hours === 0 || $due_hours % 24 === 0;
|
||||
$due_amount = $due_hours ? ($due_in_days ? intdiv($due_hours, 24) : $due_hours) : '';
|
||||
$billable = array_key_exists('ticket_template_billable', $t) && $t['ticket_template_billable'] !== null ? (string) intval($t['ticket_template_billable']) : '';
|
||||
$priority = (string) ($t['ticket_template_priority'] ?? '');
|
||||
$assigned_to = intval($t['ticket_template_assigned_to'] ?? 0);
|
||||
$placeholder_help = implode(', ', array_map(function ($name) {
|
||||
return "[$name]";
|
||||
}, array_keys(TICKET_PLACEHOLDERS)));
|
||||
|
||||
?>
|
||||
|
||||
<ul class="nav nav-pills nav-fill flex-nowrap overflow-auto mb-3 itflow-asset-form-tabs" role="tablist">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" data-bs-toggle="pill" href="#ticketTemplateFormTicket"><i class="fas fa-fw fa-life-ring me-2 d-none d-sm-inline"></i>Ticket</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="pill" href="#ticketTemplateFormSetUp"><i class="fas fa-fw fa-sliders-h me-2 d-none d-sm-inline"></i>Sets up</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content">
|
||||
|
||||
<div class="tab-pane fade show active" id="ticketTemplateFormTicket">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Template name <strong class="text-danger">*</strong></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-life-ring"></i></span>
|
||||
<input type="text" class="form-control" name="name" placeholder="New Hire Onboarding" maxlength="200" value="<?= escapeHtml($t['ticket_template_name'] ?? '') ?>" required autofocus>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">What it is for</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-info-circle"></i></span>
|
||||
<input type="text" class="form-control" name="description" placeholder="Shown when it is picked" maxlength="500" value="<?= escapeHtml($t['ticket_template_description'] ?? '') ?>">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Subject</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-tag"></i></span>
|
||||
<input type="text" class="form-control" name="subject" placeholder="New hire onboarding - [Employee Name]" maxlength="500" value="<?= escapeHtml($t['ticket_template_subject'] ?? '') ?>">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Details</label>
|
||||
<textarea class="form-control tinymceTicket" name="details"><?= escapeHtml($t['ticket_template_details'] ?? '') ?></textarea>
|
||||
<div class="form-text">
|
||||
Filled in when a ticket is raised: <span class="font-monospace"><?= escapeHtml($placeholder_help) ?></span>.
|
||||
Anything else in square brackets - <span class="font-monospace">[Employee Name]</span> - stays for whoever picks it up.
|
||||
</div>
|
||||
</div>
|
||||
<?php if (empty($t)) { ?>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Add it to a project template</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-project-diagram"></i></span>
|
||||
<select class="form-select select2" name="project_template">
|
||||
<option value="0">- No -</option>
|
||||
<?php
|
||||
$sql_project_templates = mysqli_query($mysqli, "SELECT project_template_id, project_template_name FROM project_templates WHERE project_template_archived_at IS NULL ORDER BY project_template_name ASC");
|
||||
while ($row = mysqli_fetch_assoc($sql_project_templates)) { ?>
|
||||
<option value="<?= intval($row['project_template_id']) ?>"><?= escapeHtml($row['project_template_name']) ?></option>
|
||||
<?php } ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-text">It goes at the end of that project template's tickets.</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade" id="ticketTemplateFormSetUp">
|
||||
<p class="small text-secondary">How a ticket from it is set up. Anything left as it is stays as the New Ticket window has it, and can still be changed there before the ticket is made.</p>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Category</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-layer-group"></i></span>
|
||||
<select class="form-select select2" name="category_id">
|
||||
<option value="0">- Leave as it is -</option>
|
||||
<?= categoryOptionsHtml('Ticket', intval($t['ticket_template_category_id'] ?? 0) ?: null) ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Priority</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-thermometer-half"></i></span>
|
||||
<select class="form-select select2" name="priority">
|
||||
<option value="">- Leave as it is -</option>
|
||||
<?php foreach (['Low' => 'var(--bs-info)', 'Medium' => 'var(--bs-warning)', 'High' => 'var(--bs-danger)', 'Urgent' => 'var(--bs-emphasis-color)'] as $level => $color) { ?>
|
||||
<option value="<?= $level ?>" data-color="<?= $color ?>" <?= $priority === $level ? 'selected' : '' ?>><?= $level ?></option>
|
||||
<?php } ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Assign to</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-user-check"></i></span>
|
||||
<select class="form-select select2" name="assigned_to">
|
||||
<option value="0">- Leave as it is -</option>
|
||||
<?php
|
||||
$sql_agents = mysqli_query($mysqli, "SELECT user_id, user_name, user_avatar FROM users WHERE user_type = 1 AND user_status = 1 AND (user_archived_at IS NULL OR user_id = $assigned_to) ORDER BY user_name ASC");
|
||||
while ($row = mysqli_fetch_assoc($sql_agents)) { ?>
|
||||
<option value="<?= intval($row['user_id']) ?>" <?= intval($row['user_id']) === $assigned_to ? 'selected' : '' ?><?= selectOptionUser($row['user_id'], $row['user_name'], $row['user_avatar'] ?? '') ?>><?= escapeHtml($row['user_name']) ?></option>
|
||||
<?php } ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Due</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-calendar-check"></i></span>
|
||||
<input type="number" class="form-control" name="due_amount" min="0" max="8784" placeholder="No due date" value="<?= $due_amount ?>">
|
||||
<select class="form-select" name="due_unit" style="max-width: 9rem;" aria-label="Hours or days">
|
||||
<option value="days" <?= $due_in_days ? 'selected' : '' ?>>days after</option>
|
||||
<option value="hours" <?= $due_in_days ? '' : 'selected' ?>>hours after</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-text">After it is raised.</div>
|
||||
</div>
|
||||
<?php if (!empty($config_module_enable_accounting)) { ?>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Billable</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-dollar-sign"></i></span>
|
||||
<select class="form-select" name="billable">
|
||||
<option value="" <?= $billable === '' ? 'selected' : '' ?>>- Leave as it is -</option>
|
||||
<option value="1" <?= $billable === '1' ? 'selected' : '' ?>>Billable</option>
|
||||
<option value="0" <?= $billable === '0' ? 'selected' : '' ?>>Not billable</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<?php } else { ?>
|
||||
<input type="hidden" name="billable" value="<?= escapeHtml($billable) ?>">
|
||||
<?php } ?>
|
||||
<div class="col-12">
|
||||
<?= tagFormFieldHtml('ticket', $template_tag_ids, 'Onboarding, Hardware, Escalated') ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -1,24 +1,23 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* One of a ticket template's tasks - its name and how long it takes. Its place
|
||||
* in the list is changed by dragging it on the template's page.
|
||||
*/
|
||||
|
||||
require_once '../../includes/modal_header.php';
|
||||
|
||||
$task_template_id = intval($_GET['id']);
|
||||
$task_template_id = intval($_GET['id'] ?? 0);
|
||||
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT task_template_completion_estimate, task_template_name FROM task_templates WHERE task_template_id = $task_template_id LIMIT 1"));
|
||||
|
||||
$sql = mysqli_query($mysqli, "SELECT task_template_completion_estimate, task_template_name, task_template_order FROM task_templates WHERE task_template_id = $task_template_id LIMIT 1");
|
||||
|
||||
$row = mysqli_fetch_assoc($sql);
|
||||
$task_template_name = escapeHtml($row['task_template_name']);
|
||||
$task_template_order = intval($row['task_template_order']);
|
||||
$task_template_completion_estimate = intval($row['task_template_completion_estimate']);
|
||||
//$task_template_description = escapeHtml($row['task_template_description']);
|
||||
|
||||
// Generate the HTML form content using output buffering.
|
||||
ob_start();
|
||||
|
||||
?>
|
||||
if (!$row) { ?>
|
||||
<div class="modal-body text-center text-secondary py-5">That task is gone - it may have been deleted.</div>
|
||||
<?php } else { ?>
|
||||
|
||||
<div class="modal-header bg-dark">
|
||||
<h5 class="modal-title"><i class="fa fa-fw fa-tasks me-2"></i>Editing task</h5>
|
||||
<h5 class="modal-title"><i class="fa fa-fw fa-tasks me-2"></i>Task</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<form action="post.php" method="post" autocomplete="off">
|
||||
@@ -28,18 +27,19 @@ ob_start();
|
||||
<div class="modal-body">
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Name <strong class="text-danger">*</strong></label>
|
||||
<label class="form-label">Name <strong class="text-danger">*</strong></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-tag"></i></span>
|
||||
<input type="text" class="form-control" name="name" placeholder="Name the task" maxlength="255" value="<?= $task_template_name ?>" required autofocus>
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-tasks"></i></span>
|
||||
<input type="text" class="form-control" name="name" placeholder="Name the task" maxlength="200" value="<?= escapeHtml($row['task_template_name']) ?>" required autofocus>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Estimated Completion Time <span class="text-secondary">(Minutes)</span></label>
|
||||
<label class="form-label">How long it takes</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-clock"></i></span>
|
||||
<input type="number" class="form-control" name="completion_estimate" placeholder="Estimated time to complete task in mins" value="<?= $task_template_completion_estimate ?>">
|
||||
<span class="input-group-text"><i class="far fa-fw fa-clock"></i></span>
|
||||
<input type="number" class="form-control" name="completion_estimate" min="0" placeholder="Minutes" value="<?= intval($row['task_template_completion_estimate']) ?: '' ?>">
|
||||
<span class="input-group-text">minutes</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -52,6 +52,6 @@ ob_start();
|
||||
|
||||
</form>
|
||||
|
||||
<?php
|
||||
<?php }
|
||||
|
||||
require_once '../../../includes/modal_footer.php';
|
||||
|
||||
@@ -1,56 +1,86 @@
|
||||
<?php
|
||||
|
||||
// Ticket Templates
|
||||
/*
|
||||
* Ticket templates - Admin > Ticket Templates. The work is in
|
||||
* functions/ticket_template.php.
|
||||
*/
|
||||
|
||||
defined('FROM_POST_HANDLER') || die("Direct file access is not allowed");
|
||||
|
||||
// Import shared code from user-side tickets/tasks as we reuse functions
|
||||
require_once '../agent/post/ticket.php';
|
||||
require_once '../agent/post/task.php';
|
||||
/** The template a request names - back to the list with an error when it is gone. */
|
||||
function ticketTemplateForRequest($ticket_template_id) {
|
||||
$row = ticketTemplateRow($ticket_template_id);
|
||||
if (!$row) {
|
||||
flashAlert('That ticket template no longer exists.', 'error');
|
||||
redirect('ticket_templates.php');
|
||||
}
|
||||
return $row;
|
||||
}
|
||||
|
||||
if (isset($_POST['add_ticket_template'])) {
|
||||
if (isset($_POST['add_ticket_template']) || isset($_POST['edit_ticket_template'])) {
|
||||
|
||||
validateCSRFToken();
|
||||
|
||||
$name = escapeSql($_POST['name']);
|
||||
$description = escapeSql($_POST['description']);
|
||||
$subject = escapeSql($_POST['subject']);
|
||||
$details = mysqli_real_escape_string($mysqli, $_POST['details']);
|
||||
$project_template_id = intval($_POST['project_template']);
|
||||
$editing = isset($_POST['edit_ticket_template']);
|
||||
$ticket_template_id = 0;
|
||||
if ($editing) {
|
||||
$ticket_template_id = intval(ticketTemplateForRequest($_POST['ticket_template_id'] ?? 0)['ticket_template_id']);
|
||||
}
|
||||
|
||||
mysqli_query($mysqli, "INSERT INTO ticket_templates SET ticket_template_name = '$name', ticket_template_description = '$description', ticket_template_subject = '$subject', ticket_template_details = '$details'");
|
||||
$fields = ticketTemplateFields($_POST, $error);
|
||||
if (!$fields) {
|
||||
flashAlert($error, 'error');
|
||||
redirect();
|
||||
}
|
||||
$ticket_template_id = ticketTemplateSave($ticket_template_id, $fields);
|
||||
$name = escapeSql($fields['name']);
|
||||
|
||||
$ticket_template_id = mysqli_insert_id($mysqli);
|
||||
|
||||
// At the end of the project template's tickets
|
||||
if ($project_template_id && mysqli_num_rows(mysqli_query($mysqli, "SELECT 1 FROM project_templates WHERE project_template_id = $project_template_id"))) {
|
||||
// A new one can go at the end of a project template's tickets
|
||||
$project_template_id = intval($_POST['project_template'] ?? 0);
|
||||
if (!$editing && $project_template_id && mysqli_num_rows(mysqli_query($mysqli, "SELECT 1 FROM project_templates WHERE project_template_id = $project_template_id"))) {
|
||||
$order = intval(mysqli_fetch_row(mysqli_query($mysqli, "SELECT COALESCE(MAX(ticket_template_order), 0) + 1 FROM project_template_ticket_templates WHERE project_template_id = $project_template_id"))[0]);
|
||||
mysqli_query($mysqli, "INSERT INTO project_template_ticket_templates SET project_template_id = $project_template_id, ticket_template_id = $ticket_template_id, ticket_template_order = $order");
|
||||
}
|
||||
|
||||
logAudit("Ticket Template", "Create", "$session_name created ticket template $name", 0, $ticket_template_id);
|
||||
logAudit("Ticket Template", $editing ? "Edit" : "Create", "$session_name " . ($editing ? 'edited' : 'created') . " ticket template $name", 0, $ticket_template_id);
|
||||
|
||||
flashAlert("Ticket template <strong>" . stripslashes($name) . "</strong> created");
|
||||
flashAlert("Ticket template <strong>" . escapeHtml($fields['name']) . "</strong> " . ($editing ? 'saved' : 'created'));
|
||||
|
||||
redirect();
|
||||
// A new one opens, to add its tasks
|
||||
redirect($editing ? null : "ticket_template.php?ticket_template_id=$ticket_template_id");
|
||||
|
||||
}
|
||||
|
||||
if (isset($_POST['edit_ticket_template'])) {
|
||||
if (isset($_GET['copy_ticket_template'])) {
|
||||
|
||||
validateCSRFToken();
|
||||
|
||||
$ticket_template_id = intval($_POST['ticket_template_id']);
|
||||
$name = escapeSql($_POST['name']);
|
||||
$description = escapeSql($_POST['description']);
|
||||
$subject = escapeSql($_POST['subject']);
|
||||
$details = mysqli_real_escape_string($mysqli, $_POST['details']);
|
||||
$row = ticketTemplateForRequest($_GET['copy_ticket_template']);
|
||||
$new_id = ticketTemplateCopy($row['ticket_template_id']);
|
||||
|
||||
mysqli_query($mysqli, "UPDATE ticket_templates SET ticket_template_name = '$name', ticket_template_description = '$description', ticket_template_subject = '$subject', ticket_template_details = '$details' WHERE ticket_template_id = $ticket_template_id");
|
||||
logAudit("Ticket Template", "Create", "$session_name copied ticket template " . escapeSql($row['ticket_template_name']), 0, $new_id);
|
||||
|
||||
logAudit("Ticket Template", "Edit", "$session_name edited ticket template $name", 0, $ticket_template_id);
|
||||
flashAlert("Ticket template <strong>" . escapeHtml($row['ticket_template_name']) . "</strong> copied - this is the copy");
|
||||
|
||||
flashAlert("Ticket template <strong>" . stripslashes($name) . "</strong> saved");
|
||||
redirect("ticket_template.php?ticket_template_id=$new_id");
|
||||
|
||||
}
|
||||
|
||||
if (isset($_GET['archive_ticket_template']) || isset($_GET['restore_ticket_template'])) {
|
||||
|
||||
validateCSRFToken();
|
||||
|
||||
$archive = isset($_GET['archive_ticket_template']);
|
||||
$row = ticketTemplateForRequest($archive ? $_GET['archive_ticket_template'] : $_GET['restore_ticket_template']);
|
||||
$ticket_template_id = intval($row['ticket_template_id']);
|
||||
$name = escapeSql($row['ticket_template_name']);
|
||||
|
||||
// Archived, it isn't offered for new tickets - recurring tickets and project templates that use it keep it, so restoring it is all it takes
|
||||
mysqli_query($mysqli, "UPDATE ticket_templates SET ticket_template_archived_at = " . ($archive ? 'NOW()' : 'NULL') . " WHERE ticket_template_id = $ticket_template_id");
|
||||
|
||||
logAudit("Ticket Template", $archive ? "Archive" : "Restore", "$session_name " . ($archive ? 'archived' : 'restored') . " ticket template $name", 0, $ticket_template_id);
|
||||
|
||||
flashAlert("Ticket template <strong>" . escapeHtml($row['ticket_template_name']) . "</strong> " . ($archive ? 'archived' : 'restored'), $archive ? 'error' : 'success');
|
||||
|
||||
redirect();
|
||||
|
||||
@@ -60,25 +90,28 @@ if (isset($_GET['delete_ticket_template'])) {
|
||||
|
||||
validateCSRFToken();
|
||||
|
||||
$ticket_template_id = intval($_GET['delete_ticket_template']);
|
||||
|
||||
$ticket_template_name = escapeSql(getFieldById('ticket_templates', $ticket_template_id, 'ticket_template_name'));
|
||||
$row = ticketTemplateForRequest($_GET['delete_ticket_template']);
|
||||
$ticket_template_id = intval($row['ticket_template_id']);
|
||||
$ticket_template_name = escapeSql($row['ticket_template_name']);
|
||||
|
||||
mysqli_query($mysqli, "DELETE FROM ticket_templates WHERE ticket_template_id = $ticket_template_id");
|
||||
|
||||
// Delete Associations
|
||||
// Its tasks and tags go with it, and it comes off the project templates that raise it
|
||||
mysqli_query($mysqli, "DELETE FROM task_templates WHERE task_template_ticket_template_id = $ticket_template_id");
|
||||
mysqli_query($mysqli, "DELETE FROM ticket_template_tags WHERE ticket_template_id = $ticket_template_id");
|
||||
mysqli_query($mysqli, "DELETE FROM project_template_ticket_templates WHERE ticket_template_id = $ticket_template_id");
|
||||
|
||||
// Unlink from recurring tickets rather than deleting them - the schedule is still
|
||||
// wanted, it just stops contributing tasks to the tickets it raises
|
||||
// wanted, it just stops contributing tasks to the tickets it raises. Tickets raised
|
||||
// from it keep everything they were given, but not the link.
|
||||
mysqli_query($mysqli, "UPDATE recurring_tickets SET recurring_ticket_ticket_template_id = 0 WHERE recurring_ticket_ticket_template_id = $ticket_template_id");
|
||||
mysqli_query($mysqli, "UPDATE tickets SET ticket_template_id = 0 WHERE ticket_template_id = $ticket_template_id");
|
||||
|
||||
logAudit("Ticket Template", "Delete", "$session_name deleted ticket template $ticket_template_name");
|
||||
|
||||
flashAlert("Ticket template <strong>" . stripslashes($ticket_template_name) . "</strong> and its tasks deleted", 'error');
|
||||
flashAlert("Ticket template <strong>" . escapeHtml($row['ticket_template_name']) . "</strong> and its tasks deleted", 'error');
|
||||
|
||||
redirect();
|
||||
redirect(isset($_GET['from_page']) ? 'ticket_templates.php' : null);
|
||||
|
||||
}
|
||||
|
||||
@@ -86,16 +119,48 @@ if (isset($_POST['add_ticket_template_task'])) {
|
||||
|
||||
validateCSRFToken();
|
||||
|
||||
$ticket_template_id = intval($_POST['ticket_template_id']);
|
||||
$task_name = escapeSql($_POST['task_name']);
|
||||
$row = ticketTemplateForRequest($_POST['ticket_template_id'] ?? 0);
|
||||
$ticket_template_id = intval($row['ticket_template_id']);
|
||||
$task_name = trim(mb_substr(strip_tags((string) ($_POST['task_name'] ?? '')), 0, 200));
|
||||
if ($task_name === '') {
|
||||
flashAlert('Give the task a name.', 'error');
|
||||
redirect();
|
||||
}
|
||||
$estimate = max(0, min(100000, intval($_POST['task_estimate'] ?? 0)));
|
||||
$task_name_sql = escapeSql($task_name);
|
||||
$order = intval(mysqli_fetch_row(mysqli_query($mysqli, "SELECT COALESCE(MAX(task_template_order), 0) + 1 FROM task_templates WHERE task_template_ticket_template_id = $ticket_template_id"))[0]);
|
||||
|
||||
mysqli_query($mysqli, "INSERT INTO task_templates SET task_template_name = '$task_name', task_template_ticket_template_id = $ticket_template_id");
|
||||
mysqli_query($mysqli, "INSERT INTO task_templates SET task_template_name = '$task_name_sql', task_template_order = $order, task_template_completion_estimate = $estimate, task_template_ticket_template_id = $ticket_template_id");
|
||||
|
||||
$task_template_id = mysqli_insert_id($mysqli);
|
||||
logAudit("Ticket Template", "Edit", "$session_name added task $task_name_sql to ticket template " . escapeSql($row['ticket_template_name']), 0, $ticket_template_id);
|
||||
|
||||
logAudit("Ticket Template", "Create", "$session_name created task $task_name for ticket template", 0, $ticket_template_id);
|
||||
flashAlert("Task <strong>" . escapeHtml($task_name) . "</strong> added");
|
||||
|
||||
flashAlert("Task <strong>" . stripslashes($task_name) . "</strong> added");
|
||||
redirect();
|
||||
|
||||
}
|
||||
|
||||
if (isset($_POST['edit_ticket_template_task'])) {
|
||||
|
||||
validateCSRFToken();
|
||||
|
||||
$task_template_id = intval($_POST['task_template_id'] ?? 0);
|
||||
$task = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT task_template_ticket_template_id FROM task_templates WHERE task_template_id = $task_template_id"));
|
||||
$row = ticketTemplateForRequest($task['task_template_ticket_template_id'] ?? 0);
|
||||
$task_name = trim(mb_substr(strip_tags((string) ($_POST['name'] ?? '')), 0, 200));
|
||||
if ($task_name === '') {
|
||||
flashAlert('Give the task a name.', 'error');
|
||||
redirect();
|
||||
}
|
||||
$task_name_sql = escapeSql($task_name);
|
||||
$estimate = max(0, min(100000, intval($_POST['completion_estimate'] ?? 0)));
|
||||
|
||||
// Its place in the list stays as it is - the list is reordered by dragging
|
||||
mysqli_query($mysqli, "UPDATE task_templates SET task_template_name = '$task_name_sql', task_template_completion_estimate = $estimate WHERE task_template_id = $task_template_id");
|
||||
|
||||
logAudit("Ticket Template", "Edit", "$session_name edited task $task_name_sql on ticket template " . escapeSql($row['ticket_template_name']), 0, intval($row['ticket_template_id']));
|
||||
|
||||
flashAlert("Task <strong>" . escapeHtml($task_name) . "</strong> saved");
|
||||
|
||||
redirect();
|
||||
|
||||
@@ -106,14 +171,14 @@ if (isset($_GET['delete_task_template'])) {
|
||||
validateCSRFToken();
|
||||
|
||||
$task_template_id = intval($_GET['delete_task_template']);
|
||||
|
||||
$task_template_name = escapeSql(getFieldById('task_templates', $task_template_id, 'task_template_name'));
|
||||
$task = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT task_template_name, task_template_ticket_template_id FROM task_templates WHERE task_template_id = $task_template_id"));
|
||||
$row = ticketTemplateForRequest($task['task_template_ticket_template_id'] ?? 0);
|
||||
|
||||
mysqli_query($mysqli, "DELETE FROM task_templates WHERE task_template_id = $task_template_id");
|
||||
|
||||
logAudit("Ticket Template", "Edit", "$session_name deleted task $task_template_name from ticket template");
|
||||
logAudit("Ticket Template", "Edit", "$session_name deleted task " . escapeSql($task['task_template_name']) . " from ticket template " . escapeSql($row['ticket_template_name']), 0, intval($row['ticket_template_id']));
|
||||
|
||||
flashAlert("Task <strong>" . stripslashes($task_template_name) . "</strong> deleted", 'error');
|
||||
flashAlert("Task <strong>" . escapeHtml($task['task_template_name']) . "</strong> deleted", 'error');
|
||||
|
||||
redirect();
|
||||
|
||||
|
||||
@@ -1,161 +1,314 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* One ticket template - the ticket it starts (subject and details, with its
|
||||
* placeholders marked), how it sets the ticket up, its tasks (added, changed,
|
||||
* dragged into order) and their time, and its use: how many tickets it has
|
||||
* raised, the latest of them, and the recurring tickets and project templates
|
||||
* that raise it. The work is in functions/ticket_template.php.
|
||||
*/
|
||||
|
||||
require_once "includes/inc_all_admin.php";
|
||||
|
||||
|
||||
//Initialize the HTML Purifier to prevent XSS
|
||||
require "../libs/htmlpurifier/HTMLPurifier.standalone.php";
|
||||
|
||||
$purifier_config = HTMLPurifier_Config::createDefault();
|
||||
$purifier_config->set('Cache.DefinitionImpl', null); // Disable cache by setting a non-existent directory or an invalid one
|
||||
$purifier_config->set('URI.AllowedSchemes', ['data' => true, 'src' => true, 'http' => true, 'https' => true]);
|
||||
$purifier = new HTMLPurifier($purifier_config);
|
||||
|
||||
if (isset($_GET['ticket_template_id'])) {
|
||||
$ticket_template_id = intval($_GET['ticket_template_id']);
|
||||
}
|
||||
|
||||
$sql_ticket_template = mysqli_query($mysqli, "SELECT ticket_template_created_at, ticket_template_description, ticket_template_details,
|
||||
ticket_template_name, ticket_template_subject, ticket_template_updated_at FROM ticket_templates WHERE ticket_template_id = $ticket_template_id LIMIT 1");
|
||||
|
||||
if (mysqli_num_rows($sql_ticket_template) == 0) {
|
||||
echo "<center><h1 class='text-secondary mt-5'>Nothing to see here</h1><a class='btn btn-lg btn-secondary mt-3' href='javascript:history.back()'><i class='fa fa-fw fa-arrow-left'></i> Go Back</a></center>";
|
||||
$template = ticketTemplateRow($_GET['ticket_template_id'] ?? 0);
|
||||
if (!$template) {
|
||||
echo emptyStateHtml(['thing' => 'ticket templates', 'title' => 'That ticket template is gone', 'text' => 'It may have been deleted.', 'icon' => 'fa-life-ring', 'filtered' => false,
|
||||
'actions' => [['label' => 'Ticket Templates', 'href' => 'ticket_templates.php']]]);
|
||||
require_once "../includes/footer.php";
|
||||
exit();
|
||||
}
|
||||
|
||||
$row = mysqli_fetch_assoc($sql_ticket_template);
|
||||
$ticket_template_id = intval($template['ticket_template_id']);
|
||||
$name = escapeHtml($template['ticket_template_name']);
|
||||
$archived = !empty($template['ticket_template_archived_at']);
|
||||
$tasks = ticketTemplateTasks($ticket_template_id);
|
||||
$task_minutes = array_sum(array_column($tasks, 'estimate'));
|
||||
$tag_ids = ticketTemplateTagIds($ticket_template_id);
|
||||
$tag_rows = [];
|
||||
if ($tag_ids) {
|
||||
$sql = mysqli_query($mysqli, "SELECT * FROM tags WHERE tag_id IN (" . implode(',', $tag_ids) . ") ORDER BY tag_name");
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
$tag_rows[] = $row;
|
||||
}
|
||||
}
|
||||
$usage = ticketTemplateUsage([$ticket_template_id])[$ticket_template_id] ?? ['count' => 0, 'open' => 0, 'last' => null];
|
||||
$csrf = $_SESSION['csrf_token'];
|
||||
|
||||
$ticket_template_name = escapeHtml($row['ticket_template_name']);
|
||||
$ticket_template_description = escapeHtml($row['ticket_template_description']);
|
||||
$ticket_template_subject = escapeHtml($row['ticket_template_subject']);
|
||||
$ticket_template_details = $purifier->purify($row['ticket_template_details']);
|
||||
$ticket_template_created_at = escapeHtml($row['ticket_template_created_at']);
|
||||
$ticket_template_updated_at = escapeHtml($row['ticket_template_updated_at']);
|
||||
// The tickets it raised most lately
|
||||
$recent = [];
|
||||
$sql = mysqli_query($mysqli, "SELECT ticket_id, ticket_prefix, ticket_number, ticket_subject, ticket_client_id, ticket_created_at, ticket_resolved_at, ticket_closed_at, client_name, ticket_status_name, ticket_status_color
|
||||
FROM tickets
|
||||
LEFT JOIN clients ON client_id = ticket_client_id
|
||||
LEFT JOIN ticket_statuses ON ticket_status_id = ticket_status
|
||||
WHERE ticket_template_id = $ticket_template_id AND ticket_archived_at IS NULL
|
||||
ORDER BY ticket_created_at DESC, ticket_id DESC LIMIT 8");
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
$recent[] = $row;
|
||||
}
|
||||
|
||||
// Get Task Templates
|
||||
$sql_task_templates = mysqli_query($mysqli, "SELECT task_template_completion_estimate, task_template_id, task_template_name FROM task_templates WHERE task_template_ticket_template_id = $ticket_template_id ORDER BY task_template_order ASC, task_template_id ASC");
|
||||
// What raises it
|
||||
$recurring = [];
|
||||
$sql = mysqli_query($mysqli, "SELECT recurring_ticket_id, recurring_ticket_subject, recurring_ticket_frequency, recurring_ticket_client_id, client_name FROM recurring_tickets
|
||||
LEFT JOIN clients ON client_id = recurring_ticket_client_id
|
||||
WHERE recurring_ticket_ticket_template_id = $ticket_template_id ORDER BY client_name, recurring_ticket_subject");
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
$recurring[] = $row;
|
||||
}
|
||||
$projects = [];
|
||||
$sql = mysqli_query($mysqli, "SELECT project_templates.project_template_id, project_template_name FROM project_template_ticket_templates
|
||||
JOIN project_templates ON project_templates.project_template_id = project_template_ticket_templates.project_template_id
|
||||
WHERE project_template_ticket_templates.ticket_template_id = $ticket_template_id ORDER BY project_template_name");
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
$projects[] = $row;
|
||||
}
|
||||
|
||||
// Its placeholders - filled in from the ticket, or left for whoever picks it up
|
||||
$known = array_map('strtolower', array_keys(TICKET_PLACEHOLDERS));
|
||||
$found = ticketTemplatePlaceholdersIn($template['ticket_template_subject'] . ' ' . $template['ticket_template_details'] . ' ' . implode(' ', array_column($tasks, 'name')));
|
||||
$filled = array_values(array_filter($found, function ($word) use ($known) {
|
||||
return in_array(strtolower($word), $known, true);
|
||||
}));
|
||||
$left = array_values(array_diff($found, $filled));
|
||||
|
||||
// [Words] marked where they appear - escaped text, or HTML already made safe
|
||||
$mark = function ($html) {
|
||||
return 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>', $html);
|
||||
};
|
||||
$details = trim((string) $template['ticket_template_details']) !== '' ? $mark(documentHtmlPurify($template['ticket_template_details'])) : '';
|
||||
|
||||
$edit_url = "modals/ticket_template/ticket_template_edit.php?id=$ticket_template_id";
|
||||
|
||||
?>
|
||||
|
||||
<ol class="breadcrumb d-print-none">
|
||||
<li class="breadcrumb-item">
|
||||
<a href="../index.php">Home</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item">
|
||||
<a href="users.php">Admin</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item">
|
||||
<a href="ticket_templates.php">Ticket Templates</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item active"><i class="fas fa-life-ring me-2"></i><?= $ticket_template_name ?></li>
|
||||
<li class="breadcrumb-item"><a href="../index.php">Home</a></li>
|
||||
<li class="breadcrumb-item"><a href="users.php">Admin</a></li>
|
||||
<li class="breadcrumb-item"><a href="ticket_templates.php">Ticket Templates</a></li>
|
||||
<li class="breadcrumb-item active"><?= $name ?></li>
|
||||
</ol>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-9">
|
||||
|
||||
<div class="card card-dark">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title mt-1"><?= $ticket_template_name ?></h3>
|
||||
<div class="card-tools">
|
||||
<button type="button" class="btn btn-tool btn-sm" title="Edit template" data-bs-toggle="modal" data-bs-target="#editTicketTemplateModal">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<div class="card mb-3">
|
||||
<div class="card-body d-flex flex-wrap align-items-center gap-3">
|
||||
<span class="itflow-asset-icon itflow-asset-tone-other" aria-hidden="true"><i class="fas fa-fw fa-life-ring"></i></span>
|
||||
<div class="min-w-0 flex-grow-1">
|
||||
<h1 class="h4 mb-0"><?= $name ?></h1>
|
||||
<?php if (trim((string) $template['ticket_template_description']) !== '') { ?>
|
||||
<div class="text-secondary"><?= escapeHtml($template['ticket_template_description']) ?></div>
|
||||
<?php } ?>
|
||||
<?php if ($archived) { ?>
|
||||
<div class="small text-secondary"><i class="fas fa-fw fa-archive me-1"></i>Archived <?= date('M j, Y', strtotime($template['ticket_template_archived_at'])) ?> - not offered for new tickets</div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<?php if (!$archived) { ?>
|
||||
<a class="btn btn-primary" href="/agent/tickets.php?new_from_template=<?= $ticket_template_id ?>"><i class="fas fa-fw fa-plus me-2"></i>New ticket from it</a>
|
||||
<?php } ?>
|
||||
<button type="button" class="btn btn-outline-secondary ajax-modal" data-modal-size="lg" data-modal-url="<?= $edit_url ?>"><i class="fas fa-fw fa-edit me-2"></i>Edit</button>
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-outline-secondary" title="More actions" type="button" data-bs-toggle="dropdown"><i class="fas fa-fw fa-ellipsis-h"></i></button>
|
||||
<div class="dropdown-menu dropdown-menu-end">
|
||||
<a class="dropdown-item" href="post.php?copy_ticket_template=<?= $ticket_template_id ?>&csrf_token=<?= $csrf ?>"><i class="fas fa-fw fa-copy me-2"></i>Copy</a>
|
||||
<?php if ($archived) { ?>
|
||||
<a class="dropdown-item" href="post.php?restore_ticket_template=<?= $ticket_template_id ?>&csrf_token=<?= $csrf ?>"><i class="fas fa-fw fa-redo me-2"></i>Restore</a>
|
||||
<?php } else { ?>
|
||||
<a class="dropdown-item" href="post.php?archive_ticket_template=<?= $ticket_template_id ?>&csrf_token=<?= $csrf ?>"><i class="fas fa-fw fa-archive me-2"></i>Archive</a>
|
||||
<?php } ?>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item text-danger confirm-link" href="post.php?delete_ticket_template=<?= $ticket_template_id ?>&from_page=1&csrf_token=<?= $csrf ?>"
|
||||
data-confirm-title="Delete <?= $name ?>?" data-confirm-text="Its tasks go with it. Tickets raised from it stay as they are." data-confirm-button="Delete"><i class="fas fa-fw fa-trash me-2"></i>Delete</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body prettyContent">
|
||||
<?= $ticket_template_details ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-lg-8">
|
||||
|
||||
<div class="card mb-3">
|
||||
<div class="card-header d-flex align-items-center">
|
||||
<h3 class="card-title mb-0"><i class="fas fa-fw fa-life-ring me-2"></i>The ticket it starts</h3>
|
||||
<button type="button" class="btn btn-sm btn-link ms-auto ajax-modal" data-modal-size="lg" data-modal-url="<?= $edit_url ?>"><i class="fas fa-edit me-1"></i>Edit</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="small text-secondary">Subject</div>
|
||||
<div class="fw-semibold mb-3"><?= trim((string) $template['ticket_template_subject']) !== '' ? $mark(escapeHtml($template['ticket_template_subject'])) : '<span class="text-secondary fw-normal">None - typed in on New Ticket</span>' ?></div>
|
||||
<div class="small text-secondary">Details</div>
|
||||
<div class="prettyContent"><?= $details !== '' ? $details : '<span class="text-secondary">None</span>' ?></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
|
||||
<div class="card card-dark">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title"><i class="fa fa-fw fa-tasks me-2"></i>Tasks</h5>
|
||||
<div class="card mb-3">
|
||||
<div class="card-header d-flex align-items-center">
|
||||
<h3 class="card-title mb-0"><i class="fas fa-fw fa-tasks me-2"></i>Tasks</h3>
|
||||
<span class="ms-auto small text-secondary"><?= count($tasks) ?> task<?= count($tasks) === 1 ? '' : 's' ?><?= $task_minutes ? ' · <i class="far fa-clock me-1"></i>' . escapeHtml(ticketTemplateMinutesText($task_minutes)) : '' ?></span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form action="post.php" method="post" autocomplete="off">
|
||||
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
||||
<form action="post.php" method="post" autocomplete="off" class="mb-3">
|
||||
<input type="hidden" name="csrf_token" value="<?= $csrf ?>">
|
||||
<input type="hidden" name="ticket_template_id" value="<?= $ticket_template_id ?>">
|
||||
<div class="mb-3">
|
||||
<div class="input-group input-group-sm">
|
||||
<input type="text" class="form-control" name="task_name" placeholder="Create a task" required maxlength="200">
|
||||
<button type="submit" name="add_ticket_template_task" class="btn btn-primary" title="Add task"><i class="fas fa-fw fa-check"></i></button>
|
||||
<div class="row g-2">
|
||||
<div class="col-sm">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fas fa-fw fa-tasks"></i></span>
|
||||
<input type="text" class="form-control" name="task_name" placeholder="Add a task" required maxlength="200">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-8 col-sm-3">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="far fa-fw fa-clock"></i></span>
|
||||
<input type="number" class="form-control" name="task_estimate" min="0" placeholder="Mins" aria-label="Estimate in minutes">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-4 col-sm-auto">
|
||||
<button type="submit" name="add_ticket_template_task" class="btn btn-primary w-100"><i class="fas fa-fw fa-plus me-1"></i>Add</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<table class="table table-sm" id="tasks">
|
||||
<?php
|
||||
while($row = mysqli_fetch_assoc($sql_task_templates)){
|
||||
$task_id = intval($row['task_template_id']);
|
||||
$task_name = escapeHtml($row['task_template_name']);
|
||||
$task_completion_estimate = intval($row['task_template_completion_estimate']);
|
||||
//$task_description = escapeHtml($row['task_template_description']);
|
||||
?>
|
||||
<tr data-task-id="<?= $task_id ?>">
|
||||
<td>
|
||||
<a href="#" class="drag-handle"><i class="fas fa-bars text-muted me-2"></i></a>
|
||||
<span class="text-dark"><?= $task_name ?></span>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<div class="float-end">
|
||||
<div class="dropdown dropstart text-center">
|
||||
<button class="btn btn-light text-secondary btn-sm" title="More actions" type="button" data-bs-toggle="dropdown">
|
||||
<i class="fas fa-ellipsis-v"></i>
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item ajax-modal" href="#"
|
||||
data-modal-url="modals/ticket_template/ticket_template_task_edit.php?id=<?= $task_id ?>">
|
||||
<i class="fas fa-fw fa-edit me-2"></i>Edit
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item text-danger confirm-link" href="post.php?delete_task_template=<?= $task_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
|
||||
<i class="fas fa-fw fa-trash-alt me-2"></i>Delete
|
||||
</a>
|
||||
</div>
|
||||
<?php if ($tasks) { ?>
|
||||
<div class="list-group" id="ticketTemplateTasks">
|
||||
<?php foreach ($tasks as $number => $task) { ?>
|
||||
<div class="list-group-item d-flex align-items-center gap-2" data-task-id="<?= $task['id'] ?>">
|
||||
<span class="drag-handle text-secondary" title="Drag to reorder" style="cursor: grab;"><i class="fas fa-fw fa-grip-vertical"></i></span>
|
||||
<span class="text-secondary small" style="min-width: 1.5rem;"><?= $number + 1 ?>.</span>
|
||||
<span class="flex-grow-1 min-w-0 text-break"><?= $mark(escapeHtml($task['name'])) ?></span>
|
||||
<?php if ($task['estimate']) { ?><span class="small text-secondary text-nowrap"><i class="far fa-clock me-1"></i><?= escapeHtml(ticketTemplateMinutesText($task['estimate'])) ?></span><?php } ?>
|
||||
<div class="dropdown dropstart">
|
||||
<button class="btn btn-light text-secondary btn-sm" title="More actions" type="button" data-bs-toggle="dropdown"><i class="fas fa-ellipsis-v"></i></button>
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/ticket_template/ticket_template_task_edit.php?id=<?= $task['id'] ?>"><i class="fas fa-fw fa-edit me-2"></i>Edit</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item text-danger confirm-link" href="post.php?delete_task_template=<?= $task['id'] ?>&csrf_token=<?= $csrf ?>"><i class="fas fa-fw fa-trash-alt me-2"></i>Delete</a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
</table>
|
||||
</div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<?php } else { ?>
|
||||
<div class="text-secondary small">No tasks yet. A ticket raised from it gets a copy of each, in this order - and later changes here don't touch tickets already raised.</div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
|
||||
<div class="card mb-3">
|
||||
<div class="card-header d-flex align-items-center">
|
||||
<h3 class="card-title mb-0"><i class="fas fa-fw fa-sliders-h me-2"></i>Sets up</h3>
|
||||
<button type="button" class="btn btn-sm btn-link ms-auto ajax-modal" data-modal-size="lg" data-modal-url="<?= $edit_url ?>"><i class="fas fa-edit me-1"></i>Edit</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<?php
|
||||
$leave = '<span class="text-secondary">As it is</span>';
|
||||
$set_up = [
|
||||
'Category' => intval($template['ticket_template_category_id']) && !empty($template['category_name'])
|
||||
? categoryChip(['category_name' => $template['category_name'], 'category_type' => 'Ticket', 'category_color' => $template['category_color'], 'category_icon' => $template['category_icon']]) : $leave,
|
||||
'Priority' => !empty($template['ticket_template_priority']) ? ticketPriorityHtml($template['ticket_template_priority'], 'detail') : $leave,
|
||||
'Assign to' => intval($template['ticket_template_assigned_to']) && !empty($template['assignee_name'])
|
||||
? userAvatar($template['ticket_template_assigned_to'], $template['assignee_name'], $template['assignee_avatar'] ?? '', 'sm') . ' ' . escapeHtml($template['assignee_name']) : $leave,
|
||||
'Due' => intval($template['ticket_template_due_hours']) ? escapeHtml(ticketTemplateDueText($template['ticket_template_due_hours'])) . ' after it is raised' : '<span class="text-secondary">No due date</span>',
|
||||
];
|
||||
if (!empty($config_module_enable_accounting)) {
|
||||
$set_up['Billable'] = $template['ticket_template_billable'] === null ? $leave : (intval($template['ticket_template_billable']) ? 'Billable' : 'Not billable');
|
||||
}
|
||||
$set_up['Tags'] = $tag_rows ? '<span class="d-inline-flex flex-wrap gap-1">' . implode('', array_map('tagPill', $tag_rows)) . '</span>' : '<span class="text-secondary">None</span>';
|
||||
?>
|
||||
<dl class="row mb-0 small">
|
||||
<?php foreach ($set_up as $label => $value) { ?>
|
||||
<dt class="col-5 fw-normal text-secondary"><?= $label ?></dt>
|
||||
<dd class="col-7 mb-2"><?= $value ?></dd>
|
||||
<?php } ?>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($found) { ?>
|
||||
<div class="card mb-3">
|
||||
<div class="card-header"><h3 class="card-title mb-0"><i class="fas fa-fw fa-code me-2"></i>Placeholders</h3></div>
|
||||
<div class="card-body small">
|
||||
<?php if ($filled) { ?>
|
||||
<div class="text-secondary mb-1">Filled in from the ticket</div>
|
||||
<div class="d-flex flex-wrap gap-1 mb-3">
|
||||
<?php foreach ($filled as $word) { ?>
|
||||
<span class="badge rounded-pill text-bg-light border font-monospace fw-normal" title="<?= escapeHtml(ucfirst(TICKET_PLACEHOLDERS[ucfirst(strtolower($word))] ?? '')) ?>">[<?= escapeHtml($word) ?>]</span>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<?php } ?>
|
||||
<?php if ($left) { ?>
|
||||
<div class="text-secondary mb-1">Left for whoever picks it up</div>
|
||||
<div class="d-flex flex-wrap gap-1">
|
||||
<?php foreach ($left as $word) { ?>
|
||||
<span class="badge rounded-pill text-bg-warning font-monospace fw-normal">[<?= escapeHtml($word) ?>]</span>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
<div class="card mb-3">
|
||||
<div class="card-header"><h3 class="card-title mb-0"><i class="fas fa-fw fa-chart-line me-2"></i>Used</h3></div>
|
||||
<div class="card-body">
|
||||
<?php if ($usage['count']) { ?>
|
||||
<div class="mb-2"><strong><?= $usage['count'] ?></strong> ticket<?= $usage['count'] === 1 ? '' : 's' ?> raised<?= $usage['open'] ? ', <strong>' . $usage['open'] . '</strong> still open' : '' ?> <span class="small text-secondary">- last <?= timeAgo($usage['last']) ?></span></div>
|
||||
<div class="list-group list-group-flush small">
|
||||
<?php foreach ($recent as $row) {
|
||||
$open = !$row['ticket_resolved_at'] && !$row['ticket_closed_at']; ?>
|
||||
<a class="list-group-item list-group-item-action px-0 d-flex align-items-center gap-2" href="/agent/<?= escapeHtml(ticketUrl($row['ticket_id'], $row['ticket_client_id'])) ?>">
|
||||
<span class="font-monospace text-nowrap"><?= escapeHtml(ticketReference($row)) ?></span>
|
||||
<span class="min-w-0 flex-grow-1 text-truncate <?= $open ? '' : 'text-secondary' ?>"><?= escapeHtml($row['client_name'] ?: $row['ticket_subject']) ?></span>
|
||||
<?= ticketStatusHtml($row['ticket_status_name'] ?? '', $row['ticket_status_color'] ?? '', 'detail') ?>
|
||||
</a>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<?php } else { ?>
|
||||
<div class="small text-secondary">No tickets raised from it yet - counting started with this version of ITFlow.</div>
|
||||
<?php } ?>
|
||||
<?php if ($recurring || $projects) { ?>
|
||||
<div class="small text-secondary mt-3 mb-1">Raised by</div>
|
||||
<ul class="list-unstyled small mb-0">
|
||||
<?php foreach ($recurring as $row) { ?>
|
||||
<li class="mb-1"><i class="fas fa-fw fa-redo-alt text-secondary me-1"></i><a href="/agent/recurring_tickets.php?client_id=<?= intval($row['recurring_ticket_client_id']) ?>"><?= escapeHtml($row['recurring_ticket_subject']) ?></a> <span class="text-secondary">- <?= escapeHtml(trim($row['recurring_ticket_frequency'] . ', ' . ($row['client_name'] ?? ''), ', ')) ?></span></li>
|
||||
<?php } ?>
|
||||
<?php foreach ($projects as $row) { ?>
|
||||
<li class="mb-1"><i class="fas fa-fw fa-project-diagram text-secondary me-1"></i><a href="project_template.php?project_template_id=<?= intval($row['project_template_id']) ?>"><?= escapeHtml($row['project_template_name']) ?></a> <span class="text-secondary">- project template</span></li>
|
||||
<?php } ?>
|
||||
</ul>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="../js/pretty_content.js"></script>
|
||||
|
||||
<script src="../libs/SortableJS/Sortable.min.js"></script>
|
||||
<script>
|
||||
new Sortable(document.querySelector('table#tasks tbody'), {
|
||||
handle: '.drag-handle',
|
||||
animation: 150,
|
||||
onEnd: function (evt) {
|
||||
const rows = document.querySelectorAll('table#tasks tbody tr');
|
||||
const positions = Array.from(rows).map((row, index) => ({
|
||||
id: row.dataset.taskId,
|
||||
order: index
|
||||
}));
|
||||
|
||||
itflowPostForm('/agent/ajax.php', {
|
||||
update_task_templates_order: true,
|
||||
csrf_token: '<?= $_SESSION['csrf_token'] ?>',
|
||||
ticket_template_id: <?= $ticket_template_id ?>,
|
||||
positions: positions
|
||||
(function () {
|
||||
var list = document.getElementById('ticketTemplateTasks');
|
||||
if (!list) {
|
||||
return;
|
||||
}
|
||||
new Sortable(list, {
|
||||
handle: '.drag-handle',
|
||||
animation: 150,
|
||||
onEnd: function () {
|
||||
var positions = Array.from(list.querySelectorAll('[data-task-id]')).map(function (row, index) {
|
||||
row.querySelector('.text-secondary.small').textContent = (index + 1) + '.';
|
||||
return { id: row.dataset.taskId, order: index + 1 };
|
||||
});
|
||||
itflowPostForm('/agent/ajax.php', {
|
||||
update_task_templates_order: true,
|
||||
csrf_token: <?= json_encode($csrf) ?>,
|
||||
ticket_template_id: <?= $ticket_template_id ?>,
|
||||
positions: positions
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
<?php
|
||||
|
||||
require_once "modals/ticket_template/ticket_template_edit.php";
|
||||
require_once "../includes/footer.php";
|
||||
|
||||
@@ -1,25 +1,53 @@
|
||||
<?php
|
||||
|
||||
// Default Column Sortby Filter
|
||||
$sort = "ticket_template_name";
|
||||
$order = "ASC";
|
||||
/*
|
||||
* Admin > Ticket Templates - tickets ready to raise: a subject, details and
|
||||
* tasks, and how the ticket is set up (category, priority, billing, agent, due
|
||||
* time and tags). Each row shows what it sets up, its tasks and their time,
|
||||
* and how often it has been used. A template can be changed, copied as the
|
||||
* start of another, archived - kept for the recurring tickets and project
|
||||
* templates that use it, not offered for new tickets - or deleted. The work is
|
||||
* in functions/ticket_template.php; the handlers are admin/post/ticket_template.php.
|
||||
*/
|
||||
|
||||
require_once "includes/inc_all_admin.php";
|
||||
|
||||
$sql = mysqli_query(
|
||||
$mysqli,
|
||||
"SELECT SQL_CALC_FOUND_ROWS *,
|
||||
COUNT(task_template_id) AS task_count
|
||||
FROM ticket_templates
|
||||
LEFT JOIN task_templates ON task_template_ticket_template_id = ticket_template_id
|
||||
WHERE (ticket_template_name LIKE '%$q%' OR ticket_template_description LIKE '%$q%')
|
||||
AND ticket_template_archived_at IS NULL
|
||||
GROUP BY ticket_template_id
|
||||
ORDER BY $sort $order
|
||||
LIMIT $record_from, $record_to"
|
||||
);
|
||||
$show_archived = isset($_GET['archived']);
|
||||
$search = trim((string) ($_GET['q'] ?? ''));
|
||||
$search_sql = escapeSql($search);
|
||||
|
||||
$num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
|
||||
$templates = [];
|
||||
$sql = mysqli_query($mysqli, "SELECT ticket_templates.*, category_name, category_color, category_icon, user_name AS assignee_name,
|
||||
(SELECT COUNT(*) FROM task_templates WHERE task_template_ticket_template_id = ticket_templates.ticket_template_id) AS task_count,
|
||||
(SELECT COALESCE(SUM(task_template_completion_estimate), 0) FROM task_templates WHERE task_template_ticket_template_id = ticket_templates.ticket_template_id) AS task_minutes,
|
||||
(SELECT COUNT(*) FROM recurring_tickets WHERE recurring_ticket_ticket_template_id = ticket_templates.ticket_template_id) AS recurring_count,
|
||||
(SELECT COUNT(*) FROM project_template_ticket_templates WHERE project_template_ticket_templates.ticket_template_id = ticket_templates.ticket_template_id) AS project_count
|
||||
FROM ticket_templates
|
||||
LEFT JOIN categories ON category_id = ticket_template_category_id AND ticket_template_category_id > 0
|
||||
LEFT JOIN users ON user_id = ticket_template_assigned_to AND ticket_template_assigned_to > 0
|
||||
WHERE ticket_template_archived_at IS " . ($show_archived ? 'NOT NULL' : 'NULL') . "
|
||||
" . ($search !== '' ? "AND (ticket_template_name LIKE '%$search_sql%' OR ticket_template_description LIKE '%$search_sql%' OR ticket_template_subject LIKE '%$search_sql%')" : '') . "
|
||||
ORDER BY ticket_template_name ASC");
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
$templates[intval($row['ticket_template_id'])] = $row;
|
||||
}
|
||||
$usage = ticketTemplateUsage(array_keys($templates));
|
||||
$archived_count = intval(mysqli_fetch_row(mysqli_query($mysqli, "SELECT COUNT(*) FROM ticket_templates WHERE ticket_template_archived_at IS NOT NULL"))[0]);
|
||||
$tag_rows = [];
|
||||
$sql_tags = mysqli_query($mysqli, "SELECT ticket_template_id, tags.* FROM ticket_template_tags JOIN tags ON tags.tag_id = ticket_template_tags.tag_id ORDER BY tag_name");
|
||||
while ($row = mysqli_fetch_assoc($sql_tags)) {
|
||||
$tag_rows[intval($row['ticket_template_id'])][] = $row;
|
||||
}
|
||||
|
||||
// This page with some of its query changed - null drops a key
|
||||
$page_url = function (array $change = []) use ($show_archived, $search) {
|
||||
$query = array_merge(['q' => $search !== '' ? $search : null, 'archived' => $show_archived ? 1 : null], $change);
|
||||
return 'ticket_templates.php' . (($query = http_build_query(array_filter($query, function ($v) {
|
||||
return $v !== null;
|
||||
}))) ? "?$query" : '');
|
||||
};
|
||||
|
||||
$csrf = $_SESSION['csrf_token'];
|
||||
|
||||
?>
|
||||
|
||||
@@ -30,98 +58,132 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
|
||||
<button type="button" class="btn btn-primary ajax-modal" data-modal-url="modals/ticket_template/ticket_template_add.php" data-modal-size="lg"><i class="fas fa-plus me-2"></i>New Ticket Template</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-header py-3">
|
||||
<form autocomplete="off">
|
||||
<div class="row g-2 align-items-center">
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="input-group">
|
||||
<input type="search" class="form-control" name="q" value="<?php if(isset($q)){ echo stripslashes(escapeHtml($q)); } ?>" placeholder="Search Ticket Templates">
|
||||
<button class="btn btn-dark" title="Search"><i class="fa fa-search"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-8">
|
||||
</div>
|
||||
|
||||
<div class="p-2 border-bottom">
|
||||
<p class="small text-secondary mb-2">
|
||||
A ticket ready to raise - its subject, details and tasks, and how it is set up. Pick one on New Ticket, when raising tickets for several assets or clients, and on recurring tickets;
|
||||
project templates raise theirs. <span class="font-monospace">[Client]</span>, <span class="font-monospace">[Contact]</span>, <span class="font-monospace">[Month]</span> and the like are filled in when a ticket is raised.
|
||||
</p>
|
||||
<form autocomplete="off" class="d-flex flex-wrap align-items-center gap-2">
|
||||
<?php if ($show_archived) { ?><input type="hidden" name="archived" value="1"><?php } ?>
|
||||
<div class="input-group input-group-sm w-auto">
|
||||
<input type="search" class="form-control" name="q" value="<?= escapeHtml($search) ?>" placeholder="Search templates">
|
||||
<button class="btn btn-primary" title="Search"><i class="fa fa-search"></i></button>
|
||||
</div>
|
||||
<div class="btn-group btn-group-sm" role="group" aria-label="Current or archived">
|
||||
<a class="btn btn-outline-secondary<?= $show_archived ? '' : ' active' ?>" href="<?= escapeHtml($page_url(['archived' => null])) ?>">Current</a>
|
||||
<a class="btn btn-outline-secondary<?= $show_archived ? ' active' : '' ?>" href="<?= escapeHtml($page_url(['archived' => 1])) ?>">Archived <span class="badge text-bg-secondary ms-1"><?= $archived_count ?></span></a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive-sm">
|
||||
<table class="table table-striped table-borderless table-hover mb-0">
|
||||
<thead class="text-dark <?php if($num_rows[0] == 0) { echo "d-none"; } ?>">
|
||||
<tr>
|
||||
<th class="ps-3">
|
||||
<a class="text-secondary" href="?<?= $url_query_strings_sort ?>&sort=ticket_template_name&order=<?= $disp ?>">
|
||||
Template <?php if ($sort == 'ticket_template_name') { echo $order_icon; } ?>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a class="text-secondary" href="?<?= $url_query_strings_sort ?>&sort=task_count&order=<?= $disp ?>">
|
||||
Tasks <?php if ($sort == 'task_count') { echo $order_icon; } ?>
|
||||
</a>
|
||||
</th>
|
||||
<th class="text-center">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<table class="table table-borderless table-hover align-middle mb-0">
|
||||
<?php if ($templates) { ?>
|
||||
<thead class="text-dark">
|
||||
<tr>
|
||||
<th class="ps-3">Template</th>
|
||||
<th class="d-none d-lg-table-cell">Sets up</th>
|
||||
<th class="d-none d-sm-table-cell">Tasks</th>
|
||||
<th class="d-none d-md-table-cell">Used</th>
|
||||
<th class="text-center" style="width: 1%">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<?php } ?>
|
||||
<tbody>
|
||||
<?php
|
||||
|
||||
while($row = mysqli_fetch_assoc($sql)){
|
||||
$ticket_template_id = intval($row['ticket_template_id']);
|
||||
$ticket_template_name = escapeHtml($row['ticket_template_name']);
|
||||
$ticket_template_description = escapeHtml($row['ticket_template_description']);
|
||||
$ticket_template_subject = escapeHtml($row['ticket_template_subject']);
|
||||
$ticket_template_created_at = escapeHtml($row['ticket_template_created_at']);
|
||||
<?php foreach ($templates as $template_id => $row) {
|
||||
$name = escapeHtml($row['ticket_template_name']);
|
||||
$task_count = intval($row['task_count']);
|
||||
|
||||
$minutes = intval($row['task_minutes']);
|
||||
$used = $usage[$template_id] ?? ['count' => 0, 'open' => 0, 'last' => null];
|
||||
$tags = $tag_rows[$template_id] ?? [];
|
||||
$defaults = ticketTemplateDefaultsHtml($row);
|
||||
$tags_html = $tags ? '<span class="d-inline-flex flex-wrap gap-1">' . implode('', array_map('tagPill', $tags)) . '</span>' : '';
|
||||
$set_up = trim($defaults . ($defaults !== '' && $tags_html !== '' ? ' ' : '') . $tags_html);
|
||||
$linked = [];
|
||||
if (intval($row['recurring_count'])) {
|
||||
$linked[] = intval($row['recurring_count']) . ' recurring ticket' . (intval($row['recurring_count']) === 1 ? '' : 's');
|
||||
}
|
||||
if (intval($row['project_count'])) {
|
||||
$linked[] = intval($row['project_count']) . ' project template' . (intval($row['project_count']) === 1 ? '' : 's');
|
||||
}
|
||||
ob_start(); ?>
|
||||
<?php if ($used['count']) { ?>
|
||||
<div class="small text-nowrap"><?= $used['count'] ?> ticket<?= $used['count'] === 1 ? '' : 's' ?><?= $used['open'] ? ' <span class="text-secondary">· ' . $used['open'] . ' open</span>' : '' ?></div>
|
||||
<div class="small text-secondary text-nowrap" title="<?= escapeHtml($used['last']) ?>">Last <?= timeAgo($used['last']) ?></div>
|
||||
<?php } else { ?>
|
||||
<span class="small text-secondary">Not yet</span>
|
||||
<?php } ?>
|
||||
<?php if ($linked) { ?><div class="small text-secondary"><i class="fas fa-fw fa-link me-1"></i><?= escapeHtml(implode(', ', $linked)) ?></div><?php } ?>
|
||||
<?php $used_html = ob_get_clean();
|
||||
?>
|
||||
<tr>
|
||||
<td class="ps-3">
|
||||
<a class="text-dark">
|
||||
<div class="d-flex">
|
||||
<i class="fa fa-fw fa-2x fa-life-ring me-3"></i>
|
||||
<div class="flex-grow-1">
|
||||
<div>
|
||||
<a href="ticket_template.php?ticket_template_id=<?= $ticket_template_id ?>">
|
||||
<?= $ticket_template_name ?>
|
||||
</a>
|
||||
</div>
|
||||
<div><small class="text-secondary"><?= $ticket_template_description ?></small></div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<span class="itflow-asset-icon itflow-asset-tone-other d-none d-sm-inline-flex" aria-hidden="true"><i class="fas fa-fw fa-life-ring"></i></span>
|
||||
<div class="min-w-0">
|
||||
<a class="fw-semibold text-body" href="ticket_template.php?ticket_template_id=<?= $template_id ?>"><?= $name ?></a>
|
||||
<?php if (trim((string) $row['ticket_template_description']) !== '') { ?>
|
||||
<div class="small text-secondary"><?= escapeHtml($row['ticket_template_description']) ?></div>
|
||||
<?php } elseif (trim((string) $row['ticket_template_subject']) !== '') { ?>
|
||||
<div class="small text-secondary text-truncate"><?= escapeHtml($row['ticket_template_subject']) ?></div>
|
||||
<?php } ?>
|
||||
<?php if ($row['ticket_template_archived_at']) { ?>
|
||||
<div class="small text-secondary"><i class="fas fa-fw fa-archive me-1"></i>Archived <?= date('M j, Y', strtotime($row['ticket_template_archived_at'])) ?></div>
|
||||
<?php } ?>
|
||||
<!-- On smaller screens what it sets up and its use come along under the name -->
|
||||
<?php if ($set_up !== '') { ?><div class="d-lg-none mt-1"><?= $set_up ?></div><?php } ?>
|
||||
<div class="d-md-none mt-1"><?= $used_html ?></div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
<td><?= $task_count ?></td>
|
||||
<td class="d-none d-lg-table-cell"><?= $set_up !== '' ? $set_up : '<span class="small text-secondary">Just the subject and details</span>' ?></td>
|
||||
<td class="d-none d-sm-table-cell text-nowrap">
|
||||
<?php if ($task_count) { ?>
|
||||
<div class="small"><?= $task_count ?> task<?= $task_count === 1 ? '' : 's' ?></div>
|
||||
<?php if ($minutes) { ?><div class="small text-secondary"><i class="far fa-fw fa-clock me-1"></i><?= escapeHtml(ticketTemplateMinutesText($minutes)) ?></div><?php } ?>
|
||||
<?php } else { ?>
|
||||
<span class="small text-secondary">None</span>
|
||||
<?php } ?>
|
||||
</td>
|
||||
<td class="d-none d-md-table-cell"><?= $used_html ?></td>
|
||||
<td>
|
||||
<div class="dropdown dropstart text-center">
|
||||
<button class="btn btn-secondary btn-sm" title="More actions" data-bs-toggle="dropdown">
|
||||
<i class="fas fa-ellipsis-h"></i>
|
||||
</button>
|
||||
<button class="btn btn-secondary btn-sm" title="More actions" type="button" data-bs-toggle="dropdown"><i class="fas fa-ellipsis-h"></i></button>
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item text-danger text-bold confirm-link" href="post.php?delete_ticket_template=<?= $ticket_template_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
|
||||
<i class="fas fa-fw fa-trash me-2"></i>Delete
|
||||
</a>
|
||||
<a class="dropdown-item ajax-modal" href="#" data-modal-size="lg" data-modal-url="modals/ticket_template/ticket_template_edit.php?id=<?= $template_id ?>"><i class="fas fa-fw fa-edit me-2"></i>Edit</a>
|
||||
<?php if (!$row['ticket_template_archived_at']) { ?>
|
||||
<a class="dropdown-item" href="/agent/tickets.php?new_from_template=<?= $template_id ?>"><i class="fas fa-fw fa-plus me-2"></i>New ticket from it</a>
|
||||
<?php } ?>
|
||||
<a class="dropdown-item" href="post.php?copy_ticket_template=<?= $template_id ?>&csrf_token=<?= $csrf ?>" title="A new template starting from this one"><i class="fas fa-fw fa-copy me-2"></i>Copy</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<?php if ($row['ticket_template_archived_at']) { ?>
|
||||
<a class="dropdown-item" href="post.php?restore_ticket_template=<?= $template_id ?>&csrf_token=<?= $csrf ?>"><i class="fas fa-fw fa-redo me-2"></i>Restore</a>
|
||||
<?php } else { ?>
|
||||
<a class="dropdown-item" href="post.php?archive_ticket_template=<?= $template_id ?>&csrf_token=<?= $csrf ?>" title="Not offered for new tickets - recurring tickets and project templates that use it keep it"><i class="fas fa-fw fa-archive me-2"></i>Archive</a>
|
||||
<?php } ?>
|
||||
<a class="dropdown-item text-danger text-bold confirm-link" href="post.php?delete_ticket_template=<?= $template_id ?>&csrf_token=<?= $csrf ?>"
|
||||
data-confirm-title="Delete <?= $name ?>?" data-confirm-text="Its tasks go with it<?= $linked ? ', and ' . escapeHtml(implode(' and ', $linked)) . ' stop using it' : '' ?>. Tickets raised from it stay as they are." data-confirm-button="Delete"><i class="fas fa-fw fa-trash me-2"></i>Delete</a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<?php } ?>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php
|
||||
$empty_state = [
|
||||
'thing' => 'ticket templates',
|
||||
'icon' => 'fa-life-ring',
|
||||
'text' => 'The details and tasks to start a ticket with.',
|
||||
'actions' => [['label' => 'New Ticket Template', 'modal' => 'modals/ticket_template/ticket_template_add.php', 'size' => 'lg']],
|
||||
];
|
||||
?>
|
||||
<?php require_once "../includes/filter_footer.php";
|
||||
?>
|
||||
|
||||
<?php if (!$templates) { ?>
|
||||
<?= emptyStateHtml([
|
||||
'thing' => 'ticket templates',
|
||||
'title' => $show_archived ? 'No archived ticket templates' : 'No ticket templates yet',
|
||||
'text' => $show_archived ? 'An archived template isn\'t offered for new tickets, but the recurring tickets and project templates that use it keep it.' : 'A ticket ready to raise - its subject, details and tasks, and how it is set up.',
|
||||
'icon' => 'fa-life-ring',
|
||||
'filtered' => $search !== '',
|
||||
'clear_url' => $page_url(['q' => null]),
|
||||
'actions' => $show_archived ? [] : [['label' => 'New Ticket Template', 'modal' => 'modals/ticket_template/ticket_template_add.php', 'size' => 'lg']],
|
||||
]) ?>
|
||||
<?php } ?>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
|
||||
@@ -3,69 +3,76 @@
|
||||
/*
|
||||
* ITFlow - Ticket template picker
|
||||
*
|
||||
* Shared by the ticket add modal and the recurring ticket add/edit modals. Each
|
||||
* option carries the template's subject, details and task list as data
|
||||
* attributes, which agent/js/ticket_tasks_modal.js applies to the form when the
|
||||
* template is picked.
|
||||
* Shared by New Ticket, the New Tickets windows of the assets and clients
|
||||
* lists, and the recurring ticket add/edit windows. Each option carries what
|
||||
* the template puts into the form (ticketTemplatePickerData(),
|
||||
* functions/ticket_template.php) - its subject, details and tasks, and its
|
||||
* category, priority, billing, agent, due time and tags - which
|
||||
* agent/js/ticket_tasks_modal.js writes into the form's fields marked
|
||||
* data-template-field when it is picked. A form leaves out the fields it
|
||||
* doesn't have.
|
||||
*
|
||||
* Set $selected_ticket_template_id before including to pre-select a template (an
|
||||
* archived one stays listed while it is the one selected, so an existing link
|
||||
* remains visible). Defaults to none.
|
||||
* Set before including:
|
||||
* $selected_ticket_template_id the one to start with (an archived one stays listed
|
||||
* while it is the one selected, so an existing link shows)
|
||||
* $ticket_template_field the field's name - ticket_template_id by default
|
||||
* $ticket_template_apply_on_load fill the form in from the one selected as it opens
|
||||
* $ticket_template_hint the line under it, when it should say something else
|
||||
*/
|
||||
|
||||
$selected_ticket_template_id = intval($selected_ticket_template_id ?? 0);
|
||||
$ticket_template_field = $ticket_template_field ?? 'ticket_template_id';
|
||||
$ticket_template_apply_on_load = !empty($ticket_template_apply_on_load);
|
||||
$ticket_template_hint = $ticket_template_hint ?? 'Picking one fills in the ticket - change anything before it is made. <span class="font-monospace">[Client]</span>, <span class="font-monospace">[Month]</span> and the like are filled in when it is saved.';
|
||||
|
||||
// Every template's tasks in one pass, rather than a query per option
|
||||
// Every template's tasks and tags in one pass each, rather than a query per option
|
||||
$ticket_template_tasks = [];
|
||||
|
||||
$sql_ticket_template_tasks = mysqli_query(
|
||||
$mysqli,
|
||||
"SELECT task_template_ticket_template_id, task_template_name, task_template_completion_estimate
|
||||
FROM task_templates
|
||||
ORDER BY task_template_order ASC, task_template_id ASC"
|
||||
);
|
||||
|
||||
$sql_ticket_template_tasks = mysqli_query($mysqli, "SELECT task_template_id, task_template_ticket_template_id, task_template_name, task_template_completion_estimate
|
||||
FROM task_templates ORDER BY task_template_order ASC, task_template_id ASC");
|
||||
while ($row = mysqli_fetch_assoc($sql_ticket_template_tasks)) {
|
||||
$ticket_template_tasks[intval($row['task_template_ticket_template_id'])][] = [
|
||||
'name' => $row['task_template_name'],
|
||||
'estimate' => intval($row['task_template_completion_estimate'])
|
||||
];
|
||||
$ticket_template_tasks[intval($row['task_template_ticket_template_id'])][] = ['id' => intval($row['task_template_id']), 'name' => $row['task_template_name'], 'estimate' => intval($row['task_template_completion_estimate'])];
|
||||
}
|
||||
$ticket_template_tags = [];
|
||||
$sql_ticket_template_tags = mysqli_query($mysqli, "SELECT ticket_template_id, ticket_template_tags.tag_id FROM ticket_template_tags
|
||||
JOIN tags ON tags.tag_id = ticket_template_tags.tag_id AND tag_archived_at IS NULL");
|
||||
while ($row = mysqli_fetch_row($sql_ticket_template_tags)) {
|
||||
$ticket_template_tags[intval($row[0])][] = intval($row[1]);
|
||||
}
|
||||
|
||||
$sql_ticket_templates = mysqli_query($mysqli, "SELECT ticket_templates.*, category_name, user_name AS assignee_name FROM ticket_templates
|
||||
LEFT JOIN categories ON category_id = ticket_template_category_id AND ticket_template_category_id > 0
|
||||
LEFT JOIN users ON user_id = ticket_template_assigned_to AND ticket_template_assigned_to > 0
|
||||
WHERE ticket_template_archived_at IS NULL OR ticket_template_id = $selected_ticket_template_id
|
||||
ORDER BY ticket_template_name ASC");
|
||||
|
||||
?>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Template</label>
|
||||
<label class="form-label">Template</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-cube"></i></span>
|
||||
<select class="form-select select2" id="ticket_template_select" name="ticket_template_id">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-cube"></i></span>
|
||||
<select class="form-select select2" id="ticket_template_select" name="<?= escapeHtml($ticket_template_field) ?>" data-template-picker<?= $ticket_template_apply_on_load ? ' data-template-apply="1"' : '' ?>>
|
||||
<option value="0">- No Template -</option>
|
||||
<?php
|
||||
$sql_ticket_templates = mysqli_query(
|
||||
$mysqli,
|
||||
"SELECT ticket_template_id, ticket_template_name, ticket_template_subject, ticket_template_details
|
||||
FROM ticket_templates
|
||||
WHERE ticket_template_archived_at IS NULL OR ticket_template_id = $selected_ticket_template_id
|
||||
ORDER BY ticket_template_name ASC"
|
||||
);
|
||||
|
||||
while ($row = mysqli_fetch_assoc($sql_ticket_templates)) {
|
||||
$ticket_template_id_select = intval($row['ticket_template_id']);
|
||||
$ticket_template_name_select = escapeHtml($row['ticket_template_name']);
|
||||
$ticket_template_subject_select = escapeHtml($row['ticket_template_subject']);
|
||||
$ticket_template_details_select = escapeHtml($row['ticket_template_details']);
|
||||
$ticket_template_task_list = $ticket_template_tasks[$ticket_template_id_select] ?? [];
|
||||
$task_count = count($ticket_template_task_list);
|
||||
<?php while ($row = mysqli_fetch_assoc($sql_ticket_templates)) {
|
||||
$id = intval($row['ticket_template_id']);
|
||||
$tasks = $ticket_template_tasks[$id] ?? [];
|
||||
// A second line saying what it sets up, and what it is for
|
||||
$about = [count($tasks) . ' task' . (count($tasks) === 1 ? '' : 's')];
|
||||
if (!empty($row['ticket_template_priority'])) {
|
||||
$about[] = $row['ticket_template_priority'];
|
||||
}
|
||||
if (!empty($row['category_name'])) {
|
||||
$about[] = $row['category_name'];
|
||||
}
|
||||
if (intval($row['ticket_template_due_hours'])) {
|
||||
$about[] = 'due in ' . ticketTemplateDueText($row['ticket_template_due_hours']);
|
||||
}
|
||||
$description = trim((string) $row['ticket_template_description']);
|
||||
$data = ticketTemplatePickerData($row, $tasks, $ticket_template_tags[$id] ?? []);
|
||||
?>
|
||||
<option value="<?= $ticket_template_id_select ?>"
|
||||
data-subject="<?= $ticket_template_subject_select ?>"
|
||||
data-details="<?= $ticket_template_details_select ?>"
|
||||
data-tasks="<?= escapeHtml(json_encode($ticket_template_task_list)) ?>"
|
||||
<?php if ($selected_ticket_template_id == $ticket_template_id_select) { echo "selected"; } ?>>
|
||||
<?= $ticket_template_name_select ?> (<?= $task_count ?> tasks)
|
||||
</option>
|
||||
<option value="<?= $id ?>" <?= $selected_ticket_template_id === $id ? 'selected' : '' ?><?= selectOptionData(['icon' => 'fas fa-cube', 'description' => implode(' · ', $about) . ($description !== '' ? " - $description" : ''), 'keywords' => $description]) ?><?php foreach ($data as $key => $value) { ?> data-template-<?= $key ?>="<?= escapeHtml($value) ?>"<?php } ?>><?= escapeHtml($row['ticket_template_name']) ?><?= $row['ticket_template_archived_at'] ? ' (archived)' : '' ?></option>
|
||||
<?php } ?>
|
||||
</select>
|
||||
</div>
|
||||
<small class="form-text text-muted">Picking a template fills in the subject, details and tasks below. You can edit them afterwards.</small>
|
||||
<div class="form-text"><?= $ticket_template_hint ?></div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Editable task rows for the ticket and recurring ticket add/edit modals, and the
|
||||
// ticket template picker that pre-fills them.
|
||||
// ticket template picker (agent/includes/inc_ticket_template_select.php) that
|
||||
// fills in the form - its fields marked data-template-field.
|
||||
//
|
||||
// Rows submit as parallel tasks[] and task_estimates[] arrays, aligned by their
|
||||
// order in the form, so removing a row removes both of its inputs together.
|
||||
@@ -86,27 +87,6 @@
|
||||
});
|
||||
}
|
||||
|
||||
// dataset always gives a string; jQuery used to auto-parse JSON here, so
|
||||
// parse it explicitly and still tolerate an already-parsed value
|
||||
function readTemplateTasks(option) {
|
||||
|
||||
const tasks = option.dataset.tasks;
|
||||
|
||||
if (!tasks) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (typeof tasks === 'string') {
|
||||
try {
|
||||
return JSON.parse(tasks);
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
return tasks;
|
||||
}
|
||||
|
||||
document.addEventListener('click', function (e) {
|
||||
if (!e.target.closest('#ticketTaskAdd')) {
|
||||
return;
|
||||
@@ -125,38 +105,101 @@
|
||||
}
|
||||
});
|
||||
|
||||
// Ticket template picker - fills in the subject, details and task rows
|
||||
document.addEventListener('change', function (e) {
|
||||
const select = e.target.closest('#ticket_template_select');
|
||||
if (!select) {
|
||||
// The form's field for one thing a template sets - marked data-template-field
|
||||
function templateField(form, name) {
|
||||
return form.querySelector('[data-template-field="' + name + '"]');
|
||||
}
|
||||
|
||||
// Pick a value in a <select> when it has it - the picker redraws by itself
|
||||
function pickOption(select, value) {
|
||||
if (!select || value === '' || value === undefined) {
|
||||
return;
|
||||
}
|
||||
const option = Array.from(select.options).find(o => o.value === String(value));
|
||||
if (option) {
|
||||
select.value = option.value;
|
||||
select.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
}
|
||||
|
||||
// Write a template into its form: what it sets, field by field. A field
|
||||
// the form doesn't have is skipped, and anything the template leaves
|
||||
// alone ('' on the option) stays as it is.
|
||||
function applyTemplate(select) {
|
||||
|
||||
const option = select.options[select.selectedIndex];
|
||||
const form = select.closest('form');
|
||||
|
||||
// Selecting "- No Template -" only unlinks the template - it must not wipe
|
||||
// whatever the user has already written or added
|
||||
if (!parseInt(option.value, 10)) {
|
||||
if (!option || !parseInt(option.value, 10) || !form) {
|
||||
return;
|
||||
}
|
||||
const data = option.dataset;
|
||||
|
||||
const templateSubject = option.dataset.subject || '';
|
||||
const templateDetails = option.dataset.details || '';
|
||||
|
||||
document.getElementById('subjectInput').value = templateSubject;
|
||||
|
||||
if (window.tinymce) {
|
||||
const editor = tinymce.get('detailsInput');
|
||||
if (editor) {
|
||||
editor.setContent(templateDetails);
|
||||
} else {
|
||||
document.getElementById('detailsInput').value = templateDetails;
|
||||
}
|
||||
} else {
|
||||
document.getElementById('detailsInput').value = templateDetails;
|
||||
const subject = templateField(form, 'subject') || document.getElementById('subjectInput');
|
||||
if (subject && data.templateSubject) {
|
||||
subject.value = data.templateSubject;
|
||||
}
|
||||
|
||||
setTaskRows(readTemplateTasks(option));
|
||||
const details = templateField(form, 'details') || document.getElementById('detailsInput');
|
||||
if (details && data.templateDetails) {
|
||||
const editor = window.tinymce && details.id ? tinymce.get(details.id) : null;
|
||||
if (editor) {
|
||||
editor.setContent(data.templateDetails);
|
||||
} else {
|
||||
details.value = data.templateDetails;
|
||||
}
|
||||
}
|
||||
|
||||
pickOption(templateField(form, 'priority'), data.templatePriority);
|
||||
pickOption(templateField(form, 'category'), data.templateCategory);
|
||||
pickOption(templateField(form, 'assigned'), data.templateAssigned);
|
||||
|
||||
const billable = templateField(form, 'billable');
|
||||
if (billable && data.templateBillable !== '' && data.templateBillable !== undefined) {
|
||||
billable.checked = data.templateBillable === '1';
|
||||
}
|
||||
|
||||
const due = templateField(form, 'due');
|
||||
if (due && data.templateDue) {
|
||||
due.value = data.templateDue;
|
||||
}
|
||||
|
||||
// Its tags are added to any already picked
|
||||
const tags = templateField(form, 'tags');
|
||||
if (tags && data.templateTags) {
|
||||
data.templateTags.split(',').forEach(function (id) {
|
||||
const tag = Array.from(tags.options).find(o => o.value === id);
|
||||
if (tag) {
|
||||
tag.selected = true;
|
||||
}
|
||||
});
|
||||
tags.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
|
||||
if (document.getElementById('ticketTasksContainer')) {
|
||||
let tasks = [];
|
||||
try {
|
||||
tasks = JSON.parse(data.templateTasks || '[]');
|
||||
} catch (error) {
|
||||
tasks = [];
|
||||
}
|
||||
setTaskRows(tasks);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('change', function (e) {
|
||||
const select = e.target.closest('[data-template-picker]');
|
||||
if (select) {
|
||||
applyTemplate(select);
|
||||
}
|
||||
});
|
||||
|
||||
// Opened for one template - New ticket from it - it is filled in straight away
|
||||
document.querySelectorAll('[data-template-picker][data-template-apply]').forEach(function (select) {
|
||||
select.removeAttribute('data-template-apply');
|
||||
applyTemplate(select);
|
||||
});
|
||||
|
||||
})();
|
||||
|
||||
@@ -21,16 +21,22 @@ ob_start();
|
||||
|
||||
<div class="modal-body">
|
||||
|
||||
<?php
|
||||
$ticket_template_field = 'bulk_ticket_template_id';
|
||||
$ticket_template_hint = "Picking one fills in the tickets - change anything before they are made. <span class=\"font-monospace\">[Asset]</span>, <span class=\"font-monospace\">[Contact]</span> and the like are filled in for each; with <span class=\"font-monospace\">[Asset]</span> in the subject, the asset's name isn't put in front.";
|
||||
require_once '../../includes/inc_ticket_template_select.php';
|
||||
?>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Subject <strong class="text-danger">*</strong></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-tag"></i></span>
|
||||
<input type="text" class="form-control" name="bulk_subject" placeholder="Asset Name will be prepended to Subject" maxlength="200" required>
|
||||
<input type="text" class="form-control" name="bulk_subject" data-template-field="subject" placeholder="Asset Name will be prepended to Subject" maxlength="500" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<textarea class="form-control tinymceTicket" id="textInput" name="bulk_details"></textarea>
|
||||
<textarea class="form-control tinymceTicket" id="textInput" name="bulk_details" data-template-field="details"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
@@ -40,7 +46,7 @@ ob_start();
|
||||
<label>Priority <strong class="text-danger">*</strong></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-thermometer-half"></i></span>
|
||||
<select class="form-select select2" name="bulk_priority" required>
|
||||
<select class="form-select select2" name="bulk_priority" data-template-field="priority" required>
|
||||
<option data-color="var(--bs-info)">Low</option>
|
||||
<option selected data-color="var(--bs-warning)">Medium</option>
|
||||
<option data-color="var(--bs-danger)">High</option>
|
||||
@@ -55,7 +61,7 @@ ob_start();
|
||||
<label>Category</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-layer-group"></i></span>
|
||||
<select class="form-select select2" name="bulk_category">
|
||||
<select class="form-select select2" name="bulk_category" data-template-field="category">
|
||||
<option value="0">- Not Categorized -</option>
|
||||
<?= categoryOptionsHtml('Ticket') ?>
|
||||
</select>
|
||||
@@ -70,7 +76,7 @@ ob_start();
|
||||
<label>Assign to</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-user-check"></i></span>
|
||||
<select class="form-select select2" name="bulk_assigned_to">
|
||||
<select class="form-select select2" name="bulk_assigned_to" data-template-field="assigned">
|
||||
<option value="0">Not Assigned</option>
|
||||
<?php
|
||||
|
||||
@@ -117,7 +123,7 @@ ob_start();
|
||||
<?php if ($config_module_enable_accounting) { ?>
|
||||
<div class="mb-3">
|
||||
<div class="form-check form-switch">
|
||||
<input type="checkbox" class="form-check-input" name="bulk_billable" <?php if ($config_ticket_default_billable == 1) { echo "checked"; } ?> value="1" id="billableSwitch">
|
||||
<input type="checkbox" class="form-check-input" name="bulk_billable" data-template-field="billable" <?php if ($config_ticket_default_billable == 1) { echo "checked"; } ?> value="1" id="billableSwitch">
|
||||
<label class="form-check-label" for="billableSwitch">Billable</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -131,5 +137,7 @@ ob_start();
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<script src="/agent/js/ticket_tasks_modal.js"></script>
|
||||
|
||||
<?php
|
||||
require_once '../../../includes/modal_footer.php';
|
||||
|
||||
@@ -22,16 +22,22 @@ ob_start();
|
||||
|
||||
<div class="modal-body">
|
||||
|
||||
<?php
|
||||
$ticket_template_field = 'bulk_ticket_template_id';
|
||||
$ticket_template_hint = "Picking one fills in the tickets - change anything before they are made. <span class=\"font-monospace\">[Client]</span>, <span class=\"font-monospace\">[Month]</span> and the like are filled in for each.";
|
||||
require_once '../../includes/inc_ticket_template_select.php';
|
||||
?>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Subject <strong class="text-danger">*</strong></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fas fa-fw fa-tag"></i></span>
|
||||
<input type="text" class="form-control" name="bulk_subject" placeholder="Enter a subject" maxlength="200" required>
|
||||
<input type="text" class="form-control" name="bulk_subject" data-template-field="subject" placeholder="Enter a subject" maxlength="500" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<textarea class="form-control tinymceTicket" id="textInput" name="bulk_details"></textarea>
|
||||
<textarea class="form-control tinymceTicket" id="textInput" name="bulk_details" data-template-field="details"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
@@ -41,7 +47,7 @@ ob_start();
|
||||
<label>Priority <strong class="text-danger">*</strong></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-thermometer-half"></i></span>
|
||||
<select class="form-select select2" name="bulk_priority" required>
|
||||
<select class="form-select select2" name="bulk_priority" data-template-field="priority" required>
|
||||
<option data-color="var(--bs-info)">Low</option>
|
||||
<option selected data-color="var(--bs-warning)">Medium</option>
|
||||
<option data-color="var(--bs-danger)">High</option>
|
||||
@@ -56,7 +62,7 @@ ob_start();
|
||||
<label>Category</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-layer-group"></i></span>
|
||||
<select class="form-select select2" name="bulk_category">
|
||||
<select class="form-select select2" name="bulk_category" data-template-field="category">
|
||||
<option value="0">- Not Categorized -</option>
|
||||
<?= categoryOptionsHtml('Ticket') ?>
|
||||
</select>
|
||||
@@ -74,7 +80,7 @@ ob_start();
|
||||
<label>Assign to</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-user-check"></i></span>
|
||||
<select class="form-select select2" name="bulk_assigned_to">
|
||||
<select class="form-select select2" name="bulk_assigned_to" data-template-field="assigned">
|
||||
<option value="0">Not Assigned</option>
|
||||
<?php
|
||||
|
||||
@@ -118,10 +124,17 @@ ob_start();
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-check form-switch">
|
||||
<input type="checkbox" class="form-check-input" name="use_primary_contact" value="1" id="bulkTicketPrimaryContact">
|
||||
<label class="form-check-label" for="bulkTicketPrimaryContact">For each client's primary contact</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($config_module_enable_accounting) { ?>
|
||||
<div class="mb-3">
|
||||
<div class="form-check form-switch">
|
||||
<input type="checkbox" class="form-check-input" name="bulk_billable" <?php if ($config_ticket_default_billable == 1) { echo "checked"; } ?> value="1" id="billableSwitch">
|
||||
<input type="checkbox" class="form-check-input" name="bulk_billable" data-template-field="billable" <?php if ($config_ticket_default_billable == 1) { echo "checked"; } ?> value="1" id="billableSwitch">
|
||||
<label class="form-check-label" for="billableSwitch">Billable</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -135,5 +148,7 @@ ob_start();
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<script src="/agent/js/ticket_tasks_modal.js"></script>
|
||||
|
||||
<?php
|
||||
require_once '../../../includes/modal_footer.php';
|
||||
|
||||
@@ -92,18 +92,21 @@ ob_start();
|
||||
|
||||
<?php } ?>
|
||||
|
||||
<?php require_once '../../includes/inc_ticket_template_select.php'; ?>
|
||||
<?php
|
||||
$ticket_template_hint = "Picking one fills in the recurring ticket - change anything before it is saved. <span class=\"font-monospace\">[Client]</span>, <span class=\"font-monospace\">[Month]</span> and the like are filled in each time it raises a ticket.";
|
||||
require_once '../../includes/inc_ticket_template_select.php';
|
||||
?>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Subject <strong class="text-danger">*</strong></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-tag"></i></span>
|
||||
<input type="text" class="form-control" id="subjectInput" name="subject" placeholder="Subject" maxlength="500" required>
|
||||
<input type="text" class="form-control" id="subjectInput" name="subject" data-template-field="subject" placeholder="Subject" maxlength="500" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<textarea class="form-control tinymceTicket" id="detailsInput" name="details"></textarea>
|
||||
<textarea class="form-control tinymceTicket" id="detailsInput" name="details" data-template-field="details"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
@@ -113,7 +116,7 @@ ob_start();
|
||||
<label>Priority <strong class="text-danger">*</strong></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-thermometer-half"></i></span>
|
||||
<select class="form-select select2" name="priority" required>
|
||||
<select class="form-select select2" name="priority" data-template-field="priority" required>
|
||||
<option data-color="var(--bs-info)">Low</option>
|
||||
<option selected data-color="var(--bs-warning)">Medium</option>
|
||||
<option data-color="var(--bs-danger)">High</option>
|
||||
@@ -128,7 +131,7 @@ ob_start();
|
||||
<label>Category</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-layer-group"></i></span>
|
||||
<select class="form-select select2" name="category_id">
|
||||
<select class="form-select select2" name="category_id" data-template-field="category">
|
||||
<option value="0">- Not Categorized -</option>
|
||||
<?= categoryOptionsHtml('Ticket') ?>
|
||||
</select>
|
||||
@@ -143,7 +146,7 @@ ob_start();
|
||||
<label>Assign to</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-user-check"></i></span>
|
||||
<select class="form-select select2" name="assigned_to">
|
||||
<select class="form-select select2" name="assigned_to" data-template-field="assigned">
|
||||
<option value="0">- Unassigned -</option>
|
||||
<?php
|
||||
|
||||
@@ -164,7 +167,7 @@ ob_start();
|
||||
<?php if ($config_module_enable_accounting && lookupUserPermission('module_sales') >= 2) { ?>
|
||||
<div class="mb-3">
|
||||
<div class="form-check form-switch">
|
||||
<input type="checkbox" class="form-check-input" name="billable" <?php if ($config_ticket_default_billable == 1) { echo "checked"; } ?> value="1" id="billable">
|
||||
<input type="checkbox" class="form-check-input" name="billable" <?php if ($config_ticket_default_billable == 1) { echo "checked"; } ?> value="1" id="billable" data-template-field="billable">
|
||||
<label class="form-check-label" for="billable">The tickets it raises are billable</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -107,6 +107,7 @@ ob_start();
|
||||
|
||||
<?php
|
||||
$selected_ticket_template_id = intval($recurring['recurring_ticket_ticket_template_id']);
|
||||
$ticket_template_hint = "Picking one fills in the recurring ticket - change anything before it is saved. <span class=\"font-monospace\">[Client]</span>, <span class=\"font-monospace\">[Month]</span> and the like are filled in each time it raises a ticket.";
|
||||
require_once '../../includes/inc_ticket_template_select.php';
|
||||
?>
|
||||
|
||||
@@ -114,12 +115,12 @@ ob_start();
|
||||
<label>Subject <strong class="text-danger">*</strong></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-tag"></i></span>
|
||||
<input type="text" class="form-control" id="subjectInput" name="subject" placeholder="Subject" maxlength="500" value="<?= escapeHtml($recurring['recurring_ticket_subject']) ?>" required>
|
||||
<input type="text" class="form-control" id="subjectInput" name="subject" data-template-field="subject" placeholder="Subject" maxlength="500" value="<?= escapeHtml($recurring['recurring_ticket_subject']) ?>" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<textarea class="form-control tinymceTicket" id="detailsInput" name="details"><?= escapeHtml($recurring['recurring_ticket_details']) ?></textarea>
|
||||
<textarea class="form-control tinymceTicket" id="detailsInput" name="details" data-template-field="details"><?= escapeHtml($recurring['recurring_ticket_details']) ?></textarea>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
@@ -128,7 +129,7 @@ ob_start();
|
||||
<label>Priority <strong class="text-danger">*</strong></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-thermometer-half"></i></span>
|
||||
<select class="form-select select2" name="priority" required>
|
||||
<select class="form-select select2" name="priority" data-template-field="priority" required>
|
||||
<?php foreach (['Low' => 'var(--bs-info)', 'Medium' => 'var(--bs-warning)', 'High' => 'var(--bs-danger)', 'Urgent' => 'var(--bs-emphasis-color)'] as $priority => $color) { ?>
|
||||
<option <?= $recurring['recurring_ticket_priority'] === $priority ? 'selected' : '' ?> data-color="<?= $color ?>"><?= $priority ?></option>
|
||||
<?php } ?>
|
||||
@@ -141,7 +142,7 @@ ob_start();
|
||||
<label>Category</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-layer-group"></i></span>
|
||||
<select class="form-select select2" name="category_id">
|
||||
<select class="form-select select2" name="category_id" data-template-field="category">
|
||||
<option value="0">- Not categorized -</option>
|
||||
<?= categoryOptionsHtml('Ticket', $category_id) ?>
|
||||
</select>
|
||||
@@ -155,7 +156,7 @@ ob_start();
|
||||
<label>Assign to</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-user-check"></i></span>
|
||||
<select class="form-select select2" name="assigned_to">
|
||||
<select class="form-select select2" name="assigned_to" data-template-field="assigned">
|
||||
<option value="0">- Unassigned -</option>
|
||||
<?php
|
||||
$sql_agents = mysqli_query($mysqli, "SELECT user_id, user_name, user_avatar FROM users
|
||||
@@ -170,7 +171,7 @@ ob_start();
|
||||
|
||||
<?php if ($config_module_enable_accounting && lookupUserPermission('module_sales') >= 2) { ?>
|
||||
<div class="form-check form-switch">
|
||||
<input type="checkbox" class="form-check-input" id="recurringBillable<?= $recurring_ticket_id ?>" name="billable" <?= $billable ? 'checked' : '' ?> value="1">
|
||||
<input type="checkbox" class="form-check-input" id="recurringBillable<?= $recurring_ticket_id ?>" name="billable" <?= $billable ? 'checked' : '' ?> value="1" data-template-field="billable">
|
||||
<label class="form-check-label" for="recurringBillable<?= $recurring_ticket_id ?>">The tickets it raises are billable</label>
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
@@ -20,6 +20,10 @@ $asset_id = intval($_GET['asset_id'] ?? 0);
|
||||
$location_id = intval($_GET['location_id'] ?? 0);
|
||||
$vendor_id = intval($_GET['vendor_id'] ?? 0);
|
||||
|
||||
// Opened for one template - New ticket from it, on the template's page - it starts filled in
|
||||
$selected_ticket_template_id = intval($_GET['ticket_template_id'] ?? 0);
|
||||
$ticket_template_apply_on_load = $selected_ticket_template_id > 0;
|
||||
|
||||
// A ticket started from somewhere that knows what it is about (a license's
|
||||
// Renewal Ticket) - plain text in, escaped here
|
||||
$prefill_subject = mb_substr(trim((string) ($_GET['subject'] ?? '')), 0, 500);
|
||||
@@ -148,12 +152,12 @@ ob_start();
|
||||
<label>Subject <strong class="text-danger">*</strong></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-tag"></i></span>
|
||||
<input type="text" class="form-control" id="subjectInput" name="subject" placeholder="Subject" maxlength="500" value="<?= escapeHtml($prefill_subject) ?>" required>
|
||||
<input type="text" class="form-control" id="subjectInput" name="subject" placeholder="Subject" maxlength="500" value="<?= escapeHtml($prefill_subject) ?>" data-template-field="subject" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<textarea class="form-control tinymceTicket" id="detailsInput" name="details"><?= escapeHtml($prefill_details_html) ?></textarea>
|
||||
<textarea class="form-control tinymceTicket" id="detailsInput" name="details" data-template-field="details"><?= escapeHtml($prefill_details_html) ?></textarea>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
@@ -163,7 +167,7 @@ ob_start();
|
||||
<label>Priority <strong class="text-danger">*</strong></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-thermometer-half"></i></span>
|
||||
<select class="form-select select2" name="priority" required>
|
||||
<select class="form-select select2" name="priority" data-template-field="priority" required>
|
||||
<option data-color="var(--bs-info)">Low</option>
|
||||
<option selected data-color="var(--bs-warning)">Medium</option>
|
||||
<option data-color="var(--bs-danger)">High</option>
|
||||
@@ -178,7 +182,7 @@ ob_start();
|
||||
<label>Category</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-layer-group"></i></span>
|
||||
<select class="form-select select2" name="category_id">
|
||||
<select class="form-select select2" name="category_id" data-template-field="category">
|
||||
<option value="0">- Not Categorized -</option>
|
||||
<?= categoryOptionsHtml('Ticket') ?>
|
||||
</select>
|
||||
@@ -195,7 +199,7 @@ ob_start();
|
||||
<label>Assign to</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-user-check"></i></span>
|
||||
<select class="form-select select2" name="assigned_to">
|
||||
<select class="form-select select2" name="assigned_to" data-template-field="assigned">
|
||||
<option value="0">- Unassigned -</option>
|
||||
<?php
|
||||
|
||||
@@ -218,20 +222,20 @@ ob_start();
|
||||
<label>Due</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-calendar-check"></i></span>
|
||||
<input type="datetime-local" class="form-control" name="due">
|
||||
<input type="datetime-local" class="form-control" name="due" data-template-field="due">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<?= tagFormFieldHtml('ticket', [], 'Onboarding, Hardware, Escalated') ?>
|
||||
<?= tagFormFieldHtml('ticket', [], 'Onboarding, Hardware, Escalated', 'Tags', 'data-template-field="tags"') ?>
|
||||
</div>
|
||||
|
||||
<?php if ($config_module_enable_accounting) { ?>
|
||||
<div class="mb-3">
|
||||
<div class="form-check form-switch">
|
||||
<input type="checkbox" class="form-check-input" name="billable" <?php if ($config_ticket_default_billable == 1) { echo "checked"; } ?> value="1" id="billable">
|
||||
<input type="checkbox" class="form-check-input" name="billable" <?php if ($config_ticket_default_billable == 1) { echo "checked"; } ?> value="1" id="billable" data-template-field="billable">
|
||||
<label class="form-check-label" for="billable">Mark Billable</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -718,116 +718,87 @@ if (isset($_POST['bulk_add_client_ticket'])) {
|
||||
|
||||
enforceUserPermission('module_support', 2);
|
||||
|
||||
$assigned_to = intval($_POST['bulk_assigned_to']);
|
||||
if ($assigned_to == 0) {
|
||||
$ticket_status = 1;
|
||||
} else {
|
||||
$ticket_status = 2;
|
||||
}
|
||||
$subject = escapeSql($_POST['bulk_subject']);
|
||||
$priority = escapeSql($_POST['bulk_priority']);
|
||||
$category_id = intval($_POST['bulk_category']);
|
||||
$details = mysqli_real_escape_string($mysqli, $_POST['bulk_details']);
|
||||
$project_id = intval($_POST['bulk_project']);
|
||||
$use_primary_contact = intval($_POST['use_primary_contact']);
|
||||
$ticket_template_id = intval($_POST['bulk_ticket_template_id']);
|
||||
$billable = intval($_POST['bulk_billable'] ?? 0);
|
||||
|
||||
// Check to see if adding a ticket by template
|
||||
if($ticket_template_id) {
|
||||
$sql = mysqli_query($mysqli, "SELECT ticket_template_details, ticket_template_subject FROM ticket_templates WHERE ticket_template_id = $ticket_template_id");
|
||||
$row = mysqli_fetch_assoc($sql);
|
||||
|
||||
// Override Template Subject
|
||||
if(empty($subject)) {
|
||||
$subject = escapeSql($row['ticket_template_subject']);
|
||||
}
|
||||
$details = mysqli_escape_string($mysqli, $row['ticket_template_details']);
|
||||
|
||||
// Get Associated Tasks from the ticket template
|
||||
$sql_task_templates = mysqli_query($mysqli, "SELECT task_template_name, task_template_order FROM task_templates WHERE task_template_ticket_template_id = $ticket_template_id");
|
||||
|
||||
// Every client picked, checked before any ticket is made
|
||||
$client_ids = array_values(array_unique(array_filter(array_map('intval', (array) ($_POST['client_ids'] ?? [])))));
|
||||
foreach ($client_ids as $client_id) {
|
||||
enforceClientAccess($client_id);
|
||||
}
|
||||
|
||||
// Create ticket for each selected asset
|
||||
if (isset($_POST['client_ids'])) {
|
||||
$assigned_to = ticketAgentId($_POST['bulk_assigned_to'] ?? 0);
|
||||
$ticket_status = $assigned_to ? TICKET_STATUS_OPEN : TICKET_STATUS_NEW;
|
||||
$subject_input = trim((string) ($_POST['bulk_subject'] ?? ''));
|
||||
$priority = escapeSql(ticketPriority($_POST['bulk_priority'] ?? ''));
|
||||
$category_id = ticketCategoryId($_POST['bulk_category'] ?? 0);
|
||||
$details = mysqli_real_escape_string($mysqli, (string) ($_POST['bulk_details'] ?? ''));
|
||||
$project_id = intval($_POST['bulk_project'] ?? 0);
|
||||
$use_primary_contact = !empty($_POST['use_primary_contact']);
|
||||
$ticket_template_id = intval($_POST['bulk_ticket_template_id'] ?? 0);
|
||||
$billable = intval($_POST['bulk_billable'] ?? 0) ? 1 : 0;
|
||||
$ticket_prefix = escapeSql($config_ticket_prefix);
|
||||
|
||||
// Check every selected client before creating any tickets
|
||||
foreach ($_POST['client_ids'] as $client_id) {
|
||||
enforceClientAccess(intval($client_id));
|
||||
// A template fills in what was left empty
|
||||
$template = ticketTemplateRow($ticket_template_id);
|
||||
if ($template) {
|
||||
$subject_input = $subject_input !== '' ? $subject_input : (string) $template['ticket_template_subject'];
|
||||
$details = trim((string) ($_POST['bulk_details'] ?? '')) !== '' ? $details : mysqli_real_escape_string($mysqli, (string) $template['ticket_template_details']);
|
||||
}
|
||||
if ($subject_input === '') {
|
||||
flashAlert('The tickets need a subject.', 'error');
|
||||
redirect();
|
||||
}
|
||||
$subject = escapeSql(mb_substr($subject_input, 0, 500));
|
||||
|
||||
$ticket_ids = [];
|
||||
foreach ($client_ids as $client_id) {
|
||||
if (!getFieldById('clients', $client_id, 'client_name')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get a Asset Count
|
||||
$client_count = count($_POST['client_ids']);
|
||||
// Each client's primary contact, when asked; a shared project only when it is internal
|
||||
$contact_id = 0;
|
||||
if ($use_primary_contact) {
|
||||
$primary = mysqli_fetch_row(mysqli_query($mysqli, "SELECT contact_id FROM contacts WHERE contact_client_id = $client_id AND contact_primary = 1 AND contact_archived_at IS NULL LIMIT 1"));
|
||||
$contact_id = $primary ? intval($primary[0]) : 0;
|
||||
}
|
||||
$ticket_project_id = ticketClientOwnedId('project', $project_id, $client_id);
|
||||
|
||||
foreach ($_POST['client_ids'] as $client_id) {
|
||||
$client_id = intval($client_id);
|
||||
// Atomically increment and get the new ticket number
|
||||
mysqli_query($mysqli, "UPDATE settings SET config_ticket_next_number = LAST_INSERT_ID(config_ticket_next_number), config_ticket_next_number = config_ticket_next_number + 1 WHERE company_id = 1");
|
||||
$ticket_number = mysqli_insert_id($mysqli);
|
||||
$url_key = randomString(32);
|
||||
|
||||
$sql = mysqli_query($mysqli, "SELECT client_name FROM clients WHERE client_id = $client_id");
|
||||
$row = mysqli_fetch_assoc($sql);
|
||||
mysqli_query($mysqli, "INSERT INTO tickets SET ticket_prefix = '$ticket_prefix', ticket_number = $ticket_number, ticket_source = 'Agent', ticket_category = $category_id,
|
||||
ticket_subject = '$subject', ticket_details = '$details', ticket_priority = '$priority', ticket_billable = $billable, ticket_status = $ticket_status,
|
||||
ticket_created_by = $session_user_id, ticket_assigned_to = $assigned_to, ticket_contact_id = $contact_id, ticket_url_key = '$url_key',
|
||||
ticket_client_id = $client_id, ticket_invoice_id = 0, ticket_project_id = $ticket_project_id");
|
||||
$ticket_id = mysqli_insert_id($mysqli);
|
||||
applyTicketSla($ticket_id);
|
||||
|
||||
$client_name = escapeSql($row['client_name']);
|
||||
|
||||
// Atomically increment and get the new ticket number
|
||||
mysqli_query($mysqli, "
|
||||
UPDATE settings
|
||||
SET
|
||||
config_ticket_next_number = LAST_INSERT_ID(config_ticket_next_number),
|
||||
config_ticket_next_number = config_ticket_next_number + 1
|
||||
WHERE company_id = 1
|
||||
");
|
||||
|
||||
$ticket_number = mysqli_insert_id($mysqli);
|
||||
|
||||
// Sanitize Config Vars from get_settings.php and Session Vars from check_login.php
|
||||
$config_ticket_prefix = escapeSql($config_ticket_prefix);
|
||||
$config_ticket_from_name = escapeSql($config_ticket_from_name);
|
||||
$config_ticket_from_email = escapeSql($config_ticket_from_email);
|
||||
$config_base_url = escapeSql($config_base_url);
|
||||
|
||||
//Generate a unique URL key for clients to access
|
||||
$url_key = randomString(32);
|
||||
|
||||
// Only a project of this client's, or an internal one
|
||||
$ticket_project_id = ticketClientOwnedId('project', $project_id, $client_id);
|
||||
|
||||
mysqli_query($mysqli, "INSERT INTO tickets SET ticket_prefix = '$config_ticket_prefix', ticket_number = $ticket_number, ticket_category = $category_id, ticket_subject = '$subject', ticket_details = '$details', ticket_priority = '$priority', ticket_billable = $billable, ticket_status = $ticket_status, ticket_created_by = $session_user_id, ticket_assigned_to = $assigned_to, ticket_url_key = '$url_key', ticket_client_id = $client_id, ticket_project_id = $ticket_project_id");
|
||||
|
||||
$ticket_id = mysqli_insert_id($mysqli);
|
||||
applyTicketSla($ticket_id);
|
||||
|
||||
// Add Tasks
|
||||
if (!empty($_POST['tasks'])) {
|
||||
foreach ($_POST['tasks'] as $task) {
|
||||
$task_name = escapeSql($task);
|
||||
// Check that task_name is not-empty (For some reason the !empty on the array doesnt work here like in watchers)
|
||||
if (!empty($task_name)) {
|
||||
mysqli_query($mysqli,"INSERT INTO tasks SET task_name = '$task_name', task_ticket_id = $ticket_id");
|
||||
}
|
||||
}
|
||||
// Tasks typed in, then the template's - its tags and due time too, and [Client] filled in
|
||||
foreach ((array) ($_POST['tasks'] ?? []) as $task) {
|
||||
$task_name = escapeSql(mb_substr(trim((string) $task), 0, 255));
|
||||
if ($task_name !== '') {
|
||||
mysqli_query($mysqli, "INSERT INTO tasks SET task_name = '$task_name', task_ticket_id = $ticket_id");
|
||||
}
|
||||
|
||||
// Add Tasks from Template if Template was selected
|
||||
if($ticket_template_id) {
|
||||
if (mysqli_num_rows($sql_task_templates) > 0) {
|
||||
while ($row = mysqli_fetch_assoc($sql_task_templates)) {
|
||||
$task_order = intval($row['task_template_order']);
|
||||
$task_name = escapeSql($row['task_template_name']);
|
||||
|
||||
mysqli_query($mysqli,"INSERT INTO tasks SET task_name = '$task_name', task_order = $task_order, task_ticket_id = $ticket_id");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Custom action/notif handler
|
||||
triggerCustomAction('ticket_create', $ticket_id);
|
||||
}
|
||||
if ($template) {
|
||||
ticketTemplateFinish($ticket_id, $ticket_template_id, true, true);
|
||||
}
|
||||
|
||||
logAudit("Ticket", "Bulk Create", "$session_name created $client_count tickets for $client_name");
|
||||
|
||||
flashAlert("<strong>$client_count</strong> tickets created for selected clients");
|
||||
|
||||
logAudit("Ticket", "Create", "$session_name created ticket $ticket_prefix$ticket_number - $subject", $client_id, $ticket_id);
|
||||
triggerCustomAction('ticket_create', $ticket_id);
|
||||
$ticket_ids[] = $ticket_id;
|
||||
}
|
||||
$count = count($ticket_ids);
|
||||
|
||||
// The agent they went to hears once, about all of them
|
||||
if ($ticket_ids && $assigned_to) {
|
||||
ticketMailAssigned($assigned_to, $ticket_ids, stripslashes($session_name));
|
||||
}
|
||||
|
||||
logAudit("Ticket", "Bulk Create", "$session_name created $count ticket(s) for the clients picked");
|
||||
|
||||
flashAlert("Created <strong>$count</strong> ticket" . ($count === 1 ? '' : 's') . " for the clients picked");
|
||||
|
||||
redirect();
|
||||
|
||||
|
||||
@@ -68,27 +68,6 @@ if (isset($_POST['edit_ticket_task'])) {
|
||||
|
||||
}
|
||||
|
||||
if (isset($_POST['edit_ticket_template_task'])) {
|
||||
|
||||
validateCSRFToken();
|
||||
|
||||
enforceUserPermission('module_support', 2);
|
||||
|
||||
$task_template_id = intval($_POST['task_template_id']);
|
||||
$task_name = escapeSql($_POST['name']);
|
||||
$task_order = intval($_POST['order']);
|
||||
$task_completion_estimate = intval($_POST['completion_estimate']);
|
||||
|
||||
mysqli_query($mysqli, "UPDATE task_templates SET task_template_name = '$task_name', task_template_order = $task_order, task_template_completion_estimate = $task_completion_estimate WHERE task_template_id = $task_template_id");
|
||||
|
||||
logAudit("Task", "Edit", "$session_name edited task $task_name", 0, $task_template_id);
|
||||
|
||||
flashAlert("Task <strong>" . stripslashes($task_name) . "</strong> saved");
|
||||
|
||||
redirect();
|
||||
|
||||
}
|
||||
|
||||
if (isset($_GET['delete_task'])) {
|
||||
|
||||
validateCSRFToken();
|
||||
|
||||
@@ -138,8 +138,6 @@ if (isset($_POST['add_ticket'])) {
|
||||
foreach (parseSubmittedTasks() as $task) {
|
||||
mysqli_query($mysqli, "INSERT INTO tasks SET task_name = '{$task['name']}', task_order = {$task['order']}, task_completion_estimate = {$task['estimate']}, task_ticket_id = $ticket_id");
|
||||
}
|
||||
} else {
|
||||
addTasksFromTicketTemplate($ticket_id, $ticket_template_id);
|
||||
}
|
||||
|
||||
// Store any attached files against the ticket itself
|
||||
@@ -149,6 +147,12 @@ if (isset($_POST['add_ticket'])) {
|
||||
ticketSetOtherAssets($ticket_id, $other_asset_ids);
|
||||
tagSetForRecord('ticket', $ticket_id, (array) ($_POST['tags'] ?? []));
|
||||
|
||||
// Raised from a template - kept for the template's use, and its [Client] and the like filled in.
|
||||
// The form had the template's fields in it already, so nothing more is added here.
|
||||
if ($ticket_template_id) {
|
||||
ticketTemplateFinish($ticket_id, $ticket_template_id, false, !isset($_POST['tasks_submitted']));
|
||||
}
|
||||
|
||||
// Raised about a service - from the service page's "Report a problem"
|
||||
foreach (array_unique(array_map('intval', (array) ($_POST['service_ids'] ?? []))) as $service_id) {
|
||||
$service = serviceRow($service_id);
|
||||
@@ -900,15 +904,13 @@ if (isset($_POST['bulk_add_asset_ticket'])) {
|
||||
$ticket_prefix = escapeSql($config_ticket_prefix);
|
||||
|
||||
// A template fills in what was left empty
|
||||
if ($ticket_template_id) {
|
||||
$template = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT ticket_template_details, ticket_template_subject FROM ticket_templates WHERE ticket_template_id = $ticket_template_id"));
|
||||
if ($template) {
|
||||
$subject_input = $subject_input !== '' ? $subject_input : (string) $template['ticket_template_subject'];
|
||||
$details = $details !== '' ? $details : mysqli_real_escape_string($mysqli, (string) $template['ticket_template_details']);
|
||||
}
|
||||
$template = ticketTemplateRow($ticket_template_id);
|
||||
if ($template) {
|
||||
$subject_input = $subject_input !== '' ? $subject_input : (string) $template['ticket_template_subject'];
|
||||
$details = $details !== '' ? $details : mysqli_real_escape_string($mysqli, (string) $template['ticket_template_details']);
|
||||
}
|
||||
|
||||
$count = 0;
|
||||
$ticket_ids = [];
|
||||
foreach (array_unique(array_map('intval', (array) ($_POST['asset_ids'] ?? []))) as $asset_id) {
|
||||
$asset = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT asset_client_id, asset_name, asset_contact_id, asset_location_id FROM assets WHERE asset_id = $asset_id"));
|
||||
if (!$asset) {
|
||||
@@ -921,7 +923,8 @@ if (isset($_POST['bulk_add_asset_ticket'])) {
|
||||
$contact_id = ticketClientOwnedId('contact', $asset['asset_contact_id'], $client_id);
|
||||
$location_id = ticketClientOwnedId('location', $asset['asset_location_id'], $client_id);
|
||||
$project_id = ticketClientOwnedId('project', $project_input, $client_id);
|
||||
$subject = escapeSql(mb_substr($asset['asset_name'] . ' - ' . $subject_input, 0, 500));
|
||||
// The asset's name goes in front - unless the subject says where, with [Asset]
|
||||
$subject = escapeSql(mb_substr(stripos($subject_input, '[asset]') !== false ? $subject_input : $asset['asset_name'] . ' - ' . $subject_input, 0, 500));
|
||||
|
||||
mysqli_query($mysqli, "UPDATE settings SET config_ticket_next_number = LAST_INSERT_ID(config_ticket_next_number), config_ticket_next_number = config_ticket_next_number + 1 WHERE company_id = 1");
|
||||
$ticket_number = mysqli_insert_id($mysqli);
|
||||
@@ -934,18 +937,26 @@ if (isset($_POST['bulk_add_asset_ticket'])) {
|
||||
$ticket_id = mysqli_insert_id($mysqli);
|
||||
applyTicketSla($ticket_id);
|
||||
|
||||
// Tasks from the modal, then the template's
|
||||
// Tasks from the modal, then the template's - its tags and due time too, and [Asset] filled in
|
||||
foreach ((array) ($_POST['tasks'] ?? []) as $task) {
|
||||
$task_name = escapeSql(mb_substr(trim((string) $task), 0, 255));
|
||||
if ($task_name !== '') {
|
||||
mysqli_query($mysqli, "INSERT INTO tasks SET task_name = '$task_name', task_ticket_id = $ticket_id");
|
||||
}
|
||||
}
|
||||
addTasksFromTicketTemplate($ticket_id, $ticket_template_id);
|
||||
if ($template) {
|
||||
ticketTemplateFinish($ticket_id, $ticket_template_id, true, true);
|
||||
}
|
||||
|
||||
logAudit("Ticket", "Create", "$session_name created ticket $ticket_prefix$ticket_number - $subject", $client_id, $ticket_id);
|
||||
triggerCustomAction('ticket_create', $ticket_id);
|
||||
$count++;
|
||||
$ticket_ids[] = $ticket_id;
|
||||
}
|
||||
$count = count($ticket_ids);
|
||||
|
||||
// The agent they went to hears once, about all of them
|
||||
if ($ticket_ids && $assigned_to) {
|
||||
ticketMailAssigned($assigned_to, $ticket_ids, stripslashes($session_name));
|
||||
}
|
||||
|
||||
logAudit("Ticket", "Bulk Create", "$session_name created $count ticket(s) for the assets picked");
|
||||
|
||||
@@ -536,15 +536,16 @@ $export_extra = [
|
||||
?>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($_GET['open'])) { ?>
|
||||
<?php if (!empty($_GET['open']) || !empty($_GET['new_from_template'])) { ?>
|
||||
<script>
|
||||
// Opened from somewhere that wants one ticket's quick look
|
||||
// Opened from somewhere that wants one ticket's quick look - or a new
|
||||
// ticket from a template (New ticket from it, Admin > Ticket Templates)
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var open = document.createElement('a');
|
||||
open.className = 'ajax-modal';
|
||||
open.href = '#';
|
||||
open.dataset.modalSize = 'lg';
|
||||
open.dataset.modalUrl = 'modals/ticket/ticket.php?id=<?= intval($_GET['open']) ?>';
|
||||
open.dataset.modalUrl = <?= json_encode(!empty($_GET['open']) ? 'modals/ticket/ticket.php?id=' . intval($_GET['open']) : 'modals/ticket/ticket_add.php?ticket_template_id=' . intval($_GET['new_from_template'])) ?>;
|
||||
document.body.appendChild(open);
|
||||
open.click();
|
||||
open.remove();
|
||||
|
||||
26
db.sql
26
db.sql
@@ -3363,6 +3363,23 @@ CREATE TABLE `ticket_tags` (
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Table structure for table `ticket_template_tags`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `ticket_template_tags`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `ticket_template_tags` (
|
||||
`ticket_template_id` int(11) NOT NULL,
|
||||
`tag_id` int(11) NOT NULL,
|
||||
PRIMARY KEY (`ticket_template_id`,`tag_id`),
|
||||
KEY `tag_id` (`tag_id`),
|
||||
CONSTRAINT `ticket_template_tags_ibfk_1` FOREIGN KEY (`ticket_template_id`) REFERENCES `ticket_templates` (`ticket_template_id`) ON DELETE CASCADE,
|
||||
CONSTRAINT `ticket_template_tags_ibfk_2` FOREIGN KEY (`tag_id`) REFERENCES `tags` (`tag_id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Table structure for table `ticket_templates`
|
||||
--
|
||||
@@ -3376,6 +3393,11 @@ CREATE TABLE `ticket_templates` (
|
||||
`ticket_template_description` text DEFAULT NULL,
|
||||
`ticket_template_subject` varchar(500) DEFAULT NULL,
|
||||
`ticket_template_details` longtext DEFAULT NULL,
|
||||
`ticket_template_category_id` int(11) NOT NULL DEFAULT 0,
|
||||
`ticket_template_priority` varchar(20) DEFAULT NULL,
|
||||
`ticket_template_billable` tinyint(1) DEFAULT NULL,
|
||||
`ticket_template_assigned_to` int(11) NOT NULL DEFAULT 0,
|
||||
`ticket_template_due_hours` int(11) NOT NULL DEFAULT 0,
|
||||
`ticket_template_created_at` datetime NOT NULL DEFAULT current_timestamp(),
|
||||
`ticket_template_updated_at` datetime DEFAULT NULL ON UPDATE current_timestamp(),
|
||||
`ticket_template_archived_at` datetime DEFAULT NULL,
|
||||
@@ -3465,12 +3487,14 @@ CREATE TABLE `tickets` (
|
||||
`ticket_invoice_id` int(11) NOT NULL DEFAULT 0,
|
||||
`ticket_project_id` int(11) NOT NULL DEFAULT 0,
|
||||
`ticket_recurring_ticket_id` int(11) DEFAULT 0,
|
||||
`ticket_template_id` int(11) NOT NULL DEFAULT 0,
|
||||
`ticket_order` int(11) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`ticket_id`),
|
||||
KEY `ticket_response_due_at` (`ticket_response_due_at`),
|
||||
KEY `ticket_resolution_due_at` (`ticket_resolution_due_at`),
|
||||
KEY `ticket_client_id` (`ticket_client_id`,`ticket_archived_at`),
|
||||
KEY `ticket_project_id` (`ticket_project_id`)
|
||||
KEY `ticket_project_id` (`ticket_project_id`),
|
||||
KEY `ticket_template_id` (`ticket_template_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ require_once __DIR__ . '/functions/tags.php';
|
||||
require_once __DIR__ . '/functions/categories.php';
|
||||
require_once __DIR__ . '/functions/ticket_fields.php';
|
||||
require_once __DIR__ . '/functions/ticket.php';
|
||||
require_once __DIR__ . '/functions/ticket_template.php';
|
||||
require_once __DIR__ . '/functions/project.php';
|
||||
require_once __DIR__ . '/functions/service.php';
|
||||
require_once __DIR__ . '/functions/client_export.php';
|
||||
|
||||
@@ -487,11 +487,12 @@ function projectContactId($contact_id, $client_id) {
|
||||
|
||||
/**
|
||||
* Raise the tickets of a project template on a project, in the template's
|
||||
* order, each with the template's tasks. They go to the project's client
|
||||
* contact, and to its manager when $assign_to is given (an agent id).
|
||||
* The client is not emailed - a project's tickets are planned work, not a
|
||||
* request. [Client] and the like in a subject are filled in
|
||||
* (projectTemplateSubject()). How many tickets were raised.
|
||||
* order, each set up as its ticket template says (category, priority,
|
||||
* billing, agent, due time, tags) with the template's tasks. They go to the
|
||||
* project's client contact, and to its manager when $assign_to is given (an
|
||||
* agent id) - else to the template's agent. The client is not emailed - a
|
||||
* project's tickets are planned work, not a request. [Client], [Project] and
|
||||
* the like are filled in (ticketTemplateFinish()). How many tickets were raised.
|
||||
*/
|
||||
function projectApplyTemplate($project, $project_template_id, $assign_to = 0) {
|
||||
global $mysqli, $session_user_id, $session_name, $config_ticket_prefix, $config_ticket_default_billable;
|
||||
@@ -500,31 +501,35 @@ function projectApplyTemplate($project, $project_template_id, $assign_to = 0) {
|
||||
$client_id = intval($project['project_client_id']);
|
||||
$contact_id = projectContactId($project['project_contact_id'] ?? 0, $client_id);
|
||||
$assign_to = ticketAgentId($assign_to);
|
||||
$status = $assign_to ? TICKET_STATUS_OPEN : TICKET_STATUS_NEW;
|
||||
$billable = intval($config_ticket_default_billable) === 1 ? 1 : 0;
|
||||
$prefix = escapeSql($config_ticket_prefix);
|
||||
$template_name = escapeSql((string) getFieldById('project_templates', $project_template_id, 'project_template_name'));
|
||||
|
||||
$sql = mysqli_query($mysqli, "SELECT ticket_templates.ticket_template_id, ticket_template_subject, ticket_template_name, ticket_template_details
|
||||
$sql = mysqli_query($mysqli, "SELECT project_template_ticket_templates.ticket_template_id
|
||||
FROM project_template_ticket_templates
|
||||
JOIN ticket_templates ON ticket_templates.ticket_template_id = project_template_ticket_templates.ticket_template_id
|
||||
WHERE project_template_ticket_templates.project_template_id = $project_template_id
|
||||
ORDER BY ticket_template_order ASC, ticket_template_name ASC");
|
||||
$raised = 0;
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
while ($link = mysqli_fetch_row($sql)) {
|
||||
$row = ticketTemplateRow($link[0]);
|
||||
$subject = trim((string) $row['ticket_template_subject']) !== '' ? $row['ticket_template_subject'] : $row['ticket_template_name'];
|
||||
$subject = escapeSql(mb_substr(projectTemplateSubject($subject, $project), 0, 500));
|
||||
$subject = escapeSql(mb_substr($subject, 0, 500));
|
||||
$details = mysqli_real_escape_string($mysqli, (string) $row['ticket_template_details']);
|
||||
$priority = escapeSql(ticketPriority($row['ticket_template_priority'] ?? ''));
|
||||
$category_id = ticketCategoryId($row['ticket_template_category_id']);
|
||||
$billable = $row['ticket_template_billable'] !== null ? intval($row['ticket_template_billable']) : (intval($config_ticket_default_billable) === 1 ? 1 : 0);
|
||||
$agent = $assign_to ?: ticketAgentId($row['ticket_template_assigned_to']);
|
||||
$status = $agent ? TICKET_STATUS_OPEN : TICKET_STATUS_NEW;
|
||||
mysqli_query($mysqli, "UPDATE settings SET config_ticket_next_number = LAST_INSERT_ID(config_ticket_next_number), config_ticket_next_number = config_ticket_next_number + 1 WHERE company_id = 1");
|
||||
$ticket_number = intval(mysqli_insert_id($mysqli));
|
||||
$url_key = randomString(32);
|
||||
mysqli_query($mysqli, "INSERT INTO tickets SET ticket_prefix = '$prefix', ticket_number = $ticket_number, ticket_source = 'Project',
|
||||
ticket_subject = '$subject', ticket_details = '$details', ticket_priority = 'Medium', ticket_billable = $billable, ticket_status = $status,
|
||||
ticket_created_by = " . intval($session_user_id) . ", ticket_assigned_to = $assign_to, ticket_contact_id = $contact_id, ticket_url_key = '$url_key',
|
||||
mysqli_query($mysqli, "INSERT INTO tickets SET ticket_prefix = '$prefix', ticket_number = $ticket_number, ticket_source = 'Project', ticket_category = $category_id,
|
||||
ticket_subject = '$subject', ticket_details = '$details', ticket_priority = '$priority', ticket_billable = $billable, ticket_status = $status,
|
||||
ticket_created_by = " . intval($session_user_id) . ", ticket_assigned_to = $agent, ticket_contact_id = $contact_id, ticket_url_key = '$url_key',
|
||||
ticket_client_id = $client_id, ticket_invoice_id = 0, ticket_project_id = $project_id");
|
||||
$ticket_id = mysqli_insert_id($mysqli);
|
||||
applyTicketSla($ticket_id);
|
||||
addTasksFromTicketTemplate($ticket_id, $row['ticket_template_id']);
|
||||
ticketTemplateFinish($ticket_id, $row['ticket_template_id'], true, true);
|
||||
logTicketHistory($ticket_id, "$session_name raised the ticket from project template $template_name");
|
||||
triggerCustomAction('ticket_create', $ticket_id);
|
||||
$raised++;
|
||||
@@ -532,27 +537,6 @@ function projectApplyTemplate($project, $project_template_id, $assign_to = 0) {
|
||||
return $raised;
|
||||
}
|
||||
|
||||
/**
|
||||
* A template ticket's subject with what is known filled in: [Client], [Project],
|
||||
* [Month], [Quarter] and [Year]. Anything else in brackets ([User], [Server
|
||||
* Name]) is the agent's to fill in, and stays.
|
||||
*/
|
||||
function projectTemplateSubject($subject, $project) {
|
||||
$month = intval(date('n'));
|
||||
$known = [
|
||||
'project' => (string) $project['project_name'],
|
||||
'month' => date('F Y'),
|
||||
'quarter' => 'Q' . intval(ceil($month / 3)) . ' ' . date('Y'),
|
||||
'year' => date('Y'),
|
||||
];
|
||||
if (intval($project['project_client_id']) && !empty($project['client_name'])) {
|
||||
$known['client'] = (string) $project['client_name'];
|
||||
}
|
||||
return preg_replace_callback('/\[(client|project|month|quarter|year)\]/i', function ($match) use ($known) {
|
||||
return $known[strtolower($match[1])] ?? $match[0];
|
||||
}, (string) $subject);
|
||||
}
|
||||
|
||||
/**
|
||||
* Put a ticket on a project, when it may go there: the agent may reach it,
|
||||
* and it is the project's client's (an internal project takes any). '' when
|
||||
|
||||
@@ -248,11 +248,11 @@ function tagSearchSql($entity, $like, $column = null) {
|
||||
* an admin, typing a new one in. The tags_form marker tells the save the
|
||||
* field was there, so a form without it leaves the tags alone.
|
||||
*/
|
||||
function tagFormFieldHtml($entity, array $selected, $placeholder = '', $label = 'Tags') {
|
||||
function tagFormFieldHtml($entity, array $selected, $placeholder = '', $label = 'Tags', $attributes = '') {
|
||||
global $session_is_admin;
|
||||
return '<label class="form-label">' . $label . '</label><input type="hidden" name="tags_form" value="1">'
|
||||
. '<div class="input-group"><span class="input-group-text"><i class="fa fa-fw fa-tags"></i></span>'
|
||||
. '<select class="form-select select2" name="tags[]" data-placeholder="' . escapeHtml($placeholder) . '" multiple' . (!empty($session_is_admin) ? ' data-tags="true"' : '') . '>'
|
||||
. '<select class="form-select select2" name="tags[]" data-placeholder="' . escapeHtml($placeholder) . '" multiple' . (!empty($session_is_admin) ? ' data-tags="true"' : '') . ($attributes !== '' ? ' ' . $attributes : '') . '>'
|
||||
. tagOptionsHtml($entity, $selected) . '</select></div>';
|
||||
}
|
||||
|
||||
@@ -747,7 +747,7 @@ function tagOptionHtml(array $row, array $selected = []) {
|
||||
* stays when the record already had it. Returns the tag ids it now has.
|
||||
*/
|
||||
function tagSetForRecord($entity, $record_id, array $values) {
|
||||
global $mysqli, $session_is_admin, $session_name;
|
||||
global $mysqli;
|
||||
$config = tagEntity($entity);
|
||||
$record_id = intval($record_id);
|
||||
if (!$config || !$record_id) {
|
||||
@@ -756,14 +756,37 @@ function tagSetForRecord($entity, $record_id, array $values) {
|
||||
$link = $config['link_table'];
|
||||
$link_record = $config['link_record'];
|
||||
$link_tag = $config['link_tag'];
|
||||
$type = $config['type'];
|
||||
|
||||
$had = [];
|
||||
$sql = mysqli_query($mysqli, "SELECT $link_tag FROM $link WHERE $link_record = $record_id");
|
||||
while ($row = mysqli_fetch_row($sql)) {
|
||||
$had[] = intval($row[0]);
|
||||
}
|
||||
$keep = tagKeepIds($entity, tagIdsFromValues($entity, $values), $had);
|
||||
|
||||
$remove = array_diff($had, $keep);
|
||||
if ($remove) {
|
||||
mysqli_query($mysqli, "DELETE FROM $link WHERE $link_record = $record_id AND $link_tag IN (" . implode(',', $remove) . ")");
|
||||
}
|
||||
foreach (array_diff($keep, $had) as $tag_id) {
|
||||
mysqli_query($mysqli, "INSERT IGNORE INTO $link SET $link_record = $record_id, $link_tag = $tag_id");
|
||||
}
|
||||
sort($keep);
|
||||
return $keep;
|
||||
}
|
||||
|
||||
/*
|
||||
* A Tags field's values as tag ids - ids, or the names of new tags typed in
|
||||
* by an admin, made with the next colour (or brought back when archived).
|
||||
* Not yet checked to be this kind's - tagKeepIds() does that.
|
||||
*/
|
||||
function tagIdsFromValues($entity, array $values) {
|
||||
global $mysqli, $session_is_admin, $session_name;
|
||||
$config = tagEntity($entity);
|
||||
if (!$config) {
|
||||
return [];
|
||||
}
|
||||
$type = $config['type'];
|
||||
$ids = [];
|
||||
foreach ($values as $value) {
|
||||
$value = trim((string) $value);
|
||||
@@ -790,26 +813,22 @@ function tagSetForRecord($entity, $record_id, array $values) {
|
||||
}
|
||||
}
|
||||
}
|
||||
$ids = array_values(array_unique($ids));
|
||||
return array_values(array_unique($ids));
|
||||
}
|
||||
|
||||
// This kind's tags only; archived ones only when already there
|
||||
/* Of $ids, this kind's tags - an archived one only when it is among $had, what the record already has. */
|
||||
function tagKeepIds($entity, array $ids, array $had = []) {
|
||||
global $mysqli;
|
||||
$config = tagEntity($entity);
|
||||
$ids = array_values(array_filter(array_map('intval', $ids)));
|
||||
$keep = [];
|
||||
if ($ids) {
|
||||
$sql = mysqli_query($mysqli, "SELECT tag_id, tag_archived_at FROM tags WHERE tag_type = $type AND tag_id IN (" . implode(',', $ids) . ")");
|
||||
if ($config && $ids) {
|
||||
$sql = mysqli_query($mysqli, "SELECT tag_id, tag_archived_at FROM tags WHERE tag_type = {$config['type']} AND tag_id IN (" . implode(',', $ids) . ")");
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
if (!$row['tag_archived_at'] || in_array(intval($row['tag_id']), $had, true)) {
|
||||
$keep[] = intval($row['tag_id']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$remove = array_diff($had, $keep);
|
||||
if ($remove) {
|
||||
mysqli_query($mysqli, "DELETE FROM $link WHERE $link_record = $record_id AND $link_tag IN (" . implode(',', $remove) . ")");
|
||||
}
|
||||
foreach (array_diff($keep, $had) as $tag_id) {
|
||||
mysqli_query($mysqli, "INSERT IGNORE INTO $link SET $link_record = $record_id, $link_tag = $tag_id");
|
||||
}
|
||||
sort($keep);
|
||||
return $keep;
|
||||
}
|
||||
|
||||
@@ -1436,6 +1436,14 @@ function recurringTicketRaise($recurring_ticket_id, $by = 'Cron') {
|
||||
WHERE recurring_ticket_id = $recurring_ticket_id AND recurring_ticket_assets.asset_id <> $asset_id");
|
||||
addTasksFromRecurringTicket($ticket_id, $recurring_ticket_id);
|
||||
|
||||
// The template it was made from, kept for the template's use, and its [Client], [Month]
|
||||
// and the like filled in - every time it is raised (functions/ticket_template.php)
|
||||
$template_id = intval($row['recurring_ticket_ticket_template_id'] ?? 0);
|
||||
if ($template_id && ticketTemplateRow($template_id)) {
|
||||
mysqli_query($mysqli, "UPDATE tickets SET ticket_template_id = $template_id WHERE ticket_id = $ticket_id");
|
||||
}
|
||||
ticketPlaceholdersFill($ticket_id);
|
||||
|
||||
logAudit("Ticket", "Create", escapeSql($by) . " created recurring $frequency ticket $prefix$ticket_number - $subject", $client_id, $ticket_id);
|
||||
triggerCustomAction('ticket_create', $ticket_id);
|
||||
|
||||
|
||||
375
functions/ticket_template.php
Normal file
375
functions/ticket_template.php
Normal file
@@ -0,0 +1,375 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* Ticket templates - a ticket ready to raise: its subject, details and tasks,
|
||||
* and how it is set up - category, priority, billing, who it goes to, when it
|
||||
* is due, and its tags.
|
||||
*
|
||||
* Admin > Ticket Templates makes them (admin/ticket_templates.php,
|
||||
* admin/ticket_template.php, admin/post/ticket_template.php). They are picked
|
||||
* on New Ticket, the New Tickets windows of the assets and clients lists and
|
||||
* recurring tickets (agent/includes/inc_ticket_template_select.php, which
|
||||
* fills in the form - agent/js/ticket_tasks_modal.js), and a project template
|
||||
* raises its ticket templates as they are (projectApplyTemplate()).
|
||||
*
|
||||
* Words in square brackets - [Client], [Contact], [Asset], [Site], [Agent],
|
||||
* [Project], [Ticket], [Date], [Month], [Quarter] and [Year] - are filled in
|
||||
* from the ticket when it is raised (ticketPlaceholdersFill()). Anything else
|
||||
* in brackets, [Employee Name] say, stays for whoever picks it up.
|
||||
*
|
||||
* A ticket keeps which template it came from (tickets.ticket_template_id), so
|
||||
* a template shows how often it is used and its recent tickets.
|
||||
*/
|
||||
|
||||
/** The placeholders filled in from the ticket - what each becomes. */
|
||||
const TICKET_PLACEHOLDERS = [
|
||||
'Client' => "the client's name",
|
||||
'Contact' => "the contact's name",
|
||||
'Asset' => "the asset's name",
|
||||
'Site' => "the site's name",
|
||||
'Agent' => 'the agent it is assigned to, or who raised it',
|
||||
'Project' => "the project's name",
|
||||
'Ticket' => 'its number - TCK-123',
|
||||
'Date' => "today's date",
|
||||
'Month' => 'the month - October 2026',
|
||||
'Quarter' => 'the quarter - Q4 2026',
|
||||
'Year' => 'the year',
|
||||
];
|
||||
|
||||
/** One template with its category and the agent it goes to, or null. Archived ones too. */
|
||||
function ticketTemplateRow($ticket_template_id) {
|
||||
global $mysqli;
|
||||
$ticket_template_id = intval($ticket_template_id);
|
||||
if (!$ticket_template_id) {
|
||||
return null;
|
||||
}
|
||||
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT ticket_templates.*, category_name, category_color, category_icon, category_type, category_description,
|
||||
user_name AS assignee_name, user_avatar AS assignee_avatar
|
||||
FROM ticket_templates
|
||||
LEFT JOIN categories ON category_id = ticket_template_category_id AND ticket_template_category_id > 0
|
||||
LEFT JOIN users ON user_id = ticket_template_assigned_to AND ticket_template_assigned_to > 0
|
||||
WHERE ticket_template_id = $ticket_template_id LIMIT 1"));
|
||||
return $row ?: null;
|
||||
}
|
||||
|
||||
/** A template's tasks, in order - [['id', 'name', 'estimate'], ...]. */
|
||||
function ticketTemplateTasks($ticket_template_id) {
|
||||
global $mysqli;
|
||||
$tasks = [];
|
||||
$sql = mysqli_query($mysqli, "SELECT task_template_id, task_template_name, task_template_completion_estimate FROM task_templates
|
||||
WHERE task_template_ticket_template_id = " . intval($ticket_template_id) . " ORDER BY task_template_order ASC, task_template_id ASC");
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
$tasks[] = ['id' => intval($row['task_template_id']), 'name' => (string) $row['task_template_name'], 'estimate' => intval($row['task_template_completion_estimate'])];
|
||||
}
|
||||
return $tasks;
|
||||
}
|
||||
|
||||
/** The ticket tags a template gives its tickets - ids, the archived ones too. */
|
||||
function ticketTemplateTagIds($ticket_template_id) {
|
||||
global $mysqli;
|
||||
$ids = [];
|
||||
$sql = mysqli_query($mysqli, "SELECT tag_id FROM ticket_template_tags WHERE ticket_template_id = " . intval($ticket_template_id) . " ORDER BY tag_id");
|
||||
while ($row = mysqli_fetch_row($sql)) {
|
||||
$ids[] = intval($row[0]);
|
||||
}
|
||||
return $ids;
|
||||
}
|
||||
|
||||
/** "4 hours", "1 day", "3 days" - when a template's tickets fall due; '' for not set. */
|
||||
function ticketTemplateDueText($hours) {
|
||||
$hours = intval($hours);
|
||||
if ($hours <= 0) {
|
||||
return '';
|
||||
}
|
||||
if ($hours % 24 === 0) {
|
||||
$days = intdiv($hours, 24);
|
||||
return $days === 1 ? '1 day' : "$days days";
|
||||
}
|
||||
return $hours === 1 ? '1 hour' : "$hours hours";
|
||||
}
|
||||
|
||||
/** Tasks' time in words - "45m", "2h 30m". */
|
||||
function ticketTemplateMinutesText($minutes) {
|
||||
$minutes = max(0, intval($minutes));
|
||||
return formatDuration(sprintf('%02d:%02d:00', intdiv($minutes, 60), $minutes % 60));
|
||||
}
|
||||
|
||||
/** Square-bracket words in some text - [Client], [Employee Name] - each once, as written. */
|
||||
function ticketTemplatePlaceholdersIn($text) {
|
||||
preg_match_all('/\[([A-Za-z][A-Za-z0-9 _\-\/]{0,40})\]/', strip_tags((string) $text), $matches);
|
||||
$found = [];
|
||||
foreach ($matches[1] as $name) {
|
||||
$found[strtolower(trim($name))] = $found[strtolower(trim($name))] ?? trim($name);
|
||||
}
|
||||
return array_values($found);
|
||||
}
|
||||
|
||||
/** When a ticket raised now from a template that is due in $hours is due - 'Y-m-d H:i:s', or null for not set. */
|
||||
function ticketTemplateDueAt($hours) {
|
||||
$hours = intval($hours);
|
||||
return $hours > 0 ? date('Y-m-d H:i:s', time() + $hours * 3600) : null;
|
||||
}
|
||||
|
||||
/*
|
||||
* A template's form, posted (admin/modals/ticket_template/ticket_template_form.php),
|
||||
* checked - its fields raw, or null with $error saying why. The due time is
|
||||
* due_amount in due_unit (hours or days).
|
||||
*/
|
||||
function ticketTemplateFields(array $post, &$error = '') {
|
||||
$name = trim(mb_substr(preg_replace('/\s+/', ' ', strip_tags((string) ($post['name'] ?? ''))), 0, 200));
|
||||
if ($name === '') {
|
||||
$error = 'A template needs a name.';
|
||||
return null;
|
||||
}
|
||||
$priority = (string) ($post['priority'] ?? '');
|
||||
$billable = (string) ($post['billable'] ?? '');
|
||||
$amount = max(0, intval($post['due_amount'] ?? 0));
|
||||
$hours = ($post['due_unit'] ?? 'days') === 'hours' ? $amount : $amount * 24;
|
||||
return [
|
||||
'name' => $name,
|
||||
'description' => trim(mb_substr(strip_tags((string) ($post['description'] ?? '')), 0, 500)),
|
||||
'subject' => trim(mb_substr(strip_tags((string) ($post['subject'] ?? '')), 0, 500)),
|
||||
'details' => (string) ($post['details'] ?? ''),
|
||||
'category_id' => ticketCategoryId($post['category_id'] ?? 0),
|
||||
'priority' => in_array($priority, TICKET_PRIORITIES, true) ? $priority : null,
|
||||
'billable' => $billable === '1' ? 1 : ($billable === '0' ? 0 : null),
|
||||
'assigned_to' => ticketAgentId($post['assigned_to'] ?? 0),
|
||||
'due_hours' => min($hours, 24 * 366),
|
||||
'tags' => (array) ($post['tags'] ?? []),
|
||||
];
|
||||
}
|
||||
|
||||
/** Save a template's fields (ticketTemplateFields()) - a new one when $ticket_template_id is 0. Its id. */
|
||||
function ticketTemplateSave($ticket_template_id, array $f) {
|
||||
global $mysqli;
|
||||
$ticket_template_id = intval($ticket_template_id);
|
||||
$set = "ticket_template_name = '" . escapeSql($f['name']) . "', ticket_template_description = '" . escapeSql($f['description']) . "',
|
||||
ticket_template_subject = '" . escapeSql($f['subject']) . "', ticket_template_details = '" . mysqli_real_escape_string($mysqli, $f['details']) . "',
|
||||
ticket_template_category_id = " . intval($f['category_id']) . ", ticket_template_priority = " . ($f['priority'] === null ? 'NULL' : "'" . escapeSql($f['priority']) . "'") . ",
|
||||
ticket_template_billable = " . ($f['billable'] === null ? 'NULL' : intval($f['billable'])) . ", ticket_template_assigned_to = " . intval($f['assigned_to']) . ",
|
||||
ticket_template_due_hours = " . intval($f['due_hours']);
|
||||
if ($ticket_template_id) {
|
||||
mysqli_query($mysqli, "UPDATE ticket_templates SET $set WHERE ticket_template_id = $ticket_template_id");
|
||||
} else {
|
||||
mysqli_query($mysqli, "INSERT INTO ticket_templates SET $set");
|
||||
$ticket_template_id = intval(mysqli_insert_id($mysqli));
|
||||
}
|
||||
ticketTemplateSetTags($ticket_template_id, $f['tags']);
|
||||
return $ticket_template_id;
|
||||
}
|
||||
|
||||
/* Set a template's tags from a Tags field - ticket tags, as tagSetForRecord() takes them. The ids it has now. */
|
||||
function ticketTemplateSetTags($ticket_template_id, array $values) {
|
||||
global $mysqli;
|
||||
$ticket_template_id = intval($ticket_template_id);
|
||||
$had = ticketTemplateTagIds($ticket_template_id);
|
||||
$keep = tagKeepIds('ticket', tagIdsFromValues('ticket', $values), $had);
|
||||
$remove = array_diff($had, $keep);
|
||||
if ($remove) {
|
||||
mysqli_query($mysqli, "DELETE FROM ticket_template_tags WHERE ticket_template_id = $ticket_template_id AND tag_id IN (" . implode(',', $remove) . ")");
|
||||
}
|
||||
foreach (array_diff($keep, $had) as $tag_id) {
|
||||
mysqli_query($mysqli, "INSERT IGNORE INTO ticket_template_tags SET ticket_template_id = $ticket_template_id, tag_id = " . intval($tag_id));
|
||||
}
|
||||
return $keep;
|
||||
}
|
||||
|
||||
/** A copy of a template - "Copy of" its name, its tasks and tags - not archived. The new one's id, or 0. */
|
||||
function ticketTemplateCopy($ticket_template_id) {
|
||||
global $mysqli;
|
||||
$row = ticketTemplateRow($ticket_template_id);
|
||||
if (!$row) {
|
||||
return 0;
|
||||
}
|
||||
$new_id = ticketTemplateSave(0, [
|
||||
'name' => mb_substr('Copy of ' . $row['ticket_template_name'], 0, 200), 'description' => (string) $row['ticket_template_description'],
|
||||
'subject' => (string) $row['ticket_template_subject'], 'details' => (string) $row['ticket_template_details'],
|
||||
'category_id' => intval($row['ticket_template_category_id']), 'priority' => $row['ticket_template_priority'],
|
||||
'billable' => $row['ticket_template_billable'] === null ? null : intval($row['ticket_template_billable']),
|
||||
'assigned_to' => intval($row['ticket_template_assigned_to']), 'due_hours' => intval($row['ticket_template_due_hours']),
|
||||
'tags' => [],
|
||||
]);
|
||||
foreach (ticketTemplateTagIds($ticket_template_id) as $tag_id) {
|
||||
mysqli_query($mysqli, "INSERT IGNORE INTO ticket_template_tags SET ticket_template_id = $new_id, tag_id = $tag_id");
|
||||
}
|
||||
foreach (ticketTemplateTasks($ticket_template_id) as $order => $task) {
|
||||
mysqli_query($mysqli, "INSERT INTO task_templates SET task_template_name = '" . escapeSql($task['name']) . "', task_template_order = " . ($order + 1) . ",
|
||||
task_template_completion_estimate = " . intval($task['estimate']) . ", task_template_ticket_template_id = $new_id");
|
||||
}
|
||||
return $new_id;
|
||||
}
|
||||
|
||||
/** How templates are used - [id => ['count' => tickets raised, 'open' => still open, 'last' => when the last was raised]]. */
|
||||
function ticketTemplateUsage(array $ticket_template_ids) {
|
||||
global $mysqli;
|
||||
$ids = array_values(array_filter(array_map('intval', $ticket_template_ids)));
|
||||
$usage = [];
|
||||
if (!$ids) {
|
||||
return $usage;
|
||||
}
|
||||
$sql = mysqli_query($mysqli, "SELECT ticket_template_id, COUNT(*) AS used, SUM(ticket_closed_at IS NULL AND ticket_resolved_at IS NULL) AS still_open, MAX(ticket_created_at) AS last_used
|
||||
FROM tickets WHERE ticket_template_id IN (" . implode(',', $ids) . ") AND ticket_archived_at IS NULL GROUP BY ticket_template_id");
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
$usage[intval($row['ticket_template_id'])] = ['count' => intval($row['used']), 'open' => intval($row['still_open']), 'last' => $row['last_used']];
|
||||
}
|
||||
return $usage;
|
||||
}
|
||||
|
||||
/*
|
||||
* How a template sets up its tickets, as little badges - its priority,
|
||||
* category, billing, agent and due time. HTML; '' when it sets none of them.
|
||||
*/
|
||||
function ticketTemplateDefaultsHtml(array $row) {
|
||||
$chips = [];
|
||||
if (!empty($row['ticket_template_priority'])) {
|
||||
$chips[] = ticketPriorityHtml($row['ticket_template_priority'], 'detail');
|
||||
}
|
||||
if (intval($row['ticket_template_category_id']) && !empty($row['category_name'])) {
|
||||
$chips[] = '<span class="badge rounded-pill text-bg-light border fw-normal">' . categoryChip(['category_name' => $row['category_name'], 'category_type' => 'Ticket', 'category_color' => $row['category_color'] ?? '', 'category_icon' => $row['category_icon'] ?? '']) . '</span>';
|
||||
}
|
||||
if ($row['ticket_template_billable'] !== null) {
|
||||
$chips[] = '<span class="badge rounded-pill text-bg-light border fw-normal"><i class="fas fa-fw fa-' . (intval($row['ticket_template_billable']) ? 'dollar-sign text-success' : 'ban text-secondary') . ' me-1"></i>' . (intval($row['ticket_template_billable']) ? 'Billable' : 'Not billable') . '</span>';
|
||||
}
|
||||
if (intval($row['ticket_template_assigned_to']) && !empty($row['assignee_name'])) {
|
||||
$chips[] = '<span class="badge rounded-pill text-bg-light border fw-normal"><i class="fas fa-fw fa-user-check text-secondary me-1"></i>' . escapeHtml($row['assignee_name']) . '</span>';
|
||||
}
|
||||
if (intval($row['ticket_template_due_hours'])) {
|
||||
$chips[] = '<span class="badge rounded-pill text-bg-light border fw-normal"><i class="fas fa-fw fa-calendar-check text-secondary me-1"></i>Due in ' . ticketTemplateDueText($row['ticket_template_due_hours']) . '</span>';
|
||||
}
|
||||
return $chips ? '<span class="d-inline-flex flex-wrap gap-1 align-items-center">' . implode('', $chips) . '</span>' : '';
|
||||
}
|
||||
|
||||
/*
|
||||
* What picking a template puts into a ticket form - the data attributes of
|
||||
* its <option> (agent/includes/inc_ticket_template_select.php), which
|
||||
* agent/js/ticket_tasks_modal.js reads. Due is the local date and time it
|
||||
* would be due if raised now. '' values leave the form's field alone.
|
||||
*/
|
||||
function ticketTemplatePickerData(array $row, array $tasks, array $tag_ids) {
|
||||
$due = ticketTemplateDueAt($row['ticket_template_due_hours']);
|
||||
return [
|
||||
'subject' => (string) $row['ticket_template_subject'],
|
||||
'details' => (string) $row['ticket_template_details'],
|
||||
'tasks' => json_encode(array_map(function ($task) {
|
||||
return ['name' => $task['name'], 'estimate' => $task['estimate']];
|
||||
}, $tasks)),
|
||||
'priority' => (string) ($row['ticket_template_priority'] ?? ''),
|
||||
'category' => intval($row['ticket_template_category_id']) ? (string) intval($row['ticket_template_category_id']) : '',
|
||||
'billable' => $row['ticket_template_billable'] === null ? '' : (string) intval($row['ticket_template_billable']),
|
||||
'assigned' => intval($row['ticket_template_assigned_to']) ? (string) intval($row['ticket_template_assigned_to']) : '',
|
||||
'due' => $due ? date('Y-m-d\TH:i', strtotime($due)) : '',
|
||||
'tags' => $tag_ids ? implode(',', $tag_ids) : '',
|
||||
];
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------- placeholders */
|
||||
|
||||
/** What each placeholder becomes on a ticket - name => value; '' for what the ticket doesn't have. Raw. */
|
||||
function ticketPlaceholderValues($ticket_id) {
|
||||
global $mysqli, $session_name;
|
||||
$ticket_id = intval($ticket_id);
|
||||
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT ticket_prefix, ticket_number, client_name, contact_name, asset_name, location_name, project_name, assignee.user_name AS assignee_name, creator.user_name AS creator_name
|
||||
FROM tickets
|
||||
LEFT JOIN clients ON client_id = ticket_client_id AND ticket_client_id > 0
|
||||
LEFT JOIN contacts ON contact_id = ticket_contact_id AND ticket_contact_id > 0
|
||||
LEFT JOIN assets ON asset_id = ticket_asset_id AND ticket_asset_id > 0
|
||||
LEFT JOIN locations ON location_id = ticket_location_id AND ticket_location_id > 0
|
||||
LEFT JOIN projects ON project_id = ticket_project_id AND ticket_project_id > 0
|
||||
LEFT JOIN users assignee ON assignee.user_id = ticket_assigned_to AND ticket_assigned_to > 0
|
||||
LEFT JOIN users creator ON creator.user_id = ticket_created_by AND ticket_created_by > 0
|
||||
WHERE ticket_id = $ticket_id"));
|
||||
if (!$row) {
|
||||
return [];
|
||||
}
|
||||
$month = intval(date('n'));
|
||||
return [
|
||||
'client' => (string) ($row['client_name'] ?? ''),
|
||||
'contact' => (string) ($row['contact_name'] ?? ''),
|
||||
'asset' => (string) ($row['asset_name'] ?? ''),
|
||||
'site' => (string) ($row['location_name'] ?? ''),
|
||||
'agent' => (string) ($row['assignee_name'] ?: ($row['creator_name'] ?: stripslashes((string) ($session_name ?? '')))),
|
||||
'project' => (string) ($row['project_name'] ?? ''),
|
||||
'ticket' => ticketReference($row),
|
||||
'date' => date('M j, Y'),
|
||||
'month' => date('F Y'),
|
||||
'quarter' => 'Q' . intval(ceil($month / 3)) . ' ' . date('Y'),
|
||||
'year' => date('Y'),
|
||||
];
|
||||
}
|
||||
|
||||
/*
|
||||
* $text with the placeholders filled in from $values (ticketPlaceholderValues()),
|
||||
* whatever their case. One the ticket has nothing for stays as it is. $html:
|
||||
* $text is HTML, so the values go in escaped.
|
||||
*/
|
||||
function ticketPlaceholderFill($text, array $values, $html = false) {
|
||||
$names = implode('|', array_map('strtolower', array_keys(TICKET_PLACEHOLDERS)));
|
||||
return preg_replace_callback('/\[(' . $names . ')\]/i', function ($match) use ($values, $html) {
|
||||
$value = $values[strtolower($match[1])] ?? '';
|
||||
if ($value === '') {
|
||||
return $match[0];
|
||||
}
|
||||
return $html ? escapeHtml($value) : $value;
|
||||
}, (string) $text);
|
||||
}
|
||||
|
||||
/** Fill in the placeholders in a ticket's subject, details and task names. How many of them changed. */
|
||||
function ticketPlaceholdersFill($ticket_id) {
|
||||
global $mysqli;
|
||||
$ticket_id = intval($ticket_id);
|
||||
$values = ticketPlaceholderValues($ticket_id);
|
||||
$row = $values ? mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT ticket_subject, ticket_details FROM tickets WHERE ticket_id = $ticket_id")) : null;
|
||||
if (!$row) {
|
||||
return 0;
|
||||
}
|
||||
$changed = 0;
|
||||
$subject = mb_substr(ticketPlaceholderFill($row['ticket_subject'], $values), 0, 500);
|
||||
$details = ticketPlaceholderFill($row['ticket_details'], $values, true);
|
||||
if ($subject !== $row['ticket_subject'] || $details !== $row['ticket_details']) {
|
||||
mysqli_query($mysqli, "UPDATE tickets SET ticket_subject = '" . escapeSql($subject) . "', ticket_details = '" . mysqli_real_escape_string($mysqli, $details) . "' WHERE ticket_id = $ticket_id");
|
||||
$changed++;
|
||||
}
|
||||
$sql = mysqli_query($mysqli, "SELECT task_id, task_name FROM tasks WHERE task_ticket_id = $ticket_id AND task_name LIKE '%[%]%'");
|
||||
while ($task = mysqli_fetch_assoc($sql)) {
|
||||
$name = mb_substr(ticketPlaceholderFill($task['task_name'], $values), 0, 255);
|
||||
if ($name !== $task['task_name']) {
|
||||
mysqli_query($mysqli, "UPDATE tasks SET task_name = '" . escapeSql($name) . "' WHERE task_id = " . intval($task['task_id']));
|
||||
$changed++;
|
||||
}
|
||||
}
|
||||
return $changed;
|
||||
}
|
||||
|
||||
/*
|
||||
* A ticket was just raised from a template: note which (on the ticket and in
|
||||
* its history) and fill in its placeholders. $defaults also gives it the
|
||||
* template's tags and due time when it has none - for the paths that have no
|
||||
* fields of their own for those (the New Tickets windows, project templates).
|
||||
* $tasks copies the template's tasks too. The template, or null.
|
||||
*/
|
||||
function ticketTemplateFinish($ticket_id, $ticket_template_id, $defaults = false, $tasks = false) {
|
||||
global $mysqli, $session_name;
|
||||
$ticket_id = intval($ticket_id);
|
||||
$template = ticketTemplateRow($ticket_template_id);
|
||||
if (!$ticket_id || !$template) {
|
||||
return null;
|
||||
}
|
||||
$ticket_template_id = intval($template['ticket_template_id']);
|
||||
mysqli_query($mysqli, "UPDATE tickets SET ticket_template_id = $ticket_template_id WHERE ticket_id = $ticket_id");
|
||||
if ($tasks) {
|
||||
addTasksFromTicketTemplate($ticket_id, $ticket_template_id);
|
||||
}
|
||||
if ($defaults) {
|
||||
$tag_ids = ticketTemplateTagIds($ticket_template_id);
|
||||
if ($tag_ids && !tagIdsForRecord('ticket', $ticket_id)) {
|
||||
tagSetForRecord('ticket', $ticket_id, array_map('strval', $tag_ids));
|
||||
}
|
||||
$due = ticketTemplateDueAt($template['ticket_template_due_hours']);
|
||||
if ($due) {
|
||||
mysqli_query($mysqli, "UPDATE tickets SET ticket_due_at = '$due' WHERE ticket_id = $ticket_id AND ticket_due_at IS NULL");
|
||||
}
|
||||
}
|
||||
ticketPlaceholdersFill($ticket_id);
|
||||
logTicketHistory($ticket_id, escapeSql(stripslashes((string) ($session_name ?? 'ITFlow')) . ' raised the ticket from template ' . $template['ticket_template_name']));
|
||||
return $template;
|
||||
}
|
||||
Reference in New Issue
Block a user