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']); $company_logo = escapeHtml($row['company_logo']);
// Send Email used to be gated on the PRIMARY contact having an email, which // Must use the same rule as the Send Email picker in
// hid the button on a client whose only emailable contact was a billing or // modals/invoice/invoice_email.php, or the button offers a modal that then
// secondary one. The modal can send to any of them, so gate on whether the // reports there is nobody to send to.
// client has anybody reachable at all.
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT(contact_id) AS emailable_contacts FROM contacts $row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT(contact_id) AS emailable_contacts FROM contacts
WHERE contact_client_id = $client_id WHERE contact_client_id = $client_id
AND contact_archived_at IS NULL AND contact_archived_at IS NULL
AND contact_email IS NOT NULL AND contact_email IS NOT NULL
AND contact_email != ''")); AND contact_email != ''
" . documentContactFilterSql('invoice')));
$emailable_contacts = intval($row['emailable_contacts']); $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"); $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(); enforceClientAccess();
// Everyone at the client who can actually receive mail. Primary and billing // Billing-facing contacts only - see documentContactFilterSql(). Everyone
// contacts lead because they are the ones checked by default below - that // shown is also checked by default, which is the behaviour the old one-click
// pairing is the behaviour the old one-click Send Email link had baked in. // Send Email link had baked in; the checkboxes are here to drop one, not to
// go hunting for somebody.
$sql_contacts = mysqli_query( $sql_contacts = mysqli_query(
$mysqli, $mysqli,
"SELECT contact_billing, contact_email, contact_id, contact_name, contact_primary, contact_title "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_archived_at IS NULL
AND contact_email IS NOT NULL AND contact_email IS NOT NULL
AND contact_email != '' AND contact_email != ''
" . documentContactFilterSql('invoice') . "
ORDER BY contact_primary DESC, contact_billing DESC, contact_name ASC" ORDER BY contact_primary DESC, contact_billing DESC, contact_name ASC"
); );
@@ -51,9 +53,9 @@ ob_start();
<?php if ($contact_count == 0) { ?> <?php if ($contact_count == 0) { ?>
<p class="text-muted mb-0"> <p class="text-muted mb-0">
This client has no contacts with an email address, so there is nobody to send to. This client has no primary or billing contact with an email address, so there is
Add a contact with an email address, or use Mark Sent to record that the invoice nobody to send an invoice to. Flag a contact as billing, or use Mark Sent to record
went out some other way. that the invoice went out some other way.
</p> </p>
<?php } else { ?> <?php } else { ?>

View File

@@ -16,15 +16,19 @@ $client_id = intval($row['quote_client_id']);
enforceClientAccess(); 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( $sql_contacts = mysqli_query(
$mysqli, $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 FROM contacts
WHERE contact_client_id = $client_id WHERE contact_client_id = $client_id
AND contact_archived_at IS NULL AND contact_archived_at IS NULL
AND contact_email IS NOT NULL AND contact_email IS NOT NULL
AND contact_email != '' 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); $contact_count = mysqli_num_rows($sql_contacts);
@@ -48,9 +52,9 @@ ob_start();
<?php if ($contact_count == 0) { ?> <?php if ($contact_count == 0) { ?>
<p class="text-muted mb-0"> <p class="text-muted mb-0">
This client has no contacts with an email address, so there is nobody to send to. This client has no primary, billing, technical or important contact with an email
Add a contact with an email address, or use Mark Sent to record that the quote address, so there is nobody to send a quote to. Flag a contact with one of those
went out some other way. roles, or use Mark Sent to record that the quote went out some other way.
</p> </p>
<?php } else { ?> <?php } else { ?>
@@ -68,6 +72,8 @@ ob_start();
$contact_title = escapeHtml($row['contact_title']); $contact_title = escapeHtml($row['contact_title']);
$contact_primary = intval($row['contact_primary']); $contact_primary = intval($row['contact_primary']);
$contact_billing = intval($row['contact_billing']); $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 // Default selection reproduces the old link: quotes went to
// the primary contact only. Billing contacts are listed and // the primary contact only. Billing contacts are listed and
@@ -88,6 +94,12 @@ ob_start();
<?php if ($contact_billing == 1) { ?> <?php if ($contact_billing == 1) { ?>
<span class="badge text-bg-success ms-1">Billing</span> <span class="badge text-bg-success ms-1">Billing</span>
<?php } ?> <?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)) { ?> <?php if (!empty($contact_title)) { ?>
<span class="text-muted ms-1"><?= $contact_title ?></span> <span class="text-muted ms-1"><?= $contact_title ?></span>
<?php } ?> <?php } ?>

View File

@@ -99,15 +99,15 @@ if (isset($_GET['quote_id'])) {
$company_website = escapeHtml($row['company_website']); $company_website = escapeHtml($row['company_website']);
$company_logo = escapeHtml($row['company_logo']); $company_logo = escapeHtml($row['company_logo']);
// Send Email used to be gated on the PRIMARY contact having an email, which // Must use the same rule as the Send Email picker in
// hid the button on a client whose only emailable contact was a billing or // modals/quote/quote_email.php, or the button offers a modal that then
// secondary one. The modal can send to any of them, so gate on whether the // reports there is nobody to send to.
// client has anybody reachable at all.
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT(contact_id) AS emailable_contacts FROM contacts $row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT(contact_id) AS emailable_contacts FROM contacts
WHERE contact_client_id = $client_id WHERE contact_client_id = $client_id
AND contact_archived_at IS NULL AND contact_archived_at IS NULL
AND contact_email IS NOT NULL AND contact_email IS NOT NULL
AND contact_email != ''")); AND contact_email != ''
" . documentContactFilterSql('quote')));
$emailable_contacts = intval($row['emailable_contacts']); $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"); $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(); 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. * The delivery methods offered by the Mark Sent modal on invoices and quotes.
* *