<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>TimeTrack</title> <script> "use strict"; var lastlist=null; function fetch(method,url,data/*?*/,creds/*?*/,cb){ if(!creds){ cb=data; data=undefined; creds=undefined; } else if(!cb){ cb=creds; creds=undefined; } if(!cb)throw new Error("No callback passed to fetch"); var xhr=new XMLHttpRequest(); xhr.onreadystatechange=function(ev){ if(xhr.readyState<4)return; cb(xhr.status,xhr.responseText); }; if(creds){ xhr.open(method,url,true,creds[0],creds[1]); } else { xhr.open(method,url); } xhr.send(data); } function pad(s,n,c){ if(c==null)c=" "; else c=c[0]; s=s+""; while(s.length<n)s=c+s; return s; } function toinputdate(date) { return date.getFullYear() + "-" + pad(date.getMonth()+1,2,"0") + "-" + pad(date.getDate(),2,"0") + " " + pad(date.getHours(),2,"0") + ":" + pad(date.getMinutes(),2,"0") + ":" + pad(date.getSeconds(),2,"0"); } var monthnames=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]; var weekdays=["Mon","Tue","Wed","Thu","Fri","Sat","Sun"]; function formatdate(date) { return weekdays[(date.getDay() + 6) % 7] + ", " + toinputdate(date); } function tablerowfor(ev){ var div=document.createElement("div"); div.classList.add("event"); var e,e2; e=document.createElement("span"); e.classList.add("eventtype"); e.classList.add("eventtype"+ev.type); if (ev.type == "in") e.innerHTML = "→in" else if (ev.type == "out") e.innerHTML = "←out" div.appendChild(e); e=document.createElement("span"); e.classList.add("eventsheet"); e.appendChild(document.createTextNode(ev.sheet)); div.appendChild(e); if (ev.text != "") { e=document.createElement("span"); e.classList.add("eventtext"); e.appendChild(document.createTextNode("(" + ev.text + ")")); div.appendChild(e); } var float=document.createElement("div"); float.classList.add("eventfloat"); e=document.createElement("span"); e.classList.add("eventdate"); e.appendChild(document.createTextNode(formatdate(ev.date))); e.setAttribute("title",ev.date.toString()); float.appendChild(e); e=document.createElement("div"); e.classList.add("eventbuttons"); e2=document.createElement("span"); e2.classList.add("eventdelete"); e2.appendChild(document.createTextNode("X")); e2.addEventListener("click",function(){ if(!confirm("Really delete check-"+ev.type+" event \""+ev.sheet+"\" (\""+ev.text+"\") at "+formatdate(ev.date)+"?"))return; fetch("DELETE","/timetrack/event",ev.id,function(status,body){ if(status==200)getlist(false); else alert("Delete failed: "+body); }); }); e.appendChild(e2); float.appendChild(e); div.appendChild(float); return div; } function refreshlist(list){ var listelem=document.getElementById("eventlist"); listelem.innerHTML=""; var rows=[]; var i; for(i=0;i<list.length;i++){ rows.push([list[i].date,tablerowfor(list[i])]); } if(rows.length==0){ var div=document.createElement("div"); div.classList.add("noevents"); div.appendChild(document.createTextNode("No events yet")); listelem.appendChild(div); return; } for(i=0;i<rows.length;i++){ listelem.appendChild(rows[i][1]); } } function daystart(date){ return new Date(date.getFullYear(), date.getMonth(), date.getDate()); } function weekstart(date){ var d=daystart(date); var wkday=(d.getDay()+6)%7; return new Date(d - wkday*24*3600*1000); } function refreshcalendar(list,npreweeks){ if(npreweeks==undefined)npreweeks=2; var dayTarget=38/5 - 1; var weekTotals=new Map(); // only in weeks that have entries var totalTime=0; function datekey(d){return d.getFullYear()+"-"+d.getMonth()+"-"+d.getDate();} var hist=new Map(), incheck=new Map(); var errors=[]; for(var i=0;i<list.length;i++){ var key=datekey(list[i].date); var wkey=datekey(weekstart(list[i].date)); if(list[i].type=="in"){ if(incheck.has(key)){ errors.push("Double check-in at "+formatdate(list[i].date)); continue; } incheck.set(key,list[i]); } else if(list[i].type=="out"){ if(!incheck.has(key)){ errors.push("Loose check-out at "+formatdate(list[i].date)); continue; } var thistime=list[i].date-incheck.get(key).date; incheck.delete(key); var yet=hist.has(key)?hist.get(key):0; hist.set(key,yet+thistime); var weekyet=weekTotals.has(wkey)?weekTotals.get(wkey):0; weekTotals.set(wkey,weekyet+thistime); totalTime+=thistime; } else { errors.push("Unknown type '"+list[i].type+"'"); } } var totalTarget=5*weekTotals.size*dayTarget; var totalSurplus=totalTime-totalTarget*3600*1000; var tb=document.getElementById("calendartb"); tb.innerHTML=""; var tr,td; for(var i=0;i<errors.length;i++){ tr=document.createElement("tr"); td=document.createElement("td"); td.appendChild(document.createTextNode(errors[i])); tr.appendChild(td); tb.appendChild(tr); } var tr=document.createElement("tr"); tr.appendChild(document.createElement("td")); var td; for(var i=0;i<7;i++){ td=document.createElement("td"); td.appendChild(document.createTextNode(weekdays[i])); tr.appendChild(td); } td=document.createElement("td"); var a=document.createElement("a"); a.href="javascript:refreshcalendar(lastlist,"+(npreweeks+4)+")"; a.appendChild(document.createTextNode("\u2191")); td.appendChild(a); tr.appendChild(td); tb.appendChild(tr); var now=new Date(); var today=daystart(now), thismonday=weekstart(now); for(var wkoff=-npreweeks;wkoff<=1;wkoff++){ var monday=new Date(thismonday.getTime() + wkoff*7*24*3600*1000); tr=document.createElement("tr"); td=document.createElement("td"); var label=monday.getDate().toString(); if(monday.getDate() <= 7 || wkoff == -npreweeks){ label = monthnames[monday.getMonth()].slice(0,3) + " " + label; } td.appendChild(document.createTextNode(label)); tr.appendChild(td); for(var i=0;i<7;i++){ var day=new Date(thismonday.getTime() + (wkoff*7+i)*24*3600*1000); var tm=hist.get(datekey(day))||0; var descr=Math.round(tm/1000/3600*10)/10+"h"; var target=(day.getDay()+6)%7 < 5 ? 38/5 - 1 : 0; var diff=tm - target*3600*1000; var surplus=1-Math.exp(-(diff>0?diff:0)/(3*3600*1000)); var deficit=1-Math.exp(-(diff<0?-diff:0)/(3*3600*1000)); td=document.createElement("td"); td.appendChild(document.createTextNode(descr)); td.setAttribute("style","background-color:rgb("+(255-255*surplus)+","+(255-255*(deficit+surplus))+","+(255-255*deficit)+")"); if(day.getTime()==today.getTime())td.classList.add("today"); tr.appendChild(td); } if(wkoff==-1){ // "prev" refers to "all weeks before this week" var thisWeekTotal=weekTotals.get(datekey(thismonday))||0; var prevTotal=totalTime-thisWeekTotal; var numPrevWeeks=weekTotals.size-(thisWeekTotal>0); var prevTarget=numPrevWeeks*5*dayTarget; var prevSurplus=prevTotal-prevTarget*3600*1000; var descr=Math.round(prevSurplus/1000/3600*10)/10+"h"; td=document.createElement("td"); var span=document.createElement("span"); span.setAttribute("style","display:inline-block;width: max-content;"); span.appendChild(document.createTextNode("Account: " + descr)); td.appendChild(span); tr.appendChild(td); } tb.appendChild(tr); } } function handleReceivedList(list){ lastlist=list; refreshlist(list); refreshcalendar(list); } function getlist(isinitial){ fetch("GET","/timetrack/list",function(status,body){ if(status!=200){ alert("Error: "+body); return; } var list; try { list=JSON.parse(body); } catch(e){ alert("An error occurred!"); return; } for(var i=0;i<list.length;i++)list[i].date=new Date(list[i].date); list.sort(function(a,b){return a.date-b.date;}); handleReceivedList(list); if (isinitial) document.getElementById("addeventform").scrollIntoView(); }); } function getsheets() { fetch("GET","/timetrack/sheets",function(status,body){ if(status!=200){ // alert("Error: "+body); return; } var sheets; try { sheets=JSON.parse(body); } catch(e){ // alert("An error occurred!"); return; } var el=document.getElementById("sheetselect"); el.innerHTML = "<option value=\"\" selected></option>"; for (var i = 0; i < sheets.length; i++) { var opt = document.createElement("option"); opt.value = sheets[i]; opt.innerHTML = sheets[i]; el.appendChild(opt); } }); } function doAddEvent(ev, type){ var sheet=document.getElementById("addeventsheet").value; var text=document.getElementById("addeventtext").value; var date=document.getElementById("addeventdate").value; if(typeof date=="string")date=new Date(date); fetch("POST","/timetrack/event",JSON.stringify({ sheet:sheet, text:text, date:date, type:type }),function(status,body){ if(status==200){ location.href=location.href; return; } alert("Error while adding: "+body); }); } function setSheetFromSelect() { document.getElementById("addeventsheet").value = document.getElementById("sheetselect").value; } function logoutReload(){ fetch("GET","/timetrack/authfail",undefined,["x","x"],function(status,body){ location.href=location.href; }); } function dateToNow() { document.getElementById("addeventdate").value = toinputdate(new Date()); } window.addEventListener("load",function() { getlist(true); getsheets(); dateToNow(); }); </script> <style> body{ font-family:Georgia,Times,serif; font-size:14px; } .event{ border:1px #ddd solid; border-bottom-width:0px; padding:0px 9px; background-color:#f8f8f8; width:500px; } .event:last-child{ border-bottom-width:1px; } .eventtype{ font-size:20px; width:50px; display:inline-block; } .eventtypein{ color:blue; } .eventtypeout{ color:red; } .eventsheet{ font-size:20px; margin-left:10px; } .eventtext{ font-size:15px; margin-left:10px; width:150px; display:inline-block; } .eventfloat{ float:right; text-align:right; } .eventdate{ font-size:12px; font-style:italic; display:inline-block; margin-top:6px; } .eventbuttons{ display:inline-block; margin-left:60px; font-size:10px; vertical-align:middle; width:10px; text-align:center; } .eventdelete{ margin-bottom:5px; font-size:10px; font-family:sans-serif; color:red; cursor:pointer; } #addeventform{ border:1px #ddd solid; display:inline-block; padding:5px; } #addeventform > input[type="text"] { margin-bottom: 5px; } #logoutwrapper{ float:right; } #calendartb td{ width:35px; } #calendartb td:first-child{ text-align:right; width:50px; } #calendartb td.today{ border:1px black solid; font-weight:bold; } </style> </head> <body> <div id="logoutwrapper"> <input type="button" onclick="logoutReload();" value="Logout"> </div> <h1>TimeTrack</h1> <div id="eventlist"></div> <br><br> <div id="addeventform"> Sheet: <input type="text" id="addeventsheet" placeholder="Sheet"> <select id="sheetselect" onchange="setSheetFromSelect()"></select> <br> Text: <input type="text" id="addeventtext" placeholder="Text"> (optional) <br> Date: <input type="datetime" id="addeventdate" placeholder="YYYY-MM-DD HH:MM:SS" size="25"> <input type="button" onclick="dateToNow()" value="now"> <br> <input type="button" onclick="doAddEvent(event, 'in')" value="Add IN" style="margin-top: 5px"> <input type="button" onclick="doAddEvent(event, 'out')" value="Add OUT" style="margin-top: 5px; margin-left: 20px"> </div> <br><br> <div id="calendar"> <table><tbody id="calendartb"></tbody></table> </div> </body> </html>