diff --git a/CHANGELOG.md b/CHANGELOG.md index 099c264e..38642ef2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,39 @@ This file documents all notable changes made to ITFlow. +## [25.05] + +### Added / Changed +- Expanded file upload allow-list to include .bat and .stk file types. +- Added full backup/restore functionality. Backup downloads a zip that includes the SQL dump and uploads folder, setup now has option to restore from zip backup. +- Migrated Asset and Contact Links to modals to resolve variable overlap issue. +- Added Pagination to Notification Modal. +- Removed 500 Records Per Page option. +- Removed unused old DB checks in the top nav. +- Clients can now use the portal to setup Stripe automatic payments themselves for recurring invoices +- Automatic payments are now disabled for all recurring invoices if the saved payment method is removed +- Added Card Details and Payment added to Client Stripe. +- UI / UX updates to guest pay Make use of cards. +- Don't show Checkbox columns when ticket is closed, compact ticket list now matches round pills for status and priority. +- Ticket UI/UX update allow the ticket toolbar to be a little more mobile-friendly +- UI / UX Updates to Expenses - Combine Category and Description into 1 column. +- Country information is now displayed in Invoices, Quotes, Recurring Invoices, Clients, Locations, and the client top header. +- Added country-based search filters in Locations and Clients sections. +- Changed the settings name from Integrations to Identity Providers to make room for future iDPs (e.g. Google). +- Bump FullCalendar from 6.1.15 to 6.1.17. +- Bump DataTables from 2.2.2 to 2.3.1. +- Bump TCPDF from 6.8.2 to 6.9.4. +- Bump tinyMCE from 7.7.1 to 7.9.0. +- Bump phpMailer from 6.9.2 to 6.10.0. +- Bump stripe-php from 16.4.0 to 17.2.1. + + +### Fixed +- "None" option for SMTP encryption now functions correctly. +- Debug table row counts now reflect actual counts instead of relying on SHOW TABLE STATUS. +- Archived Categories now display properly. +- Stripe saved payment methods are now limited to credit/debit cards only. + ## [25.03.6] ### Fixed @@ -280,4 +313,4 @@ This file documents all notable changes made to ITFlow. ## [24.12] ### Added / Changed -- Introduced versioned releases for the first time! \ No newline at end of file +- Introduced versioned releases for the first time! diff --git a/SECURITY.md b/SECURITY.md index bd6ba7fd..44ec9dcd 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -12,10 +12,8 @@ We operate a rolling release model. Any bug fixes will be released into latest version of ITFlow, so you must stay up-to-date. | Version | Supported | -| ------- | ------------------ | -| Beta | :x: | -| 24.12 | :white_check_mark: | -| 25.1 | :white_check_mark: (When released) | +|---------| ------------------ | +| 25.05 | :white_check_mark: | ## Reporting a Vulnerability via GitHub Security Advisories diff --git a/admin_app_log.php b/admin_app_log.php index f5aa1970..ec30f510 100644 --- a/admin_app_log.php +++ b/admin_app_log.php @@ -48,14 +48,16 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));

App Logs

-
+
-
- -
- - +
+
+ +
+ + +
diff --git a/admin_audit_log.php b/admin_audit_log.php index 980035a3..9f61b0b1 100644 --- a/admin_audit_log.php +++ b/admin_audit_log.php @@ -75,7 +75,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
-
+
@@ -85,7 +85,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
-
+
@@ -125,7 +125,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
-
+
@@ -198,7 +198,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
- "> + text-nowrap"> + + + + + + + + + +
diff --git a/admin_backup.php b/admin_backup.php index 841ab95d..6acb4780 100644 --- a/admin_backup.php +++ b/admin_backup.php @@ -8,7 +8,7 @@ require_once "includes/inc_all_admin.php"; @@ -20,12 +20,12 @@ require_once "includes/inc_all_admin.php";
-
-
-
- +
+
+ +
+
-
diff --git a/admin_category.php b/admin_category.php index d27228f5..18bfa5c5 100644 --- a/admin_category.php +++ b/admin_category.php @@ -27,10 +27,6 @@ $sql = mysqli_query( ); $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()")); -if (isset($_GET['archived'])) { - $category = "Archived"; -} - ?>
@@ -98,7 +94,7 @@ if (isset($_GET['archived'])) { } else { echo 'btn-default'; } ?>">Ticket - diff --git a/admin_debug.php b/admin_debug.php index dcb11792..edd3cb9c 100644 --- a/admin_debug.php +++ b/admin_debug.php @@ -296,7 +296,13 @@ if ($tablesResult) { while ($table = $tablesResult->fetch_assoc()) { $tableName = $table['Name']; - $tableRows = $table['Rows']; + + // Accurate row count + $countResult = $mysqli->query("SELECT COUNT(*) AS cnt FROM `$tableName`"); + $countRow = $countResult->fetch_assoc(); + $tableRows = $countRow['cnt']; + $countResult->free(); + $dataLength = $table['Data_length']; $indexLength = $table['Index_length']; $tableSize = ($dataLength + $indexLength) / (1024 * 1024); // Size in MB @@ -336,11 +342,6 @@ if ($tablesResult) { 'name' => 'Total database size (MB)', 'value' => round($totalSize, 2) . ' MB', ]; -} else { - $databaseStats[] = [ - 'name' => 'Database connection error', - 'value' => $mysqli->error, - ]; } // Section: Database Structure Comparison @@ -518,24 +519,26 @@ $mysqli->close();
- - - - - - - - - - - - - - - - - -
ITFlow release version
Current DB Version
Current Code Commit
Current Branch
+
+ + + + + + + + + + + + + + + + + +
ITFlow release version
Current DB Version
Current Code Commit
Current Branch
+

System Information

@@ -552,200 +555,209 @@ $mysqli->close();

PHP Extensions and Configuration

- - - - - - - - - - - - - +
+
PHP Extensions
- - - - - -
+ + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - - - - - - - + + + + + + + + + + + + + - - - - - - - - - - - - - + + + + + + + + + + + + + - - - - - - - - - - - - - + + + + + + + + + + + + + - - + + + + + + + + + + - - - - - - - - - - - - + + + + - - -
PHP Extensions
PHP Configuration
- - - - - -
+ + + + + +
PHP Configuration
Shell Commands
- - - - - -
+ + + + + +
Shell Commands
SSL Checks
- - - - - -
+ + + + + +
SSL Checks
Domain Checks
- - - - - -
+ + + + + +
Domain Checks
+ + + + + +
File Permissions
- - - - - -
File Permissions
- +
+ + + + + +
+

Database Structure Comparison

- - - - +
+
+ + + + + + + + + - - + - - - - - - - -
No discrepancies found between the database and db.sql file.
No discrepancies found between the database and db.sql file.
+ + + +

Uploads Directory Stats

- - - - - - - - - -
+
+ + + + + + + + + +
+

Database Stats

- - - - - - - - - -
+
+ + + + + + + + + +
+

Table Stats

- - - - - - - - - - +
+
Table NameFields / RowsSize (MB)
+ - - - + + + - - -
Table NameFields / RowsSize (MB)
+ + + + + + + + + + + +
diff --git a/admin_settings_integration.php b/admin_identity_provider.php similarity index 59% rename from admin_settings_integration.php rename to admin_identity_provider.php index 408a61ef..2149815d 100644 --- a/admin_settings_integration.php +++ b/admin_identity_provider.php @@ -4,13 +4,29 @@ require_once "includes/inc_all_admin.php";
-

Integration Settings

+

Identity Providers

Client Portal SSO via Microsoft Entra

+ +
+ +
+
+ +
+ +
+
+
@@ -33,11 +49,10 @@ require_once "includes/inc_all_admin.php";
- +
- "> + text-nowrap"> + + + + + + + + + + + + +
diff --git a/admin_settings_company.php b/admin_settings_company.php index dceee89e..17ba0239 100644 --- a/admin_settings_company.php +++ b/admin_settings_company.php @@ -113,7 +113,7 @@ $company_initials = nullable_htmlentities(initials($company_name));
-
+
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NotificationApp NotifyTech Email NotifyClient Email NotifyCreate Ticket
Expirations
-
Domain Expiration Notice
- - (This setting triggers a notification when a domain is approaching its expiration date, specifically at 1, 7 and 45 days prior to expiry.) - -
-
- value="1"> - -
-
-
Certificate Expiration Notice
- - (This setting triggers a notification when a certificate is approaching its expiration date, specifically at 1, 7 and 45 days prior to expiry.) - -
-
-
Asset Warranty Expiration Notice
- - (This setting triggers a notification when an asset is approaching its expiration date, specifically at 1, 7 and 45 days prior to expiry.) - -
-
Billing
-
Invoice Reminders
- - (This will automatically dispatch a reminder email for the invoice to the primary contact's email every 30 days following the invoice's due date.) - -
- - -
- value="1" id="sendInvoiceRemindersSwitch"> - -
-
-
Send Recurring Invoice
- - (This will notify all primary and billing contacts of a client that a new invoice was generated from recurring invoices) - -
- - -
- value="1" id="sendRecurringSwitch"> - -
-
Operational
-
Send clients general notification emails
- (Should clients receive automatic emails when tickets are raised/closed?) -
-
- value="1" id="ticketNotificationSwitch"> - -
-
-
Shared Item View
- (Notify when Shared items are viewed) -
-
-
Cron Execution
- (Notify when the nightly cron job ran) -
-
-
ITFlow Updates
- (Notify when ITFlow has an update) -
-
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NotificationApp NotifyTech Email NotifyClient Email NotifyCreate Ticket
Expirations
+
Domain Expiration Notice
+ + (This setting triggers a notification when a domain is approaching its expiration date, specifically at 1, 7 and 45 days prior to expiry.) + +
+
+ value="1"> + +
+
+
Certificate Expiration Notice
+ + (This setting triggers a notification when a certificate is approaching its expiration date, specifically at 1, 7 and 45 days prior to expiry.) + +
+
+
Asset Warranty Expiration Notice
+ + (This setting triggers a notification when an asset is approaching its expiration date, specifically at 1, 7 and 45 days prior to expiry.) + +
+
Billing
+
Invoice Reminders
+ + (This will automatically dispatch a reminder email for the invoice to the primary contact's email every 30 days following the invoice's due date.) + +
+ + +
+ value="1" id="sendInvoiceRemindersSwitch"> + +
+
+
Send Recurring Invoice
+ + (This will notify all primary and billing contacts of a client that a new invoice was generated from recurring invoices) + +
+ + +
+ value="1" id="sendRecurringSwitch"> + +
+
Operational
+
Send clients general notification emails
+ (Should clients receive automatic emails when tickets are raised/closed?) +
+
+ value="1" id="ticketNotificationSwitch"> + +
+
+
Shared Item View
+ (Notify when Shared items are viewed) +
+
+
Cron Execution
+ (Notify when the nightly cron job ran) +
+
+
ITFlow Updates
+ (Notify when ITFlow has an update) +
+
+

diff --git a/admin_settings_online_payment_clients.php b/admin_settings_online_payment_clients.php index f160a2b1..c4deb42d 100644 --- a/admin_settings_online_payment_clients.php +++ b/admin_settings_online_payment_clients.php @@ -11,56 +11,63 @@ $stripe_clients_sql = mysqli_query($mysqli, "SELECT * FROM client_stripe LEFT JO
- - - - - - - - - - - - - - +
+
ClientStripe Customer IDStripe Payment IDAction
+ - - - - - + + + + + + + + - + -
- - ClientStripe Customer IDStripe Payment IDPayment DetailsCreatedAction
+ ?> + +
+ +
+
diff --git a/admin_ticket_status.php b/admin_ticket_status.php index 6c687a0e..8b98dcff 100644 --- a/admin_ticket_status.php +++ b/admin_ticket_status.php @@ -1,7 +1,7 @@ Active
"; } else { - $ticket_status_display = "
Disabled
"; + $ticket_status_display = "
Inactive
"; } ?> @@ -97,7 +97,6 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()")); - 5 ) { ?>
- diff --git a/admin_ticket_template_details.php b/admin_ticket_template_details.php index b8280aed..99b3ab78 100644 --- a/admin_ticket_template_details.php +++ b/admin_ticket_template_details.php @@ -20,7 +20,7 @@ $sql_ticket_templates = mysqli_query($mysqli, "SELECT * FROM ticket_templates WH $row = mysqli_fetch_array($sql_ticket_templates); $ticket_template_name = nullable_htmlentities($row['ticket_template_name']); -$ticket_template_description = nullable_htmlentities($row['ticket_template_description']); +//$ticket_template_description = nullable_htmlentities($row['ticket_template_description']); $ticket_template_subject = nullable_htmlentities($row['ticket_template_subject']); $ticket_template_details = $purifier->purify($row['ticket_template_details']); $ticket_template_created_at = nullable_htmlentities($row['ticket_template_created_at']); @@ -45,7 +45,7 @@ $sql_task_templates = mysqli_query($mysqli, "SELECT * FROM task_templates WHERE
-
+
@@ -54,7 +54,7 @@ $sql_task_templates = mysqli_query($mysqli, "SELECT * FROM task_templates WHERE

-
+
@@ -72,7 +72,7 @@ $sql_task_templates = mysqli_query($mysqli, "SELECT * FROM task_templates WHERE
-
+
@@ -82,11 +82,8 @@ $sql_task_templates = mysqli_query($mysqli, "SELECT * FROM task_templates WHERE
-
-
- -
- +
+
@@ -99,19 +96,18 @@ $sql_task_templates = mysqli_query($mysqli, "SELECT * FROM task_templates WHERE $task_id = intval($row['task_template_id']); $task_name = nullable_htmlentities($row['task_template_name']); $task_completion_estimate = intval($row['task_template_completion_estimate']); - $task_description = nullable_htmlentities($row['task_template_description']); + //$task_description = nullable_htmlentities($row['task_template_description']); ?> - - m - - + +
@@ -44,6 +45,16 @@ ob_start();
+
+ +
+
+ +
+ +
+
+
@@ -52,7 +63,7 @@ ob_start();
diff --git a/ajax/ajax_notifications.php b/ajax/ajax_notifications.php index dbb7e701..8652b8ad 100644 --- a/ajax/ajax_notifications.php +++ b/ajax/ajax_notifications.php @@ -25,46 +25,56 @@ ob_start();
@@ -1193,10 +1206,4 @@ require_once "modals/credential_add_modal.php"; require_once "modals/client_document_add_modal.php"; require_once "modals/client_file_upload_modal.php"; -require_once "modals/asset_link_software_modal.php"; -require_once "modals/asset_link_credential_modal.php"; -require_once "modals/asset_link_service_modal.php"; -require_once "modals/asset_link_document_modal.php"; -require_once "modals/asset_link_file_modal.php"; - require_once "includes/footer.php"; diff --git a/assets.php b/assets.php index ad5c230e..f6d0f1cb 100644 --- a/assets.php +++ b/assets.php @@ -205,7 +205,7 @@ if (mysqli_num_rows($os_sql) > 0) {
-
+
@@ -338,7 +338,7 @@ if (mysqli_num_rows($os_sql) > 0) {
- "> + text-nowrap"> + ">
-
+
" autofocus>
@@ -126,7 +126,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()")); ">Leads
-
@@ -519,7 +519,7 @@ if (mysqli_num_rows($os_sql) > 0) { if ($contact_name) { $contact_name_display = " $contact_name $contact_archive_display diff --git a/client/includes/header.php b/client/includes/header.php index f06abcdc..06a53e4c 100644 --- a/client/includes/header.php +++ b/client/includes/header.php @@ -56,6 +56,7 @@ header("X-Frame-Options: DENY"); // Legacy diff --git a/client/post.php b/client/post.php index ea329e89..03ec1773 100644 --- a/client/post.php +++ b/client/post.php @@ -38,7 +38,7 @@ if (isset($_POST['add_ticket'])) { $new_config_ticket_next_number = $config_ticket_next_number + 1; mysqli_query($mysqli, "UPDATE settings SET config_ticket_next_number = $new_config_ticket_next_number WHERE company_id = 1"); - mysqli_query($mysqli, "INSERT INTO tickets SET ticket_prefix = '$config_ticket_prefix', ticket_number = $ticket_number, ticket_category = $category, ticket_subject = '$subject', ticket_details = '$details', ticket_priority = '$priority', ticket_status = 1, ticket_billable = $config_ticket_default_billable, ticket_created_by = 0, ticket_contact_id = $session_contact_id, ticket_url_key = '$url_key', ticket_client_id = $session_client_id"); + mysqli_query($mysqli, "INSERT INTO tickets SET ticket_prefix = '$config_ticket_prefix', ticket_number = $ticket_number, ticket_source = 'Portal', ticket_category = $category, ticket_subject = '$subject', ticket_details = '$details', ticket_priority = '$priority', ticket_status = 1, ticket_billable = $config_ticket_default_billable, ticket_created_by = $session_user_id, ticket_contact_id = $session_contact_id, ticket_url_key = '$url_key', ticket_client_id = $session_client_id"); $ticket_id = mysqli_insert_id($mysqli); // Notify agent DL of the new ticket, if populated with a valid email @@ -424,7 +424,7 @@ if (isset($_POST['edit_contact'])) { logAction("Contact", "Edit", "Client contact $session_contact_name edited contact $contact_name in the client portal", $session_client_id, $contact_id); $_SESSION['alert_message'] = "Contact $contact_name updated"; - + header('Location: contacts.php'); customAction('contact_update', $contact_id); @@ -597,10 +597,22 @@ if (isset($_GET['stripe_save_card'])) { // Get some card/payment method details for the email/logging $payment_method_details = $stripe->paymentMethods->retrieve($payment_method); - $card_info = sanitizeInput($payment_method_details->card->display_brand) . " " . sanitizeInput($payment_method_details->card->last4); + $card_type = sanitizeInput($payment_method_details->card->brand); + $last4 = sanitizeInput($payment_method_details->card->last4); + $expiry_month = sanitizeInput($payment_method_details->card->exp_month); + $expiry_year = sanitizeInput($payment_method_details->card->exp_year); + + // Format the payment details string (Visa - 4324 | Exp 12/25) + $stripe_pm_details = "$card_type - $last4 | Exp $expiry_month/$expiry_year"; + + // Save the formatted payment details into stripe_pm_details + $update_query = " + UPDATE client_stripe + SET stripe_pm_details = '$stripe_pm_details' + WHERE client_id = $session_client_id LIMIT 1"; + mysqli_query($mysqli, $update_query); // Send email confirmation - // Company Details & Settings $sql_settings = mysqli_query($mysqli, "SELECT * FROM companies, settings WHERE companies.company_id = settings.company_id AND companies.company_id = 1"); $row = mysqli_fetch_array($sql_settings); @@ -617,7 +629,7 @@ if (isset($_GET['stripe_save_card'])) { if (!empty($config_smtp_host)) { $subject = "Payment method saved"; - $body = "Hello $session_contact_name,

We’re writing to confirm that your payment details have been securely stored with Stripe, our trusted payment processor.

By agreeing to save your payment information, you have authorized us to automatically bill your card ($card_info) for any future invoices. The payment details you’ve provided are securely stored with Stripe and will be used solely for invoices. We do not have access to your full card details.

You may update or remove your payment information at any time using the portal.

Thank you for your business!

--
$company_name - Billing Department
$config_invoice_from_email
$company_phone"; + $body = "Hello $session_contact_name,

We’re writing to confirm that your payment details have been securely stored with Stripe, our trusted payment processor.

By agreeing to save your payment information, you have authorized us to automatically bill your card ($stripe_pm_details) for any future invoices. The payment details you’ve provided are securely stored with Stripe and will be used solely for invoices. We do not have access to your full card details.

You may update or remove your payment information at any time using the portal.

Thank you for your business!

--
$company_name - Billing Department
$config_invoice_from_email
$company_phone"; $data = [ [ @@ -635,12 +647,11 @@ if (isset($_GET['stripe_save_card'])) { } // Logging - logAction("Stripe", "Update", "$session_contact_name saved payment method ($card_info) for future automatic payments (PM: $payment_method)", $session_client_id, $session_client_id); + logAction("Stripe", "Update", "$session_contact_name saved payment method ($stripe_pm_details) for future automatic payments (PM: $payment_method)", $session_client_id, $session_client_id); // Redirect $_SESSION['alert_message'] = "Payment method saved - thank you"; header('Location: autopay.php'); - } if (isset($_GET['stripe_remove_pm'])) { @@ -677,11 +688,65 @@ if (isset($_GET['stripe_remove_pm'])) { } // Remove payment method from ITFlow - mysqli_query($mysqli, "UPDATE client_stripe SET stripe_pm = NULL WHERE client_id = $session_client_id LIMIT 1"); - + mysqli_query($mysqli, "UPDATE client_stripe SET stripe_pm = NULL, stripe_pm_details = NULL WHERE client_id = $session_client_id LIMIT 1"); + + // Remove Auto Pay on recurring invoices that are stripe + $sql_recurring_invoices = mysqli_query($mysqli, "SELECT recurring_invoice_id FROM recurring_invoices WHERE recurring_invoice_client_id = $session_client_id"); + + while ($row = mysqli_fetch_array($sql_recurring_invoices)) { + $recurring_invoice_id = intval($row['recurring_invoice_id']); + mysqli_query($mysqli, "DELETE FROM recurring_payments WHERE recurring_payment_method = 'Stripe' AND recurring_payment_recurring_invoice_id = $recurring_invoice_id"); + } + // Logging & Redirect logAction("Stripe", "Update", "$session_contact_name deleted saved Stripe payment method (PM: $payment_method)", $session_client_id, $session_client_id); $_SESSION['alert_message'] = "Payment method removed"; header('Location: autopay.php'); -} \ No newline at end of file +} + +if (isset($_POST['add_recurring_payment'])) { + + $recurring_invoice_id = intval($_POST['recurring_invoice_id']); + + // Get Recurring Info for logging and alerting + $sql = mysqli_query($mysqli, "SELECT * FROM recurring_invoices WHERE recurring_invoice_id = $recurring_invoice_id"); + $row = mysqli_fetch_array($sql); + $recurring_invoice_prefix = sanitizeInput($row['recurring_invoice_prefix']); + $recurring_invoice_number = intval($row['recurring_invoice_number']); + $recurring_invoice_amount = floatval($row['recurring_invoice_amount']); + $recurring_invoice_currency_code = sanitizeInput($row['recurring_invoice_currency_code']); + + mysqli_query($mysqli,"INSERT INTO recurring_payments SET recurring_payment_currency_code = '$recurring_invoice_currency_code', recurring_payment_account_id = $config_stripe_account, recurring_payment_method = 'Stripe', recurring_payment_recurring_invoice_id = $recurring_invoice_id"); + + // Get Payment ID for reference + $recurring_payment_id = mysqli_insert_id($mysqli); + + // Logging + logAction("Recurring Invoice", "Auto Payment", "$session_name created Auto Pay for Recurring Invoice $recurring_invoice_prefix$recurring_invoice_number in the amount of " . numfmt_format_currency($currency_format, $recurring_invoice_amount, $recurring_invoice_currency_code), $session_client_id, $recurring_invoice_id); + + + $_SESSION['alert_message'] = "Automatic Payment enabled for Recurring Invoice $recurring_invoice_prefix$recurring_invoice_number"; + + header("Location: " . $_SERVER["HTTP_REFERER"]); +} + +if (isset($_POST['delete_recurring_payment'])) { + $recurring_invoice_id = intval($_POST['recurring_invoice_id']); + + // Get the invoice total and details + $sql = mysqli_query($mysqli,"SELECT * FROM recurring_invoices WHERE recurring_invoice_id = $recurring_invoice_id"); + $row = mysqli_fetch_array($sql); + $recurring_invoice_prefix = sanitizeInput($row['recurring_invoice_prefix']); + $recurring_invoice_number = intval($row['recurring_invoice_number']); + + mysqli_query($mysqli,"DELETE FROM recurring_payments WHERE recurring_payment_recurring_invoice_id = $recurring_invoice_id"); + + // Logging + logAction("Recurring Invoice", "Auto Payment", "$session_name removed auto Pay from Recurring Invoice $recurring_invoice_prefix$recurring_invoice_number", $session_client_id, $recurring_invoice_id); + + $_SESSION['alert_message'] = "Automatic Payment disabled for Recurring Invoice $recurring_invoice_prefix$recurring_invoice_number"; + + header("Location: " . $_SERVER["HTTP_REFERER"]); + +} diff --git a/client/recurring_invoices.php b/client/recurring_invoices.php new file mode 100644 index 00000000..6f0c15c1 --- /dev/null +++ b/client/recurring_invoices.php @@ -0,0 +1,120 @@ + + +

Recurring Invoices

+
+ +
+ + + + + + + + + + + + + + + + + + + "; + } else { + $auto_pay_display = " + + Create + + "; + //require "recurring_payment_add_modal.php"; + } + } + + if (empty($recurring_invoice_scope)) { + $recurring_invoice_scope_display = "-"; + } else { + $recurring_invoice_scope_display = $recurring_invoice_scope; + } + ?> + + + + + + + + + + + + + +
ScopeAmountNext Bill DateFrequencyAuto Pay
ly + +
+ + + + + + +
+ + Add Card Details First + +
+ +
+ +
+ + -
No device--
- "> + text-nowrap"> @@ -417,7 +421,7 @@ if (isset($_GET['invoice_id'])) { - > + >
@@ -779,11 +783,11 @@ require_once "includes/footer.php"; { columns: [ { - text: , + text: , style: 'invoiceBillingAddress' }, { - text: , + text: , style: 'invoiceBillingAddressClient' }, ] diff --git a/invoices.php b/invoices.php index 8f185f4c..b697ade7 100644 --- a/invoices.php +++ b/invoices.php @@ -172,7 +172,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
-
+
@@ -228,7 +228,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
@@ -286,7 +286,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()")); if (empty($location_address) && empty($location_city) && empty($location_state) && empty($location_zip)) { $location_address_display = "-"; } else { - $location_address_display = "$location_address
$location_city $location_state $location_zip"; + $location_address_display = "$location_address
$location_city $location_state $location_zip
$location_country"; } $contact_id = intval($row['contact_id']); $contact_name = nullable_htmlentities($row['contact_name']); diff --git a/contact_details.php b/contact_details.php index f77ff726..99047afb 100644 --- a/contact_details.php +++ b/contact_details.php @@ -294,31 +294,47 @@ if (isset($_GET['contact_id'])) {
@@ -1192,11 +1208,4 @@ require_once "modals/credential_add_modal.php"; require_once "modals/client_document_add_modal.php"; require_once "modals/client_file_upload_modal.php"; -require_once "modals/contact_link_asset_modal.php"; -require_once "modals/contact_link_software_modal.php"; -require_once "modals/contact_link_credential_modal.php"; -require_once "modals/contact_link_service_modal.php"; -require_once "modals/contact_link_document_modal.php"; -require_once "modals/contact_link_file_modal.php"; - require_once "includes/footer.php"; diff --git a/contacts.php b/contacts.php index 1ef1c912..9da09fe4 100644 --- a/contacts.php +++ b/contacts.php @@ -110,7 +110,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
-
+
@@ -165,7 +165,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
-
+
-
+
@@ -242,7 +242,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
- "> + text-nowrap"> - - + - + + + + + + 0) { ?> + + + + + + + +
@@ -335,7 +335,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()")); $credential_tags_display = implode('', $credential_tag_name_display_array); if ($credential_contact_id) { - $credential_contact_display = " @@ -412,11 +412,11 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
+ diff --git a/css/itflow_custom.css b/css/itflow_custom.css index 8b93a921..50b0d582 100644 --- a/css/itflow_custom.css +++ b/css/itflow_custom.css @@ -21,10 +21,9 @@ } .drag-handle { - cursor: grab; - touch-action: none; - user-select: none; + cursor: grab !important; } + .drag-handle:active { - cursor: grabbing; + cursor: grabbing !important; } \ No newline at end of file diff --git a/css/quote_dropdowns_fix.css b/css/quote_dropdowns_fix.css new file mode 100644 index 00000000..24e039f8 --- /dev/null +++ b/css/quote_dropdowns_fix.css @@ -0,0 +1,15 @@ +/*! + * AdminLTE 3.2.0 Specific Dropdown Fix + * Targets .fix-quote-dropdown only + * Prevents alignment bugs in split button dropdowns going too far left + * (ChatGPT) + */ + +.fix-quote-dropdown .dropdown-menu { + left: auto !important; + right: 0 !important; + top: calc(100% + 0.25rem) !important; + transform: none !important; + min-width: max-content; + z-index: 1050; +} diff --git a/dashboard.php b/dashboard.php index 58b854bf..5165f9e0 100644 --- a/dashboard.php +++ b/dashboard.php @@ -41,7 +41,7 @@ $sql_years_select = mysqli_query($mysqli, " - -
+
>
diff --git a/database_updates.php b/database_updates.php index 3a7a593e..e4fba32d 100644 --- a/database_updates.php +++ b/database_updates.php @@ -3457,10 +3457,19 @@ if (LATEST_DATABASE_VERSION > CURRENT_DATABASE_VERSION) { mysqli_query($mysqli, "UPDATE `settings` SET `config_current_database_version` = '2.1.3'"); } - // if (CURRENT_DATABASE_VERSION == '2.1.3') { - // // Insert queries here required to update to DB version 2.1.4 + if (CURRENT_DATABASE_VERSION == '2.1.3') { + mysqli_query($mysqli, "ALTER TABLE `client_stripe` ADD `stripe_pm_details` VARCHAR(200) DEFAULT NULL AFTER `stripe_pm`"); + mysqli_query($mysqli, "ALTER TABLE `client_stripe` ADD `stripe_pm_created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP AFTER `stripe_pm_details`"); + + mysqli_query($mysqli, "UPDATE `settings` SET `config_current_database_version` = '2.1.4'"); + } + + + + // if (CURRENT_DATABASE_VERSION == '2.1.4') { + // // Insert queries here required to update to DB version 2.1.5 // // Then, update the database to the next sequential version - // mysqli_query($mysqli, "UPDATE `settings` SET `config_current_database_version` = '2.1.4'"); + // mysqli_query($mysqli, "UPDATE `settings` SET `config_current_database_version` = '2.1.5'"); // } } else { diff --git a/db.sql b/db.sql index bfb82256..203f5771 100644 --- a/db.sql +++ b/db.sql @@ -482,6 +482,8 @@ CREATE TABLE `client_stripe` ( `client_id` int(11) NOT NULL, `stripe_id` varchar(255) NOT NULL, `stripe_pm` varchar(255) DEFAULT NULL, + `stripe_pm_details` varchar(200) DEFAULT NULL, + `stripe_pm_created_at` datetime NOT NULL DEFAULT current_timestamp(), PRIMARY KEY (`client_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; /*!40101 SET character_set_client = @saved_cs_client */; @@ -2498,4 +2500,4 @@ CREATE TABLE `vendors` ( /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; --- Dump completed on 2025-03-31 12:05:41 +-- Dump completed on 2025-05-24 13:23:21 diff --git a/domains.php b/domains.php index 4bd988a1..61e2e92e 100644 --- a/domains.php +++ b/domains.php @@ -92,7 +92,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
-
+
- "> + text-nowrap"> - - diff --git a/functions.php b/functions.php index 671acedf..1c1e9b6f 100644 --- a/functions.php +++ b/functions.php @@ -705,7 +705,17 @@ function sendSingleEmail($config_smtp_host, $config_smtp_username, $config_smtp_ $mail->SMTPAuth = $smtp_auth; // Enable SMTP authentication $mail->Username = $config_smtp_username; // SMTP username $mail->Password = $config_smtp_password; // SMTP password - $mail->SMTPSecure = $config_smtp_encryption; // Enable TLS encryption, `ssl` also accepted + if ($config_smtp_encryption == 'None') { + $mail->SMTPOptions = array( + 'ssl' => array( + 'verify_peer' => false, + 'verify_peer_name' => false, + )); + $mail->SMTPSecure = false; + $mail->SMTPAutoTLS = false; + } else { + $mail->SMTPSecure = $config_smtp_encryption; // Enable TLS encryption, `ssl` also accepted + } $mail->Port = $config_smtp_port; // TCP port to connect to //Recipients @@ -1487,7 +1497,7 @@ function appNotify($type, $details, $action = null, $client_id = 0, $entity_id = $sql = mysqli_query($mysqli, "SELECT user_id FROM users WHERE user_type = 1 AND user_status = 1 AND user_archived_at IS NULL "); - + while ($row = mysqli_fetch_array($sql)) { $user_id = intval($row['user_id']); @@ -1535,7 +1545,7 @@ function getFallback($data) { * @param int $id The record's id. * @param string $field The field (column) to retrieve. * @param string $escape_method The escape method: 'sql' (default, auto-detects int), 'html', 'json', or 'int'. - * + * * @return mixed The escaped field value, or null if not found or invalid input. */ function getFieldById($table, $id, $field, $escape_method = 'sql') { @@ -1631,7 +1641,7 @@ function display_folder_options($parent_folder_id, $client_id, $folder_location // Check if this folder is selected $selected = ''; - if ((isset($_GET['folder_id']) && intval($_GET['folder_id']) === $folder_id) || + if ((isset($_GET['folder_id']) && intval($_GET['folder_id']) === $folder_id) || (isset($_POST['folder']) && intval($_POST['folder']) === $folder_id)) { $selected = 'selected'; } @@ -1641,4 +1651,4 @@ function display_folder_options($parent_folder_id, $client_id, $folder_location // Recursively display subfolders display_folder_options($folder_id, $client_id, $folder_location, $indent + 1); } -} \ No newline at end of file +} diff --git a/guest/guest_ajax.php b/guest/guest_ajax.php index 8c35e4eb..7e73c451 100644 --- a/guest/guest_ajax.php +++ b/guest/guest_ajax.php @@ -97,9 +97,7 @@ if (isset($_GET['stripe_create_pi'])) { 'itflow_invoice_number' => $invoice_prefix . $invoice_number, 'itflow_invoice_id' => $invoice_id, ], - 'automatic_payment_methods' => [ - 'enabled' => true, - ], + 'payment_method_types' => ['card'], ]); $output = [ diff --git a/guest/guest_pay_invoice_stripe.php b/guest/guest_pay_invoice_stripe.php index c50758d6..4d6127bd 100644 --- a/guest/guest_pay_invoice_stripe.php +++ b/guest/guest_pay_invoice_stripe.php @@ -97,71 +97,85 @@ if (isset($_GET['invoice_id'], $_GET['url_key']) && !isset($_GET['payment_intent
-

Payment for Invoice:

-
-
-
diff --git a/expenses.php b/expenses.php index 6b330e12..9e1da9ec 100644 --- a/expenses.php +++ b/expenses.php @@ -228,19 +228,18 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()")); Date -
- - Vendor - - Category + / + + Description + - - Description + + Vendor @@ -313,9 +312,11 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()")); + + +
+
- - - - - - - - - +
+
+

Payment for Invoice:

+
+
+
ProductQtyTotal
+ - - - + + + + + + + $item_total = 0; + while ($row = mysqli_fetch_array($sql_invoice_items)) { + $item_name = nullable_htmlentities($row['item_name']); + $item_quantity = floatval($row['item_quantity']); + $item_total = floatval($row['item_total']); + ?> + + + + + - -
ProductQtyTotal
+ + 0) { ?> +
Discount + +
Paid + +
+
-
- 0){ echo "Discount: " . numfmt_format_currency($currency_format, $invoice_discount, $invoice_currency_code); } ?> - -
- 0) { ?> Already paid:
-

Payment Total:

-
-

- - - -
-
diff --git a/includes/get_settings.php b/includes/get_settings.php index d142b709..80e6fa7b 100644 --- a/includes/get_settings.php +++ b/includes/get_settings.php @@ -121,6 +121,8 @@ $config_log_retention = intval($row['config_log_retention']); // Locale $config_currency_format = "US_en"; $config_timezone = $row['config_timezone']; +$config_date_format = "Y-m-d"; +$config_time_format = "H:i"; // Theme $config_theme = $row['config_theme']; diff --git a/includes/inc_client_top_head.php b/includes/inc_client_top_head.php index e82f7e8c..208ee51e 100644 --- a/includes/inc_client_top_head.php +++ b/includes/inc_client_top_head.php @@ -59,7 +59,12 @@
@@ -307,6 +310,7 @@ if (isset($_GET['invoice_id'])) {
  • +
  • @@ -317,6 +321,7 @@ if (isset($_GET['invoice_id'])) {
  • +
  • @@ -380,31 +385,30 @@ if (isset($_GET['invoice_id'])) {
    -
    -
    - + + -
    -
    +
    - "> + text-nowrap"> +
    diff --git a/js/guest_pay_invoice_stripe.js b/js/guest_pay_invoice_stripe.js index ab8a60d5..d867a575 100644 --- a/js/guest_pay_invoice_stripe.js +++ b/js/guest_pay_invoice_stripe.js @@ -23,9 +23,6 @@ async function initialize() { elements = stripe.elements({ clientSecret }); - const linkAuthenticationElement = elements.create("linkAuthentication"); - linkAuthenticationElement.mount("#link-authentication-element"); - const paymentElementOptions = { layout: "tabs", }; diff --git a/locations.php b/locations.php index 87f8e73f..dbdf2dc3 100644 --- a/locations.php +++ b/locations.php @@ -47,7 +47,7 @@ $sql = mysqli_query( LEFT JOIN tags ON tags.tag_id = location_tags.tag_id WHERE location_$archive_query $tag_query - AND (location_name LIKE '%$q%' OR location_description LIKE '%$q%' OR location_address LIKE '%$q%' OR location_phone LIKE '%$phone_query%' OR tag_name LIKE '%$q%' OR client_name LIKE '%$q%') + AND (location_name LIKE '%$q%' OR location_description LIKE '%$q%' OR location_address LIKE '%$q%' OR location_city LIKE '%$q%' OR location_state LIKE '%$q%' OR location_zip LIKE '%$q%' OR location_country LIKE '%$q%' OR location_phone LIKE '%$phone_query%' OR tag_name LIKE '%$q%' OR client_name LIKE '%$q%') $access_permission_query $client_query GROUP BY location_id @@ -99,7 +99,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()")); " target="_blank">
    " target="_blank">
    $location_country"; ?>
    diff --git a/modals/asset_link_credential_modal.php b/modals/asset_link_credential_modal.php deleted file mode 100644 index 8a1ca42b..00000000 --- a/modals/asset_link_credential_modal.php +++ /dev/null @@ -1,51 +0,0 @@ - diff --git a/modals/asset_link_document_modal.php b/modals/asset_link_document_modal.php deleted file mode 100644 index 52c6eb5d..00000000 --- a/modals/asset_link_document_modal.php +++ /dev/null @@ -1,57 +0,0 @@ - diff --git a/modals/asset_link_file_modal.php b/modals/asset_link_file_modal.php deleted file mode 100644 index 3c8f7e1f..00000000 --- a/modals/asset_link_file_modal.php +++ /dev/null @@ -1,59 +0,0 @@ - diff --git a/modals/asset_link_service_modal.php b/modals/asset_link_service_modal.php deleted file mode 100644 index 89f55ece..00000000 --- a/modals/asset_link_service_modal.php +++ /dev/null @@ -1,56 +0,0 @@ - diff --git a/modals/asset_link_software_modal.php b/modals/asset_link_software_modal.php deleted file mode 100644 index 747d177e..00000000 --- a/modals/asset_link_software_modal.php +++ /dev/null @@ -1,58 +0,0 @@ - diff --git a/modals/client_file_upload_modal.php b/modals/client_file_upload_modal.php index f0bcc73a..02f1fd42 100644 --- a/modals/client_file_upload_modal.php +++ b/modals/client_file_upload_modal.php @@ -44,7 +44,7 @@
    - +
    Up to 20 files can be uploaded at once by holding down CTRL and selecting files diff --git a/modals/contact_link_asset_modal.php b/modals/contact_link_asset_modal.php deleted file mode 100644 index 2d957ad2..00000000 --- a/modals/contact_link_asset_modal.php +++ /dev/null @@ -1,51 +0,0 @@ - diff --git a/modals/contact_link_credential_modal.php b/modals/contact_link_credential_modal.php deleted file mode 100644 index fb5e41c0..00000000 --- a/modals/contact_link_credential_modal.php +++ /dev/null @@ -1,51 +0,0 @@ - diff --git a/modals/contact_link_document_modal.php b/modals/contact_link_document_modal.php deleted file mode 100644 index 26973df7..00000000 --- a/modals/contact_link_document_modal.php +++ /dev/null @@ -1,57 +0,0 @@ - diff --git a/modals/contact_link_file_modal.php b/modals/contact_link_file_modal.php deleted file mode 100644 index e8cf363f..00000000 --- a/modals/contact_link_file_modal.php +++ /dev/null @@ -1,59 +0,0 @@ - diff --git a/modals/contact_link_service_modal.php b/modals/contact_link_service_modal.php deleted file mode 100644 index a65ecffd..00000000 --- a/modals/contact_link_service_modal.php +++ /dev/null @@ -1,56 +0,0 @@ - diff --git a/modals/contact_link_software_modal.php b/modals/contact_link_software_modal.php deleted file mode 100644 index 0afc59d4..00000000 --- a/modals/contact_link_software_modal.php +++ /dev/null @@ -1,58 +0,0 @@ - diff --git a/modals/invoice_recurring_add_modal.php b/modals/invoice_recurring_add_modal.php index 463f4bd9..dead0ff5 100644 --- a/modals/invoice_recurring_add_modal.php +++ b/modals/invoice_recurring_add_modal.php @@ -27,7 +27,7 @@ diff --git a/modals/project_add_modal.php b/modals/project_add_modal.php index 90c047cd..8f2ba7de 100644 --- a/modals/project_add_modal.php +++ b/modals/project_add_modal.php @@ -88,25 +88,25 @@ -
    - -
    -
    - +
    + +
    +
    + +
    +
    -
    -
    diff --git a/modals/project_link_ticket_modal.php b/modals/project_link_ticket_modal.php index c50e287c..72ad05ee 100644 --- a/modals/project_link_ticket_modal.php +++ b/modals/project_link_ticket_modal.php @@ -2,7 +2,7 @@