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
commit 3106685972
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 96 additions and 118 deletions

View File

@ -2,6 +2,23 @@
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]
***BACK UP*** before updating.

View File

@ -72,6 +72,7 @@
<p>Saved Payments</p>
</a>
</li>
<?php } ?>
<li class="nav-item">
<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>
@ -84,7 +85,7 @@
<p>AI Models</p>
</a>
</li>
<?php } ?>
<?php if ($config_module_enable_ticketing) { ?>
<li class="nav-item">
<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">
</div>
<?php if ($config_ai_enable == 1) { ?>
<!-- Prompt for AI -->
<div class="form-group">
<label>Enter a prompt for the type of IT documentation you want to generate:</label>
<div class="input-group mb-3">
@ -27,7 +25,6 @@
</div>
</div>
</div>
<?php } ?>
<!-- TinyMCE Content -->
<div class="form-group">

View File

@ -300,7 +300,7 @@ if (isset($_GET['export_quote_pdf'])) {
// Start TCPDF
$pdf = new TCPDF('P', 'mm', 'A4', true, 'UTF-8', false);
$pdf->SetMargins(15, 15, 15);
$pdf->SetMargins(10, 10, 10);
$pdf->setPrintHeader(false);
$pdf->setPrintFooter(false);
$pdf->AddPage();
@ -397,7 +397,7 @@ if (isset($_GET['export_quote_pdf'])) {
// Totals
$html .= '<table width="100%" cellspacing="0" cellpadding="4">
<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%">
<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>';
@ -526,7 +526,7 @@ if (isset($_GET['export_invoice_pdf'])) {
// Start TCPDF
$pdf = new TCPDF('P', 'mm', 'A4', true, 'UTF-8', false);
$pdf->SetMargins(15, 15, 15);
$pdf->SetMargins(10, 10, 10);
$pdf->setPrintHeader(false);
$pdf->setPrintFooter(false);
$pdf->AddPage();
@ -620,7 +620,7 @@ if (isset($_GET['export_invoice_pdf'])) {
// Totals
$html .= '<table width="100%" cellspacing="0" cellpadding="4">
<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%">
<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>';

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.
*/
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" => "user", "content" => $userText],
],
"temperature" => 0.7
"temperature" => 0.5
];
// Initialize cURL session to the OpenAI Chat API.
@ -78,6 +78,8 @@ if (isset($_GET['ai_reword'])) {
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");
$row = mysqli_fetch_array($sql);
@ -89,21 +91,27 @@ if (isset($_GET['ai_ticket_summary'])) {
$ticket_id = intval($_POST['ticket_id']);
// Query the database for ticket details
// (You can reuse code from ticket.php or write a simplified query here)
$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
LEFT JOIN ticket_statuses ON ticket_status = ticket_status_id
LEFT JOIN categories ON ticket_category = category_id
WHERE ticket_id = $ticket_id
LIMIT 1
");
$row = mysqli_fetch_assoc($sql);
$ticket_subject = $row['ticket_subject'];
$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
$sql_replies = mysqli_query($mysqli, "
SELECT ticket_reply, ticket_reply_type
SELECT ticket_reply, ticket_reply_type, user_name
FROM ticket_replies
LEFT JOIN users ON ticket_reply_by = user_id
WHERE ticket_reply_ticket_id = $ticket_id
AND ticket_reply_archived_at IS NULL
ORDER BY ticket_reply_id ASC
@ -113,20 +121,52 @@ if (isset($_GET['ai_ticket_summary'])) {
while ($reply = mysqli_fetch_assoc($sql_replies)) {
$reply_type = $reply['ticket_reply_type'];
$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 = "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:";
$prompt = "
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
$post_data = [
"model" => "$model_name",
"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]
],
"temperature" => 0.7
"temperature" => 0.3
];
$ch = curl_init();
@ -149,8 +189,8 @@ if (isset($_GET['ai_ticket_summary'])) {
$response_data = json_decode($response, true);
$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'])) {
@ -183,7 +223,7 @@ if (isset($_GET['ai_create_document_template'])) {
["role" => "system", "content" => $system_message],
["role" => "user", "content" => $user_message]
],
"temperature" => 0.7
"temperature" => 0.5
];
$ch = curl_init();

View File

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

View File

@ -504,7 +504,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
<?php } ?>
<?php
if (!empty($client_tags_display)) { ?>
<div class="mt-1">
<div class="mt-1 text-wrap">
<?php echo $client_tags_display; ?>
</div>
<?php } ?>
@ -566,10 +566,12 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
<span class="text-secondary">Paid</span>
<span><?php echo numfmt_format_currency($currency_format, $amount_paid, $session_company_currency); ?></span>
</div>
<?php if ($credit_balance > 0) { ?>
<div class="d-flex justify-content-between">
<span class="text-secondary">Credit</span>
<span class="text-success"><?php echo numfmt_format_currency($currency_format, $credit_balance, $session_company_currency); ?></span>
</div>
<?php } ?>
<div class="d-flex justify-content-between">
<span class="text-secondary">Monthly</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-header pb-1 pt-2 px-3">
<div class="card-title">
@ -16,10 +18,12 @@
<i class="fas fa-fw fa-edit mr-2"></i>Edit Client
</a>
<?php if (lookupUserPermission("module_billing") >= 2) { ?>
<?php if ($show_add_credit) { ?>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="#" data-toggle="modal" data-target="#addCreditModal">
<i class="fas fa-fw fa-wallet mr-2"></i>Add Credit
</a>
<?php } ?>
<?php } ?>
<?php if (lookupUserPermission("module_client") >= 3) { ?>
<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
$pdf = new TCPDF('P', 'mm', 'A4', true, 'UTF-8', false);
$pdf->SetMargins(15, 15, 15);
$pdf->SetMargins(10, 10, 10);
$pdf->setPrintHeader(false);
$pdf->setPrintFooter(false);
$pdf->AddPage();
@ -2326,7 +2326,7 @@ if (isset($_GET['export_invoice_pdf'])) {
// Totals
$html .= '<table width="100%" cellspacing="0" cellpadding="4">
<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%">
<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>';

View File

@ -669,7 +669,7 @@ if (isset($_GET['export_quote_pdf'])) {
// Start TCPDF
$pdf = new TCPDF('P', 'mm', 'A4', true, 'UTF-8', false);
$pdf->SetMargins(15, 15, 15);
$pdf->SetMargins(10, 10, 10);
$pdf->setPrintHeader(false);
$pdf->setPrintFooter(false);
$pdf->AddPage();
@ -766,7 +766,7 @@ if (isset($_GET['export_quote_pdf'])) {
// Totals
$html .= '<table width="100%" cellspacing="0" cellpadding="4">
<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%">
<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>';

View File

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