mirror of
https://github.com/itflow-org/itflow
synced 2026-08-31 11:55:11 +00:00
Add Quick Send Quote / Invoice to replicate the old single click to Send Email
This commit is contained in:
@@ -246,9 +246,13 @@ if (isset($_GET['invoice_id'])) {
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
<?php if (!empty($config_smtp_provider) && $emailable_contacts > 0) { ?>
|
||||
<button type="submit" class="dropdown-item" form="quickSendInvoice"
|
||||
name="invoice_id" value="<?= $invoice_id ?>">
|
||||
<i class="fas fa-fw fa-bolt me-2"></i>Quick Send
|
||||
</button>
|
||||
<a class="dropdown-item ajax-modal" href="#"
|
||||
data-modal-url="modals/invoice/invoice_email.php?invoice_id=<?= $invoice_id ?>">
|
||||
<i class="fas fa-fw fa-paper-plane me-2"></i>Send Email
|
||||
<i class="fas fa-fw fa-paper-plane me-2"></i>Send Email<span class="text-muted">...</span>
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<?php } ?>
|
||||
@@ -314,9 +318,13 @@ if (isset($_GET['invoice_id'])) {
|
||||
<i class="fa fa-fw fa-box-open text-secondary me-2"></i>Packing Slip
|
||||
</a>
|
||||
<?php if (!empty($config_smtp_provider) && $emailable_contacts > 0) { ?>
|
||||
<button type="submit" class="dropdown-item" form="quickSendInvoice"
|
||||
name="invoice_id" value="<?= $invoice_id ?>">
|
||||
<i class="fa fa-fw fa-bolt text-secondary me-2"></i>Quick Send
|
||||
</button>
|
||||
<a class="dropdown-item ajax-modal" href="#"
|
||||
data-modal-url="modals/invoice/invoice_email.php?invoice_id=<?= $invoice_id ?>">
|
||||
<i class="fa fa-fw fa-paper-plane text-secondary me-2"></i>Send Email
|
||||
<i class="fa fa-fw fa-paper-plane text-secondary me-2"></i>Send Email<span class="text-muted">...</span>
|
||||
</a>
|
||||
<?php } ?>
|
||||
<a class="dropdown-item clipboardjs" href="#" data-clipboard-text="https://<?= $config_base_url ?>/guest/guest_view_invoice.php?invoice_id=<?= "$invoice_id&url_key=$invoice_url_key" ?>">
|
||||
@@ -758,6 +766,25 @@ if (isset($_GET['invoice_id'])) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (lookupUserPermission("module_sales") >= 2 && !empty($config_smtp_provider) && $emailable_contacts > 0) { ?>
|
||||
<?php
|
||||
/*
|
||||
* One hidden form for the page, targeted by the Quick Send buttons via
|
||||
* their form="" attribute, so a button can sit inside a dropdown
|
||||
* without needing a form of its own. The button carries the id as its
|
||||
* own name/value, which a submit button contributes to the submission.
|
||||
*
|
||||
* Must stay inside this block - $emailable_contacts is only set on the
|
||||
* path where the document was found.
|
||||
*/
|
||||
?>
|
||||
<form id="quickSendInvoice" action="post.php" method="post" class="d-none">
|
||||
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
||||
<input type="hidden" name="email_invoice" value="1">
|
||||
<input type="hidden" name="quick_send" value="1">
|
||||
</form>
|
||||
<?php } ?>
|
||||
<?php
|
||||
include_once "modals/invoice/invoice_add_ticket.php";
|
||||
|
||||
|
||||
@@ -434,9 +434,13 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<?php if (!empty($config_smtp_provider)) { ?>
|
||||
<button type="submit" class="dropdown-item" form="quickSendInvoice"
|
||||
name="invoice_id" value="<?= $invoice_id ?>">
|
||||
<i class="fas fa-fw fa-bolt me-2"></i>Quick Send
|
||||
</button>
|
||||
<a class="dropdown-item ajax-modal" href="#"
|
||||
data-modal-url="modals/invoice/invoice_email.php?invoice_id=<?= $invoice_id ?>">
|
||||
<i class="fas fa-fw fa-paper-plane me-2"></i>Send Email
|
||||
<i class="fas fa-fw fa-paper-plane me-2"></i>Send Email<span class="text-muted">...</span>
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<?php } ?>
|
||||
@@ -468,6 +472,24 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
|
||||
<?php require_once "../includes/filter_footer.php"; ?>
|
||||
</div>
|
||||
|
||||
<?php if (lookupUserPermission("module_sales") >= 2 && !empty($config_smtp_provider)) { ?>
|
||||
<?php
|
||||
/*
|
||||
* One hidden form for the whole page, targeted by the Quick Send buttons via
|
||||
* their form="" attribute. It cannot be a form per button: agent/invoices.php
|
||||
* wraps its table in a bulkActions form, and a nested form is invalid
|
||||
* HTML - the browser drops the inner one and the click silently submits the
|
||||
* bulk action instead. The button carries the id as its own name/value, which
|
||||
* a submit button contributes to the submission.
|
||||
*/
|
||||
?>
|
||||
<form id="quickSendInvoice" action="post.php" method="post" class="d-none">
|
||||
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
||||
<input type="hidden" name="email_invoice" value="1">
|
||||
<input type="hidden" name="quick_send" value="1">
|
||||
</form>
|
||||
<?php } ?>
|
||||
|
||||
<script src="../js/bulk_actions.js"></script>
|
||||
|
||||
<?php
|
||||
|
||||
@@ -34,6 +34,22 @@ $sql_contacts = mysqli_query(
|
||||
|
||||
$contact_count = mysqli_num_rows($sql_contacts);
|
||||
|
||||
// Which boxes open ticked. Read from the same helper Quick Send uses, so the
|
||||
// modal and the one-click path can never disagree about who "the defaults" are.
|
||||
$default_contact_ids = [];
|
||||
$sql_defaults = mysqli_query(
|
||||
$mysqli,
|
||||
"SELECT contact_id FROM contacts
|
||||
WHERE contact_client_id = $client_id
|
||||
AND contact_archived_at IS NULL
|
||||
AND contact_email IS NOT NULL
|
||||
AND contact_email != ''
|
||||
" . documentDefaultContactFilterSql('invoice')
|
||||
);
|
||||
while ($row = mysqli_fetch_assoc($sql_defaults)) {
|
||||
$default_contact_ids[] = intval($row['contact_id']);
|
||||
}
|
||||
|
||||
ob_start();
|
||||
|
||||
?>
|
||||
@@ -74,10 +90,7 @@ ob_start();
|
||||
$contact_primary = intval($row['contact_primary']);
|
||||
$contact_billing = intval($row['contact_billing']);
|
||||
|
||||
// Default selection reproduces the old link exactly: the
|
||||
// primary contact was the recipient and every billing
|
||||
// contact got a copy
|
||||
$contact_checked = ($contact_primary == 1 || $contact_billing == 1);
|
||||
$contact_checked = in_array($contact_id, $default_contact_ids, true);
|
||||
|
||||
?>
|
||||
|
||||
|
||||
@@ -33,6 +33,22 @@ $sql_contacts = mysqli_query(
|
||||
|
||||
$contact_count = mysqli_num_rows($sql_contacts);
|
||||
|
||||
// Which boxes open ticked. Read from the same helper Quick Send uses, so the
|
||||
// modal and the one-click path can never disagree about who "the defaults" are.
|
||||
$default_contact_ids = [];
|
||||
$sql_defaults = mysqli_query(
|
||||
$mysqli,
|
||||
"SELECT contact_id FROM contacts
|
||||
WHERE contact_client_id = $client_id
|
||||
AND contact_archived_at IS NULL
|
||||
AND contact_email IS NOT NULL
|
||||
AND contact_email != ''
|
||||
" . documentDefaultContactFilterSql('quote')
|
||||
);
|
||||
while ($row = mysqli_fetch_assoc($sql_defaults)) {
|
||||
$default_contact_ids[] = intval($row['contact_id']);
|
||||
}
|
||||
|
||||
ob_start();
|
||||
|
||||
?>
|
||||
@@ -75,11 +91,7 @@ ob_start();
|
||||
$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
|
||||
// one click away, but stay unchecked - a quote is a sales
|
||||
// conversation, not a bill
|
||||
$contact_checked = ($contact_primary == 1);
|
||||
$contact_checked = in_array($contact_id, $default_contact_ids, true);
|
||||
|
||||
?>
|
||||
|
||||
|
||||
@@ -544,35 +544,49 @@ if (isset($_POST['email_invoice'])) {
|
||||
|
||||
enforceClientAccess();
|
||||
|
||||
// Recipients come from the Send Email modal's contact picker. Scoping the
|
||||
// lookup to this invoice's client is what makes a tampered contact_id
|
||||
// harmless - it simply matches nothing.
|
||||
$selected_contacts = $_POST['contacts'] ?? [];
|
||||
if (!is_array($selected_contacts)) {
|
||||
$selected_contacts = [];
|
||||
}
|
||||
$selected_contact_ids = array_filter(array_unique(array_map('intval', $selected_contacts)));
|
||||
// Two ways in. Quick Send skips the modal and resolves the default
|
||||
// recipients server-side; the modal posts an explicit contacts[] list.
|
||||
// Scoping either lookup to this invoice's client is what makes a tampered
|
||||
// contact_id harmless - it simply matches nothing.
|
||||
if (!empty($_POST['quick_send'])) {
|
||||
|
||||
if (empty($selected_contact_ids)) {
|
||||
flashAlert("Select at least one contact to send to", 'error');
|
||||
redirect();
|
||||
}
|
||||
$recipient_filter = documentDefaultContactFilterSql('invoice');
|
||||
|
||||
$selected_contact_id_list = implode(',', $selected_contact_ids);
|
||||
} else {
|
||||
|
||||
$selected_contacts = $_POST['contacts'] ?? [];
|
||||
if (!is_array($selected_contacts)) {
|
||||
$selected_contacts = [];
|
||||
}
|
||||
$selected_contact_ids = array_filter(array_unique(array_map('intval', $selected_contacts)));
|
||||
|
||||
if (empty($selected_contact_ids)) {
|
||||
flashAlert("Select at least one contact to send to", 'error');
|
||||
redirect();
|
||||
}
|
||||
|
||||
$selected_contact_id_list = implode(',', $selected_contact_ids);
|
||||
$recipient_filter = "AND contact_id IN ($selected_contact_id_list)";
|
||||
|
||||
}
|
||||
|
||||
$sql_recipients = mysqli_query(
|
||||
$mysqli,
|
||||
"SELECT contact_email, contact_name FROM contacts
|
||||
WHERE contact_id IN ($selected_contact_id_list)
|
||||
AND contact_client_id = $client_id
|
||||
WHERE contact_client_id = $client_id
|
||||
AND contact_archived_at IS NULL
|
||||
AND contact_email IS NOT NULL
|
||||
AND contact_email != ''
|
||||
$recipient_filter
|
||||
ORDER BY contact_primary DESC, contact_billing DESC, contact_name ASC"
|
||||
);
|
||||
|
||||
if (mysqli_num_rows($sql_recipients) == 0) {
|
||||
flashAlert("None of the selected contacts have a usable email address", 'error');
|
||||
if (!empty($_POST['quick_send'])) {
|
||||
flashAlert("No default contacts to quick send to - use Send Email to choose recipients", 'error');
|
||||
} else {
|
||||
flashAlert("None of the selected contacts have a usable email address", 'error');
|
||||
}
|
||||
redirect();
|
||||
}
|
||||
|
||||
|
||||
@@ -596,35 +596,49 @@ if (isset($_POST['email_quote'])) {
|
||||
|
||||
enforceClientAccess();
|
||||
|
||||
// Recipients come from the Send Email modal's contact picker. Scoping the
|
||||
// lookup to this quote's client is what makes a tampered contact_id
|
||||
// harmless - it simply matches nothing.
|
||||
$selected_contacts = $_POST['contacts'] ?? [];
|
||||
if (!is_array($selected_contacts)) {
|
||||
$selected_contacts = [];
|
||||
}
|
||||
$selected_contact_ids = array_filter(array_unique(array_map('intval', $selected_contacts)));
|
||||
// Two ways in. Quick Send skips the modal and resolves the default
|
||||
// recipients server-side; the modal posts an explicit contacts[] list.
|
||||
// Scoping either lookup to this quote's client is what makes a tampered
|
||||
// contact_id harmless - it simply matches nothing.
|
||||
if (!empty($_POST['quick_send'])) {
|
||||
|
||||
if (empty($selected_contact_ids)) {
|
||||
flashAlert("Select at least one contact to send to", 'error');
|
||||
redirect();
|
||||
}
|
||||
$recipient_filter = documentDefaultContactFilterSql('quote');
|
||||
|
||||
$selected_contact_id_list = implode(',', $selected_contact_ids);
|
||||
} else {
|
||||
|
||||
$selected_contacts = $_POST['contacts'] ?? [];
|
||||
if (!is_array($selected_contacts)) {
|
||||
$selected_contacts = [];
|
||||
}
|
||||
$selected_contact_ids = array_filter(array_unique(array_map('intval', $selected_contacts)));
|
||||
|
||||
if (empty($selected_contact_ids)) {
|
||||
flashAlert("Select at least one contact to send to", 'error');
|
||||
redirect();
|
||||
}
|
||||
|
||||
$selected_contact_id_list = implode(',', $selected_contact_ids);
|
||||
$recipient_filter = "AND contact_id IN ($selected_contact_id_list)";
|
||||
|
||||
}
|
||||
|
||||
$sql_recipients = mysqli_query(
|
||||
$mysqli,
|
||||
"SELECT contact_email, contact_name FROM contacts
|
||||
WHERE contact_id IN ($selected_contact_id_list)
|
||||
AND contact_client_id = $client_id
|
||||
WHERE contact_client_id = $client_id
|
||||
AND contact_archived_at IS NULL
|
||||
AND contact_email IS NOT NULL
|
||||
AND contact_email != ''
|
||||
$recipient_filter
|
||||
ORDER BY contact_primary DESC, contact_billing DESC, contact_name ASC"
|
||||
);
|
||||
|
||||
if (mysqli_num_rows($sql_recipients) == 0) {
|
||||
flashAlert("None of the selected contacts have a usable email address", 'error');
|
||||
if (!empty($_POST['quick_send'])) {
|
||||
flashAlert("No default contacts to quick send to - use Send Email to choose recipients", 'error');
|
||||
} else {
|
||||
flashAlert("None of the selected contacts have a usable email address", 'error');
|
||||
}
|
||||
redirect();
|
||||
}
|
||||
|
||||
|
||||
@@ -168,9 +168,13 @@ if (isset($_GET['quote_id'])) {
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
<?php if (!empty($config_smtp_provider) && $emailable_contacts > 0) { ?>
|
||||
<button type="submit" class="dropdown-item" form="quickSendQuote"
|
||||
name="quote_id" value="<?= $quote_id ?>">
|
||||
<i class="fas fa-fw fa-bolt me-2"></i>Quick Send
|
||||
</button>
|
||||
<a class="dropdown-item ajax-modal" href="#"
|
||||
data-modal-url="modals/quote/quote_email.php?quote_id=<?= $quote_id ?>">
|
||||
<i class="fas fa-fw fa-paper-plane me-2"></i>Send Email
|
||||
<i class="fas fa-fw fa-paper-plane me-2"></i>Send Email<span class="text-muted">...</span>
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<?php } ?>
|
||||
@@ -233,9 +237,13 @@ if (isset($_GET['quote_id'])) {
|
||||
<i class="fa fa-fw fa-download text-secondary me-2"></i>Download PDF
|
||||
</a>
|
||||
<?php if (!empty($config_smtp_provider) && $emailable_contacts > 0) { ?>
|
||||
<button type="submit" class="dropdown-item" form="quickSendQuote"
|
||||
name="quote_id" value="<?= $quote_id ?>">
|
||||
<i class="fa fa-fw fa-bolt text-secondary me-2"></i>Quick Send
|
||||
</button>
|
||||
<a class="dropdown-item ajax-modal" href="#"
|
||||
data-modal-url="modals/quote/quote_email.php?quote_id=<?= $quote_id ?>">
|
||||
<i class="fa fa-fw fa-paper-plane text-secondary me-2"></i>Send Email
|
||||
<i class="fa fa-fw fa-paper-plane text-secondary me-2"></i>Send Email<span class="text-muted">...</span>
|
||||
</a>
|
||||
<?php } ?>
|
||||
<a class="dropdown-item clipboardjs" href="#" data-clipboard-text="https://<?= $config_base_url ?>/guest/guest_view_quote.php?quote_id=<?= "$quote_id&url_key=$quote_url_key" ?>">
|
||||
@@ -598,6 +606,25 @@ if (isset($_GET['quote_id'])) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (lookupUserPermission("module_sales") >= 2 && !empty($config_smtp_provider) && $emailable_contacts > 0) { ?>
|
||||
<?php
|
||||
/*
|
||||
* One hidden form for the page, targeted by the Quick Send buttons via
|
||||
* their form="" attribute, so a button can sit inside a dropdown
|
||||
* without needing a form of its own. The button carries the id as its
|
||||
* own name/value, which a submit button contributes to the submission.
|
||||
*
|
||||
* Must stay inside this block - $emailable_contacts is only set on the
|
||||
* path where the document was found.
|
||||
*/
|
||||
?>
|
||||
<form id="quickSendQuote" action="post.php" method="post" class="d-none">
|
||||
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
||||
<input type="hidden" name="email_quote" value="1">
|
||||
<input type="hidden" name="quick_send" value="1">
|
||||
</form>
|
||||
<?php } ?>
|
||||
|
||||
<?php
|
||||
}
|
||||
|
||||
|
||||
@@ -224,9 +224,13 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
|
||||
</a>
|
||||
<?php if (!empty($config_smtp_provider)) { ?>
|
||||
<div class="dropdown-divider"></div>
|
||||
<button type="submit" class="dropdown-item" form="quickSendQuote"
|
||||
name="quote_id" value="<?= $quote_id ?>">
|
||||
<i class="fas fa-fw fa-bolt me-2"></i>Quick Send
|
||||
</button>
|
||||
<a class="dropdown-item ajax-modal" href="#"
|
||||
data-modal-url="modals/quote/quote_email.php?quote_id=<?= $quote_id ?>">
|
||||
<i class="fas fa-fw fa-paper-plane me-2"></i>Email
|
||||
<i class="fas fa-fw fa-paper-plane me-2"></i>Email<span class="text-muted">...</span>
|
||||
</a>
|
||||
<?php } ?>
|
||||
<?php if (lookupUserPermission("module_sales") >= 3) { ?>
|
||||
@@ -254,5 +258,23 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
|
||||
?>
|
||||
</div>
|
||||
|
||||
<?php if (lookupUserPermission("module_sales") >= 2 && !empty($config_smtp_provider)) { ?>
|
||||
<?php
|
||||
/*
|
||||
* One hidden form for the whole page, targeted by the Quick Send buttons via
|
||||
* their form="" attribute. It cannot be a form per button: agent/invoices.php
|
||||
* wraps its table in a bulkActions form, and a nested form is invalid
|
||||
* HTML - the browser drops the inner one and the click silently submits the
|
||||
* bulk action instead. The button carries the id as its own name/value, which
|
||||
* a submit button contributes to the submission.
|
||||
*/
|
||||
?>
|
||||
<form id="quickSendQuote" action="post.php" method="post" class="d-none">
|
||||
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
||||
<input type="hidden" name="email_quote" value="1">
|
||||
<input type="hidden" name="quick_send" value="1">
|
||||
</form>
|
||||
<?php } ?>
|
||||
|
||||
<?php
|
||||
require_once "../includes/footer.php";
|
||||
|
||||
@@ -814,6 +814,30 @@ function documentContactFilterSql($document_type) {
|
||||
return "AND (contact_primary = 1 OR contact_billing = 1)";
|
||||
}
|
||||
|
||||
/*
|
||||
* Which of the offered contacts are selected by default, as a SQL fragment.
|
||||
*
|
||||
* Two things read this and they must agree: the Send Email picker uses it to
|
||||
* decide which boxes open ticked, and Quick Send uses it as the whole
|
||||
* recipient list - Quick Send is exactly "send to the ticked ones without
|
||||
* opening the modal".
|
||||
*
|
||||
* invoice - primary and billing, which is everyone the picker offers. A bill
|
||||
* has no recipient you would routinely leave out.
|
||||
* quote - primary only. Technical and important contacts are offered
|
||||
* because they often want the quote, but sending unasked to
|
||||
* someone who did not request pricing is not a safe default.
|
||||
*
|
||||
* Always a subset of documentContactFilterSql() for the same document type.
|
||||
*/
|
||||
function documentDefaultContactFilterSql($document_type) {
|
||||
if ($document_type === 'quote') {
|
||||
return "AND contact_primary = 1";
|
||||
}
|
||||
|
||||
return "AND (contact_primary = 1 OR contact_billing = 1)";
|
||||
}
|
||||
|
||||
/*
|
||||
* The delivery methods offered by the Mark Sent modal on invoices and quotes.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user