// Cadangan satu arah SINERGI -> Google Sheets. Jalankan setup sekali. function setup() { const p=PropertiesService.getScriptProperties(); if(!p.getProperty('BACKUP_TOKEN'))p.setProperty('BACKUP_TOKEN',Utilities.getUuid().replace(/-/g,'')+Utilities.getUuid().replace(/-/g,'')); if(!p.getProperty('BACKUP_FOLDER'))p.setProperty('BACKUP_FOLDER',DriveApp.createFolder('Cadangan Monitoring Kontrak SINERGI').getId()); console.log('Siap. Salin BACKUP_TOKEN dari Project Settings > Script Properties ke pengaturan portal.'); } function doPost(e) { let input;try{if(!e.postData||e.postData.contents.length>15000000)throw Error();input=JSON.parse(e.postData.contents);}catch{return reply({ok:false});} const p=PropertiesService.getScriptProperties(),expected=p.getProperty('BACKUP_TOKEN'); if(!expected||typeof input.token!=='string'||!safeEqual(expected,input.token)||!/^[a-f0-9]{64}$/.test(input.revision)||!Array.isArray(input.rows)||input.rows.length>30000)return reply({ok:false}); const lock=LockService.getScriptLock();if(!lock.tryLock(1000))return reply({ok:false}); try { const folder=DriveApp.getFolderById(p.getProperty('BACKUP_FOLDER')),name='SINERGI Kontrak '+input.revision; const existing=folder.getFilesByName(name);if(existing.hasNext())return reply({ok:true,revision:input.revision,url:'https://docs.google.com/spreadsheets/d/'+existing.next().getId()+'/edit'}); const book=SpreadsheetApp.create('SINERGI Kontrak - proses '+input.revision); const configs=[['non-tibet','MDU Non Tibet',['kr','date','purpose','vendor','unit','location','material','qty','status','arrival']],['tibet','MDU Tibet',['kr','date','purpose','vendor','unit','location','material','qty','status','arrival']],['jaskondis','Jaskon Terkontrak',['no','kr','date','vendor','job','value','khs','district','subdistrict','village','hamlet','up3','ulp','spmk','duration','end','jtm','jtr','gardu50','gardu100','valueRaw','invalidValue']]]; const labels={kr:'No. KR',date:'Tanggal KR',purpose:'Fungsi',vendor:'Pabrikan / Vendor',unit:'Unit Pelaksana',location:'Loko',material:'Material',qty:'Qty',status:'Status Kedatangan',arrival:'Tanggal Tiba',no:'NO',job:'Nama Pekerjaan',value:'Nilai KR Jasa',khs:'Nomor Kontrak KHS',district:'Kabupaten',subdistrict:'Kecamatan',village:'Desa',hamlet:'Dusun',up3:'UP3',ulp:'ULP',spmk:'Tanggal SPMK',duration:'Durasi KR',end:'Tanggal Akhir KR',jtm:'JTM (m)',jtr:'JTR (m)',gardu50:'Gardu 50',gardu100:'Gardu 100',valueRaw:'Nilai sumber awal',invalidValue:'Perlu koreksi nilai',id:'ID Portal',version:'Versi',updatedAt:'Diperbarui'}; configs.forEach(function(c,i){const sheet=i===0?book.getSheets()[0].setName(c[1]):book.insertSheet(c[1]);const keys=c[2].concat(['id','version','updatedAt']);const rows=input.rows.filter(r=>r.kind===c[0]).map(r=>keys.map(k=>safeCell(r[k])));const values=[keys.map(k=>labels[k])].concat(rows);if(sheet.getMaxRows()