Google App Script not firing every time onedit trigger is used - google-apps-script

Google App script not firing all JavaScript pop up boxes using on edit trigger. Does not fire when a change is made to the sheet for multiple users. Also only works when it feels like it. I have set the trigger to onEdit(). My code:
function Error() {
var sheet = SpreadsheetApp.getActiveSheet();
if (sheet.getName() == "Protocal Check List 2"){
var ui = SpreadsheetApp.getUi(); // Same variations.
var ss = SpreadsheetApp.getActiveSpreadsheet();
var active = ss.getActiveCell();
var getactive = ss.getActiveCell().getDisplayValue();
var column = ss.getActiveRange().getColumn();
var row = ss.getActiveRange().getRow();
var msg = '';
var section = sheet.getRange('C'+ row);
switch (column) {
case 9:
var msg = "error 1";
break;
case 10:
var msg = "Error 2";
break;
default:
msg = "Error";
break;
}
if(getactive == "TRUE"){
if(column >= 9 && column <= 60
){
var result = ui.alert(
"pika Says",
msg,
ui.ButtonSet.YES_NO);
// Process the user's response.
if (result == ui.Button.YES) {
// User clicked "Yes".
window.alert('Task Complete: \n\n' + msg);
active.setValue('True');
} else {
var i = 0;
while (i < 1) {
var result = ui.prompt(
'Please detail cause of the problem:',
ui.ButtonSet.OK);
var text = result.getResponseText();
var textcount = text.length;
if(textcount > 0) {
i++;
}}
var cell = "H" + row;
var emailAddress = "email#gmail.com";
var d = new Date();
var n = d.toDateString();
var t = d.toTimeString();
var staffname = ss.getRange(cell).getValues();
var message = "Date: " + n +"\n\nTime: " + t +"\n\nStaff: " + staffname + "\n\nError: " + msg + "\n\nProblem: " + text;
var subject = msg;
var thedate = n + " / " + t;
ss.getRange('A1').setValue(thedate);
ss.getRange('B1').setValue(staffname);
ss.getRange('C1').setValue(msg);
ss.getRange('D1').setValue(text);
var s1 = ss.getRange('A1:D1'); //assign the range you want to copy
var s1v = s1.getValues();
var tss = SpreadsheetApp.openById('1mOzdRgKxiP5iB9j7PqUWKKo5oymWuAeQZ1jJ1s6qL9E'); //replace with destination ID
var ts = tss.getSheetByName('AB Protocal'); //replace with destination Sheet tab name
ts.getRange(ts.getLastRow()+1, 1, 1,4).setValues(s1v); //you will need to define the size of the copied data see getRange()
MailApp.sendEmail(emailAddress, subject, message);
// User clicked "No" or X in the title bar.
//ui.alert("The following note has been sent to the Duty Manager: \n\n" + text);
active.setValue('FALSE');
}}}}
}
Any help would be appreciated. I need it to run every signal time for users who have access to the sheet.

You'll have to
assume that onEdit triggers are best-effort, but may not catch all
edits done to the spreadsheet.
Bug thread is here.

To get around the bug as mentioned by Edward implemente a dumb router.
// create a new file -> router.gs
function routerOnEdit(e) {
myOnEditHook1(e);
myOnEditHook2(e);
myOnEditHook3(e);
}

I have submitted an issue, which is the updated version of a long-running 7 year issue. They closed it in 2022, saying the feature is intended. They have now added a feature where onEdit can queue up to 2 trigger events. The issue I'm having is that the second edit still does not trigger. Despite what the documentation says.
Note: The onEdit() trigger only queues up to 2 trigger events.
View the new issue here: https://issuetracker.google.com/issues/221703754

Related

Google App Script - automatically send out email when a new row

I'm trying to build a script that can automatically send email when a new row is updated. I only want to trigger the new updated rows and mark 'sent' after sending them out.
Here's the script that's currently working well. I can run it with a click:
function sendMail()
{
var name = 0;
var request = 1;
var id = 2;
var select = 3;
var time = 4;
var email = 5;
var status = 6;
var error = 7;
var emailTemp = HtmlService.createTemplateFromFile("email");
var ws = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Email list");
var data = ws.getRange("A2:I" + ws.getLastRow()).getValues();
data = data.filter(function(r){ return r[8] == true });
data.forEach(function(row)
{
emailTemp.fn = row[name];
emailTemp.req = row[request];
emailTemp.id = row[id];
emailTemp.erm = row[error];
emailTemp.ta = row[select];
emailTemp.time = row[time];
var htmlMessage = emailTemp.evaluate().getContent();
GmailApp.sendEmail(row[email],
"Failure message",
"Please submit again.",
{name: "My name", htmlBody: htmlMessage}
);
});
}
I have studied this query but I still have no idea how it works.
Please share some relevant source or some solution, thank you!
Although I'm not sure about mark 'sent' after sending them out., if "sent" is put to the column "J", from I assume it's filled in another column, like col8 or col 9. of your reply, I guessed that you wanted to put "sent" to the column "J" because the columns 8 and 9 (H and I) have already been used in your script. If my understanding is correct, how about the following modification?
Modified script:
Please copy and paste the following script to the script editor of Google Spreadsheet and save the script. And please install OnEdit trigger to the function sendMail. By this, when the cell is edited, the script is run.
function sendMail() {
var ws = SpreadsheetApp.getActiveSheet();
if (ws.getSheetName() != "Email list") return;
var name = 0;
var request = 1;
var id = 2;
var select = 3;
var time = 4;
var email = 5;
var status = 6;
var error = 7;
var emailTemp = {};
var emailTemp = HtmlService.createTemplateFromFile("email");
var data = ws.getRange("A2:J" + ws.getLastRow()).getValues();
var ranges = data.map(function (row, i) {
if (row[8] == true && row[9] != "sent") {
emailTemp.fn = row[name];
emailTemp.req = row[request];
emailTemp.id = row[id];
emailTemp.erm = row[error];
emailTemp.ta = row[select];
emailTemp.time = row[time];
var htmlMessage = emailTemp.evaluate().getContent();
GmailApp.sendEmail(row[email],
"Failure message",
"Please submit again.",
{ name: "My name", htmlBody: htmlMessage }
);
return "J" + (i + 2);
}
return "";
}).filter(String);
ws.getRangeList(ranges).setValue("sent");
}
In this case, when the active sheet is "Email list", the script is run. And, "sent" is put to the column "J".
Note:
I think that the event object of OnEdit can be used. But from your question, I thought that you might want to directly run the script with the script editor. So I didn't use the event object.
It seems that the columns 8 and 9 (H and I ) are used in your script like emailTemp.erm = row[error]; and data = data.filter(function(r){ return r[8] == true });. So I put "sent" to the column "J". Please be careful this.

trying to add Guest through google appscript

Am trying to add a Guest to the calendar through google apps script that takes a calendar request from the user through Google forms---> it then updates a Google online excel sheet if no conflicts and makes the reservation if no conflict all this works fine but if the user tries to edit the time for the reservation they can't as they aren't the owner of the calendar to make edits so the only workaround I could think of is adding there email from the submitted request and add them as guest to that specific event but it ended unsuccessfully.
user submits a google form request with his email name time etc.
google forms update google excel sheet and checks that there are no conflicts.
updates the calendar successfully and sends a confirmation to the user's email.
The user needs to edit the time of his reservation on the calendar but can't as he is not a guest and can't edit the calendar (trying to solve this by automatically adding him as a guest to the requested event.
read so solutions here on adding location but was unsuccessful in adding the user as a guest.
tried adding
var body = {
'sendNotification': true,
'attendees': attendees
};
var ret = Calendar.Events.patch(body, calendarId, eventId);
but was unsuccessful
Code looks like:
// Room Reservation
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheets()[0];
var lastRow = sheet.getLastRow();
var lastColumn = sheet.getLastColumn();
// Calendar to output reservations to
var cal145 = CalendarApp.getCalendarById('xxxxxx#resource.calendar.google.com');
// Create use data from user submission
// This function submits data from the google form and inputs it into the "Room Reservation" sheet by the user
function Submission(){
var row = lastRow;
this.timestamp = sheet.getRange(row, 1).getValue();
this.NameReason = sheet.getRange(row, 2).getValue();
this.email = sheet.getRange(row, 3).getValue();
this.date = sheet.getRange(row, 5).getValue();
this.time = sheet.getRange(row, 6).getValue();
this.duration = sheet.getRange(row, 7).getValue();
this.room = sheet.getRange(row, 8).getValue();
// Info not from spreadsheet
this.roomInt = this.room.replace(/\D+/g, '');
this.status;
this.dateString = (this.date.getMonth() + 1) + '/' + this.date.getDate() + '/' + this.date.getYear();
this.timeString = this.time.toLocaleTimeString();
this.date.setHours(this.time.getHours());
this.date.setMinutes(this.time.getMinutes());
this.calendar = eval('cal' + String(this.roomInt));
return this;
}
// Use duration to create endTime variable
function getEndTime(request){
request.endTime = new Date(request.date);
switch (request.duration){
case "1 hour":
request.endTime.setMinutes(request.date.getMinutes() + 60);
request.endTimeString = request.endTime.toLocaleTimeString();
break;
case "2 hours":
request.endTime.setMinutes(request.date.getMinutes() + 120);
request.endTimeString = request.endTime.toLocaleTimeString();
break;
case "3 hours":
request.endTime.setMinutes(request.date.getMinutes() + 180);
request.endTimeString = request.endTime.toLocaleTimeString();
break;
}
}
// Check for appointment conflicts
function getConflicts(request){
var conflicts = request.calendar.getEvents(request.date, request.endTime);
if (conflicts.length < 1) {
request.status = "Approve";
} else {
request.status = "Conflict";
}
}
function draftEmail(request){
request.buttonLink = "xxxxxxx ";
request.buttonText = "New Request";
switch (request.status) {
case "Approve":
request.subject = "Confirmation: " + request.room + " Reservation for " + request.dateString;
request.header = "Confirmation";
request.message = "Your room reservation has been scheduled.";
break;
case "Conflict":
request.subject = "Conflict with " + request.room + "Reservation for " + request.dateString;
request.header = "Conflict";
request.message = "There is a scheduling conflict. Please pick another time."
request.buttonText = "Reschedule";
break;
}
}
function updateCalendar(request){
var event = request.calendar.createEvent(
request.NameReason,
request.date,
//request.reason,
request.endTime
)
}
function sendEmail(request){
MailApp.sendEmail({
to: request.email,
subject: request.header,
htmlBody: makeEmail(request)
})
sheet.getRange(lastRow, lastColumn).setValue("Sent: " + request.status);
}
function sendinvite(request)
{
request.addGuest(email);
}
// --------------- main --------------------
function main(){
var request = new Submission();
getEndTime(request);
getConflicts(request);
draftEmail(request);
Logger.log(request);
sendEmail(request);
if (request.status == "Approve") updateCalendar(request);
if (request.status == "Approve")
{
sendinvite(request);
}
}
The final result should let the user edit the time of the event created although he is not the owner of the calendar. the function should add the user as a guest to that specific event he requested so he could edit the time.
this script works fine currently as is without the addition of guest sendinvite function.
thanks

Send email when cell is edited using email in parallel cell

I have read a lot of onEdit and Triggers script but still I cannot achieve what I want to achieve, I code a lot in excel VBA and google sheet is very different. So the thing is based on my screenshot, what I want is to send an email once the cell contains "Approved", "denied", "In progress" and the email address must be based on the parallel of the edited cell. I'm dying to get this work done.
The code is based on the internet but I cannot tweak it based on my data/sheet.
You can add a custom function to a dropdown menu in the Spreadsheets UI with the following script. This will allow you to circumvent the onEdit() restriction that doesn't allow one to use the MailApp class, but it is at the cost of having users manually call the script instead of the script running automatically.
Here the user will select "Send E-Mail" from the dropdown menu, and it will prompt him/her for the Primary Key via an input prompt modal. The row of the corresponding key will be identified and an e-mail sent out after status is automatically changed to "approved". This script assumes that the spreadsheet contains at least four columns with header rows "Primary Key", "Description", "Email", and "Status" in any order.
Please note: this code was tested successfully. Please update lines 20 and 21 by replacing the square brackets and text contained therein that defines sheetURL and workSheetName variables.
function onOpen() {
var ui = SpreadsheetApp.getUi();
ui.createMenu('Custom Menu')
.addItem('Send E-Mail', 'sendEmail')
.addToUi();
}
function sendEmail(){
// Display a dialog box with a title, message, input field, and "OK" and "Cancel" buttons. The
// user can also close the dialog by clicking the close button in its title bar.
var ui = SpreadsheetApp.getUi();
var response = ui.prompt('Pop-Up Prompt', 'Please enter primary key:', ui.ButtonSet.OK_CANCEL);
// Process the user's response.
if (response.getSelectedButton() == ui.Button.OK) {
Logger.log('The user entered the following primary key:', response.getResponseText());
// Map the header rows in order that column position is not hard-coded.
var sheetURL = '[ENTER YOUR SHEET URL HERE]';
var workSheetName = '[ENTER YOUR WORKSHEET NAME HERE]';
var sheet = SpreadsheetApp.openByUrl(sheetURL).getSheetByName(workSheetName);
var lastColumn = sheet.getLastColumn();
var headerRange = sheet.getRange(1, 1, 1, lastColumn);
var headers = headerRange.getValues();
for (var i=1; i<headers[0].length+1; i++) {
switch (headers[0][i-1]){
case "Primary Key":
var primaryKeyIndex = i;
break;
case "Description":
var descriptionIndex = i;
break;
case "Email":
var emailIndex = i;
break;
case "Status":
var statusIndex = i;
break;
}
}
// Header rows mapped.
// Search for row corresponding to primary key.
var primaryKey = response.getResponseText();
var keyRow = findInColumn(columnToLetter(primaryKeyIndex), primaryKey);
if (keyRow == -1){
ui.alert('Primary Key "'+ primaryKey + '" not found.');
} else {
ui.alert('Primary Key "'+ primaryKey + '" found at row: ' +keyRow+ '.');
sheet.getRange(keyRow, statusIndex).setValue("Approved");
//Prepare Email
var subject = "test";
var email = sheet.getRange(keyRow, emailIndex).getValue();
var body = "Hi, \n\n Your entry with primary key " + primaryKey + " is now approved.";
MailApp.sendEmail(email, subject, body);
}
} else if (response.getSelectedButton() == ui.Button.CANCEL) {
Logger.log('The user clicked cancel.');
} else {
Logger.log('The user clicked the close button in the dialog\'s title bar.');
}
}
// Helper function to find corresponding row to data in column.
function findInColumn(column, data) {
var sheet = SpreadsheetApp.getActiveSpreadsheet();
var column = sheet.getRange(column + ":" + column); // like A:A
var values = column.getValues();
var row = 0;
while ( String(values[row]) && String(values[row][0]) !== String(data) ) {
row++;
}
if (String(values[row][0]) === String(data))
return row+1;
else
return -1;
}
// Helper function to convert Column Number to Column Letter
function columnToLetter(column){
var temp, letter = '';
while (column > 0)
{
temp = (column - 1) % 26;
letter = String.fromCharCode(temp + 65) + letter;
column = (column - temp - 1) / 26;
}
return letter;
}
I'd suggest that you not use the onEdit trigger for sending email. I think it's over used by many users. If you are, you will have to go with the installable triggers. This is an example email solution that looks pretty clean that came in yesterday.
You can use most of this code below. Modify the email portions to suit your needs.
This code checks for sheet name to be 'Form Responses' and edited column header to be 'Status' as from pics given above.
function onEdit() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var aSheet = ss.getActiveSheet();
// check sheet name
if (aSheet.getName() != 'Form Responses') return;
var range = ss.getActiveRange();
var row = range.getRow();
var col = range.getColumn();
// Logger.log(col);
var headers = aSheet.getRange(1,1,1,aSheet.getLastColumn()).getValues()[0];
// Logger.log(headers[col-1]);
// check column header
if (headers[col-1] != 'Status') return;
var value = range.getValue();
var values = ["approved", "denied", "in progress"]; // values to check for
// check values
if (values.indexOf(value.toString().toLowerCase()) == -1) return;
// Logger.log(row);
var rowValues = aSheet.getRange(row, 1, 1, aSheet.getLastColumn()).getValues()[0];
Logger.log(rowValues);
// change as necessary
var recipient = rowValues[1]; // email is in 2nd column
var body = 'Email body'; // create body
var subject = 'Test'; // set subject
// send email
MailApp.sendEmail(recipient, subject, body);
}

Google App Script Optimisation

Ok, I'm in need of some help optimising (where possible) and error checking my code.
My code has ran error free for 20+ weeks. Now all of a sudden, the script 'hangs' while executing the .setvalues on line 190. This is the section that archives the information.
Error received is "Service Timed Out : Spreadsheets" and "Exception: Service Error: Spreadsheets".
The Scripts runs between 2-3am on Sundays, when the servers should be less congested. The Script has also never timed out when running manually. I have not been able to replicate this error, even when tripling or quadrupling the working data.
So, I'll start.
My script runs in 4 sections.
Section 1 :
Validate information - Remove filters, Unhide Rows/columns and delete blank rows.
Section 2 :
Copy the selected sheet to a new spreadsheet and email this to selected users as an attachment in Excel format.
Section 3 :
Clear the data from the original sheet to prevent the possibility of duplication.
Section 4 :
This is the part that fails, TRY and paste the copied values into the archived spreadsheet.
Previously, there was no loop to re-attempt this. If it failed I would receive an email with the excel document.
The loop doesn't seem to be helping. Other than, it pasted half the information into my archive this weekend past.
If this helps, the data that is being moved is about 8000 rows and 15 columns, so about 120,000 cells. (Not that much)
If anyone can suggest any amendments or improvements, please feel free.
Full code below.
//******************** Menu Start ************************//
function onOpen() {
var ui = SpreadsheetApp.getUi();
ui.createMenu('Admin')
.addItem('Update to Raw', 'moveData')
.addSeparator()
.addSubMenu(ui.createMenu('Authorise')
.addItem('Authorise Scripts', 'Auth'))
.addToUi();
}
//******************** Menu End ************************//
//******************** Authorisation Start ************************//
function Auth(){
var email = Session.getActiveUser().getEmail();
var temp = new Date();
if (temp == "Blank") {
// These calls will never be visited
onOpen();
moveData();
clearData();
RemoveFilter();
DeleteBlankRows();
UnhideAllRowsAndColumns();
UnhideAllRowsAndColumnsRaw();
clearDataRaw();
} else {
Browser.msgBox("The Backup script has now been authorized for "+email+". Each user only has to do this once.");
}
}
//******************** Authorisation End ************************//
//******************** Clear Source Sheet Start ************************//
function clearData() {
var Spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
var source = Spreadsheet.getSheetByName("Data");
source.deleteRows(2,source.getLastRow()-1);
}
//******************** Clear Source Sheet End ************************//
//******************** Copy Data Start ************************//
function ArchiveData() {
var Spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
var source = Spreadsheet.getSheetByName("Data");
var targetkey = Spreadsheet.getSheetByName("Archive").getRange("C1").getValue();
var tSpreadsheet = SpreadsheetApp.openById(targetkey);
var target = tSpreadsheet.getSheetByName("Raw");
try{
// Information Quality Checks
RemoveFilter();
UnhideAllRowsAndColumns();
DeleteBlankRows();
var storedata = source.getRange(2,1,source.getLastRow(),source.getLastColumn()).getValues();
var sn = Spreadsheet.getName();
var URL = Spreadsheet.getUrl();
var message = URL;
var date = Utilities.formatDate(new Date(), "GMT+1", "dd-MM-yyyy HH:mm");
var subject = sn + " - Script Complete : " + date;
var emailTo = ["Recipient1#gmail.co.uk","Recipient2#gmail.co.uk",
"Recipient3#gmail.co.uk","Recipient4#gmail.co.uk","Recipient5#gmail.co.uk"];
// Google Sheets Extract Sheet Hack //
// Create a new Spreadsheet and copy the current sheet into it//
var newSpreadsheet = SpreadsheetApp.create("Call Log Script Export");
source.copyTo(newSpreadsheet);
newSpreadsheet.getSheetByName('Sheet1').activate();
newSpreadsheet.deleteActiveSheet();
// newSpreadsheet.getSheetByName('!Copied Sheet Name!').setName("Source Export") //
var ssID = newSpreadsheet.getId();
var url = "https://docs.google.com/spreadsheets/d/" + ssID + "/export?format=xlsx&id=" + ssID;
var requestData = {"method": "GET","headers":{"Authorization":"Bearer "+ScriptApp.getOAuthToken()}};
var result = UrlFetchApp.fetch(url , requestData);
var contents = result.getContent();
MailApp.sendEmail(emailTo, subject, message,
{attachments:[{fileName:"Call Log Script Export.xls", content:contents, mimeType:"application//xls"}]});
//------------------------- Move Data -------------------------//
var senddata = target.getRange(target.getLastRow()+1, 1, source.getLastRow(),source.getLastColumn() );
//------------------------- Clear Data Call -------------------------//
// ------------- Clears Source Sheet ------------- //
clearData();
var retryLimit = 4;
var retryDelay = 1000;
var retry;
for (retry = 0; retry <= retryLimit; retry++) {
try {
// do the spreadsheet operation that might fail
senddata.setValues(storedata);
// Delete the wasted sheet we created, so our Drive stays tidy
DriveApp.getFileById(ssID).setTrashed(true);
SpreadsheetApp.flush();
break;
}
catch (e) {
Logger.log('Failed on try ' + retry + ', exception: ' + e);
if (retry == retryLimit) {
throw e;
}
Utilities.sleep(retryDelay);
}
}
//------------------------- Copy Data Mid -------------------------//
}
//------------------------- Catch and Send Error Start -------------------------//
catch(err){
var error = err.lineNumber + ' - ' + err;
var URL = Spreadsheet.getUrl();
var sn = Spreadsheet.getName();
var date = Utilities.formatDate(new Date(), "GMT+1", "dd-MM-yyyy HH:mm");
var emailadd = ["Recipient1#gmail.co.uk","Recipient2#gmail.co.uk",
"Recipient3#gmail.co.uk","Recipient4#gmail.co.uk","Recipient5#gmail.co.uk"];
var subject = sn + " : Archive Script Error";
var body = URL + " - - - Date - - - " + date + " - - - Error Code - - - " + error
MailApp.sendEmail(emailadd,subject,body);
}
//------------------------- Catch and Send Error End -------------------------//
}
//******************** Copy Data End ************************//
//******************** Unhide Start ************************//
function UnhideAllRowsAndColumns() {
var Spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
var source = Spreadsheet.getSheetByName("Data");
var fullSheetRange = source.getRange(1,1,source.getMaxRows(), source.getMaxColumns() )
source.unhideColumn( fullSheetRange );
source.unhideRow( fullSheetRange ) ;
}
//******************** Unhide End ************************//
//******************** Delete Blank Start ************************//
function DeleteBlankRows() {
var Spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
var source = Spreadsheet.getSheetByName("Data");
var rows = source.getDataRange();
var numRows = rows.getNumRows();
var values = rows.getValues();
var rowsDeleted = 0;
for (var i = 0; i <= numRows - 1; i++) {
var row = values[i];
if (row[1] == '') {
source.deleteRow((parseInt(i)+1) - rowsDeleted);
rowsDeleted++;
}
}
}
//******************** Delete Blank End ************************//
//******************** Remove Filter Start ************************//
function RemoveFilter(){
var Spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
var source = Spreadsheet.getSheetByName("Data");
var row = 1 //the row with filter
var rowBefore = row
source.insertRowBefore(row); //inserts a line before the filter
row++;
var Line = source.getRange(row + ":" + row); //gets filter line in A1N
Line.moveTo(source.getRange(rowBefore + ":" + rowBefore)); //move to new line in A1N
source.deleteRow(row); //deletes the filter line
}
//******************** Remove Filter End ************************//
I looked over the code pretty thoroughly and I don't see any obvious problems. But I'm curious about this line:
var senddata = target.getRange(target.getLastRow()+1, 1, source.getLastRow(),source.getLastColumn() );
Since I can't see your data I can't confirm that the range height and width for senddata are the same as the dimensions of storedata and if they aren't then that could cause a problem.
"Service Timed Out : Spreadsheets"
error started happening on my script today and after a few trials what I could do to get away with that was to:
Make a full copy of the sheet (that contains script) and start using that sheet.
Cheers,
So here's what's bugging me:
var storedata = source.getRange(2,1,source.getLastRow(),source.getLastColumn()).getValues();
var senddata = target.getRange(target.getLastRow()+1, 1, source.getLastRow(),source.getLastColumn() );
storedata is an array that has to have the same dimensions as the range senddata.
number of rows in the storedata range is source.getLastRow()-1.
number of columns in the storedata range is source.getLastColumn() = 15
number of rows in senddata is source.getLastRow() - target.getLastRow() + 1
number of columns in senddate is source.getLastColumn() = 15
so:
source.getLastRow()-1 = source.getLastRow() - target.getLastRow() + 1
// add 1 to both sides
source.getLastRow() = source.getLastRow() - target.getLastRow() + 2
subtract source.getLastRow() from both sides
target.getLastRow() = 2 //is this true
Is that always true? Or am I just totally off the mark here.

Send emails from google spreadsheet - coding suggestions

I am still learning the ropes here. Based on code suggested by other contributors, I put together a script to send reminder emails to consultants who record their time entries using a Google Form. The spreadsheet first imports calendar entries with all the Job information for each consultant. After the calendar entries are imported, if the consultant has not yet recorded their time entry, the following script will send them an email with a reminder to do so:
function sendReminder() {
var rmndrFrom = "XYZ, Inc.";
var myemail = "support#xyz.com";
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Pending");
var numRows = sheet.getLastRow();
var lastCol = sheet.getLastColumn();
var dataRange = sheet.getRange(2, 1, numRows, lastCol); // row 1 is the header row
var sheetData = dataRange.getValues();
for (var i = 0; i < sheetData.length; ++i) {
var row = sheetData[i];
if (row[0]){
var jobNumb = row[0]; // Job Number
var conName = row[2]; // Consultant Name
var conMail = row[3]; // Consultant Email
var jobDate = row[4]; // Date
// format email string
var subject = "Time Entry Reminder: " + conName + " / Job " + jobNumb;
try {
var conMsgH = 'This is a reminder for you to record your time entry for Job #<strong>' + jobNum + '/' + jobDate + '</strong>';
// strip HTML for plain text message
var conMsgP = conMsgH.replace(/\<br\/\>/gi, '\n').replace(/(<([^>]+)>)/ig, "");
// send reminder to consultants
MailApp.sendEmail(conMail, subject, conMsgP, { htmlBody:conMsgH, name:rmndrFrom });
} catch (e) { // error handler
MailApp.sendEmail(myemail, "Error in sending reminder email.", e.message);
}
}
}
}
So basically, this script parses the Pending sheet, if column A has a Job Number, it will send a reminder email to the consultant with that Job Number. However, a single consultant may have several job numbers to their name. I would like the script to send a single email to each consultant with a list of the Job Numbers for which they have to record their time entries.
Thanks in advance for your kind help. Any suggestions on how to optimize the code will also be very much appreciated.
There are a number of ways that you can approach this. One way is to keep a sheet with the consultants emails, names and a list of their job numbers. Load this data into your script, a list of all job ids and the job info. Then filter the job ids based on the consultants list and build your email, or you could just send that list of numbers for a very short script.
Another way is to do all of that sorting per consultant in the code and send out the emails that way. This is the approach I've taken, and I've also made use of the iterative JS functions map, filter and reduce more details at MDN.
The code is posted below, but if you would like to take a look at it attached to a spreadsheet and commented (as well as the functions to build that extra sheet with just the consultants info on it) take a look here.
Below is my iteration of your function. I hope it is helpful for your situation:
var rmndrFrom = "XYZ, Inc.";
var myemail = "me#emailisawesome.com";
var subject = "Time Entry Reminder";
function sendReminder() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Pending");
var numRows = sheet.getLastRow();
var lastCol = sheet.getLastColumn();
var sheetData = sheet.getRange(2, 1, numRows-1, lastCol).getValues();
var cons = sheet.getRange(2,3,numRows-1,1).getValues().reduce(flatten_).filter(getUniqueConsultants_);
cons.forEach(sendEmail_, sheetData);
}
function sendEmail_(consultant) {
var consultantsJobs = this.filter(getJobsForConsultant_, consultant);
var jobList = consultantsJobs.map(buildJobLine_).join("<br>");
try {
var conMsgH = "Hi " + consultant + ",<br>";
conMsgH += "This is a reminder for you to record your time entry for the following jobs:<br><br>";
conMsgH += jobList;
conMsgH += "<br><br>Thank you for your cooperation.";
var conMsgP = conMsgH.replace(/\<br\/\>/gi, '\n').replace(/(<([^>]+)>)/ig, "");
MailApp.sendEmail(consultantsJobs[0][3], subject, conMsgP, {htmlBody:conMsgH, name:rmndrFrom});
} catch (e) {
MailApp.sendEmail(myemail, "Error in sending reminder email.", e.message);
}
}
function buildJobLine_(job) {
return "Job #" + job[0] + " on " + Utilities.formatDate(job[4], Session.getTimeZone(), "MMM dd yyyy");
}
function getJobsForConsultant_(row) {
return row[2] == this;
}
function getUniqueConsultants_(v,i,a) {
return a.indexOf(v) == i;
}
function flatten_(a,b) {
return a.concat(b);
}
I must say that fooby's answer is far beyond my JS skills, I'm sure it will work nicely but I still feel like proposing something different (and simpler from my pov), just for the fun of it ;-)
The main difference with your original script is the sorting of the array that allowed me to detect duplicate names and threat it accordingly. The html composition could be far better for sure but that was not your main request.
Here is the code
function sendReminder() {
var rmndrFrom = "XYZ, Inc.";
var myemail = "support#xyz.com";
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Pending");
var initial = "Hi conName,<BR>This is a reminder for you to record your time entry for the following jobs : <BR><BR><table border = 1 cellpadding = 3 bgcolor='#FFFFBB'><TR><TD>";
var sheetData = sheet.getDataRange().getValues();// simplified code to get the data array
sheetData.shift();//skips the headers
sheetData.sort(function(x,y){
var xp = x[2];// sort on column 3 but you can change here...
var yp = y[2];// sort on column 3 but you can change here...
return xp == yp ? 0 : xp < yp ? -1 : 1;// sort ascending, eventually change here...
});
// Logger.log(sheetData);// sorted
var last = sheetData.length
var i = 1;//index 0 is handled outside the loop
var row = sheetData[0];
var subject = "Time Entry Reminder: " + row[2] + " / Job "
var conMsgH = initial
var msgComponent = makeline_(row)
subject += msgComponent[0]
conMsgH += msgComponent[1]
while (i<last){
if(sheetData[i][2]!=sheetData[i-1][2]||i==last-1){
sendData_(sheetData[i-1][3],sheetData[i-1][2],subject,conMsgH)
var subject = "Time Entry Reminder: " + sheetData[i][2] + " / Job "
var conMsgH = initial;
}
msgComponent = makeline_(sheetData[i])
subject += msgComponent[0]
conMsgH += msgComponent[1]
++i
}
}
function sendData_(conMail,conName,subject,conMsgH){
conMsgH = conMsgH.substr(0,conMsgH.length-8)+'</TABLE>'
conMsgH = conMsgH.replace('conName',conName)
var conMsgP = conMsgH.replace(/<\/tr>/ig, '\n').replace(/<br>/ig, '\n').replace(/(<([^>]+)>)/ig, "")
subject = subject.substr(0,subject.length-2);// remove the last '+ '
// Logger.log(subject)
// Logger.log(conMsgH)
Logger.log(conMsgP)
// Logger.log(conMail)
try{
// send reminder to consultants
MailApp.sendEmail(conMail, subject, conMsgP, { htmlBody:conMsgH, name:rmndrFrom });
} catch (e) { // error handler
// MailApp.sendEmail(myemail, "Error in sending reminder email.", e.message);
}
}
function makeline_(row){
var jobNumb = row[0]; // Job Number
var conName = row[2]; // Consultant Name
var conMail = row[3]; // Consultant Email
var descr = row[1]; // description
var FUS1=new Date(row[4]).toString().substr(25,6)+':00';// get timezone of this event, taking care of daylight savings
var jobDate = Utilities.formatDate(row[4], FUS1, "MMM dd yyyy # hh:mm aaa"); // Date
var subject = jobNumb+' + ';
var conMsgH = 'Job #'+jobNumb + '</TD><TD>' + jobDate + '</TD><TD>' + descr + '</TD></TR><TR><TD>';
return [subject,conMsgH];
}
EDIT : made some improvement in the mail format, used a table to show jobs & dates + removed some bugs ;-) (to be honest, I made this also for my personal use as I am having almost the same use case )