summaryrefslogtreecommitdiff
path: root/modules/timetrack2/timetrack.html
diff options
context:
space:
mode:
Diffstat (limited to 'modules/timetrack2/timetrack.html')
-rw-r--r--modules/timetrack2/timetrack.html510
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>