https://saweria.co/jamilsaiful

CBT MTS

SCRIPT CBT CONTOH

/** * KSL UjianKu VER 1 * Google Apps Script + Google Sheets CBT * Optimized: batch reads, document cache, one-page app, no external libraries. */ const APP = { VERSION: '1.0.0', SHEETS: { SETTINGS: 'PENGATURAN', STUDENTS: 'PESERTA', SUBJECTS: 'MAPEL', QUESTIONS: 'BANK_SOAL', EXAMS: 'UJIAN', EXAM_QUESTIONS: 'UJIAN_SOAL', SESSIONS: 'SESI_UJIAN', ANSWERS: 'JAWABAN', RESULTS: 'HASIL_UJIAN', ADMINS: 'ADMIN', AUDIT: 'AUDIT_LOG' }, SESSION_TTL: 21600, // 6 jam CACHE_TTL: 300 }; function doGet() { return HtmlService.createTemplateFromFile('Index') .evaluate() .setTitle('KSL UjianKu') .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL) .addMetaTag('viewport', 'width=device-width, initial-scale=1, viewport-fit=cover'); } function include(filename) { return HtmlService.createHtmlOutputFromFile(filename).getContent(); } /** * Jalankan sekali pada spreadsheet kosong/baru. */ function setupUjianKuVer1() { const ss = SpreadsheetApp.getActive(); const specs = [ [APP.SHEETS.SETTINGS, ['KEY','VALUE','UPDATED_AT']], [APP.SHEETS.STUDENTS, ['PESERTA_ID','NIS','NAMA','KELAS','USERNAME','PASSWORD_HASH','STATUS','CREATED_AT','UPDATED_AT']], [APP.SHEETS.SUBJECTS, ['MAPEL_ID','KODE','NAMA_MAPEL','STATUS','CREATED_AT']], [APP.SHEETS.QUESTIONS, ['SOAL_ID','MAPEL_ID','TIPE','PERTANYAAN','GAMBAR_URL','OPSI_A','OPSI_B','OPSI_C','OPSI_D','OPSI_E','KUNCI','BOBOT','STATUS','CREATED_AT','UPDATED_AT']], [APP.SHEETS.EXAMS, ['UJIAN_ID','KODE_UJIAN','JUDUL','MAPEL_ID','KELAS_TARGET','MULAI_AT','SELESAI_AT','DURASI_MENIT','KKM','ACAK_SOAL','ACAK_OPSI','TAMPIL_NILAI','STATUS','CREATED_AT','UPDATED_AT']], [APP.SHEETS.EXAM_QUESTIONS, ['UJIAN_ID','SOAL_ID','URUTAN']], [APP.SHEETS.SESSIONS, ['SESSION_ID','UJIAN_ID','PESERTA_ID','START_AT','DEADLINE_AT','SUBMIT_AT','STATUS','QUESTION_ORDER_JSON','OPTION_ORDER_JSON','LAST_HEARTBEAT','CREATED_AT']], [APP.SHEETS.ANSWERS, ['SESSION_ID','UJIAN_ID','PESERTA_ID','SOAL_ID','JAWABAN','RAGU','ANSWERED_AT']], [APP.SHEETS.RESULTS, ['HASIL_ID','SESSION_ID','UJIAN_ID','PESERTA_ID','BENAR','SALAH','KOSONG','SKOR','LULUS','SUBMIT_AT']], [APP.SHEETS.ADMINS, ['ADMIN_ID','USERNAME','PASSWORD_HASH','NAMA','ROLE','STATUS','CREATED_AT']], [APP.SHEETS.AUDIT, ['LOG_ID','WAKTU','AKTOR_ID','AKTOR_NAMA','AKSI','OBJEK','DETAIL_JSON']] ]; specs.forEach(([name, headers]) => ensureSheet_(ss, name, headers)); seedSettings_(); seedDemoData_(); formatSheets_(); SpreadsheetApp.flush(); return {ok:true, message:'KSL UjianKu VER 1 siap digunakan.'}; } function ensureSheet_(ss, name, headers) { let sh = ss.getSheetByName(name); if (!sh) sh = ss.insertSheet(name); if (sh.getLastRow() === 0) { sh.getRange(1,1,1,headers.length).setValues([headers]); sh.setFrozenRows(1); } else { const existing = sh.getRange(1,1,1,headers.length).getValues()[0]; if (existing.join('|') !== headers.join('|')) { sh.getRange(1,1,1,headers.length).setValues([headers]); } } } function seedSettings_() { const sh = sh_(APP.SHEETS.SETTINGS); if (sh.getLastRow() > 1) return; const now = new Date(); const rows = [ ['APP_NAME','KSL UjianKu',now], ['SCHOOL_NAME','SMA Kucing Storia',now], ['SCHOOL_SUBTITLE','Computer Based Test',now], ['PRIMARY_COLOR','#2563EB',now], ['ACCENT_COLOR','#7C3AED',now], ['LOGO_URL','',now], ['WELCOME_TEXT','Ujian digital yang cepat, fokus, dan nyaman digunakan.',now] ]; sh.getRange(2,1,rows.length,3).setValues(rows); } function seedDemoData_() { const now = new Date(); const students = sh_(APP.SHEETS.STUDENTS); if (students.getLastRow() === 1) { const pwd = hash_('123456'); students.getRange(2,1,3,9).setValues([ ['P001','2026001','Ahmad Fauzan','IX A','2026001',pwd,'AKTIF',now,now], ['P002','2026002','Aisyah Putri','IX A','2026002',pwd,'AKTIF',now,now], ['P003','2026003','Bima Pratama','IX A','2026003',pwd,'AKTIF',now,now] ]); } const subjects = sh_(APP.SHEETS.SUBJECTS); if (subjects.getLastRow() === 1) { subjects.getRange(2,1,2,5).setValues([ ['M001','MTK','Matematika','AKTIF',now], ['M002','IPA','Ilmu Pengetahuan Alam','AKTIF',now] ]); } const questions = sh_(APP.SHEETS.QUESTIONS); if (questions.getLastRow() === 1) { const qs = [ ['S001','M001','PG','Hasil dari 12 × 8 adalah...','','86','92','96','108','','C',1,'AKTIF',now,now], ['S002','M001','PG','Nilai dari 144 ÷ 12 adalah...','','10','11','12','14','','C',1,'AKTIF',now,now], ['S003','M001','PG','Jika x + 7 = 15, maka nilai x adalah...','','6','7','8','9','','C',1,'AKTIF',now,now], ['S004','M001','PG','Keliling persegi dengan sisi 9 cm adalah...','','18 cm','27 cm','36 cm','81 cm','','C',1,'AKTIF',now,now], ['S005','M001','PG','25% dari 200 adalah...','','25','40','50','75','','C',1,'AKTIF',now,now], ['S006','M001','PG','Bilangan prima di bawah ini adalah...','','21','27','29','33','','C',1,'AKTIF',now,now], ['S007','M001','PG','Hasil 3² + 4² adalah...','','7','12','25','49','','C',1,'AKTIF',now,now], ['S008','M001','PG','Bentuk desimal dari 3/4 adalah...','','0,25','0,5','0,75','1,25','','C',1,'AKTIF',now,now], ['S009','M001','PG','Rata-rata dari 6, 8, dan 10 adalah...','','7','8','9','10','','B',1,'AKTIF',now,now], ['S010','M001','PG','Luas segitiga dengan alas 10 cm dan tinggi 8 cm adalah...','','18 cm²','40 cm²','80 cm²','90 cm²','','B',1,'AKTIF',now,now] ]; questions.getRange(2,1,qs.length,15).setValues(qs); } const exams = sh_(APP.SHEETS.EXAMS); if (exams.getLastRow() === 1) { const start = new Date(now.getTime() - 24*60*60*1000); const end = new Date(now.getTime() + 30*24*60*60*1000); exams.getRange(2,1,1,15).setValues([[ 'U001','MTK-IXA-01','Ujian Matematika Dasar','M001','IX A', start,end,60,75,true,true,true,'AKTIF',now,now ]]); } const eq = sh_(APP.SHEETS.EXAM_QUESTIONS); if (eq.getLastRow() === 1) { const rows = Array.from({length:10}, (_,i)=>['U001','S'+String(i+1).padStart(3,'0'),i+1]); eq.getRange(2,1,rows.length,3).setValues(rows); } const admins = sh_(APP.SHEETS.ADMINS); if (admins.getLastRow() === 1) { admins.getRange(2,1,1,7).setValues([[ 'A001','admin',hash_('admin123'),'Administrator','SUPERADMIN','AKTIF',now ]]); } } /* ========================= PUBLIC API ========================= */ function getPublicBootstrap() { const settings = getSettings_(); return { ok: true, version: APP.VERSION, brand: { appName: settings.APP_NAME || 'KSL UjianKu', schoolName: settings.SCHOOL_NAME || '', subtitle: settings.SCHOOL_SUBTITLE || '', primary: settings.PRIMARY_COLOR || '#2563EB', accent: settings.ACCENT_COLOR || '#7C3AED', logoUrl: settings.LOGO_URL || '', welcomeText: settings.WELCOME_TEXT || '' } }; } function loginPeserta(payload) { payload = payload || {}; const username = clean_(payload.username, 80); const password = String(payload.password || ''); if (!username || !password) throw new Error('Username dan password wajib diisi.'); const rows = dataObjects_(APP.SHEETS.STUDENTS); const p = rows.find(x => String(x.USERNAME).trim() === username && String(x.STATUS).toUpperCase() === 'AKTIF' && safeEq_(String(x.PASSWORD_HASH), hash_(password)) ); if (!p) throw new Error('Username atau password tidak sesuai.'); const token = Utilities.getUuid().replace(/-/g,''); CacheService.getScriptCache().put('auth:'+token, JSON.stringify({ pesertaId:p.PESERTA_ID, nama:p.NAMA, kelas:p.KELAS, nis:p.NIS, type:'peserta' }), APP.SESSION_TTL); audit_(p.PESERTA_ID, p.NAMA, 'LOGIN', 'PESERTA', {kelas:p.KELAS}); return {ok:true, token, user:{id:p.PESERTA_ID,nama:p.NAMA,kelas:p.KELAS,nis:p.NIS}}; } function getDashboardPeserta(token) { const auth = auth_(token, 'peserta'); const now = new Date(); const exams = dataObjects_(APP.SHEETS.EXAMS) .filter(x => String(x.STATUS).toUpperCase()==='AKTIF') .filter(x => !x.KELAS_TARGET || String(x.KELAS_TARGET).trim() === String(auth.kelas).trim()) .map(x => { const start = toDate_(x.MULAI_AT), end = toDate_(x.SELESAI_AT); let state = 'TERSEDIA'; if (start && now < start) state = 'TERJADWAL'; if (end && now > end) state = 'SELESAI'; const result = findResult_(x.UJIAN_ID, auth.pesertaId); if (result) state = 'SUDAH_DIKERJAKAN'; return { ujianId:x.UJIAN_ID, kode:x.KODE_UJIAN, judul:x.JUDUL, mapelId:x.MAPEL_ID, mulai:iso_(start), selesai:iso_(end), durasi:Number(x.DURASI_MENIT||0), kkm:Number(x.KKM||0), tampilNilai:bool_(x.TAMPIL_NILAI), state, result: result ? {skor:Number(result.SKOR||0), lulus:String(result.LULUS)==='YA'} : null }; }); return {ok:true, user:auth, exams}; } function startExam(token, ujianId) { const auth = auth_(token, 'peserta'); const exam = dataObjects_(APP.SHEETS.EXAMS).find(x => String(x.UJIAN_ID) === String(ujianId)); if (!exam || String(exam.STATUS).toUpperCase() !== 'AKTIF') throw new Error('Ujian tidak tersedia.'); const now = new Date(); const startAt = toDate_(exam.MULAI_AT), endAt = toDate_(exam.SELESAI_AT); if (startAt && now < startAt) throw new Error('Ujian belum dimulai.'); if (endAt && now > endAt) throw new Error('Periode ujian telah berakhir.'); if (exam.KELAS_TARGET && String(exam.KELAS_TARGET).trim() !== String(auth.kelas).trim()) { throw new Error('Ujian tidak tersedia untuk kelas Anda.'); } if (findResult_(ujianId, auth.pesertaId)) throw new Error('Ujian ini sudah pernah diselesaikan.'); let session = findOpenSession_(ujianId, auth.pesertaId); if (!session) { session = createSession_(exam, auth); } const deadline = toDate_(session.DEADLINE_AT); if (deadline && now >= deadline) { finalizeSession_(session.SESSION_ID); throw new Error('Waktu ujian telah habis.'); } const pack = buildQuestionPack_(exam, session); const saved = getAnswersMap_(session.SESSION_ID); return { ok:true, sessionId:session.SESSION_ID, exam:{ ujianId:exam.UJIAN_ID, judul:exam.JUDUL, kode:exam.KODE_UJIAN, durasi:Number(exam.DURASI_MENIT||0), deadline:iso_(deadline), kkm:Number(exam.KKM||0) }, questions:pack, saved }; } function saveAnswer(token, payload) { const auth = auth_(token, 'peserta'); payload = payload || {}; const session = getSession_(payload.sessionId); validateSessionOwner_(session, auth); if (String(session.STATUS) !== 'AKTIF') throw new Error('Sesi ujian sudah ditutup.'); if (new Date() >= toDate_(session.DEADLINE_AT)) { finalizeSession_(session.SESSION_ID); throw new Error('Waktu ujian telah habis.'); } const soalId = clean_(payload.soalId, 60); const jawaban = clean_(payload.jawaban, 5).toUpperCase(); const ragu = !!payload.ragu; if (!soalId) throw new Error('Soal tidak valid.'); if (jawaban && !['A','B','C','D','E'].includes(jawaban)) throw new Error('Jawaban tidak valid.'); upsertAnswer_(session, auth, soalId, jawaban, ragu); touchSession_(session.SESSION_ID); return {ok:true, savedAt:iso_(new Date())}; } function submitExam(token, sessionId) { const auth = auth_(token, 'peserta'); const session = getSession_(sessionId); validateSessionOwner_(session, auth); const result = finalizeSession_(sessionId); const exam = dataObjects_(APP.SHEETS.EXAMS).find(x => String(x.UJIAN_ID) === String(session.UJIAN_ID)); return { ok:true, result: { benar:result.BENAR, salah:result.SALAH, kosong:result.KOSONG, skor:result.SKOR, lulus:result.LULUS === 'YA', showScore: exam ? bool_(exam.TAMPIL_NILAI) : true } }; } /* ========================= EXAM ENGINE ========================= */ function createSession_(exam, auth) { const lock = LockService.getScriptLock(); lock.waitLock(10000); try { const existing = findOpenSession_(exam.UJIAN_ID, auth.pesertaId); if (existing) return existing; const eq = dataObjects_(APP.SHEETS.EXAM_QUESTIONS) .filter(x => String(x.UJIAN_ID) === String(exam.UJIAN_ID)) .sort((a,b)=>Number(a.URUTAN||0)-Number(b.URUTAN||0)); let order = eq.map(x=>String(x.SOAL_ID)); if (bool_(exam.ACAK_SOAL)) shuffle_(order); const questions = questionMap_(); const optionOrder = {}; order.forEach(id => { const q = questions[id]; const keys = ['A','B','C','D','E'].filter(k => String(q && q['OPSI_'+k] || '').trim()); if (bool_(exam.ACAK_OPSI)) shuffle_(keys); optionOrder[id] = keys; }); const now = new Date(); const durationEnd = new Date(now.getTime() + Number(exam.DURASI_MENIT||0)*60000); const examEnd = toDate_(exam.SELESAI_AT); const deadline = examEnd && examEnd < durationEnd ? examEnd : durationEnd; const sessionId = 'SES-' + Utilities.getUuid().slice(0,8).toUpperCase(); const row = [ sessionId, exam.UJIAN_ID, auth.pesertaId, now, deadline, '', 'AKTIF', JSON.stringify(order), JSON.stringify(optionOrder), now, now ]; sh_(APP.SHEETS.SESSIONS).appendRow(row); audit_(auth.pesertaId, auth.nama, 'START_EXAM', exam.UJIAN_ID, {sessionId}); return objectFromHeaders_(headers_(APP.SHEETS.SESSIONS), row); } finally { lock.releaseLock(); } } function buildQuestionPack_(exam, session) { const qmap = questionMap_(); const order = parseJson_(session.QUESTION_ORDER_JSON, []); const optionOrder = parseJson_(session.OPTION_ORDER_JSON, {}); return order.map((id, idx) => { const q = qmap[id]; if (!q) return null; const opts = {}; (optionOrder[id] || ['A','B','C','D','E']).forEach(k => { const text = q['OPSI_'+k]; if (String(text||'').trim()) opts[k] = String(text); }); return { no:idx+1, soalId:String(q.SOAL_ID), pertanyaan:String(q.PERTANYAAN||''), gambarUrl:String(q.GAMBAR_URL||''), options:opts }; }).filter(Boolean); } function finalizeSession_(sessionId) { const lock = LockService.getScriptLock(); lock.waitLock(15000); try { const session = getSession_(sessionId); if (!session) throw new Error('Sesi tidak ditemukan.'); const existing = dataObjects_(APP.SHEETS.RESULTS).find(x => String(x.SESSION_ID) === String(sessionId)); if (existing) return normalizeResult_(existing); const qmap = questionMap_(); const order = parseJson_(session.QUESTION_ORDER_JSON, []); const answers = getAnswersMap_(sessionId); let benar=0, salah=0, kosong=0, earned=0, total=0; order.forEach(id => { const q = qmap[id]; if (!q) return; const weight = Number(q.BOBOT || 1); total += weight; const ans = answers[id] && answers[id].jawaban ? String(answers[id].jawaban).toUpperCase() : ''; if (!ans) kosong++; else if (ans === String(q.KUNCI||'').toUpperCase()) { benar++; earned += weight; } else salah++; }); const score = total ? Math.round((earned/total)*10000)/100 : 0; const exam = dataObjects_(APP.SHEETS.EXAMS).find(x => String(x.UJIAN_ID) === String(session.UJIAN_ID)); const kkm = Number(exam && exam.KKM || 0); const submitAt = new Date(); const result = { HASIL_ID:'HSL-' + Utilities.getUuid().slice(0,8).toUpperCase(), SESSION_ID:sessionId, UJIAN_ID:session.UJIAN_ID, PESERTA_ID:session.PESERTA_ID, BENAR:benar, SALAH:salah, KOSONG:kosong, SKOR:score, LULUS:score>=kkm?'YA':'TIDAK', SUBMIT_AT:submitAt }; sh_(APP.SHEETS.RESULTS).appendRow([ result.HASIL_ID,result.SESSION_ID,result.UJIAN_ID,result.PESERTA_ID, result.BENAR,result.SALAH,result.KOSONG,result.SKOR,result.LULUS,result.SUBMIT_AT ]); updateSessionStatus_(sessionId, 'SELESAI', submitAt); audit_(session.PESERTA_ID, '', 'SUBMIT_EXAM', session.UJIAN_ID, {sessionId, skor:score}); return result; } finally { lock.releaseLock(); } } function upsertAnswer_(session, auth, soalId, jawaban, ragu) { const sh = sh_(APP.SHEETS.ANSWERS); const values = sh.getDataRange().getValues(); const h = values[0]; const iSession=h.indexOf('SESSION_ID'), iSoal=h.indexOf('SOAL_ID'); for (let r=1; r String(x.SESSION_ID)===String(sessionId)); const out={}; rows.forEach(x => out[String(x.SOAL_ID)] = { jawaban:String(x.JAWABAN||''), ragu:String(x.RAGU)==='YA' }); return out; } /* ========================= HELPERS ========================= */ function auth_(token, expectedType) { token = clean_(token, 128); if (!token) throw new Error('Sesi login tidak ditemukan.'); const raw = CacheService.getScriptCache().get('auth:'+token); if (!raw) throw new Error('Sesi login telah berakhir. Silakan login kembali.'); const data = JSON.parse(raw); if (expectedType && data.type !== expectedType) throw new Error('Akses tidak diizinkan.'); CacheService.getScriptCache().put('auth:'+token, JSON.stringify(data), APP.SESSION_TTL); return data; } function getSettings_() { const cache = CacheService.getDocumentCache(); const key = 'settings:v1'; const cached = cache.get(key); if (cached) return JSON.parse(cached); const rows = dataObjects_(APP.SHEETS.SETTINGS); const obj={}; rows.forEach(x=>obj[String(x.KEY)]=x.VALUE); cache.put(key, JSON.stringify(obj), APP.CACHE_TTL); return obj; } function questionMap_() { const cache = CacheService.getDocumentCache(); const key = 'questions:active:v1'; const cached = cache.get(key); if (cached) return JSON.parse(cached); const rows = dataObjects_(APP.SHEETS.QUESTIONS).filter(x=>String(x.STATUS).toUpperCase()==='AKTIF'); const map={}; rows.forEach(x=>map[String(x.SOAL_ID)]=x); cache.put(key, JSON.stringify(map), 120); return map; } function findOpenSession_(ujianId, pesertaId) { return dataObjects_(APP.SHEETS.SESSIONS).find(x => String(x.UJIAN_ID)===String(ujianId) && String(x.PESERTA_ID)===String(pesertaId) && String(x.STATUS)==='AKTIF' ) || null; } function getSession_(sessionId) { return dataObjects_(APP.SHEETS.SESSIONS).find(x=>String(x.SESSION_ID)===String(sessionId)) || null; } function validateSessionOwner_(session, auth) { if (!session) throw new Error('Sesi ujian tidak ditemukan.'); if (String(session.PESERTA_ID)!==String(auth.pesertaId)) throw new Error('Sesi ujian tidak valid.'); } function touchSession_(sessionId) { const sh=sh_(APP.SHEETS.SESSIONS), data=sh.getDataRange().getValues(), h=data[0]; const idc=h.indexOf('SESSION_ID'), hb=h.indexOf('LAST_HEARTBEAT'); for(let r=1;r String(x.UJIAN_ID)===String(ujianId) && String(x.PESERTA_ID)===String(pesertaId) ) || null; } function normalizeResult_(x){ return { HASIL_ID:x.HASIL_ID, SESSION_ID:x.SESSION_ID, UJIAN_ID:x.UJIAN_ID, PESERTA_ID:x.PESERTA_ID, BENAR:Number(x.BENAR||0), SALAH:Number(x.SALAH||0), KOSONG:Number(x.KOSONG||0), SKOR:Number(x.SKOR||0), LULUS:String(x.LULUS), SUBMIT_AT:x.SUBMIT_AT }; } function dataObjects_(sheetName) { const sh=sh_(sheetName), values=sh.getDataRange().getValues(); if(values.length<2) return []; const headers=values[0].map(String); return values.slice(1).filter(r=>r.some(v=>v!=='' && v!==null)).map(r=>objectFromHeaders_(headers,r)); } function objectFromHeaders_(headers,row) { const o={}; headers.forEach((h,i)=>o[h]=row[i]); return o; } function headers_(name){ return sh_(name).getRange(1,1,1,sh_(name).getLastColumn()).getValues()[0]; } function sh_(name){ const s=SpreadsheetApp.getActive().getSheetByName(name); if(!s) throw new Error('Sheet '+name+' belum ada. Jalankan setupUjianKuVer1().'); return s; } function parseJson_(s,fallback){ try{return JSON.parse(String(s||''))}catch(e){return fallback} } function bool_(v){ return v===true || String(v).toUpperCase()==='TRUE' || String(v)==='1' || String(v).toUpperCase()==='YA'; } function iso_(d){ return d instanceof Date && !isNaN(d) ? d.toISOString() : ''; } function toDate_(v){ if(v instanceof Date) return v; if(!v) return null; const d=new Date(v); return isNaN(d)?null:d; } function clean_(v,max){ return String(v==null?'':v).trim().slice(0,max||500); } function safeEq_(a,b){ if(a.length!==b.length)return false; let x=0; for(let i=0;i('0'+(b&255).toString(16)).slice(-2)).join(''); } function shuffle_(a){ for(let i=a.length-1;i>0;i--){ const j=Math.floor(Math.random()*(i+1)); [a[i],a[j]]=[a[j],a[i]]; } return a; } function audit_(actorId, actorName, action, object, detail) { try { sh_(APP.SHEETS.AUDIT).appendRow([ 'LOG-'+Utilities.getUuid().slice(0,8).toUpperCase(), new Date(), actorId||'',actorName||'',action||'',object||'',JSON.stringify(detail||{}) ]); } catch(e) {} } function formatSheets_() { const ss=SpreadsheetApp.getActive(); Object.values(APP.SHEETS).forEach(name=>{ const sh=ss.getSheetByName(name); if(!sh)return; const lc=sh.getLastColumn(); if(lc){ sh.getRange(1,1,1,lc).setFontWeight('bold').setBackground('#0F172A').setFontColor('#FFFFFF'); sh.autoResizeColumns(1,lc); } }); }

Komentar