From abd79c0aedaaa09e2a70d9e685f16061e03c911d Mon Sep 17 00:00:00 2001 From: johnnyq Date: Mon, 3 Aug 2026 15:23:50 -0400 Subject: [PATCH] Feature: Add Income Bulk Actions (Account, Payment Method and category --- agent/income.php | 48 +++ .../income/income_bulk_edit_account.php | 75 +++++ .../income/income_bulk_edit_category.php | 119 +++++++ .../modals/income/income_bulk_edit_method.php | 58 ++++ agent/post/income.php | 298 ++++++++++++++++++ agent/post/income_model.php | 36 +++ 6 files changed, 634 insertions(+) create mode 100644 agent/modals/income/income_bulk_edit_account.php create mode 100644 agent/modals/income/income_bulk_edit_category.php create mode 100644 agent/modals/income/income_bulk_edit_method.php create mode 100644 agent/post/income_model.php diff --git a/agent/income.php b/agent/income.php index 95c5b4b0..e74f044f 100644 --- a/agent/income.php +++ b/agent/income.php @@ -278,6 +278,38 @@ $summary_total_income = floatval($row['total_income']); + = 3 && lookupUserPermission("module_financial") >= 3) { ?> +
+
+
+ +
+
+
+
" id="advancedFilter">
@@ -342,6 +374,13 @@ $summary_total_income = floatval($row['total_income']); text-nowrap"> + = 3 && lookupUserPermission("module_financial") >= 3) { ?> + + + = 3 && lookupUserPermission("module_financial") >= 3) { ?> + +
+
+ +
+
Date @@ -435,6 +474,13 @@ $summary_total_income = floatval($row['total_income']); ?>
+
+ +
+
+ + + + +
+ + + + +
+ + + + +
+ + + + +
+ + + + +
+ + + + +
+ +$account_name to $updated_count income record(s)"); + } else { + flashAlert("No income records were updated", 'error'); + } + + redirect(); + +} + +if (isset($_POST['bulk_edit_income_category'])) { + + validateCSRFToken(); + + enforceUserPermission('module_sales', 3); + enforceUserPermission('module_financial', 3); + + require_once 'income_model.php'; + + $category_id = intval($_POST['bulk_category_id']); + + // Get Category name for logging and Notification - and confirm it is a live Income category + $sql_category = mysqli_query($mysqli, "SELECT category_name FROM categories WHERE category_id = $category_id AND category_type = 'Income' AND category_archived_at IS NULL LIMIT 1"); + $row = mysqli_fetch_assoc($sql_category); + + if (!$row || !$income_count) { + flashAlert("Nothing to update", 'error'); + redirect(); + } + + $category_name = escapeSql($row['category_name']); + + $revenue_updated_count = 0; + $invoice_updated_count = 0; + $skipped_count = 0; + + // Revenues carry their own category + foreach ($revenue_ids as $revenue_id) { + + $sql = mysqli_query($mysqli, "SELECT revenue_description, revenue_client_id FROM revenues WHERE revenue_id = $revenue_id AND revenue_archived_at IS NULL"); + $row = mysqli_fetch_assoc($sql); + + if (!$row) { + $skipped_count++; + continue; + } + + $revenue_description = escapeSql($row['revenue_description']); + $client_id = intval($row['revenue_client_id']); + + if ($client_id) { + enforceClientAccess($client_id); + } + + mysqli_query($mysqli, "UPDATE revenues SET revenue_category_id = $category_id WHERE revenue_id = $revenue_id"); + + logAudit("Revenue", "Edit", "$session_name assigned revenue $revenue_description to category $category_name", $client_id, $revenue_id); + + $revenue_updated_count++; + + } + + // A payment has no category of its own - it inherits the one on the invoice it was paid + // against, so this writes to the INVOICE. Two selected payments against the same invoice + // therefore collapse into a single invoice update, and a payment with no invoice is skipped. + $invoice_ids = []; + + foreach ($payment_ids as $payment_id) { + + $sql = mysqli_query($mysqli, "SELECT payment_invoice_id FROM payments WHERE payment_id = $payment_id AND payment_archived_at IS NULL"); + $row = mysqli_fetch_assoc($sql); + $payment_invoice_id = intval($row['payment_invoice_id'] ?? 0); + + if ($payment_invoice_id) { + $invoice_ids[$payment_invoice_id] = $payment_invoice_id; + } else { + $skipped_count++; + } + + } + + foreach ($invoice_ids as $invoice_id) { + + $sql = mysqli_query($mysqli, "SELECT invoice_prefix, invoice_number, invoice_client_id FROM invoices WHERE invoice_id = $invoice_id"); + $row = mysqli_fetch_assoc($sql); + + if (!$row) { + $skipped_count++; + continue; + } + + $invoice_prefix = escapeSql($row['invoice_prefix']); + $invoice_number = intval($row['invoice_number']); + $client_id = intval($row['invoice_client_id']); + + enforceClientAccess($client_id); + + mysqli_query($mysqli, "UPDATE invoices SET invoice_category_id = $category_id WHERE invoice_id = $invoice_id"); + + logAudit("Invoice", "Edit", "$session_name assigned invoice $invoice_prefix$invoice_number to category $category_name", $client_id, $invoice_id); + + $invoice_updated_count++; + + } + + // Spell out the invoice leg - the user selected payments, not invoices + $updated_summary = []; + if ($revenue_updated_count) { + $updated_summary[] = "$revenue_updated_count revenue(s)"; + } + if ($invoice_updated_count) { + $updated_summary[] = "$invoice_updated_count invoice(s) behind the selected payment(s)"; + } + + if ($updated_summary) { + + logAudit("Income", "Bulk Edit", "$session_name assigned category $category_name to $revenue_updated_count revenue(s) and $invoice_updated_count invoice(s)"); + + $skipped_note = ''; + if ($skipped_count) { + $skipped_note = " - $skipped_count record(s) skipped"; + } + + flashAlert("You assigned category $category_name to " . implode(' and ', $updated_summary) . $skipped_note); + + } else { + flashAlert("No income records were categorised - a payment can only take a category from the invoice it was paid against", 'error'); + } + + redirect(); + +} + +if (isset($_POST['bulk_edit_income_method'])) { + + validateCSRFToken(); + + enforceUserPermission('module_sales', 3); + enforceUserPermission('module_financial', 3); + + require_once 'income_model.php'; + + // The method is stored by name on both tables, so validate it against the lookup list + $payment_method = escapeSql($_POST['bulk_payment_method']); + + $sql_payment_method = mysqli_query($mysqli, "SELECT payment_method_name FROM payment_methods WHERE payment_method_name = '$payment_method' LIMIT 1"); + $row = mysqli_fetch_assoc($sql_payment_method); + + if (!$row || !$income_count) { + flashAlert("Nothing to update", 'error'); + redirect(); + } + + $payment_method = escapeSql($row['payment_method_name']); + + $updated_count = 0; + + // Payments - client comes from the invoice the payment was made against + foreach ($payment_ids as $payment_id) { + + $sql = mysqli_query($mysqli, "SELECT payment_reference, invoice_client_id FROM payments LEFT JOIN invoices ON payment_invoice_id = invoice_id WHERE payment_id = $payment_id AND payment_archived_at IS NULL"); + $row = mysqli_fetch_assoc($sql); + + if (!$row) { + continue; + } + + $payment_reference = escapeSql($row['payment_reference']); + $client_id = intval($row['invoice_client_id']); + + if ($client_id) { + enforceClientAccess($client_id); + } + + mysqli_query($mysqli, "UPDATE payments SET payment_method = '$payment_method' WHERE payment_id = $payment_id"); + + logAudit("Payment", "Edit", "$session_name set payment $payment_reference to payment method $payment_method", $client_id, $payment_id); + + $updated_count++; + + } + + // Revenues + foreach ($revenue_ids as $revenue_id) { + + $sql = mysqli_query($mysqli, "SELECT revenue_description, revenue_client_id FROM revenues WHERE revenue_id = $revenue_id AND revenue_archived_at IS NULL"); + $row = mysqli_fetch_assoc($sql); + + if (!$row) { + continue; + } + + $revenue_description = escapeSql($row['revenue_description']); + $client_id = intval($row['revenue_client_id']); + + if ($client_id) { + enforceClientAccess($client_id); + } + + mysqli_query($mysqli, "UPDATE revenues SET revenue_payment_method = '$payment_method' WHERE revenue_id = $revenue_id"); + + logAudit("Revenue", "Edit", "$session_name set revenue $revenue_description to payment method $payment_method", $client_id, $revenue_id); + + $updated_count++; + + } + + if ($updated_count) { + logAudit("Income", "Bulk Edit", "$session_name set $updated_count income record(s) to payment method $payment_method"); + flashAlert("You set payment method $payment_method on $updated_count income record(s)"); + } else { + flashAlert("No income records were updated", 'error'); + } + + redirect(); + +} + if (isset($_POST['export_income'])) { validateCSRFToken(); diff --git a/agent/post/income_model.php b/agent/post/income_model.php new file mode 100644 index 00000000..2e094482 --- /dev/null +++ b/agent/post/income_model.php @@ -0,0 +1,36 @@ +