Merge pull request #1234 from itflow-org/develop

v25.09.1
This commit is contained in:
Johnny
2025-09-07 11:44:10 -04:00
committed by GitHub
14 changed files with 96 additions and 118 deletions

View File

@@ -2,6 +2,23 @@
This file documents all notable changes made to ITFlow. This file documents all notable changes made to ITFlow.
## [25.09.1]
### Fixes
- **Web Installer**: Resolved issue with broken installer caused by incorrect database schema file name.
- Hide the "Add Credit" button as the feature is not fully implemented yet.
- Corrected long invoice/quote notes that were overlapping with the footer in PDF exports.
- Fixed AI settings not appearing in the Admin Menu when the Billing module was disabled.
- Enabled wrapping of client tags when they are too long.
- Fixed an issue where AI was not functioning correctly.
- Removed extra spacing between the contact name and icon in the Ticket Details contact card.
### Features
- Redesigned **AI Ticket Summary**, now divided into 3 sections: Main Issue, Actions Taken, and Resolution/Next Steps.
- Updated the **AI Ticket Summary** prompt to include ticket status, reply author, source, category, and priority.
---
## [25.09] ## [25.09]
***BACK UP*** before updating. ***BACK UP*** before updating.

View File

@@ -72,6 +72,7 @@
<p>Saved Payments</p> <p>Saved Payments</p>
</a> </a>
</li> </li>
<?php } ?>
<li class="nav-item"> <li class="nav-item">
<a href="ai_provider.php" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'ai_provider.php' ? 'active' : ''); ?>"> <a href="ai_provider.php" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'ai_provider.php' ? 'active' : ''); ?>">
<i class="nav-icon fas fa-robot"></i> <i class="nav-icon fas fa-robot"></i>
@@ -84,7 +85,7 @@
<p>AI Models</p> <p>AI Models</p>
</a> </a>
</li> </li>
<?php } ?>
<?php if ($config_module_enable_ticketing) { ?> <?php if ($config_module_enable_ticketing) { ?>
<li class="nav-item"> <li class="nav-item">
<a href="ticket_status.php" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'ticket_status.php' ? 'active' : ''); ?>"> <a href="ticket_status.php" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'ticket_status.php' ? 'active' : ''); ?>">

View File

@@ -14,8 +14,6 @@
<input type="text" class="form-control" name="name" placeholder="Template name" maxlength="200"> <input type="text" class="form-control" name="name" placeholder="Template name" maxlength="200">
</div> </div>
<?php if ($config_ai_enable == 1) { ?>
<!-- Prompt for AI -->
<div class="form-group"> <div class="form-group">
<label>Enter a prompt for the type of IT documentation you want to generate:</label> <label>Enter a prompt for the type of IT documentation you want to generate:</label>
<div class="input-group mb-3"> <div class="input-group mb-3">
@@ -27,7 +25,6 @@
</div> </div>
</div> </div>
</div> </div>
<?php } ?>
<!-- TinyMCE Content --> <!-- TinyMCE Content -->
<div class="form-group"> <div class="form-group">

View File

@@ -300,7 +300,7 @@ if (isset($_GET['export_quote_pdf'])) {
// Start TCPDF // Start TCPDF
$pdf = new TCPDF('P', 'mm', 'A4', true, 'UTF-8', false); $pdf = new TCPDF('P', 'mm', 'A4', true, 'UTF-8', false);
$pdf->SetMargins(15, 15, 15); $pdf->SetMargins(10, 10, 10);
$pdf->setPrintHeader(false); $pdf->setPrintHeader(false);
$pdf->setPrintFooter(false); $pdf->setPrintFooter(false);
$pdf->AddPage(); $pdf->AddPage();
@@ -397,7 +397,7 @@ if (isset($_GET['export_quote_pdf'])) {
// Totals // Totals
$html .= '<table width="100%" cellspacing="0" cellpadding="4"> $html .= '<table width="100%" cellspacing="0" cellpadding="4">
<tr> <tr>
<td width="60%" rowspan="6" valign="top"><i>' . nl2br($quote_note) . '</i></td> <td width="60%"><i style="font-size:9pt;">' . nl2br($quote_note) . '</i></td>
<td width="40%"> <td width="40%">
<table width="100%" cellpadding="3" cellspacing="0"> <table width="100%" cellpadding="3" cellspacing="0">
<tr><td>Subtotal:</td><td align="right">' . numfmt_format_currency($currency_format, $sub_total, $quote_currency_code) . '</td></tr>'; <tr><td>Subtotal:</td><td align="right">' . numfmt_format_currency($currency_format, $sub_total, $quote_currency_code) . '</td></tr>';
@@ -526,7 +526,7 @@ if (isset($_GET['export_invoice_pdf'])) {
// Start TCPDF // Start TCPDF
$pdf = new TCPDF('P', 'mm', 'A4', true, 'UTF-8', false); $pdf = new TCPDF('P', 'mm', 'A4', true, 'UTF-8', false);
$pdf->SetMargins(15, 15, 15); $pdf->SetMargins(10, 10, 10);
$pdf->setPrintHeader(false); $pdf->setPrintHeader(false);
$pdf->setPrintFooter(false); $pdf->setPrintFooter(false);
$pdf->AddPage(); $pdf->AddPage();
@@ -620,7 +620,7 @@ if (isset($_GET['export_invoice_pdf'])) {
// Totals // Totals
$html .= '<table width="100%" cellspacing="0" cellpadding="4"> $html .= '<table width="100%" cellspacing="0" cellpadding="4">
<tr> <tr>
<td width="60%" rowspan="6" valign="top"><i>' . nl2br($invoice_note) . '</i></td> <td width="60%"><i style="font-size:9pt;">' . nl2br($invoice_note) . '</i></td>
<td width="40%"> <td width="40%">
<table width="100%" cellpadding="3" cellspacing="0"> <table width="100%" cellpadding="3" cellspacing="0">
<tr><td>Subtotal:</td><td align="right">' . numfmt_format_currency($currency_format, $sub_total, $invoice_currency_code) . '</td></tr>'; <tr><td>Subtotal:</td><td align="right">' . numfmt_format_currency($currency_format, $sub_total, $invoice_currency_code) . '</td></tr>';

View File

@@ -5,4 +5,4 @@
* Update this file each time we merge develop into master. Format is YY.MM (add a .v if there is more than one release a month. * Update this file each time we merge develop into master. Format is YY.MM (add a .v if there is more than one release a month.
*/ */
DEFINE("APP_VERSION", "25.09"); DEFINE("APP_VERSION", "25.09.1");

View File

@@ -1,41 +0,0 @@
document.getElementById('rewordButton').addEventListener('click', function() {
var textInput = this.closest('form').querySelector('textarea');
var ticketDescription = document.getElementById('ticketDescription');
var rewordButton = document.getElementById('rewordButton');
var undoButton = document.getElementById('undoButton');
var previousText = textInput.value; // Store the current text
// Disable the Reword button and show loading state
rewordButton.disabled = true;
rewordButton.innerText = 'Processing...';
fetch('post.php?ai_reword', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
// Body with the text to reword and the ticket description
body: JSON.stringify({
text: textInput.value,
ticketDescription: ticketDescription.innerText.valueOf(),
}),
})
.then(response => response.json())
.then(data => {
textInput.value = data.rewordedText || 'Error: Could not reword the text.';
rewordButton.disabled = false;
rewordButton.innerText = 'Reword'; // Reset button text
undoButton.style.display = 'inline'; // Show the Undo button
// Set up the Undo button to revert to the previous text
undoButton.onclick = function() {
textInput.value = previousText;
this.style.display = 'none'; // Hide the Undo button again
};
})
.catch(error => {
console.error('Error:', error);
rewordButton.disabled = false;
rewordButton.innerText = 'Reword'; // Reset button text
});
});

View File

@@ -31,7 +31,7 @@ if (isset($_GET['ai_reword'])) {
["role" => "system", "content" => $promptText], ["role" => "system", "content" => $promptText],
["role" => "user", "content" => $userText], ["role" => "user", "content" => $userText],
], ],
"temperature" => 0.7 "temperature" => 0.5
]; ];
// Initialize cURL session to the OpenAI Chat API. // Initialize cURL session to the OpenAI Chat API.
@@ -78,6 +78,8 @@ if (isset($_GET['ai_reword'])) {
if (isset($_GET['ai_ticket_summary'])) { if (isset($_GET['ai_ticket_summary'])) {
header('Content-Type: text/html; charset=UTF-8');
$sql = mysqli_query($mysqli, "SELECT * FROM ai_models LEFT JOIN ai_providers ON ai_model_ai_provider_id = ai_provider_id WHERE ai_model_use_case = 'General' LIMIT 1"); $sql = mysqli_query($mysqli, "SELECT * FROM ai_models LEFT JOIN ai_providers ON ai_model_ai_provider_id = ai_provider_id WHERE ai_model_use_case = 'General' LIMIT 1");
$row = mysqli_fetch_array($sql); $row = mysqli_fetch_array($sql);
@@ -89,21 +91,27 @@ if (isset($_GET['ai_ticket_summary'])) {
$ticket_id = intval($_POST['ticket_id']); $ticket_id = intval($_POST['ticket_id']);
// Query the database for ticket details // Query the database for ticket details
// (You can reuse code from ticket.php or write a simplified query here)
$sql = mysqli_query($mysqli, " $sql = mysqli_query($mysqli, "
SELECT ticket_subject, ticket_details SELECT ticket_subject, ticket_details, ticket_source, ticket_priority, ticket_status_name, category_name
FROM tickets FROM tickets
LEFT JOIN ticket_statuses ON ticket_status = ticket_status_id
LEFT JOIN categories ON ticket_category = category_id
WHERE ticket_id = $ticket_id WHERE ticket_id = $ticket_id
LIMIT 1 LIMIT 1
"); ");
$row = mysqli_fetch_assoc($sql); $row = mysqli_fetch_assoc($sql);
$ticket_subject = $row['ticket_subject']; $ticket_subject = $row['ticket_subject'];
$ticket_details = strip_tags($row['ticket_details']); // strip HTML for cleaner prompt $ticket_details = strip_tags($row['ticket_details']); // strip HTML for cleaner prompt
$ticket_status = $row['ticket_status_name'];
$ticket_category = $row['category_name'];
$ticket_source = $row['ticket_source'];
$ticket_priority = $row['ticket_priority'];
// Get ticket replies // Get ticket replies
$sql_replies = mysqli_query($mysqli, " $sql_replies = mysqli_query($mysqli, "
SELECT ticket_reply, ticket_reply_type SELECT ticket_reply, ticket_reply_type, user_name
FROM ticket_replies FROM ticket_replies
LEFT JOIN users ON ticket_reply_by = user_id
WHERE ticket_reply_ticket_id = $ticket_id WHERE ticket_reply_ticket_id = $ticket_id
AND ticket_reply_archived_at IS NULL AND ticket_reply_archived_at IS NULL
ORDER BY ticket_reply_id ASC ORDER BY ticket_reply_id ASC
@@ -113,20 +121,52 @@ if (isset($_GET['ai_ticket_summary'])) {
while ($reply = mysqli_fetch_assoc($sql_replies)) { while ($reply = mysqli_fetch_assoc($sql_replies)) {
$reply_type = $reply['ticket_reply_type']; $reply_type = $reply['ticket_reply_type'];
$reply_text = strip_tags($reply['ticket_reply']); $reply_text = strip_tags($reply['ticket_reply']);
$all_replies_text .= "\n[$reply_type]: $reply_text"; $reply_by = $reply['user_name'];
$all_replies_text .= "\nReply Type: $reply_type Reply By: $reply_by: Reply Text: $reply_text";
} }
// Craft a prompt for ChatGPT $prompt = "
$prompt = "Based on the language detection (case not detected use default English), dont show \"Language Detected\", and Summarize using this language, the following ticket and its responses in a concise and clear way. The summary should be short, highlight the main issue, the actions taken, and any resolution steps:\n\nTicket Subject: $ticket_subject\nTicket Details: $ticket_details\nReplies:$all_replies_text\n\nShort Summary:"; Summarize the following IT support ticket and its responses in a concise, clear, and professional manner.
The summary should include:
1. Main Issue: What was the problem reported by the user?
2. Actions Taken: What steps were taken to address the issue?
3. Resolution or Next Steps: Was the issue resolved or is it ongoing?
Please ensure:
- If there are multiple issues, summarize each separately.
- Urgency: If the ticket or replies express urgency or escalation, highlight it.
- Attachments: If mentioned in the ticket, note any relevant attachments or files.
- Avoid extra explanations or unnecessary information.
Ticket Data:
- Ticket Source: $ticket_source
- Current Ticket Status: $ticket_status
- Ticket Priority: $ticket_priority
- Ticket Category: $ticket_category
- Ticket Subject: $ticket_subject
- Ticket Details: $ticket_details
- Replies:
$all_replies_text
Formatting instructions:
- Use valid HTML tags only.
- Use <h3> for section headers (Main Issue, Actions Taken, Resolution).
- Use <ul><li> for bullet points under each section.
- Do NOT wrap the output in ```html or any other code fences.
- Do NOT include <html>, <head>, or <body>.
- Output only the summary content in pure HTML.
If any part of the ticket or replies is unclear or ambiguous, mention it in the summary and suggest if further clarification is needed.
";
// Prepare the POST data // Prepare the POST data
$post_data = [ $post_data = [
"model" => "$model_name", "model" => "$model_name",
"messages" => [ "messages" => [
["role" => "system", "content" => "You are a helpful assistant."], ["role" => "system", "content" => "Your task is to summarize IT support tickets with clear, concise details."],
["role" => "user", "content" => $prompt] ["role" => "user", "content" => $prompt]
], ],
"temperature" => 0.7 "temperature" => 0.3
]; ];
$ch = curl_init(); $ch = curl_init();
@@ -149,8 +189,8 @@ if (isset($_GET['ai_ticket_summary'])) {
$response_data = json_decode($response, true); $response_data = json_decode($response, true);
$summary = $response_data['choices'][0]['message']['content'] ?? "No summary available."; $summary = $response_data['choices'][0]['message']['content'] ?? "No summary available.";
// Print the summary
echo nl2br(htmlentities($summary)); echo $summary; // nl2br to convert newlines to <br>, htmlspecialchars to prevent XSS
} }
if (isset($_GET['ai_create_document_template'])) { if (isset($_GET['ai_create_document_template'])) {
@@ -183,7 +223,7 @@ if (isset($_GET['ai_create_document_template'])) {
["role" => "system", "content" => $system_message], ["role" => "system", "content" => $system_message],
["role" => "user", "content" => $user_message] ["role" => "user", "content" => $user_message]
], ],
"temperature" => 0.7 "temperature" => 0.5
]; ];
$ch = curl_init(); $ch = curl_init();

View File

@@ -89,7 +89,7 @@ if (isset($_POST['add_database'])) {
// Name of the file // Name of the file
$filename = 'install.sql'; $filename = '../db.sql';
// Temporary variable, used to store current query // Temporary variable, used to store current query
$templine = ''; $templine = '';
// Read in entire file // Read in entire file

View File

@@ -504,7 +504,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
<?php } ?> <?php } ?>
<?php <?php
if (!empty($client_tags_display)) { ?> if (!empty($client_tags_display)) { ?>
<div class="mt-1"> <div class="mt-1 text-wrap">
<?php echo $client_tags_display; ?> <?php echo $client_tags_display; ?>
</div> </div>
<?php } ?> <?php } ?>
@@ -566,10 +566,12 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
<span class="text-secondary">Paid</span> <span class="text-secondary">Paid</span>
<span><?php echo numfmt_format_currency($currency_format, $amount_paid, $session_company_currency); ?></span> <span><?php echo numfmt_format_currency($currency_format, $amount_paid, $session_company_currency); ?></span>
</div> </div>
<?php if ($credit_balance > 0) { ?>
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
<span class="text-secondary">Credit</span> <span class="text-secondary">Credit</span>
<span class="text-success"><?php echo numfmt_format_currency($currency_format, $credit_balance, $session_company_currency); ?></span> <span class="text-success"><?php echo numfmt_format_currency($currency_format, $credit_balance, $session_company_currency); ?></span>
</div> </div>
<?php } ?>
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
<span class="text-secondary">Monthly</span> <span class="text-secondary">Monthly</span>
<span><?php echo numfmt_format_currency($currency_format, $recurring_monthly, $session_company_currency); ?></span> <span><?php echo numfmt_format_currency($currency_format, $recurring_monthly, $session_company_currency); ?></span>

View File

@@ -1,3 +1,5 @@
<?php $show_add_credit = 0; // Remove once credits is added hides the button ?>
<div class="card d-print-none"> <div class="card d-print-none">
<div class="card-header pb-1 pt-2 px-3"> <div class="card-header pb-1 pt-2 px-3">
<div class="card-title"> <div class="card-title">
@@ -16,10 +18,12 @@
<i class="fas fa-fw fa-edit mr-2"></i>Edit Client <i class="fas fa-fw fa-edit mr-2"></i>Edit Client
</a> </a>
<?php if (lookupUserPermission("module_billing") >= 2) { ?> <?php if (lookupUserPermission("module_billing") >= 2) { ?>
<?php if ($show_add_credit) { ?>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a class="dropdown-item" href="#" data-toggle="modal" data-target="#addCreditModal"> <a class="dropdown-item" href="#" data-toggle="modal" data-target="#addCreditModal">
<i class="fas fa-fw fa-wallet mr-2"></i>Add Credit <i class="fas fa-fw fa-wallet mr-2"></i>Add Credit
</a> </a>
<?php } ?>
<?php } ?> <?php } ?>
<?php if (lookupUserPermission("module_client") >= 3) { ?> <?php if (lookupUserPermission("module_client") >= 3) { ?>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>

View File

@@ -1,41 +0,0 @@
document.getElementById('rewordButton').addEventListener('click', function() {
var textInput = this.closest('form').querySelector('textarea');
var ticketDescription = document.getElementById('ticketDescription');
var rewordButton = document.getElementById('rewordButton');
var undoButton = document.getElementById('undoButton');
var previousText = textInput.value; // Store the current text
// Disable the Reword button and show loading state
rewordButton.disabled = true;
rewordButton.innerText = 'Processing...';
fetch('post.php?ai_reword', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
// Body with the text to reword and the ticket description
body: JSON.stringify({
text: textInput.value,
ticketDescription: ticketDescription.innerText.valueOf(),
}),
})
.then(response => response.json())
.then(data => {
textInput.value = data.rewordedText || 'Error: Could not reword the text.';
rewordButton.disabled = false;
rewordButton.innerText = 'Reword'; // Reset button text
undoButton.style.display = 'inline'; // Show the Undo button
// Set up the Undo button to revert to the previous text
undoButton.onclick = function() {
textInput.value = previousText;
this.style.display = 'none'; // Hide the Undo button again
};
})
.catch(error => {
console.error('Error:', error);
rewordButton.disabled = false;
rewordButton.innerText = 'Reword'; // Reset button text
});
});

View File

@@ -2232,7 +2232,7 @@ if (isset($_GET['export_invoice_pdf'])) {
// Start TCPDF // Start TCPDF
$pdf = new TCPDF('P', 'mm', 'A4', true, 'UTF-8', false); $pdf = new TCPDF('P', 'mm', 'A4', true, 'UTF-8', false);
$pdf->SetMargins(15, 15, 15); $pdf->SetMargins(10, 10, 10);
$pdf->setPrintHeader(false); $pdf->setPrintHeader(false);
$pdf->setPrintFooter(false); $pdf->setPrintFooter(false);
$pdf->AddPage(); $pdf->AddPage();
@@ -2326,7 +2326,7 @@ if (isset($_GET['export_invoice_pdf'])) {
// Totals // Totals
$html .= '<table width="100%" cellspacing="0" cellpadding="4"> $html .= '<table width="100%" cellspacing="0" cellpadding="4">
<tr> <tr>
<td width="60%" rowspan="6" valign="top"><i>' . nl2br($invoice_note) . '</i></td> <td width="60%"><i style="font-size:9pt;">' . nl2br($invoice_note) . '</i></td>
<td width="40%"> <td width="40%">
<table width="100%" cellpadding="3" cellspacing="0"> <table width="100%" cellpadding="3" cellspacing="0">
<tr><td>Subtotal:</td><td align="right">' . numfmt_format_currency($currency_format, $sub_total, $invoice_currency_code) . '</td></tr>'; <tr><td>Subtotal:</td><td align="right">' . numfmt_format_currency($currency_format, $sub_total, $invoice_currency_code) . '</td></tr>';

View File

@@ -669,7 +669,7 @@ if (isset($_GET['export_quote_pdf'])) {
// Start TCPDF // Start TCPDF
$pdf = new TCPDF('P', 'mm', 'A4', true, 'UTF-8', false); $pdf = new TCPDF('P', 'mm', 'A4', true, 'UTF-8', false);
$pdf->SetMargins(15, 15, 15); $pdf->SetMargins(10, 10, 10);
$pdf->setPrintHeader(false); $pdf->setPrintHeader(false);
$pdf->setPrintFooter(false); $pdf->setPrintFooter(false);
$pdf->AddPage(); $pdf->AddPage();
@@ -766,7 +766,7 @@ if (isset($_GET['export_quote_pdf'])) {
// Totals // Totals
$html .= '<table width="100%" cellspacing="0" cellpadding="4"> $html .= '<table width="100%" cellspacing="0" cellpadding="4">
<tr> <tr>
<td width="60%" rowspan="6" valign="top"><i>' . nl2br($quote_note) . '</i></td> <td width="60%"><i style="font-size:9pt;">' . nl2br($quote_note) . '</i></td>
<td width="40%"> <td width="40%">
<table width="100%" cellpadding="3" cellspacing="0"> <table width="100%" cellpadding="3" cellspacing="0">
<tr><td>Subtotal:</td><td align="right">' . numfmt_format_currency($currency_format, $sub_total, $quote_currency_code) . '</td></tr>'; <tr><td>Subtotal:</td><td align="right">' . numfmt_format_currency($currency_format, $sub_total, $quote_currency_code) . '</td></tr>';

View File

@@ -933,8 +933,7 @@ if (isset($_GET['ticket_id'])) {
<div> <div>
<i class="fa fa-fw fa-user text-secondary mr-2"></i><a href="#" class="ajax-modal" <i class="fa fa-fw fa-user text-secondary mr-2"></i><a href="#" class="ajax-modal"
data-modal-size="lg" data-modal-size="lg"
data-modal-url="modals/contact/contact_details.php?id=<?= $contact_id ?>"> data-modal-url="modals/contact/contact_details.php?id=<?= $contact_id ?>"><strong><?= $contact_name ?></strong>
<strong><?php echo $contact_name; ?></strong>
</a> </a>
</div> </div>
@@ -1276,17 +1275,17 @@ require_once "../includes/footer.php";
<!-- Summary Modal --> <!-- Summary Modal -->
<div class="modal fade" id="summaryModal" tabindex="-1"> <div class="modal fade" id="summaryModal" tabindex="-1">
<div class="modal-dialog modal-lg" role="document"> <div class="modal-dialog modal-lg">
<div class="modal-content bg-dark"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header bg-dark">
<h5 class="modal-title" id="summaryModalTitle">Ticket Summary</h5> <h5 class="modal-title" id="summaryModalTitle">Ticket Summary</h5>
<button type="button" class="close text-white" data-dismiss="modal" aria-label="Close"> <button type="button" class="close text-white" data-dismiss="modal">
<span>&times;</span> <span>&times;</span>
</button> </button>
</div> </div>
<div class="modal-body bg-white"> <div class="modal-body">
<div id="summaryContent" class="text-center"> <div id="summaryContent">
<i class="fas fa-spinner fa-spin"></i> Generating summary... <div class="text-center"><i class="fas fa-spinner fa-spin"></i> Generating summary...</div>
</div> </div>
</div> </div>
</div> </div>