From 68b15ca49b3aa757b60a48ac9bee152f7f1fe263 Mon Sep 17 00:00:00 2001 From: johnnyq Date: Fri, 28 Aug 2026 13:43:51 -0400 Subject: [PATCH] Email Invoice Only shows Billing and Primary, Quote shows Important Billing, Primary and Technical contacts that can be selected --- agent/invoice.php | 10 ++++----- agent/modals/invoice/invoice_email.php | 14 +++++++------ agent/modals/quote/quote_email.php | 22 +++++++++++++++----- agent/quote.php | 10 ++++----- functions/app.php | 28 ++++++++++++++++++++++++++ 5 files changed, 63 insertions(+), 21 deletions(-) diff --git a/agent/invoice.php b/agent/invoice.php index 74ee65681..96fee66ad 100644 --- a/agent/invoice.php +++ b/agent/invoice.php @@ -106,15 +106,15 @@ if (isset($_GET['invoice_id'])) { } $company_logo = escapeHtml($row['company_logo']); - // Send Email used to be gated on the PRIMARY contact having an email, which - // hid the button on a client whose only emailable contact was a billing or - // secondary one. The modal can send to any of them, so gate on whether the - // client has anybody reachable at all. + // Must use the same rule as the Send Email picker in + // modals/invoice/invoice_email.php, or the button offers a modal that then + // reports there is nobody to send to. $row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT(contact_id) AS emailable_contacts FROM contacts WHERE contact_client_id = $client_id AND contact_archived_at IS NULL AND contact_email IS NOT NULL - AND contact_email != ''")); + AND contact_email != '' + " . documentContactFilterSql('invoice'))); $emailable_contacts = intval($row['emailable_contacts']); $sql_history = mysqli_query($mysqli, "SELECT history_created_at, history_description, history_status FROM history WHERE history_invoice_id = $invoice_id ORDER BY history_id DESC"); diff --git a/agent/modals/invoice/invoice_email.php b/agent/modals/invoice/invoice_email.php index b7b908495..c4bbddc30 100644 --- a/agent/modals/invoice/invoice_email.php +++ b/agent/modals/invoice/invoice_email.php @@ -16,9 +16,10 @@ $client_id = intval($row['invoice_client_id']); enforceClientAccess(); -// Everyone at the client who can actually receive mail. Primary and billing -// contacts lead because they are the ones checked by default below - that -// pairing is the behaviour the old one-click Send Email link had baked in. +// Billing-facing contacts only - see documentContactFilterSql(). Everyone +// shown is also checked by default, which is the behaviour the old one-click +// Send Email link had baked in; the checkboxes are here to drop one, not to +// go hunting for somebody. $sql_contacts = mysqli_query( $mysqli, "SELECT contact_billing, contact_email, contact_id, contact_name, contact_primary, contact_title @@ -27,6 +28,7 @@ $sql_contacts = mysqli_query( AND contact_archived_at IS NULL AND contact_email IS NOT NULL AND contact_email != '' + " . documentContactFilterSql('invoice') . " ORDER BY contact_primary DESC, contact_billing DESC, contact_name ASC" ); @@ -51,9 +53,9 @@ ob_start();

- This client has no contacts with an email address, so there is nobody to send to. - Add a contact with an email address, or use Mark Sent to record that the invoice - went out some other way. + This client has no primary or billing contact with an email address, so there is + nobody to send an invoice to. Flag a contact as billing, or use Mark Sent to record + that the invoice went out some other way.

diff --git a/agent/modals/quote/quote_email.php b/agent/modals/quote/quote_email.php index 2a4cdd0ad..5863a3005 100644 --- a/agent/modals/quote/quote_email.php +++ b/agent/modals/quote/quote_email.php @@ -16,15 +16,19 @@ $client_id = intval($row['quote_client_id']); enforceClientAccess(); +// Wider than the invoice picker - see documentContactFilterSql(). A quote is +// read by whoever scoped the work as often as by whoever signs it. $sql_contacts = mysqli_query( $mysqli, - "SELECT contact_billing, contact_email, contact_id, contact_name, contact_primary, contact_title + "SELECT contact_billing, contact_email, contact_id, contact_important, contact_name, + contact_primary, contact_technical, contact_title FROM contacts WHERE contact_client_id = $client_id AND contact_archived_at IS NULL AND contact_email IS NOT NULL AND contact_email != '' - ORDER BY contact_primary DESC, contact_billing DESC, contact_name ASC" + " . documentContactFilterSql('quote') . " + ORDER BY contact_primary DESC, contact_billing DESC, contact_technical DESC, contact_name ASC" ); $contact_count = mysqli_num_rows($sql_contacts); @@ -48,9 +52,9 @@ ob_start();

- This client has no contacts with an email address, so there is nobody to send to. - Add a contact with an email address, or use Mark Sent to record that the quote - went out some other way. + This client has no primary, billing, technical or important contact with an email + address, so there is nobody to send a quote to. Flag a contact with one of those + roles, or use Mark Sent to record that the quote went out some other way.

@@ -68,6 +72,8 @@ ob_start(); $contact_title = escapeHtml($row['contact_title']); $contact_primary = intval($row['contact_primary']); $contact_billing = intval($row['contact_billing']); + $contact_technical = intval($row['contact_technical']); + $contact_important = intval($row['contact_important']); // Default selection reproduces the old link: quotes went to // the primary contact only. Billing contacts are listed and @@ -88,6 +94,12 @@ ob_start(); Billing + + Technical + + + Important + diff --git a/agent/quote.php b/agent/quote.php index 48b70952b..c19ec6a0a 100644 --- a/agent/quote.php +++ b/agent/quote.php @@ -99,15 +99,15 @@ if (isset($_GET['quote_id'])) { $company_website = escapeHtml($row['company_website']); $company_logo = escapeHtml($row['company_logo']); - // Send Email used to be gated on the PRIMARY contact having an email, which - // hid the button on a client whose only emailable contact was a billing or - // secondary one. The modal can send to any of them, so gate on whether the - // client has anybody reachable at all. + // Must use the same rule as the Send Email picker in + // modals/quote/quote_email.php, or the button offers a modal that then + // reports there is nobody to send to. $row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT(contact_id) AS emailable_contacts FROM contacts WHERE contact_client_id = $client_id AND contact_archived_at IS NULL AND contact_email IS NOT NULL - AND contact_email != ''")); + AND contact_email != '' + " . documentContactFilterSql('quote'))); $emailable_contacts = intval($row['emailable_contacts']); $sql_history = mysqli_query($mysqli, "SELECT history_created_at, history_description, history_status FROM history WHERE history_quote_id = $quote_id ORDER BY history_id DESC"); diff --git a/functions/app.php b/functions/app.php index 9362cd4cb..9d86e481a 100644 --- a/functions/app.php +++ b/functions/app.php @@ -786,6 +786,34 @@ function createiCalStrCancel($datetime, $title, $uid) { return $cal_event->export(); } +/* + * Which contacts a document's Send Email picker offers, as a SQL fragment. + * + * A client with fifty contacts should not hand the agent a fifty-row list to + * scroll, so the picker is narrowed to the people a given document type is + * actually addressed to: + * + * invoice - primary and billing. A bill goes to whoever pays it. + * quote - primary, billing, technical and important. A quote gets read by + * the person who scoped the work as often as the one who signs it. + * + * Lives here rather than inline in the modals because the Send Email button on + * agent/invoice.php and agent/quote.php gates on a COUNT using the same rule - + * if the two drift, the button appears and then the modal it opens reports + * there is nobody to send to. + * + * Note the post handlers deliberately do NOT re-apply this filter. It is a + * shortlist, not a permission boundary: any agent who can send a document can + * already set these flags on a contact. + */ +function documentContactFilterSql($document_type) { + if ($document_type === 'quote') { + return "AND (contact_primary = 1 OR contact_billing = 1 OR contact_technical = 1 OR contact_important = 1)"; + } + + return "AND (contact_primary = 1 OR contact_billing = 1)"; +} + /* * The delivery methods offered by the Mark Sent modal on invoices and quotes. *