const SHEET_USERS = 'Users'; const SHEET_EXAMS = 'Exams'; const SHEET_QUESTIONS = 'Questions'; const SHEET_RESULTS = 'Results'; function setupAwal() { const ss = SpreadsheetApp.getActiveSpreadsheet(); const sheetsInfo = [ { name: SHEET_USERS, headers: ['id', 'username', 'password', 'role'] }, { name: SHEET_EXAMS, headers: ['id', 'title', 'status'] }, { name: SHEET_QUESTIONS, headers: ['id', 'exam_id', 'text', 'opt_a', 'opt_b', 'opt_c', 'opt_d', 'answer', 'image_url'] }, { name: SHEET_RESULTS, headers: ['id', 'exam_id', 'student', 'score', 'is_cheated', 'timestamp'] } ]; sheetsInfo.forEach(info => { let sheet = ss.getSheetByName(info.name); if (!sheet) { sheet = ss.insertSheet(info.name); sheet.appendRow(info.headers); sheet.getRange("A1:Z1").setFontWeight("bold").setBackground("#f3f4f6"); sheet.setFrozenRows(1); } }); // Inject Dummy Data if Users sheet is empty (only header exists) const userSheet = ss.getSheetByName(SHEET_USERS); if (userSheet.getLastRow() === 1) { userSheet.appendRow(['u1', 'admin', '123', 'admin']); userSheet.appendRow(['u2', 'user', '123', 'student']); } return "Setup Berhasil! Silakan deploy sebagai Web App."; } function doPost(e) { const lock = LockService.getScriptLock(); try { lock.waitLock(10000); const action = e.parameter.action; if (action === 'login') return handleLogin(e.parameter); if (action === 'save_exam_full') return handleSaveExamFull(e.parameter); if (action === 'submit_result') return handleSubmitResult(e.parameter); if (action === 'upload_image') return handleUploadImage(e.parameter); return ContentService.createTextOutput(JSON.stringify({status: 'error', message: 'Unknown action'})).setMimeType(ContentService.MimeType.JSON); } catch (err) { return ContentService.createTextOutput(JSON.stringify({status: 'error', message: err.message})).setMimeType(ContentService.MimeType.JSON); } finally { lock.releaseLock(); } } // Untuk menghindari error OPTIONS/CORS preflight function doGet(e) { return ContentService.createTextOutput("Examify Backend Active").setMimeType(ContentService.MimeType.TEXT); } function handleLogin(p) { const ss = SpreadsheetApp.getActiveSpreadsheet(); // Validate User const users = ss.getSheetByName(SHEET_USERS).getDataRange().getValues(); let user = null; for (let i = 1; i < users.length; i++) { if (String(users[i][1]) === String(p.username) && String(users[i][2]) === String(p.password)) { user = { id: users[i][0], username: users[i][1], role: users[i][3] }; break; } } if (!user) return createJSONResponse({status: 'error', message: 'Username / Password salah!'}); // Fetch all related data in one go to avoid multiple roundtrips const exams = getSheetDataAsObjects(ss.getSheetByName(SHEET_EXAMS)); const questions = getSheetDataAsObjects(ss.getSheetByName(SHEET_QUESTIONS)); const results = getSheetDataAsObjects(ss.getSheetByName(SHEET_RESULTS)); return createJSONResponse({ status: 'success', user: user, exams: exams, questions: questions, results: results }); } function handleSaveExamFull(p) { const ss = SpreadsheetApp.getActiveSpreadsheet(); const examSheet = ss.getSheetByName(SHEET_EXAMS); const qSheet = ss.getSheetByName(SHEET_QUESTIONS); // 1. Upsert Exam const exams = examSheet.getDataRange().getValues(); let examFoundIndex = -1; for(let i = 1; i < exams.length; i++) { if(exams[i][0] === p.exam_id) { examFoundIndex = i + 1; break; } } if(examFoundIndex > 0) { examSheet.getRange(examFoundIndex, 2).setValue(p.title); } else { examSheet.appendRow([p.exam_id, p.title, 'active']); } // 2. Overwrite Questions for this exam (Delete old, insert new) // Delete old const qData = qSheet.getDataRange().getValues(); for(let i = qData.length - 1; i >= 1; i--) { if(qData[i][1] === p.exam_id) qSheet.deleteRow(i + 1); } // Insert new const newQs = JSON.parse(p.questions); if(newQs.length > 0) { const rowsToInsert = newQs.map(q => [q.id, q.exam_id, q.text, q.opt_a, q.opt_b, q.opt_c, q.opt_d, q.answer, q.image_url || '']); // Batch insert for performance qSheet.getRange(qSheet.getLastRow() + 1, 1, rowsToInsert.length, 9).setValues(rowsToInsert); } return createJSONResponse({status: 'success'}); } function handleSubmitResult(p) { const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SHEET_RESULTS); const isCheated = p.is_cheated === 'true' || p.is_cheated === true; sheet.appendRow([ p.id, p.exam_id, p.student, Number(p.score), isCheated, new Date().toISOString() ]); return createJSONResponse({status: 'success'}); } function getSheetDataAsObjects(sheet) { if (sheet.getLastRow() <= 1) return []; const data = sheet.getDataRange().getValues(); const headers = data[0]; const result = []; for (let i = 1; i < data.length; i++) { const obj = {}; for (let j = 0; j < headers.length; j++) { obj[headers[j]] = data[i][j]; } result.push(obj); } return result; } function createJSONResponse(data) { return ContentService.createTextOutput(JSON.stringify(data)).setMimeType(ContentService.MimeType.JSON); } /* PLACEHOLDER: FUNGSI CRUD BARU */ function handleUploadImage(p) { try { const base64Data = p.base64.split(',')[1]; const blob = Utilities.newBlob(Utilities.base64Decode(base64Data), p.mimeType || 'image/jpeg', p.filename || ('img_' + Date.now() + '.jpg')); const folderName = 'Examify_Uploads'; let folders = DriveApp.getFoldersByName(folderName); let folder = folders.hasNext() ? folders.next() : DriveApp.createFolder(folderName); const file = folder.createFile(blob); file.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW); const fileId = file.getId(); const fileUrl = `https://drive.google.com/uc?export=view&id=${fileId}`; // Direct view link return createJSONResponse({status: 'success', url: fileUrl}); } catch (err) { return createJSONResponse({status: 'error', message: err.message}); } }