Email Invoice Only shows Billing and Primary, Quote shows Important Billing, Primary and Technical contacts that can be selected

This commit is contained in:
johnnyq
2026-08-28 13:43:51 -04:00
parent 72e6f23e19
commit 68b15ca49b
5 changed files with 63 additions and 21 deletions

View File

@@ -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");

View File

@@ -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();
<?php if ($contact_count == 0) { ?>
<p class="text-muted mb-0">
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.
</p>
<?php } else { ?>

View File

@@ -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();
<?php if ($contact_count == 0) { ?>
<p class="text-muted mb-0">
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.
</p>
<?php } else { ?>
@@ -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();
<?php if ($contact_billing == 1) { ?>
<span class="badge text-bg-success ms-1">Billing</span>
<?php } ?>
<?php if ($contact_technical == 1) { ?>
<span class="badge text-bg-info ms-1">Technical</span>
<?php } ?>
<?php if ($contact_important == 1) { ?>
<span class="badge text-bg-warning ms-1">Important</span>
<?php } ?>
<?php if (!empty($contact_title)) { ?>
<span class="text-muted ms-1"><?= $contact_title ?></span>
<?php } ?>

View File

@@ -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");

View File

@@ -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.
*