AutoServis
Profesionalni sistem upravljanja servisom
Prijava u sistem
Email
Lozinka
Zapamti me
Nemate pristup? Kontaktirajte nas:
📞 +387 65 731 090
Auto Servis PRO · v2.0
AUTO SERVIS
by Slavko Vidmar
Servisni softver · v2.0
📋
Odaberite nalog s liste
ili pritisnite N za novi nalog
'; var win = window.open('','_blank','width=794,height=1123'); win.document.write(html); win.document.close(); win.onload = function(){ win.focus(); win.print(); }; } function printRacun(){ var invHTML = document.getElementById('invPaper').innerHTML; var win = window.open('', '_blank', 'width=794,height=1123'); win.document.write(''+ 'Račun — Auto Servis'+ ''+ ''+ ''+invHTML+' '); win.document.close(); win.onload = function(){ win.focus(); win.print(); }; } /* ════════════ MODALI ════════════ */ function openModal(id){ document.getElementById(id).classList.add('open'); } function closeModal(id){ document.getElementById(id).classList.remove('open'); } document.querySelectorAll('.mover').forEach(function(m){ m.addEventListener('click', function(e){ if(e.target===m) m.classList.remove('open'); }); }); /* ════════════ TOAST ════════════ */ function toast(msg,type){ type=type||'ok'; document.getElementById('tm').textContent=msg; var ti=document.getElementById('ti'); ti.textContent = type==='ok'?'✓' : type==='info'?'ℹ' : '⚠'; ti.className = 'ti '+(type==='ok'?'ok' : type==='info'?'info' : 'err'); var el=document.getElementById('toast'); el.classList.add('on'); setTimeout(function(){ el.classList.remove('on'); }, 3000); } /* ════════════ TIPKOVNICA ════════════ */ document.addEventListener('keydown', function(e){ var tg=e.target.tagName; if(e.key==='n' && tg!=='INPUT' && tg!=='SELECT' && tg!=='TEXTAREA') openNew(); if(e.key==='Escape'){ document.querySelectorAll('.mover.open').forEach(function(m){ m.classList.remove('open'); }); dismissNotif(); } }); /* ════════════ PODSJETNIK ZA TERMINE ════════════ */ // Pamti pokazane notifikacije u sessionStorage (preživljava F5, ne preživljava zatvaranje) function notifShown(key){ var shown = JSON.parse(sessionStorage.getItem('_notif') || '{}'); return !!shown[key]; } function notifMark(key){ var shown = JSON.parse(sessionStorage.getItem('_notif') || '{}'); shown[key] = true; sessionStorage.setItem('_notif', JSON.stringify(shown)); } function localDateStr(){ // Lokalni datum, ne UTC var d = new Date(); var yy = d.getFullYear(); var mm = String(d.getMonth()+1).padStart(2,'0'); var dd = String(d.getDate()).padStart(2,'0'); return yy+'-'+mm+'-'+dd; } function checkUpcoming(){ var now = new Date(); var todayStr = localDateStr(); var upcoming = orders.filter(function(o){ return o.date === todayStr && o.time && o.status !== 'done'; }); upcoming.forEach(function(o){ var parts = o.time.split(':'); var termTime = new Date(now.getFullYear(), now.getMonth(), now.getDate(), parseInt(parts[0]||0), parseInt(parts[1]||0), 0); var diffMin = (termTime - now) / 60000; // 60 minuta prije — prozor 55–65 min var key60 = o.id + '_60_' + todayStr; if(diffMin > 55 && diffMin <= 65 && !notifShown(key60)){ notifMark(key60); showNotif(o, Math.round(diffMin)); } // 10 minuta prije — prozor 5–15 min var key10 = o.id + '_10_' + todayStr; if(diffMin > 5 && diffMin <= 15 && !notifShown(key10)){ notifMark(key10); showNotif(o, Math.round(diffMin)); } // 0 — tačno na termin (prozor -2 do +3 min) var key0 = o.id + '_0_' + todayStr; if(diffMin > -2 && diffMin <= 3 && !notifShown(key0)){ notifMark(key0); showNotif(o, 0); } }); } function showNotif(o, minLeft){ var overlay = document.getElementById('notif-overlay'); var ico = document.getElementById('notif-ico'); var lbl = document.getElementById('notif-label'); if(ico) ico.textContent = '⏰'; if(lbl){ lbl.textContent = 'PODSJETNIK — NADOLAZEĆI TERMIN'; lbl.style.color='#3a6bd0'; } document.getElementById('notif-title').textContent = o.car + ' — ' + o.customer; var vrijemeStr = (minLeft === 0) ? '⏰ TERMIN JE SADA!' : 'za ' + minLeft + ' minuta'; document.getElementById('notif-sub').innerHTML = '' + o.desc + '
' + 'Termin zakazan za: ' + o.time + '
' + vrijemeStr; document.getElementById('notif-open-btn').onclick = function(){ dismissNotif(); selectOrder(o.id); showView('detalji'); }; overlay.style.display = 'flex'; // Zvučni signal (kratki beep) try { var ctx = new (window.AudioContext || window.webkitAudioContext)(); var osc = ctx.createOscillator(); var gain = ctx.createGain(); osc.connect(gain); gain.connect(ctx.destination); osc.frequency.value = 880; gain.gain.setValueAtTime(.3, ctx.currentTime); gain.gain.exponentialRampToValueAtTime(.001, ctx.currentTime + .6); osc.start(); osc.stop(ctx.currentTime + .6); } catch(e){} } function dismissNotif(){ document.getElementById('notif-overlay').style.display = 'none'; } // Provjera svake minute /* ════════════════════════════════════════════════════ EXCEL EXPORT — SheetJS sa bojama (plava/siva/svjetlo plava) ════════════════════════════════════════════════════ */ /* Pomoćne funkcije za stilizovanje ćelija */ function _xlStyle(opts){ var s = {}; if(opts.bold || opts.header) s.font = { bold: true, sz: opts.sz||11, color: opts.fontColor ? {rgb: opts.fontColor} : undefined, name:'Calibri' }; if(opts.header){ s.font = { bold:true, sz:opts.sz||11, color:{rgb:'FFFFFF'}, name:'Calibri' }; } if(opts.fill) s.fill = { fgColor:{ rgb: opts.fill }, patternType:'solid' }; if(opts.border) s.border = { top:{style:'thin',color:{rgb:'B8C8E8'}}, bottom:{style:'thin',color:{rgb:'B8C8E8'}}, left:{style:'thin',color:{rgb:'B8C8E8'}}, right:{style:'thin',color:{rgb:'B8C8E8'}} }; if(opts.align) s.alignment = { horizontal: opts.align, vertical:'center', wrapText: true }; if(opts.numFmt) s.numFmt = opts.numFmt; return s; } function _xlCell(v, opts){ return { v: v, t: typeof v==='number'?'n':'s', s: _xlStyle(opts||{}) }; } function _xlNum(v, opts){ return { v: v, t:'n', s: _xlStyle(opts||{}) }; } /* Boje */ var XL = { PLAVA: '1E4D9B', // tamno plava — header SV_PLAVA: '2E6FD4', // srednja plava PLAVA_BG: 'D6E4F7', // svjetlo plava pozadina SIVA_BG: 'F2F4F8', // svjetlo siva SIVA: '8090B0', // srednja siva ZELENA: '16A05A', // ukupno / prihod NARANDZ: 'D47800', // dijelovi BIJELA: 'FFFFFF', RED_PARNA: 'EDF3FB', // parna redovi }; /* ════════════════════════════════════════════════════ EXCEL EXPORT — Produktivnost radnika ════════════════════════════════════════════════════ */ function exportRadniciExcel(){ if(typeof XLSX === 'undefined'){ toast('SheetJS nije dostupan. Provjeri internet vezu.','err'); return; } var od = window._radOd || null; var doo = window._radDo || null; var periodoStr = (od && doo) ? fmtDate(od)+' — '+fmtDate(doo) : 'Sve vrijeme'; // Filtriraj naloge po periodu var filtNalozi = orders.filter(function(o){ if(o.status !== 'done') return false; if(od && o.date < od) return false; if(doo && o.date > doo) return false; return true; }); var radnici = getRadnici(); // ── Izracunaj statistiku ── var stats = {}; radnici.forEach(function(r){ stats[r.name] = { sati:0, zarada:0, usluge:[], nalogSet:[] }; }); stats['— Nedodijeljeno —'] = { sati:0, zarada:0, usluge:[], nalogSet:[] }; filtNalozi.forEach(function(o){ (o.services||[]).forEach(function(s){ var key = (s.radnik && stats[s.radnik]) ? s.radnik : '— Nedodijeljeno —'; var norm = NORMATIVI.find(function(n){ return n.naziv.toLowerCase()===s.name.toLowerCase(); }); var sati = norm ? norm.sati : 0; stats[key].sati += sati; stats[key].zarada += parseFloat(s.price)||0; stats[key].usluge.push({ datum: o.date, nalog: o.id, vozilo: o.car, klijent: o.customer, usluga: s.name, sati: sati, cijena: parseFloat(s.price)||0 }); if(stats[key].nalogSet.indexOf(o.id) < 0) stats[key].nalogSet.push(o.id); }); }); var wb = XLSX.utils.book_new(); // ════ SHEET 1: Pregled svih radnika ════ var s1 = []; var merges1 = []; // Header s1.push([ _xlCell('AUTO SERVIS PRO — Produktivnost radnika',{header:true,fill:XL.PLAVA,sz:13,align:'center'}), _xlCell('',{}),_xlCell('',{}),_xlCell('',{}),_xlCell('',{}) ]); merges1.push({s:{r:0,c:0},e:{r:0,c:4}}); s1.push([ _xlCell('Period: '+periodoStr,{fill:XL.SV_PLAVA,fontColor:'FFFFFF',bold:true,align:'center'}), _xlCell('',{}),_xlCell('',{}),_xlCell('',{}),_xlCell('',{}) ]); merges1.push({s:{r:1,c:0},e:{r:1,c:4}}); s1.push([_xlCell('',{})]); // Zaglavlje tabele s1.push([ _xlCell('Radnik', {header:true,fill:XL.PLAVA}), _xlCell('Broj usluga', {header:true,fill:XL.PLAVA,align:'center'}), _xlCell('Normativi (h)', {header:true,fill:XL.PLAVA,align:'center'}), _xlCell('Prihod od rada (KM)', {header:true,fill:XL.PLAVA,align:'right'}), _xlCell('Broj naloga', {header:true,fill:XL.PLAVA,align:'center'}) ]); // Sortiraj radnike po satima var sortedR = radnici.slice().sort(function(a,b){ return stats[b.name].sati - stats[a.name].sati; }); var ukSati=0, ukZarada=0, ukUsluge=0; sortedR.forEach(function(r,i){ var st = stats[r.name]; var bg = i%2===0 ? XL.BIJELA : XL.RED_PARNA; s1.push([ _xlCell(r.name, {fill:bg,border:true,bold:true}), _xlNum(st.usluge.length, {fill:bg,border:true,align:'center'}), _xlNum(Math.round(st.sati*10)/10, {fill:bg,border:true,numFmt:'#,##0.0',align:'center'}), _xlNum(Math.round(st.zarada*100)/100, {fill:bg,border:true,numFmt:'#,##0.00',align:'right'}), _xlNum(st.nalogSet.length, {fill:bg,border:true,align:'center'}) ]); ukSati+=st.sati; ukZarada+=st.zarada; ukUsluge+=st.usluge.length; }); // Nedodijeljeno red var nd = stats['— Nedodijeljeno —']; if(nd.usluge.length > 0){ s1.push([ _xlCell('— Nedodijeljeno —', {fill:'FFF3CD',border:true,bold:true}), _xlNum(nd.usluge.length, {fill:'FFF3CD',border:true,align:'center'}), _xlNum(Math.round(nd.sati*10)/10, {fill:'FFF3CD',border:true,numFmt:'#,##0.0',align:'center'}), _xlNum(Math.round(nd.zarada*100)/100, {fill:'FFF3CD',border:true,numFmt:'#,##0.00',align:'right'}), _xlNum(nd.nalogSet.length, {fill:'FFF3CD',border:true,align:'center'}) ]); } // Ukupno s1.push([ _xlCell('UKUPNO', {bold:true,fill:XL.SIVA_BG}), _xlNum(ukUsluge, {bold:true,fill:XL.PLAVA_BG,align:'center'}), _xlNum(Math.round(ukSati*10)/10, {bold:true,fill:XL.PLAVA_BG,numFmt:'#,##0.0',align:'center'}), _xlNum(Math.round(ukZarada*100)/100, {bold:true,fill:'D4EFDF',numFmt:'#,##0.00',align:'right'}), _xlCell('',{fill:XL.SIVA_BG}) ]); var ws1 = XLSX.utils.aoa_to_sheet(s1); ws1['!merges'] = merges1; ws1['!cols'] = [{wch:24},{wch:14},{wch:16},{wch:20},{wch:14}]; ws1['!rows'] = [{hpt:22},{hpt:18},{hpt:6},{hpt:18}]; XLSX.utils.book_append_sheet(wb, ws1, 'Pregled'); // ════ SHEET 2: Detalji po radniku ════ var s2 = []; var merges2 = []; s2.push([ _xlCell('DETALJI — Sve usluge po radniku',{header:true,fill:XL.PLAVA,sz:12,align:'center'}), _xlCell('',{}),_xlCell('',{}),_xlCell('',{}),_xlCell('',{}),_xlCell('',{}),_xlCell('',{}) ]); merges2.push({s:{r:0,c:0},e:{r:0,c:6}}); s2.push([ _xlCell('Period: '+periodoStr,{fill:XL.SV_PLAVA,fontColor:'FFFFFF',align:'center'}), _xlCell('',{}),_xlCell('',{}),_xlCell('',{}),_xlCell('',{}),_xlCell('',{}),_xlCell('',{}) ]); merges2.push({s:{r:1,c:0},e:{r:1,c:6}}); s2.push([_xlCell('',{})]); // Header detalji s2.push([ _xlCell('Radnik', {header:true,fill:XL.PLAVA}), _xlCell('Datum', {header:true,fill:XL.PLAVA}), _xlCell('Nalog', {header:true,fill:XL.PLAVA}), _xlCell('Vozilo', {header:true,fill:XL.PLAVA}), _xlCell('Klijent', {header:true,fill:XL.PLAVA}), _xlCell('Usluga', {header:true,fill:XL.PLAVA}), _xlCell('Normativi (h)',{header:true,fill:XL.PLAVA,align:'center'}), _xlCell('Cijena (KM)', {header:true,fill:XL.PLAVA,align:'right'}) ]); // Grupiraj detalje po radniku var allR = sortedR.concat([{name:'— Nedodijeljeno —'}]); var rowIdx = 0; allR.forEach(function(r){ var st = stats[r.name]; if(!st || !st.usluge.length) return; // Naslov grupe za radnika s2.push([ _xlCell('👷 '+r.name, {bold:true,fill:XL.PLAVA_BG,border:true}), _xlCell('',{fill:XL.PLAVA_BG}),_xlCell('',{fill:XL.PLAVA_BG}), _xlCell('',{fill:XL.PLAVA_BG}),_xlCell('',{fill:XL.PLAVA_BG}), _xlCell('',{fill:XL.PLAVA_BG}), _xlNum(Math.round(st.sati*10)/10,{bold:true,fill:XL.PLAVA_BG,numFmt:'#,##0.0',align:'center'}), _xlNum(Math.round(st.zarada*100)/100,{bold:true,fill:XL.PLAVA_BG,numFmt:'#,##0.00',align:'right'}) ]); rowIdx++; // Usluge tog radnika st.usluge.sort(function(a,b){ return a.datum.localeCompare(b.datum); }) .forEach(function(u,i){ var bg = i%2===0 ? XL.BIJELA : XL.RED_PARNA; s2.push([ _xlCell(r.name, {fill:bg,border:true}), _xlCell(fmtDate(u.datum), {fill:bg,border:true}), _xlCell(u.nalog, {fill:bg,border:true}), _xlCell(u.vozilo, {fill:bg,border:true}), _xlCell(u.klijent, {fill:bg,border:true}), _xlCell(u.usluga, {fill:bg,border:true}), _xlNum(u.sati, {fill:bg,border:true,numFmt:'#,##0.0',align:'center'}), _xlNum(u.cijena, {fill:bg,border:true,numFmt:'#,##0.00',align:'right'}) ]); }); // Prazna linija između radnika s2.push([_xlCell('',{})]); }); var ws2 = XLSX.utils.aoa_to_sheet(s2); ws2['!merges'] = merges2; ws2['!cols'] = [{wch:22},{wch:12},{wch:14},{wch:22},{wch:20},{wch:32},{wch:14},{wch:14}]; ws2['!rows'] = [{hpt:22},{hpt:18},{hpt:6},{hpt:18}]; XLSX.utils.book_append_sheet(wb, ws2, 'Detalji'); var naziv = 'AutoServis-radnici'+(od?'-'+od:'')+(doo?'-do-'+doo:'')+'.xlsx'; XLSX.writeFile(wb, naziv); toast('Excel produktivnost preuzet! ('+filtNalozi.length+' naloga)', 'ok'); } /* ════════ EXPORT: DNEVNI/PERIOD IZVJEŠTAJ ════════ */ function exportIzvjestajExcel(){ if(typeof XLSX === 'undefined'){ toast('SheetJS nije dostupan. Provjeri internet vezu.','err'); return; } var mod = window._izvMod || 'dan'; var naslov, odabrani, periodoString; if(mod === 'period'){ var od = window._izvOd || today(); var doo = window._izvDo || today(); odabrani = orders.filter(function(o){ return o.date >= od && o.date <= doo; }); naslov = 'AutoServis-izvjestaj-'+od+'-do-'+doo+'.xlsx'; periodoString = fmtDate(od) + ' — ' + fmtDate(doo); } else { var datum = window._izvjestajDate || today(); odabrani = orders.filter(function(o){ return o.date === datum; }); naslov = 'AutoServis-izvjestaj-'+datum+'.xlsx'; periodoString = fmtDate(datum) + (datum===today()?' (Danas)':''); } if(!odabrani.length){ toast('Nema naloga za odabrani period.','info'); return; } var ws_data = []; var merges = []; var row = 0; // ── Naslov izvještaja ── ws_data.push([ _xlCell('AUTO SERVIS PRO — Izvještaj', { header:true, fill:XL.PLAVA, sz:14, align:'center' }), _xlCell('', {}), _xlCell('', {}), _xlCell('', {}), _xlCell('', {}), _xlCell('', {}), _xlCell('', {}) ]); merges.push({s:{r:0,c:0}, e:{r:0,c:6}}); row++; // ── Period ── ws_data.push([ _xlCell('Period: ' + periodoString, { fill:XL.SV_PLAVA, fontColor:'FFFFFF', bold:true, align:'center' }), _xlCell('', {}), _xlCell('', {}), _xlCell('', {}), _xlCell('', {}), _xlCell('', {}), _xlCell('', {}) ]); merges.push({s:{r:1,c:0}, e:{r:1,c:6}}); row++; // ── Prazna ── ws_data.push([_xlCell('',{})]); row++; // ── Header tabele ── ws_data.push([ _xlCell('Rb.', { header:true, fill:XL.PLAVA, align:'center' }), _xlCell('Nalog', { header:true, fill:XL.PLAVA }), _xlCell('Datum', { header:true, fill:XL.PLAVA }), _xlCell('Klijent', { header:true, fill:XL.PLAVA }), _xlCell('Vozilo', { header:true, fill:XL.PLAVA }), _xlCell('Usluge (KM)',{ header:true, fill:XL.PLAVA, align:'right' }), _xlCell('Dijelovi (KM)',{ header:true, fill:XL.PLAVA, align:'right' }), _xlCell('UKUPNO (KM)',{ header:true, fill:XL.PLAVA, align:'right' }), _xlCell('Status', { header:true, fill:XL.PLAVA }), _xlCell('Izvršioci', { header:true, fill:XL.PLAVA }) ]); row++; // ── Redovi podataka ── var ukSvc = 0, ukPrt = 0, ukTotal = 0; odabrani.sort(function(a,b){ return a.date.localeCompare(b.date)||(a.time||'').localeCompare(b.time||''); }); odabrani.forEach(function(o, i){ var ts = svc(o); var tp = prt(o); var tt = ts + tp; var izvr = (o.services||[]).filter(function(s){ return s.radnik; }) .map(function(s){ return s.radnik; }) .filter(function(r,j,a){ return a.indexOf(r)===j; }).join(', '); var sm = { active:'Aktivni', waiting:'Na čekanju', scheduled:'Zakazan', done:'Završeno' }; var parno = i%2===0; var bg = parno ? XL.BIJELA : XL.RED_PARNA; var opts = { fill:bg, border:true }; ukSvc += ts; ukPrt += tp; ukTotal += tt; ws_data.push([ _xlNum(i+1, Object.assign({},opts,{align:'center'})), _xlCell(o.id, opts), _xlCell(fmtDate(o.date), opts), _xlCell(o.customer, opts), _xlCell(o.car, opts), _xlNum(Math.round(ts*100)/100, Object.assign({},opts,{numFmt:'#,##0.00',align:'right'})), _xlNum(Math.round(tp*100)/100, Object.assign({},opts,{numFmt:'#,##0.00',align:'right'})), _xlNum(Math.round(tt*100)/100, Object.assign({},opts,{numFmt:'#,##0.00',align:'right',bold:true})), _xlCell(sm[o.status]||o.status, opts), _xlCell(izvr, opts) ]); row++; }); // ── Ukupno red ── ws_data.push([ _xlCell('', {}), _xlCell('', {}), _xlCell('', {}), _xlCell('', {}), _xlCell('UKUPNO:', { bold:true, fill:XL.SIVA_BG, align:'right' }), _xlNum(Math.round(ukSvc*100)/100, { bold:true, fill:XL.PLAVA_BG, numFmt:'#,##0.00', align:'right' }), _xlNum(Math.round(ukPrt*100)/100, { bold:true, fill:XL.PLAVA_BG, numFmt:'#,##0.00', align:'right' }), _xlNum(Math.round(ukTotal*100)/100, { bold:true, fill:'D4EFDF', numFmt:'#,##0.00', align:'right' }), _xlCell('', {}), _xlCell('', {}) ]); // ── Kreiraj worksheet ── var ws = XLSX.utils.aoa_to_sheet(ws_data); ws['!merges'] = merges; ws['!cols'] = [ {wch:5}, {wch:14}, {wch:12}, {wch:20}, {wch:22}, {wch:14}, {wch:14}, {wch:14}, {wch:12}, {wch:22} ]; ws['!rows'] = [{hpt:22},{hpt:18},{hpt:6},{hpt:18}]; var wb = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb, ws, 'Izvještaj'); XLSX.writeFile(wb, naslov); toast('Excel izvještaj preuzet! ('+odabrani.length+' naloga)', 'ok'); } /* ════════ EXPORT: SVE NALOGE (Statistika) ════════ */ function exportSviNaloziExcel(){ if(typeof XLSX === 'undefined'){ toast('SheetJS nije dostupan. Provjeri internet vezu.','err'); return; } if(!orders.length){ toast('Nema naloga za export.','info'); return; } var ws_data = []; var merges = []; // Naslov ws_data.push([ _xlCell('AUTO SERVIS PRO — Pregled svih naloga', { header:true, fill:XL.PLAVA, sz:13, align:'center' }), _xlCell('',{}),_xlCell('',{}),_xlCell('',{}),_xlCell('',{}),_xlCell('',{}),_xlCell('',{}),_xlCell('',{}),_xlCell('',{}) ]); merges.push({s:{r:0,c:0},e:{r:0,c:8}}); ws_data.push([ _xlCell('Exportovano: '+fmtDate(today()), { fill:XL.SV_PLAVA, fontColor:'FFFFFF', align:'center' }), _xlCell('',{}),_xlCell('',{}),_xlCell('',{}),_xlCell('',{}),_xlCell('',{}),_xlCell('',{}),_xlCell('',{}),_xlCell('',{}) ]); merges.push({s:{r:1,c:0},e:{r:1,c:8}}); ws_data.push([_xlCell('',{})]); // Header ws_data.push([ _xlCell('Nalog', {header:true,fill:XL.PLAVA}), _xlCell('Datum', {header:true,fill:XL.PLAVA}), _xlCell('Klijent', {header:true,fill:XL.PLAVA}), _xlCell('Vozilo', {header:true,fill:XL.PLAVA}), _xlCell('Opis', {header:true,fill:XL.PLAVA}), _xlCell('Status', {header:true,fill:XL.PLAVA}), _xlCell('Usluge (KM)', {header:true,fill:XL.PLAVA,align:'right'}), _xlCell('Dijelovi (KM)',{header:true,fill:XL.PLAVA,align:'right'}), _xlCell('Ukupno (KM)', {header:true,fill:XL.PLAVA,align:'right'}) ]); var sm = { active:'Aktivni', waiting:'Na čekanju', scheduled:'Zakazan', done:'Završeno' }; var sorted = orders.slice().sort(function(a,b){ return b.date.localeCompare(a.date); }); var ukupno = 0; sorted.forEach(function(o,i){ var ts = svc(o), tp = prt(o), tt = ts+tp; ukupno += tt; var bg = i%2===0 ? XL.BIJELA : XL.RED_PARNA; var opt = {fill:bg,border:true}; ws_data.push([ _xlCell(o.id, opt), _xlCell(fmtDate(o.date), opt), _xlCell(o.customer, opt), _xlCell(o.car, opt), _xlCell(o.desc, opt), _xlCell(sm[o.status]||o.status, opt), _xlNum(Math.round(ts*100)/100, Object.assign({},opt,{numFmt:'#,##0.00',align:'right'})), _xlNum(Math.round(tp*100)/100, Object.assign({},opt,{numFmt:'#,##0.00',align:'right'})), _xlNum(Math.round(tt*100)/100, Object.assign({},opt,{numFmt:'#,##0.00',align:'right',bold:true})) ]); }); // Ukupno ws_data.push([ _xlCell('',{}),_xlCell('',{}),_xlCell('',{}),_xlCell('',{}),_xlCell('',{}), _xlCell('UKUPNO:', {bold:true,fill:XL.SIVA_BG,align:'right'}), _xlCell('',{}),_xlCell('',{}), _xlNum(Math.round(ukupno*100)/100, {bold:true,fill:'D4EFDF',numFmt:'#,##0.00',align:'right'}) ]); var ws = XLSX.utils.aoa_to_sheet(ws_data); ws['!merges'] = merges; ws['!cols'] = [{wch:14},{wch:12},{wch:20},{wch:22},{wch:28},{wch:12},{wch:14},{wch:14},{wch:14}]; ws['!rows'] = [{hpt:22},{hpt:18},{hpt:6},{hpt:18}]; var wb = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb, ws, 'Svi nalozi'); XLSX.writeFile(wb, 'AutoServis-svi-nalozi-'+today()+'.xlsx'); toast('Excel preuzet! ('+orders.length+' naloga)', 'ok'); } /* ════════════════════════════════════════════════════ MREŽA SERVISA — upis, ažuriranje, prikaz na mapi ════════════════════════════════════════════════════ */ /* Upis/ažuriranje servisa u mrežu sa GPS lokacijom */ async function upisiUMrezuServisa(){ var p = getPostavke(); if(!p.naziv){ toast('Unesite naziv servisa u Postavkama prije upisa u mrežu.','err'); return; } // Dohvati validan token (automatski osvježi ako treba) var token = await getValidToken(); if(!token){ toast('Niste prijavljeni. Prijavite se ponovo.','err'); return; } // Dohvati GPS lokaciju if(!navigator.geolocation){ upisiUMrezuBezGPS(p, token); return; } toast('Dohvatam vašu lokaciju...','info'); navigator.geolocation.getCurrentPosition( function(pos){ var lat = pos.coords.latitude; var lng = pos.coords.longitude; upisiUMrezuSaGPS(p, token, lat, lng); }, function(err){ // GPS greška - pokušaj ponovo sa svježim zahtjevom toast('Tražim lokaciju ponovo...','info'); navigator.geolocation.getCurrentPosition( function(pos2){ upisiUMrezuSaGPS(p, token, pos2.coords.latitude, pos2.coords.longitude); }, function(err2){ toast('Nije moguće dohvatiti GPS lokaciju. Provjerite dozvole u browseru.','err'); }, { timeout: 15000, maximumAge: 0, enableHighAccuracy: true } ); }, { timeout: 10000, maximumAge: 0, enableHighAccuracy: true } ); } function upisiUMrezuSaGPS(p, token, lat, lng){ localStorage.setItem('mreza_lat', lat); localStorage.setItem('mreza_lng', lng); _upisiUMrezu(p, token, lat, lng); } function upisiUMrezuBezGPS(p, token){ var lat = parseFloat(localStorage.getItem('mreza_lat') || '0'); var lng = parseFloat(localStorage.getItem('mreza_lng') || '0'); _upisiUMrezu(p, token, lat, lng); } async function _upisiUMrezu(p, token, lat, lng){ try { var _u = {}; try { _u = JSON.parse(localStorage.getItem('sb_user')||'{}'); } catch(e){} var userId = _u.id || null; if(!userId){ toast('Greška: korisnik nije prijavljen.','err'); return; } var podaci = { user_id: userId, naziv: p.naziv || '', adresa: p.adresa || '', telefon: p.telefon || '', grad: localStorage.getItem('grad_servisa') || '', lat: lat || null, lng: lng || null, radno_vrijeme: localStorage.getItem('radno_vrijeme') || '', aktivan: true }; // Pokušaj PATCH prvo var patchR = await fetch(SB_URL+'/rest/v1/mreza_servisa?user_id=eq.'+userId, { method: 'PATCH', headers: { 'apikey': SB_KEY, 'Authorization': 'Bearer '+token, 'Content-Type': 'application/json', 'Prefer': 'count=exact,return=minimal' }, body: JSON.stringify(podaci) }); var r; // Provjeri Content-Range header koji govori koliko redova je ažurirano var contentRange = patchR.headers.get('Content-Range') || ''; var countMatch = contentRange.match(/\/(\d+)$/); var updatedCount = countMatch ? parseInt(countMatch[1]) : -1; if(patchR.ok && updatedCount > 0){ // PATCH je ažurirao red - OK r = patchR; } else if(patchR.ok && updatedCount === 0){ // Nema zapisa - radi INSERT r = await fetch(SB_URL+'/rest/v1/mreza_servisa', { method: 'POST', headers: { 'apikey': SB_KEY, 'Authorization': 'Bearer '+token, 'Content-Type': 'application/json', 'Prefer': 'return=minimal' }, body: JSON.stringify(podaci) }); } else { r = patchR; } if(r.ok || r.status === 201 || r.status === 204){ localStorage.setItem('mreza_servis_upisan', '1'); toast('Servis uspješno upisan u mrežu! '+(lat?'📍 Lokacija sačuvana.':''), 'ok'); renderSettings(); } else { var errTxt = await r.text(); toast('Greška pri upisu: '+errTxt, 'err'); } } catch(e){ toast('Greška veze: '+e.message, 'err'); } } /* Dohvati sve servise iz mreže */ async function dohvatiMrezuServisa(){ try { var r = await fetch(SB_URL+'/rest/v1/mreza_servisa?select=*&aktivan=eq.true&order=naziv.asc', { headers: { 'apikey': SB_KEY, 'Authorization': 'Bearer '+(localStorage.getItem('sb_token')||'') } }); if(r.ok) return await r.json(); } catch(e){} return []; } /* Dohvati servise za QR portal (bez autentikacije) */ async function dohvatiMrezuZaPortal(){ try { var r = await fetch(SB_URL+'/rest/v1/mreza_servisa?select=naziv,adresa,telefon,grad,lat,lng,radno_vrijeme&aktivan=eq.true', { headers: { 'apikey': SB_KEY } }); if(r.ok) return await r.json(); } catch(e){} return []; } /* Izračunaj udaljenost između dvije GPS tačke (Haversine formula) */ function gpsUdaljenost(lat1, lng1, lat2, lng2){ var R = 6371; // km var dLat = (lat2-lat1) * Math.PI/180; var dLng = (lng2-lng1) * Math.PI/180; var a = Math.sin(dLat/2)*Math.sin(dLat/2) + Math.cos(lat1*Math.PI/180)*Math.cos(lat2*Math.PI/180)* Math.sin(dLng/2)*Math.sin(dLng/2); return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); } /* ════════════════════════════════════════════════════ BACKUP PODATAKA — export / import ════════════════════════════════════════════════════ */ function exportBackup(){ var backup = { verzija: '2.0', datum: new Date().toISOString(), app: 'AutoServisPRO', podaci: { nalozi: JSON.parse(localStorage.getItem('as5') || '[]'), katalog: JSON.parse(localStorage.getItem('as_katalog') || '[]'), custom_normativi:JSON.parse(localStorage.getItem('custom_normativi') || '[]'), cijena_sata: localStorage.getItem('cijena_sata') || '25', naziv_servisa: localStorage.getItem('naziv_servisa') || '', telefon_servisa: localStorage.getItem('telefon_servisa') || '', adresa_servisa: localStorage.getItem('adresa_servisa') || '', pib_servisa: localStorage.getItem('pib_servisa') || '', logo_servisa: localStorage.getItem('logo_servisa') || '' } }; var nalozi = backup.podaci.nalozi.length; var katalog = backup.podaci.katalog.length; var json = JSON.stringify(backup, null, 2); var blob = new Blob([json], { type: 'application/json' }); var url = URL.createObjectURL(blob); var a = document.createElement('a'); a.href = url; a.download = 'AutoServis-backup-' + new Date().toISOString().slice(0,10) + '.json'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); toast('Backup sačuvan! (' + nalozi + ' naloga, ' + katalog + ' katalog stavki)', 'ok'); } function importBackup(){ var input = document.createElement('input'); input.type = 'file'; input.accept = '.json'; input.onchange = function(e){ var file = e.target.files[0]; if(!file) return; var reader = new FileReader(); reader.onload = function(ev){ try { var backup = JSON.parse(ev.target.result); // Validacija if(!backup.app || backup.app !== 'AutoServisPRO'){ toast('Neispravan backup fajl!', 'err'); return; } if(!backup.podaci || !backup.podaci.nalozi){ toast('Backup fajl je oštećen!', 'err'); return; } var nalozi = backup.podaci.nalozi.length; var datum = backup.datum ? backup.datum.slice(0,10) : '?'; var poruka = 'Backup od ' + datum + ' sadrži ' + nalozi + ' naloga.\n\n'; poruka += 'UPOZORENJE: Svi trenutni podaci će biti ZAMIJENJENI.\n\n'; poruka += 'Nastaviti?'; if(!confirm(poruka)) return; // Uvoz svih podataka var p = backup.podaci; localStorage.setItem('as5', JSON.stringify(p.nalozi)); localStorage.setItem('as_katalog', JSON.stringify(p.katalog || [])); localStorage.setItem('custom_normativi', JSON.stringify(p.custom_normativi|| [])); if(p.cijena_sata) localStorage.setItem('cijena_sata', p.cijena_sata); if(p.naziv_servisa) localStorage.setItem('naziv_servisa', p.naziv_servisa); if(p.telefon_servisa) localStorage.setItem('telefon_servisa', p.telefon_servisa); if(p.adresa_servisa) localStorage.setItem('adresa_servisa', p.adresa_servisa); if(p.pib_servisa) localStorage.setItem('pib_servisa', p.pib_servisa); if(p.logo_servisa) localStorage.setItem('logo_servisa', p.logo_servisa); // Reload podataka u memoriju orders.length = 0; JSON.parse(localStorage.getItem('as5') || '[]').forEach(function(o){ orders.push(o); }); // Osvježi UI renderList(); updateChips(); renderTermini(); renderSettings(); toast('Backup uspješno učitan! ' + nalozi + ' naloga vraćeno.', 'ok'); } catch(err){ toast('Greška pri čitanju backup fajla: ' + err.message, 'err'); } }; reader.readAsText(file); }; input.click(); } setInterval(checkUpcoming, 60000); // Provjeri odmah pri pokretanju (standardni check 60/10 min) setTimeout(checkUpcoming, 3000); // Pri pokretanju — prikaži SVE nadolazeće termine za danas setTimeout(function(){ var now = new Date(); var todayStr = localDateStr(); var upcoming = orders.filter(function(o){ return o.date === todayStr && o.time && o.status !== 'done'; }); // Sortiraj po vremenu upcoming.sort(function(a,b){ return a.time.localeCompare(b.time); }); if(!upcoming.length) return; // Izgradi listu termina var listaHTML = upcoming.map(function(o){ var parts = o.time.split(':'); var termTime = new Date(now.getFullYear(), now.getMonth(), now.getDate(), parseInt(parts[0]||0), parseInt(parts[1]||0), 0); var diffMin = Math.round((termTime - now) / 60000); var vrijemeStr; if(diffMin < 0) vrijemeStr = 'Prošao'; else if(diffMin === 0) vrijemeStr = 'SADA'; else if(diffMin < 60) vrijemeStr = 'za '+diffMin+' min'; else vrijemeStr = 'za '+Math.floor(diffMin/60)+'h '+(diffMin%60?diffMin%60+'min':'')+''; var sm = SM[o.status]||SM.waiting; var div = document.createElement('div'); div.style.cssText = 'display:grid;grid-template-columns:50px 1fr auto;align-items:center;gap:10px;padding:10px 16px;border-bottom:1px solid #f0f2f8;cursor:pointer;transition:background .12s'; div.setAttribute('data-oid', o.id); div.addEventListener('mouseover', function(){ this.style.background='#f5f7fb'; }); div.addEventListener('mouseout', function(){ this.style.background=''; }); div.addEventListener('click', function(){ dismissDnevni(); selectOrder(this.dataset.oid); showView('detalji'); }); div.innerHTML = ''+o.time+'' + '
'+o.car+' ('+o.customer+')
' + '
'+o.desc+'
' + vrijemeStr; return div.outerHTML; }).join(''); // Prikaži overlay var ov = document.getElementById('dnevni-overlay'); if(!ov){ ov = document.createElement('div'); ov.id = 'dnevni-overlay'; ov.style.cssText = 'position:fixed;inset:0;z-index:8888;background:rgba(10,15,40,.6);display:flex;align-items:center;justify-content:center;backdrop-filter:blur(4px)'; document.body.appendChild(ov); } ov.innerHTML = '
'+ '
'+ '
'+ '
Dobro jutro!
'+ '
Termini za danas — '+upcoming.length+' nalog'+(upcoming.length>1?'a':'')+'
'+ '
'+ '
📅
'+ '
'+ '
'+listaHTML+'
'+ '
'+ ''+ '
'+ '
'; ov.style.display = 'flex'; }, 1200); function dismissDnevni(){ var ov = document.getElementById('dnevni-overlay'); if(ov) ov.style.display = 'none'; } // ═══ SPLASH SCREEN LOGIKA ═══ window.addEventListener('load', function(){ var splash = document.getElementById('splash'); if(!splash) return; // Nakon 4.5s — fade out setTimeout(function(){ splash.style.transition = 'opacity 0.8s ease'; splash.style.opacity = '0'; // Nakon fade out — ukloni iz DOM setTimeout(function(){ if(splash.parentNode) splash.parentNode.removeChild(splash); }, 850); }, 4500); }); /* ════════════ AUTH — SUPABASE LOGIN ════════════ */ function loginTab(tab){ // Prikazuje login ili reset formu (registracija je uklonjena) ['login','reset'].forEach(function(f){ var el = document.getElementById('forma-'+f); if(el) el.style.display = f===tab ? 'block' : 'none'; }); ['login-err','reset-msg'].forEach(function(id){ var e=document.getElementById(id); if(e) e.textContent=''; }); } async function prijaviSe(){ var email=(document.getElementById('login-email')||{}).value||''; var pass=(document.getElementById('login-pass')||{}).value||''; var remember=(document.getElementById('login-remember')||{}).checked||false; var err=document.getElementById('login-err'); var btn=document.getElementById('btn-prijava'); if(!email.trim()||!pass){ if(err) err.textContent='Unesite email i lozinku'; return; } if(btn){ btn.textContent='Prijavljivanje...'; btn.disabled=true; } try{ var r=await fetch(SB_URL+'/auth/v1/token?grant_type=password',{ method:'POST', headers:{'apikey':SB_KEY,'Content-Type':'application/json'}, body:JSON.stringify({email:email.trim(),password:pass}) }); var d=await r.json(); if(r.ok&&d.access_token){ // Spremi refresh token if(d.refresh_token) localStorage.setItem('sb_refresh_token', d.refresh_token); // Zapamti me — sacuvaj email u localStorage ako je checkbox označen if(remember){ localStorage.setItem('sb_remember_email', email.trim()); } else { localStorage.removeItem('sb_remember_email'); } authUspjesno(d.access_token,d.user); } else { var msg=d.error_description||d.msg||'Pogresna email adresa ili lozinka'; if(msg.indexOf('Invalid')>=0) msg='Pogresna email adresa ili lozinka'; if(err) err.textContent=msg; } } catch(e){ if(err) err.textContent='Greska veze. Provjerite internet.'; } if(btn){ btn.textContent='Prijavi se'; btn.disabled=false; } } async function registrujSe(){ var naziv=(document.getElementById('reg-naziv')||{}).value||''; var email=(document.getElementById('reg-email')||{}).value||''; var pass=(document.getElementById('reg-pass')||{}).value||''; var pass2=(document.getElementById('reg-pass2')||{}).value||''; var err=document.getElementById('reg-err'); var ok=document.getElementById('reg-ok'); var btn=document.getElementById('btn-registracija'); if(err) err.textContent=''; if(ok) ok.textContent=''; if(!naziv.trim()){ if(err) err.textContent='Unesite naziv radionice'; return; } if(!email.trim()){ if(err) err.textContent='Unesite email'; return; } if(pass.length<6){ if(err) err.textContent='Lozinka mora imati min. 6 znakova'; return; } if(pass!==pass2){ if(err) err.textContent='Lozinke se ne poklapaju'; return; } if(btn){ btn.textContent='Kreiram nalog...'; btn.disabled=true; } try{ var r=await fetch(SB_URL+'/auth/v1/signup',{ method:'POST', headers:{'apikey':SB_KEY,'Content-Type':'application/json'}, body:JSON.stringify({email:email.trim(),password:pass,data:{naziv:naziv.trim()}}) }); var d=await r.json(); if(r.ok&&d.access_token){ authUspjesno(d.access_token,d.user); } else if(r.ok){ if(ok) ok.textContent='Nalog kreiran! Provjerite email za potvrdu.'; loginTab('login'); } else { var msg=d.msg||d.error_description||'Greska registracije'; if(msg.indexOf('already')>=0) msg='Ovaj email je vec registrovan'; if(err) err.textContent=msg; } } catch(e){ if(err) err.textContent='Greska veze.'; } if(btn){ btn.textContent='Kreiraj nalog'; btn.disabled=false; } } async function resetLozinka(){ var email=(document.getElementById('reset-email')||{}).value||''; var msg=document.getElementById('reset-msg'); if(!email.trim()){ if(msg){ msg.style.color='#ff6b6b'; msg.textContent='Unesite email'; } return; } try{ await fetch(SB_URL+'/auth/v1/recover',{method:'POST',headers:{'apikey':SB_KEY,'Content-Type':'application/json'},body:JSON.stringify({email:email.trim()})}); if(msg){ msg.style.color='#16a05a'; msg.textContent='Link poslan na '+email.trim(); } } catch(e){ if(msg){ msg.style.color='#ff6b6b'; msg.textContent='Greska veze.'; } } } /* ════════════════════════════════════════════════════ VREMENSKI KOD ZA PREBACIVANJE UREĐAJA Mijenja se svaka 24h, baziran na datumu + tajnoj rijeci ════════════════════════════════════════════════════ */ var TAJNA_RIJEC = 'AutoServisPRO2026SV'; // Samo vlasnik zna function generirajDnevniKod(){ var danas = new Date(); var dateStr = danas.getFullYear() + '' + String(danas.getMonth()+1).padStart(2,'0') + String(danas.getDate()).padStart(2,'0'); var input = dateStr + TAJNA_RIJEC; // Jednostavan hash var hash = 0; for(var i=0; i🔑
' + '
Dnevni kod za danas
' + '
'+datumStr+' — važi samo danas
' + '
'+kod+'
' + '
Saopćite ovaj kod korisniku koji treba prebaciti licencu na novi uređaj. Sutra kod se automatski mijenja.
' + '' + ''; document.body.appendChild(ov); var btnZ = document.getElementById('btn-zatvori-kod'); if(btnZ) btnZ.onclick = function(){ ov.remove(); }; } /* ════════════════════════════════════════════════════ ZAŠTITA UREĐAJA — jedan nalog, jedan uređaj ════════════════════════════════════════════════════ */ function getDeviceId(){ var id = localStorage.getItem('_device_id'); if(!id){ id = 'dev_' + Date.now() + '_' + Math.random().toString(36).substr(2,9); localStorage.setItem('_device_id', id); } return id; } async function provjeriUredjaj(token, user){ var meta = user.user_metadata || {}; var email = (user.email || '').toLowerCase(); // Vlasnik nema ograničenja uređaja if(email === 'slavkovidmar72@gmail.com') return true; var registrovaniUredjaj = meta.device_id || null; var mojUredjaj = getDeviceId(); // Ako nema registrovanog uređaja — registruj ovaj if(!registrovaniUredjaj){ await registrujUredjaj(token, mojUredjaj); return true; } // Ako je isti uređaj — OK if(registrovaniUredjaj === mojUredjaj) return true; // Drugi uređaj — blokiraj i prikaži poruku prikaziBlokUredjaja(token, mojUredjaj, registrovaniUredjaj); return false; } async function registrujUredjaj(token, deviceId){ try { await fetch(SB_URL+'/auth/v1/user', { method: 'PUT', headers: { 'apikey': SB_KEY, 'Authorization': 'Bearer '+token, 'Content-Type': 'application/json' }, body: JSON.stringify({ data: { device_id: deviceId, device_registered: new Date().toISOString() } }) }); } catch(e){ console.error('Device registracija greška:', e); } } function prikaziBlokUredjaja(token, mojUredjaj, registrovaniUredjaj){ // Sakrij login overlay var lov = document.getElementById('login-overlay'); if(lov) lov.style.display='none'; // Prikaži blok ekran var blok = document.createElement('div'); blok.id = 'device-blok-overlay'; blok.style.cssText = 'position:fixed;inset:0;background:#0f141e;z-index:99999;display:flex;align-items:center;justify-content:center;font-family:Inter,sans-serif'; blok.innerHTML = '
' + '
🔒
' + '
Pristup odbijen
' + '
' + 'Ova licenca je već aktivirana na drugom uređaju.
' + 'Jedan nalog može se koristiti samo na jednom uređaju.' + '
' + '
' + '
Opcije:
' + '
1. Koristite originalni uređaj
' + '
2. Kontaktirajte podršku za prijenos licence
' + '
' + '' + '
' + '
Prebaci licencu na ovaj uređaj:
' + '
' + '' + '' + '
' + '
' + '
Podrška: +387 65 731 090
' + '
'; document.body.appendChild(blok); // Postavi onclick nakon dodavanja u DOM setTimeout(function(){ var btn = document.getElementById('btn-prebaci-uredjaj'); if(btn) btn.onclick = function(){ var input = document.getElementById('kod-uredjaj-input'); var uneseni = input ? input.value : ''; if(!uneseni.trim()){ input.style.borderColor = '#d63535'; input.placeholder = 'Unesite kod koji ste dobili od podrške!'; return; } if(!provjeriDnevniKod(uneseni)){ input.style.borderColor = '#d63535'; input.value = ''; input.placeholder = 'Neispravan kod — pokušajte ponovo'; return; } prebaciUredjaj(token, mojUredjaj); }; }, 50); } async function prebaciUredjaj(token, noviDeviceId){ // Korisnik tvrdi da je ovo njegov uređaj — zatraži potvrdu var potvrda = confirm('Da li ste sigurni? Stari uređaj više neće moći pristupiti aplikaciji.'); if(!potvrda) return; await registrujUredjaj(token, noviDeviceId); var blok = document.getElementById('device-blok-overlay'); if(blok) blok.remove(); toast('Uređaj uspješno prebačen!', 'ok'); // Ponovo inicijaliziraj setTimeout(function(){ window.location.reload(); }, 1000); } function authUspjesno(token, user){ localStorage.setItem('sb_token',token); localStorage.setItem('sb_user',JSON.stringify(user)); // ── PROVJERA LICENCE ── var meta = user.user_metadata || {}; var licencaDo = meta.licenca_do || null; // format: "YYYY-MM-DD" if(licencaDo){ var danas = new Date(); danas.setHours(0,0,0,0); var istek = new Date(licencaDo); istek.setHours(0,0,0,0); var diffDani = Math.round((istek - danas) / (1000*60*60*24)); if(diffDani < 0){ // Licenca istekla — blokiraj pristup var ov = document.getElementById('login-overlay'); if(ov) ov.style.display='flex'; // Prikaži poruku u login erroru setTimeout(function(){ var err = document.getElementById('login-err'); if(err){ err.style.color='#ff6b6b'; err.textContent='Vaša licenca je istekla '+Math.abs(diffDani)+' dana(dana) unazad. Kontaktirajte: 065 731 090'; } }, 100); localStorage.removeItem('sb_token'); localStorage.removeItem('sb_user'); return; } // Licenca ističe za 7 ili manje dana — prikaži upozorenje if(diffDani <= 7){ setTimeout(function(){ prikaziLicencaUpozorenje(diffDani, licencaDo); }, 2000); } } // ── PROVJERA UREĐAJA ── provjeriUredjaj(token, user).then(function(ok){ if(!ok) return; var ov2=document.getElementById('login-overlay'); if(ov2) ov2.style.display='none'; var naziv=meta.naziv||user.email||''; var nn=document.getElementById('nav-naziv'); var bo=document.getElementById('btn-odjava'); if(nn){ nn.textContent=naziv; nn.style.display='inline'; } if(bo) bo.style.display='inline-block'; }); } function zatvoriLicencaUpozorenje(){ var el=document.getElementById('lic-warn-overlay'); if(el) el.remove(); } function prikaziLicencaUpozorenje(dani, datumIsteka){ // Nemoj prikazivati više puta u istoj sesiji if(sessionStorage.getItem('_lic_warn')) return; sessionStorage.setItem('_lic_warn','1'); var boja = dani <= 3 ? '#d63535' : '#e8a020'; var ikona = dani <= 3 ? '🔴' : '🟡'; var poruka = dani === 0 ? 'Vaša licenca ističe DANAS!' : 'Vaša licenca ističe za ' + dani + (dani === 1 ? ' dan' : ' dana') + '!'; // Formatiraj datum var p = datumIsteka.split('-'); var datumFmt = p[2]+'.'+p[1]+'.'+p[0]+'.'; var overlay = document.createElement('div'); overlay.id = 'lic-warn-overlay'; overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:9998;display:flex;align-items:center;justify-content:center;backdrop-filter:blur(4px)'; overlay.innerHTML = '
' +'
' +'
'+ikona+'
' +'
'+poruka+'
' +'
Datum isteka: '+datumFmt+'
' +'
' +'
' +'
' +'Da biste nastavili koristiti Auto Servis PRO, obnovite licencu na vrijeme. ' +'Nakon isteka pristup će biti automatski onemogućen.' +'
' +'
' +'
Za obnovu kontaktirajte:
' +'
📞 065 731 090
' +'
' +'' +'
' +'
'; document.body.appendChild(overlay); overlay.addEventListener('click', function(e){ if(e.target===overlay){ zatvoriLicencaUpozorenje(); } }); } function odjaviSe(){ if(!confirm('Odjaviti se iz aplikacije?')) return; localStorage.removeItem('sb_token'); localStorage.removeItem('sb_user'); var ov=document.getElementById('login-overlay'); if(ov) ov.style.display='flex'; var nn=document.getElementById('nav-naziv'); var bo=document.getElementById('btn-odjava'); if(nn) nn.style.display='none'; if(bo) bo.style.display='none'; loginTab('login'); } /* Auto refresh tokena - poziva se svaki sat */ async function refreshToken(){ var token = localStorage.getItem('sb_token'); if(!token) return null; try { var r = await fetch(SB_URL+'/auth/v1/token?grant_type=refresh_token', { method: 'POST', headers: {'apikey': SB_KEY, 'Content-Type': 'application/json'}, body: JSON.stringify({ refresh_token: localStorage.getItem('sb_refresh_token') || '' }) }); if(r.ok){ var d = await r.json(); if(d.access_token){ localStorage.setItem('sb_token', d.access_token); if(d.refresh_token) localStorage.setItem('sb_refresh_token', d.refresh_token); return d.access_token; } } } catch(e){} return token; // vrati stari ako refresh ne uspije } /* Dohvati validan token - osvježi ako treba */ async function getValidToken(){ var token = localStorage.getItem('sb_token'); if(!token) return null; try { var parts = token.split('.'); if(parts[1]){ var data = JSON.parse(atob(parts[1])); var now = Math.floor(Date.now()/1000); // Ako ističe za manje od 10 minuta - osvježi if(data.exp && data.exp - now < 600){ var fresh = await refreshToken(); return fresh; } } } catch(e){} return token; } async function authInit(){ // Automatski ažuriraj stari portal URL var oldPortal = localStorage.getItem('portal_url') || ''; if(oldPortal.indexOf('sensational-boba') >= 0 || oldPortal.indexOf('netlify.app') >= 0){ localStorage.setItem('portal_url', 'https://cunami72.github.io/autoservis-portal'); } var token=localStorage.getItem('sb_token'); var userStr=localStorage.getItem('sb_user'); // Popuni zapamćeni email var remEmail=localStorage.getItem('sb_remember_email'); if(remEmail){ var el=document.getElementById('login-email'); if(el){ el.value=remEmail; } var cb=document.getElementById('login-remember'); if(cb) cb.checked=true; } if(!token||!userStr){ document.getElementById('login-overlay').style.display='flex'; return; } try{ var r=await fetch(SB_URL+'/auth/v1/user',{headers:{'apikey':SB_KEY,'Authorization':'Bearer '+token}}); if(r.ok){ var u=await r.json(); localStorage.setItem('sb_user',JSON.stringify(u)); authUspjesno(token,u); } else { localStorage.removeItem('sb_token'); localStorage.removeItem('sb_user'); document.getElementById('login-overlay').style.display='flex'; } } catch(e){ authUspjesno(token,JSON.parse(userStr)); } // offline - koristi cache } /* ════════════ QR KOD — VOZILA ════════════ */ var MJ=['jan','feb','mar','apr','maj','jun','jul','aug','sep','okt','nov','dec']; function formatDatum(d){ if(!d) return ''; var p=d.split('-'); return p[2]+'. '+MJ[parseInt(p[1])-1]+' '+p[0]+'.'; } function getNazivServisa(){ var n=localStorage.getItem('naziv_servisa'); if(!n){ n=prompt('Unesite naziv vaseg servisa za QR portal:','Auto Servis'); if(n) localStorage.setItem('naziv_servisa',n.trim()); else n='Auto Servis'; } return n; } function getTelefonServisa(){ var t=localStorage.getItem('telefon_servisa'); if(!t){ t=prompt('Unesite broj telefona vaseg servisa (prikazuje se na QR kartici):',''); if(t) localStorage.setItem('telefon_servisa',t.trim()); else t=''; } return t; } function getVoziloId(o){ var str=(o.customer+'|'+o.car+'|'+(o.year||'')).toLowerCase(); var h=0; for(var i=0;i' +'' +'' // Vozilo +'
'+o.car+(o.year?' ('+o.year+')':'')+'
' +'
'+o.customer+'
' // Servis info +'
' +'
'+naziv+'
' +(telefon?'
📞 '+telefon+'
':'') +'
' +''; btn.style.display='block'; btn.textContent='🖨️ Štampaj QR karticu'; btn.style.background='#1a1d2e'; btn.onclick=function(){ stampajQRKarticu(o,vid,naziv,telefon,servisi); }; ov.style.display='flex'; } function stampajQRKarticu(o,vid,naziv,telefon,servisi){ servisi = servisi || []; if(!Array.isArray(servisi)) servisi = []; var portalBase2 = localStorage.getItem('portal_url') || ''; var qrTarget = portalBase2 ? portalBase2+'?id='+vid : SB_URL+'/rest/v1/vozila_historija?id=eq.'+vid+'&select=*&apikey='+SB_KEY; var qr='https://api.qrserver.com/v1/create-qr-code/?size=260x260&margin=10&data='+encodeURIComponent(qrTarget); var html='' +'' +'' +'' +'
' +'
' +'' +'
'+o.car+(o.year?' ('+o.year+')':'')+' · '+o.customer+'
' +'
' +'
' +'' +'
Skenirajte za historiju servisa
' +'
' +'
' +'
'+naziv+'
' +(telefon?'
📞 '+telefon+'
':'') +'
' +'
' +' '; var w=window.open('','_blank','width=400,height=600'); w.document.write(html); w.document.close(); w.onload=function(){ w.focus(); w.print(); }; } /* ════════════ POSTAVKE RADIONICE ════════════ */ function getPostavke(){ return { naziv: localStorage.getItem('naziv_servisa') || '', telefon: localStorage.getItem('telefon_servisa') || '', adresa: localStorage.getItem('adresa_servisa') || '', logo: localStorage.getItem('logo_servisa') || '' }; } function savePostavku(key, value){ localStorage.setItem(key, value); if(key === 'naziv_servisa'){ var el = document.getElementById('nav-naziv'); if(el) el.textContent = value; } toast('Sacuvano!', 'ok'); } function spremiPostavku(key){ var el = document.getElementById('set-' + key); if(!el) return; savePostavku(key, el.value.trim()); renderSettings(); } function saveCijenaSataSettings(){ var el = document.getElementById('cs-input-settings'); if(el){ setCijenaSata(el.value); toast('Sacuvano!','ok'); } } function escSet(s){ return (s||'').replace(/"/g,'"'); } function postavkaRow(key, label, value, placeholder){ return '
' + '
'+label+'
' + '
' + '' + '' + '
' + '
'; } function spremiLozinku(){ var p1=(document.getElementById('set-new-pass')||{}).value||''; var p2=(document.getElementById('set-new-pass2')||{}).value||''; var msg=document.getElementById('set-pass-msg'); if(!p1.trim()){ if(msg){msg.style.color='#d63535';msg.textContent='Unesite novu lozinku';} return; } if(p1.length<6){ if(msg){msg.style.color='#d63535';msg.textContent='Lozinka mora imati min. 6 znakova';} return; } if(p1!==p2){ if(msg){msg.style.color='#d63535';msg.textContent='Lozinke se ne poklapaju';} return; } var token=localStorage.getItem('sb_token'); if(!token){ if(msg){msg.style.color='#d63535';msg.textContent='Niste prijavljeni';} return; } if(msg){msg.style.color='#888';msg.textContent='Mijenjam lozinku...';} fetch(SB_URL+'/auth/v1/user',{ method:'PUT', headers:{'apikey':SB_KEY,'Authorization':'Bearer '+token,'Content-Type':'application/json'}, body:JSON.stringify({password:p1}) }).then(function(r){ if(r.ok){ if(msg){msg.style.color='#16a05a';msg.textContent='Lozinka uspješno promijenjena!';} document.getElementById('set-new-pass').value=''; document.getElementById('set-new-pass2').value=''; } else { r.json().then(function(d){ if(msg){msg.style.color='#d63535';msg.textContent=d.msg||d.error_description||'Greška pri promjeni lozinke';} }); } }).catch(function(){ if(msg){msg.style.color='#d63535';msg.textContent='Greška veze. Provjerite internet.';} }); } function headerPreview(p){ var logo = p.logo; var naziv = p.naziv || 'Naziv radionice'; return '
' + (logo ? '' : '
🏭
') + '
' + '
'+escSet(naziv)+'
' + (p.adresa ? '
'+escSet(p.adresa)+'
' : '') + (p.telefon ? '
📞 '+escSet(p.telefon)+'
' : '') + '
' + '
'; } function uploadLogo(input){ var file = input.files[0]; if(!file) return; if(file.size > 200*1024){ toast('Logo mora biti manji od 200KB','err'); return; } var reader = new FileReader(); reader.onload = function(e){ localStorage.setItem('logo_servisa', e.target.result); toast('Logo sacuvan!','ok'); renderSettings(); }; reader.readAsDataURL(file); } function obrisiLogo(){ localStorage.removeItem('logo_servisa'); toast('Logo obrisan','ok'); renderSettings(); } function getLogoHTML(size){ size = size || 60; var logo = localStorage.getItem('logo_servisa'); if(logo) return ''; return ''; } function renderSettings(){ var user_email = ''; try { var u = JSON.parse(localStorage.getItem('sb_user')||'{}'); user_email = u.email||''; } catch(e){} var el = document.getElementById('settings-content'); if(!el) return; var p = getPostavke(); var cs = getCijenaSata(); var valuta = getValuta(); /* ── Kartica helper ── */ function card(ico, bgIco, title, sub, body){ return '
' // Header +'
' +'
' +'
'+ico+'
' +'
' +'
'+title+'
' +'
'+sub+'
' +'
' +'
' +'
' +body +'
'; } function field(id, label, val, ph, type){ type = type || 'text'; return '
' +'' +'' +'
'; } function saveBtn(fn, label){ label = label || 'Sačuvaj promjene'; return '
' +'' +'
'; } /* ── Kartica 1: Firma + Logo ── */ var firmaBody = '
' // Lijevo - polja +'
' +'
' +field('naziv_servisa','Naziv radionice', p.naziv, 'npr. Auto Servis Vidmar') +field('telefon_servisa','Broj telefona', p.telefon, 'npr. 065 731 090') +'
' +field('adresa_servisa','Adresa', p.adresa, 'npr. Ulica bb, Grad') +'
' +'
' +'
' +'Ovi podaci se prikazuju na svim dokumentima i QR kodu vozila.' +'
' +'
' // Desno - logo +'
' +'
' +(p.logo ? '' : '🏭') +'
' +'' +(p.logo ? '' : '') +'
' +'
' +saveBtn('sacuvajPostavkeFirma'); /* ── Kartica 2: Cijene ── */ var cijenaBody = '
' +'
' +'' +'
' +'' +''+valuta+'/sat' +'
' +'
' +'
' +'' +'
' +'' +'' +'
' +'
Prikazuje se na svim računima i izvještajima
' +'
' +'
' +saveBtn('saveCijenaSataSettings'); /* ── Kartica 3: Lozinka ── */ var lozinkaBody = '
' +'
' +'' +'' +'
' +'
' +'' +'' +'
' +'
' +'
' +saveBtn('spremiLozinku','Promijeni lozinku'); /* ── Kartica 4: Mreža ── */ var mrezaBody = '
' +'
' +'
📍 Lokacija na mapi
' +'
Ažurirajte GPS lokaciju
' +'' +'
' +'
' +'
Status u mreži
' +'
Vidljivost za klijente
' +'
🟢 Aktivan
' +'
' +'
' +'
' +'💡 Ažurirajte lokaciju svaki put kada promijenite adresu servisa.' +'
'; /* ── Kartica 5: Backup ── */ var backupBody = '
' +'
' +'
📤 Izvezi backup
' +'
Preuzmi sve podatke kao .json fajl
' +'' +'
' +'
' +'
📥 Uvezi backup
' +'
Učitaj prethodno sačuvan .json fajl
' +'' +'
' +'
' +'
' +'💡 Radite backup jednom sedmično i čuvajte ga na barem 2 mjesta (USB + cloud).' +'
'; /* ── Kartica 6: Preview ── */ var previewBody = headerPreview(p); /* ── Kartica 7: Dnevni kod (samo vlasnik) ── */ var kodKartica = user_email === 'slavkovidmar72@gmail.com' ? card('🗝️','#fdeaea','Dnevni kod','Saopćite korisniku koji treba prebaciti licencu', '') : ''; el.innerHTML = '
' +'
⚙️ Postavke
' +'
Upravljajte aplikacijom i prilagodite je svojim potrebama
' +card('🏢','#edf2fc','Podaci radionice','Ovi podaci prikazuju se na dokumentima i QR kodu', firmaBody) +card('💰','#fef9e3','Cijene i valuta','Postavite osnovne cijene i valutu', cijenaBody) +card('🔑','#fdeaea','Sigurnost','Upravljajte svojom lozinkom', lozinkaBody) +card('🌐','#e8f7ef','Mreža servisa','Vidljivost na mapi za klijente', mrezaBody) +card('💾','#f0f7ff','Backup i podaci','Upravljajte backup-om i izvozom podataka', backupBody) +card('👁️','#f5f7fb','Preview dokumenta','Kako izgleda zaglavlje na računima', previewBody) +kodKartica +'
'; } function settField(key, label, value, placeholder){ return '
' +'' +'' +'
'; } function sacuvajPostavkeFirma(){ var fields = ['naziv_servisa','telefon_servisa','adresa_servisa']; fields.forEach(function(key){ // Proba sa i bez "set-" prefiksa var el = document.getElementById('set-'+key) || document.getElementById(key); if(el) localStorage.setItem(key, el.value.trim()); }); var naziv = localStorage.getItem('naziv_servisa')||''; var nn = document.getElementById('nav-naziv'); if(nn) nn.textContent = naziv; toast('Postavke sačuvane!', 'ok'); renderSettings(); } /* ══════════════════════════════ MOBILNI JS ══════════════════════════════ */ var _isMob = false; var _mobView = 'lista'; function mobVise(){ if(!_isMob) return; var m = document.getElementById('mob-vise-menu'); var o = document.getElementById('mob-vise-overlay'); var btn = document.getElementById('mbn-vise'); if(!m) return; var isOpen = m.style.display !== 'none'; m.style.display = isOpen ? 'none' : 'block'; if(o) o.style.display = isOpen ? 'none' : 'block'; if(btn) btn.classList.toggle('on', !isOpen); } function mobZatvoriVise(){ var m = document.getElementById('mob-vise-menu'); var o = document.getElementById('mob-vise-overlay'); var btn = document.getElementById('mbn-vise'); if(m) m.style.display = 'none'; if(o) o.style.display = 'none'; if(btn) btn.classList.remove('on'); } function mobNavVise(view){ mobZatvoriVise(); document.querySelectorAll('.mob-nav-btn').forEach(function(b){ b.classList.remove('on'); b.querySelector('.mni-bg').style.background=''; }); var btn = document.getElementById('mbn-vise'); if(btn){ btn.classList.add('on'); btn.querySelector('.mni-bg').style.background='#edf2fc'; } showView(view); mobShowDetail(); } (function(){ var sx=0, sy=0, threshold=60, restraint=120; function onStart(e){ var t=e.touches?e.touches[0]:e; sx=t.clientX; sy=t.clientY; } function onEnd(e){ if(!_isMob) return; var t=e.changedTouches?e.changedTouches[0]:e; var dx=t.clientX-sx, dy=t.clientY-sy; if(Math.abs(dx)restraint) return; if(dx>0&&_mobView==='detail'){ mobGoBack(); } else if(dx<0&&_mobView==='lista'&&selId){ mobShowDetail(); } } document.addEventListener('touchstart',onStart,{passive:true}); document.addEventListener('touchend',onEnd,{passive:true}); })(); function checkMobile(){ _isMob = window.innerWidth <= 768; var main = document.querySelector('.main'); if(!main) return; if(_isMob){ main.classList.add('mob-show-lista'); main.classList.remove('mob-show-detail'); if(!sessionStorage.getItem('_mob_hint')){ sessionStorage.setItem('_mob_hint','1'); setTimeout(function(){ var h = document.createElement('div'); h.style.cssText = 'position:fixed;bottom:calc(64px + env(safe-area-inset-bottom));left:50%;transform:translateX(-50%);background:rgba(26,29,46,.92);color:#fff;font-family:Inter,sans-serif;font-size:12px;font-weight:600;padding:9px 18px;border-radius:20px;z-index:9000;white-space:nowrap;box-shadow:0 4px 16px rgba(0,0,0,.3);pointer-events:none'; h.textContent = '👆 Koristite donju navigaciju za sve funkcije'; document.body.appendChild(h); setTimeout(function(){ h.style.opacity='0'; h.style.transition='opacity .5s'; setTimeout(function(){ h.remove(); },600); },3500); }, 1200); } } else { main.classList.remove('mob-show-lista','mob-show-detail'); } } function mobShowDetail(){ _mobView = 'detail'; var main = document.querySelector('.main'); if(main){ main.classList.add('mob-show-detail'); main.classList.remove('mob-show-lista'); } var bb = document.getElementById('mob-back-btn'); if(bb) bb.style.display='flex'; } function mobGoBack(){ _mobView = 'lista'; var main = document.querySelector('.main'); if(main){ main.classList.add('mob-show-lista'); main.classList.remove('mob-show-detail'); } var bb = document.getElementById('mob-back-btn'); if(bb) bb.style.display='none'; } function mobNav(view){ document.querySelectorAll('.mob-nav-btn').forEach(function(b){ b.classList.remove('on'); var bg = b.querySelector('.mni-bg'); if(bg) bg.style.background=''; }); var btn = document.getElementById('mbn-'+view); if(btn){ btn.classList.add('on'); var bg = btn.querySelector('.mni-bg'); if(bg) bg.style.background='#edf2fc'; } showView(view); if(view==='detalji'||view==='kasa'){ if(selId) mobShowDetail(); } else { mobShowDetail(); } } window.addEventListener('resize', function(){ checkMobile(); }); /* ── Sidebar JS ── */ function sidebarInit(){ var collapsed = localStorage.getItem('sidebar_collapsed') === '1'; var sb = document.getElementById('app-sidebar'); var btn = document.getElementById('sidebar-toggle'); if(!sb) return; if(collapsed){ sb.classList.add('collapsed'); if(btn) btn.textContent = '☰'; } else { sb.classList.remove('collapsed'); if(btn) btn.textContent = '◀'; } // Sinhronizuj aktivnu stavku sa trenutnim viewom var currentView = localStorage.getItem('current_view') || 'detalji'; document.querySelectorAll('.sn-item').forEach(function(b){ b.classList.remove('on'); }); var sn = document.getElementById('sn-'+currentView); if(sn) sn.classList.add('on'); document.querySelectorAll('#mob-sb-panel .sn-item').forEach(function(b){ b.classList.remove('on'); }); var msn = document.getElementById('msn-'+currentView); if(msn) msn.classList.add('on'); } function sidebarToggle(){ var sb = document.getElementById('app-sidebar'); var btn = document.getElementById('sidebar-toggle'); if(!sb) return; var isCollapsed = sb.classList.toggle('collapsed'); localStorage.setItem('sidebar_collapsed', isCollapsed ? '1' : '0'); if(btn) btn.textContent = isCollapsed ? '☰' : '◀'; // Spremi stanje setTimeout(function(){ window.dispatchEvent(new Event('resize')); }, 310); } document.addEventListener('keydown', function(e){ if((e.ctrlKey || e.metaKey) && e.key === 'b'){ e.preventDefault(); sidebarToggle(); } }); document.addEventListener('DOMContentLoaded', function(){ var btn = document.getElementById('sidebar-toggle'); if(btn) btn.addEventListener('click', sidebarToggle); sidebarInit(); }); function mobSidebarOpen(){ document.getElementById('mob-sb-overlay').classList.add('open'); document.getElementById('mob-sb-panel').classList.add('open'); } function mobSidebarClose(){ document.getElementById('mob-sb-overlay').classList.remove('open'); document.getElementById('mob-sb-panel').classList.remove('open'); } /* Override showView da sinhronizuje sidebar */ var _origShowView2 = showView; showView = function(v){ _origShowView2(v); localStorage.setItem('current_view', v); // Desktop sidebar document.querySelectorAll('#app-sidebar .sn-item').forEach(function(b){ b.classList.remove('on'); }); var sn = document.getElementById('sn-'+v); if(sn) sn.classList.add('on'); // Mob sidebar document.querySelectorAll('#mob-sb-panel .sn-item').forEach(function(b){ b.classList.remove('on'); }); var msn = document.getElementById('msn-'+v); if(msn) msn.classList.add('on'); }; // Override selectOrder za mobilni var _origSelectOrder = selectOrder; selectOrder = function(id){ _origSelectOrder(id); if(_isMob){ mobShowDetail(); var bb = document.getElementById('mob-back-btn'); if(bb) bb.style.display='flex'; } };
🔧
AUTO SERVIS