Send email if cells' value is below certain value - google-apps-script

I am absolutely new to this and pulling my hair out trying to make a script for work.
I need to check employee's certifications on a daily basis and have them re-certified if expired.
Here is a "testing" spreadsheet with random data: https://docs.google.com/spreadsheets/d/1vJ8ms5ZLqmnv4N1upNHD4SRfgIgIbEAAndvUNy-s9S4/edit?usp=sharing
It lists personnel working for my department along with their badge numbers and number of days their certifications are valid for. The original sheet takes the days value from another spreadsheet, but it shouldn't affect this (I think?).
What I'm trying to achieve is write a script that checks all numbers in C3:G24.
If any cell in this range has a value lower than 15, it should pull their badge number and name from the same row, along with the "days" their certificates are valid for and send an email containing all this data.
For example
Subject: Certifications about to expire
E-mail content: Your employee's (Name from col B) certification with Badge# (# from Col A) will expire in X days (where X = the number from range C3:G24).
So far my best attempt was to at least make it send ANY e-mail on edit, but failing miserably trying to adapt any script found online.
Here is what worked to at least send an e-mail but then I did something to break it:
function checkValue()
{
var ss = SpreadsheetApp.getActive();//not sure if needed, the spreadsheet eventually doesn't have to be open/active
var sheet = ss.getSheetByName("Certifications");
//not sure if this is ok
var valueToCheck = sheet.getRange("C3:G24").getValue();
//Here I'd like to change the "days" based on needs
if(valueToCheck < 15)
{
MailApp.sendEmail("email#company.com","Certifications","Your employee certification will expire soon" + valueToCheck+ ".");
}
}
Can someone please help guide me in the right direction?

here is what I would do:
function checkValue()
{
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("Certifications");
var valueToCheck = sheet.getDataRange().getValues();
var resultValues = [];
valueToCheck = valueToCheck.filter(function(element){
var val = 0
if (parseInt(element[2]) < 15)
{
resultValues.push({col: "Cert1", value: element[2]})
return (true);
}
else if (parseInt(element[3]) < 15)
{
resultValues.push({col: "Cert2", value: element[3]})
return (true);
}
else if (parseInt(element[4]) < 15)
{
resultValues.push({col: "Cert3", value: element[4]})
return (true);
}
else if (parseInt(element[5]) < 15)
{
resultValues.push({col: "Cert4", value: element[5]})
return (true);
}
else if (parseInt(element[6]) < 15)
{
resultValues.push({col: "Cert5", value: element[6]})
return (true);
}
})
for(var i = 0; i < valueToCheck.length; i++)
{
MailApp.sendEmail("mail#company.com","Certifications","your employee's " + valueToCheck[i][1] + "with badge " + valueToCheck[i][0] + " certification will expire in " + resultValues[i].value + " days (column " + resultValues[i].col + ").");
}
}
use the getValues() function to retrieve datas.
then filter the values based on condtion of value being less than 15
at the same time grab the column name and the less than 15 data.
parse through both arrays to send datas to your mail

Related

multiple users using google spreadsheet as data entry form and Database, overwriting each others data

I want to make a sales entry system with google spreadsheet for multiple users.
User 1 will use the data entry form tab named "Main", using inputdata1()
User 2 will use the data entry form tab named "Sub", using inputdata2()
Each of them will write data in a new row that I found it by using the getlastrowspecial function (e.g. it this case lets say its row 10)
If both of them execute the code simultaneously. Sometimes I will see User 1's data being written on row 10 and User 2's data overwriting User 1's data in the same row on row 10. The checking to see if the row is empty before written is not working. I wanted to keep User 2 to wait until User 1's code is completely executed. I dont mind if User 2 need to call the function again.
Please help if there is any method to do this. Thank you very much!
var ss = SpreadsheetApp.getActiveSpreadsheet()
var db = ss.getSheetByName("DB")
var mainInput = ss.getSheetByName("Main")
var subInput = ss.getSheetByName("Sub")
function inputData1() {
var input1 = mainInput.getRange("E2:I2").getValues()
Logger.log(input1)
var lr = getLastRowSpecial(db, "A1:I")
Logger.log(lr)
if (db.getRange(lr + 1, 5).getValue() !== "") {
SpreadsheetApp.getUi().alert("Please try again")
return
} else {
db.getRange(lr + 1, 5, 1, input1[0].length).setValues(input1)
}
}
function inputData2() {
var input2 = subInput.getRange("E2:I2").getValues()
var lr = getLastRowSpecial(db, "A1:I")
if (db.getRange(lr + 1, 5).getValue() !== "") {
SpreadsheetApp.getUi().alert("Please try again")
return
} else {
db.getRange(lr + 1, 5, 1, input2[0].length).setValues(input2)
}
}
// following function getLastRowSpecial() for getting the last row with blank row"
function getLastRowSpecial(sheetlastrow, rangeString) {
var rng = sheetlastrow.getRange(rangeString).getValues();
var lrIndex;
for (var i = rng.length - 1; i >= 0; i--) {
lrIndex = i;
if (!rng[i].every(function (c) { return c == ""; })) {
break;
}
}
return lrIndex + 1;
}
Replace Range.setValues() with Sheet.appendRow(), like this:
current:
db.getRange(lr + 1, 5, 1, input1[0].length).setValues(input1)
new:
db.appendRow(input1[0]);
If you need the values to start in column E, use this:
db.appendRow([null, null, null, null].concat(input1[0]));
Alternatively, follow Cooper's advice and use the Lock Service. If you choose to go this route, also consider using the appendRows_() utility function.

Sending email reminders for Google Sheet tasks

Updated the code based on suggestions below, The email does not contain the summary, any help fix this would be appreciated! The test file is attached below,
function sendEmail(){
SpreadsheetApp.getActiveSpreadsheet().getSheetByName("2021-12 {3600950}").activate();
var ss =
SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
//data on sheet, filter for any row where the status is still "assigned"
var data = ss.getDataRange().getValues()
var assigned = data.reduce(function(acc, curr) {
if(curr[5] === "assigned") {
acc.push(curr)
}
return acc
},[])
// unique list of emails
var Assignee = [0]
var Summary = [2]
var compareAssignee = []
for (i=0; i<assigned.length; i++){
compareAssignee.push(assigned[i][0])
}
//loop unique emails, all the tasks for each of those users, and send a simple email with the tasks
for (var i=0; i<Assignee.length; i++){
var Summary = assigned.reduce(function(acc, curr) {
if(curr[0] === Assignee[i])
{
acc.push(String.fromCharCode() + "pending task: " + curr[2] +
Summary[2])
//this puts a line break before each task, so when the email is
sent each one is on its own line.
}
return acc
},[])
console.log(Summary)
MailApp.sendEmail
MailApp.sendEmail(compareAssignee[0],"pending RPP task(s)",Summary[2])
}
}
function scheduleEmails(){
// Schedules for the first of every month
ScriptApp.newTrigger("sendEmail")
.timeBased()
.onMonthDay(28)
.atHour(1)
.create();
}
You want to send an email at the end of every month to users who have tasks that are still in "assigned" status.
The sendEmail script below finds all the tasks for each user and sends an email to them listing each of their tasks that are still "assigned". EDIT: You indicated in a comment above that emails are in Col 1, tasks are in Col 3 and status is in Col6. I updated the code to reflect that below.
Check out this sample email to see the results.
The second function creates a trigger that runs sendEmail every month. You indicated you wanted to send the email on the last day of the month, but it seems Google has a hard time with that. Some other folks came up with workarounds. I like this one: send the reminder on the 1st of the month, but at 1 in the morning! You can see more of their work here
function sendEmail(){
SpreadsheetApp.getActiveSpreadsheet().getSheetByName("status").activate();
var ss = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
//grab all the data in the sheet, and then filter for any row where the status is still "assigned"
var data = ss.getDataRange().getValues()
var assigned = data.reduce(function(acc, curr) {
if(curr[5] === "assigned") {
acc.push(curr)
}
return acc
},[])
// From all the tasks still in "assigned" status, get a unique list of emails.
var uniqueEmails = []
var compareEmails = []
for (i=0; i<assigned.length; i++){
compareEmails.push(assigned[i][0])
}
uniqueEmails = [...new Set(compareEmails)]
//loop through the unique emails, grab all the tasks for each of those users, and send a simple email with the tasks listed.
for (var i=0; i<uniqueEmails.length; i++){
var tasksPerUser = assigned.reduce(function(acc, curr) {
if(curr[0] === uniqueEmails[i]) {
acc.push(String.fromCharCode(10) + "pending task: " + curr[2]) //this puts a line break before each task, so when the email is sent each one is on its own line.
}
return acc
},[])
console.log(tasksPerUser)
MailApp.sendEmail(uniqueEmails[i],"pending tasks",tasksPerUser)
}
}
function scheduleEmails(){
// Schedules for the first of every month
ScriptApp.newTrigger("sendEmail")
.timeBased()
.onMonthDay(1)
.atHour(1)
.create();
}

How to assign a tag to an event that would allow for 3d conflict detection on a google calendar?

I'm attempting to create a resource management system that would allow members of my office to reserve a room through forms, creating a calendar event on a public calendar.
The issue I am facing in the creation process revolves around conflict checking complications, due to the fact that I would like to have all the calendar events located on one calendar per office. The result is multiple resources on one calendar, and the need for my conflict checking script to not only check for conflicting events but also evaluate the room in which the events are.
Ideally, I would need the script to check first if there are any conflicts, and if there are, it would then check the room name of the conflict event. If the events are in different rooms, it will approve the event creation. If otherwise, it would be denied, and the form respondent would receive an email. I've scoured through the documentation, but with my limited knowledge of the language and coding in general I haven't been able to find a good solution.
At first, I thought I would be able to compare event descriptions, which I would set as the room name based on input from the form submissions. When I called all of the conflicts within the start and end time I would then evaluate their descsriptions with the description of the event to be created and check from there. Unfortunately, from what I tested, it is not possible to do with multiple conflicting events.
I then have what I have listed here where I have an array of the rooms submitted in the past, from the spreadsheet attached to the form, and it checks through them. The issue here is that it doesn't account for start and end times only room names.
My principal question in all of this would be, is there a way to assign a tag of some sort to my event on creation that I would then be able to call back in the conflict checking portion of my script. Ideally, this tag would be something I can assign myself and then evaluate with the new form submission data. I think the best way would be through inviting a room resource to the event, but I have been unable to figure out how to do that as well.
function getConflicts(request){
var conflicts = request.calendar.getEvents(request.dateTime, request.edateTime);
//var description = conflicts.getDescription();
if(conflicts.length >= 1){
Logger.log(conflicts.length);
for (var i=0; i<conflicts.length; i++) {
if (request.room != request.slroom[i]) {
request.status = "Approve";
} else if (request.room == request.slroom[i]) {
request.status = "Conflict";
}
}}
else request.status = "Approve";
}
Full Script:
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheets()[0];
var lastRow = sheet.getLastRow();
var lastColumn = sheet.getLastColumn();
var calendarb = CalendarApp.getCalendarById("link");
function Submission(){
var row = lastRow;
var slrow = (lastRow-1);
this.timestamp = sheet.getRange(row, 1).getValue();
this.name = sheet.getRange(row , 3).getValue() + ": " + sheet.getRange(row, 5).getValue();
this.email = sheet.getRange(row, 2).getValue();
this.dateTime = sheet.getRange(row, 6).getValue();
this.edateTime = sheet.getRange(row,7).getValue();
this.room = sheet.getRange(row, 5).getValue();
this.slroom = sheet.getRange(2, 5, (lastRow-1)).getValues();
this.roomstart =sheet.getRange(2, 6, (lastRow-1)).getValues();
this.roomend = sheet.getRange(2, 7, (lastRow-1)).getValues();
//this.roomInt = this.room.replace(/\D+/g, '');
//this.status;
this.calendar = calendarb;
return this;
var event;
}
function getConflicts(request){
var conflicts = request.calendar.getEvents(request.dateTime, request.edateTime);
//var description = conflicts.getDescription();
if(conflicts.length >= 1){
Logger.log(conflicts.length);
for (var i=0; i<conflicts.length; i++) {
if (request.room != request.slroom[i]) {
request.status = "Approve";
} else if (request.room == request.slroom[i]) {
request.status = "Conflict";
}
}}
else request.status = "Approve";
}
function draftEmail(request){
request.buttonLink = "link";
request.buttonText = "New Request";
switch (request.status) {
case "Approve":
request.subject = "Confirmation: " + request.room + " Reservation for " + request.dateTime + "-" + request.edateTime;
request.header = "Confirmation";
request.message = "Your room reservation has been scheduled.";
break;
case "Conflict":
request.subject = "Conflict with " + request.room + " Reservation for " + request.dateTime + "-" + request.edateTime;
request.header = "Conflict";
request.message = "There is a scheduling conflict. Please pick another room or time."
request.buttonText = "Reschedule";
break;
}
}
function updateCalendar(request){
var event = {
summary: request.name,
location: 'Location',
description: request.room,
start: {
dateTime: request.dateTime.toISOString()
},
end: {
dateTime: request.edateTime.toISOString()
},
attendees: [
{email: request.email}
],
tag: request.room
};
event = Calendar.Events.insert(event, "Calandar ID");
// request.calendar.addSmsReminder(10);
Logger.log('Event Tag: ' + event.description);
Logger.log('Event ID: ' + event.id);
}
function sendEmail(request){
MailApp.sendEmail({
to: request.email,
subject: request.header,
htmlBody: makeEmail(request)
})
sheet.getRange(lastRow, 8).setValue("Sent: " + request.status);
}
function main(){
var request = new Submission();
getConflicts(request);
draftEmail(request);
if (request.status == "Approve") updateCalendar(request);
sendEmail(request);
}
My spreadsheet is set up in this order
A: Timestamp
B: Email Adress
C: Name
D: Location
E: Room
F: Start Date & Time
G: End Date & Time
Sample Sheet
Based on your code, you store your room details in your event's description. Why not use the descriptions of the conflicted events to check if the room is still available? In this manner, we don't need to tag our events.
I also modified some lines as to improve the efficiency of your whole script. Kindly see changes below.
Code:
function Submission() {
// get values by bulk
var rowValues = sheet.getRange(lastRow, 1, 1, 7).getValues().flat();
// avoid using this as that contains another values you don't need
// create a request variable instead
var request = {};
request.timestamp = rowValues[0];
request.name = rowValues[2] + ": " + rowValues[4];
request.email = rowValues[1];
request.dateTime = rowValues[5];
request.edateTime = rowValues[6];
request.room = rowValues[4];
request.calendar = calendarb;
// No need to store previous lists of rooms and their time.
// See getConflicts for clarification
return request;
}
function getConflicts(request) {
var conflicts = request.calendar.getEvents(request.dateTime, request.edateTime);
// Since you stored the room into the description
// Filter the conflicts by comparing conflicts' descriptions to the room
if(conflicts.filter(conflict => conflict.getDescription() == request.room).length > 0)
request.status = "Conflict";
else
request.status = "Approve";
}
Changes:
Get the row values by bulk.
Create request variable instead of using this. The latter contains other values you don't need.
Removed getting past room and date time values for other events.
Makes use of the descriptions of the conflicts and filter them by comparing that to your room.

Sending stock alert emails using google apps script

I am trying to setup a simple google apps script to send emails when inventory on various components hits the defined threshold. After reading and searching I put together a script that appears to work but starts to send erroneous emails when I increase the range being pulled from my spreadsheet.
Here's my code and further details are below.
var ss = SpreadsheetApp.getActive();
var sheet = ss.getSheetByName("Inventory Report");
var howFar = 5 //sheet.getMaxRows(); // how many rows of data
var onHand = sheet.getRange(2,14,howFar,1).getValues();
var minimum = sheet.getRange(2,16,howFar,1).getValues();
var itemNum = sheet.getRange(2,1,howFar,1).getValues();
var itemName = sheet.getRange(2,2,howFar,1).getValues();
var sendTo = "test#gmail.com";
var overStock = 1.5; // warning factor over minimum
function stockAlert() {
for (var i = 0; i < onHand.length; i++) {
if (onHand[i] < minimum[i] * overStock && onHand[i] > minimum[i]) {
MailApp.sendEmail( sendTo , "Testing Stock Reorder Notifications", itemNum[i] + " " + itemName[i] +" - Stock is low! Current stock is " + onHand[i] + ". Minimum is " + minimum[i] + ".");
}
else if (minimum[i] > 0 && onHand[i] < minimum[i]) {
MailApp.sendEmail( sendTo , "Testing Stock Cirtical Notifications", itemNum[i] + " " + itemName[i] +" - Stock is Critical and will be depleted! Current stock is " + onHand[i] + ". Minimum is " + minimum[i] + ".");
}
}
}
In my sheet the values for minimum are 200, 400, 200, 300, 600
In my sheet the values for onHand are 270, 270, 920, 920, 1830
This means I should see one low stock email for the first set of values, one critical stock email for the second set of values and no emails for the last 3 sets of values.
If var howfar = 3, the script sends the two appropriate emails.
If var howfar = 5, I get a third critical stock email for the fifth set of values that should not be sent. Interestingly the email body shows that it's referencing the correct set of values but else if should be false.
The body of the incorrect email reads:
itemNum itemName - Stock is Critical and will be depleted! Current stock is 1830. Minimum is 600.
Given my extensive background in not coding, I hope and assume this will be a simple fix, but any all help is greatly appreciated!
Any chance the values are treated as text in the spreadsheet? Note that the string "1830" is indeed < the string "600". If they are text in the spreadsheet (rather than numbers), then when Apps Script reads the values in, they will be kept as Strings.
edit: indeed, this is the source of your issue - you compare 2D arrays at the Array level:
Logger.log(minimum[i]); // "[600.0]"
Logger.log(typeof minimum[i]); // object
Logger.log(minimum[i][0]); // 600.0
Logger.log(typeof minimum[i][0]); // number
The simplest fix is then to simply access the desired element of the 2D array. Since you acquired only single columns, there is only 1 element in each inner array (at index 0). Thus, <array>[i][0] instead of <array>[i].
An extension to this, which will work for situations in which the sheet values may be text, is to explicitly cast to a number before comparing by using the JS function parseInt(val, radix). Assuming minimum and others are 2D arrays as they are in your question code:
for (var i = 0; i < onHand.length; i++) {
var min = parseInt(minimum[i][0], 10),
avail = parseInt(onHand[i][0], 10);
if (avail < min) {
// send critical stock email
}
else if (avail < min * overStock) {
// send reorder email
}
else {
// on hand amount is > needed
}
}
For a blank string, e.g. parseInt("", 10), or other non-numeric inputs the return value is the number NaN which is neither > or < than actual numbers, so bad inputs should not result in an email being sent.
A different issue is that your script populates global variables with interface calls, which results in slower execution of any script. A better approach is to wrap the related setup in a function:
function getStockAmounts() {
// Return an object of the inventory values.
const stock = SpreadsheetApp.getActive().getSheetByName("some name");
const numHeaders = 1,
numVals = stock.getLastRow() - numHeaders;
return [
{p: 'minimum', col: 16},
{p: 'onHand', col: 14},
{p: 'itemName', col: 2},
{p: 'itemNum', col: 1}
].reduce(function (obj, key) {
obj[key.p] = stock.getRange(numHeaders + 1, key.col, numVals, 1)
.getValues()
// Return a 1-D array, rather than a 2-D array, since all these are single-column variables.
.map(function (row) { return row[0]; });
return obj;
}, {'numVals': numVals});
}
and then call this from your script:
function foo() {
const stocks = getStockAmounts();
for (var i = 0; i < stocks.numVals; i++) {
var min = stocks.minimum[i]; // 1D arrays, so only 1 index is needed.
...
Range#getValues()
Array#reduce

Auto Email Google App Script: do while condition

I been working on this script for long time, it is Google App script, it sends email alert automatically to email provided, and I have script triggered at running every 1 minute.
so if cell total is greater than 201 it will send email to user.
but the problem is it send email every minute script is ran.
I need help with coding where if email has been sent already once, it will not send again unless cell value is less than 201 again and goes back to greater than 201,
I was thinking of making a cell which will contain text "Sent" or "Not Sent"
if it says "Not Sent" let the email code run if total is greater than 201
if is says "Sent" and total is greater than 201 don't let email code run..
I know I am not every clear but It has been very hard to get help on this.
Here is the code.
and if this works I'm sure lots of people can use this script for their use.
function sendEmail(email_address, email_subject, email_message) {
MailApp.sendEmail(email_address, email_subject, email_message);
}
function test_sendEmail() {
var sheet = SpreadsheetApp.getActiveSheet();
var cell = sheet.setActiveCell('A2');
var criterion_cutoff = 201;
var i = 0;
var addr;
var subj;
var msg;
do {
addr = cell.offset(i,0).getValue();
subj = cell.offset(i,1).getValue();
msg = cell.offset(i,2).getValue();
criterion = cell.offset(i,3).getValue();
if(criterion == criterion_cutoff) {
sendEmail(addr,subj,msg);
// Browser.msgBox('Sending email to: ' + addr);
}
i++;
} while( cell.offset(i, 0).getValue().length > 0 )
Browser.msgBox('Done!');
}
so I was think of adding if else condition outside of do while
if(SpreadsheetApp.getActiveSheet().getRange(21,6).getValue() != 'Not Sent') {
do {
//same stuff as above
} while(condition)
}
else
//don't know wht else to do in else condtion so just using googleclock
SpreadsheetApp.getActiveSheet().getRange(2,7).setValue('=GoogleClock()')
I guess I've figured it out on my own. I want to share my solution so other can use it.
so I have added 2 if else condtion
first one will check if cell value is < 201
if it is less than 201 than set somecell example F5 = Not Sent
another if else loop that will check
if value of F5 != Sent and cell value == 201
than it will run the email code
and right after running that email code it will set cell F5 to Sent
so next time loop runs again it will not send email again unless value of cell is
but this has one minor issue it will only work for 1st row out of 3 rows, you can make it for all 3 rows by adding more if conditions, I don't need it so I am not worried about it.
function sendEmail(email_address, email_subject, email_message) {
MailApp.sendEmail(email_address, email_subject, email_message);
}
function test_sendEmail() {
var sheet = SpreadsheetApp.getActiveSheet();
var cell = sheet.setActiveCell('A2');
var criterion_cutoff = 201;
var i = 0;
var addr;
var subj;
var msg;
if((SpreadsheetApp.getActiveSheet().getRange(2,4).getValue() < '201')) {
SpreadsheetApp.getActiveSheet().getRange(2,6).setValue('Not Sent');
}
else
if((SpreadsheetApp.getActiveSheet().getRange(2,6).getValue() != 'Sent') && (SpreadsheetApp.getActiveSheet().getRange(2,4).getValue() == '201')) {
do {
addr = cell.offset(i,0).getValue();
subj = cell.offset(i,1).getValue();
msg = cell.offset(i,2).getValue();
criterion = cell.offset(i,3).getValue();
if(criterion == criterion_cutoff) {
sendEmail(addr,subj,msg);
// Browser.msgBox('Sending email to: ' + addr);
SpreadsheetApp.getActiveSheet().getRange(2,6).setValue('Sent');
}
i++;
} while( cell.offset(i, 0).getValue().length > 0 )
Browser.msgBox('Done!');
}
}