KRONOS

Norský finanční deník

0
0
0
h
}[S.targetCurrency||'CZK']||S.targetCurrency||'';const _hcw=document.getElementById('heroCzk');if(_hcw){_hcw.parentElement.style.display=_sc?'':'none';if(_sc)_hcw.parentElement.innerHTML='≈ '+fmtN(net*rate)+' '+_sym;}document.getElementById('heroGoalLbl').textContent=fmtN(goal);document.getElementById('heroPct').textContent=pct+'%';document.getElementById('heroBar').style.width=pct+'%';document.getElementById('heroGoalWrap').style.display=goal>0?'block':'none';const cmpEl=document.getElementById('heroCmp');if(prevNet>0){const diff=Math.round((net-prevNet)/prevNet*100);cmpEl.style.display='inline';cmpEl.textContent=(diff>=0?'▲ +':'▼ ')+diff+'%';}else cmpEl.style.display='none';const msgs=[];if(pct>=100)msgs.push('🎯 '+t('goal')+' ✓');else if(pct>=75)msgs.push((100-pct)+'% → '+t('goal'));const msgEl=document.getElementById('heroMsg');if(msgs.length){msgEl.textContent=msgs[0];msgEl.style.display='inline-block';}else msgEl.style.display='none';const prevH=prev.reduce((s,x)=>s+x.hours,0);document.getElementById('mHours').textContent=fmtH(totH)+' h';document.getElementById('mDays').textContent=workedDays;document.getElementById('mGross').textContent=fmtN(gross);document.getElementById('mTax').textContent=fmtN(taxA);const hCmp=document.getElementById('mHoursCmp');if(prevH>0){const d=Math.round((totH-prevH)/prevH*100);hCmp.textContent=(d>=0?'▲ +':'▼ ')+d+'%';hCmp.className='sc-cmp '+(d>0?'up':d<0?'dn':'eq');}else hCmp.textContent='';const dates=[...new Set(allData.map(x=>x.date))].sort((a,b)=>b.localeCompare(a));let streak=0,cur=new Date();cur.setHours(12,0,0,0);for(const d of dates){const dk=cur.toISOString().slice(0,10);if(d===dk){streak++;cur.setDate(cur.getDate()-1);}else if(d=2){sb.style.display='block';document.getElementById('streakTxt').textContent=`${streak} ${t('streakMsg')}`;}else sb.style.display='none';const daysInM=new Date(yr,mo,0).getDate(),todayD=now.getMonth()+1===mo&&now.getFullYear()===yr?now.getDate():daysInM;const avgH=workedDays>0?totH/workedDays:0;let wdLeft=0;for(let d=todayD+1;d<=daysInM;d++){const dw=new Date(yr,mo-1,d).getDay();if(dw>0&&dw<6)wdLeft++;}const projected=net+(avgH*wage*nf()*wdLeft);const pb=document.getElementById('predBanner');if(fe.length>0&&wdLeft>0){pb.style.display='flex';document.getElementById('predTxt').textContent=`~${fmtN(projected)} kr (≈ ${fmtN(projected*rate)} Kč)${projected>=goal?' ✓':''}`;}else pb.style.display='none';renderCharts(fe,wage,m,yr,mo,daysInM,todayD,avgH);renderMonthList(fe,wage);const htEl=document.getElementById('homeTasks');if(htEl){const pending=allTasks.filter(x=>x.status!=='done');if(pending.length){htEl.innerHTML=`
${t('tasks')}${pending.length}
${pending.slice(0,5).map(tk=>`
${tk.status==='progress'?'
':''}
${tk.title}
${tk.notes?.length?tk.notes.length+' '+t('taskNotes').toLowerCase():t(tk.status)}
${t(tk.status)}
`).join('')}
`;}else htEl.innerHTML='';}} function renderMonthList(fe,wage){const cnt=fe.length;document.getElementById('mListCount').textContent=cnt;const btn=document.getElementById('btnDelMonth');if(btn)btn.style.display=cnt>0?'block':'none';const list=document.getElementById('mList');if(!fe.length){list.innerHTML=`
${t('noEntries')}
`;return;}list.innerHTML=fe.map(x=>{const gV=Math.round(x.hours*wage),nV=Math.round(x.hours*wage*nf()),big=x.hours>=8,col=big?'#10B981':'#6366F1',bg=big?'#F0FDF4':'#EEF2FF';return`
${fmtDate(x.date)}${x.employer?` ${x.employer}`:''}
${x.from||''}${x.to?' → '+x.to:''} · ${fmtH(x.hours)} h${x.breakMin>0?` ${x.breakMin}min`:''}
+${fmtN(gV)} kr
${fmtN(nV)} kr
`;}).join('');} function renderCharts(fe,wage,m,yr,mo,dim,todayD,avgH){const labels=[],earn=[],hrs=[],isGross=earnMode==='gross',mult=isGross?1:nf();for(let d=1;d<=dim;d++){const k=`${m}-${pad(d)}`,h=fe.filter(x=>x.date===k).reduce((s,x)=>s+x.hours,0);labels.push(d);earn.push(+(h*wage*mult).toFixed(0));hrs.push(+h.toFixed(2));}const ep=earn.filter(v=>v>0),avgE=ep.length?Math.round(ep.reduce((a,b)=>a+b,0)/ep.length):0,hp=hrs.filter(v=>v>0),avgHrs=hp.length?+(hp.reduce((a,b)=>a+b,0)/hp.length).toFixed(2):0;document.getElementById('chartAvgLbl').textContent=`${t('avgLabel')} ${fmtN(avgE)} kr`;document.getElementById('hoursAvgLbl').textContent=`${t('avgLabel')} ${String(avgHrs).replace('.',',')} h`;['bar','hours','cum','week'].forEach(k=>{if(charts[k]){charts[k].destroy();charts[k]=null;}});charts.bar=new Chart(document.getElementById('barChart'),{type:'bar',data:{labels,datasets:[{data:earn,backgroundColor:earn.map(v=>v>avgE&&v>0?'#6366F1':v>0?'#A5B4FC':'transparent'),borderRadius:4,barPercentage:.7},{type:'line',data:labels.map(()=>avgE),borderColor:'#F59E0B',borderWidth:1.5,borderDash:[5,4],pointRadius:0,fill:false}]},options:{...chartOpts({y:{ticks:{callback:v=>v>=1000?Math.round(v/1000)+'k':v}}}),plugins:{legend:{display:false},tooltip:{callbacks:{label:ctx=>fmtN(ctx.parsed.y)+' kr'}}}}});charts.hours=new Chart(document.getElementById('hoursChart'),{type:'bar',data:{labels,datasets:[{data:hrs,backgroundColor:hrs.map(v=>v>=8?'#10B981':v>0?'#6EE7B7':'transparent'),borderRadius:4,barPercentage:.7},{type:'line',data:labels.map(()=>avgHrs),borderColor:'#6366F1',borderWidth:1.5,borderDash:[5,4],pointRadius:0,fill:false}]},options:{...chartOpts({y:{ticks:{callback:v=>v+'h'}}}),plugins:{legend:{display:false},tooltip:{callbacks:{label:ctx=>ctx.parsed.y+' h'}}}}});let cum=0,cumP=0;const cumR=[],cumPred=[],wAdpd=new Set(fe.map(x=>x.date)).size>0?fe.reduce((s,x)=>s+x.hours,0)/new Set(fe.map(x=>x.date)).size:0;for(let d=1;d<=dim;d++){const k=`${m}-${pad(d)}`,h=fe.filter(x=>x.date===k).reduce((s,x)=>s+x.hours,0);if(h>0)cum+=h*wage*nf();cumR.push(d<=todayD?Math.round(cum):null);if(d===todayD)cumP=cum;const dw=new Date(yr,mo-1,d).getDay();if(d>=todayD){if(dw>0&&dw<6&&d>todayD)cumP+=wAdpd*wage*nf();cumPred.push(Math.round(cumP));}else cumPred.push(null);}charts.cum=new Chart(document.getElementById('cumChart'),{type:'line',data:{labels,datasets:[{data:cumR,borderColor:'#6366F1',borderWidth:2,fill:true,backgroundColor:'rgba(99,102,241,.06)',pointRadius:0,tension:.4,spanGaps:false},{data:cumPred,borderColor:'#94A3B8',borderWidth:1.5,borderDash:[4,4],pointRadius:0,fill:false,tension:.4,spanGaps:false}]},options:{...chartOpts({y:{ticks:{callback:v=>v>=1000?Math.round(v/1000)+'k':v}}}),plugins:{legend:{display:false},tooltip:{callbacks:{label:ctx=>fmtN(ctx.parsed.y)+' kr'}}}}});const wl=T[lang].wd,ws=[0,0,0,0,0,0,0],wc=[0,0,0,0,0,0,0];allData.forEach(x=>{const dw=(new Date(x.date+'T12:00:00').getDay()+6)%7;ws[dw]+=x.hours*wage*nf();wc[dw]++;});const wa=ws.map((s,i)=>wc[i]>0?Math.round(s/wc[i]):0);charts.week=new Chart(document.getElementById('weekChart'),{type:'bar',data:{labels:wl,datasets:[{data:wa,backgroundColor:wa.map((_,i)=>i<5?'#6366F1':'#A5B4FC'),borderRadius:3,barPercentage:.7}]},options:{...chartOpts({y:{ticks:{callback:v=>v>=1000?Math.round(v/1000)+'k':v}}}),plugins:{legend:{display:false},tooltip:{callbacks:{label:ctx=>fmtN(ctx.parsed.y)+' kr'}}}}});} function renderCareer(){const wage=S.wage,rate=S.rate,totH=allData.reduce((s,x)=>s+x.hours,0),net=totH*wage*nf(),days=new Set(allData.map(x=>x.date)).size;let bestNet=0;allData.forEach(x=>{const n=x.hours*wage*nf();if(n>bestNet)bestNet=n;});document.getElementById('cHours').textContent=fmtH(totH)+' h';document.getElementById('cNet').textContent=fmtN(net);document.getElementById('cDays').textContent=days;document.getElementById('cBest').textContent=fmtN(bestNet);const msData=[{icon:'🌱',label:t('totalHours').split(' ')[0],target:1,val:allData.length},{icon:'⚡',label:'100 h',target:100,val:totH},{icon:'💎',label:'500 h',target:500,val:totH},{icon:'💰',label:'10k kr',target:10000,val:net},{icon:'🏆',label:'50k kr',target:50000,val:net},{icon:'🚀',label:'100k kr',target:100000,val:net}];document.getElementById('milestones').innerHTML=msData.map(ms=>{const done=ms.val>=ms.target,pct=Math.min(100,Math.round(ms.val/ms.target*100));return`
${ms.icon}
${ms.label}${done?' ':''}
${pct}%
`;}).join('');const byMonth={};allData.forEach(x=>{const mm=x.date.slice(0,7);if(!byMonth[mm])byMonth[mm]={h:0,net:0};byMonth[mm].h+=x.hours;byMonth[mm].net+=x.hours*wage*nf();});const mths=Object.keys(byMonth).sort((a,b)=>b.localeCompare(a)),maxNet=Math.max(...mths.map(mm=>byMonth[mm].net),1);document.getElementById('monthBreakdown').innerHTML=mths.map(mm=>{const[yr2,mo2]=mm.split('-').map(Number),d=byMonth[mm],p=Math.min(100,Math.round(d.net/maxNet*100)),isG=d.net>=S.goal;return`
${T[lang].months[mo2-1]} ${yr2}
${isG?'':''}
${fmtN(d.net)} kr
${fmtH(d.h)} h
`;}).join('');const byEmp={};allData.forEach(x=>{const e=x.employer||'—';if(!byEmp[e])byEmp[e]={h:0,net:0,days:new Set()};byEmp[e].h+=x.hours;byEmp[e].net+=x.hours*wage*nf();byEmp[e].days.add(x.date);});document.getElementById('empBreakdown').innerHTML=Object.entries(byEmp).sort((a,b)=>b[1].net-a[1].net).map(([name,d])=>`
${name}${fmtN(d.net)} kr · ${fmtH(d.h)} h
≈ ${fmtN(d.net*rate)} Kč · Ø ${fmtN(d.net/Math.max(1,d.days.size))} kr/${t('perDay')}
`).join('');const hm=document.getElementById('heatmap'),dMap={};allData.forEach(x=>{dMap[x.date]=(dMap[x.date]||0)+x.hours;});const start=new Date(),dow=(start.getDay()+6)%7;start.setDate(start.getDate()-363-dow);const totalW=Math.ceil(365/7)+1;let hmHtml='',lastM=-1;const mlabels=[];for(let w=0;w`;}}hm.innerHTML=hmHtml;hm.style.gridTemplateColumns=`repeat(${totalW},12px)`;const mlEl=document.getElementById('hmMonthLabels');mlEl.style.width=totalW*15+'px';mlEl.innerHTML=mlabels.map(l=>`${T[lang].months[l.m].slice(0,3)}`).join('');} window.goToMonth=m=>{selectedMonth=m;document.getElementById('monthPick').value=m;setTab('home');}; const dlBlob=(c,n,t2)=>{const a=document.createElement('a');a.href=URL.createObjectURL(new Blob(['\uFEFF'+c],{type:t2}));a.download=n;a.click();}; window.exportCSV=()=>{const hdr='Date;Employer;Hours;NOK gross;NOK net;CZK;Break;Note';const rows=allData.map(x=>{const g=Math.round(x.hours*S.wage),n=Math.round(g*nf());return[x.date,x.employer||'',x.hours.toFixed(2).replace('.',','),g,n,Math.round(n*S.rate),x.breakMin||0,'"'+(x.note||'').replace(/"/g,'""')+'"'].join(';')});dlBlob([hdr,...rows].join('\n'),'kronos_export.csv','text/csv;charset=utf-8;');}; window.exportJSON=()=>dlBlob(JSON.stringify({exportDate:new Date().toISOString(),settings:S,logs:allData},null,2),'kronos_backup.json','application/json'); window.importJSON=e=>{const file=e.target.files[0];if(!file)return;const r=new FileReader();r.onload=async ev=>{try{const d=JSON.parse(ev.target.result);if(!d.logs)throw new Error('Invalid');for(const log of d.logs){const{_id,...l}=log;await addDoc(collection(db,`users/${user.uid}/logs`),l);}const m=document.getElementById('importMsg');m.textContent='OK: '+d.logs.length;m.style.color='var(--ok)';m.style.display='block';setTimeout(()=>m.style.display='none',3000);}catch(err){const m=document.getElementById('importMsg');m.textContent='Error: '+err.message;m.style.color='var(--er)';m.style.display='block';}e.target.value='';};r.readAsText(file);}; // PDF const PDF_T={cs:{title:'Výkaz hodin',name:'Jméno',company:'Firma',period:'Období',date:'Datum',time:'Od – Do',hours:'Hodiny',note:'Co jsem dělal',total:'CELKEM HODIN',page:'Strana',generated:'Vygenerováno',daysCount:'Odpracovaných dní',signEmp:'Zaměstnanec',signCo:'Zaměstnavatel'},en:{title:'Hours report',name:'Name',company:'Company',period:'Period',date:'Date',time:'From – To',hours:'Hours',note:'Description',total:'TOTAL HOURS',page:'Page',generated:'Generated',daysCount:'Days worked',signEmp:'Employee',signCo:'Employer'},no:{title:'Timeoversikt',name:'Navn',company:'Firma',period:'Periode',date:'Dato',time:'Fra – Til',hours:'Timer',note:'Beskrivelse',total:'TOTALT TIMER',page:'Side',generated:'Generert',daysCount:'Arbeidsdager',signEmp:'Ansatt',signCo:'Arbeidsgiver'}}; let _pdfFontB64=null; async function loadPdfFont(){if(_pdfFontB64)return _pdfFontB64;try{const r=await fetch('https://cdn.jsdelivr.net/npm/dejavu-fonts-ttf@2.37.3/ttf/DejaVuSans.ttf');if(!r.ok)return null;const buf=await r.arrayBuffer(),bytes=new Uint8Array(buf);let bin='';const ck=0x8000;for(let i=0;i{window._pdfMonth=targetMonth||getM();document.getElementById('pdfLoadingMsg').style.display='none';document.getElementById('pdfLangModal').classList.add('open');}; window.generatePDF=async pdfLang=>{if(!window.jspdf){alert('PDF loading…');return;}document.getElementById('pdfLoadingMsg').style.display='block';const m=window._pdfMonth||getM(),fe=allData.filter(x=>x.date&&x.date.slice(0,7)===m).sort((a,b)=>a.date.localeCompare(b.date)||(a.from||'').localeCompare(b.from||''));const{jsPDF}=window.jspdf;const doc2=new jsPDF();const PT=PDF_T[pdfLang]||PDF_T.cs;const fb=await loadPdfFont();const fnt=fb?'DejaVu':'helvetica';if(fb){doc2.addFileToVFS('DejaVuSans.ttf',fb);doc2.addFont('DejaVuSans.ttf','DejaVu','normal');doc2.addFont('DejaVuSans.ttf','DejaVu','bold');doc2.setFont('DejaVu');}const[yr,mo]=m.split('-').map(Number),monthName=T[pdfLang].months[mo-1]+' '+yr;const totH=fe.reduce((s,x)=>s+x.hours,0),days=new Set(fe.map(x=>x.date)).size;const decSep=pdfLang==='en'?'.':',';const fmtHrs=h=>h.toFixed(2).replace('.',decSep)+' h';const dlocale=pdfLang==='no'?'nb-NO':pdfLang==='en'?'en-GB':'cs-CZ';const fmtPdfDate=d=>new Date(d+'T12:00:00').toLocaleDateString(dlocale,{weekday:'short',day:'numeric',month:'short'});doc2.setFillColor(99,102,241);doc2.rect(0,0,210,16,'F');doc2.setTextColor(255);doc2.setFontSize(12);doc2.text('KRONOS',14,7.5);doc2.setFontSize(9);doc2.text(PT.title+' — '+monthName,14,13);doc2.setFontSize(7);doc2.text(new Date().toLocaleDateString(dlocale),196,13,{align:'right'});doc2.setTextColor(40);doc2.setFontSize(8.5);let y=21;const infoParts=[S.name&&(PT.name+': '+S.name),S.company&&(PT.company+': '+S.company),PT.period+': '+monthName,PT.daysCount+': '+days].filter(Boolean);doc2.text(infoParts.join(' · '),14,y);const rc=fe.length,fs=rc<=18?9.5:rc<=26?8.5:rc<=34?7.5:rc<=44?6.8:6.2,pad2=rc<=18?2.2:rc<=26?1.6:rc<=34?1.2:rc<=44?.95:.8,noteOv=rc>26?'ellipsize':'linebreak';doc2.autoTable({startY:y+4,head:[[PT.date,PT.time,PT.hours,PT.note]],body:fe.map(x=>[fmtPdfDate(x.date),(x.from||'—')+' – '+(x.to||'—'),fmtHrs(x.hours),x.note||'']),foot:[[{content:PT.total,colSpan:2,styles:{halign:'left'}},{content:fmtHrs(totH),colSpan:2,styles:{halign:'right'}}]],showFoot:'lastPage',theme:'striped',styles:{font:fnt,fontSize:fs,cellPadding:pad2,overflow:'linebreak',valign:'middle',lineWidth:0},headStyles:{fillColor:[99,102,241],textColor:255,fontSize:fs,font:fnt,fontStyle:'bold',cellPadding:pad2+0.3},footStyles:{fillColor:[99,102,241],textColor:255,fontSize:fs+1.5,font:fnt,fontStyle:'bold',cellPadding:pad2+1.5},alternateRowStyles:{fillColor:[243,244,251]},columnStyles:{0:{cellWidth:34},1:{cellWidth:26,halign:'center'},2:{cellWidth:17,halign:'right',fontStyle:'bold'},3:{cellWidth:'auto',overflow:noteOv}},margin:{left:14,right:14,bottom:14}});const finalY=doc2.lastAutoTable.finalY,ph=doc2.internal.pageSize.height,sigY=Math.min(finalY+12,ph-10);if(sigY>finalY+4){doc2.setDrawColor(180);doc2.setLineWidth(.3);doc2.line(20,sigY,80,sigY);doc2.line(130,sigY,190,sigY);doc2.setTextColor(120);doc2.setFontSize(7);doc2.text(PT.signEmp,20,sigY+3.2);doc2.text(PT.signCo,130,sigY+3.2);}const pgs=doc2.internal.getNumberOfPages();for(let i=1;i<=pgs;i++){doc2.setPage(i);doc2.setFontSize(7);doc2.setTextColor(150);doc2.text('KRONOS · '+(S.name||'')+' · '+monthName,14,ph-5);doc2.text(PT.page+' '+i+'/'+pgs,196,ph-5,{align:'right'});}doc2.save(`kronos_${(S.name||'vykaz').replace(/\s+/g,'_')}_${m}.pdf`);document.getElementById('pdfLangModal').classList.remove('open');document.getElementById('pdfLoadingMsg').style.display='none';window._pdfMonth=null;}; document.addEventListener('visibilitychange',()=>{if(document.visibilityState==='visible'&&user){fetchRate();fetchRateHistory();render();startClockInterval();}});