function bookId_() { const id = PropertiesService.getScriptProperties().getProperty('BOOK_ID'); if (!id) throw new Error('Előbb futtasd a setupTemplate függvényt az üres táblázatból.'); return id; } const ZONE = 'Europe/Budapest'; // Run once in the editor, BEFORE deploying. Never deploy with public access. function initialize() { const active = Session.getActiveUser().getEmail().toLowerCase(); const effective = Session.getEffectiveUser().getEmail().toLowerCase(); if (!active || active !== effective) throw new Error('A saját fiókodból futtasd az előkészítést.'); const props = PropertiesService.getScriptProperties(); const owner = props.getProperty('OWNER_EMAIL'); if (owner && owner !== active) throw new Error('Már másik tulajdonos van beállítva.'); workbook_(); props.setProperty('OWNER_EMAIL', active); return 'Előkészítve. Közzététel: végrehajtás saját néven, hozzáférés: Csak én.'; } function authorize_() { const owner = PropertiesService.getScriptProperties().getProperty('OWNER_EMAIL'); const active = Session.getActiveUser().getEmail().toLowerCase(); if (!owner || !active || active !== owner) throw new Error('Csak a beállított saját Google-fiókkal használható.'); } function doGet() { authorize_(); return HtmlService.createHtmlOutputFromFile('Index').setTitle('Tankolás') .addMetaTag('viewport', 'width=device-width, initial-scale=1'); } function workbook_() { const book = SpreadsheetApp.openById(bookId_()); const sheet = book.getSheetByName('Tankolások'); if (!sheet || !book.getSheetByName('Listák')) throw new Error('Hiányzó munkalap.'); const headers = sheet.getRange(1, 1, 1, 14).getValues()[0]; const expected = {0:'Dátum',2:'Márka',3:'Hely',4:'Egységár',5:'Teljes költség',7:'Kilométer óra',8:'Megtett út',11:'Pénznem',12:'Tele tank?'}; Object.keys(expected).forEach(i => {if(headers[i] !== expected[i]) throw new Error('A táblázat oszlopai megváltoztak. A mentés leállt.');}); return book; } function history_(sheet) { const all = sheet.getRange(2, 1, sheet.getMaxRows()-1, 14).getValues(); return all.map((values,i)=>({row:i+2,values})).filter(r=>r.values[0] !== ''); } function dateText_(value) { if (!(value instanceof Date) || isNaN(value.getTime())) throw new Error('Hibás dátum van a táblázatban.'); return Utilities.formatDate(value, ZONE, 'yyyy-MM-dd'); } function fuelOptions_(sheet) { const rule = sheet.getRange('K2').getDataValidation(); if (!rule || rule.getCriteriaType() !== SpreadsheetApp.DataValidationCriteria.VALUE_IN_LIST) throw new Error('Az üzemanyaglista ellenőrzést igényel.'); return rule.getCriteriaValues()[0]; } function getInitialData() { authorize_(); const book = workbook_(), sheet = book.getSheetByName('Tankolások'); const records = history_(sheet), last = records[records.length-1]; const stations = [], seen = new Set(); records.slice().reverse().forEach(r=>{ const [brand, place] = [String(r.values[2]),String(r.values[3])]; const key = JSON.stringify([brand,place]); if (brand && place && !seen.has(key)) {seen.add(key);stations.push({brand,place});} }); const lists = book.getSheetByName('Listák').getRange('A2:B200').getValues(); const now = new Date(); return {today:Utilities.formatDate(now,ZONE,'yyyy-MM-dd'), time:Utilities.formatDate(now,ZONE,'HH:mm'), stations, brands:lists.map(r=>r[0]).filter(String), places:lists.map(r=>r[1]).filter(String), fuels:fuelOptions_(sheet), last: last ? {date:dateText_(last.values[0]),odometer:last.values[7],fuel:last.values[10]}:null}; } function number_(value, name) { const text = String(value == null ? '' : value).trim().replace(/[\s\u00a0\u202f]/g,'').replace(',','.'); if (!/^\d+(\.\d+)?$/.test(text)) throw new Error(name+': adj meg pozitív számot.'); const n=Number(text); if (!Number.isFinite(n) || n <= 0) throw new Error(name+': adj meg pozitív számot.'); return n; } function text_(value, name, limit) { const s=String(value == null ? '' : value).trim(); if (!s || s.length>limit || /^[=+@\-]/.test(s) || /[\x00-\x1f]/.test(s)) throw new Error(name+': ellenőrizd a beírt szöveget.'); return s; } function normalize_(p) { if (!p || typeof p !== 'object') throw new Error('Hiányzó adatok.'); if (!/^\d{4}-\d{2}-\d{2}$/.test(p.date||'')) throw new Error('Hibás dátum.'); const [y,m,d]=p.date.split('-').map(Number), dt=new Date(Date.UTC(y,m-1,d)); if (dt.toISOString().slice(0,10)!==p.date) throw new Error('Nem létező dátum.'); if (!['HUF','EUR'].includes(p.currency)) throw new Error('Válassz pénznemet.'); if (typeof p.full !== 'boolean') throw new Error('Hiányzó tele tank jelölés.'); const odometer=number_(p.odometer,'Km-óra'); if (!Number.isSafeInteger(odometer) || odometer>9999999) throw new Error('A km-óra egész szám legyen.'); const unitPrice=number_(p.unitPrice,'Literár'), total=number_(p.total,'Fizetett összeg'); if (total/unitPrice>200 || total/unitPrice<0.1) throw new Error('A számított liter szokatlan. Ellenőrizd az árat és a végösszeget.'); const note=p.note ? text_(p.note,'Megjegyzés',500):''; if (p.full && /nem\s*teli\s*tank/i.test(note)) throw new Error('A megjegyzés nem tele tankot jelez: vedd ki a pipát.'); const time=String(p.time||''); if(time && !/^([01]\d|2[0-3]):[0-5]\d$/.test(time)) throw new Error('Hibás idő.'); const fuel=String(p.fuel == null ? '' : p.fuel).trim(); return {date:p.date,dateSerial:dt.getTime()/86400000+25569,brand:text_(p.brand,'Márka',80),place:text_(p.place,'Hely',120),fuel:fuel?text_(fuel,'Üzemanyag',80):'',currency:p.currency,full:p.full,odometer,unitPrice,total,note,time}; } function sameRecord_(r,p) { const v=r.values; return dateText_(v[0])===p.date && v[2]===p.brand && v[3]===p.place && v[4]===p.unitPrice && v[5]===p.total && v[7]===p.odometer && v[11]===p.currency; } function addOption_(sheet,column,value) { const values=sheet.getRange(2,column,199,1).getValues().flat(); if(values.includes(value)) return; const empty=values.findIndex(v=>v===''); if(empty<0) throw new Error('A kútlista megtelt. Bővítsd a Listák lapot.'); sheet.getRange(empty+2,column).setValue(value); } function addFuelOption_(sheet,value) { const options=fuelOptions_(sheet); if (!value || options.includes(value)) return; const rule=sheet.getRange('K2').getDataValidation().copy() .requireValueInList(options.concat([value]),true).build(); sheet.getRange(2,11,sheet.getMaxRows()-1,1).setDataValidation(rule); } function consumptionFormula_(row,records,full) { if(!full) return ''; const previous=records.slice().reverse().find(r=>r.values[12]===true && typeof r.values[7]==='number' && r.values[7]>0); if(!previous) return ''; return '=IFERROR(SUM(G'+(previous.row+1)+':G'+row+')/(H'+row+'-H'+previous.row+')*100,"")'; } function saveFuel(payload) { authorize_(); const p=normalize_(payload), lock=LockService.getScriptLock(); lock.waitLock(20000); try { const book=workbook_(), sheet=book.getSheetByName('Tankolások'), records=history_(sheet); const duplicate=records.find(r=>sameRecord_(r,p)); if(duplicate) { const expectedNote=p.full?p.note:['Nem teli tank!',p.note].filter(Boolean).join(' '); if(duplicate.values[10]!==p.fuel || duplicate.values[12]!==p.full || String(duplicate.values[13]||'')!==expectedNote) throw new Error('Ez a tankolás már szerepel, de más üzemanyaggal, tele tank jelöléssel vagy megjegyzéssel. A meglévő sort a táblázatban javítsd.'); return {alreadySaved:true,row:duplicate.row,liters:p.total/p.unitPrice,odometer:p.odometer}; } const last=records[records.length-1]; if(p.date>Utilities.formatDate(new Date(),ZONE,'yyyy-MM-dd')) throw new Error('Jövőbeli tankolást nem lehet menteni.'); if(last && p.datetypeof r.values[7]==='number'?r.values[7]:0)); if(p.odometer<=maxKm) throw new Error('A km-óra legyen nagyobb az utolsó értéknél: '+maxKm+' km.'); fuelOptions_(sheet); // Verify the existing list before any write. const row=last?last.row+1:2; if(row>sheet.getMaxRows()) throw new Error('Elfogytak az előkészített sorok. Bővítsd a táblázatot.'); const target=sheet.getRange(row,1,1,14), current=target.getValues()[0], formulas=target.getFormulas()[0]; if(current.some((v,i)=> ![6,8,9,12].includes(i) && v!=='' || [6,8,9].includes(i) && v!=='' && !formulas[i])) throw new Error('A következő sor nem üres. A mentés leállt az adatok védelmében.'); const lists=book.getSheetByName('Listák'); addOption_(lists,1,p.brand); addOption_(lists,2,p.place); addFuelOption_(sheet,p.fuel); // Apply safe presentation/constraints before the single row-content write. const exemplar=sheet.getRange(Math.max(2,row-1),1,1,14); exemplar.copyTo(target,SpreadsheetApp.CopyPasteType.PASTE_FORMAT,false); exemplar.copyTo(target,SpreadsheetApp.CopyPasteType.PASTE_DATA_VALIDATION,false); sheet.getRange(row,1).setNumberFormat('yyyy.mm.dd.'); sheet.getRange(row,2).setNumberFormat('hh:mm'); sheet.getRange(row,5).setNumberFormat(p.currency==='HUF'?'#,##0.00" Ft/l"':'0.000" €/l"'); sheet.getRange(row,6).setNumberFormat(p.currency==='HUF'?'#,##0" Ft"':'#,##0.00" €"'); sheet.getRange(row,7).setNumberFormat('#,##0.00" l"'); sheet.getRange(row,8,1,2).setNumberFormat('#,##0" km"'); sheet.getRange(row,10).setNumberFormat('0.00" l/100 km"'); const time=p.time?p.time.split(':').map(Number):null; const note=p.full?p.note:['Nem teli tank!',p.note].filter(Boolean).join(' '); const values=[p.dateSerial,time?(time[0]*60+time[1])/1440:'',p.brand,p.place,p.unitPrice,p.total, '=IFERROR(F'+row+'/E'+row+',"")',p.odometer,maxKm?'=H'+row+'-'+maxKm:'', consumptionFormula_(row,records,p.full),p.fuel,p.currency,p.full,note]; target.setValues([values]); SpreadsheetApp.flush(); return {alreadySaved:false,row,liters:p.total/p.unitPrice,distance:maxKm?p.odometer-maxKm:null,odometer:p.odometer}; } finally {lock.releaseLock();} }