Created
April 12, 2023 11:57
-
-
Save jeroenvollenbrock/1ae6e3644d6d35dac691d11d886e444d to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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