Merge pull request 'finley/110-extra-ccs' (#113) from finley/110-extra-ccs into master

Reviewed-on: cmc/cmc-sales#113
Reviewed-by: kzrl <karl@cordes.com.au>
This commit is contained in:
kzrl 2025-07-12 15:47:49 -07:00
commit d804a88d15
2 changed files with 439 additions and 8 deletions

View file

@ -389,7 +389,6 @@ ENDINSTRUCTIONS;
if(isset($source_document_id)) {
//This is not ideal. But nothing else is either.
$sourceDoc = $this->Document->find('first', array('conditions' => array('Document.id' => $source_document_id)));
@ -1114,21 +1113,18 @@ EOT;
$this->Email->delivery = 'smtp';
$document = $this->Document->read(null,$id);
if(empty($document['Document']['pdf_filename'])) {
$this->Session->setFlash(__('Error. Please generate the PDF before attempting to email it', true));
return;
}
else {
$pdf_dir = Configure::read('pdf_directory');
$attachment_files = array($pdf_dir.$document['Document']['pdf_filename']);
foreach($document['DocumentAttachment'] as $document_attachment) {
$attachment = $this->Document->DocumentAttachment->Attachment->read(null, $document_attachment['attachment_id']);
$attachment_files[] = $attachment['Attachment']['file'];
$attachment = $this->Document->DocumentAttachment->Attachment->read(null, $document_attachment['attachment_id']);
$attachment_files[] = $attachment['Attachment']['file'];
}
$this->Email->attachments = $attachment_files;
}
$enquiry = $this->Document->getEnquiry($document);
@ -1209,6 +1205,221 @@ EOT;
}
function format_email($email) {
$email = trim($email);
// Basic RFC 5322 email validation
if (!preg_match('/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/', $email)) {
return '';
}
return "$email <$email>";
}
function parse_email_to_array($input) {
try {
if (empty($input) || !is_string($input)) {
return array();
}
$input = trim($input);
if ($input === '') {
return array();
}
if (strpos($input, ',') !== false) {
$parts = explode(',', $input);
$result = array();
foreach ($parts as $email) {
$email = $this->format_email($email);
if ($email !== '') {
$result[] = $email;
}
}
return $result;
} else {
$result = $this->format_email($input);
$array[] = $result;
return $array;
}
} catch (Exception $e) {
return array();
}
}
/**
* Email the PDF(document + attachments) for this Document to custom recipients.
*
* @param int $id - Document ID
* @param string $to - Recipient email address (comma-separated if multiple)
* @param string|null $cc - CC email address(es), optional (comma-separated)
* @param string|null $bcc - BCC email address(es), optional (comma-separated)
*/
function email_pdf_with_custom_recipients($id = null, $to = null, $cc = null, $bcc = null) {
// Disable automatic rendering of a view
$this->autoRender = false;
// Retrieve recipient emails from form data if not provided as arguments
if (empty($to) && !empty($this->params['form']['to'])) {
$to = $this->params['form']['to'];
}
if (empty($cc) && isset($this->params['form']['cc'])) {
$cc = $this->params['form']['cc'];
}
if (empty($bcc) && isset($this->params['form']['bcc'])) {
$bcc = $this->params['form']['bcc'];
}
// Basic validation for required parameters
if (empty($id) || empty($to)) {
$msg = 'Document ID and recipient email are required.';
echo json_encode(array('success' => false, 'message' => $msg));
return;
}
// Configure SMTP settings for email delivery
$this->Email->smtpOptions = Configure::read('smtp_settings');
$this->Email->delivery = 'smtp';
// Load the document and its attachments
$document = $this->Document->read(null, $id);
error_log("[email_pdf_with_custom_recipients] Document loaded: " . print_r($document['Document'], true));
error_log("[email_pdf_with_custom_recipients] DocumentAttachments: " . print_r($document['DocumentAttachment'], true));
if (empty($document) || empty($document['Document'])) {
$msg = 'Document not found.';
echo json_encode(array('success' => false, 'message' => $msg));
return;
}
// Ensure the PDF has been generated before emailing
if (empty($document['Document']['pdf_filename'])) {
$msg = 'Error. Please generate the PDF before attempting to email it';
echo json_encode(array('success' => false, 'message' => $msg));
return;
}
// Build the list of attachments (PDF + any additional attachments)
$pdf_dir = Configure::read('pdf_directory');
$attachment_files = array($pdf_dir.$document['Document']['pdf_filename']);
foreach($document['DocumentAttachment'] as $document_attachment) {
$attachment = $this->Document->DocumentAttachment->Attachment->read(null, $document_attachment['attachment_id']);
$attachment_files[] = $attachment['Attachment']['file'];
error_log("[email_pdf_with_custom_recipients] Added attachment: " . $attachment['Attachment']['file']);
}
if (!empty($document['DocumentAttachment'])) {
foreach ($document['DocumentAttachment'] as $document_attachment) {
if (!empty($document_attachment['attachment_id'])) {
$attachment = $this->Document->DocumentAttachment->Attachment->read(null, $document_attachment['attachment_id']);
if (!empty($attachment['Attachment']['file'])) {
$attachment_files[] = $attachment['Attachment']['file'];
error_log("[email_pdf_with_custom_recipients] Added attachment: " . $attachment['Attachment']['file']);
}
}
}
}
error_log("[email_pdf_with_custom_recipients] All attachments: " . print_r($attachment_files, true));
$this->Email->attachments = $attachment_files;
// Get related enquiry for the document
$enquiry = $this->Document->getEnquiry($document);
// Parse and validate recipient email addresses
$toArray = $this->parse_email_to_array($to);
if (empty($toArray)) {
$msg = 'Invalid recipient email address.';
echo json_encode(array('success' => false, 'message' => $msg));
return;
} else {
$this->Email->to = implode(', ', $toArray);
}
$ccArray = $this->parse_email_to_array($cc);
if (!empty($ccArray)) {
$this->Email->cc = $ccArray;
}
$bccArray = $this->parse_email_to_array($bcc);
if (!empty($bccArray)) {
$this->Email->bcc = $bccArray;
}
// Set reply-to and from addresses
$this->Email->replyTo = 'CMC Technologies - Sales <sales@cmctechnologies.com.au>';
$this->Email->from = 'CMC Technologies - Sales <sales@cmctechnologies.com.au>';
// Determine document type, email template, and subject
$docType = $this->Document->getDocType($document);
$template = $docType . '_email';
$subject = !empty($enquiry['Enquiry']['title']) ? $enquiry['Enquiry']['title'] . ' ' : 'Document';
error_log("[email_pdf_with_custom_recipients] Enquiry Title: " . empty($enquiry['Enquiry']['title']) . $enquiry['Enquiry']['title']);
// Customise subject and template based on document type
switch($docType) {
case 'quote':
$subject = !empty($enquiry['Enquiry']['title']) ? "Quotation: " . $enquiry['Enquiry']['title'] : 'Quotation';
break;
case 'invoice':
$subject = $this->invoice_email_subject($document);
if (!empty($document['Invoice']['id'])) {
$this->set('invoice', $this->Document->Invoice->find('first', array('conditions'=>array('Invoice.id'=>$document['Invoice']['id']))));
}
if (!empty($document['Invoice']['job_id'])) {
$this->set('job', $this->Document->Invoice->Job->find('first', array('conditions'=>array('Job.id'=>$document['Invoice']['job_id']))));
}
break;
case 'purchaseOrder':
$subject .= "Purchase Order";
$primary_contact = null;
if (!empty($document['PurchaseOrder']['principle_id'])) {
$primary_contact = $this->Document->User->find('first', array('conditions'=>array('User.principle_id' => $document['PurchaseOrder']['principle_id'],'User.primary_contact' => 1)));
}
if(empty($primary_contact)) {
$msg = 'Unable to send. No primary Principle Contact';
echo json_encode(array('success' => false, 'message' => $msg));
return;
}
$subject = $this->po_email_subject($document['PurchaseOrder']);
if (!empty($document['OrderAcknowledgement']['job_id'])) {
$this->set('job', $this->Document->PurchaseOrder->Job->find('first', array('conditions'=>array('Job.id'=>$document['OrderAcknowledgement']['job_id']))));
}
break;
case 'orderAck':
$subject = $this->orderack_email_subject($document);
if (!empty($document['OrderAcknowledgement']['job_id'])) {
$this->set('job', $this->Document->OrderAcknowledgement->Job->find('first', array('conditions'=>array('Job.id'=>$document['OrderAcknowledgement']['job_id']))));
}
if (!empty($document['OrderAcknowledgement']['signature_required'])) {
$template = 'orderAck_email_signature_required';
}
break;
case 'packingList':
$subject = $this->packing_list_email_subject($document);
break;
}
// Set email template and other parameters
$this->Email->template = $template;
$this->Email->subject = $subject;
$this->Email->sendAs = 'both';
$this->Email->charset = 'iso-8859-1';
$this->set('enquiry', $enquiry);
$this->set('document', $document);
$this->set('DocFullName', $this->Document->getDocFullName($document['Document']['type']));
// Attempt to send the email and handle errors
$sent = false;
try {
$sent = $this->Email->send();
} catch (Exception $e) {
$msg = 'Exception: ' . $e->getMessage();
echo json_encode(array('success' => false, 'message' => $msg));
return;
}
if ($sent) {
echo json_encode(array('success' => true, 'message' => 'The Email has been sent'));
return;
} else {
$msg = 'The Email has NOT been sent';
echo json_encode(array('success' => false, 'message' => $msg, 'smtp_errors' => $this->Email->smtpError));
return;
}
echo json_encode(array('success' => false, 'message' => 'No response from email function'));
}
// generateShippingReference builds the Shipping Instructions: with the PO number and job titles.
function generateShippingInstructions($document_id) {
$this->layout = 'ajax';
@ -1331,5 +1542,4 @@ ENDINSTRUCTIONS;
}
return $newDoc[$model];
}
}

View file

@ -6,9 +6,147 @@
<div class="docButtons">
<button id="paymentReceived">Enter Payment Received</button>
<button id="pdfDocButton" data-url="/documents/pdf/<?= $document['Document']['id']; ?>">Generate PDF</button>
<button id="emailDocButton" data-url="/documents/email_pdf/<?= $document['Document']['id']; ?>">Email Invoice to customer</button>
<button id="emailInvoiceButton" data-url="/documents/email_pdf_with_custom_recipients/<?= $document['Document']['id']; ?>">Email Invoice</button>
</div>
<!-- Email Modal -->
<div id="emailModal" class="modal-pop" style="display:none; position:fixed; top:50%; left:50%; transform:translate(-50%, -50%); background:#fff; border:2px solid #1976d2; padding:14px 28px 18px 28px; z-index:1000; min-width:370px; font-size:14px; box-shadow: 0 8px 32px rgba(0,0,0,0.25), 0 1.5px 8px rgba(0,0,0,0.10); border-radius:12px;">
<h3 style="font-size:1.5em; margin-top:0; padding-bottom:10px;">Email Invoice</h3>
<form id="emailInvoiceForm">
<div class="input">
<label for="emailTo">To:
<span class="info-icon" title="Use comma separated values for multiple addresses">&#9432;</span>
</label>
<input type="text" id="emailTo" name="to" class="emailInputSmall" required value="<?= isset($accountsEmail) ? h($accountsEmail) : '' ?>" />
</div>
<div class="input">
<label for="emailCc">Cc:
<span class="info-icon" title="Use comma separated values for multiple addresses">&#9432;</span>
</label>
<input type="text" id="emailCc" name="cc" class="emailInputSmall" value="<?= isset($enquiry['Contact']['email']) ? h($enquiry['Contact']['email']) : '' ?>" />
</div>
<div class="input">
<label for="emailBcc">Bcc:
<span class="info-icon" title="Use comma separated values for multiple addresses">&#9432;</span>
</label>
<input type="text" id="emailBcc" name="bcc" class="emailInputSmall" value="sales@cmctechnologies.com.au" />
</div>
<div id="emailError" style="color:red; display:none; margin-top:8px;"></div>
<div class="modal-btn-row">
<button type="button" id="cancelEmailInvoice" class="modal-btn">Cancel</button>
<button type="submit" id="sendEmailInvoice" class="modal-btn" style="min-width:80px;">Send</button>
</div>
</form>
</div>
<div id="modalOverlay" style="display:none; position:fixed; top:0; left:0; width:100vw; height:100vh; background:rgba(0,0,0,0.3); z-index:999;"></div>
<script>
document.getElementById('emailInvoiceButton').addEventListener('click', function() {
document.getElementById('emailModal').style.display = 'block';
document.getElementById('modalOverlay').style.display = 'block';
document.getElementById('emailError').style.display = 'none';
document.getElementById('emailError').textContent = '';
// Reset to default values each time modal opens
document.getElementById('emailTo').value = "<?= isset($accountsEmail) ? h($accountsEmail) : '' ?>";
document.getElementById('emailCc').value = "<?= isset($enquiry['Contact']['email']) ? h($enquiry['Contact']['email']) : '' ?>";
document.getElementById('emailBcc').value = 'sales@cmctechnologies.com.au';
// Disable send button if 'to' field is empty
var sendBtn = document.getElementById('sendEmailInvoice');
sendBtn.disabled = !document.getElementById('emailTo').value.trim();
if (sendBtn.disabled) {
sendBtn.setAttribute('aria-disabled-msg', 'An invoice recipient must be specified');
} else {
sendBtn.removeAttribute('aria-disabled-msg');
}
});
document.getElementById('emailTo').addEventListener('input', function() {
var sendBtn = document.getElementById('sendEmailInvoice');
sendBtn.disabled = !this.value.trim();
if (sendBtn.disabled) {
sendBtn.setAttribute('aria-disabled-msg', 'An invoice recipient must be specified');
} else {
sendBtn.removeAttribute('aria-disabled-msg');
}
});
document.getElementById('cancelEmailInvoice').addEventListener('click', function() {
document.getElementById('emailModal').style.display = 'none';
document.getElementById('modalOverlay').style.display = 'none';
});
document.getElementById('modalOverlay').addEventListener('click', function() {
document.getElementById('emailModal').style.display = 'none';
document.getElementById('modalOverlay').style.display = 'none';
});
document.getElementById('emailInvoiceForm').addEventListener('submit', function(e) {
if (!confirm('Are you sure you want to send this invoice?')) {
e.preventDefault();
return;
}
e.preventDefault();
var to = document.getElementById('emailTo').value;
var cc = document.getElementById('emailCc').value;
var bcc = document.getElementById('emailBcc').value;
var url = document.getElementById('emailInvoiceButton').getAttribute('data-url');
var formData = new FormData();
formData.append('to', to);
formData.append('cc', cc);
formData.append('bcc', bcc);
var errorDiv = document.getElementById('emailError');
errorDiv.style.display = 'none';
errorDiv.textContent = '';
fetch(url, {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(async response => {
// Always read the response as text first, then try to parse as JSON
let text = await response.text();
let data;
try {
data = JSON.parse(text);
} catch (err) {
data = { error: 'Unexpected server response.', raw: text };
}
return data;
})
.then(data => {
// Show SMTP errors if present, otherwise show any error or failure message
if (data && (data.error || data.smtp_errors || data.success === false)) {
var msgArr = [];
if (data.smtp_errors) {
msgArr.push('SMTP Error: ' + data.smtp_errors);
}
if (data.error) {
msgArr.push(data.error);
}
if (data.success === false && data.message) {
msgArr.push(data.message);
}
if (data.raw) {
msgArr.push(data.raw);
} else if (data.error) {
msgArr.push('<em>No response receieved.</em>');
}
if (msgArr.length === 0) {
msgArr.push('Failed to send email. No error details provided.');
}
errorDiv.innerHTML = msgArr.join('<br>');
errorDiv.style.display = 'block';
} else {
alert(data.message || 'Email sent!');
document.getElementById('emailModal').style.display = 'none';
document.getElementById('modalOverlay').style.display = 'none';
}
})
.catch((err) => {
errorDiv.innerHTML = 'Failed to send email: ' + (err && err.message ? err.message : err);
errorDiv.style.display = 'block';
});
});
</script>
<div class="docOperations">
<h3>Create new Documents based on this</h3>
<ul class="document-buttons">
@ -64,3 +202,86 @@
<?php //debug($docType);?>
<? //debug($invoice); ?>
<style>
.emailInputSmall {
width: 100%;
font-size: 13px;
padding: 4px 6px;
}
.modal-pop {
box-shadow: 0 8px 32px rgba(0,0,0,0.25), 0 1.5px 8px rgba(0,0,0,0.10);
border-radius: 12px;
border: 2px solid #1976d2;
background: linear-gradient(135deg, #f8fafc 80%, #e3f2fd 100%);
padding: 28px 28px 18px 28px !important;
min-width: 370px;
font-size: 14px;
transition: box-shadow 0.2s;
}
.modal-btn-row {
width: 100%;
margin-top: 16px;
display: flex;
justify-content: flex-end;
gap: 10px;
/* align-items: center; */
}
.modal-btn {
font-size: 13px !important;
padding: 4px 16px !important;
border-radius: 6px;
border: 1px solid #1976d2;
background: #1976d2;
color: #fff;
transition: background 0.2s, color 0.2s;
box-shadow: 0 1px 4px rgba(25, 118, 210, 0.08);
}
.modal-btn:disabled, #sendEmailInvoice:disabled {
background: #eee !important;
color: #aaa !important;
border: 1px solid #ccc !important;
cursor: not-allowed !important;
}
.info-icon {
display: inline-block;
margin-left: 3px !important;
margin-bottom: 2px !important;
color: #888;
cursor: pointer;
font-size: 1em;
vertical-align: middle;
position: relative;
}
.info-icon:hover::after {
content: attr(title);
position: absolute;
left: 20px;
top: -18px;
background: #333;
color: #fff;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
white-space: nowrap;
z-index: 1001;
}
#sendEmailInvoice[aria-disabled-msg]:hover::after {
content: attr(aria-disabled-msg);
position: absolute;
left: 50%;
top: 110%;
transform: translateX(-50%);
background: #333;
color: #fff;
padding: 4px 10px;
border-radius: 4px;
font-size: 13px;
white-space: nowrap;
z-index: 1002;
pointer-events: none;
}
#sendEmailInvoice {
position: relative;
}
</style>