Automatically locking a range (column) as each date passes? - javascript

I have a sheet that employees will update daily with information about tasks done that day. Each column has a date in the header row (row 3 in this case), and after the end of the following day I want that column to lock so it cannot be edited further except by myself and one other. This is to prevent people from covering up mistakes or accidentally changing or deleting data.
I am looking for a script or something that will accomplish this. This sheet has about 45 tabs and I need the same thing applied to all of them.
My idea is possibly a script that triggers at a certain time based off the date in the header row, so if the date is May 5th 2017, the respective column would lock itself at midnight on the 6th.
A link to a copy of my sheet, minus data is here.
Alternatively, if there is a way to simply lock any cell 24 hours after the most recent data is entered into it, and prevent further editing by everyone except select people, that could work too if the ideal method isn't doable.

Yes, there is a way to do this.
I will briefly describe the solution:
Let's say that the first row has 1:1 contains consecutive dates.
Create function lockColumns which would create new protected range.
Add function lockColumns to time trigger, which triggers every day between 0:01 and 1:00 am.
And now some code:
function lockColumns() {
var ss = SpreadsheetApp.getActive().getSheetByName('Sheet 1')
var range = ss.getRange('1:1').getValues()[0];
var today = new Date();
var todayCol = null;
for (var i=0; i<range.length; i++) {
if (today.isSameDateAs(range[i])) {
todayCol = i;
break;
}
}
var rangeToProtect = ss.getRange(1, todayCol +1, ss.getMaxRows(), 1)
var protection = rangeToProtect.protect().setDescription('Protected range');
// Ensure the current user is an editor before removing others. Otherwise, if the user's edit
// permission comes from a group, the script will throw an exception upon removing the group.
var me = Session.getEffectiveUser();
protection.addEditor(me);
protection.removeEditors(protection.getEditors());
if (protection.canDomainEdit()) {
protection.setDomainEdit(false);
}
protection.addEditor('email#gmail.com'); // second person with edit permissions
}
/*
http://stackoverflow.com/a/4428396/2351523
*/
Date.prototype.isSameDateAs = function(pDate) {
return (
this.getFullYear() === pDate.getFullYear() &&
this.getMonth() === pDate.getMonth() &&
this.getDate() === pDate.getDate()
);
}

Related

Using Apps Script for Google Sheets, Insert a Header Row after change in cell value

I have not done any scripting for about 7 years. I used to write a lot of VBA scripts with SQL for Access and Excel. I am now trying to automate a Google sheet row insertion with formatting and a cell value edit in the new row.
I am hoping someone who can script with their eyes closed will help this old lady with a script to automate a volunteer task for an all volunteer food coop so that I can pass this task on to someone with less spreadsheet skill.
I have written the task in my own fudgy language and am hoping someone can translate it into the proper language and syntax. Here it is:
function (createReceivingSheet)
for each cell in range A2: A500
if right(this.cell, 6) != right(this.cell.-1, 6)
insert.row.above(this.cell)
format(new row) bold, underline, font:arial, 12pt
merge (newrow.column1:column5)
format (newrow.cell.column1) border:bottom
case edit(newrow,cell.column1)
when original.cell = "02 GM *" then "GO MACRO",
when original.cell = "000 *" then "PRODUCE"
end
End function
In other words I want to insert a formatted title row above each change in vendor where the vendor code is the first 6 characters of the cells in column A.
I need the script to iterate through Col A:
compare each cell with the cell above
if the first 6 characters of the current cell are not equal to the first 6 characters of the cell above, then insert a row above
format the newly inserted row in bold, underline, 12 pt
merge the first 5 columns in the newly inserted row
format the merged cells in the newly inserted row with a bottom border
populate the first cell (column A) of the newly inserted row with a value based on a set of case statements when the original cell = "X" or "Y" or "Z" etc.
I do not know if this is an appropriate question to ask on this forum. Please let me know if you can help or if this is too much to ask on this forum.
I think this covers most of your pseudocode. At the bottom, you'll have to add the values that you want to put into the new cells.
This onOpen function adds a new menu after Help. Select a range then use the menu item to run partitionVendors.
There's good documentation here for when you need to add more features: https://developers.google.com/apps-script/reference/spreadsheet/spreadsheet-app?hl=en
const VENDOR_PREFIX_LENGTH = 6;
const MENU_TITLE = "Stack Overflow";
/**
* Adds a menu to the spreadsheet when the file is opened
*/
function onOpen(e) {
var ui = SpreadsheetApp.getUi();
const menu = ui.createMenu(MENU_TITLE)
menu.addItem("partition vendors", "partitionVendors")
.addToUi();
}
function partitionVendors() {
const sheet = SpreadsheetApp.getActiveSpreadsheet();
const range = SpreadsheetApp.getActiveRange();
for (let r = range.getNumRows(); r > 1; r--) {
const currentCellValue = range.getCell(r, 1).getValues()[0][0];
const previousCellValue = range.getCell(r - 1, 1).getValues()[0][0];
if (currentCellValue.slice(0, VENDOR_PREFIX_LENGTH) !== previousCellValue.slice(0, VENDOR_PREFIX_LENGTH)) {
const newRowNum = range.getLastRow() - range.getNumRows() + r;
sheet.insertRowBefore(newRowNum);
// assuming the selection is in column A, merge columns A to E
const newCellRange = sheet.getRange(`A${newRowNum}:E${newRowNum}`);
newCellRange.merge()
.setFontWeight("bold")
.setFontLine("underline")
.setFontFamily("Arial")
.setFontSize(12)
.setBorder(null, null, true, null, null, null);
// populate values here
if (currentCellValue === "value1") {
newCellRange.setValue("header for value1");
} // else if ...
}
}
}

Check if a column header title matches a string and if so then return the column index

I am trying to write a script for google sheets which returns the date in the next cell when the user enters 'y' in the current cell. I have a script which does this already, but the problem with my script is that the columns which it is evaluating is based on the column index, which means if our data set ever grows then these columns always have to stay in the same index which is creating a lot of organizational issues.
My question is..
Is it possible to look for the column header title rather than the column index in my code, and if so, what changes would I need to make?
function onEdit(e) {
if ([19].indexOf(e.range.columnStart) == -1 || ['y', 'Y'].indexOf(e.value) == -1) return;
e.range.offset(0, 1)
.setValue(Utilities.formatDate(new Date(), "GMT-5", "MM-dd-yyyy"))
}
This code currently looks at column index 19 and when either 'y' or 'Y' is entered into a cell in column index 19 it then outputs the date in the next cell in column 20.
How can I change the code to look for where the column header = 'Replied?' rather than index?
Goal:
If the following criteria is met:
Value is written into column 19 (S).
Header of column 19 (S) is 'Replied?'.
Value written is either 'Y' or 'y'.
Then write a date into the adjacent cell.
Code:
function onEdit(e) {
var sh = e.source.getActiveSheet();
var row = e.range.getRow();
var col = e.range.getColumn();
var value = e.value.toUpperCase();
var header = sh.getRange(1, col).getValue();
if (col === 19 && value === 'Y' && header === 'Replied?') {
sh.getRange(row, 20).setValue(Utilities.formatDate(new Date(), "GMT-5", "MM-dd-yyyy"))
}
}
Explanation:
I've based everything on the event objects passed to your onEdit trigger. For var value I have used toUpperCase() so that we don't have to check for either 'Y' OR 'y', only 'Y' alone. Also, instead of using range.offset I have just specified column 20 specifically in the getRange().setValue().
References:
Event Objects
String.toUpperCase()
One possible way to do this is to name the column/ cell in google sheets. See this website on how to.
Basically:
Open a spreadsheet in Google Sheets.
Select the cells you want to name.
Click Data and then Named ranges. A menu will open on the right.
Type the range name you want.
To change the range, click Spreadsheet Grid.
Select a range in the spreadsheet or type the new range into the text box, then - click Ok.
Click Done.
You can then refer to that named cell in google scripts by creating a custom function
function myGetRangeByName(n) { // just a wrapper
return SpreadsheetApp.getActiveSpreadsheet().getRangeByName(n).getA1Notation();
}
Then, in a cell on the spreadsheet:
myGetRangeByName("Names")
I'd do this.
function onEdit(e) {
var editedColumn = e.range.columnStart;
var sh = SpreadsheetApp.getActiveSpreadsheet();
var ss = sh.getSheetByName("This");//you only want onedits to the specific page
var data = ss.getDataRange().getValues();
var header = data[0][editedColumn];
if (header != "Replied") return;
if(e.value.toLowerCase() == "y"){
e.range.offset(0, 1)
.setValue(Utilities.formatDate(new Date(), "GMT-5", "MM-dd-yyyy"));}
}
You could also consider using a checkbox, that might be faster for your users.

Automatic sort multiple columns once entire row of data is entered

I have designed a google spreadsheet to help improve efficiency of material flow. I want to automatically sort the data by 2 different columns to prioritize critical parts that need to be received first once the entire row of data is entered. The problem I am having is that the data is sorting as soon as you enter one of the columns I am calling to sort but the columns I want to sort are not the last column of that row of data that needs to be entered. I am trying to use an if statement to not execute the sort until the last column has been entered else throw an error statement that says you must enter data in this column to proceed. Logically, the code makes sense to me but I have only an adequate understanding of computer language. I keep receiving an error in line 10 that the range is not found. I believe my error is the syntax in trying to call the last column. Any help would be greatly appreciated
**function autosort(){
// Variable Declaration
var sheet = SpreadsheetApp.getActiveSpreadsheet();
var NewsheetName = SpreadsheetApp.getActiveSheet().getName();
var sheetName = sheet.getSheetByName(NewsheetName);
var lastCol = sheetName.getLastColumn();
var lastColBlank = SpreadsheetApp.getActiveSheet().getRange(lastCol).isBlank()
// Find range to sort
var range = sheetName.getRange("A2:G");
// Sorting algorithm
if (lastColBlank == false ){
range.sort([6,5]);
}
else {
throw ("error: If trailer # is unavailable, please enter N/A");
}
}**
Try this code:
function onEdit() {
var sh = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
var editedCell = sh.getActiveRange().getColumnIndex();
if(editedCell == 2) {
var range = sh.getRange("A2:B10");
var blank = range.isBlank()
var values = range.getValues();
Logger.log(values)
for (var i = 0; i<values.length; i++) {
//values.length returns 11 for eleven items
// values[10][0] would be the 11th row 1st column
Logger.log(values[i][1])
if (values[i][0] == "" || values[i][1] == "") {
//throw new Error("error: If trailer # is unavailable, please enter N/A");
var ui = SpreadsheetApp.getUi();
var response = ui.alert('If trailer # is unavailable, please enter N/A');
}else{
range.sort({column: 2});
}
}
}
}
If you'll look into the code, the sort depends on the second column to be edited. Then, will check the range if the is still a blanked cell.
Just apply your additional code to meet your goal and I think that will work.
Hope this helps.

Hide all but today's columns on four sheets (Google Spreadsheets script)

Goal: I'm trying to create a behavior tracker for four classes in Google Spreadsheets. The tracker has nine sheets: Class7A, Class7B, Class8A, Class8B, and Mon-Fri summary sheets. The goal was for each ClassXX sheet to have behavior tracking information for an entire week, but for the default view to show only the current day's information.
Attempts: During initial workup (with only the Class7A sheet created), I got this to work using a modification of the script found here (Thank you Jacob Jan Tuinstra!): Optimize Google Script for Hiding Columns
I modified it to check the value in the third row of each column (which held a 1 for Monday, 2 for Tuesday, etc), and if it did not match the numerical equivalent for the day of the week (var d = new Date(); var n = d.getDay();), then it would hide that column. This process was somewhat slow - I'm assuming because of the iterating through each column - but it worked.
Quite excited, I went ahead and added the rest of the sheets, and tried again - but the code as written, seems to affect only the current sheet. I tried modifying it by replacing var sheet = ss.getSheets()[0]; with for script that iterated through the columns, until i>4 (I've since lost that piece of code), with no luck.
Deciding to go back and try adapting the original version of the script to instead explicitly run multiple times for each named sheet, I found the that script no longer seems to work at all. I get various version of "cannot find XX function in sheet" or "cannot find XX function in Range."
Source: A shared version (with student info scrubbed) can be found here: https://docs.google.com/spreadsheets/d/1OMq4a4_Gh_xyNk_IRy-mwJn5Hq36RXmdAzTzx7dGii0/edit?usp=sharing (editing is on).
Stretch Goal: Ultimately, I need to get this to reliably show only the current day's columns (either through preset ranges (same for each sheet), or the 1-5 values), and I need it to do so for all four ClassXX sheets, but not the summary pages (and preferably more quickly than the iterations). If necessary, I can remove the summary pages and set them up externally, but that's not my first preference. I would deeply appreciate any help with this; so far my attempts have seemed to only take me backwards.
Thanks!
Current code:
function onOpen() {
// get active spreadsheet
var ss = SpreadsheetApp.getActiveSpreadsheet();
// create menu
var menu = [
{name: "Show Today Only", functionName: "hideColumn"},
{name: "Show All Days", functionName: "showColumn"},
{name: "Clear Week - WARNING will delete all data", functionName: "clearWeek"}
];
// add to menu
ss.addMenu("Show Days", menu);
}
var d = new Date();
var n = d.getDay();
function hideColumn() {
// get active spreadsheet
var ss = SpreadsheetApp.getActiveSpreadsheet();
// get first sheet
var sheet = ss.getSheets()[0];
// get data
var data = sheet.getDataRange();
// get number of columns
var lastCol = data.getLastColumn()+1;
Logger.log(lastCol);
// itterate through columns
for(var i=1; i<lastCol; i++) {
if(data.getCell(2, i).getValue() != n) {
sheet.hideColumns(i);
}
}
}
function showColumn() {
// get active spreadsheet
var ss = SpreadsheetApp.getActiveSpreadsheet();
// get first sheet
var sheet = ss.getSheets()[0];
// get data
var data = sheet.getDataRange();
// get number of columns
var lastCol = data.getLastColumn();
// show all columns
sheet.showColumns(1, lastCol);
}
I cannot recreate the problem of the script not working at all, it's working fine for Class7A so that part is working fine.
So let's look at the two other problems:
Applying this to all Sheets
Speeding up the script
First let's create some globals we use in both functions
var d = new Date();
var n = d.getDay();
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheetNames = ss.getSheets().map(function(sheet) {return sheet.getName();});
var classSheets = sheetNames.filter(function(sheetName) {return sheetName.match("Class")});
Now we can iterate over classSheets and get the sheet by name and hide columns in each.
However hiding each individual column is very slow.
The sheet is built very structured, every week has 12 columns (except for friday which doesn't have the grey bar), so we can just calculate the ranges we want to hide.
function hideColumn() {
classSheets.map(function(sheetName){
var sheet = ss.getSheetByName(sheetName);
if (n == 1) {
// Hide everything after the first three columns + Monday
sheet.hideColumns(3 + 11, 12 * 4);
} else if (n == 5) {
// Hide everything to the left except the leftmost three columns
sheet.hideColumns(3, 4 * 12);
} else {
// Hide everything left of the current day
sheet.hideColumns(3, (n - 1) * 12);
// Hide everything after the current day
sheet.hideColumns(3 + n * 12, (5 - n) * 12 - 1);
}
});
}
Lastly we can shorten showColumn
function showColumn() {
classSheets.map(function(sheetName){
var sheet = ss.getSheetByName(sheetName);
var lastCol = sheet.getLastColumn();
sheet.showColumns(1, lastCol);
});
}

How i get appointment time (From time,To time) in jquery

I am selecting time slot on dragging on time slot cell. After selecting time slot, I enter patient name in textbox and click on select button then patient name goes to selected time slot. The user can select multiple time slot for multilpe patient name and onclick of allot button I have to insert patient name with time slot (From time To time) to database.
I have problem in getting alloted time slot ie.From time and To time in jquery.
$("#btnAllot").click(function () {
//how i get alloted time here.
$('tr').each(function () {
$(this).find('td').each(function () {
if ($(this).hasClass('yell')) {
alert($(this).closest('tr').find('td:eq(0)').text());
};
});
});
}
see jsbin on dragging on time slot cell
Ok, here is one way to do it:
You iterate over each row whose third cell has a rowspan attribute. This indicates the start of a new appointment. You can get the start time by examining the siblings (sort of) and the end time by getting the row that is rowspan - 1 elements away.
There might be better ways, but this might give you a start.
For example:
var find_closest_hour = function($row) {
var $cell = $row.children('td:first-child'),
hour = "";
// probably add something here
while($cell.length && !(hour = $.trim($cell.text()))) {
$cell = $cell.parent().prev().children('td:first-child');
}
return hour;
};
var $all_tds = $('#tableAppointment tr td:nth-child(3)'),
$tds = $all_tds.filter('[rowspan]');
// will contain a list of objects [{patient: name, start: time, end: time},...]
var appointments = $tds.map(function() {
var $this = $(this),
$row = $this.parent(),
$cells = $row.children('td'),
patient = $.trim($this.text()),
start = find_closest_hour($row).split(':', 1) + ":" + $.trim($cells.eq(1).text()),
$end_row, end;
if(this.rowspan == 1) {
end = start;
}
else {
$end_row = $all_tds.eq($all_tds.index(this) + this.rowSpan - 1).parent();
end = find_closest_hour($end_row).split(':', 1) + ":" + $.trim($end_row.children('td').eq(1).text());
}
return {patient: patient, start: start, end: end};
}).get();
DEMO
I will let you figure out how to format the times properly ;)
Note: This very much depends on your current table structure and is likely to break if you change it. To make things more flexible, you could add classes to cells that contain the important information, for example hour to each first cell that contains the hour of the day and appointment_start to the cell of an appointment. Then you could search for/filter by these.

Categories