diff options
author | Tom Smeding <tom@tomsmeding.com> | 2022-03-09 00:08:00 +0100 |
---|---|---|
committer | Tom Smeding <tom@tomsmeding.com> | 2022-03-09 00:08:00 +0100 |
commit | 78eedbb2aba66695cacac8d832a44512379e4a22 (patch) | |
tree | 6492fdd56ab6fe5b9cb953c349654518fef641cb /modules/timetrack2/timetrack.html | |
parent | 1e082de1d574ab0cf3404e63d65798210590e5f3 (diff) |
Timetrack 2
Diffstat (limited to 'modules/timetrack2/timetrack.html')
-rw-r--r-- | modules/timetrack2/timetrack.html | 510 |
1 files changed, 510 insertions, 0 deletions
diff --git a/modules/timetrack2/timetrack.html b/modules/timetrack2/timetrack.html new file mode 100644 index 0000000..209458d --- /dev/null +++ b/modules/timetrack2/timetrack.html @@ -0,0 +1,510 @@ +<!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 weekstart(date){ + var d=daystart(date); + var wkday=(d.getDay()+6)%7; + return new Date(d - wkday*24*3600*1000); +} + +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 = 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 * 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> |