Skip to content

Instantly share code, notes, and snippets.

@jeroenvollenbrock
Created April 12, 2023 11:57
Show Gist options
  • Save jeroenvollenbrock/1ae6e3644d6d35dac691d11d886e444d to your computer and use it in GitHub Desktop.
Save jeroenvollenbrock/1ae6e3644d6d35dac691d11d886e444d to your computer and use it in GitHub Desktop.
const SHEET_TPL = [
'Nick Name',
'Project Name',
'Job Name',
'Approved',
'Draft',
'Not Submitted',
'Pending',
'Total Hours',
'Billable',
];
const NON_BILLABLES = [
'Verlof',
'Overige uren',
'Ziekteverlof',
];
const IGNORED_JOBS = ['TO', 'Parttime'];
const SORT_ORDER = ['Total Hours', 'Job Name', 'Project Name', '-Billable'];
function onOpen(e) {
SpreadsheetApp.getUi()
.createMenu('Zoho')
.addItem('Clean', 'cleanSheet')
.addToUi();
}
function getOrCreateSheet(sheetName, tpl) {
const name = sheetName || SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().getName();
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(name) ?? SpreadsheetApp.getActiveSpreadsheet().insertSheet(name);
const headers = sheet.getRange('1:1').getValues();
const missingColumns = tpl.filter(c => !headers.includes(c));
missingColumns.forEach((column, i) => {
sheet.getRange(1, i + headers.length).setValue(column);
});
return sheet;
}
function camelCase(name) {
return name.toLowerCase().replace(/[^a-zA-Z0-9]+(.)/g, (m, chr) => chr.toUpperCase());
}
function loadSheetObjects(result, range) {
const rows = range.getValues();
result.splice(0, result.length);
result.toDelete = [];
for (let i = 1; i < rows.length; i++) {
const data = {};
for (let j = 0; j < rows[i].length; j++) {
const key = camelCase(rows[0][j]);
data[key] = rows[i][j];
data['set'+key[0].toUpperCase()+key.substring(1)] = (value) => {
data[key] = value;
range.getCell(i+1, j+1).setValue(value);
}
data.softDelete = () => result.toDelete.push(i+1);
}
result.push(data);
}
}
function loadSheetAsObject(sheetName, tpl) {
const sheet = getOrCreateSheet(sheetName, tpl);
const range = sheet.getDataRange();
sheet.setFrozenRows(1);
sheet.clearFormats();
const headers = sheet.getSheetValues(1,1,1,range.getWidth())[0];
const camelCasedHeaders = headers.map(camelCase);
const result = [];
loadSheetObjects(result, range);
result.sheet = sheet;
result.range = range;
result.commitDelete = () => {
let rows = result.toDelete.sort((a,b) => b-a);
while(rows.length) {
let count = rows.findIndex((v, i, a) => i !== 0 && 1+v !== a[i-1]);
count = count < 0 ? rows.length : count;
sheet.deleteRows(rows[count-1], count);
rows = rows.slice(count);
}
loadSheetObjects(result, range);
}
if(sheet.getFilter()) {
sheet.getFilter().remove();
}
const filter = range.createFilter();
result.sortSheet = (...args) => {
args.forEach((arg) => {
const desc = arg[0] === '-';
if(desc) {
arg = arg.substring(1);
}
const idx = headers.indexOf(arg)+1;
filter.sort(idx, !desc);
});
loadSheetObjects(result, range);
}
result.sortColumn = (columnName) => {
sheet.sort(camelCasedHeaders.indexOf(columnName)+1);
loadSheetObjects(result, range);
}
result.hideValues = (columnName, ...hiddenValues) => {
const criteria = SpreadsheetApp.newFilterCriteria()
.setHiddenValues(hiddenValues)
.build();
filter.setColumnFilterCriteria(camelCasedHeaders.indexOf(columnName)+1, criteria);
};
return result;
}
function cleanSheet() {
const hours = loadSheetAsObject(null, SHEET_TPL);
hours.forEach((row, index) => {
if(!row.nickName) {
row.setNickName(hours[index-1].nickName);
}
if(!row.projectName && row.jobName) {
row.setProjectName(hours[index-1].projectName);
}
row.setBillable(NON_BILLABLES.includes(row.projectName) ? 'N' : 'J');
});
hours.sortColumn('jobName');
hours.filter((row)=> {
return IGNORED_JOBS.includes(row.jobName) || !row.jobName;
}).forEach(row => row.softDelete());
hours.commitDelete();
hours.sortSheet(...SORT_ORDER);
hours.hideValues('billable', 'J');
const totals = {};
hours
.filter(entry => entry.billable === 'N')
.forEach(entry => totals[entry.jobName] = entry.totalHours + (totals[entry.jobName] || 0));
totals['Billable'] = hours.filter(entry => entry.billable === 'J').reduce((r, entry) => r += entry.totalHours, 0);
const grandTotal = Object.values(totals).reduce((a,b) => a+b);
totals['Grand Total'] = grandTotal;
const totalRows = Object.entries(totals).sort((a,b) => b[1]-a[1]).map(([category, hours]) => ({
category,
hours,
percentage: (Math.round(10000*hours/grandTotal)/100+'%').replace('.',','),
}));
totalRows.unshift({category: 'Categorie', hours: 'Uren', percentage: 'Percentage'})
const totalRange = hours.sheet.getRange(6, 10, totalRows.length, 3);
for(let i = 0; i < totalRows.length; i++) {
const row = Object.values(totalRows[i]);
for (let j = 0; j < row.length; j++) {
totalRange.getCell(i+1, j+1).setValue(row[j]);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment