diff --git a/README.md b/README.md
index 2985a484..ab9597c0 100644
--- a/README.md
+++ b/README.md
@@ -93,6 +93,7 @@ If you want to improve ITFlow, feel free to fork the repo and create a pull requ
We’re incredibly grateful to the organizations and individuals who support the project - a big thank you to:
- CompuMatter
- F1 for HELP
+- digiBandit
- JetBrains (PhpStorm)
## License
diff --git a/admin/api_keys.php b/admin/api_keys.php
index c1ef53d6..eb21404c 100644
--- a/admin/api_keys.php
+++ b/admin/api_keys.php
@@ -49,7 +49,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
@@ -139,9 +139,16 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
diff --git a/admin/post/api_keys.php b/admin/post/api_keys.php
index 5d383fa5..6b500166 100644
--- a/admin/post/api_keys.php
+++ b/admin/post/api_keys.php
@@ -31,6 +31,27 @@ if (isset($_POST['add_api_key'])) {
}
+if (isset($_GET['revoke_api_key'])) {
+
+ validateCSRFToken($_GET['csrf_token']);
+
+ $api_key_id = intval($_GET['revoke_api_key']);
+
+ // Get API Key Name
+ $row = mysqli_fetch_assoc(mysqli_query($mysqli,"SELECT api_key_name, api_key_client_id FROM api_keys WHERE api_key_id = $api_key_id"));
+ $api_key_name = sanitizeInput($row['api_key_name']);
+ $client_id = intval($row['api_key_client_id']);
+
+ mysqli_query($mysqli,"UPDATE api_keys SET api_key_expire = NOW() WHERE api_key_id = $api_key_id");
+
+ logAction("API Key", "Revoke", "$session_name revoked API key $name", $client_id);
+
+ flash_alert("API Key $name revoked", 'error');
+
+ redirect();
+
+}
+
if (isset($_GET['delete_api_key'])) {
validateCSRFToken($_GET['csrf_token']);
diff --git a/agent/includes/side_nav.php b/agent/includes/side_nav.php
index e9290b17..9c87bb47 100644
--- a/agent/includes/side_nav.php
+++ b/agent/includes/side_nav.php
@@ -125,49 +125,52 @@
- = 1) { ?>
+
+
-
- ">
-
- Payments
-
-
-
- ">
-
- Vendors
-
-
-
- ">
-
- Expenses
-
-
-
- ">
-
-
- Recurring Expenses
-
-
-
-
-
-
-
- ">
-
- Accounts
-
-
-
- ">
-
- Transfers
-
-
+ = 1) { ?>
+
+ ">
+
+ Payments
+
+
+
+ ">
+
+ Vendors
+
+
+
+ ">
+
+ Expenses
+
+
+
+ ">
+
+
+ Recurring Expenses
+
+
+
+
+
+
+
+ ">
+
+ Accounts
+
+
+
+ ">
+
+ Transfers
+
+
+
">
diff --git a/agent/invoice.php b/agent/invoice.php
index d14a9897..3719004d 100644
--- a/agent/invoice.php
+++ b/agent/invoice.php
@@ -288,6 +288,9 @@ if (isset($_GET['invoice_id'])) {
Download PDF
+
+ Packing Slip
+
Send Email
diff --git a/agent/modals/asset/asset_copy.php b/agent/modals/asset/asset_copy.php
index 6740530d..401183f3 100644
--- a/agent/modals/asset/asset_copy.php
+++ b/agent/modals/asset/asset_copy.php
@@ -26,6 +26,7 @@ $asset_mac = nullable_htmlentities($row['interface_mac']);
$asset_uri = nullable_htmlentities($row['asset_uri']);
$asset_uri_2 = nullable_htmlentities($row['asset_uri_2']);
$asset_status = nullable_htmlentities($row['asset_status']);
+$asset_purchase_reference = nullable_htmlentities($row['asset_purchase_reference']);
$asset_purchase_date = nullable_htmlentities($row['asset_purchase_date']);
$asset_warranty_expire = nullable_htmlentities($row['asset_warranty_expire']);
$asset_install_date = nullable_htmlentities($row['asset_install_date']);
@@ -370,7 +371,7 @@ ob_start();
-
+
diff --git a/agent/modals/asset/asset_edit.php b/agent/modals/asset/asset_edit.php
index 5aab22d2..5ae88afa 100644
--- a/agent/modals/asset/asset_edit.php
+++ b/agent/modals/asset/asset_edit.php
@@ -508,9 +508,10 @@ ob_start();
$asset_history_created_at $asset_history_description ";
+ $asset_history_status = nullable_htmlentities($row['asset_history_status']);
+ $asset_history_description = nullable_htmlentities($row['asset_history_description']);
+ $asset_history_created_at = nullable_htmlentities($row['asset_history_created_at']);
+ echo "$asset_history_created_at - $asset_history_status $asset_history_description ";
}
?>
diff --git a/agent/post/asset.php b/agent/post/asset.php
index e572f217..354528b7 100644
--- a/agent/post/asset.php
+++ b/agent/post/asset.php
@@ -128,6 +128,9 @@ if (isset($_POST['edit_asset'])) {
}
}
+ // Add to History
+ mysqli_query($mysqli,"INSERT INTO asset_history SET asset_history_status = '$status', asset_history_description = '$session_name updated $name', asset_history_asset_id = $asset_id");
+
logAction("Asset", "Edit", "$session_name edited asset $name", $client_id, $asset_id);
flash_alert("Asset $name edited");
@@ -152,6 +155,9 @@ if (isset($_GET['archive_asset'])) {
mysqli_query($mysqli,"UPDATE assets SET asset_archived_at = NOW() WHERE asset_id = $asset_id");
+ // Add to History
+ mysqli_query($mysqli,"INSERT INTO asset_history SET asset_history_status = 'Archived', asset_history_description = '$session_name archived $asset_name', asset_history_asset_id = $asset_id");
+
logAction("Asset", "Archive", "$session_name archived asset $asset_name", $client_id, $asset_id);
flash_alert("Asset $asset_name archived", 'error');
@@ -176,6 +182,9 @@ if (isset($_GET['unarchive_asset'])) {
mysqli_query($mysqli,"UPDATE assets SET asset_archived_at = NULL WHERE asset_id = $asset_id");
+ // Add to History
+ mysqli_query($mysqli,"INSERT INTO asset_history SET asset_history_status = 'UnArchived', asset_history_description = '$session_name unarchived $asset_name', asset_history_asset_id = $asset_id");
+
logAction("Asset", "Unarchive", "$session_name unarchived asset $asset_name", $client_id, $asset_id);
flash_alert("Asset $asset_name Unarchived");
@@ -391,6 +400,7 @@ if (isset($_POST['bulk_transfer_client_asset'])) {
// Archive/log the current asset
$notes = $asset_notes . "\r\n\r\n---\r\n* " . date('Y-m-d H:i:s') . ": Transferred asset $asset_name (old asset ID: $current_asset_id) from $current_client_name to $new_client_name (new asset ID: $new_asset_id)";
mysqli_query($mysqli,"UPDATE assets SET asset_archived_at = NOW() WHERE asset_id = $current_asset_id");
+ mysqli_query($mysqli,"INSERT INTO asset_history SET asset_history_status = 'Transferred', asset_history_description = '$session_name transferred $asset_name to $new_client_name', asset_history_asset_id = $current_asset_id");
// Log Archive
logAction("Asset", "Archive", "$session_name archived asset $asset_name (via transfer)", $current_client_id, $current_asset_id);
@@ -402,6 +412,7 @@ if (isset($_POST['bulk_transfer_client_asset'])) {
// Log the new asset
$notes = $asset_notes . "\r\n\r\n---\r\n* " . date('Y-m-d H:i:s') . ": Transferred asset $asset_name (old asset ID: $current_asset_id) from $current_client_name to $new_client_name (new asset ID: $new_asset_id)";
logAction("Asset", "Create", "$session_name created asset $name (via transfer)", $new_client_id, $new_asset_id);
+ mysqli_query($mysqli,"INSERT INTO asset_history SET asset_history_status = 'Transferred', asset_history_description = '$session_name created asset via transfer from $current_client_name', asset_history_asset_id = $new_asset_id");
logAction("Asset", "Transfer", "$session_name Transferred asset $asset_name (old asset ID: $current_asset_id) from $current_client_name to $new_client_name (new asset ID: $new_asset_id)", $new_client_id, $new_asset_id);
@@ -486,6 +497,9 @@ if (isset($_POST['bulk_edit_asset_status'])) {
logAction("Asset", "Edit", "$session_name set status to $status on $asset_name", $client_id, $asset_id);
+ // Add to History
+ mysqli_query($mysqli,"INSERT INTO asset_history SET asset_history_status = '$status', asset_history_description = '$session_name updated $asset_name', asset_history_asset_id = $asset_id");
+
}
logAction("Asset", "Bulk Edit", "$session_name set status to $status on $asset_count assets", $client_id);
@@ -521,6 +535,9 @@ if (isset($_POST['bulk_archive_assets'])) {
logAction("Asset", "Archive", "$session_name archived asset $asset_name", $client_id, $asset_id);
+ // Add to History
+ mysqli_query($mysqli,"INSERT INTO asset_history SET asset_history_status = 'Archived', asset_history_description = '$session_name archived $asset_name', asset_history_asset_id = $asset_id");
+
}
logAction("Asset", "Bulk Archive", "$session_name archived $count assets", $client_id);
@@ -558,6 +575,9 @@ if (isset($_POST['bulk_unarchive_assets'])) {
// Individual Asset logging
logAction("Asset", "Unarchive", "$session_name unarchived asset $asset_name", $client_id, $asset_id);
+ // Add to History
+ mysqli_query($mysqli,"INSERT INTO asset_history SET asset_history_status = 'UnArchived', asset_history_description = '$session_name unarchived $asset_name', asset_history_asset_id = $asset_id");
+
}
logAction("Asset", "Bulk Unarchive", "$session_name unarchived $count assets");
diff --git a/agent/post/invoice.php b/agent/post/invoice.php
index cea45b0e..667e603f 100644
--- a/agent/post/invoice.php
+++ b/agent/post/invoice.php
@@ -35,7 +35,7 @@ if (isset($_POST['add_invoice'])) {
$invoice_id = mysqli_insert_id($mysqli);
- mysqli_query($mysqli,"INSERT INTO history SET history_status = 'Draft', history_description = 'Invoice created', history_invoice_id = $invoice_id");
+ mysqli_query($mysqli,"INSERT INTO history SET history_status = 'Draft', history_description = 'Invoice created by $session_name', history_invoice_id = $invoice_id");
logAction("Invoice", "Create", "$session_name created Invoice $config_invoice_prefix$invoice_number - $scope", $client_id, $invoice_id);
@@ -159,7 +159,7 @@ if (isset($_GET['mark_invoice_sent'])) {
mysqli_query($mysqli,"UPDATE invoices SET invoice_status = 'Sent' WHERE invoice_id = $invoice_id");
- mysqli_query($mysqli,"INSERT INTO history SET history_status = 'Sent', history_description = 'Invoice marked sent', history_invoice_id = $invoice_id");
+ mysqli_query($mysqli,"INSERT INTO history SET history_status = 'Sent', history_description = 'Invoice marked sent by $session_name', history_invoice_id = $invoice_id");
logAction("Invoice", "Edit", "$session_name marked invoice $invoice_prefix$invoice_number sent", $client_id, $invoice_id);
@@ -205,7 +205,7 @@ if (isset($_GET['cancel_invoice'])) {
mysqli_query($mysqli,"UPDATE invoices SET invoice_status = 'Cancelled' WHERE invoice_id = $invoice_id");
- mysqli_query($mysqli,"INSERT INTO history SET history_status = 'Cancelled', history_description = 'Invoice cancelled', history_invoice_id = $invoice_id");
+ mysqli_query($mysqli,"INSERT INTO history SET history_status = 'Cancelled', history_description = 'Invoice cancelled by $session_name', history_invoice_id = $invoice_id");
logAction("Invoice", "Edit", "$session_name cancelled invoice $invoice_prefix$invoice_number", $client_id, $invoice_id);
@@ -586,7 +586,7 @@ if (isset($_GET['email_invoice'])) {
flash_alert("Invoice sent!");
- mysqli_query($mysqli,"INSERT INTO history SET history_status = 'Sent', history_description = 'Invoice sent to the mail queue ID: $email_id', history_invoice_id = $invoice_id");
+ mysqli_query($mysqli,"INSERT INTO history SET history_status = 'Sent', history_description = 'Invoice sent by $session_name (mail queue ID: $email_id)', history_invoice_id = $invoice_id");
// Don't change the status to sent if the status is anything but draft
if ($invoice_status == 'Draft') {
@@ -944,6 +944,153 @@ if (isset($_GET['export_invoice_pdf'])) {
}
+if (isset($_GET['export_invoice_packing_slip'])) {
+
+ $invoice_id = intval($_GET['export_invoice_packing_slip']);
+
+ $sql = mysqli_query(
+ $mysqli,
+ "SELECT * FROM invoices
+ LEFT JOIN clients ON invoice_client_id = client_id
+ LEFT JOIN contacts ON clients.client_id = contacts.contact_client_id AND contact_primary = 1
+ LEFT JOIN locations ON clients.client_id = locations.location_client_id AND location_primary = 1
+ WHERE invoice_id = $invoice_id
+ $access_permission_query
+ LIMIT 1"
+ );
+
+ $row = mysqli_fetch_assoc($sql);
+ $invoice_id = intval($row['invoice_id']);
+ $invoice_prefix = nullable_htmlentities($row['invoice_prefix']);
+ $invoice_number = intval($row['invoice_number']);
+ $invoice_date = nullable_htmlentities($row['invoice_date']);
+ $client_id = intval($row['client_id']);
+ $client_name = nullable_htmlentities($row['client_name']);
+ $location_address = nullable_htmlentities($row['location_address']);
+ $location_city = nullable_htmlentities($row['location_city']);
+ $location_state = nullable_htmlentities($row['location_state']);
+ $location_zip = nullable_htmlentities($row['location_zip']);
+ $location_country = nullable_htmlentities($row['location_country']);
+ $contact_email = nullable_htmlentities($row['contact_email']);
+ $contact_phone_country_code = nullable_htmlentities($row['contact_phone_country_code']);
+ $contact_phone = nullable_htmlentities(formatPhoneNumber($row['contact_phone'], $contact_phone_country_code));
+ $contact_extension = nullable_htmlentities($row['contact_extension']);
+
+ $sql = mysqli_query($mysqli, "SELECT * FROM companies WHERE company_id = 1");
+ $row = mysqli_fetch_assoc($sql);
+ $company_id = intval($row['company_id']);
+ $company_name = nullable_htmlentities($row['company_name']);
+ $company_country = nullable_htmlentities($row['company_country']);
+ $company_address = nullable_htmlentities($row['company_address']);
+ $company_city = nullable_htmlentities($row['company_city']);
+ $company_state = nullable_htmlentities($row['company_state']);
+ $company_zip = nullable_htmlentities($row['company_zip']);
+ $company_phone_country_code = nullable_htmlentities($row['company_phone_country_code']);
+ $company_phone = nullable_htmlentities(formatPhoneNumber($row['company_phone'], $company_phone_country_code));
+ $company_email = nullable_htmlentities($row['company_email']);
+ $company_website = nullable_htmlentities($row['company_website']);
+ $company_tax_id = nullable_htmlentities($row['company_tax_id']);
+ if ($config_invoice_show_tax_id && !empty($company_tax_id)) {
+ $company_tax_id_display = "Tax ID: $company_tax_id";
+ } else {
+ $company_tax_id_display = "";
+ }
+ $company_logo = nullable_htmlentities($row['company_logo']);
+
+ require_once("../plugins/TCPDF/tcpdf.php");
+
+ // Start TCPDF
+ $pdf = new TCPDF('P', 'mm', 'A4', true, 'UTF-8', false);
+ $pdf->SetMargins(10, 10, 10);
+ $pdf->setPrintHeader(false);
+ $pdf->setPrintFooter(false);
+ $pdf->AddPage();
+ $pdf->SetFont('helvetica', '', 10);
+
+ // Logo + Right Columns
+ $html = '
+
+ ';
+ if (!empty($company_logo) && file_exists("../uploads/settings/$company_logo")) {
+ $html .= ' ';
+ }
+ $html .= '
+
+ Packing Slip
+ ' . $invoice_prefix . $invoice_number . ' ';
+ $html .= '
+
+
';
+
+ // Billing titles
+ $html .= '
+
+ ' . $company_name . '
+ ' . $client_name . '
+
+
+ ' . nl2br("$company_address\n$company_city $company_state $company_zip\n$company_country\n$company_phone\n$company_website\n$company_tax_id_display") . '
+ ' . nl2br("$location_address\n$location_city $location_state $location_zip\n$location_country\n$contact_email\n$contact_phone") . '
+
+
';
+
+ // Items header
+ $html .= '
+
+
+ Item
+ Qty
+ Picked?
+ ';
+
+ // Load items
+ $sub_total = 0;
+ $total_tax = 0;
+
+ $sql_items = mysqli_query($mysqli, "SELECT * FROM invoice_items WHERE item_invoice_id = $invoice_id ORDER BY item_order ASC");
+ while ($item = mysqli_fetch_assoc($sql_items)) {
+ $name = $item['item_name'];
+ $qty = $item['item_quantity'];
+
+ $html .= '
+
+ ' . $name . '
+ ' . number_format($qty, 2) . '
+
+
+
+ ';
+ }
+ $html .= '
';
+
+
+ // Picked/Checked by
+ $html .= '
+
+
+
+ Picked By:
+
+
+ Checked By:
+
+
+
+ ';
+
+ $pdf->writeHTML($html, true, false, true, false, '');
+
+ $filename = preg_replace('/[^A-Za-z0-9_\-]/', '_', "{$invoice_date}_{$company_name}_{$client_name}_Invoice_{$invoice_prefix}{$invoice_number}");
+ $pdf->Output("$filename.pdf", 'I');
+
+ exit;
+
+}
+
if (isset($_POST['bulk_edit_invoice_category'])) {
$category_id = intval($_POST['bulk_category_id']);
diff --git a/cron/ticket_email_parser.php b/cron/ticket_email_parser.php
index 13fca0f2..b2d72ce5 100644
--- a/cron/ticket_email_parser.php
+++ b/cron/ticket_email_parser.php
@@ -150,8 +150,10 @@ function addTicket($contact_id, $contact_name, $contact_email, $client_id, $date
mysqli_query($mysqli, "INSERT INTO ticket_watchers SET watcher_email = '$contact_email_esc', watcher_ticket_id = $id");
}
+ // External email
+ $bad_pattern = "/do[\W_]*not[\W_]*reply|no[\W_]*reply/i";
$data = [];
- if ($config_ticket_client_general_notifications == 1) {
+ if ($config_ticket_client_general_notifications == 1 && !preg_match($bad_pattern, $contact_email)) {
$subject_email = "Ticket created - [$config_ticket_prefix$ticket_number] - $subject";
$body = "##- Please type your reply above this line -## Hello $contact_name, Thank you for your email. A ticket regarding \"$subject\" has been automatically created for you. Ticket: $config_ticket_prefix$ticket_number Subject: $subject Status: New Portal: View ticket -- $company_name - Support $config_ticket_from_email $company_phone";
$data[] = [
@@ -164,6 +166,7 @@ function addTicket($contact_id, $contact_name, $contact_email, $client_id, $date
];
}
+ // Internal email
if ($config_ticket_new_ticket_notification_email) {
if ($client_id == 0) {
$client_name = "Guest";
@@ -611,7 +614,16 @@ foreach ($messages as $message) {
// Body (prefer HTML)
$message_body_html = $message->getHTMLBody();
$message_body_text = $message->getTextBody();
- $message_body = $message_body_html ?: nl2br(htmlspecialchars((string)$message_body_text));
+ $message_body_raw = $message->getRawBody();
+
+ if (!empty($message_body_html)) {
+ $message_body = $message_body_html;
+ } elseif (!empty($message_body_text)) {
+ $message_body = nl2br(htmlspecialchars($message_body_text));
+ } else {
+ // Final fallback
+ $message_body = nl2br(htmlspecialchars($message_body_raw));
+ }
// Handle attachments (inline vs regular)
$attachments = [];