<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>TimeTrack</title> <script> "use strict"; var ROOT_ENDPOINT="/timetrack2"; 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"); } function daystart(date){ return new Date(date.getFullYear(), date.getMonth(), date.getDate()); } function shiftdays(date, off) { var d = new Date(date); d.setDate(d.getDate() + off); return d; } function weekstart(date){ return shiftdays(daystart(date), -(date.getDay() + 6) % 7); } 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 formatdaterange(d1, d2) { var s1 = formatdate(d1); if (daystart(d1).getTime() == daystart(d2).getTime()) { return formatdate(d1) + " - " + pad(d2.getHours(), 2, "0") + ":" + pad(d2.getMinutes(), 2, "0") + ":" + pad(d2.getSeconds(), 2, "0") } else { return formatdate(d1) + " - " + formatdate(d2); } } function tablerowfor(ev){ var div=document.createElement("div"); div.classList.add("event"); var e,e2; 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(formatdaterange(ev.indate,ev.outdate))); e.setAttribute("title",ev.indate.toString() + " - " + ev.outdate.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 event \""+ev.sheet+"\" (\""+ev.text+"\") at "+formatdate(ev.indate)+"?"))return; fetch("DELETE",ROOT_ENDPOINT+"/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 = ""; if (list.length == 0) { var div = document.createElement("div"); div.classList.add("noevents"); div.appendChild(document.createTextNode("No events yet")); listelem.appendChild(div); return; } var maxshown = 40; if (list.length > maxshown) { var div = document.createElement("div"); div.classList.add("moreevents"); div.appendChild(document.createTextNode("... more events ...")); listelem.appendChild(div); } for (var i = Math.max(0, list.length - maxshown); i < list.length; i++) { listelem.appendChild(tablerowfor(list[i])); } } function refreshcalendar(list,npreweeks){ if (npreweeks == undefined) npreweeks = 2; var dayTargetH = 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(); for (var i = 0; i < list.length; i++) { var key = datekey(list[i].indate); var wkey = datekey(weekstart(list[i].indate)); var thistime = list[i].outdate - list[i].indate; 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; } var totalTargetH = 5 * weekTotals.size * dayTargetH; var totalSurplus = totalTime - totalTargetH * 3600 * 1000; var tb = document.getElementById("calendartb"); tb.innerHTML = ""; 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 = shiftdays(thismonday, wkoff * 7); 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 = shiftdays(thismonday, wkoff * 7 + i); 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 * dayTargetH; 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 updateCurrentBox(current) { var sheet = current ? current.sheet : ""; var text = current ? current.text : ""; var indate = current ? toinputdate(current.indate) : ""; var e = document.getElementById("currentsheet"); e.innerHTML = ""; e.appendChild(document.createTextNode(sheet)); e = document.getElementById("currenttext"); e.innerHTML = ""; e.appendChild(document.createTextNode(text)); e = document.getElementById("currentindate"); e.innerHTML = ""; e.appendChild(document.createTextNode(indate)); var box = document.getElementById("currentbox"); var inputs = box.getElementsByTagName("input"); if (current == null) { for (var i = 0; i < inputs.length; i++) inputs[i].setAttribute("disabled", ""); box.classList.add("disabled"); box.classList.remove("enabled"); } else { for (var i = 0; i < inputs.length; i++) inputs[i].removeAttribute("disabled"); box.classList.add("enabled"); box.classList.remove("disabled"); } } function getlist(isinitial){ fetch("GET", ROOT_ENDPOINT + "/list", function(status, body) { if (status != 200) { alert("Error: "+body); return; } var obj; try { obj = JSON.parse(body); } catch(e){ alert("An error occurred!"); return; } var list = obj.list, current = obj.current; for (var i = 0; i < list.length; i++) { list[i].indate = new Date(list[i].indate); list[i].outdate = new Date(list[i].outdate); } if (current != null) current.indate = new Date(current.indate); handleReceivedList(list); updateCurrentBox(current); if (isinitial) document.getElementById("checkinbox").scrollIntoView(); dateToNow("checkindate"); dateToNow("checkoutdate"); }); } function getsheets() { fetch("GET",ROOT_ENDPOINT+"/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 doCheckin(atdate) { if (atdate) atdate = new Date(document.getElementById("checkindate").value); else atdate = new Date(); var sheet=document.getElementById("checkinsheet").value; var text=document.getElementById("checkintext").value; fetch("POST", ROOT_ENDPOINT + "/checkin", JSON.stringify({ sheet: sheet, text: text, date: atdate }), function(status, body) { if (status != 200) { alert("Error performing check-in: " + body); return; } getlist(false); }); } function doCheckout(atdate) { if (atdate) atdate = new Date(document.getElementById("checkoutdate").value); else atdate = new Date(); fetch("POST", ROOT_ENDPOINT + "/checkout", JSON.stringify({ date: atdate }), function(status, body) { if (status != 200) { alert("Error performing check-out: " + body); return; } getlist(false); }); } function setSheetFromSelect() { document.getElementById("checkinsheet").value = document.getElementById("sheetselect").value; } function logoutReload(){ fetch("GET", ROOT_ENDPOINT + "/authfail", undefined, ["x", "x"], function(status, body) { location.href = location.href; }); } function dateToNow(textboxid) { document.getElementById(textboxid).value = toinputdate(new Date()); } window.addEventListener("load", function() { getlist(true); getsheets(); dateToNow("checkindate"); dateToNow("checkoutdate"); }); </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; } .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; } #currentbox.enabled{ background-color: #dfd; } #currentbox.disabled{ background-color: #eee; } #currentsheet{ font-weight: bold; } #checkinbox, #currentbox{ border:1px #ddd solid; display:inline-block; padding:5px; } #checkinbox > input[type="text"], #currentbox > 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="currentbox"> Checked into: <span id="currentsheet"></span> (<span id="currenttext"></span>)<br> Check-in at: <span id="currentindate"></span><br><br> <input type="button" onclick="doCheckout(false)" value="Check out now"><br> Or: <input type="datetime" id="checkoutdate" placeholder="YYYY-MM-DD HH:MM:SS" size="25"> <input type="button" onclick="dateToNow('checkoutdate')" value="now"><br> <input type="button" onclick="doCheckout(true)" value="Check out at date"> </div> <br><br> <div id="checkinbox"> Sheet: <input type="text" id="checkinsheet" placeholder="Sheet"> <select id="sheetselect" onchange="setSheetFromSelect()"></select> <br> Text: <input type="text" id="checkintext" placeholder="Text"> (optional) <br> <input type="button" onclick="doCheckin(false)" value="Check in now"><br> Or: <input type="datetime" id="checkindate" placeholder="YYYY-MM-DD HH:MM:SS" size="25"> <input type="button" onclick="dateToNow('checkindate')" value="now"><br> <input type="button" onclick="doCheckin(true)" value="Check in at date"> </div> <br><br> <div id="calendar"> <table><tbody id="calendartb"></tbody></table> </div> </body> </html>