/** Astra Learning Hub - educational Google Sheets approval tracker. * Bound to a Google Sheet. Does not send messages or use external APIs. * Authorization: only accounts in APPROVERS may approve/reject. * IMPORTANT: This is a demonstration, not an enterprise access-control system. */ const CFG = Object.freeze({ REQUESTS: 'Requests', AUDIT: 'Audit', APPROVERS: ['approver@example.com'], // REPLACE before use REQUEST_HEADERS: ['ID','Created At','Requester','Type','Description','Status','Updated At','Reviewer','Decision Note'], AUDIT_HEADERS: ['At','ID','Action','Actor','Note'] }); function onOpen() { SpreadsheetApp.getUi().createMenu('Astra Tracker') .addItem('1. Initialize sheets','initializeTracker') .addItem('2. Add sample request','addSampleRequest') .addItem('3. Review selected request','reviewSelectedRequest') .addToUi(); } function initializeTracker() { const ss = SpreadsheetApp.getActiveSpreadsheet(); [[CFG.REQUESTS, CFG.REQUEST_HEADERS],[CFG.AUDIT, CFG.AUDIT_HEADERS]].forEach(([name,headers])=>{ let sh=ss.getSheetByName(name) || ss.insertSheet(name); if (sh.getLastRow()===0) sh.appendRow(headers); else { const found=sh.getRange(1,1,1,headers.length).getValues()[0]; if (JSON.stringify(found)!==JSON.stringify(headers)) throw new Error('Unexpected headers in '+name+'; do not overwrite existing data.'); } sh.setFrozenRows(1); }); SpreadsheetApp.getUi().alert('Tracker sheets ready. Set APPROVERS in Code.gs.'); } function addSampleRequest(){ const ui=SpreadsheetApp.getUi(); const who=ui.prompt('Requester name').getResponseText().trim(); if(!who || who.length>100){ui.alert('Enter a name (1–100 characters).');return;} const description=ui.prompt('Request description').getResponseText().trim(); if(!description || description.length>500){ui.alert('Enter a description (1–500 characters).');return;} const id=createRequest_(who,'General',description); ui.alert('Created request: '+id); } function createRequest_(requester,type,description){ const lock=LockService.getDocumentLock(); if(!lock)throw new Error('Open as a spreadsheet-bound script.'); lock.waitLock(20000); try { const ss=SpreadsheetApp.getActiveSpreadsheet(); const sh=ss.getSheetByName(CFG.REQUESTS),audit=ss.getSheetByName(CFG.AUDIT); if(!sh || !audit)throw new Error('Run Initialize sheets first.'); const id='AST-'+Utilities.getUuid().slice(0,8).toUpperCase(); const now=new Date(); sh.appendRow([id,now,requester,type,description,'Pending',now,'','']); audit.appendRow([now,id,'CREATED',requester,'']); return id; } finally {lock.releaseLock();} } function reviewSelectedRequest(){ const ui=SpreadsheetApp.getUi(),ss=SpreadsheetApp.getActiveSpreadsheet(); const sh=ss.getActiveSheet(); if(sh.getName()!==CFG.REQUESTS || sh.getActiveRange().getRow()<2){ui.alert('Select a request row in Requests sheet.');return;} const row=sh.getActiveRange().getRow(); const actor=Session.getActiveUser().getEmail().toLowerCase(); if(!actor || !CFG.APPROVERS.map(x=>x.toLowerCase()).includes(actor)){ ui.alert('Not authorized. Set your verified email in APPROVERS.');return; } const action=ui.prompt('Decision: APPROVE or REJECT').getResponseText().trim().toUpperCase(); if(!['APPROVE','REJECT'].includes(action)){ui.alert('Cancelled: enter APPROVE or REJECT.');return;} const note=ui.prompt('Decision note').getResponseText().trim(); if(!note || note.length>500){ui.alert('A decision note (1–500 characters) is required.');return;} const lock=LockService.getDocumentLock();lock.waitLock(20000); try{ const id=String(sh.getRange(row,1).getValue()); const status=String(sh.getRange(row,6).getValue()); if(!id || status!=='Pending'){ui.alert('Only Pending requests can be reviewed.');return;} const now=new Date(); sh.getRange(row,6,1,4).setValues([[action==='APPROVE'?'Approved':'Rejected',now,actor,note]]); ss.getSheetByName(CFG.AUDIT).appendRow([now,id,action,actor,note]); ui.alert(id+' '+(action==='APPROVE'?'Approved':'Rejected')); }finally{lock.releaseLock();} }